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#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
15pub enum TargetType {
16 #[serde(rename = "Path")]
18 Path,
19 #[serde(rename = "App")]
21 App,
22 #[serde(rename = "File")]
24 File,
25 #[serde(rename = "Url")]
27 Url,
28 #[serde(rename = "Command")]
30 Command,
31 #[serde(rename = "BuiltinCommand")]
33 BuiltinCommand,
34 #[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#[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ExecutionContext {
106 pub target: ExecutionTarget,
107 pub display_name: String,
108 pub user_args: Vec<String>,
110 pub parameter_snapshot: ParameterSnapshot,
112 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#[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 #[error("Window activation failed, fallback to: {fallback_action}")]
144 ActivationFailed { fallback_action: String },
145}
146
147#[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#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SearchCandidate {
160 #[serde(rename = "id")]
162 pub id: CandidateId,
163 #[serde(rename = "name")]
165 pub name: String,
166 #[serde(rename = "icon")]
168 pub icon: IconRequest,
169 #[serde(rename = "target")]
171 pub target: ExecutionTarget,
172 #[serde(rename = "keywords")]
174 pub keywords: Vec<String>,
175 #[serde(rename = "bias")]
177 pub bias: f64,
178 #[serde(rename = "triggerKeywords")]
180 pub trigger_keywords: Vec<String>,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
189pub enum ScoreDetailKind {
190 #[default]
192 #[serde(rename = "add")]
193 Add,
194 #[serde(rename = "multiply")]
196 Multiply,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ScoreDetail {
202 #[serde(rename = "score")]
204 pub score: f64,
205 #[serde(rename = "weight")]
207 pub weight: f64,
208 #[serde(rename = "description")]
210 pub description: String,
211 #[serde(rename = "kind", default)]
213 pub kind: ScoreDetailKind,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ScoredCandidate {
219 #[serde(rename = "candidateId")]
221 pub candidate_id: CandidateId,
222 #[serde(rename = "score")]
224 pub score: f64,
225 #[serde(rename = "detailedScore")]
228 pub detailed_score: Vec<ScoreDetail>,
229}
230
231#[async_trait]
233pub trait DataSource: Configurable {
234 async fn fetch_candidates(&self) -> CachedCandidateData;
235}
236
237#[async_trait]
239pub trait KeywordOptimizer: Configurable {
240 async fn optimize(&self, keyword: &str) -> Vec<String>;
242 fn uses_context(&self) -> bool {
244 false
245 }
246 fn get_priority(&self) -> u32;
248}
249
250#[async_trait]
251pub trait KeywordInjector: Configurable {
252 async fn inject_keywords(&self, candidate: &SearchCandidate) -> Vec<String>;
256}
257
258#[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#[async_trait]
274pub trait ScoreBooster: Configurable {
275 async fn record(&self, candidate_id: CandidateId, data: &CachedCandidateData, query: &str);
277 async fn boost(
279 &self,
280 candidates: &mut Vec<ScoredCandidate>,
281 data: &CachedCandidateData,
282 query: &str,
283 );
284}
285
286#[async_trait]
290pub trait ActionExecutor: Configurable {
291 fn supported_target_types(&self) -> Vec<TargetType>;
293
294 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 async fn execute(&self, ctx: &ExecutionContext, action_id: &str) -> Result<(), ExecutionError>;
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
320pub enum QueryChannel {
321 #[default]
323 #[serde(rename = "ui")]
324 Ui,
325 #[serde(rename = "cli")]
327 Cli,
328 #[serde(rename = "panel")]
331 Panel,
332}
333
334#[derive(Debug, Clone)]
341pub struct QueryRevisionGate {
342 revision: u64,
344 latest: Arc<AtomicU64>,
346}
347
348impl QueryRevisionGate {
349 pub fn new(revision: u64, latest: Arc<AtomicU64>) -> Self {
351 Self { revision, latest }
352 }
353
354 pub fn revision(&self) -> u64 {
356 self.revision
357 }
358
359 pub fn is_current(&self) -> bool {
361 self.latest.load(Ordering::Relaxed) == self.revision
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct PluginContext {
369 pub trace_id: String,
371 pub query_id: Option<String>,
373 pub plugin_id: Option<String>,
375 #[serde(skip)]
378 pub query_revision_gate: Option<QueryRevisionGate>,
379 #[serde(default)]
381 pub query_channel: QueryChannel,
382 #[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 pub fn set_query_revision_gate(&mut self, gate: QueryRevisionGate) {
410 self.query_revision_gate = Some(gate);
411 }
412
413 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct Query {
432 pub id: String,
434 pub raw_query: String,
436 pub search_term: String,
438 #[serde(rename = "confirm", default)]
441 pub confirm: bool,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
447pub enum PanelQueryTrigger {
448 #[default]
450 #[serde(rename = "onInput")]
451 OnInput,
452 #[serde(rename = "onEnter")]
454 OnEnter,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
460pub struct PanelKeyBinding {
461 #[serde(rename = "key")]
463 pub key: String,
464 #[serde(rename = "action")]
466 pub action: PanelKeyAction,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(tag = "kind")]
473pub enum PanelKeyAction {
474 #[serde(rename = "confirm")]
477 Confirm,
478 #[serde(rename = "executeAction")]
480 ExecuteAction {
481 #[serde(rename = "actionId")]
483 action_id: Option<String>,
484 },
485 #[serde(rename = "goBack")]
487 GoBack,
488 #[serde(rename = "gotoPanel")]
490 GotoPanel {
491 #[serde(rename = "panelId")]
492 panel_id: String,
493 },
494 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
513pub struct PanelInteraction {
514 #[serde(rename = "queryTrigger", default)]
516 pub query_trigger: PanelQueryTrigger,
517 #[serde(rename = "queryDebounceMs", default)]
519 pub query_debounce_ms: u64,
520 #[serde(rename = "bindings", default)]
524 pub bindings: Vec<PanelKeyBinding>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
533pub enum QueryResponse {
534 #[serde(rename = "list")]
539 List {
540 #[serde(rename = "results")]
542 results: Vec<ListItem>,
543 },
544 #[serde(rename = "customPanel")]
549 CustomPanel {
550 #[serde(rename = "panelType")]
552 panel_type: String,
553 #[serde(rename = "data")]
555 data: serde_json::Value,
556 #[serde(rename = "actions")]
558 actions: Vec<ResultAction>,
559 #[serde(rename = "keepSearchBar")]
561 keep_search_bar: bool,
562 },
563 #[serde(rename = "empty")]
569 Empty,
570 #[serde(rename = "inlineParam")]
573 InlineParam {
574 #[serde(rename = "candidateId")]
576 candidate_id: CandidateId,
577 #[serde(rename = "triggerKeyword")]
579 trigger_keyword: String,
580 #[serde(rename = "userArgCount")]
582 user_arg_count: usize,
583 },
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct ListItem {
590 #[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 #[serde(rename = "actions")]
603 pub actions: Vec<ResultAction>,
604 #[serde(rename = "targetType")]
606 pub target_type: String,
607 #[serde(rename = "userArgCount")]
609 pub user_arg_count: usize,
610 #[serde(rename = "hasSystemParams")]
612 pub has_system_params: bool,
613 #[serde(rename = "triggerKeywords")]
615 pub trigger_keywords: Vec<String>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct ResultAction {
622 #[serde(rename = "id")]
624 pub id: String,
625 #[serde(rename = "label")]
627 pub label: String,
628 #[serde(rename = "icon")]
630 pub icon: IconRequest,
631 #[serde(rename = "isDefault")]
633 pub is_default: bool,
634 #[serde(rename = "shortcutKey")]
637 pub shortcut_key: String,
638}
639
640#[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 #[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 #[serde(rename = "kind", default)]
667 pub kind: PluginKind,
668 #[serde(rename = "hotkey", default)]
670 pub hotkey: Option<String>,
671 #[serde(rename = "icon", default)]
674 pub icon: Option<String>,
675 #[serde(rename = "mode", default)]
679 pub mode: PluginMode,
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
689pub enum PluginMode {
690 #[serde(rename = "inline")]
692 Inline,
693 #[default]
695 #[serde(rename = "panel")]
696 Panel,
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
705pub enum PluginKind {
706 #[serde(rename = "builtin")]
708 Builtin,
709 #[default]
711 #[serde(rename = "third-party")]
712 ThirdParty,
713}
714
715#[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 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 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 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 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 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": [{"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}