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
785#[cfg(test)]
786mod tests {
787 use super::{
788 PanelInteraction, PanelKeyAction, PanelKeyBinding, PanelQueryTrigger, PluginContext,
789 QueryChannel, QueryRevisionGate,
790 };
791 use serde_json::json;
792 use std::sync::atomic::{AtomicU64, Ordering};
793 use std::sync::Arc;
794
795 #[test]
796 fn query_revision_gate_tracks_latest() {
798 let latest = Arc::new(AtomicU64::new(2));
799 let gate = QueryRevisionGate::new(2, latest.clone());
800 assert!(gate.is_current(), "版本号与最新一致时应有效");
801 assert_eq!(gate.revision(), 2);
802
803 latest.fetch_add(1, Ordering::Relaxed);
804 assert!(!gate.is_current(), "更新的查询到达后应失效");
805 }
806
807 #[test]
808 fn plugin_context_gate_defaults_to_current() {
810 let mut ctx = PluginContext::new("trace-1");
811 assert!(ctx.is_query_current(), "无门控时应恒为最新");
812 assert_eq!(ctx.query_revision(), 0);
813 assert_eq!(
814 ctx.query_channel,
815 QueryChannel::Ui,
816 "未显式注入时应缺省为 GUI 通道"
817 );
818
819 let latest = Arc::new(AtomicU64::new(1));
820 ctx.set_query_revision_gate(QueryRevisionGate::new(1, latest));
821 assert!(ctx.is_query_current());
822 }
823
824 #[test]
825 fn plugin_context_skips_gate_in_serialization() {
827 let mut ctx = PluginContext::new("trace-2");
828 ctx.set_query_revision_gate(QueryRevisionGate::new(1, Arc::new(AtomicU64::new(1))));
829 let json = serde_json::to_string(&ctx).expect("上下文应可序列化");
830 assert!(
831 !json.contains("revision") && !json.contains("gate"),
832 "门控字段不应出现在序列化结果中: {}",
833 json
834 );
835 assert!(
836 !json.contains("handle"),
837 "句柄字段不应出现在序列化结果中: {}",
838 json
839 );
840
841 let roundtrip: PluginContext = serde_json::from_str(&json).expect("上下文应可反序列化");
842 assert!(
843 roundtrip.query_revision_gate.is_none(),
844 "反序列化后门控应为 None"
845 );
846 assert!(roundtrip.is_query_current());
847 assert_eq!(
848 roundtrip.query_channel,
849 QueryChannel::Ui,
850 "通道字段应参与序列化且缺省为 GUI 通道"
851 );
852 }
853
854 #[test]
855 fn query_channel_and_target_type_serialize_with_stable_keys() {
857 assert_eq!(
858 serde_json::to_value(QueryChannel::Ui).unwrap(),
859 json!("ui"),
860 "GUI 通道键名应为 ui"
861 );
862 assert_eq!(
863 serde_json::to_value(QueryChannel::Cli).unwrap(),
864 json!("cli"),
865 "CLI 通道键名应为 cli"
866 );
867 assert_eq!(
868 serde_json::to_value(QueryChannel::Panel).unwrap(),
869 json!("panel"),
870 "面板通道键名应为 panel"
871 );
872
873 use super::TargetType;
874 assert_eq!(
875 serde_json::to_value(TargetType::Plugin).unwrap(),
876 json!("Plugin"),
877 "插件目标类型键名应与前端词表一致"
878 );
879 assert_eq!(
880 serde_json::to_value(TargetType::Path).unwrap(),
881 json!("Path")
882 );
883 }
884
885 #[test]
886 fn panel_interaction_serializes_with_stable_contract() {
888 let interaction = PanelInteraction {
889 query_trigger: PanelQueryTrigger::OnEnter,
890 query_debounce_ms: 300,
891 bindings: vec![PanelKeyBinding {
892 key: "Enter".to_string(),
893 action: PanelKeyAction::Confirm,
894 }],
895 };
896 let value = serde_json::to_value(&interaction).expect("交互策略应可序列化");
897 assert_eq!(
898 value,
899 json!({
900 "queryTrigger": "onEnter",
901 "queryDebounceMs": 300,
902 "bindings": [{"key": "Enter", "action": {"kind": "confirm"}}],
904 })
905 );
906
907 let default_value: PanelInteraction =
908 serde_json::from_value(json!({})).expect("缺失交互策略字段时应使用默认值");
909 assert_eq!(default_value, PanelInteraction::default());
910 }
911}