# 插件生命周期与 Plugin 接口

> 文档：`vjcad/doc/plugin/plugin-basics.md`
> 示例：`vjcad/example/plugin/04pluginmanager.js`

## 1. Plugin 接口

```ts
import type { Plugin, PluginManifest, PluginContext } from 'vjcad';

const myPlugin: Plugin = {
    manifest: PluginManifest,

    async onLoad(context):    Promise<void> { /* 加载完成（未激活） */ },
    async onActivate(context):  Promise<void> { /* 激活：注册命令/UI/事件 */ },
    async onDeactivate(context): Promise<void> { /* 停用：注销/清理 */ },
    async onUnload(context):    Promise<void> { /* 最终清理 */ }
};

export default myPlugin;
```

四个钩子都是**可选**的，签名 `Promise<void> | void`（同步函数也行，管理器统一 `await`）。抛错后果按阶段不同：

| 抛错钩子 | 结果 |
|---|---|
| `onLoad` | 插件**不登记**（`getPluginInfo` 查不到）、已注入的 CSS 移除，`load*` 抛 `PluginError`；修好后直接重新 load |
| `onActivate` | 状态 `Error` 且仍留在列表中；再 `activate` 会抛 `PluginError`（"in state error, cannot activate"），须先 `unload` 再重新 load |
| `onDeactivate` | 状态 `Error`，且**跳过** `context._cleanup()`——命令/菜单/Ribbon/事件仍挂着；`unload` 可完成清理 |
| `onUnload` | 照样从列表移除、清理侧栏面板，但 `_cleanup()` 与 CSS 移除被跳过，`unload` 抛 `PluginError` |

> URL 加载（`loadFromUrl/loadFromPath/loadByName` 与 `new MainView({ plugins })`）的文件必须是打包好的 IIFE（挂全局变量），上面的 `export default` 是源码写法，详见 [overview](overview.md) §3 说明。

## 2. PluginManifest

```ts
interface PluginManifest {
    id: string;                       // 必填，唯一
    name: string;                     // 必填，显示名
    version: string;                  // 必填，semver
    author?: string;
    description?: string;
    keywords?: string[];
    dependencies?: string[];          // 依赖的其他插件 id
    minEngineVersion?: string;        // 最低 vjcad 版本
    homepage?: string;
    icon?: string;                    // 插件自身图标
}
```

`dependencies` 只是声明性元数据：`PluginManager` 既不会自动安装依赖，激活时**也不做校验**（bundle 中 `activate()` 只检查状态并调用 `onActivate`）。需要自行先 `loadXxx + activate` 依赖插件。

## 3. 状态枚举

```ts
enum PluginState {
    Unloaded     = 'unloaded',
    Loading      = 'loading',
    Loaded       = 'loaded',
    Activating   = 'activating',
    Active       = 'active',
    Deactivating = 'deactivating',
    Unloading    = 'unloading',
    Error        = 'error'
}
```

```mermaid
stateDiagram-v2
    [*] --> Unloaded
    Unloaded --> Loading: load / loadByName / loadFromUrl
    Loading --> Loaded: onLoad
    Loaded --> Activating: activate
    Activating --> Active: onActivate
    Active --> Deactivating: deactivate
    Deactivating --> Loaded: onDeactivate
    Loaded --> Unloading: unload
    Unloading --> Unloaded: onUnload
    Loading --> Error: 加载失败
    Activating --> Error: 激活失败
    Deactivating --> Error: 停用失败
    Unloading --> Error: 卸载失败
    Error --> Unloaded: 重置
```

## 4. 钩子用途与边界

| 钩子 | 触发时机 | 推荐做的事 | 不建议做的事 |
|---|---|---|---|
| `onLoad` | `pm.load*` 解析完插件 | 校验依赖、初始化无副作用变量 | 注册命令 / 改 UI（此时还未激活） |
| `onActivate` | `pm.activate(id)` | `context.registerCommand / registerIcon / addMenuItem / addRibbon* / on(event)` | 长时间阻塞（改用 setTimeout） |
| `onDeactivate` | `pm.deactivate(id)` | 与 `onActivate` 对称的 `unregister / remove` | 抛错（会进入 Error 状态且跳过自动清理） |
| `onUnload` | `pm.unload(id)` | 释放最终资源（关闭 worker / socket） | 重新注册命令 |

## 5. 自动激活

`loadFromUrl / loadFromContent` 默认 **autoActivate = true**：加载完即自动激活。要显式手动控制，在**加载选项**里传 `{ autoActivate: false }`：

```js
await pm.load({
    type: PluginSourceType.Url,
    js:   'plugins/my-plugin.js',     // 必须是 IIFE 产物（挂全局变量），不能是 ES module 文件
    css:  'plugins/my-plugin.css'
}, { autoActivate: false });          // 放第 2 参；写在 source 对象里会被丢弃（加载器重建 source 时不带 autoActivate）
await pm.activate('my-plugin');
// 等价：await pm.loadFromUrl('plugins/my-plugin.js', 'plugins/my-plugin.css', { autoActivate: false });
```

补充说明：`pm.load({type: PluginSourceType.Content, js: code})` 对**含 `export` 的 ES module 字符串**走 Blob URL + `import()`，取 `default` 导出；不含 `import/export` 的 IIFE 代码则扫描全局 `vjcadPlugin*` 变量找带 `manifest` 的对象。`autoActivate` 默认 true，`load` 返回插件 id，`getPluginInfo(id).state` 激活后为 `"active"`（枚举值是 `Active="active"`，不是 `"activated"`）。

## 6. 生命周期事件

`CadEvents` 暴露了一组插件事件，可在外部监听：

| 事件 | 触发 |
|---|---|
| `PluginLoading` / `PluginLoaded` | 加载前 / 后 |
| `PluginActivating` / `PluginActivated` | 激活前 / 后 |
| `PluginDeactivating` / `PluginDeactivated` | 停用前 / 后 |
| `PluginUnloading` / `PluginUnloaded` | 卸载前 / 后 |
| `PluginError` | 任何阶段失败 |

```js
Engine.eventManager.on(CadEvents.PluginLoaded, args => {
    console.log('插件已加载:', args.pluginId);
});
Engine.eventManager.on(CadEvents.PluginError, args => {
    console.error('插件错误:', args.pluginId, args.error);
});
```

## 7. 没有 PluginManager 的"轻量版"

```js
class MyPlugin {
    install()   { /* 注册 */ }
    uninstall() { /* 注销 */ }
}
new MyPlugin().install();
```

完整可运行例子在 `vjcad/example/plugin/01basic.js`：
- `IconRegistry.registerCommandIcon`
- `CommandRegistry.regist`
- `MenuRegistry.getInstance().addMenuItem('tool', { command })`
- `Engine.view.ribbonBar.addGroup(...)`

`uninstall` 时按相同顺序逆向 `unregist / removeMenuItem / removeGroup`。

## 红旗

- `onActivate / onDeactivate` 抛异常会让插件进入 `Error`，再 `activate` 直接抛 `PluginError`，必须先 `unload` 再重新 load；`onLoad` 抛错则插件根本不登记（见 §1 表）
- `dependencies` 没自动安装；先确保依赖插件已 `loadXxx + activate`
- `autoActivate=false` 时记得自己调 `pm.activate(id)`，否则插件不可见
- 不要在 `onActivate` 里阻塞（数据库 / fetch），用 `setTimeout`/`Promise.then` 异步推进
- `PluginManager` 按 `manifest.id` 去重：同 id 已加载时 `load*` 直接返回已有 id，不会再跑 `onLoad`（`loadFromUrl` 等仍会先把脚本再执行一次，`loadByName` 则提前按 id 短路）；要换新版本传 `{ forceReload: true }`（内部先 `unload` 再注册）

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/plugin/plugin-basics.md`
- 示例：`vjcad/example/plugin/04pluginmanager.js`
