第三节:插件生命周期
加载初始化与卸载

作者:小学子 📚 | 日期:2026年4月2日 | 第五阶段 · 模块五


# 第五阶段 · 模块五 · 第三节:插件生命周期

核心问题

插件是如何加载的?插件的初始化流程是什么?插件的卸载需要注意什么?


◇ 本节位置


        Claude Code 全局架构
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │  插件系统                                                            │
        │                                                                      │
        │  插件生命周期 ← 本节                                                 │
        │  ├── 发现(Discover)                                               │
        │  ├── 加载(Load)                                                   │
        │  ├── 初始化(Initialize)                                           │
        │  └── 卸载(Unload)                                                 │
        └─────────────────────────────────────────────────────────────────────┘
        


一、生命周期概览

1.1 四个阶段


        ┌─────────────────────────────────────────────────────────────────────┐
        │  插件生命周期                                                        │
        │                                                                      │
        │  1. 发现(Discover)                                                │
        │     └─> 扫描插件目录,找到 manifest.json                            │
        │                                                                      │
        │  2. 加载(Load)                                                    │
        │     └─> 读取配置,验证插件                                          │
        │                                                                      │
        │  3. 初始化(Initialize)                                            │
        │     └─> 执行入口脚本,注册命令/工具/钩子                            │
        │                                                                      │
        │  4. 卸载(Unload)                                                  │
        │     └─> 清理资源,移除注册                                           │
        └─────────────────────────────────────────────────────────────────────┘
        

1.2 五问分析

问 1:插件在什么时候加载?


        // 启动时加载
        // 位置:src/utils/plugins/pluginLoader.ts
        
        async function loadAllPlugins(): Promise<Plugin[]> {
          // 1. 扫描用户插件目录
          // 2. 扫描项目插件目录
          // 3. 扫描市场插件目录
          // 4. 对每个插件执行加载流程
        }
        


二、发现阶段

2.1 扫描插件目录

源码位置:`src/utils/plugins/pluginLoader.ts`


        async function discoverPlugins(): Promise<PluginPath[]> {
          const paths: PluginPath[] = [];
        
          // 用户插件
          const userPluginDir = path.join(os.homedir(), '.claude', 'plugins');
          if (await pathExists(userPluginDir)) {
            paths.push(...await scanDir(userPluginDir));
          }
        
          // 项目插件
          const projectPluginDir = path.join(process.cwd(), '.claude', 'plugins');
          if (await pathExists(projectPluginDir)) {
            paths.push(...await scanDir(projectPluginDir));
          }
        
          return paths;
        }
        
        async function scanDir(dir: string): Promise<PluginPath[]> {
          const entries = await readdir(dir);
          return entries
            .filter(name => !name.startsWith('.'))  // 忽略隐藏目录
            .map(name => path.join(dir, name));
        }
        

2.2 读取 Manifest


        async function loadPluginManifest(
          manifestPath: string,
          pluginName: string,
          source: string,
        ): Promise<PluginManifest> {
          // 如果 manifest 不存在,创建默认
          if (!(await pathExists(manifestPath))) {
            return {
              name: pluginName,
              description: `Plugin from ${source}`,
            };
          }
        
          // 读取并解析
          const content = await readFile(manifestPath, { encoding: 'utf-8' });
          const parsedJson = jsonParse(content);
        
          // 验证 schema
          const result = PluginManifestSchema().safeParse(parsedJson);
          if (!result.success) {
            throw new Error(`Invalid manifest: ${result.error}`);
          }
        
          return result.data;
        }
        

2.3 五问分析

问 1:manifest.json 是什么?


        // 插件的配置文件
        // 必须包含:name, description
        // 可选包含:version, commands, tools, hooks
        
        // 示例
        {
          "name": "my-plugin",
          "version": "1.0.0",
          "description": "A useful plugin",
          "commands": [{ "name": "hello", "path": "./commands/hello.ts" }],
          "tools": [{ "name": "myTool", "path": "./tools/myTool.ts" }]
        }
        


三、加载阶段

3.1 验证插件


        async function loadPlugin(pluginPath: string): Promise<Plugin | null> {
          // 1. 检查是否启用
          if (!isPluginEnabled(pluginPath)) {
            return null;
          }
        
          // 2. 读取 manifest
          const manifest = await loadPluginManifest(
            path.join(pluginPath, 'plugin.json'),
            path.basename(pluginPath),
            pluginPath,
          );
        
          // 3. 验证插件内容
          await validatePluginContents(pluginPath);
        
          // 4. 加载设置
          const settings = await loadPluginSettings(pluginPath, manifest);
        
          return { manifest, path: pluginPath, settings };
        }
        

3.2 插件设置


        async function loadPluginSettings(
          pluginPath: string,
          manifest: PluginManifest,
        ): Promise<PluginSettings> {
          // 插件可以有自己的设置文件
          const settingsPath = path.join(pluginPath, 'settings.json');
        
          if (await pathExists(settingsPath)) {
            const content = await readFile(settingsPath, { encoding: 'utf-8' });
            return jsonParse(content);
          }
        
          return {};
        }
        

3.3 五问分析

问 1:验证插件做什么?


        // 验证内容完整性
        async function validatePluginContents(pluginPath: string): Promise<void> {
          // 1. 检查入口文件存在
          // 2. 检查依赖可用
          // 3. 检查权限声明
          // 4. 扫描恶意代码(可选)
        }
        


四、初始化阶段

4.1 执行入口脚本


        async function initializePlugin(plugin: Plugin): Promise<void> {
          // 1. 注册命令
          for (const cmd of plugin.manifest.commands ?? []) {
            await registerCommand(plugin.path, cmd);
          }
        
          // 2. 注册工具
          for (const tool of plugin.manifest.tools ?? []) {
            await registerTool(plugin.path, tool);
          }
        
          // 3. 注册钩子
          for (const hook of plugin.manifest.hooks ?? []) {
            await registerHook(plugin.path, hook);
          }
        
          // 4. 初始化插件(如果需要)
          if (plugin.manifest.onInit) {
            await executePluginCode(plugin.path, plugin.manifest.onInit);
          }
        }
        

4.2 加载钩子

源码位置:`src/utils/plugins/pluginLoader.ts` 第 1224 行


        async function loadPluginHooks(
          hooksPath: string,
          pluginName: string,
        ): Promise<PluginHooks> {
          // 插件可以提供钩子
          if (!(await pathExists(hooksPath))) {
            return {};
          }
        
          // 动态导入钩子模块
          const hooks = await import(hooksPath);
        
          return {
            preQuery: hooks.preQuery,
            postQuery: hooks.postQuery,
            preTool: hooks.preTool,
            postTool: hooks.postTool,
          };
        }
        

4.3 五问分析

问 1:钩子是什么?


        // 钩子允许插件在特定时机执行代码
        type PluginHooks = {
          preQuery?: (params: QueryParams) => Promise<void>;
          postQuery?: (result: QueryResult) => Promise<void>;
          preTool?: (tool: ToolCall) => Promise<void>;
          postTool?: (result: ToolResult) => Promise<void>;
        };
        

问 2:插件初始化失败怎么办?


        // 单个插件失败不影响其他插件
        async function loadAllPlugins(): Promise<Plugin[]> {
          const plugins = await discoverPlugins();
          const results = await Promise.allSettled(
            plugins.map(p => loadPlugin(p)),
          );
        
          // 收集成功的
          return results
            .filter(r => r.status === 'fulfilled')
            .map(r => r.value);
        }
        


五、卸载阶段

5.1 清理资源


        async function unloadPlugin(pluginId: string): Promise<void> {
          // 1. 移除命令注册
          for (const cmd of getPluginCommands(pluginId)) {
            unregisterCommand(cmd.name);
          }
        
          // 2. 移除工具注册
          for (const tool of getPluginTools(pluginId)) {
            unregisterTool(tool.name);
          }
        
          // 3. 移除钩子
          unregisterPluginHooks(pluginId);
        
          // 4. 执行清理脚本(如果有)
          const plugin = getPlugin(pluginId);
          if (plugin.manifest.onUnload) {
            await executePluginCode(plugin.path, plugin.manifest.onUnload);
          }
        }
        

5.2 卸载 vs 禁用


        // 禁用:保留文件,移除注册
        async function disablePlugin(pluginId: string): Promise<void> {
          // 从 enabledPlugins 移除
          removeFromEnabledList(pluginId);
          // 不执行清理,只是下次不加载
        }
        
        // 卸载:完全删除
        async function uninstallPlugin(pluginId: string): Promise<void> {
          await disablePlugin(pluginId);
          await deletePluginFiles(pluginId);  // 危险!
        }
        

5.3 五问分析

问 1:卸载会删除用户数据吗?


        // 不会
        // ~/.claude/plugins/ 只包含插件代码
        // 用户数据在 ~/.claude/settings.json
        
        // 插件可能创建额外的数据
        // 卸载时不会自动清理
        // 用户需要手动删除
        


六、思考题

思考题 1:热重载支持吗?

答案


        // 目前不支持热重载
        // 修改插件后需要重启 Claude Code
        
        // 但可以重新加载
        claude plugins reload
        


思考题 2:插件可以依赖其他插件吗?

答案


        // 不支持插件间依赖
        // 每个插件独立加载
        
        // 如果需要共享代码,使用 MCP 服务器
        // 插件可以连接 MCP 服务器来使用共享功能
        


思考题 3:如何调试加载问题?

答案


        // 1. 查看详细日志
        claude plugins list --verbose
        
        // 2. 验证插件
        claude plugins validate ./my-plugin
        
        // 3. 逐步加载
        // 在 manifest 中注释掉部分功能,逐一测试
        


七、延伸阅读

文件核心内容
`src/utils/plugins/pluginLoader.ts`插件加载器
`src/utils/plugins/validatePlugin.ts`插件验证


八、下节预告

下一节我们将深入 自定义插件开发

- 创建插件项目

- 编写 manifest

- 打包和发布


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*