Skip to main content

zerolaunch_plugin_host/
manager.rs

1//! PluginHostManager — 第三方插件的顶层编排管理器。
2
3use dashmap::DashMap;
4use parking_lot::RwLock;
5use std::collections::HashSet;
6use std::future::Future;
7use std::path::{Path, PathBuf};
8use std::pin::Pin;
9use std::sync::atomic::{AtomicU32, Ordering};
10use std::sync::{Arc, OnceLock};
11use tokio::sync::mpsc;
12use tokio::sync::Mutex as AsyncMutex;
13use tracing::{error, info, warn};
14
15use zerolaunch_plugin_api::config::Configurable;
16use zerolaunch_plugin_api::plugin::{KeywordInputSource, PluginKind, PluginMetadata, PluginMode};
17use zerolaunch_plugin_protocol::manifest::Manifest;
18use zerolaunch_plugin_protocol::messages::{ComponentKind, KeywordOptimizerInfo};
19use zerolaunch_plugin_protocol::ProtocolError;
20
21use crate::adapter::remote_component::{RemoteComponent, RemoteComponentKind};
22use crate::host_dispatch::HostCallHandler;
23use crate::process::force_kill_process;
24use crate::process::{PluginProcess, ProcessState};
25
26/// 重启回调类型别名:接收重新注册的适配器,
27/// 返回一个 future 用于把它们重新注册进 ConfigManager / SessionRouter。
28pub type RestartCallback =
29    Arc<dyn Fn(PluginRegistration) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
30
31/// 崩溃即解注册回调:接收崩溃插件的旧注册包,通知 src-tauri 立即解注册
32/// CM/SR 中的组件并清理 HostApi 句柄。在崩溃处理的第一步调用,
33/// 与后续重启成败无关——放弃重启时不需要任何额外清理。
34pub type CrashCallback = Arc<dyn Fn(PluginRegistration) + Send + Sync>;
35
36/// 单个第三方插件的完整注册包。
37///
38/// 一个插件可以在 manifest 中声明提供多个组件(例如同时提供 DataSource 和 ActionExecutor),
39/// 所有组件统一存放在 `components` 中,由消费者按 `RemoteComponent::kind` 过滤后注册到
40/// 对应的子系统(Configurable / DataSource / ActionExecutor / Plugin)。
41#[derive(Clone, Debug)]
42pub struct PluginRegistration {
43    /// 插件唯一标识,对应 manifest.toml 中的 plugin.id
44    pub plugin_id: String,
45    /// 原始 manifest 全文快照
46    pub manifest: Manifest,
47    /// 插件实际安装目录(load 时的真实路径,不依赖目录名 == plugin_id 的隐式契约)。
48    /// 卸载/重载/覆盖安装按此路径定位,而非 plugins_dir.join(plugin_id) 反查。
49    pub plugin_dir: PathBuf,
50    /// 插件级元数据(宿主从清单构造,与各 Plugin 组件共享同一 Arc)。
51    /// 供 build_plugin_info 取插件级 priority。
52    pub metadata: Arc<PluginMetadata>,
53
54    /// 该插件的所有远程组件。
55    /// 每个组件都是一个 `RemoteComponent`,同时实现多个 trait;
56    /// 消费者通过 `as_data_source()` / `as_action_executor()` / `as_plugin()` 按需转换。
57    pub components: Vec<Arc<RemoteComponent>>,
58}
59
60/// 插件运行状态(跨 IPC 序列化的运行状态契约)。
61///
62/// 由 plugin-host 从 `ProcessState` 映射产出,经 `InstalledPluginInfo.state` 下发,
63/// 消费方为前端插件管理页与 CLI 的插件列表展示;序列化为 snake_case 小写字符串
64/// (`starting` / `running` / `stopped` / `crashed` / `error` / `unknown`),
65/// 前端/CLI 按枚举值精确匹配。无负载变体:`ProcessState` 的 restarts/last_error
66/// 属诊断信息,经日志可观测,不进入 IPC 契约(Debug 字符串格式曾直接透传,已废弃)。
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum PluginRuntimeState {
70    /// 子进程启动中 —— 握手/组件发现阶段产生,前端非运行态展示。
71    #[serde(rename = "starting")]
72    Starting,
73    /// 运行中 —— 握手完成、watchdog 存活期间;内置插件恒为该态。
74    #[serde(rename = "running")]
75    Running,
76    /// 已停止 —— 优雅关闭或 auto_restart 关闭后退出。
77    #[serde(rename = "stopped")]
78    Stopped,
79    /// 已崩溃 —— 进程异常退出且重启失败(restarts 细节见日志)。
80    #[serde(rename = "crashed")]
81    Crashed,
82    /// 启动错误 —— 握手/协议失败(具体错误见日志)。
83    #[serde(rename = "error")]
84    Error,
85    /// 状态未知 —— 进程条目不存在(如崩溃重启间隙),默认值。
86    #[serde(rename = "unknown")]
87    #[default]
88    Unknown,
89}
90
91impl From<&ProcessState> for PluginRuntimeState {
92    fn from(state: &ProcessState) -> Self {
93        match state {
94            ProcessState::Starting => Self::Starting,
95            ProcessState::Running => Self::Running,
96            ProcessState::Stopped => Self::Stopped,
97            ProcessState::Crashed { .. } => Self::Crashed,
98            ProcessState::Error(_) => Self::Error,
99        }
100    }
101}
102
103/// 重启崩溃插件所需的上下文。
104struct PluginRestartContext {
105    manifest: Manifest,
106    plugin_dir: PathBuf,
107    host_call_handler: Arc<dyn HostCallHandler>,
108    /// 崩溃通知通道发送端副本:重新拉起的进程沿用同一通道。
109    crash_tx: mpsc::Sender<String>,
110    /// 重启成功后调用:供 src-tauri 用新适配器重新注册
111    /// ConfigManager 与 SessionRouter。
112    /// 返回 future 使调用方可避免 `block_on` 及其 `!Send` 风险。
113    on_restart: RestartCallback,
114    /// 崩溃即解注册回调:崩溃处理第一步调用(旧注册包交还 src-tauri 清理 CM/SR)。
115    on_crash: CrashCallback,
116    /// 持久化的重启计数器。每次重新生成前原子递增;
117    /// 当达到 manifest.runtime.max_restart 时不再尝试重启。
118    restart_count: AtomicU32,
119    /// 加载时的宿主界面语言,重启沿用;崩溃时宿主经 update_locale 更新为实时语言。
120    locale: RwLock<String>,
121}
122
123impl std::fmt::Debug for PluginRestartContext {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("PluginRestartContext")
126            .field("manifest", &self.manifest)
127            .field("plugin_dir", &self.plugin_dir)
128            .field("host_call_handler", &self.host_call_handler)
129            .field("crash_tx", &self.crash_tx)
130            .field("on_restart", &"<RestartCallback>")
131            .field("restart_count", &self.restart_count)
132            .finish()
133    }
134}
135
136/// 第三方插件子进程的顶层管理器。
137pub struct PluginHostManager {
138    /// 已加载插件的子进程映射。
139    pub processes: Arc<DashMap<String, Arc<PluginProcess>>>,
140    /// 已加载插件的组件注册包(含 DataSource / Executor / Plugin 适配器)。
141    pub plugins: Arc<DashMap<String, PluginRegistration>>,
142    /// 插件数据的根目录。
143    pub data_dir_root: PathBuf,
144    /// 插件 stderr 日志的根目录。
145    pub log_dir_root: PathBuf,
146    /// 插件安装目录。
147    plugins_dir: PathBuf,
148    /// 每次加载插件时保存重启上下文,崩溃后可重新拉起。
149    restart_contexts: Arc<DashMap<String, Arc<PluginRestartContext>>>,
150    /// 序列化 load/teardown 的注册段(占位检查、spawn、登记、卸载清理)。
151    load_lock: Arc<AsyncMutex<()>>,
152    /// 内置组件 id 集合(冲突预检数据源之一)。
153    ///
154    /// 由 src-tauri 在启动时注入一次(内置组件注册完毕、第三方加载之前);
155    /// 前提是内置组件集合启动后稳定。RwLock 仅用于装配期一次性写入,
156    /// 预检读取为短临界区同步查询,不跨 .await。
157    builtin_component_ids: RwLock<HashSet<String>>,
158    /// 自引用 Arc:供内部崩溃处理任务(crash_loop/handle_crash)复用 load() 等 &self 方法。
159    ///
160    /// new() 构造时一次性写入,之后只读;不使用 RwLock 的原子自引模式
161    /// (AsyncDrop 竞态),OnceLock 在构造完成前无并发访问。
162    self_arc: OnceLock<Arc<Self>>,
163}
164
165/// 插件加载操作错误类型。
166#[derive(Debug, thiserror::Error)]
167pub enum PluginLoadError {
168    #[error("manifest error: {0}")]
169    Manifest(String),
170    #[error("protocol error: {0}")]
171    Protocol(#[from] ProtocolError),
172    #[error("plugin already loaded: {0}")]
173    AlreadyLoaded(String),
174    #[error("plugin not found: {0}")]
175    NotFound(String),
176    /// 组件 id 与已注册组件(内置或其他插件)冲突,插件加载被拒。
177    #[error(
178        "component id collision for plugin {plugin_id}: '{component_id}' is already registered"
179    )]
180    ComponentIdCollision {
181        /// 被拒绝加载的插件 id。
182        plugin_id: String,
183        /// 与已注册组件冲突的组件 id。
184        component_id: String,
185    },
186}
187
188impl PluginHostManager {
189    pub fn new(plugins_dir: PathBuf, data_dir_root: PathBuf, log_dir_root: PathBuf) -> Arc<Self> {
190        let mgr = Arc::new(Self {
191            processes: Arc::new(DashMap::new()),
192            plugins: Arc::new(DashMap::new()),
193            data_dir_root,
194            log_dir_root,
195            plugins_dir,
196            restart_contexts: Arc::new(DashMap::new()),
197            load_lock: Arc::new(AsyncMutex::new(())),
198            builtin_component_ids: RwLock::new(HashSet::new()),
199            self_arc: OnceLock::new(),
200        });
201        if mgr.self_arc.set(Arc::clone(&mgr)).is_err() {
202            panic!("PluginHostManager self arc already set");
203        }
204        mgr
205    }
206
207    /// 注入内置组件 id 集合(冲突预检数据源)。
208    ///
209    /// 由 src-tauri 在内置组件注册完毕后调用一次;此后内置组件集合
210    /// 启动期稳定,不再更新。预检据此识别「与内置组件撞 id」。
211    pub fn set_builtin_component_ids(&self, ids: HashSet<String>) {
212        *self.builtin_component_ids.write() = ids;
213    }
214
215    /// 启动崩溃处理任务。
216    ///
217    /// 同步包装:async 闭包在独立函数中构造并立即 move 进 tokio::spawn,
218    /// 避免 load 的 future 状态机保守保留该闭包(其内部 await crash_loop →
219    /// handle_crash → load 形成间接递归),导致 Send 推断失败。
220    fn spawn_crash_loop(&self, crash_rx: mpsc::Receiver<String>) {
221        let mgr = Arc::clone(self.self_arc.get().expect("self arc set in load"));
222        tokio::spawn(async move {
223            crash_loop(mgr, crash_rx).await;
224        });
225    }
226
227    /// 从包含 manifest.toml 的目录加载一个插件。
228    ///
229    /// `on_restart` 存入重启上下文,崩溃重启成功后被调用,
230    /// 供调用方(src-tauri)重新注册新适配器。
231    /// `on_crash` 在崩溃检测到的那一刻(任何重启尝试之前)以旧注册包调用,
232    /// 供调用方解注册 CM/SR 中的过期组件。
233    /// `restart_count` 为已发生的重启次数(初次加载传 0),用于延续崩溃恢复计数。
234    /// `locale` 为宿主当前界面语言,随 initialize 握手下发插件进程。
235    pub async fn load(
236        &self,
237        plugin_dir: &Path,
238        host_call_handler: Arc<dyn HostCallHandler>,
239        on_restart: RestartCallback,
240        on_crash: CrashCallback,
241        restart_count: u32,
242        locale: &str,
243    ) -> Result<PluginRegistration, PluginLoadError> {
244        let manifest_path = plugin_dir.join("manifest.toml");
245        let manifest_bytes = std::fs::read_to_string(&manifest_path)
246            .map_err(|e| PluginLoadError::Manifest(format!("cannot read manifest.toml: {}", e)))?;
247
248        let manifest: Manifest = toml::from_str(&manifest_bytes)
249            .map_err(|e| PluginLoadError::Manifest(format!("invalid manifest: {}", e)))?;
250
251        // 校验 manifest
252        validate_manifest(&manifest, plugin_dir)?;
253
254        let plugin_id = manifest.plugin.id.clone();
255
256        // 序列化整个注册段(spawn/登记/冲突预检),load 与 teardown 互斥。
257        let _guard = self.load_lock.lock().await;
258
259        // 查重:已加载则拒绝
260        if self.processes.contains_key(&plugin_id) {
261            return Err(PluginLoadError::AlreadyLoaded(plugin_id));
262        }
263
264        let data_dir = self.data_dir_root.join(&plugin_id);
265        let log_dir = self.log_dir_root.clone();
266
267        // 确保数据目录存在
268        if let Err(e) = std::fs::create_dir_all(&data_dir) {
269            warn!(
270                "Failed to create plugin data dir {}: {}",
271                data_dir.display(),
272                e
273            );
274        }
275        if let Err(e) = std::fs::create_dir_all(&log_dir) {
276            warn!(
277                "Failed to create plugin log dir {}: {}",
278                log_dir.display(),
279                e
280            );
281        }
282
283        info!("Loading plugin {} from {}", plugin_id, plugin_dir.display());
284
285        // 创建持久崩溃通知通道:管理器持有接收端,发送端跨多次重启共享。
286        let (crash_tx, crash_rx) = mpsc::channel::<String>(4);
287
288        // 启动子进程并完成握手;失败时直接返回(尚未登记任何条目)
289        let process = match PluginProcess::spawn(
290            &manifest,
291            plugin_dir,
292            &data_dir,
293            &log_dir,
294            host_call_handler.clone(),
295            crash_tx.clone(),
296            restart_count,
297            locale,
298        )
299        .await
300        {
301            Ok(p) => p,
302            Err(e) => {
303                // spawn 失败:尚未登记任何条目,无需额外清理
304                return Err(e.into());
305            }
306        };
307
308        // 在把进程移入 Arc 之前取出 client
309        let client = process.client.clone();
310
311        // 保存重启上下文,供崩溃恢复使用
312        self.restart_contexts.insert(
313            plugin_id.clone(),
314            Arc::new(PluginRestartContext {
315                manifest: manifest.clone(),
316                plugin_dir: plugin_dir.to_path_buf(),
317                host_call_handler,
318                crash_tx,
319                on_restart,
320                on_crash,
321                restart_count: AtomicU32::new(restart_count),
322                locale: RwLock::new(locale.to_string()),
323            }),
324        );
325
326        // 启动崩溃监听任务:崩溃时重新加载该插件
327        self.spawn_crash_loop(crash_rx);
328
329        // 在发现组件之前先登记进程,闭合重启窗口:
330        // 若插件在 discover_components() 期间崩溃,watchdog 能找到进程条目,
331        // 且 crash_loop 会正确处理重新拉起。
332        let process = Arc::new(process);
333        self.processes.insert(plugin_id.clone(), process.clone());
334
335        // 发现组件(经 Arc 调用——discover_components 取 &self)
336        let init_result = match process.discover_components().await {
337            Ok(result) => result,
338            Err(e) => {
339                self.shutdown_loaded_process(&plugin_id).await;
340                return Err(PluginLoadError::Protocol(e));
341            }
342        };
343
344        // 从发现的组件构建适配器
345        let adapters = build_components(&plugin_id, plugin_dir, &manifest, client, &init_result);
346
347        // 冲突预检:组件 id 清单来自插件运行时自报(get_components RPC),
348        // 只能在 spawn 之后获得。这里在登记 hm.plugins 之前校验,
349        // 任一组件 id 与已加载插件或内置组件冲突则整包拒绝:
350        // 关闭子进程并清理全部登记,避免「进程残留 + 半提交注册」。
351        // 自身组件尚未登记进 plugins map,自碰撞在结构上不可能。
352        let component_ids = adapters
353            .components
354            .iter()
355            .map(|c| c.component_id().to_string())
356            .collect::<Vec<_>>();
357        if let Some(collision) =
358            find_component_id_collision(&component_ids, |id| self.component_id_is_taken(id))
359        {
360            error!(
361                "拒绝加载插件 {}:组件 id '{}' 已被其他已注册组件占用,清理进程并放弃加载",
362                plugin_id, collision
363            );
364            self.shutdown_loaded_process(&plugin_id).await;
365            return Err(PluginLoadError::ComponentIdCollision {
366                plugin_id,
367                component_id: collision,
368            });
369        }
370
371        // 先 clone 再登记,返回值无需二次 DashMap 查找 + 6 次字段克隆。
372        let registered = adapters.clone();
373        self.plugins.insert(plugin_id.clone(), adapters);
374        Ok(registered)
375    }
376
377    /// 返回插件安装目录(显式存储,非派生)。
378    pub fn plugins_dir(&self) -> &Path {
379        &self.plugins_dir
380    }
381
382    /// 组件 id 占用查询:已加载第三方插件的组件 或 内置组件集合命中即视为占用。
383    ///
384    /// 仅用于 load 冲突预检(同步短临界区,不跨 .await);
385    /// 查询时自身组件尚未登记进 plugins map,故不会误判自碰撞。
386    fn component_id_is_taken(&self, id: &str) -> bool {
387        let taken_by_plugin = self
388            .plugins
389            .iter()
390            .any(|e| e.value().components.iter().any(|c| c.component_id() == id));
391        taken_by_plugin || self.builtin_component_ids.read().contains(id)
392    }
393
394    /// 卸载插件:关闭子进程、移除全部登记并删除诊断日志(含轮转 .log.1)。
395    /// 与 teardown(加载失败清理)的区别:teardown 保留日志供故障溯源。
396    pub async fn unload(&self, plugin_id: &str) -> Result<(), PluginLoadError> {
397        info!("Unloading plugin {}", plugin_id);
398        self.teardown(plugin_id).await;
399        // 显式卸载才删日志:失败清理路径保留诊断日志,便于故障溯源
400        for suffix in ["", ".1"] {
401            let log_file = self
402                .log_dir_root
403                .join(format!("{}.log{}", plugin_id, suffix));
404            let _ = std::fs::remove_file(&log_file);
405        }
406        Ok(())
407    }
408
409    /// 关闭插件子进程并从注册表移除登记。
410    ///
411    /// 与 load 共用 load_lock,卸载与加载互斥(load 失败清理不调本函数,
412    /// 改调 shutdown_loaded_process——AsyncMutex 非重入,持锁时调用会死锁)。
413    async fn teardown(&self, plugin_id: &str) {
414        let _guard = self.load_lock.lock().await;
415        // 移除重启上下文并注销该插件的订阅回调与定时器(推送通道随进程关闭失效)。
416        if let Some((_, ctx)) = self.restart_contexts.remove(plugin_id) {
417            ctx.host_call_handler.detach_event_sink();
418        }
419        self.shutdown_loaded_process(plugin_id).await;
420        self.plugins.remove(plugin_id);
421    }
422
423    /// 关闭已加载插件的子进程并移除进程登记。
424    /// shutdown() 独占 Arc;若 try_unwrap 失败(进程 Arc 在其他处被克隆),
425    /// 先标记 Stopped 阻止 watchdog 重启,再关闭 stdin 并强杀进程。
426    async fn shutdown_loaded_process(&self, plugin_id: &str) {
427        if let Some((_, proc)) = self.processes.remove(plugin_id) {
428            match Arc::try_unwrap(proc) {
429                Ok(process) => process.shutdown(std::time::Duration::from_secs(5)).await,
430                Err(arc) => {
431                    warn!(
432                        "Plugin {} process Arc has {} strong references; forcing kill. \n                         This may indicate a leaked clone of the process handle.",
433                        plugin_id,
434                        Arc::strong_count(&arc)
435                    );
436                    arc.state.write().clone_from(&ProcessState::Stopped);
437                    arc.client.close_transport();
438                    if let Some(pid) = arc.pid {
439                        force_kill_process(pid);
440                    }
441                }
442            }
443        }
444    }
445
446    /// 更新插件重启上下文中的语言快照。
447    ///
448    /// 宿主在崩溃回调中调用(崩溃时刻取最新语言),保证崩溃重启的
449    /// initialize 握手携带最新 locale,而非首次加载时的快照。
450    /// 插件不存在(正常卸载等)时静默。
451    pub fn update_locale(&self, plugin_id: &str, locale: &str) {
452        if let Some(ctx) = self.restart_contexts.get_mut(plugin_id) {
453            *ctx.locale.write() = locale.to_string();
454        }
455    }
456
457    /// 为全部已加载适配器构造 `InstalledPluginInfo`。
458    ///
459    /// `enabled_fn` 按适配器调用以确定 `enabled` 字段;
460    /// 调用方传入查询 `ConfigManager::is_enabled` 的闭包。
461    pub fn list_plugin_info(
462        &self,
463        enabled_fn: impl Fn(&PluginRegistration) -> bool,
464    ) -> Vec<InstalledPluginInfo> {
465        let mut result: Vec<InstalledPluginInfo> = self
466            .plugins
467            .iter()
468            .map(|entry| {
469                let registration = entry.value();
470                let process_state = self
471                    .processes
472                    .get(&registration.plugin_id)
473                    .map(|p| PluginRuntimeState::from(&*p.state.read()))
474                    .unwrap_or_default();
475                build_plugin_info(registration, process_state, enabled_fn(registration))
476            })
477            .collect();
478        result.sort_by_key(|p| (p.priority, p.plugin_id.clone()));
479        result
480    }
481
482    /// 单个插件的运行时信息(按 id 直查 hm.plugins,不遍历全量列表)。
483    ///
484    /// 与 list_plugin_info 共用同一构造逻辑(build_plugin_info)。
485    pub fn get_plugin_info(
486        &self,
487        plugin_id: &str,
488        enabled_fn: impl Fn(&PluginRegistration) -> bool,
489    ) -> Option<InstalledPluginInfo> {
490        let entry = self.plugins.get(plugin_id)?;
491        let registration = entry.value();
492        let process_state = self
493            .processes
494            .get(plugin_id)
495            .map(|p| PluginRuntimeState::from(&*p.state.read()))
496            .unwrap_or_default();
497        Some(build_plugin_info(
498            registration,
499            process_state,
500            enabled_fn(registration),
501        ))
502    }
503}
504
505/// 由 PluginRegistration 构造运行时信息条目(list_plugin_info / get_plugin_info 共用)。
506fn build_plugin_info(
507    registration: &PluginRegistration,
508    process_state: PluginRuntimeState,
509    enabled: bool,
510) -> InstalledPluginInfo {
511    // 优先级统一取插件级元数据声明值(上层 list_plugins/plugin_info 不再覆盖,
512    // 此即全部路径的唯一来源),不再取组件 priority 最小值。
513    let priority = registration.metadata.priority;
514    InstalledPluginInfo {
515        plugin_id: registration.plugin_id.clone(),
516        name: registration.manifest.plugin.name.clone(),
517        version: registration.manifest.plugin.version.clone(),
518        description: registration.manifest.plugin.description.clone(),
519        author: registration.manifest.plugin.author.clone(),
520        state: process_state,
521        enabled,
522        kind: PluginKind::ThirdParty,
523        priority,
524        component_ids: registration
525            .components
526            .iter()
527            .map(|c| c.component_id().to_string())
528            .collect(),
529        hotkey: registration.metadata.hotkey.clone(),
530        icon: registration.metadata.icon.clone(),
531        mode: registration.metadata.mode,
532    }
533}
534
535/// 已安装插件的信息(供管理界面 / CLI 使用)。
536#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
537pub struct InstalledPluginInfo {
538    #[serde(rename = "pluginId")]
539    pub plugin_id: String,
540    #[serde(rename = "name")]
541    pub name: String,
542    #[serde(rename = "version")]
543    pub version: String,
544    #[serde(rename = "description")]
545    pub description: String,
546    #[serde(rename = "author")]
547    pub author: String,
548    #[serde(rename = "state")]
549    pub state: PluginRuntimeState,
550    #[serde(rename = "enabled")]
551    pub enabled: bool,
552    /// 插件种类:内置或第三方(内置条目由 src-tauri 合并时填充)。
553    #[serde(rename = "kind", default)]
554    pub kind: PluginKind,
555    #[serde(rename = "priority")]
556    pub priority: u32,
557    /// 该插件注册的组件 id 列表(前端据此关联 ConfigManager 中的组件配置)。
558    #[serde(rename = "componentIds", default)]
559    pub component_ids: Vec<String>,
560    /// 全局唤醒快捷键(如 "Ctrl+E"),可空。
561    #[serde(rename = "hotkey", default)]
562    pub hotkey: Option<String>,
563    /// 插件显示图标(data URL,如 "data:image/png;base64,..."),可空表示无图标。
564    /// 来源为 manifest [icon] 段,host 层无条件读取。
565    #[serde(rename = "icon", default)]
566    pub icon: Option<String>,
567    /// 插件形态:inline = 行内插件;panel = 独立插件。
568    /// 行内/独立判定以此字段为唯一权威依据。
569    #[serde(rename = "mode", default)]
570    pub mode: PluginMode,
571}
572
573// ─── 辅助函数 ─────────────────────────────────────────────────────
574
575fn validate_manifest(manifest: &Manifest, plugin_dir: &Path) -> Result<(), PluginLoadError> {
576    let id = &manifest.plugin.id;
577
578    // 校验插件 ID 格式(正则只编译一次)
579    static PLUGIN_ID_RE: OnceLock<regex::Regex> = OnceLock::new();
580    let re = PLUGIN_ID_RE.get_or_init(|| {
581        regex::Regex::new(zerolaunch_plugin_protocol::manifest::PLUGIN_ID_RE).unwrap()
582    });
583    if !re.is_match(id) {
584        return Err(PluginLoadError::Manifest(format!(
585            "invalid plugin id '{}': must match reverse domain",
586            id
587        )));
588    }
589
590    // 校验版本
591    if semver::Version::parse(&manifest.plugin.version).is_err() {
592        return Err(PluginLoadError::Manifest(format!(
593            "invalid plugin version '{}'",
594            manifest.plugin.version
595        )));
596    }
597
598    // 校验必需的 provides 声明
599    if manifest.components.provides.is_empty() {
600        return Err(PluginLoadError::Manifest(
601            "components.provides must have at least one entry".into(),
602        ));
603    }
604
605    for p in &manifest.components.provides {
606        if !zerolaunch_plugin_protocol::manifest::REQUIRED_PROVIDES_VALUES.contains(&p.as_str()) {
607            return Err(PluginLoadError::Manifest(format!(
608                "unknown component type '{}'",
609                p
610            )));
611        }
612    }
613
614    // Validate command path does not escape the plugin directory
615    let cmd_path = plugin_dir.join(&manifest.runtime.command);
616    let canonical_cmd = cmd_path
617        .canonicalize()
618        .map_err(|e| PluginLoadError::Manifest(format!("command not found: {}", e)))?;
619    let canonical_plugin_dir = plugin_dir
620        .canonicalize()
621        .map_err(|e| PluginLoadError::Manifest(format!("plugin dir canonicalize: {}", e)))?;
622    if !canonical_cmd.starts_with(&canonical_plugin_dir) {
623        return Err(PluginLoadError::Manifest(
624            "command path escapes plugin directory".into(),
625        ));
626    }
627
628    // Validate icon path does not escape the plugin directory (if declared).
629    // 文件缺失/不可读不阻断加载(图标可选,由 read_plugin_icon 降级为 None);
630    // 仅当文件存在且能解析时校验逃逸(安全项硬拒绝)。
631    if let Some(icon) = &manifest.icon {
632        if let Ok(canonical_icon) = plugin_dir.join(&icon.path).canonicalize() {
633            if !canonical_icon.starts_with(&canonical_plugin_dir) {
634                return Err(PluginLoadError::Manifest(
635                    "icon path escapes plugin directory".into(),
636                ));
637            }
638        }
639    }
640
641    Ok(())
642}
643
644// ─── 辅助函数:按 component_id 从 Vec<(String, T)> 中查找值 ───
645
646/// 读取 manifest [icon] 段声明的图标文件(相对插件目录)并转为 data URL。
647/// 路径逃逸插件目录、文件缺失或超过大小上限返回 None(图标缺失不阻断加载)。
648fn read_plugin_icon(plugin_dir: &Path, icon_path: &str) -> Option<String> {
649    let icon_abs = plugin_dir.join(icon_path);
650    let canonical_icon = icon_abs.canonicalize().ok()?;
651    let canonical_plugin_dir = plugin_dir.canonicalize().ok()?;
652    // 防路径遍历:图标必须位于插件目录内
653    if !canonical_icon.starts_with(&canonical_plugin_dir) {
654        warn!("插件图标路径逃逸插件目录,忽略: {}", icon_path);
655        return None;
656    }
657    // 上限预检:避免把超大文件整个读进内存(字节级上限由 icon::to_data_url 兜底)
658    let meta = std::fs::metadata(&canonical_icon).ok()?;
659    if meta.len() > crate::icon::MAX_ICON_BYTES {
660        warn!("插件图标超过 1MB 上限,忽略: {}", icon_path);
661        return None;
662    }
663    let bytes = std::fs::read(&canonical_icon).ok()?;
664    crate::icon::to_data_url(crate::icon::mime_from_extension(&canonical_icon), &bytes)
665}
666
667/// 从 `Vec<(String, T)>` 中按 component_id 查找值,找不到返回 default。
668fn find_by_id<T: Clone + Default>(map: &[(String, T)], component_id: &str) -> T {
669    map.iter()
670        .find(|(id, _)| id == component_id)
671        .map(|(_, v)| v.clone())
672        .unwrap_or_default()
673}
674
675/// 从 settings_values 中查找,找不到返回 Null(区别于 default)。
676fn find_settings_value(
677    values: &[(String, serde_json::Value)],
678    component_id: &str,
679) -> serde_json::Value {
680    values
681        .iter()
682        .find(|(id, _)| id == component_id)
683        .map(|(_, v)| v.clone())
684        .unwrap_or(serde_json::Value::Null)
685}
686
687// ─── build_components ───────────────────────────────────────────────
688
689/// 从组件 id 列表中找出第一个已被占用(与已加载插件或内置组件冲突)的 id。
690///
691/// 仅在本模块加载(load)预检中使用;返回 None 表示全部 id 可用。
692/// 独立成纯函数便于单测。
693fn find_component_id_collision(
694    component_ids: &[String],
695    component_id_taken: impl Fn(&str) -> bool,
696) -> Option<String> {
697    component_ids
698        .iter()
699        .find(|id| component_id_taken(id))
700        .cloned()
701}
702
703/// 从 InitResult 构建所有 `RemoteComponent`。
704///
705/// 每个组件统一构造为 `RemoteComponent`;种类专属数据(target_types、result_actions、
706/// metadata)根据 `ComponentKind` 放入 `RemoteComponentKind` 对应变体。
707fn build_components(
708    plugin_id: &str,
709    plugin_dir: &Path,
710    manifest: &Manifest,
711    client: Arc<crate::client::JsonRpcClient>,
712    init_result: &crate::process::InitResult,
713) -> PluginRegistration {
714    // 插件级元数据由宿主从清单构造;包 Arc 后与各 Plugin 组件共享同一份。
715    let plugin_metadata = PluginMetadata {
716        id: plugin_id.to_string(),
717        name: manifest.plugin.name.clone(),
718        version: manifest.plugin.version.clone(),
719        description: manifest.plugin.description.clone(),
720        author: manifest.plugin.author.clone(),
721        trigger_keywords: manifest.plugin.trigger_keywords.clone(),
722        trigger_description: manifest.plugin.trigger_description.clone(),
723        supported_os: manifest.plugin.supported_os.clone(),
724        priority: manifest.plugin.priority,
725        // 第三方插件由宿主强制标注
726        kind: PluginKind::ThirdParty,
727        hotkey: manifest.plugin.hotkey.clone(),
728        // 图标:从清单 [icon] 段读取
729        icon: manifest
730            .icon
731            .as_ref()
732            .and_then(|icon| read_plugin_icon(plugin_dir, &icon.path)),
733        mode: manifest.plugin.mode,
734    };
735    // 热键仅对 panel 形态生效(前端热键表与 wake_plugin 均按 mode 过滤)
736    if plugin_metadata.hotkey.is_some() && plugin_metadata.mode != PluginMode::Panel {
737        warn!(
738            plugin_id = plugin_id,
739            declared = ?plugin_metadata.mode,
740            "插件清单声明热键但形态非 panel:热键仅对 panel 形态生效,该热键将不注册"
741        );
742    }
743    let plugin_metadata = Arc::new(plugin_metadata);
744
745    let components: Vec<Arc<RemoteComponent>> = init_result
746        .components
747        .iter()
748        .map(|comp| {
749            let schema = find_by_id(&init_result.settings_schemas, &comp.component_id);
750            let settings = find_settings_value(&init_result.settings_values, &comp.component_id);
751            let config_actions = find_by_id(&init_result.config_actions_map, &comp.component_id);
752            let default_enabled = find_by_id(&init_result.default_enabled_map, &comp.component_id);
753
754            // 组件优先级为 u32(协议层已统一),无负数可能,无需钳制
755            let priority = comp.priority;
756
757            let kind = match &comp.kind {
758                ComponentKind::Plugin => {
759                    let interaction_policy =
760                        find_by_id(&init_result.interaction_policy_map, &comp.component_id);
761                    RemoteComponentKind::Plugin {
762                        metadata: Arc::clone(&plugin_metadata),
763                        interaction_policy: parking_lot::RwLock::new(interaction_policy),
764                    }
765                }
766                ComponentKind::DataSource => RemoteComponentKind::DataSource,
767                ComponentKind::ActionExecutor { target_types } => {
768                    let result_actions =
769                        find_by_id(&init_result.executor_actions_map, &comp.component_id);
770                    RemoteComponentKind::ActionExecutor {
771                        target_types: target_types.clone(),
772                        result_actions,
773                    }
774                }
775                ComponentKind::SearchEngine => RemoteComponentKind::SearchEngine,
776                ComponentKind::ScoreBooster => RemoteComponentKind::ScoreBooster,
777                ComponentKind::KeywordOptimizer => {
778                    // 与 process.rs discover 兜底语义一致:RPC 失败时 input_source 取
779                    // Refined(精化层默认,最安全——只做幂等精化不读原始名)、
780                    // priority 取组件自身优先级(不用 Default 的 priority=0)。
781                    let info = init_result
782                        .keyword_optimizer_info_map
783                        .iter()
784                        .find(|(id, _)| id == &comp.component_id)
785                        .map(|(_, v)| v.clone())
786                        .unwrap_or(KeywordOptimizerInfo {
787                            input_source: KeywordInputSource::Refined,
788                            priority,
789                        });
790                    RemoteComponentKind::KeywordOptimizer {
791                        info: parking_lot::RwLock::new(info),
792                    }
793                }
794                ComponentKind::KeywordInjector => RemoteComponentKind::KeywordInjector,
795            };
796
797            Arc::new(RemoteComponent::new(
798                comp.component_id.clone(),
799                comp.component_name.clone(),
800                comp.component_description.clone(),
801                comp.component_type,
802                priority,
803                client.clone(),
804                schema,
805                settings,
806                config_actions,
807                default_enabled,
808                kind,
809            ))
810        })
811        .collect();
812
813    let registration = PluginRegistration {
814        plugin_id: plugin_id.to_string(),
815        manifest: manifest.clone(),
816        // 记录真实安装目录:卸载/重载按此定位,不依赖目录名 == plugin_id 契约
817        plugin_dir: plugin_dir.to_path_buf(),
818        metadata: plugin_metadata,
819        components,
820    };
821
822    #[cfg(debug_assertions)]
823    {
824        // 唯一源断言:所有 Plugin 组件必须与 registration.metadata 共享同一 Arc。
825        // 若未来某处改为独立 clone,此处立即失败,防止两副本单边漂移。
826        for comp in &registration.components {
827            if let RemoteComponentKind::Plugin { metadata, .. } = &comp.kind {
828                debug_assert!(
829                    Arc::ptr_eq(metadata, &registration.metadata),
830                    "组件元数据与 PluginRegistration.metadata 必须同源共享(Arc::clone)"
831                );
832            }
833        }
834    }
835
836    registration
837}
838
839/// 崩溃处理主循环:监听 crash channel,串行处理每个崩溃事件。
840///
841/// 每次 load 创建新 channel 时 spawn 一个;channel 关闭(进程与上下文均销毁)即退出。
842async fn crash_loop(mgr: Arc<PluginHostManager>, mut crash_rx: mpsc::Receiver<String>) {
843    while let Some(plugin_id) = crash_rx.recv().await {
844        handle_crash(&mgr, &plugin_id).await;
845    }
846}
847
848/// 处理单个插件崩溃:崩溃即解注册 → 计数/上限检查 → 复用 load() 重启。
849///
850/// 旧注册包在第一步交还 src-tauri 解注册(CM/SR/host_api),
851/// 之后无论重启成败都不再有残留,无需任何「放弃清理」路径。
852async fn handle_crash(mgr: &PluginHostManager, plugin_id: &str) {
853    warn!("Watchdog triggered restart for plugin: {}", plugin_id);
854
855    // 崩溃即解注册:从登记中移除旧进程与旧注册包,并把旧注册包
856    // 立即交还 src-tauri 清理 CM/SR/host_api。自身组件因此不再
857    // 出现在预检数据源中,后续 load 的重启预检不会自碰撞。
858    let prev = mgr.plugins.remove(plugin_id);
859    mgr.processes.remove(plugin_id);
860
861    // 取出 owned Arc:DashMap 读守卫仅在闭包内存活,map 返回即释放,
862    // 不跨后续 load(可能耗时数秒)的 .await。
863    let Some(ctx) = mgr
864        .restart_contexts
865        .get(plugin_id)
866        .map(|r| Arc::clone(r.value()))
867    else {
868        warn!(
869            "Crash notification for plugin '{}' but no restart context found — \
870             plugin may have been unloaded concurrently",
871            plugin_id
872        );
873        return;
874    };
875    // 先 detach:on_crash 会注销 HostApi 句柄,之后无从清理。
876    ctx.host_call_handler.detach_event_sink();
877
878    if let Some((_, prev)) = prev {
879        (ctx.on_crash)(prev);
880    }
881    // 原子递增持久化的重启计数器,并检查是否超出 max_restart。
882    // 这是**唯一**追踪重启次数的地方——不在看门狗或
883    // PluginProcess 中(它们每次重启都会被替换)。
884    let new_count = ctx.restart_count.fetch_add(1, Ordering::SeqCst) + 1;
885    let max_restart = ctx.manifest.runtime.max_restart;
886    if new_count > max_restart {
887        error!(
888            "Plugin {} exceeded max restarts ({}/{}) — not restarting",
889            plugin_id, new_count, max_restart
890        );
891        mgr.restart_contexts.remove(plugin_id);
892        return;
893    }
894
895    // 复用 load():spawn/discover/预检/登记与初次加载同一路径,
896    // 失败路径由 load 自行清理(冲突/discover 失败会 teardown),
897    // 此处只需在 load 失败时移除旧上下文。
898    // 读取最新语言并克隆:RwLock guard 不得跨 .await 持有
899    // (load 内部会 await spawn 握手)。
900    let locale = ctx.locale.read().clone();
901    match mgr
902        .load(
903            &ctx.plugin_dir,
904            ctx.host_call_handler.clone(),
905            ctx.on_restart.clone(),
906            ctx.on_crash.clone(),
907            new_count,
908            &locale,
909        )
910        .await
911    {
912        Ok(registered) => {
913            // 重启成功:通知 src-tauri 注册新适配器(旧组件已在崩溃第一步解注册)
914            (ctx.on_restart)(registered).await;
915            info!("Plugin {} successfully restarted", plugin_id);
916        }
917        Err(e) => {
918            error!("Failed to restart plugin {}: {}", plugin_id, e);
919            mgr.restart_contexts.remove(plugin_id);
920        }
921    }
922}
923
924#[cfg(test)]
925mod tests {
926    use super::find_component_id_collision;
927
928    /// 冲突预检契约:返回第一个被占用的组件 id;全部可用时返回 None。
929    #[test]
930    fn collision_check_finds_first_taken_id() {
931        let ids = vec!["free".to_string(), "taken".to_string(), "other".to_string()];
932        let taken = |id: &str| id == "taken";
933        assert_eq!(
934            find_component_id_collision(&ids, taken),
935            Some("taken".to_string())
936        );
937    }
938
939    /// 无冲突时返回 None,加载预检可放行。
940    #[test]
941    fn collision_check_returns_none_when_all_free() {
942        let ids = vec!["a".to_string(), "b".to_string()];
943        assert_eq!(find_component_id_collision(&ids, |_| false), None);
944    }
945}