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    // 获得优先级,优先级小的优化器会先被调用,优先级相同的优化器会按照注册的顺序被调用
243    fn get_priority(&self) -> u32;
244    // 声明本优化器消费的关键词产物来源。候选管道按 KeywordInputSource 分层执行,
245    // 优化器只读取其声明来源的关键词,输出追加到最终关键词池。
246    // 默认 Refined:对归一化小写基与后续派生词的精化产物全集做变换(安全、幂等)。
247    fn input_source(&self) -> KeywordInputSource {
248        KeywordInputSource::Refined
249    }
250}
251
252/// 关键词优化器输入来源 —— 候选关键词管道中的产物引用。
253///
254/// 候选管道按此枚举确定每个优化器的输入:产物有序累积且不回流到已执行层,
255/// 杜绝缩写器反复作用于派生词的词根污染(如 `QQ yin le`→`QQ`→`qq`)。
256/// 本枚举替代旧 `uses_context: bool`("跑整个累积池 vs 只跑原始名"的粗糙二分),
257/// 将输入来源显式建模为受控 DAG(DAG-lite):系统内建产物 + 对其他优化器
258/// 输出的命名引用,第三方优化器可声明消费任意已注册优化器的产物。
259/// 跨 IPC 序列化(keyword_optimizer_info RPC / 设置 schema),键名 camelCase。
260#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
261pub enum KeywordInputSource {
262    /// 原始展示名(大小写保留,未折叠空格)。
263    /// 仅供依赖原始大小写的缩写器消费(如驼峰缩写);原始名不进入最终关键词池。
264    #[serde(rename = "originalName")]
265    OriginalName,
266    /// 归一化小写基:去版本号 + 小写 + 折叠连续空格。
267    /// 候选管道的公共起点,始终在管道内建生成并进最终关键词池
268    /// (对齐 legacy original_lower 语义;含版本完整名另作增强关键词进池)。
269    #[serde(rename = "normalizedBase")]
270    NormalizedBase,
271    /// 精化产物全集:归一化基与所有已产出关键词的累积池。
272    /// 供幂等精化器(版本移除/空格处理/符号移除)与通用转换器消费;默认来源。
273    #[serde(rename = "refined")]
274    #[default]
275    Refined,
276    /// 指定生产者优化器的输出:本优化器声明消费另一已注册优化器的产物。
277    /// 例如首字母提取器依赖拼音转换器输出(`qq yin le`→`qyl`)。
278    /// 执行约束:`producer_id` 对应优化器必须已注册且 priority 小于本优化器
279    /// (保证按 priority 升序执行时生产者先运行)。违反该约束(producer 未注册/
280    /// 未产出/priority 逆序)时消费者静默空输入并记 warn 日志,依赖配置方保证。
281    #[serde(rename = "optimizerOutput")]
282    OptimizerOutput {
283        /// 被引用产物所属优化器的 component_id。
284        #[serde(rename = "producerId")]
285        producer_id: String,
286    },
287}
288
289#[async_trait]
290pub trait KeywordInjector: Configurable {
291    /// 根据候选项的完整上下文注入额外关键字。
292    /// 与 KeywordOptimizer 不同,此方法可以访问候选项的 target、icon 等完整信息,
293    /// 用于实现"基于候选身份的关键字注入"(如别名)。
294    async fn inject_keywords(&self, candidate: &SearchCandidate) -> Vec<String>;
295}
296
297// 表示一个搜索引擎,用于计算搜索候选项的分数
298// 用于根据搜索候选项的分数进行排序
299// 搜索引擎通常计算的是一个候选项与用户输入之间的关系
300#[async_trait]
301pub trait SearchEngine: Configurable {
302    async fn calculate_scores(
303        &self,
304        candidates: &CachedCandidateData,
305        query: &str,
306    ) -> Vec<ScoredCandidate>;
307}
308
309// 表示一个分数优化器,用于对搜索候选项的分数进行优化
310// 用于根据搜索候选项的分数进行排序
311// 分数优化器则是计算的是 *所有* 候选项与用户输入之间的关系
312#[async_trait]
313pub trait ScoreBooster: Configurable {
314    // 记录用户输入了这个查询时,选择的是这个候选项
315    async fn record(&self, candidate_id: CandidateId, data: &CachedCandidateData, query: &str);
316    // 根据用户历史输入的查询与选择的候选项,优化当前查询所得到的所有候选项的分数
317    async fn boost(
318        &self,
319        candidates: &mut Vec<ScoredCandidate>,
320        data: &CachedCandidateData,
321        query: &str,
322    );
323}
324
325/// 动作执行器 trait
326/// 每个 Executor 可以声明支持多种 TargetType 和多种 Action
327/// Executor 继承 Configurable,以支持统一配置管理和发现
328#[async_trait]
329pub trait ActionExecutor: Configurable {
330    /// 返回该 Executor 支持的目标类型集合
331    fn supported_target_types(&self) -> Vec<TargetType>;
332
333    /// 返回该 Executor 支持的动作列表
334    fn supported_actions(&self) -> Vec<ResultAction> {
335        vec![ResultAction {
336            id: "execute".to_string(),
337            label: "执行".to_string(),
338            icon: IconRequest::Path(String::new()),
339            is_default: true,
340            shortcut_key: String::new(),
341        }]
342    }
343
344    /// 根据动作 ID 执行对应的操作
345    /// 参数:ctx - 执行上下文;action_id - 动作 ID
346    /// 返回:执行成功返回 Ok(()),失败返回 ExecutionError
347    async fn execute(&self, ctx: &ExecutionContext, action_id: &str) -> Result<(), ExecutionError>;
348}
349
350/// 查询来源通道 —— 标识查询进入后端的入口。
351///
352/// 仅 GUI 通道参与会话写入与查询版本域:会话写入由入口结构保证
353/// (宿主的 GUI 查询入口是唯一的会话写点),版本域(`QueryRevisionGate`)
354/// 同样只覆盖 GUI 查询与热键唤醒;CLI 与面板通道为只读辅助路径 ——
355/// 不改写会话、不分配版本号、结果不作废。
356///
357/// 序列化键名显式标注(serde-rename 契约):本枚举经 PluginContext 跨 RPC
358/// 传输到远端插件进程,键名更改会使旧 SDK 二进制反序列化失败。
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
360pub enum QueryChannel {
361    /// 主窗口 GUI 查询(bridge_query,用户搜索栏输入)—— 唯一允许改写会话的通道。
362    #[default]
363    #[serde(rename = "ui")]
364    Ui,
365    /// 本地 CLI HTTP 查询(/v1/query,外部进程只读辅助路径)。
366    #[serde(rename = "cli")]
367    Cli,
368    /// 沉浸式/行内插件面板内查询(bridge_query 显式指定插件)——
369    /// GUI 进程内只读辅助路径:不改写会话、不参与用户输入版本竞争。
370    #[serde(rename = "panel")]
371    Panel,
372}
373
374/// 查询版本门控:宿主在会写会话的查询入口(GUI 查询、热键唤醒)分配
375/// 单调递增版本号,供插件在写入跨查询共享状态(如翻译结果缓存)前判断
376/// 自身查询是否已被更新的查询取代。
377///
378/// 只读入口(CLI/面板查询)不分配版本号,其结果不作废。
379/// 仅宿主进程内使用(内置插件经 PluginContext 注入);不跨 RPC 传输,
380/// 远端插件或直接构造的上下文无门控,此时视为始终为最新。
381#[derive(Debug, Clone)]
382pub struct QueryRevisionGate {
383    /// 当前查询被分配的版本号。
384    revision: u64,
385    /// 宿主侧「最新已分配版本」计数器(每次查询入口 fetch_add)。
386    latest: Arc<AtomicU64>,
387}
388
389impl QueryRevisionGate {
390    /// 创建门控:revision 为当前查询的版本号,latest 为共享的最新版本计数器。
391    pub fn new(revision: u64, latest: Arc<AtomicU64>) -> Self {
392        Self { revision, latest }
393    }
394
395    /// 当前查询的版本号,供日志与追踪使用。
396    pub fn revision(&self) -> u64 {
397        self.revision
398    }
399
400    /// 当前查询是否仍是最新:成立才允许写入跨查询共享状态。
401    pub fn is_current(&self) -> bool {
402        self.latest.load(Ordering::Relaxed) == self.revision
403    }
404}
405
406/// 请求级上下文,在宿主与插件之间共享。
407/// 服务于插件生命周期/查询/动作调用,并携带日志关联 ID。
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct PluginContext {
410    // 当前的请求 ID
411    pub trace_id: String,
412    // 当前的请求 ID
413    pub query_id: Option<String>,
414    // 处理当前请求的插件 ID
415    pub plugin_id: Option<String>,
416    /// 查询版本门控(宿主注入;#[serde(skip)] 不跨 RPC 传输,
417    /// 远端插件或直接构造的上下文为 None,此时视为始终为最新)。
418    #[serde(skip)]
419    pub query_revision_gate: Option<QueryRevisionGate>,
420    /// 查询来源通道(宿主注入;远端插件经 RPC 反序列化时缺省视为 GUI 通道)。
421    #[serde(default)]
422    pub query_channel: QueryChannel,
423    /// 宿主当前界面语言(如 "zh-Hans"),供插件生成本地化文本;
424    /// 远端插件反序列化缺省为空串,可经 host/i18n.get_locale 主动查询。
425    #[serde(default)]
426    pub locale: String,
427}
428
429impl PluginContext {
430    pub fn new(trace_id: &str) -> Self {
431        Self {
432            trace_id: trace_id.to_string(),
433            query_id: None,
434            plugin_id: None,
435            query_revision_gate: None,
436            query_channel: QueryChannel::Ui,
437            locale: String::new(),
438        }
439    }
440
441    pub fn with_query(&mut self, query_id: String) {
442        self.query_id = Some(query_id);
443    }
444
445    pub fn with_plugin_id(&mut self, plugin_id: String) {
446        self.plugin_id = Some(plugin_id);
447    }
448
449    /// 注入查询版本门控(宿主查询入口调用)。
450    pub fn set_query_revision_gate(&mut self, gate: QueryRevisionGate) {
451        self.query_revision_gate = Some(gate);
452    }
453
454    /// 当前查询是否仍是最新;无门控(远端插件、测试、mock)恒为 true。
455    pub fn is_query_current(&self) -> bool {
456        self.query_revision_gate
457            .as_ref()
458            .is_none_or(|g| g.is_current())
459    }
460
461    /// 当前查询的版本号;无门控时为 0,仅供日志使用。
462    pub fn query_revision(&self) -> u64 {
463        self.query_revision_gate
464            .as_ref()
465            .map_or(0, |g| g.revision())
466    }
467}
468
469/// 发送给插件查询处理器的标准化查询载荷。
470/// 服务于查询分发和插件侧搜索逻辑。
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct Query {
473    /// 本次查询的唯一标识,取自 bridge_query 中生成的 trace_id,用于日志关联和插件上下文。
474    pub id: String,
475    /// 用户在搜索栏中输入的原始字符串,未经任何处理,用于触发器匹配和日志记录。
476    pub raw_query: String,
477    /// 派生自 raw_query 的搜索词。普通搜索为全小写形式,插件模式为剥离触发关键词后的剩余部分。
478    pub search_term: String,
479    /// 是否由用户显式确认(如按 Enter)触发的查询。
480    /// 行内插件手动模式(PanelQueryTrigger::OnEnter)用它区分确认查询与预览查询;默认 false。
481    #[serde(rename = "confirm", default)]
482    pub confirm: bool,
483}
484
485/// 插件面板查询触发方式的通用语义。
486/// 服务于宿主判断行内插件模式下输入后是否自动发起查询。
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
488pub enum PanelQueryTrigger {
489    /// 输入后自动触发查询(默认;配合 query_debounce_ms 防抖)。
490    #[default]
491    #[serde(rename = "onInput")]
492    OnInput,
493    /// 输入不自动触发查询,由用户按 Enter 手动触发。
494    #[serde(rename = "onEnter")]
495    OnEnter,
496}
497
498/// 插件面板按键绑定 —— 声明式按键契约的最小单元。
499/// 服务于宿主解释执行:插件声明按键 → 宿主翻译为动作语义。
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct PanelKeyBinding {
502    /// 按键格式:"Enter" | "Ctrl+Enter" | "Escape" | "Tab" | "a"。
503    #[serde(rename = "key")]
504    pub key: String,
505    /// 按键触发的动作。
506    #[serde(rename = "action")]
507    pub action: PanelKeyAction,
508}
509
510/// 面板按键动作 —— 宿主解释执行的动作语义。
511/// 服务于插件面板的完整按键权声明(键盘状态机契约)。
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513#[serde(tag = "kind")]
514pub enum PanelKeyAction {
515    /// 确认当前面板状态(Enter 标准语义,宿主 confirmQuery 三分支):
516    /// 面板有可执行动作时执行默认动作(如复制结果),否则发起确认查询(翻译/计算/失败重试等)。
517    #[serde(rename = "confirm")]
518    Confirm,
519    /// 执行面板动作:None = 执行面板默认动作(不指定);Some(id) = 执行指定动作。
520    #[serde(rename = "executeAction")]
521    ExecuteAction {
522        /// 动作 ID:None = 执行面板默认动作;Some = 执行指定动作。
523        #[serde(rename = "actionId")]
524        action_id: Option<String>,
525    },
526    /// 返回默认面板。
527    #[serde(rename = "goBack")]
528    GoBack,
529    /// 跳转到同一插件内的子面板。
530    #[serde(rename = "gotoPanel")]
531    GotoPanel {
532        #[serde(rename = "panelId")]
533        panel_id: String,
534    },
535    /// 触发插件自定义动作(经面板动作通道回插件,action 即插件动作 ID)。
536    #[serde(rename = "custom")]
537    Custom {
538        #[serde(rename = "action")]
539        action: String,
540        #[serde(rename = "args")]
541        args: serde_json::Value,
542    },
543}
544
545/// 插件面板响应携带的通用交互策略。
546/// 服务于宿主处理输入查询触发时机,不属于插件持久化配置。
547///
548/// 形态语义(行内/沉浸式均为同一份策略,宿主无条件推送):
549/// - `bindings` 对行内与沉浸式**均生效**——沉浸式全屏面板的退出
550///   只能靠插件声明(宿主零兜底),裁剪会导致无法退出;
551/// - `query_trigger` / `query_debounce_ms` 仅行内形态有消费方
552///   (沉浸式隐藏搜索栏,无输入触发路径),闲置时无实害。
553#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
554pub struct PanelInteraction {
555    /// 查询触发方式:onInput 输入自动触发 / onEnter 由用户按 Enter 手动触发。
556    #[serde(rename = "queryTrigger", default)]
557    pub query_trigger: PanelQueryTrigger,
558    /// 后续输入触发查询前的防抖延迟,单位为毫秒(仅 onInput 模式生效)。
559    #[serde(rename = "queryDebounceMs", default)]
560    pub query_debounce_ms: u64,
561    /// 面板按键绑定列表 —— 声明式按键契约(声明即接管:命中绑定由宿主解释执行,
562    /// 未声明的键一律交还浏览器/输入框,宿主不做兜底)。
563    /// 反序列化缺省为空列表(旧插件未声明时按键全部放行)。
564    #[serde(rename = "bindings", default)]
565    pub bindings: Vec<PanelKeyBinding>,
566}
567
568/// 插件查询响应 —— 一次查询的展示结果契约(跨 IPC 序列化,字段键名与前端
569/// `BridgeQueryResponse.mode` 词表对齐)。
570///
571/// 由 `Plugin::query` / 宿主流程返回,经 SessionDispatcher 路由后包装为
572/// `BridgeQueryResponse` 下发前端;四种变体对应前端不同的展示形态。
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub enum QueryResponse {
575    /// 候选列表结果 —— 默认搜索与插件均可返回,前端按列表渲染。
576    ///
577    /// 空列表即空结果(前端映射 mode "search" + 空数组,展示形态层面
578    /// 不区分 List/Empty)。
579    #[serde(rename = "list")]
580    List {
581        /// 排序后的候选项列表(含动作、占位符统计、触发关键词等展示元数据)。
582        #[serde(rename = "results")]
583        results: Vec<ListItem>,
584    },
585    /// 插件自定义面板 —— 触发式插件接管会话时的渲染结果。
586    ///
587    /// `keep_search_bar` 决定面板形态:true 为行内面板(保留搜索栏,前端
588    /// mode "plugin_panel"),false 为全页面接管(mode "plugin_immersive")。
589    #[serde(rename = "customPanel")]
590    CustomPanel {
591        /// 面板类型标识,前端按此选择面板组件渲染。
592        #[serde(rename = "panelType")]
593        panel_type: String,
594        /// 面板数据(自由 JSON,面板自行定义结构)。
595        #[serde(rename = "data")]
596        data: serde_json::Value,
597        /// 面板动作列表(供 Enter 执行默认动作 / 面板内动作切换)。
598        #[serde(rename = "actions")]
599        actions: Vec<ResultAction>,
600        /// 是否保留搜索栏(true = 行内面板;false = 全页面接管)。
601        #[serde(rename = "keepSearchBar")]
602        keep_search_bar: bool,
603    },
604    /// 空结果 —— 无任何展示内容。
605    ///
606    /// 前端映射 mode "search" + 空数组(与 `List` 空列表行为一致)。
607    /// 注意:插件命中触发词后即使返回 Empty 也是「已处理」,
608    /// 不得继续 fallback 到默认搜索。
609    #[serde(rename = "empty")]
610    Empty,
611    /// 行内参数模式:后端检测到触发关键词+空格后自动进入。
612    /// 前端据此清空搜索栏并展示参数输入 UI。
613    #[serde(rename = "inlineParam")]
614    InlineParam {
615        /// 目标候选项 ID(确认时回传执行)。
616        #[serde(rename = "candidateId")]
617        candidate_id: CandidateId,
618        /// 命中的触发关键词(前端展示 + 退出判定镜像使用)。
619        #[serde(rename = "triggerKeyword")]
620        trigger_keyword: String,
621        /// 该候选项要求的用户参数个数(前端据此校验输入完整性)。
622        #[serde(rename = "userArgCount")]
623        user_arg_count: usize,
624    },
625}
626
627/// 插件返回给宿主的搜索结果项。
628/// 服务于结果聚合、排序与 UI 渲染。
629#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct ListItem {
631    // 这个是候选项的唯一标识符
632    #[serde(rename = "id")]
633    pub id: CandidateId,
634    #[serde(rename = "title")]
635    pub title: String,
636    #[serde(rename = "subtitle")]
637    pub subtitle: String,
638    #[serde(rename = "icon")]
639    pub icon: IconRequest,
640    #[serde(rename = "score")]
641    pub score: f64,
642    // 一个动作列表中只可以有一个默认动作,默认动作会在用户直接按下回车时被触发(由程序员保证)
643    #[serde(rename = "actions")]
644    pub actions: Vec<ResultAction>,
645    /// 目标类型字符串,供前端 ResultItemProvider 匹配使用
646    #[serde(rename = "targetType")]
647    pub target_type: String,
648    /// 用户参数 {} 的数量
649    #[serde(rename = "userArgCount")]
650    pub user_arg_count: usize,
651    /// 是否包含系统参数({clip}, {hwnd}, {selection})
652    #[serde(rename = "hasSystemParams")]
653    pub has_system_params: bool,
654    /// 触发关键词列表
655    #[serde(rename = "triggerKeywords")]
656    pub trigger_keywords: Vec<String>,
657}
658
659/// 挂载在查询结果上的动作项。
660/// 服务于用户触发后的 Plugin::execute_action 执行流程。
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct ResultAction {
663    // 这个是动作的唯一标识符,通常是一个字符串,由插件定义
664    #[serde(rename = "id")]
665    pub id: String,
666    // 这个是动作的显示名称,用于展示在 UI 上
667    #[serde(rename = "label")]
668    pub label: String,
669    // 这个是该选项的图标,用于展示在 UI 上
670    #[serde(rename = "icon")]
671    pub icon: IconRequest,
672    // 是不是默认的动作,默认的动作会在用户直接按下回车时被触发
673    #[serde(rename = "isDefault")]
674    pub is_default: bool,
675    /// 快捷键提示,格式如 "Shift+Enter"、"Ctrl+Enter"
676    /// 前端根据此字段匹配修饰键到 action 的映射
677    #[serde(rename = "shortcutKey")]
678    pub shortcut_key: String,
679}
680
681/// 单个插件实例的静态元数据描述。
682/// 服务于注册中心索引、触发词路由与插件发现/展示。
683/// 第三方插件的取值由宿主从 `manifest.toml` 构造,内置插件由代码构造。
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct PluginMetadata {
686    #[serde(rename = "id")]
687    pub id: String,
688    #[serde(rename = "name")]
689    pub name: String,
690    #[serde(rename = "version")]
691    pub version: String,
692    #[serde(rename = "description")]
693    pub description: String,
694    #[serde(rename = "author")]
695    pub author: String,
696    /// 触发关键词列表 —— **语义随形态而变**:
697    /// - 行内形态(Inline):路由触发词。宿主把它随判定请求交给插件(`Plugin::match_query`
698    ///   的 `declared_trigger_keywords`,默认实现即据此判「触发词 + 空格」),并据此推导
699    ///   交给插件的查询词:命中关键词规则 → 取触发词之后的剩余(模型 keywords),
700    ///   否则取原始输入(模型 custom);
701    /// - 沉浸式形态(Panel):**候选搜索关键字**——宿主不将其写入触发词路由,
702    ///   而是注入到该插件的默认搜索候选项匹配关键字中(用户输入该词显示候选项)。
703    #[serde(rename = "triggerKeywords")]
704    pub trigger_keywords: Vec<String>,
705    /// 动态触发说明(可选)—— 无触发词、由插件自定义 `match_query` 判定时,
706    /// 说明满足什么形态的输入会被命中(如"输入形如网址时")。
707    /// 仅用于设置页展示,不参与路由判定。内置插件填 i18n key,第三方取自 manifest。
708    #[serde(rename = "triggerDescription", default)]
709    pub trigger_description: Option<String>,
710    #[serde(rename = "supportedOs")]
711    pub supported_os: Vec<String>,
712    #[serde(rename = "priority")]
713    pub priority: u32,
714    /// 插件种类(宿主管辖的运行属性):内置代码构造恒为 Builtin;
715    /// 第三方由 plugin-host 加载时强制覆盖为 ThirdParty(防插件谎报)。
716    #[serde(rename = "kind", default)]
717    pub kind: PluginKind,
718    /// 全局唤醒快捷键(如 "Ctrl+E"),可空。
719    #[serde(rename = "hotkey", default)]
720    pub hotkey: Option<String>,
721    /// 插件显示图标(data URL,如 "data:image/png;base64,..."),可空表示无图标。
722    /// 内置与第三方统一为数据,消费方直接透传。
723    #[serde(rename = "icon", default)]
724    pub icon: Option<String>,
725    /// 插件形态:inline = 行内插件(触发词路由,保留搜索栏);panel = 独立插件
726    /// (候选/热键唤出,唤醒后接管搜索窗口)。决定热键唤醒资格与图标展示门控。
727    /// 缺省 Panel:旧插件无此字段时按 panel 处理(兼容旧热键插件行为)。
728    #[serde(rename = "mode", default)]
729    pub mode: PluginMode,
730}
731
732/// 插件形态 —— 决定唤醒方式与展示形态。
733/// 与 PluginKind(内置/第三方)正交;序列化键名 "inline"/"panel"。
734///
735/// Inline = 触发词路由(trigger_keywords 生效);Panel = 触发词不参与路由
736/// (转为候选搜索关键字),仅经热键(hotkey)或候选项选中唤醒,且响应必须为
737/// `CustomPanel{keep_search_bar: false}`(全窗口接管)。
738#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
739pub enum PluginMode {
740    /// 行内插件:仅关键词唤醒,结果/面板嵌入搜索窗口(保留搜索栏)。
741    #[serde(rename = "inline")]
742    Inline,
743    /// 独立插件:触发词转为候选搜索关键字,仅经热键(hotkey)或候选项选中唤醒,
744    /// 响应必须为 `CustomPanel{keep_search_bar: false}`(全窗口接管)。
745    #[default]
746    #[serde(rename = "panel")]
747    Panel,
748}
749
750/// 插件种类 —— 区分内置(编译进二进制)与第三方(外部子进程)。
751///
752/// 跨 IPC 序列化(InstalledPluginInfo.kind / 前端行模型),
753/// 也用于 PluginManager 内部统一视图 PluginInfo.kind;
754/// 前端以 JSON 键名 "builtin"/"third-party" 作联合类型判断。
755#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
756pub enum PluginKind {
757    /// 编译进二进制、由 inventory 自动发现的内置插件。
758    #[serde(rename = "builtin")]
759    Builtin,
760    /// 外部子进程加载的第三方插件(子进程 + stdio JSON-RPC)。
761    #[default]
762    #[serde(rename = "third-party")]
763    ThirdParty,
764}
765
766/// 插件层统一错误类型。
767/// 服务于生命周期/查询/动作/设置相关错误在宿主与插件间传播。
768#[derive(Debug, thiserror::Error)]
769pub enum PluginError {
770    #[error("Plugin not found: {0}")]
771    NotFound(String),
772
773    #[error("Plugin initialization failed: {0}")]
774    InitFailed(String),
775
776    #[error("Query failed: {0}")]
777    QueryFailed(String),
778
779    #[error("Action execution failed: {0}")]
780    ActionFailed(String),
781
782    #[error("Invalid setting: {0}")]
783    InvalidSetting(String),
784}
785
786#[cfg(test)]
787mod tests {
788    use super::{
789        PanelInteraction, PanelKeyAction, PanelKeyBinding, PanelQueryTrigger, PluginContext,
790        QueryChannel, QueryRevisionGate,
791    };
792    use serde_json::json;
793    use std::sync::atomic::{AtomicU64, Ordering};
794    use std::sync::Arc;
795
796    #[test]
797    /// 验证门控:计数器未越过时当前查询仍有效,越过后失效。
798    fn query_revision_gate_tracks_latest() {
799        let latest = Arc::new(AtomicU64::new(2));
800        let gate = QueryRevisionGate::new(2, latest.clone());
801        assert!(gate.is_current(), "版本号与最新一致时应有效");
802        assert_eq!(gate.revision(), 2);
803
804        latest.fetch_add(1, Ordering::Relaxed);
805        assert!(!gate.is_current(), "更新的查询到达后应失效");
806    }
807
808    #[test]
809    /// 验证 PluginContext 门控默认行为与注入行为。
810    fn plugin_context_gate_defaults_to_current() {
811        let mut ctx = PluginContext::new("trace-1");
812        assert!(ctx.is_query_current(), "无门控时应恒为最新");
813        assert_eq!(ctx.query_revision(), 0);
814        assert_eq!(
815            ctx.query_channel,
816            QueryChannel::Ui,
817            "未显式注入时应缺省为 GUI 通道"
818        );
819
820        let latest = Arc::new(AtomicU64::new(1));
821        ctx.set_query_revision_gate(QueryRevisionGate::new(1, latest));
822        assert!(ctx.is_query_current());
823    }
824
825    #[test]
826    /// 验证门控与句柄字段不参与序列化(不跨 RPC 传输),反序列化后为 None。
827    fn plugin_context_skips_gate_in_serialization() {
828        let mut ctx = PluginContext::new("trace-2");
829        ctx.set_query_revision_gate(QueryRevisionGate::new(1, Arc::new(AtomicU64::new(1))));
830        let json = serde_json::to_string(&ctx).expect("上下文应可序列化");
831        assert!(
832            !json.contains("revision") && !json.contains("gate"),
833            "门控字段不应出现在序列化结果中: {}",
834            json
835        );
836        assert!(
837            !json.contains("handle"),
838            "句柄字段不应出现在序列化结果中: {}",
839            json
840        );
841
842        let roundtrip: PluginContext = serde_json::from_str(&json).expect("上下文应可反序列化");
843        assert!(
844            roundtrip.query_revision_gate.is_none(),
845            "反序列化后门控应为 None"
846        );
847        assert!(roundtrip.is_query_current());
848        assert_eq!(
849            roundtrip.query_channel,
850            QueryChannel::Ui,
851            "通道字段应参与序列化且缺省为 GUI 通道"
852        );
853    }
854
855    #[test]
856    /// 验证通道与目标类型的序列化键名契约(serde-rename)。
857    fn query_channel_and_target_type_serialize_with_stable_keys() {
858        assert_eq!(
859            serde_json::to_value(QueryChannel::Ui).unwrap(),
860            json!("ui"),
861            "GUI 通道键名应为 ui"
862        );
863        assert_eq!(
864            serde_json::to_value(QueryChannel::Cli).unwrap(),
865            json!("cli"),
866            "CLI 通道键名应为 cli"
867        );
868        assert_eq!(
869            serde_json::to_value(QueryChannel::Panel).unwrap(),
870            json!("panel"),
871            "面板通道键名应为 panel"
872        );
873
874        use super::TargetType;
875        assert_eq!(
876            serde_json::to_value(TargetType::Plugin).unwrap(),
877            json!("Plugin"),
878            "插件目标类型键名应与前端词表一致"
879        );
880        assert_eq!(
881            serde_json::to_value(TargetType::Path).unwrap(),
882            json!("Path")
883        );
884    }
885
886    #[test]
887    /// 验证面板交互策略的 JSON 字段名、枚举值和默认值。
888    fn panel_interaction_serializes_with_stable_contract() {
889        let interaction = PanelInteraction {
890            query_trigger: PanelQueryTrigger::OnEnter,
891            query_debounce_ms: 300,
892            bindings: vec![PanelKeyBinding {
893                key: "Enter".to_string(),
894                action: PanelKeyAction::Confirm,
895            }],
896        };
897        let value = serde_json::to_value(&interaction).expect("交互策略应可序列化");
898        assert_eq!(
899            value,
900            json!({
901                "queryTrigger": "onEnter",
902                "queryDebounceMs": 300,
903                // 完整传输契约:bindings 始终序列化(no-skip-serializing-if),缺省为空列表
904                "bindings": [{"key": "Enter", "action": {"kind": "confirm"}}],
905            })
906        );
907
908        let default_value: PanelInteraction =
909            serde_json::from_value(json!({})).expect("缺失交互策略字段时应使用默认值");
910        assert_eq!(default_value, PanelInteraction::default());
911    }
912}