sloth-d2c-mcp 1.0.4-beta87 → 1.0.4-beta90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/run.js CHANGED
@@ -113,6 +113,21 @@ if (args[0] === 'config') {
113
113
 
114
114
  console.log(`[sloth] 已在后台启动,PID: ${child.pid}`);
115
115
  process.exit(0);
116
+ } else if (args[0] === 'add') {
117
+ // 设置环境变量标识这是 add 命令(安装插件)
118
+ process.env.SLOTH_COMMAND = 'add';
119
+ // 传递插件名称参数
120
+ process.env.SLOTH_ADD_ARGS = JSON.stringify(args.slice(1));
121
+ } else if (args[0] === 'remove') {
122
+ // 设置环境变量标识这是 remove 命令(卸载插件)
123
+ process.env.SLOTH_COMMAND = 'remove';
124
+ // 传递插件名称参数
125
+ process.env.SLOTH_REMOVE_ARGS = JSON.stringify(args.slice(1));
126
+ } else if (args[0] === 'plugins') {
127
+ // 设置环境变量标识这是 plugins 命令(列出插件)
128
+ process.env.SLOTH_COMMAND = 'plugins';
129
+ // 传递子命令参数
130
+ process.env.SLOTH_PLUGINS_ARGS = JSON.stringify(args.slice(1));
116
131
  } else if (args[0] === 'stop') {
117
132
  // stop 命令:停止监听 3100/3101 端口的 sloth 进程
118
133
  const { execSync } = await import('node:child_process');
@@ -14,6 +14,8 @@ import { Logger } from './utils/logger.js';
14
14
  import { getAvailablePort, saveImageFile, replaceImageSrc, formatListRoots } from './utils/utils.js';
15
15
  import { processSampling, buildNestingStructure } from './core/sampling.js';
16
16
  import { buildComponentMappingPrompt, buildFullUpdatePromptForAI, buildPlaceholderPrompt } from './core/prompt-builder.js';
17
+ import { pluginManager } from './plugin/manager.js';
18
+ import { installPlugin, uninstallPlugin, listPlugins } from './plugin/loader.js';
17
19
  import { detectClientApiSupport, clearCapabilitiesCache } from './utils/client-capabilities.js';
18
20
  // @ts-ignore
19
21
  import * as flatted from 'flatted';
@@ -103,6 +105,9 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
103
105
  Logger.log('标准化根目录:', root);
104
106
  // 设置 fileManager 的工作目录根路径
105
107
  fileManager.setWorkspaceRoot(root);
108
+ // 设置插件管理器的工作区根目录并初始化
109
+ pluginManager.setWorkspaceRoot(root);
110
+ await pluginManager.initialize();
106
111
  }
107
112
  catch (error) {
108
113
  Logger.log('获取根目录时出错:', error);
@@ -194,6 +199,8 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
194
199
  console.log('d2cNodeList', d2cNodeList);
195
200
  console.log('imageMap', imageMap);
196
201
  console.log('absoluteHtml', absoluteHtml);
202
+ pluginManager.executeHook('onFetchImage', { imageMap });
203
+ console.log('pluginManager executeHook onFetchImage', imageMap);
197
204
  // 如果配置了figmaApiKey,则可以预览,传入absoluteHtml
198
205
  if (!hasLaunchWebview) {
199
206
  // await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml)
@@ -288,7 +295,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
288
295
  const aggregationPrompt = savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt;
289
296
  const finalPrompt = savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt;
290
297
  const noSamplingPrompt = savedPromptSetting?.noSamplingAggregationPrompt || noSamplingAggregationPrompt;
291
- const codeSnippets = [];
298
+ let codeSnippets = [];
292
299
  let isSupportSampling = clientApiSupport.sampling;
293
300
  Logger.log(`客户端 API 支持检测: sampling=${isSupportSampling}, roots=${clientApiSupport.roots}`);
294
301
  // 收集已映射的组件信息,用于在最终写入时添加提示词
@@ -431,6 +438,14 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
431
438
  codeSnippets.unshift(placeholderPrompt + '\n' + replacedCode);
432
439
  }
433
440
  }
441
+ const onSamplingComplete = await pluginManager.executeHook('onSamplingComplete', {
442
+ codeSnippets,
443
+ imageMap
444
+ });
445
+ codeSnippets = onSamplingComplete?.codeSnippets || codeSnippets;
446
+ imageMap = onSamplingComplete?.imageMap || imageMap;
447
+ console.log('executeHook onSamplingComplete', imageMap);
448
+ console.log('executeHook onSamplingComplete', codeSnippets);
434
449
  // 保存图片文件
435
450
  saveImageFile({ imageMap, root });
436
451
  Logger.log('处理完成,生成的代码片段数量:', codeSnippets.length);
@@ -651,6 +666,9 @@ async function main() {
651
666
  console.log(' config 显示配置文件信息');
652
667
  console.log(' page 显示 interceptor-web 页面地址');
653
668
  console.log(' framework 添加或管理框架配置');
669
+ console.log(' add 安装 sloth 插件');
670
+ console.log(' remove 卸载 sloth 插件');
671
+ console.log(' plugins 列出已安装的插件');
654
672
  console.log(' clear 清理缓存文件');
655
673
  console.log(' version 显示版本信息');
656
674
  console.log('');
@@ -667,6 +685,9 @@ async function main() {
667
685
  console.log(' sloth page --list # 列出所有页面文件');
668
686
  console.log(' sloth framework react # 添加 react 框架');
669
687
  console.log(' sloth framework --list # 列出所有框架');
688
+ console.log(' sloth add sloth-plugin-cos # 安装插件');
689
+ console.log(' sloth remove sloth-plugin-cos # 卸载插件');
690
+ console.log(' sloth plugins # 列出已安装插件');
670
691
  console.log(' sloth clear # 清理缓存文件');
671
692
  console.log(' sloth version # 显示版本信息');
672
693
  console.log(' sloth --version # 显示版本信息');
@@ -1021,9 +1042,93 @@ async function main() {
1021
1042
  }
1022
1043
  process.exit(0);
1023
1044
  }
1045
+ // 检查是否是 add 命令(安装插件)
1046
+ if (process.env.SLOTH_COMMAND === 'add') {
1047
+ const addArgsStr = process.env.SLOTH_ADD_ARGS || '[]';
1048
+ const addArgs = JSON.parse(addArgsStr);
1049
+ // 处理帮助参数
1050
+ if (addArgs.includes('--help') || addArgs.includes('-h')) {
1051
+ console.log('使用方法: sloth add <插件包名>');
1052
+ console.log('');
1053
+ console.log('安装 sloth 插件');
1054
+ console.log('');
1055
+ console.log('示例:');
1056
+ console.log(' sloth add sloth-plugin-cos # 安装 COS 上传插件');
1057
+ console.log(' sloth add @my/sloth-plugin # 安装 scoped 插件');
1058
+ process.exit(0);
1059
+ }
1060
+ if (addArgs.length === 0) {
1061
+ console.log('错误: 请提供插件包名');
1062
+ console.log('使用 "sloth add --help" 查看帮助信息');
1063
+ process.exit(1);
1064
+ }
1065
+ const packageName = addArgs[0];
1066
+ const result = await installPlugin(packageName);
1067
+ process.exit(result.success ? 0 : 1);
1068
+ }
1069
+ // 检查是否是 remove 命令(卸载插件)
1070
+ if (process.env.SLOTH_COMMAND === 'remove') {
1071
+ const removeArgsStr = process.env.SLOTH_REMOVE_ARGS || '[]';
1072
+ const removeArgs = JSON.parse(removeArgsStr);
1073
+ // 处理帮助参数
1074
+ if (removeArgs.includes('--help') || removeArgs.includes('-h')) {
1075
+ console.log('使用方法: sloth remove <插件包名>');
1076
+ console.log('');
1077
+ console.log('卸载 sloth 插件');
1078
+ console.log('');
1079
+ console.log('示例:');
1080
+ console.log(' sloth remove sloth-plugin-cos # 卸载 COS 上传插件');
1081
+ process.exit(0);
1082
+ }
1083
+ if (removeArgs.length === 0) {
1084
+ console.log('错误: 请提供插件包名');
1085
+ console.log('使用 "sloth remove --help" 查看帮助信息');
1086
+ process.exit(1);
1087
+ }
1088
+ const packageName = removeArgs[0];
1089
+ const result = await uninstallPlugin(packageName);
1090
+ process.exit(result.success ? 0 : 1);
1091
+ }
1092
+ // 检查是否是 plugins 命令(列出插件)
1093
+ if (process.env.SLOTH_COMMAND === 'plugins') {
1094
+ const pluginsArgsStr = process.env.SLOTH_PLUGINS_ARGS || '[]';
1095
+ const pluginsArgs = JSON.parse(pluginsArgsStr);
1096
+ const isJsonOutput = pluginsArgs.includes('--json');
1097
+ // 处理帮助参数
1098
+ if (pluginsArgs.includes('--help') || pluginsArgs.includes('-h')) {
1099
+ console.log('使用方法: sloth plugins [选项]');
1100
+ console.log('');
1101
+ console.log('列出已安装的 sloth 插件');
1102
+ console.log('');
1103
+ console.log('选项:');
1104
+ console.log(' --help, -h 显示帮助信息');
1105
+ console.log(' --json 以 JSON 格式输出');
1106
+ process.exit(0);
1107
+ }
1108
+ const plugins = await listPlugins();
1109
+ if (isJsonOutput) {
1110
+ console.log(JSON.stringify({ plugins }, null, 2));
1111
+ }
1112
+ else {
1113
+ if (plugins.length === 0) {
1114
+ console.log('暂无已安装的插件');
1115
+ console.log('');
1116
+ console.log('使用 "sloth add <插件包名>" 安装插件');
1117
+ }
1118
+ else {
1119
+ console.log('已安装的插件:');
1120
+ plugins.forEach((plugin, index) => {
1121
+ console.log(` ${index + 1}. ${plugin}`);
1122
+ });
1123
+ }
1124
+ }
1125
+ process.exit(0);
1126
+ }
1024
1127
  try {
1025
1128
  // 先输出基本启动信息到控制台,确保即使 VSCode 日志失败也能看到启动过程
1026
1129
  console.log(`[${new Date().toISOString()}] Starting Figma transcoding interceptor MCP server...`);
1130
+ // 注意:插件管理器的初始化已移到 d2c_figma 工具中
1131
+ // 因为需要先获取工作区根目录才能加载工作区插件
1027
1132
  // Create config manager instance to pass to server
1028
1133
  await startListening(true);
1029
1134
  }
@@ -0,0 +1,4 @@
1
+ // 导出插件管理器
2
+ export { pluginManager } from './manager.js';
3
+ // 导出插件加载器函数
4
+ export { installPlugin, uninstallPlugin, listPlugins, listWorkspacePlugins, loadAllPlugins, loadWorkspacePlugins, loadPlugin } from './loader.js';
@@ -0,0 +1,320 @@
1
+ import { execSync } from 'child_process';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import envPaths from 'env-paths';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ // 检测是否存在 pnpm
9
+ function hasPnpm() {
10
+ try {
11
+ execSync('pnpm --version', { stdio: 'ignore' });
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ // 获取包管理器命令
19
+ function getPackageManager() {
20
+ return hasPnpm() ? 'pnpm' : 'npm';
21
+ }
22
+ // 使用 env-paths 获取配置目录(与 config-manager 保持一致)
23
+ const paths = envPaths('d2c-mcp');
24
+ // 获取全局 sloth-d2c-mcp 包的目录
25
+ function getGlobalPackageDir() {
26
+ // 在构建后的目录中: dist/build/plugin/loader.js
27
+ // 需要向上三级才能找到包根目录
28
+ return path.resolve(__dirname, '..', '..', '..');
29
+ }
30
+ // 获取全局插件配置文件路径(与 config.json 在同一目录)
31
+ function getGlobalPluginsConfigPath() {
32
+ return path.join(paths.config, 'plugins.json');
33
+ }
34
+ // 获取工作区插件目录路径
35
+ function getWorkspacePluginDir(workspaceRoot) {
36
+ return path.join(workspaceRoot, '.sloth', 'plugin');
37
+ }
38
+ // 获取工作区插件配置文件路径
39
+ function getWorkspacePluginsConfigPath(workspaceRoot) {
40
+ return path.join(getWorkspacePluginDir(workspaceRoot), 'plugins.json');
41
+ }
42
+ // 加载全局插件配置
43
+ async function loadGlobalPluginsConfig() {
44
+ const configPath = getGlobalPluginsConfigPath();
45
+ try {
46
+ const content = await fs.readFile(configPath, 'utf-8');
47
+ return JSON.parse(content);
48
+ }
49
+ catch {
50
+ return { plugins: [] };
51
+ }
52
+ }
53
+ // 保存全局插件配置
54
+ async function saveGlobalPluginsConfig(config) {
55
+ const configPath = getGlobalPluginsConfigPath();
56
+ // 确保目录存在
57
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
58
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
59
+ }
60
+ // 加载工作区插件配置
61
+ async function loadWorkspacePluginsConfig(workspaceRoot) {
62
+ const configPath = getWorkspacePluginsConfigPath(workspaceRoot);
63
+ try {
64
+ const content = await fs.readFile(configPath, 'utf-8');
65
+ return JSON.parse(content);
66
+ }
67
+ catch {
68
+ return { plugins: [] };
69
+ }
70
+ }
71
+ /**
72
+ * 解析插件配置项,统一返回 { name, config } 格式
73
+ */
74
+ function parsePluginConfigItem(item) {
75
+ if (typeof item === 'string') {
76
+ return { name: item };
77
+ }
78
+ return { name: item.name, config: item.config };
79
+ }
80
+ // 检查本地插件是否存在
81
+ async function checkLocalPlugin(workspaceRoot, pluginName) {
82
+ const pluginDir = getWorkspacePluginDir(workspaceRoot);
83
+ const localPluginPath = path.join(pluginDir, pluginName, 'index.cjs');
84
+ try {
85
+ await fs.access(localPluginPath);
86
+ return localPluginPath;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ // 检查 npm 包是否已安装
93
+ async function isNpmPackageInstalled(packageName) {
94
+ try {
95
+ await import(packageName);
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ // 尝试安装 npm 包
103
+ async function tryInstallNpmPackage(packageName) {
104
+ const globalDir = getGlobalPackageDir();
105
+ const pm = getPackageManager();
106
+ try {
107
+ console.log(`[sloth] 尝试安装插件包: ${packageName}`);
108
+ execSync(`${pm} install ${packageName}`, {
109
+ cwd: globalDir,
110
+ stdio: 'pipe',
111
+ });
112
+ return true;
113
+ }
114
+ catch (error) {
115
+ console.error(`[sloth] 安装插件包 ${packageName} 失败:`, error);
116
+ return false;
117
+ }
118
+ }
119
+ /**
120
+ * 安装插件 npm 包到全局(sloth add 命令)
121
+ * 注意:这只是安装到全局,不会直接生效
122
+ * 需要在工作区的 .sloth/plugin/plugins.json 中声明才会生效
123
+ * @param packageName 插件包名
124
+ */
125
+ export async function installPlugin(packageName) {
126
+ const globalDir = getGlobalPackageDir();
127
+ console.log(`[sloth] 正在安装插件: ${packageName}`);
128
+ console.log(`[sloth] 安装目录: ${globalDir}`);
129
+ try {
130
+ const pm = getPackageManager();
131
+ // 在全局包目录下执行安装
132
+ execSync(`${pm} install ${packageName}`, {
133
+ cwd: globalDir,
134
+ stdio: 'inherit',
135
+ });
136
+ // 更新全局插件配置(记录已安装的插件)
137
+ const config = await loadGlobalPluginsConfig();
138
+ const existingNames = config.plugins.map(item => parsePluginConfigItem(item).name);
139
+ if (!existingNames.includes(packageName)) {
140
+ config.plugins.push(packageName);
141
+ await saveGlobalPluginsConfig(config);
142
+ }
143
+ console.log(`[sloth] 插件 ${packageName} 安装成功`);
144
+ console.log(`[sloth] 注意:需要在工作区的 .sloth/plugin/plugins.json 中声明才会生效`);
145
+ return {
146
+ success: true,
147
+ message: `插件 ${packageName} 安装成功。请在工作区的 .sloth/plugin/plugins.json 中添加该插件以启用。`
148
+ };
149
+ }
150
+ catch (error) {
151
+ const errorMessage = error instanceof Error ? error.message : String(error);
152
+ console.error(`[sloth] 插件安装失败: ${errorMessage}`);
153
+ return { success: false, message: `插件安装失败: ${errorMessage}` };
154
+ }
155
+ }
156
+ /**
157
+ * 卸载插件 npm 包
158
+ * @param packageName 插件包名
159
+ */
160
+ export async function uninstallPlugin(packageName) {
161
+ const globalDir = getGlobalPackageDir();
162
+ console.log(`[sloth] 正在卸载插件: ${packageName}`);
163
+ try {
164
+ const pm = getPackageManager();
165
+ // 在全局包目录下执行卸载
166
+ execSync(`${pm} uninstall ${packageName}`, {
167
+ cwd: globalDir,
168
+ stdio: 'inherit',
169
+ });
170
+ // 更新全局插件配置
171
+ const config = await loadGlobalPluginsConfig();
172
+ config.plugins = config.plugins.filter((p) => parsePluginConfigItem(p).name !== packageName);
173
+ await saveGlobalPluginsConfig(config);
174
+ console.log(`[sloth] 插件 ${packageName} 卸载成功`);
175
+ return { success: true, message: `插件 ${packageName} 卸载成功` };
176
+ }
177
+ catch (error) {
178
+ const errorMessage = error instanceof Error ? error.message : String(error);
179
+ console.error(`[sloth] 插件卸载失败: ${errorMessage}`);
180
+ return { success: false, message: `插件卸载失败: ${errorMessage}` };
181
+ }
182
+ }
183
+ /**
184
+ * 列出全局已安装的插件
185
+ */
186
+ export async function listPlugins() {
187
+ const config = await loadGlobalPluginsConfig();
188
+ return config.plugins.map(item => parsePluginConfigItem(item).name);
189
+ }
190
+ /**
191
+ * 列出工作区声明的插件
192
+ * @param workspaceRoot 工作区根目录
193
+ */
194
+ export async function listWorkspacePlugins(workspaceRoot) {
195
+ const config = await loadWorkspacePluginsConfig(workspaceRoot);
196
+ return config.plugins.map(item => parsePluginConfigItem(item));
197
+ }
198
+ /**
199
+ * 校验并加载工作区声明的所有插件
200
+ * 1. 先尝试查找 .sloth/plugin/ 文件夹下的本地插件
201
+ * 2. 再尝试通过 pnpm/npm 安装声明的 npm 包
202
+ * 3. 都失败则抛出错误
203
+ * @param workspaceRoot 工作区根目录
204
+ */
205
+ export async function loadWorkspacePlugins(workspaceRoot) {
206
+ const config = await loadWorkspacePluginsConfig(workspaceRoot);
207
+ const plugins = [];
208
+ const failedPlugins = [];
209
+ console.log(`[sloth] 开始加载工作区插件,工作区: ${workspaceRoot}`);
210
+ const pluginNames = config.plugins.map(item => parsePluginConfigItem(item).name);
211
+ console.log(`[sloth] 声明的插件: ${pluginNames.join(', ') || '无'}`);
212
+ for (const pluginItem of config.plugins) {
213
+ const { name: pluginName, config: pluginConfig } = parsePluginConfigItem(pluginItem);
214
+ let loaded = false;
215
+ console.log('pluginConfig', pluginConfig);
216
+ // 1. 先尝试查找本地插件
217
+ const localPluginPath = await checkLocalPlugin(workspaceRoot, pluginName);
218
+ if (localPluginPath) {
219
+ try {
220
+ console.log(`[sloth] 尝试加载本地插件: ${localPluginPath}`);
221
+ const pluginModule = await import(localPluginPath);
222
+ const plugin = {
223
+ name: pluginName,
224
+ ...(pluginModule.default || pluginModule),
225
+ _config: pluginConfig,
226
+ };
227
+ plugins.push(plugin);
228
+ console.log(`[sloth] 本地插件 ${pluginName} 加载成功`);
229
+ loaded = true;
230
+ }
231
+ catch (error) {
232
+ console.error(`[sloth] 本地插件 ${pluginName} 加载失败:`, error);
233
+ }
234
+ }
235
+ // 2. 如果本地插件不存在或加载失败,尝试加载 npm 包
236
+ if (!loaded) {
237
+ // 检查是否已安装
238
+ const isInstalled = await isNpmPackageInstalled(pluginName);
239
+ if (!isInstalled) {
240
+ // 尝试安装
241
+ console.log(`[sloth] 插件 ${pluginName} 未安装,尝试安装...`);
242
+ const installSuccess = await tryInstallNpmPackage(pluginName);
243
+ if (!installSuccess) {
244
+ failedPlugins.push(pluginName);
245
+ continue;
246
+ }
247
+ }
248
+ // 加载 npm 包
249
+ try {
250
+ const pluginModule = await import(pluginName);
251
+ console.log('pluginModule', pluginModule);
252
+ const plugin = {
253
+ name: pluginName,
254
+ ...(pluginModule.default || pluginModule),
255
+ _config: pluginConfig,
256
+ };
257
+ plugins.push(plugin);
258
+ console.log(`[sloth] npm 插件 ${pluginName} 加载成功`);
259
+ loaded = true;
260
+ }
261
+ catch (error) {
262
+ console.error(`[sloth] npm 插件 ${pluginName} 加载失败:`, error);
263
+ failedPlugins.push(pluginName);
264
+ }
265
+ }
266
+ }
267
+ // 如果有插件加载失败,打印错误
268
+ if (failedPlugins.length > 0) {
269
+ console.error(`以下插件加载失败: ${failedPlugins.join(', ')}。` +
270
+ `请确保插件存在于 .sloth/plugin/<pluginName>/index.js 或为有效的 npm 包。`);
271
+ }
272
+ return plugins;
273
+ }
274
+ /**
275
+ * 动态加载所有已安装的全局插件(旧方法,保留兼容)
276
+ * @deprecated 请使用 loadWorkspacePlugins 代替
277
+ */
278
+ export async function loadAllPlugins() {
279
+ const config = await loadGlobalPluginsConfig();
280
+ const plugins = [];
281
+ for (const pluginItem of config.plugins) {
282
+ const { name: packageName, config: pluginConfig } = parsePluginConfigItem(pluginItem);
283
+ try {
284
+ // 动态导入插件模块
285
+ const pluginModule = await import(packageName);
286
+ console.log('pluginModule', pluginModule);
287
+ console.log('pluginConfig', pluginConfig);
288
+ const plugin = {
289
+ name: packageName,
290
+ ...(pluginModule.default || pluginModule),
291
+ _config: pluginConfig,
292
+ };
293
+ plugins.push(plugin);
294
+ console.log(`[sloth] 插件 ${packageName} 加载成功`);
295
+ }
296
+ catch (error) {
297
+ console.error(`[sloth] 插件 ${packageName} 加载失败:`, error);
298
+ }
299
+ }
300
+ return plugins;
301
+ }
302
+ /**
303
+ * 动态加载单个插件
304
+ * @param packageName 插件包名
305
+ */
306
+ export async function loadPlugin(packageName) {
307
+ try {
308
+ const pluginModule = await import(packageName);
309
+ const plugin = {
310
+ name: packageName,
311
+ ...(pluginModule.default || pluginModule),
312
+ };
313
+ console.log(`[sloth] 插件 ${packageName} 加载成功`);
314
+ return plugin;
315
+ }
316
+ catch (error) {
317
+ console.error(`[sloth] 插件 ${packageName} 加载失败:`, error);
318
+ return null;
319
+ }
320
+ }
@@ -0,0 +1,127 @@
1
+ import { loadAllPlugins, loadWorkspacePlugins } from './loader.js';
2
+ /**
3
+ * 插件管理器 - 负责管理生命周期 hooks 并按顺序调用插件
4
+ */
5
+ class PluginManager {
6
+ plugins = [];
7
+ initialized = false;
8
+ workspaceRoot = '';
9
+ /**
10
+ * 设置工作区根目录
11
+ * @param root 工作区根目录
12
+ */
13
+ setWorkspaceRoot(root) {
14
+ this.workspaceRoot = root;
15
+ // 重置初始化状态,以便重新加载工作区插件
16
+ if (this.initialized) {
17
+ this.reset();
18
+ }
19
+ }
20
+ /**
21
+ * 获取工作区根目录
22
+ */
23
+ getWorkspaceRoot() {
24
+ return this.workspaceRoot;
25
+ }
26
+ /**
27
+ * 初始化插件管理器
28
+ * 如果设置了工作区根目录,则加载工作区插件
29
+ * 否则加载全局插件(兼容旧逻辑)
30
+ */
31
+ async initialize() {
32
+ if (this.initialized) {
33
+ return;
34
+ }
35
+ try {
36
+ if (this.workspaceRoot) {
37
+ // 加载工作区声明的插件
38
+ console.log(`[sloth] 从工作区加载插件: ${this.workspaceRoot}`);
39
+ this.plugins = await loadWorkspacePlugins(this.workspaceRoot);
40
+ }
41
+ else {
42
+ // 兼容旧逻辑:加载全局插件
43
+ console.log(`[sloth] 未设置工作区,加载全局插件`);
44
+ this.plugins = await loadAllPlugins();
45
+ }
46
+ this.initialized = true;
47
+ console.log(`[sloth] 插件管理器初始化完成,已加载 ${this.plugins.length} 个插件`);
48
+ }
49
+ catch (error) {
50
+ console.error('[sloth] 插件管理器初始化失败:', error);
51
+ // 如果是工作区模式,抛出错误让调用方知道
52
+ if (this.workspaceRoot) {
53
+ throw error;
54
+ }
55
+ this.plugins = [];
56
+ this.initialized = true;
57
+ }
58
+ }
59
+ /**
60
+ * 获取已加载的插件列表
61
+ */
62
+ getPlugins() {
63
+ return [...this.plugins];
64
+ }
65
+ /**
66
+ * 注册一个插件
67
+ */
68
+ registerPlugin(plugin) {
69
+ this.plugins.push(plugin);
70
+ console.log(`[sloth] 插件 ${plugin.name} 已注册`);
71
+ }
72
+ /**
73
+ * 执行指定的 hook
74
+ * @param hookName hook 名称
75
+ * @param params hook 参数
76
+ * 按顺序调用所有插件,每个插件都会执行,返回最后一个成功的结果
77
+ * 每个插件执行时会收到上一个插件的信息 { lastPlugin, returnVal }
78
+ */
79
+ async executeHook(hookName, params) {
80
+ if (!this.initialized) {
81
+ await this.initialize();
82
+ }
83
+ const pluginsWithHook = this.plugins.filter((p) => typeof p[hookName] === 'function');
84
+ if (pluginsWithHook.length === 0) {
85
+ console.log(`[sloth] 没有插件注册 ${hookName} hook`);
86
+ return params;
87
+ }
88
+ let lastPlugin = '';
89
+ let lastReturnVal = null;
90
+ for (const plugin of pluginsWithHook) {
91
+ try {
92
+ console.log(`[sloth] 执行插件 ${plugin.name} 的 ${hookName} hook`);
93
+ const hookFn = plugin[hookName];
94
+ const context = {
95
+ lastPlugin,
96
+ returnVal: lastReturnVal,
97
+ config: plugin._config,
98
+ };
99
+ const result = await hookFn({
100
+ ...params,
101
+ ...(lastReturnVal || {}),
102
+ _config: context.config,
103
+ }, context);
104
+ // 更新上一个插件信息
105
+ lastPlugin = plugin.name;
106
+ lastReturnVal = {
107
+ ...params,
108
+ ...(lastReturnVal || {}),
109
+ ...(result || {}),
110
+ };
111
+ }
112
+ catch (error) {
113
+ console.error(`[sloth] 插件 ${plugin.name} 执行 ${hookName} 出错:`, error);
114
+ }
115
+ }
116
+ return lastReturnVal || params;
117
+ }
118
+ /**
119
+ * 重置插件管理器
120
+ */
121
+ reset() {
122
+ this.plugins = [];
123
+ this.initialized = false;
124
+ }
125
+ }
126
+ // 导出单例实例
127
+ export const pluginManager = new PluginManager();
@@ -0,0 +1 @@
1
+ export {};