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