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: "执行".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)]
756pub enum PluginKind {
757 #[serde(rename = "builtin")]
759 Builtin,
760 #[default]
762 #[serde(rename = "third-party")]
763 ThirdParty,
764}
765
766#[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 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 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 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 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 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": [{"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}