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 get_priority(&self) -> u32;
244 fn input_source(&self) -> KeywordInputSource {
248 KeywordInputSource::Refined
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
261pub enum KeywordInputSource {
262 #[serde(rename = "originalName")]
265 OriginalName,
266 #[serde(rename = "normalizedBase")]
270 NormalizedBase,
271 #[serde(rename = "refined")]
274 #[default]
275 Refined,
276 #[serde(rename = "optimizerOutput")]
282 OptimizerOutput {
283 #[serde(rename = "producerId")]
285 producer_id: String,
286 },
287}
288
289#[async_trait]
290pub trait KeywordInjector: Configurable {
291 async fn inject_keywords(&self, candidate: &SearchCandidate) -> Vec<String>;
295}
296
297#[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#[async_trait]
313pub trait ScoreBooster: Configurable {
314 async fn record(&self, candidate_id: CandidateId, data: &CachedCandidateData, query: &str);
316 async fn boost(
318 &self,
319 candidates: &mut Vec<ScoredCandidate>,
320 data: &CachedCandidateData,
321 query: &str,
322 );
323}
324
325#[async_trait]
329pub trait ActionExecutor: Configurable {
330 fn supported_target_types(&self) -> Vec<TargetType>;
332
333 fn supported_actions(&self) -> Vec<ResultAction> {
335 vec![ResultAction {
336 id: "execute".to_string(),
337 label: "common.run".to_string(),
338 icon: IconRequest::Path(String::new()),
339 is_default: true,
340 shortcut_key: String::new(),
341 }]
342 }
343
344 async fn execute(&self, ctx: &ExecutionContext, action_id: &str) -> Result<(), ExecutionError>;
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
360pub enum QueryChannel {
361 #[default]
363 #[serde(rename = "ui")]
364 Ui,
365 #[serde(rename = "cli")]
367 Cli,
368 #[serde(rename = "panel")]
371 Panel,
372}
373
374#[derive(Debug, Clone)]
382pub struct QueryRevisionGate {
383 revision: u64,
385 latest: Arc<AtomicU64>,
387}
388
389impl QueryRevisionGate {
390 pub fn new(revision: u64, latest: Arc<AtomicU64>) -> Self {
392 Self { revision, latest }
393 }
394
395 pub fn revision(&self) -> u64 {
397 self.revision
398 }
399
400 pub fn is_current(&self) -> bool {
402 self.latest.load(Ordering::Relaxed) == self.revision
403 }
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct PluginContext {
410 pub trace_id: String,
412 pub query_id: Option<String>,
414 pub plugin_id: Option<String>,
416 #[serde(skip)]
419 pub query_revision_gate: Option<QueryRevisionGate>,
420 #[serde(default)]
422 pub query_channel: QueryChannel,
423 #[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 pub fn set_query_revision_gate(&mut self, gate: QueryRevisionGate) {
451 self.query_revision_gate = Some(gate);
452 }
453
454 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct Query {
473 pub id: String,
475 pub raw_query: String,
477 pub search_term: String,
479 #[serde(rename = "confirm", default)]
482 pub confirm: bool,
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
488pub enum PanelQueryTrigger {
489 #[default]
491 #[serde(rename = "onInput")]
492 OnInput,
493 #[serde(rename = "onEnter")]
495 OnEnter,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct PanelKeyBinding {
502 #[serde(rename = "key")]
504 pub key: String,
505 #[serde(rename = "action")]
507 pub action: PanelKeyAction,
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513#[serde(tag = "kind")]
514pub enum PanelKeyAction {
515 #[serde(rename = "confirm")]
518 Confirm,
519 #[serde(rename = "executeAction")]
521 ExecuteAction {
522 #[serde(rename = "actionId")]
524 action_id: Option<String>,
525 },
526 #[serde(rename = "goBack")]
528 GoBack,
529 #[serde(rename = "gotoPanel")]
531 GotoPanel {
532 #[serde(rename = "panelId")]
533 panel_id: String,
534 },
535 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
554pub struct PanelInteraction {
555 #[serde(rename = "queryTrigger", default)]
557 pub query_trigger: PanelQueryTrigger,
558 #[serde(rename = "queryDebounceMs", default)]
560 pub query_debounce_ms: u64,
561 #[serde(rename = "bindings", default)]
565 pub bindings: Vec<PanelKeyBinding>,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
574pub enum QueryResponse {
575 #[serde(rename = "list")]
580 List {
581 #[serde(rename = "results")]
583 results: Vec<ListItem>,
584 },
585 #[serde(rename = "customPanel")]
590 CustomPanel {
591 #[serde(rename = "panelType")]
593 panel_type: String,
594 #[serde(rename = "data")]
596 data: serde_json::Value,
597 #[serde(rename = "actions")]
599 actions: Vec<ResultAction>,
600 #[serde(rename = "keepSearchBar")]
602 keep_search_bar: bool,
603 },
604 #[serde(rename = "empty")]
610 Empty,
611 #[serde(rename = "inlineParam")]
614 InlineParam {
615 #[serde(rename = "candidateId")]
617 candidate_id: CandidateId,
618 #[serde(rename = "triggerKeyword")]
620 trigger_keyword: String,
621 #[serde(rename = "userArgCount")]
623 user_arg_count: usize,
624 },
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct ListItem {
631 #[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 #[serde(rename = "actions")]
644 pub actions: Vec<ResultAction>,
645 #[serde(rename = "targetType")]
647 pub target_type: String,
648 #[serde(rename = "userArgCount")]
650 pub user_arg_count: usize,
651 #[serde(rename = "hasSystemParams")]
653 pub has_system_params: bool,
654 #[serde(rename = "triggerKeywords")]
656 pub trigger_keywords: Vec<String>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct ResultAction {
663 #[serde(rename = "id")]
665 pub id: String,
666 #[serde(rename = "label")]
668 pub label: String,
669 #[serde(rename = "icon")]
671 pub icon: IconRequest,
672 #[serde(rename = "isDefault")]
674 pub is_default: bool,
675 #[serde(rename = "shortcutKey")]
678 pub shortcut_key: String,
679}
680
681#[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 #[serde(rename = "triggerKeywords")]
704 pub trigger_keywords: Vec<String>,
705 #[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 #[serde(rename = "kind", default)]
717 pub kind: PluginKind,
718 #[serde(rename = "hotkey", default)]
720 pub hotkey: Option<String>,
721 #[serde(rename = "icon", default)]
724 pub icon: Option<String>,
725 #[serde(rename = "mode", default)]
729 pub mode: PluginMode,
730}
731
732#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
739pub enum PluginMode {
740 #[serde(rename = "inline")]
742 Inline,
743 #[default]
746 #[serde(rename = "panel")]
747 Panel,
748}
749
750#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
755pub enum PluginKind {
756 #[serde(rename = "builtin")]
758 Builtin,
759 #[default]
761 #[serde(rename = "third-party")]
762 ThirdParty,
763}
764
765#[derive(Debug, thiserror::Error)]
768pub enum PluginError {
769 #[error("Plugin not found: {0}")]
770 NotFound(String),
771
772 #[error("Plugin initialization failed: {0}")]
773 InitFailed(String),
774
775 #[error("Query failed: {0}")]
776 QueryFailed(String),
777
778 #[error("Action execution failed: {0}")]
779 ActionFailed(String),
780
781 #[error("Invalid setting: {0}")]
782 InvalidSetting(String),
783
784 #[error("Plugin shutdown failed: {0}")]
787 ShutdownFailed(String),
788}
789
790#[cfg(test)]
791mod tests {
792 use super::{
793 PanelInteraction, PanelKeyAction, PanelKeyBinding, PanelQueryTrigger, PluginContext,
794 QueryChannel, QueryRevisionGate,
795 };
796 use serde_json::json;
797 use std::sync::atomic::{AtomicU64, Ordering};
798 use std::sync::Arc;
799
800 #[test]
801 fn query_revision_gate_tracks_latest() {
803 let latest = Arc::new(AtomicU64::new(2));
804 let gate = QueryRevisionGate::new(2, latest.clone());
805 assert!(gate.is_current(), "版本号与最新一致时应有效");
806 assert_eq!(gate.revision(), 2);
807
808 latest.fetch_add(1, Ordering::Relaxed);
809 assert!(!gate.is_current(), "更新的查询到达后应失效");
810 }
811
812 #[test]
813 fn plugin_context_gate_defaults_to_current() {
815 let mut ctx = PluginContext::new("trace-1");
816 assert!(ctx.is_query_current(), "无门控时应恒为最新");
817 assert_eq!(ctx.query_revision(), 0);
818 assert_eq!(
819 ctx.query_channel,
820 QueryChannel::Ui,
821 "未显式注入时应缺省为 GUI 通道"
822 );
823
824 let latest = Arc::new(AtomicU64::new(1));
825 ctx.set_query_revision_gate(QueryRevisionGate::new(1, latest));
826 assert!(ctx.is_query_current());
827 }
828
829 #[test]
830 fn plugin_context_skips_gate_in_serialization() {
832 let mut ctx = PluginContext::new("trace-2");
833 ctx.set_query_revision_gate(QueryRevisionGate::new(1, Arc::new(AtomicU64::new(1))));
834 let json = serde_json::to_string(&ctx).expect("上下文应可序列化");
835 assert!(
836 !json.contains("revision") && !json.contains("gate"),
837 "门控字段不应出现在序列化结果中: {}",
838 json
839 );
840 assert!(
841 !json.contains("handle"),
842 "句柄字段不应出现在序列化结果中: {}",
843 json
844 );
845
846 let roundtrip: PluginContext = serde_json::from_str(&json).expect("上下文应可反序列化");
847 assert!(
848 roundtrip.query_revision_gate.is_none(),
849 "反序列化后门控应为 None"
850 );
851 assert!(roundtrip.is_query_current());
852 assert_eq!(
853 roundtrip.query_channel,
854 QueryChannel::Ui,
855 "通道字段应参与序列化且缺省为 GUI 通道"
856 );
857 }
858
859 #[test]
860 fn query_channel_and_target_type_serialize_with_stable_keys() {
862 assert_eq!(
863 serde_json::to_value(QueryChannel::Ui).unwrap(),
864 json!("ui"),
865 "GUI 通道键名应为 ui"
866 );
867 assert_eq!(
868 serde_json::to_value(QueryChannel::Cli).unwrap(),
869 json!("cli"),
870 "CLI 通道键名应为 cli"
871 );
872 assert_eq!(
873 serde_json::to_value(QueryChannel::Panel).unwrap(),
874 json!("panel"),
875 "面板通道键名应为 panel"
876 );
877
878 use super::TargetType;
879 assert_eq!(
880 serde_json::to_value(TargetType::Plugin).unwrap(),
881 json!("Plugin"),
882 "插件目标类型键名应与前端词表一致"
883 );
884 assert_eq!(
885 serde_json::to_value(TargetType::Path).unwrap(),
886 json!("Path")
887 );
888 }
889
890 #[test]
891 fn panel_interaction_serializes_with_stable_contract() {
893 let interaction = PanelInteraction {
894 query_trigger: PanelQueryTrigger::OnEnter,
895 query_debounce_ms: 300,
896 bindings: vec![PanelKeyBinding {
897 key: "Enter".to_string(),
898 action: PanelKeyAction::Confirm,
899 }],
900 };
901 let value = serde_json::to_value(&interaction).expect("交互策略应可序列化");
902 assert_eq!(
903 value,
904 json!({
905 "queryTrigger": "onEnter",
906 "queryDebounceMs": 300,
907 "bindings": [{"key": "Enter", "action": {"kind": "confirm"}}],
909 })
910 );
911
912 let default_value: PanelInteraction =
913 serde_json::from_value(json!({})).expect("缺失交互策略字段时应使用默认值");
914 assert_eq!(default_value, PanelInteraction::default());
915 }
916}