# 第五阶段 · 模块五 · 第三节:插件生命周期
插件是如何加载的?插件的初始化流程是什么?插件的卸载需要注意什么?
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 插件系统 │
│ │
│ 插件生命周期 ← 本节 │
│ ├── 发现(Discover) │
│ ├── 加载(Load) │
│ ├── 初始化(Initialize) │
│ └── 卸载(Unload) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 插件生命周期 │
│ │
│ 1. 发现(Discover) │
│ └─> 扫描插件目录,找到 manifest.json │
│ │
│ 2. 加载(Load) │
│ └─> 读取配置,验证插件 │
│ │
│ 3. 初始化(Initialize) │
│ └─> 执行入口脚本,注册命令/工具/钩子 │
│ │
│ 4. 卸载(Unload) │
│ └─> 清理资源,移除注册 │
└─────────────────────────────────────────────────────────────────────┘
问 1:插件在什么时候加载?
// 启动时加载
// 位置:src/utils/plugins/pluginLoader.ts
async function loadAllPlugins(): Promise<Plugin[]> {
// 1. 扫描用户插件目录
// 2. 扫描项目插件目录
// 3. 扫描市场插件目录
// 4. 对每个插件执行加载流程
}
源码位置:`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));
}
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;
}
问 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" }]
}
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 };
}
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 {};
}
问 1:验证插件做什么?
// 验证内容完整性
async function validatePluginContents(pluginPath: string): Promise<void> {
// 1. 检查入口文件存在
// 2. 检查依赖可用
// 3. 检查权限声明
// 4. 扫描恶意代码(可选)
}
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);
}
}
源码位置:`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,
};
}
问 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);
}
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);
}
}
// 禁用:保留文件,移除注册
async function disablePlugin(pluginId: string): Promise<void> {
// 从 enabledPlugins 移除
removeFromEnabledList(pluginId);
// 不执行清理,只是下次不加载
}
// 卸载:完全删除
async function uninstallPlugin(pluginId: string): Promise<void> {
await disablePlugin(pluginId);
await deletePluginFiles(pluginId); // 危险!
}
问 1:卸载会删除用户数据吗?
// 不会
// ~/.claude/plugins/ 只包含插件代码
// 用户数据在 ~/.claude/settings.json
// 插件可能创建额外的数据
// 卸载时不会自动清理
// 用户需要手动删除
答案:
// 目前不支持热重载
// 修改插件后需要重启 Claude Code
// 但可以重新加载
claude plugins reload
答案:
// 不支持插件间依赖
// 每个插件独立加载
// 如果需要共享代码,使用 MCP 服务器
// 插件可以连接 MCP 服务器来使用共享功能
答案:
// 1. 查看详细日志
claude plugins list --verbose
// 2. 验证插件
claude plugins validate ./my-plugin
// 3. 逐步加载
// 在 manifest 中注释掉部分功能,逐一测试
| 文件 | 核心内容 |
| `src/utils/plugins/pluginLoader.ts` | 插件加载器 |
| `src/utils/plugins/validatePlugin.ts` | 插件验证 |
下一节我们将深入 自定义插件开发:
- 创建插件项目
- 编写 manifest
- 打包和发布
*- 第一轮:□ 事实准确性*
*- 第二轮:□ 深度与洞见*
*- 第三轮:□ 可读性与价值*