Skip to main content

zerolaunch_plugin_api/host/
plugin_handle.rs

1use crate::host::{CacheLevel, HostApiError, OpenTarget};
2use crate::platform::capabilities::PlatformCapabilities;
3use crate::services::app::{AppEnumerator, AppInfo, AppLauncher};
4use crate::services::clipboard::ClipboardManager;
5use crate::services::focus_monitor::{FocusCallback, FocusMonitor};
6use crate::services::hotkey::types::{HotkeyCallback, HotkeyEventFilter};
7use crate::services::hotkey::HotkeyManager;
8use crate::services::icon::icon_cache::IconCacheService;
9use crate::services::icon::icon_extractor::IconExtractor;
10use crate::services::installation_monitor::types::InstallationCallback;
11use crate::services::installation_monitor::InstallationMonitor;
12use crate::services::model::{
13    ModelChatRequest, ModelChatResponse, ModelEmbeddingRequest, ModelEmbeddingResponse, ModelError,
14    ModelInfo, ModelService, ModelSimilarityRequest, ModelSimilarityResponse,
15};
16use crate::services::parameter::resolver::ParameterResolver;
17use crate::services::parameter::types::ParameterSnapshot;
18use crate::services::path::path_resolver::{KnownPath, PathResolver};
19use crate::services::resource::AppResourceService;
20use crate::services::shell::lnk_resolver::LnkResolver;
21use crate::services::shell::resource_loader::ResourceLoader;
22use crate::services::shell::ShellExecutor;
23use crate::services::storage::storage_service::StorageService;
24use crate::services::theme::{Theme, ThemeProvider};
25use crate::services::timer::types::{TimerCallback, TimerId, TimerMode};
26use crate::services::timer::TimerManager;
27use crate::services::window::WindowManager;
28use crate::services::IconRequest;
29use parking_lot::RwLock;
30use std::sync::Arc;
31
32use super::sdk_config::PluginSdkConfig;
33/// 插件服务句柄,绑定插件身份与配置。
34/// 跨平台 struct,通过 Arc<dyn IconExtractor> 等平台 trait 注入平台代码。
35/// 插件通过 HostApi::register() 获取此句柄,后续所有服务调用通过句柄完成。
36/// 句柄自动应用注册时的插件配置(如缓存等级),插件无需在每次调用时传递配置。
37pub struct PluginHandle {
38    plugin_id: String,
39    config: RwLock<PluginSdkConfig>,
40    capabilities: PlatformCapabilities,
41    /// 图标提取器,由 HostApi 注入的平台实现
42    icon_extractor: Arc<dyn IconExtractor>,
43    /// 图标缓存服务,由 HostApi 共享
44    icon_cache: Arc<IconCacheService>,
45    /// Shell 执行器,由 HostApi 注入的平台实现
46    shell_executor: Arc<dyn ShellExecutor>,
47    /// 窗口管理器,由 HostApi 注入的平台实现
48    window_manager: Arc<dyn WindowManager>,
49    /// 路径解析器,由 HostApi 注入的平台实现
50    path_resolver: Arc<dyn PathResolver>,
51    /// 应用枚举器,由 HostApi 注入的平台实现
52    app_enumerator: Arc<dyn AppEnumerator>,
53    /// 应用启动器,由 HostApi 注入的平台实现
54    app_launcher: Arc<dyn AppLauncher>,
55    /// Lnk 快捷方式解析器,由 HostApi 注入的平台实现
56    lnk_resolver: Arc<dyn LnkResolver>,
57    /// 资源加载器,由 HostApi 注入的平台实现
58    resource_loader: Arc<dyn ResourceLoader>,
59    /// 参数解析器,由 HostApi 注入
60    parameter_resolver: Arc<dyn ParameterResolver>,
61    /// 定时器管理器,由 HostApi 注入
62    timer_manager: Arc<dyn TimerManager>,
63    /// 应用资源服务,由 HostApi 注入
64    app_resource: Arc<AppResourceService>,
65    /// 存储服务,由 HostApi 注入(共享 RwLock,reconfigure 后自动可见)
66    storage: Arc<RwLock<Arc<dyn StorageService>>>,
67    /// 按键管理器,由 HostApi 注入
68    hotkey_manager: Arc<dyn HotkeyManager>,
69    /// 安装监控器,由 HostApi 注入
70    installation_monitor: Arc<dyn InstallationMonitor>,
71    /// 聚焦监控器,由 HostApi 注入
72    focus_monitor: Arc<dyn FocusMonitor>,
73    /// 剪贴板管理器,由 HostApi 注入
74    clipboard_manager: Arc<dyn ClipboardManager>,
75    /// 主题提供器,由 HostApi 注入(system 模式时查询系统主题)
76    theme_provider: Arc<dyn ThemeProvider>,
77    /// 宿主当前主题配置模式(system/light/dark),由 HostApi 共享
78    theme_mode: Arc<RwLock<String>>,
79    /// 模型服务,由 HostApi 注入
80    model_service: Arc<dyn ModelService>,
81}
82
83impl PluginHandle {
84    /// Creates a new PluginHandle with all the service references injected.
85    #[allow(clippy::too_many_arguments)]
86    pub fn new(
87        plugin_id: String,
88        config: PluginSdkConfig,
89        capabilities: PlatformCapabilities,
90        theme_provider: Arc<dyn ThemeProvider>,
91        theme_mode: Arc<RwLock<String>>,
92        icon_extractor: Arc<dyn IconExtractor>,
93        icon_cache: Arc<IconCacheService>,
94        shell_executor: Arc<dyn ShellExecutor>,
95        window_manager: Arc<dyn WindowManager>,
96        path_resolver: Arc<dyn PathResolver>,
97        app_enumerator: Arc<dyn AppEnumerator>,
98        app_launcher: Arc<dyn AppLauncher>,
99        lnk_resolver: Arc<dyn LnkResolver>,
100        resource_loader: Arc<dyn ResourceLoader>,
101        parameter_resolver: Arc<dyn ParameterResolver>,
102        timer_manager: Arc<dyn TimerManager>,
103        app_resource: Arc<AppResourceService>,
104        storage: Arc<RwLock<Arc<dyn StorageService>>>,
105        hotkey_manager: Arc<dyn HotkeyManager>,
106        installation_monitor: Arc<dyn InstallationMonitor>,
107        focus_monitor: Arc<dyn FocusMonitor>,
108        clipboard_manager: Arc<dyn ClipboardManager>,
109        model_service: Arc<dyn ModelService>,
110    ) -> Self {
111        Self {
112            plugin_id,
113            config: RwLock::new(config),
114            capabilities,
115            theme_provider,
116            theme_mode,
117            icon_extractor,
118            icon_cache,
119            shell_executor,
120            window_manager,
121            path_resolver,
122            app_enumerator,
123            app_launcher,
124            lnk_resolver,
125            resource_loader,
126            parameter_resolver,
127            timer_manager,
128            app_resource,
129            storage,
130            hotkey_manager,
131            installation_monitor,
132            focus_monitor,
133            clipboard_manager,
134            model_service,
135        }
136    }
137
138    /// 返回当前插件 ID(宿主模型缓存等按插件隔离目录需要)。
139    pub fn plugin_id(&self) -> &str {
140        &self.plugin_id
141    }
142
143    /// 获取当前图标缓存等级,None 时返回默认值 Full。
144    fn icon_cache_level(&self) -> CacheLevel {
145        self.config.read().icon_cache_level.unwrap_or_default()
146    }
147
148    // ===== 图标服务 =====
149
150    /// 根据图标请求提取图标数据,行为由注册时的缓存等级决定。
151    /// 参数:request - 图标请求(路径/网址/扩展名)。
152    /// 返回:WebP 格式的图标字节数据(回退路径可能为 PNG),失败返回 HostApiError。
153    pub async fn get_icon(&self, request: IconRequest) -> Result<Vec<u8>, HostApiError> {
154        let level = self.icon_cache_level();
155        self.icon_extractor
156            .get_icon(&self.icon_cache, &request, level)
157            .await
158    }
159
160    /// 提取图标数据,失败时回退到默认图标。
161    /// 与 get_icon 不同,此方法永不返回错误,提取失败时返回默认图标数据。
162    pub async fn get_icon_or_default(&self, request: IconRequest) -> Vec<u8> {
163        let level = self.icon_cache_level();
164        match self
165            .icon_extractor
166            .get_icon(&self.icon_cache, &request, level)
167            .await
168        {
169            Ok(data) if !data.is_empty() => data,
170            _ => {
171                tracing::warn!("图标提取失败,使用默认图标: {:?}", request);
172                self.icon_extractor.load_default_icon(&request).await
173            }
174        }
175    }
176
177    /// 强制从磁盘提取图标数据并根据缓存等级更新缓存。
178    /// 与 get_icon 不同,此方法跳过缓存读取,直接提取并更新缓存。
179    /// 参数:request - 图标请求(路径/网址/扩展名)。
180    /// 返回:WebP 格式的图标字节数据(回退路径可能为 PNG),失败返回 HostApiError。
181    pub async fn get_icon_and_update_cache(
182        &self,
183        request: IconRequest,
184    ) -> Result<Vec<u8>, HostApiError> {
185        let level = self.icon_cache_level();
186        self.icon_extractor
187            .get_icon_and_update_cache(&self.icon_cache, &request, level)
188            .await
189    }
190
191    /// 覆盖指定 IconRequest 的缓存图标为自定义图标文件。
192    /// 参数:original_request - 需要覆盖图标的原始 IconRequest
193    ///       custom_icon_path - 用户选择的自定义图标文件路径
194    /// 返回:成功返回 Ok(()), 失败返回 HostApiError
195    pub async fn override_icon_cache(
196        &self,
197        original_request: &IconRequest,
198        custom_icon_path: &str,
199    ) -> Result<(), HostApiError> {
200        // 缓存键后缀与提取/查询链路一致(.webp)
201        let hash_key = original_request.get_hash_string() + ".webp";
202
203        // 从自定义文件提取并处理图标
204        let custom_request = IconRequest::Path(custom_icon_path.to_string());
205        let data = self
206            .icon_extractor
207            .extract_and_process(&custom_request)
208            .await?;
209
210        // 覆盖写入 L1 + L2 缓存
211        self.icon_cache.set_l1(&hash_key, data.clone());
212        self.icon_cache.set_l2(&hash_key, data).await;
213
214        Ok(())
215    }
216
217    // ===== Shell 服务 =====
218
219    /// 使用系统默认方式打开目标(文件/网址/文件夹)。
220    /// 参数:target - 打开目标。
221    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
222    pub async fn shell_open(&self, target: OpenTarget) -> Result<(), HostApiError> {
223        self.shell_executor.shell_open(&target).await
224    }
225
226    /// 在文件资源管理器中打开指定路径的父目录并选中该文件。
227    /// 参数:path - 要打开所在位置的文件路径。
228    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
229    pub async fn shell_open_folder(&self, path: &str) -> Result<(), HostApiError> {
230        self.shell_executor.shell_open_folder(path).await
231    }
232
233    /// 以管理员权限启动程序。
234    /// 参数:path - 要以管理员身份运行的程序路径。
235    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
236    pub async fn shell_execute_elevation(&self, path: &str) -> Result<(), HostApiError> {
237        self.shell_executor.shell_execute_elevation(path).await
238    }
239
240    /// 执行命令字符串(后台运行,无窗口)。
241    /// 参数:command - 要执行的命令字符串。
242    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
243    pub async fn shell_execute_command(&self, command: &str) -> Result<(), HostApiError> {
244        self.shell_executor.shell_execute_command(command).await
245    }
246
247    // ===== 窗口服务 =====
248
249    /// 根据进程名(如 "chrome.exe")激活已存在的窗口。
250    /// 参数:process_name - 进程名(含扩展名)。
251    /// 返回:成功激活返回 Ok(true),未找到窗口返回 Ok(false),失败返回 HostApiError。
252    pub async fn activate_window_by_process(
253        &self,
254        process_name: &str,
255    ) -> Result<bool, HostApiError> {
256        self.window_manager
257            .activate_window_by_process(process_name)
258            .await
259    }
260
261    /// 根据窗口标题的部分内容激活已存在的窗口。
262    /// 参数:title - 窗口标题的部分匹配文本。
263    /// 返回:成功激活返回 Ok(true),未找到窗口返回 Ok(false),失败返回 HostApiError。
264    pub async fn activate_window_by_title(&self, title: &str) -> Result<bool, HostApiError> {
265        self.window_manager.activate_window_by_title(title).await
266    }
267
268    /// 根据进程 PID 激活已存在的窗口。
269    /// 参数:pid - 进程标识符。
270    /// 返回:成功激活返回 Ok(true),未找到窗口返回 Ok(false),失败返回 HostApiError。
271    pub async fn activate_window_by_pid(&self, pid: u32) -> Result<bool, HostApiError> {
272        self.window_manager.activate_window_by_pid(pid).await
273    }
274
275    // ===== 路径服务 =====
276
277    /// 根据已知路径类型解析实际文件系统路径。
278    /// 参数:path - 已知路径类型枚举。
279    /// 返回:解析后的路径字符串,失败返回 HostApiError。
280    pub fn resolve_path(&self, path: KnownPath) -> Result<String, HostApiError> {
281        self.path_resolver.resolve_path(path)
282    }
283
284    // ===== 剪贴板服务 =====
285
286    /// 将文本写入系统剪贴板。
287    /// 参数:text - 要写入的文本内容。
288    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
289    pub fn set_clipboard_text(&self, text: &str) -> Result<(), HostApiError> {
290        self.clipboard_manager.set_text(text)
291    }
292
293    // ===== 应用服务 =====
294
295    /// 枚举当前平台已安装的应用。
296    /// 参数:无。
297    /// 返回:应用信息列表。
298    pub async fn enumerate_apps(&self) -> Vec<AppInfo> {
299        self.app_enumerator.enumerate_apps().await
300    }
301
302    /// 启动指定应用。
303    /// 参数:app_id - 应用唯一标识;args - 启动参数(可选)。
304    /// 返回:成功返回 Ok(pid),失败返回 HostApiError。
305    pub async fn launch_app(
306        &self,
307        app_id: &str,
308        args: Option<&[String]>,
309    ) -> Result<u32, HostApiError> {
310        self.app_launcher.launch_app(app_id, args).await
311    }
312
313    // ===== 应用资源服务 =====
314
315    /// 根据名称获取内置图标资源的文件系统路径。
316    /// 参数:name - 图标名称(如 "tray_icon", "web_pages" 等)。
317    /// 返回:图标路径,未注册则返回 None。
318    pub fn get_app_icon_path(&self, name: &str) -> Option<String> {
319        self.app_resource.get_icon_path(name)
320    }
321
322    // ===== 快捷方式解析 =====
323
324    /// 解析 .lnk 快捷方式文件的目标路径。
325    /// 参数:lnk_path - .lnk 文件的路径。
326    /// 返回:解析成功返回目标路径,失败返回 None。
327    pub fn resolve_lnk_target(&self, lnk_path: &str) -> Option<String> {
328        self.lnk_resolver.resolve_lnk_target(lnk_path)
329    }
330
331    /// 解析指定目录下的 desktop.ini 文件,提取 [LocalizedFileNames] 部分。
332    /// 参数:dir_path - 要解析的目录路径。
333    /// 返回:从原始文件名到本地化名称的映射。
334    pub fn parse_localized_names_from_dir(
335        &self,
336        dir_path: &std::path::Path,
337    ) -> std::collections::HashMap<String, String> {
338        self.resource_loader
339            .parse_localized_names_from_dir(dir_path)
340    }
341
342    // ===== 配置管理 =====
343
344    /// 更新插件的 SDK 配置。
345    /// 参数:config - 新的插件 SDK 配置。
346    /// 返回:无。
347    /// 特性:立即生效,影响后续所有服务调用。
348    pub fn update_config(&self, config: PluginSdkConfig) {
349        *self.config.write() = config;
350    }
351
352    // ===== 能力查询 =====
353
354    /// 查询当前平台支持的能力集合。
355    /// 参数:无。
356    /// 返回:平台能力的不可变引用。
357    pub fn capabilities(&self) -> &PlatformCapabilities {
358        &self.capabilities
359    }
360
361    /// 查询宿主当前实际生效的界面主题。
362    /// 显式 light/dark 配置直接返回;system 模式委托平台读取系统主题。
363    pub fn get_theme(&self) -> Result<Theme, HostApiError> {
364        match self.theme_mode.read().as_str() {
365            "light" => Ok(Theme::Light),
366            "dark" => Ok(Theme::Dark),
367            _ => self.theme_provider.current_system_theme(),
368        }
369    }
370
371    /// 查询系统主题(未应用宿主显式 light/dark 配置),供前端 system 模式跟随。
372    pub fn get_system_theme(&self) -> Result<Theme, HostApiError> {
373        self.theme_provider.current_system_theme()
374    }
375
376    // ===== 模型服务 =====
377
378    /// 全网模型清单(聚合缓存,含所有已注册提供方)。
379    pub fn model_list(&self) -> Vec<ModelInfo> {
380        self.model_service.list_models()
381    }
382
383    /// 按 model_id 调用文本生成。
384    pub async fn model_chat(&self, req: ModelChatRequest) -> Result<ModelChatResponse, ModelError> {
385        self.model_service.chat(req).await
386    }
387
388    /// 按 model_id 调用文本向量化(task_type 必填,宿主对缺失/未知值返回 InvalidRequest)。
389    pub async fn model_embedding(
390        &self,
391        req: ModelEmbeddingRequest,
392    ) -> Result<ModelEmbeddingResponse, ModelError> {
393        self.model_service.embedding(req).await
394    }
395
396    /// 按 model_id 计算查询向量与多个目标向量的相似度。
397    pub async fn model_similarity(
398        &self,
399        req: ModelSimilarityRequest,
400    ) -> Result<ModelSimilarityResponse, ModelError> {
401        self.model_service.similarity(req).await
402    }
403
404    // ===== 参数解析服务 =====
405
406    /// 解析参数模板
407    ///
408    /// 参数:
409    /// - template: 包含占位符的模板字符串
410    /// - user_args: 用户输入的参数列表
411    /// - snapshot: 系统参数快照(不透明句柄)
412    ///
413    /// 返回:填充后的完整字符串
414    pub async fn resolve_parameters(
415        &self,
416        template: &str,
417        user_args: &[String],
418        snapshot: &ParameterSnapshot,
419    ) -> Result<String, HostApiError> {
420        self.parameter_resolver
421            .resolve(template, user_args, snapshot)
422            .await
423            .map_err(|e| HostApiError::ParameterResolutionFailed {
424                reason: e.to_string(),
425            })
426    }
427
428    /// 统计模板中需要用户输入的参数数量
429    ///
430    /// 参数:template - 模板字符串
431    /// 返回:位置参数的数量
432    pub fn count_user_parameters(&self, template: &str) -> usize {
433        self.parameter_resolver.count_user_parameters(template)
434    }
435
436    /// 检查模板是否包含系统参数
437    ///
438    /// 参数:template - 模板字符串
439    /// 返回:是否包含系统参数
440    pub fn has_system_parameters(&self, template: &str) -> bool {
441        self.parameter_resolver.has_system_parameters(template)
442    }
443
444    // ===== 定时器服务 =====
445
446    /// 创建一个一次性定时器,在指定延迟后触发回调。
447    ///
448    /// 参数:
449    /// - delay: 触发延迟时长
450    /// - callback: 触发时调用的回调函数
451    ///
452    /// 返回:TimerId,可用于取消定时器。
453    pub async fn set_timeout(
454        &self,
455        delay: std::time::Duration,
456        callback: TimerCallback,
457    ) -> Result<TimerId, HostApiError> {
458        self.timer_manager
459            .set_timer(delay, TimerMode::OneShot, callback)
460            .await
461    }
462
463    /// 创建一个重复定时器,每隔指定间隔触发回调。
464    ///
465    /// 参数:
466    /// - interval: 触发间隔时长
467    /// - callback: 每次触发时调用的回调函数
468    ///
469    /// 返回:TimerId,可用于取消定时器。
470    pub async fn set_interval(
471        &self,
472        interval: std::time::Duration,
473        callback: TimerCallback,
474    ) -> Result<TimerId, HostApiError> {
475        self.timer_manager
476            .set_timer(interval, TimerMode::Interval, callback)
477            .await
478    }
479
480    /// 取消指定 ID 的定时器。
481    ///
482    /// 参数:id - 要取消的定时器 ID。
483    pub async fn cancel_timer(&self, id: TimerId) -> Result<(), HostApiError> {
484        self.timer_manager.cancel_timer(id).await
485    }
486
487    /// 取消所有定时器。
488    pub async fn cancel_all_timers(&self) -> Result<(), HostApiError> {
489        self.timer_manager.cancel_all().await
490    }
491
492    // ===== 资源管理 =====
493    /// 上传资源文件到本插件的资源空间。
494    pub async fn resource_upload(
495        &self,
496        resource_id: &str,
497        file_path: &str,
498        max_size: Option<u64>,
499    ) -> Result<String, HostApiError> {
500        let path = std::path::Path::new(file_path);
501
502        if let Some(limit) = max_size {
503            let metadata = tokio::fs::metadata(path).await.map_err(|e| {
504                HostApiError::StorageOperationFailed {
505                    file: file_path.to_string(),
506                    reason: format!("读取文件元数据失败: {}", e),
507                }
508            })?;
509            if metadata.len() > limit {
510                return Err(HostApiError::StorageOperationFailed {
511                    file: file_path.to_string(),
512                    reason: format!("文件大小 {} 超过限制 {} 字节", metadata.len(), limit),
513                });
514            }
515        }
516
517        let data =
518            tokio::fs::read(path)
519                .await
520                .map_err(|e| HostApiError::StorageOperationFailed {
521                    file: file_path.to_string(),
522                    reason: format!("读取文件失败: {}", e),
523                })?;
524
525        // 直接使用 resource_id 作为存储标识符,避免对用户指定的标识符做额外变换。
526        let storage_path = build_resource_path(&self.plugin_id, Some(resource_id))?;
527        let storage = self.storage.read().clone();
528        storage.upload(&storage_path, &data).await.map_err(|e| {
529            HostApiError::StorageOperationFailed {
530                file: storage_path,
531                reason: e.to_string(),
532            }
533        })?;
534        Ok(resource_id.to_string())
535    }
536
537    /// 直接写入资源字节数据,无需先创建临时文件或提供本地路径。
538    /// 参数:resource_id - 资源标识符;data - 资源字节内容。
539    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
540    pub async fn resource_put(&self, resource_id: &str, data: &[u8]) -> Result<(), HostApiError> {
541        let storage_path = build_resource_path(&self.plugin_id, Some(resource_id))?;
542        let storage: Arc<dyn StorageService> = self.storage.read().clone();
543        storage.upload(&storage_path, data).await.map_err(|e| {
544            HostApiError::StorageOperationFailed {
545                file: storage_path,
546                reason: e.to_string(),
547            }
548        })
549    }
550
551    /// 获取资源文件内容。
552    pub async fn resource_get(&self, resource_id: &str) -> Result<Vec<u8>, HostApiError> {
553        let path = build_resource_path(&self.plugin_id, Some(resource_id))?;
554        let storage = self.storage.read().clone();
555        storage
556            .download(&path)
557            .await
558            .map_err(|e| HostApiError::StorageOperationFailed {
559                file: path,
560                reason: e.to_string(),
561            })?
562            .ok_or_else(|| HostApiError::ResourceNotFound {
563                id: resource_id.to_string(),
564            })
565    }
566
567    /// 删除资源文件。
568    pub async fn resource_delete(&self, resource_id: &str) -> Result<(), HostApiError> {
569        let path = build_resource_path(&self.plugin_id, Some(resource_id))?;
570        let storage = self.storage.read().clone();
571        storage
572            .delete(&path)
573            .await
574            .map_err(|e| HostApiError::StorageOperationFailed {
575                file: path,
576                reason: e.to_string(),
577            })
578    }
579
580    /// 列出本插件的所有资源。
581    pub async fn resource_list(&self) -> Result<Vec<String>, HostApiError> {
582        let prefix = build_resource_path(&self.plugin_id, None)?;
583        let storage = self.storage.read().clone();
584        storage
585            .list(&prefix)
586            .await
587            .map_err(|e| HostApiError::StorageOperationFailed {
588                file: prefix,
589                reason: e.to_string(),
590            })
591    }
592
593    // ===== 本地缓存 =====
594
595    /// 写入插件本地缓存。
596    /// 参数:domain - 缓存域(按用途隔离,如 "model-embedding");key - 缓存键;data - 缓存字节内容。
597    /// 返回:成功返回 Ok(())。
598    /// 与 resource_* 的区别:缓存存放可再生的本地数据(如模型向量),
599    /// 不经过 StorageService,WebDAV 同步模式不会上传远端;
600    /// 路径为 <app_data>/plugin-cache/<plugin_id>/<domain>/<key>。
601    pub async fn cache_put(
602        &self,
603        domain: &str,
604        key: &str,
605        data: &[u8],
606    ) -> Result<(), HostApiError> {
607        let cache_root = self.resolve_path(KnownPath::AppCacheDir)?;
608        let path = build_cache_path(&cache_root, &self.plugin_id, domain, key)?;
609        if let Some(parent) = std::path::Path::new(&path).parent() {
610            tokio::fs::create_dir_all(parent).await.map_err(|e| {
611                HostApiError::StorageOperationFailed {
612                    file: path.clone(),
613                    reason: format!("创建缓存目录失败: {}", e),
614                }
615            })?;
616        }
617        tokio::fs::write(&path, data)
618            .await
619            .map_err(|e| HostApiError::StorageOperationFailed {
620                file: path,
621                reason: format!("写入缓存文件失败: {}", e),
622            })?;
623        Ok(())
624    }
625
626    /// 读取插件本地缓存;缓存不存在时返回 Ok(None)。
627    pub async fn cache_get(
628        &self,
629        domain: &str,
630        key: &str,
631    ) -> Result<Option<Vec<u8>>, HostApiError> {
632        let cache_root = self.resolve_path(KnownPath::AppCacheDir)?;
633        let path = build_cache_path(&cache_root, &self.plugin_id, domain, key)?;
634        match tokio::fs::read(&path).await {
635            Ok(data) => Ok(Some(data)),
636            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
637            Err(e) => Err(HostApiError::StorageOperationFailed {
638                file: path,
639                reason: format!("读取缓存文件失败: {}", e),
640            }),
641        }
642    }
643
644    /// 删除插件本地缓存条目;缓存不存在时视为成功。
645    pub async fn cache_delete(&self, domain: &str, key: &str) -> Result<(), HostApiError> {
646        let cache_root = self.resolve_path(KnownPath::AppCacheDir)?;
647        let path = build_cache_path(&cache_root, &self.plugin_id, domain, key)?;
648        match tokio::fs::remove_file(&path).await {
649            Ok(()) => Ok(()),
650            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
651            Err(e) => Err(HostApiError::StorageOperationFailed {
652                file: path,
653                reason: format!("删除缓存文件失败: {}", e),
654            }),
655        }
656    }
657
658    /// 容量控制:缓存域条目超过 max_entries 时按修改时间删除最旧条目。
659    /// 目录结构 <cache_root>/<plugin_id>/<domain>/<sha前2>/<sha>.bin(两级分片)。
660    /// 参数:domain - 缓存域;max_entries - 条目数上限。
661    /// 返回:成功返回 Ok(())(目录不存在视为无需清理)。
662    pub async fn cache_cleanup(
663        &self,
664        domain: &str,
665        max_entries: usize,
666    ) -> Result<(), HostApiError> {
667        let cache_root = self.resolve_path(KnownPath::AppCacheDir)?;
668        let domain_dir = std::path::Path::new(&cache_root)
669            .join(&self.plugin_id)
670            .join(domain);
671        if !domain_dir.exists() {
672            return Ok(());
673        }
674
675        // 收集全部条目(含两级分片目录)及其修改时间
676        let mut entries: Vec<(std::time::SystemTime, std::path::PathBuf)> = Vec::new();
677        for sub in
678            std::fs::read_dir(&domain_dir).map_err(|e| HostApiError::StorageOperationFailed {
679                file: domain_dir.to_string_lossy().to_string(),
680                reason: format!("读取缓存目录失败: {}", e),
681            })?
682        {
683            let sub = sub.map_err(|e| HostApiError::StorageOperationFailed {
684                file: domain_dir.to_string_lossy().to_string(),
685                reason: format!("读取缓存子目录失败: {}", e),
686            })?;
687            if !sub.path().is_dir() {
688                continue;
689            }
690            for file in
691                std::fs::read_dir(sub.path()).map_err(|e| HostApiError::StorageOperationFailed {
692                    file: sub.path().to_string_lossy().to_string(),
693                    reason: format!("读取缓存分片目录失败: {}", e),
694                })?
695            {
696                let file = file.map_err(|e| HostApiError::StorageOperationFailed {
697                    file: sub.path().to_string_lossy().to_string(),
698                    reason: format!("读取缓存条目失败: {}", e),
699                })?;
700                let meta = file
701                    .metadata()
702                    .map_err(|e| HostApiError::StorageOperationFailed {
703                        file: file.path().to_string_lossy().to_string(),
704                        reason: format!("读取缓存条目元数据失败: {}", e),
705                    })?;
706                if meta.is_file() {
707                    entries.push((
708                        meta.modified().unwrap_or(std::time::UNIX_EPOCH),
709                        file.path(),
710                    ));
711                }
712            }
713        }
714
715        if entries.len() <= max_entries {
716            return Ok(());
717        }
718        entries.sort_by_key(|(mtime, _)| *mtime);
719        let excess = entries.len() - max_entries;
720        for (_, path) in entries.into_iter().take(excess) {
721            let _ = std::fs::remove_file(&path);
722        }
723        Ok(())
724    }
725
726    // ===== 推送式回调注册 =====
727
728    /// 为回调 ID 添加插件前缀,避免不同插件间的 ID 冲突。
729    fn prefix_callback_id(&self, id: &str) -> String {
730        format!("{}:{}", self.plugin_id, id)
731    }
732
733    /// 注册按键事件回调。
734    /// 参数:id - 回调标识(自动前缀化为 "{plugin_id}:{id}");filter - 事件过滤器;callback - 回调函数。
735    pub fn register_hotkey_callback(
736        &self,
737        id: &str,
738        filter: HotkeyEventFilter,
739        callback: HotkeyCallback,
740    ) {
741        let prefixed = self.prefix_callback_id(id);
742        self.hotkey_manager
743            .register_callback(&prefixed, filter, callback);
744    }
745
746    /// 注销按键事件回调。
747    /// 参数:id - 回调标识(自动前缀化为 "{plugin_id}:{id}")。
748    pub fn unregister_hotkey_callback(&self, id: &str) {
749        let prefixed = self.prefix_callback_id(id);
750        self.hotkey_manager.unregister_callback(&prefixed);
751    }
752
753    /// 注册安装事件回调。
754    /// 参数:id - 回调标识(自动前缀化为 "{plugin_id}:{id}");callback - 回调函数。
755    pub fn register_installation_callback(&self, id: &str, callback: InstallationCallback) {
756        let prefixed = self.prefix_callback_id(id);
757        self.installation_monitor
758            .register_callback(&prefixed, callback);
759    }
760
761    /// 注销安装事件回调。
762    /// 参数:id - 回调标识(自动前缀化为 "{plugin_id}:{id}")。
763    pub fn unregister_installation_callback(&self, id: &str) {
764        let prefixed = self.prefix_callback_id(id);
765        self.installation_monitor.unregister_callback(&prefixed);
766    }
767
768    /// 注册焦点事件回调。
769    /// 参数:id - 回调标识(自动前缀化为 "{plugin_id}:{id}");callback - 回调函数。
770    pub fn register_focus_callback(&self, id: &str, callback: FocusCallback) {
771        let prefixed = self.prefix_callback_id(id);
772        self.focus_monitor.register_callback(&prefixed, callback);
773    }
774
775    /// 注销焦点事件回调。
776    /// 参数:id - 回调标识(自动前缀化为 "{plugin_id}:{id}")。
777    pub fn unregister_focus_callback(&self, id: &str) {
778        let prefixed = self.prefix_callback_id(id);
779        self.focus_monitor.unregister_callback(&prefixed);
780    }
781}
782
783/// 构建资源存储路径,校验文件名防止路径遍历攻击。
784/// 使用 PathBuf 确保路径构建的安全性。
785/// 返回 Unix 风格路径(存储后端约定)。
786fn build_resource_path(plugin_id: &str, filename: Option<&str>) -> Result<String, HostApiError> {
787    let base = std::path::PathBuf::from_iter(["resources", plugin_id]);
788    let base_normalized = normalize_path(&base);
789
790    let mut path = base.clone();
791    if let Some(name) = filename {
792        // 拒绝空字符串以及 "." / ".." 字面量
793        if name.is_empty() || name == "." || name == ".." {
794            return Err(HostApiError::PathTraversalRejected {
795                path: name.to_string(),
796            });
797        }
798        path.push(name);
799        let normalized = normalize_path(&path);
800        // Path::starts_with 按组件边界匹配。因此 "resources/test" 不会错误地
801        // 作为 "resources/test_evil/..." 的前缀,无需追加尾部分隔符。
802        let is_valid = normalized == base_normalized || normalized.starts_with(&base_normalized);
803        if !is_valid {
804            return Err(HostApiError::PathTraversalRejected {
805                path: name.to_string(),
806            });
807        }
808    }
809    Ok(path.to_string_lossy().replace('\\', "/"))
810}
811
812/// 标准化路径,解析 `.` 和 `..` 组件。
813/// 纯内存操作,不访问文件系统。
814fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
815    let mut result = std::path::PathBuf::new();
816    for component in path.components() {
817        match component {
818            std::path::Component::ParentDir => {
819                result.pop();
820            }
821            std::path::Component::CurDir => {
822                // 跳过
823            }
824            other => {
825                result.push(other);
826            }
827        }
828    }
829    result
830}
831
832/// 构建本地缓存文件路径:`<cache_root>/<plugin_id>/<domain>/<key>`。
833/// 校验 domain 与 key(拒绝空串、"."、".." 与路径穿越),策略同 build_resource_path。
834/// 缓存根目录为宿主 app data 下的 plugin-cache/,不经 StorageService。
835fn build_cache_path(
836    cache_root: &str,
837    plugin_id: &str,
838    domain: &str,
839    key: &str,
840) -> Result<String, HostApiError> {
841    for segment in [domain, key] {
842        if segment.is_empty() || segment == "." || segment == ".." {
843            return Err(HostApiError::PathTraversalRejected {
844                path: segment.to_string(),
845            });
846        }
847    }
848    let base = std::path::Path::new(cache_root)
849        .join(plugin_id)
850        .join(domain);
851    let base_normalized = normalize_path(&base);
852    let mut path = base;
853    path.push(key);
854    let normalized = normalize_path(&path);
855    if normalized != base_normalized && !normalized.starts_with(&base_normalized) {
856        return Err(HostApiError::PathTraversalRejected {
857            path: key.to_string(),
858        });
859    }
860    Ok(path.to_string_lossy().replace('\\', "/"))
861}
862
863#[cfg(test)]
864mod tests {
865    use super::*;
866
867    #[test]
868    fn normalize_path_removes_cur_dir() {
869        let input = std::path::Path::new("a/./b/./c");
870        let result = normalize_path(input);
871        assert_eq!(result, std::path::PathBuf::from("a/b/c"));
872    }
873
874    #[test]
875    fn normalize_path_resolves_parent_dir() {
876        let input = std::path::Path::new("a/b/../c");
877        let result = normalize_path(input);
878        assert_eq!(result, std::path::PathBuf::from("a/c"));
879    }
880
881    #[test]
882    fn normalize_path_handles_leading_dotdot() {
883        let input = std::path::Path::new("../../../etc/passwd");
884        let result = normalize_path(input);
885        // 前导 .. pop 空栈,最终只剩下 etc/passwd
886        assert_eq!(result, std::path::PathBuf::from("etc/passwd"));
887    }
888
889    #[test]
890    fn build_resource_path_rejects_parent_dir_traversal() {
891        let result = build_resource_path("test-plugin", Some("../../../secret"));
892        assert!(result.is_err());
893        match result {
894            Err(HostApiError::PathTraversalRejected { path }) => {
895                assert!(path.contains(".."));
896            }
897            _ => panic!("expected PathTraversalRejected"),
898        }
899    }
900
901    #[test]
902    fn build_resource_path_rejects_cross_plugin_traversal() {
903        // 插件 "test" 尝试通过 .. 访问插件 "test_evil" 的资源
904        let result = build_resource_path("test", Some("../test_evil/secret.txt"));
905        assert!(matches!(
906            result,
907            Err(HostApiError::PathTraversalRejected { .. })
908        ));
909    }
910
911    #[test]
912    fn build_resource_path_rejects_dot_literal() {
913        let result = build_resource_path("test-plugin", Some("."));
914        assert!(matches!(
915            result,
916            Err(HostApiError::PathTraversalRejected { .. })
917        ));
918    }
919
920    #[test]
921    fn build_resource_path_rejects_dotdot_literal() {
922        let result = build_resource_path("test-plugin", Some(".."));
923        assert!(matches!(
924            result,
925            Err(HostApiError::PathTraversalRejected { .. })
926        ));
927    }
928
929    #[test]
930    fn build_resource_path_accepts_valid_filename() {
931        let result = build_resource_path("test-plugin", Some("icon.png"));
932        assert!(result.is_ok());
933        let path = result.unwrap();
934        assert!(path.starts_with("resources/test-plugin/"));
935        assert!(path.ends_with("icon.png"));
936    }
937
938    #[test]
939    fn build_cache_path_accepts_valid_segments() {
940        let result = build_cache_path(
941            "C:/mock/zl-cache",
942            "test-plugin",
943            "model-embedding",
944            "ab/abc123.bin",
945        );
946        assert!(result.is_ok());
947        let path = result.unwrap();
948        assert!(path.starts_with("C:/mock/zl-cache/test-plugin/model-embedding/"));
949        assert!(path.ends_with("abc123.bin"));
950    }
951
952    #[test]
953    fn build_cache_path_rejects_traversal() {
954        for (domain, key) in [
955            ("..", "a.bin"),
956            ("a", "../../x.bin"),
957            ("a", "../b/x.bin"),
958            ("a", ".."),
959            ("a", "."),
960            ("a", ""),
961        ] {
962            assert!(
963                build_cache_path("C:/mock/zl-cache", "test-plugin", domain, key).is_err(),
964                "应拒绝 domain={domain:?} key={key:?}"
965            );
966        }
967    }
968
969    #[test]
970    fn build_resource_path_accepts_none_filename() {
971        let result = build_resource_path("test-plugin", None);
972        assert!(result.is_ok());
973        let path = result.unwrap();
974        assert_eq!(path, "resources/test-plugin");
975    }
976
977    #[test]
978    fn build_resource_path_rejects_empty_filename() {
979        let result = build_resource_path("test-plugin", Some(""));
980        assert!(matches!(
981            result,
982            Err(HostApiError::PathTraversalRejected { .. })
983        ));
984    }
985
986    // ── starts_with 组件级匹配验证 ────────────────────────────
987
988    #[test]
989    fn starts_with_component_boundary_prevents_false_prefix_match() {
990        // 验证 Path::starts_with 按组件边界匹配:
991        // "resources/test" 不是 "resources/test_evil/..." 的前缀。
992        // 这意味着 build_resource_path 不需要尾部分隔符来防止误匹配。
993        let base = std::path::Path::new("resources/test");
994        let evil = std::path::Path::new("resources/test_evil/secret.txt");
995        assert!(!evil.starts_with(base));
996    }
997
998    #[test]
999    fn build_resource_path_rejects_same_prefix_traversal() {
1000        // 插件 "test" 尝试写 path = "test_evil/secret.txt",该路径落在
1001        // resources/test/test_evil/secret.txt → 应被允许(在自己的空间内)。
1002        // 但尝试通过 ../ 逃逸到 test_evil 才被拒绝。
1003        let result = build_resource_path("test", Some("../test_evil/secret.txt"));
1004        assert!(matches!(
1005            result,
1006            Err(HostApiError::PathTraversalRejected { .. })
1007        ));
1008    }
1009
1010    #[test]
1011    fn build_resource_path_allows_subdirectory_with_same_prefix() {
1012        // 资源名 "test_data.txt" 在插件 "test" 下 → resources/test/test_data.txt
1013        // 这不是路径遍历,应被允许。
1014        let result = build_resource_path("test", Some("test_data.txt"));
1015        assert!(result.is_ok());
1016        let path = result.unwrap();
1017        assert_eq!(path, "resources/test/test_data.txt");
1018    }
1019
1020    #[test]
1021    fn build_resource_path_allows_nested_subdir() {
1022        // 允许 plugin_id/test/subdir/file.png 这种深层嵌套
1023        let result = build_resource_path("test", Some("subdir/file.png"));
1024        assert!(result.is_ok());
1025        let path = result.unwrap();
1026        assert_eq!(path, "resources/test/subdir/file.png");
1027    }
1028
1029    #[test]
1030    fn pathbuf_push_empty_is_functionally_noop() {
1031        // 验证 PathBuf::push("") 在 Eq 和 starts_with 语义上是空操作。
1032        // 这确认了 build_resource_path 不需要它来提高安全性。
1033        let mut with_trailing = std::path::PathBuf::from("resources/test");
1034        with_trailing.push("");
1035        let without_trailing = std::path::PathBuf::from("resources/test");
1036
1037        // Eq: 认为相等
1038        assert_eq!(with_trailing, without_trailing);
1039
1040        // starts_with: 行为一致
1041        let child = std::path::Path::new("resources/test/icon.png");
1042        assert!(child.starts_with(&with_trailing));
1043        assert!(child.starts_with(&without_trailing));
1044
1045        let unrelated = std::path::Path::new("resources/test_evil/secret.txt");
1046        assert!(!unrelated.starts_with(&with_trailing));
1047        assert!(!unrelated.starts_with(&without_trailing));
1048    }
1049}