flashclaw 1.0.0

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/config/plugins.json +23 -0
  4. package/dist/agent-runner.d.ts +103 -0
  5. package/dist/agent-runner.d.ts.map +1 -0
  6. package/dist/agent-runner.js +530 -0
  7. package/dist/agent-runner.js.map +1 -0
  8. package/dist/cli.d.ts +7 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +497 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +68 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +252 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/config-schema.d.ts +21 -0
  17. package/dist/config-schema.d.ts.map +1 -0
  18. package/dist/config-schema.js +26 -0
  19. package/dist/config-schema.js.map +1 -0
  20. package/dist/config.d.ts +11 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +36 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/core/api-client.d.ts +236 -0
  25. package/dist/core/api-client.d.ts.map +1 -0
  26. package/dist/core/api-client.js +369 -0
  27. package/dist/core/api-client.js.map +1 -0
  28. package/dist/core/memory.d.ts +291 -0
  29. package/dist/core/memory.d.ts.map +1 -0
  30. package/dist/core/memory.js +754 -0
  31. package/dist/core/memory.js.map +1 -0
  32. package/dist/core/model-capabilities.d.ts +45 -0
  33. package/dist/core/model-capabilities.d.ts.map +1 -0
  34. package/dist/core/model-capabilities.js +85 -0
  35. package/dist/core/model-capabilities.js.map +1 -0
  36. package/dist/db.d.ts +103 -0
  37. package/dist/db.d.ts.map +1 -0
  38. package/dist/db.js +380 -0
  39. package/dist/db.js.map +1 -0
  40. package/dist/errors.d.ts +22 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/errors.js +44 -0
  43. package/dist/errors.js.map +1 -0
  44. package/dist/health.d.ts +27 -0
  45. package/dist/health.d.ts.map +1 -0
  46. package/dist/health.js +55 -0
  47. package/dist/health.js.map +1 -0
  48. package/dist/index.d.ts +11 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +1181 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/logger.d.ts +9 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +19 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/message-queue.d.ts +69 -0
  57. package/dist/message-queue.d.ts.map +1 -0
  58. package/dist/message-queue.js +198 -0
  59. package/dist/message-queue.js.map +1 -0
  60. package/dist/metrics.d.ts +46 -0
  61. package/dist/metrics.d.ts.map +1 -0
  62. package/dist/metrics.js +101 -0
  63. package/dist/metrics.js.map +1 -0
  64. package/dist/paths.d.ts +81 -0
  65. package/dist/paths.d.ts.map +1 -0
  66. package/dist/paths.js +127 -0
  67. package/dist/paths.js.map +1 -0
  68. package/dist/plugins/index.d.ts +9 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +13 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/plugins/installer.d.ts +120 -0
  73. package/dist/plugins/installer.d.ts.map +1 -0
  74. package/dist/plugins/installer.js +1008 -0
  75. package/dist/plugins/installer.js.map +1 -0
  76. package/dist/plugins/loader.d.ts +37 -0
  77. package/dist/plugins/loader.d.ts.map +1 -0
  78. package/dist/plugins/loader.js +429 -0
  79. package/dist/plugins/loader.js.map +1 -0
  80. package/dist/plugins/manager.d.ts +72 -0
  81. package/dist/plugins/manager.d.ts.map +1 -0
  82. package/dist/plugins/manager.js +187 -0
  83. package/dist/plugins/manager.js.map +1 -0
  84. package/dist/plugins/types.d.ts +101 -0
  85. package/dist/plugins/types.d.ts.map +1 -0
  86. package/dist/plugins/types.js +12 -0
  87. package/dist/plugins/types.js.map +1 -0
  88. package/dist/session-tracker.d.ts +81 -0
  89. package/dist/session-tracker.d.ts.map +1 -0
  90. package/dist/session-tracker.js +228 -0
  91. package/dist/session-tracker.js.map +1 -0
  92. package/dist/task-scheduler.d.ts +47 -0
  93. package/dist/task-scheduler.d.ts.map +1 -0
  94. package/dist/task-scheduler.js +331 -0
  95. package/dist/task-scheduler.js.map +1 -0
  96. package/dist/types.d.ts +57 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +2 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/utils/env-substitute.d.ts +63 -0
  101. package/dist/utils/env-substitute.d.ts.map +1 -0
  102. package/dist/utils/env-substitute.js +133 -0
  103. package/dist/utils/env-substitute.js.map +1 -0
  104. package/dist/utils/log-rotate.d.ts +19 -0
  105. package/dist/utils/log-rotate.d.ts.map +1 -0
  106. package/dist/utils/log-rotate.js +85 -0
  107. package/dist/utils/log-rotate.js.map +1 -0
  108. package/dist/utils/rate-limiter.d.ts +38 -0
  109. package/dist/utils/rate-limiter.d.ts.map +1 -0
  110. package/dist/utils/rate-limiter.js +79 -0
  111. package/dist/utils/rate-limiter.js.map +1 -0
  112. package/dist/utils/retry.d.ts +10 -0
  113. package/dist/utils/retry.d.ts.map +1 -0
  114. package/dist/utils/retry.js +47 -0
  115. package/dist/utils/retry.js.map +1 -0
  116. package/dist/utils.d.ts +86 -0
  117. package/dist/utils.d.ts.map +1 -0
  118. package/dist/utils.js +218 -0
  119. package/dist/utils.js.map +1 -0
  120. package/package.json +78 -0
  121. package/plugins/cancel-task/index.ts +161 -0
  122. package/plugins/cancel-task/plugin.json +9 -0
  123. package/plugins/feishu/index.ts +944 -0
  124. package/plugins/feishu/plugin.json +29 -0
  125. package/plugins/list-tasks/index.ts +150 -0
  126. package/plugins/list-tasks/plugin.json +9 -0
  127. package/plugins/memory/index.ts +190 -0
  128. package/plugins/memory/plugin.json +7 -0
  129. package/plugins/pause-task/index.ts +95 -0
  130. package/plugins/pause-task/plugin.json +8 -0
  131. package/plugins/register-group/index.ts +147 -0
  132. package/plugins/register-group/plugin.json +7 -0
  133. package/plugins/resume-task/index.ts +92 -0
  134. package/plugins/resume-task/plugin.json +8 -0
  135. package/plugins/schedule-task/index.ts +248 -0
  136. package/plugins/schedule-task/plugin.json +9 -0
  137. package/plugins/send-message/index.ts +75 -0
  138. package/plugins/send-message/plugin.json +9 -0
@@ -0,0 +1,1008 @@
1
+ /**
2
+ * FlashClaw 插件安装器
3
+ * 支持从 GitHub 下载、安装、卸载和更新插件
4
+ */
5
+ import { promises as fs } from 'fs';
6
+ import { existsSync } from 'fs';
7
+ import { join, basename, dirname, resolve, sep, isAbsolute, normalize, relative } from 'path';
8
+ import { homedir, platform } from 'os';
9
+ import { execFile } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { fileURLToPath } from 'url';
12
+ const execFileAsync = promisify(execFile);
13
+ // ============================================================================
14
+ // 代理支持
15
+ // ============================================================================
16
+ /**
17
+ * 获取代理 URL
18
+ * 支持环境变量: HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy
19
+ */
20
+ function getProxyUrl() {
21
+ return process.env.HTTPS_PROXY ||
22
+ process.env.HTTP_PROXY ||
23
+ process.env.https_proxy ||
24
+ process.env.http_proxy ||
25
+ null;
26
+ }
27
+ /**
28
+ * 使用系统命令下载文件(自动处理代理和重定向)
29
+ * Windows 使用 PowerShell,Unix 使用 curl
30
+ */
31
+ async function downloadFile(url, destPath) {
32
+ const isWindows = platform() === 'win32';
33
+ const proxyUrl = getProxyUrl();
34
+ if (proxyUrl) {
35
+ log.debug(`使用代理: ${proxyUrl}`);
36
+ }
37
+ else {
38
+ log.debug('直接连接(无代理)');
39
+ }
40
+ try {
41
+ if (isWindows) {
42
+ const script = '& { param([string]$Url,[string]$OutFile,[string]$Proxy) ' +
43
+ 'if ($Proxy) { $proxyObj = New-Object System.Net.WebProxy($Proxy); [System.Net.WebRequest]::DefaultWebProxy = $proxyObj } ' +
44
+ 'Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing }';
45
+ await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script, url, destPath, proxyUrl ?? ''], { timeout: 120000, windowsHide: true });
46
+ }
47
+ else {
48
+ const args = ['-fL', '-o', destPath];
49
+ if (proxyUrl) {
50
+ args.push('-x', proxyUrl);
51
+ }
52
+ args.push(url);
53
+ await execFileAsync('curl', args, { timeout: 120000 });
54
+ }
55
+ }
56
+ catch (err) {
57
+ throw new Error(`下载失败: ${err instanceof Error ? err.message : String(err)}`);
58
+ }
59
+ }
60
+ /**
61
+ * 带代理支持的 fetch(用于小文件如 JSON)
62
+ * 自动检测环境变量中的代理设置
63
+ */
64
+ async function fetchWithProxy(url) {
65
+ const proxyUrl = getProxyUrl();
66
+ const isWindows = platform() === 'win32';
67
+ if (!proxyUrl) {
68
+ log.debug('直接连接(无代理)');
69
+ const response = await fetch(url);
70
+ return response;
71
+ }
72
+ log.debug(`使用代理: ${proxyUrl}`);
73
+ // 使用系统命令获取内容
74
+ try {
75
+ let content;
76
+ if (isWindows) {
77
+ const script = '& { param([string]$Url,[string]$Proxy) ' +
78
+ '$proxyObj = New-Object System.Net.WebProxy($Proxy); ' +
79
+ '[System.Net.WebRequest]::DefaultWebProxy = $proxyObj; ' +
80
+ '(Invoke-WebRequest -Uri $Url -UseBasicParsing).Content }';
81
+ const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script, url, proxyUrl], { timeout: 30000, windowsHide: true });
82
+ content = stdout ?? '';
83
+ }
84
+ else {
85
+ const args = ['-fsSL', '-x', proxyUrl, url];
86
+ const { stdout } = await execFileAsync('curl', args, { timeout: 30000 });
87
+ content = stdout ?? '';
88
+ }
89
+ return {
90
+ ok: true,
91
+ status: 200,
92
+ statusText: 'OK',
93
+ json: async () => JSON.parse(content),
94
+ text: async () => content,
95
+ };
96
+ }
97
+ catch (err) {
98
+ // 代理失败时回退到直接连接
99
+ log.warn(`代理请求失败,尝试直接连接...`);
100
+ const response = await fetch(url);
101
+ return response;
102
+ }
103
+ }
104
+ // ============================================================================
105
+ // 输入校验
106
+ // ============================================================================
107
+ const PLUGIN_NAME_PATTERN = /^[a-z0-9][a-z0-9-_]{0,63}$/;
108
+ function isValidPluginName(name) {
109
+ return PLUGIN_NAME_PATTERN.test(name);
110
+ }
111
+ function resolvePluginDir(baseDir, name) {
112
+ if (!isValidPluginName(name))
113
+ return null;
114
+ const base = resolve(baseDir);
115
+ const target = resolve(baseDir, name);
116
+ const relativePath = relative(base, target);
117
+ if (relativePath.startsWith('..') || isAbsolute(relativePath))
118
+ return null;
119
+ return target;
120
+ }
121
+ function isSafeRelativePath(value) {
122
+ if (!value || typeof value !== 'string')
123
+ return false;
124
+ if (isAbsolute(value))
125
+ return false;
126
+ const segments = value.split(/[\\/]+/);
127
+ if (segments.some((segment) => segment === '..')) {
128
+ return false;
129
+ }
130
+ const normalized = normalize(value);
131
+ if (normalized === '..' || normalized.startsWith(`..${sep}`) || normalized.includes(`${sep}..${sep}`)) {
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+ // ES 模块中获取 __dirname
137
+ const __filename = fileURLToPath(import.meta.url);
138
+ const __dirname = dirname(__filename);
139
+ // ============================================================================
140
+ // 颜色输出工具
141
+ // ============================================================================
142
+ const colors = {
143
+ reset: '\x1b[0m',
144
+ bright: '\x1b[1m',
145
+ dim: '\x1b[2m',
146
+ red: '\x1b[31m',
147
+ green: '\x1b[32m',
148
+ yellow: '\x1b[33m',
149
+ blue: '\x1b[34m',
150
+ magenta: '\x1b[35m',
151
+ cyan: '\x1b[36m',
152
+ white: '\x1b[37m',
153
+ };
154
+ const log = {
155
+ info: (msg) => console.log(`${colors.cyan}[INFO]${colors.reset} ${msg}`),
156
+ success: (msg) => console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`),
157
+ warn: (msg) => console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`),
158
+ error: (msg) => console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`),
159
+ debug: (msg) => console.log(`${colors.dim}[DEBUG]${colors.reset} ${msg}`),
160
+ step: (msg) => console.log(`${colors.magenta}[STEP]${colors.reset} ${msg}`),
161
+ };
162
+ function validateRegistry(data) {
163
+ if (!data || typeof data !== 'object') {
164
+ throw new Error('注册表格式错误');
165
+ }
166
+ const registry = data;
167
+ if (!registry.plugins || typeof registry.plugins !== 'object') {
168
+ throw new Error('注册表缺少 plugins 字段');
169
+ }
170
+ for (const [key, plugin] of Object.entries(registry.plugins)) {
171
+ if (!plugin || typeof plugin !== 'object') {
172
+ throw new Error(`插件定义无效: ${key}`);
173
+ }
174
+ const p = plugin;
175
+ if (!p.name || !p.description || !p.type || !p.version || !p.author) {
176
+ throw new Error(`插件字段不完整: ${key}`);
177
+ }
178
+ if (!Array.isArray(p.tags)) {
179
+ throw new Error(`插件 tags 必须为数组: ${key}`);
180
+ }
181
+ }
182
+ return registry;
183
+ }
184
+ // ============================================================================
185
+ // 路径工具
186
+ // ============================================================================
187
+ /**
188
+ * 获取 FlashClaw 主目录
189
+ */
190
+ function getFlashClawHome() {
191
+ return process.env.FLASHCLAW_HOME || join(homedir(), '.flashclaw');
192
+ }
193
+ /**
194
+ * 获取用户插件安装目录
195
+ */
196
+ export function getUserPluginsDir() {
197
+ return join(getFlashClawHome(), 'plugins');
198
+ }
199
+ /**
200
+ * 获取本地注册表路径(项目内置)
201
+ */
202
+ function getLocalRegistryPath() {
203
+ // 相对于此文件的路径
204
+ return join(__dirname, '..', '..', 'plugins', 'registry.json');
205
+ }
206
+ /**
207
+ * 获取缓存的远程注册表路径
208
+ */
209
+ function getCachedRegistryPath() {
210
+ return join(getFlashClawHome(), 'cache', 'registry.json');
211
+ }
212
+ /**
213
+ * 获取已安装插件元数据文件路径
214
+ */
215
+ function getInstalledMetaPath() {
216
+ return join(getFlashClawHome(), 'plugins', '.installed.json');
217
+ }
218
+ /**
219
+ * 确保目录存在
220
+ */
221
+ async function ensureDir(dir) {
222
+ if (!existsSync(dir)) {
223
+ await fs.mkdir(dir, { recursive: true });
224
+ }
225
+ }
226
+ // ============================================================================
227
+ // 注册表管理
228
+ // ============================================================================
229
+ // GitHub API 地址
230
+ const GITHUB_API_BASE = 'https://api.github.com/repos';
231
+ const OFFICIAL_REPO = 'GuLu9527/flashclaw';
232
+ const COMMUNITY_PLUGINS_PATH = 'community-plugins';
233
+ /**
234
+ * 从 GitHub API 获取可用插件列表
235
+ * 直接读取 community-plugins 目录结构
236
+ */
237
+ async function fetchAvailablePluginsFromGitHub() {
238
+ const apiUrl = `${GITHUB_API_BASE}/${OFFICIAL_REPO}/contents/${COMMUNITY_PLUGINS_PATH}`;
239
+ log.debug(`获取插件列表: ${apiUrl}`);
240
+ const response = await fetchWithProxy(apiUrl);
241
+ if (!response.ok) {
242
+ throw new Error(`GitHub API 请求失败: HTTP ${response.status}`);
243
+ }
244
+ const items = await response.json();
245
+ const plugins = [];
246
+ // 过滤出目录(每个目录就是一个插件)
247
+ const pluginDirs = items.filter(item => item.type === 'dir');
248
+ // 并行获取每个插件的 plugin.json
249
+ const pluginPromises = pluginDirs.map(async (dir) => {
250
+ try {
251
+ const pluginJsonUrl = `https://raw.githubusercontent.com/${OFFICIAL_REPO}/main/${COMMUNITY_PLUGINS_PATH}/${dir.name}/plugin.json`;
252
+ const pluginResponse = await fetchWithProxy(pluginJsonUrl);
253
+ if (pluginResponse.ok) {
254
+ const manifest = await pluginResponse.json();
255
+ return {
256
+ name: manifest.name || dir.name,
257
+ description: manifest.description || '无描述',
258
+ type: manifest.type || 'tool',
259
+ version: manifest.version || '1.0.0',
260
+ author: manifest.author || 'unknown',
261
+ tags: manifest.tags || [],
262
+ };
263
+ }
264
+ }
265
+ catch (err) {
266
+ log.debug(`获取插件 ${dir.name} 信息失败: ${err instanceof Error ? err.message : String(err)}`);
267
+ }
268
+ return null;
269
+ });
270
+ const results = await Promise.all(pluginPromises);
271
+ for (const plugin of results) {
272
+ if (plugin) {
273
+ plugins.push(plugin);
274
+ }
275
+ }
276
+ return plugins;
277
+ }
278
+ /**
279
+ * 获取可用插件缓存路径
280
+ */
281
+ function getPluginsCachePath() {
282
+ return join(getFlashClawHome(), 'cache', 'available-plugins.json');
283
+ }
284
+ /**
285
+ * 缓存可用插件列表
286
+ */
287
+ async function cacheAvailablePlugins(plugins) {
288
+ const cacheDir = join(getFlashClawHome(), 'cache');
289
+ await ensureDir(cacheDir);
290
+ const cachePath = getPluginsCachePath();
291
+ const cacheData = {
292
+ updated: new Date().toISOString(),
293
+ plugins,
294
+ };
295
+ await fs.writeFile(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
296
+ log.debug(`插件列表已缓存到 ${cachePath}`);
297
+ }
298
+ /**
299
+ * 读取缓存的插件列表
300
+ */
301
+ async function readCachedPlugins() {
302
+ const cachePath = getPluginsCachePath();
303
+ try {
304
+ if (existsSync(cachePath)) {
305
+ const content = await fs.readFile(cachePath, 'utf-8');
306
+ const data = JSON.parse(content);
307
+ // 检查缓存是否过期(1小时)
308
+ const updated = new Date(data.updated);
309
+ const now = new Date();
310
+ const hoursDiff = (now.getTime() - updated.getTime()) / (1000 * 60 * 60);
311
+ if (hoursDiff < 1 && Array.isArray(data.plugins)) {
312
+ log.debug('使用缓存的插件列表');
313
+ return data.plugins;
314
+ }
315
+ }
316
+ }
317
+ catch {
318
+ // 忽略缓存读取错误
319
+ }
320
+ return null;
321
+ }
322
+ /**
323
+ * 获取插件注册表(兼容旧接口)
324
+ * 现在从 GitHub API 动态获取
325
+ */
326
+ export async function getRegistry(forceRemote = false) {
327
+ // 获取可用插件列表
328
+ let plugins;
329
+ if (!forceRemote) {
330
+ const cached = await readCachedPlugins();
331
+ if (cached) {
332
+ plugins = cached;
333
+ }
334
+ else {
335
+ plugins = await fetchAvailablePluginsFromGitHub();
336
+ await cacheAvailablePlugins(plugins);
337
+ }
338
+ }
339
+ else {
340
+ log.info('正在从 GitHub 获取最新插件列表...');
341
+ plugins = await fetchAvailablePluginsFromGitHub();
342
+ await cacheAvailablePlugins(plugins);
343
+ log.success('插件列表获取成功');
344
+ }
345
+ // 转换为 Registry 格式
346
+ const pluginsMap = {};
347
+ for (const plugin of plugins) {
348
+ pluginsMap[plugin.name] = plugin;
349
+ }
350
+ return {
351
+ version: new Date().toISOString().split('T')[0],
352
+ updated: new Date().toISOString(),
353
+ officialRepo: OFFICIAL_REPO,
354
+ officialPluginsPath: COMMUNITY_PLUGINS_PATH,
355
+ plugins: pluginsMap,
356
+ };
357
+ }
358
+ /**
359
+ * 更新注册表(从远程)
360
+ */
361
+ export async function updateRegistry() {
362
+ return getRegistry(true);
363
+ }
364
+ // ============================================================================
365
+ // 插件下载
366
+ // ============================================================================
367
+ /**
368
+ * 从 GitHub 下载插件
369
+ * 下载仓库的 ZIP 文件并解压
370
+ *
371
+ * @param repo GitHub 仓库路径 (如 "owner/repo")
372
+ * @param targetDir 目标目录
373
+ * @param branch 分支名 (默认 "main")
374
+ */
375
+ export async function downloadPlugin(repo, targetDir, branch = 'main') {
376
+ const repoPattern = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
377
+ if (!repoPattern.test(repo)) {
378
+ throw new Error(`仓库格式不合法: ${repo}`);
379
+ }
380
+ const branchPattern = /^[a-zA-Z0-9._/-]+$/;
381
+ if (!branchPattern.test(branch) || branch.includes('..')) {
382
+ throw new Error(`分支名不合法: ${branch}`);
383
+ }
384
+ const zipUrl = `https://github.com/${repo}/archive/refs/heads/${branch}.zip`;
385
+ const tempDir = join(getFlashClawHome(), 'temp');
386
+ const tempZip = join(tempDir, `${repo.replace('/', '-')}-${branch}.zip`);
387
+ const extractDir = join(tempDir, 'extract');
388
+ log.step(`下载插件: ${repo}`);
389
+ log.debug(`ZIP URL: ${zipUrl}`);
390
+ // 确保临时目录存在
391
+ await ensureDir(tempDir);
392
+ try {
393
+ // 下载 ZIP 文件(使用系统命令,自动处理代理和重定向)
394
+ log.info('正在下载 ZIP 文件...');
395
+ await downloadFile(zipUrl, tempZip);
396
+ log.debug(`ZIP 文件已保存: ${tempZip}`);
397
+ // 确保目标目录存在
398
+ await ensureDir(targetDir);
399
+ // 清理并创建解压目录
400
+ if (existsSync(extractDir)) {
401
+ await fs.rm(extractDir, { recursive: true, force: true });
402
+ }
403
+ await ensureDir(extractDir);
404
+ // 解压 ZIP 文件 - 使用系统命令
405
+ log.info('正在解压...');
406
+ await extractZip(tempZip, extractDir);
407
+ // 找到解压后的目录(GitHub ZIP 包含一个以 repo-branch 命名的根目录)
408
+ const entries = await fs.readdir(extractDir, { withFileTypes: true });
409
+ const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
410
+ if (directories.length === 0) {
411
+ throw new Error('解压后找不到插件目录');
412
+ }
413
+ const repoName = basename(repo);
414
+ const extractedFolder = directories.find((dir) => dir.toLowerCase().startsWith(repoName.toLowerCase()) ||
415
+ dir.includes(repoName)) || directories[0];
416
+ const sourcePath = join(extractDir, extractedFolder);
417
+ // 复制文件到目标目录
418
+ log.info('正在安装文件...');
419
+ await copyDir(sourcePath, targetDir);
420
+ log.success(`插件已下载到: ${targetDir}`);
421
+ }
422
+ finally {
423
+ // 清理临时文件
424
+ try {
425
+ if (existsSync(tempZip)) {
426
+ await fs.unlink(tempZip);
427
+ }
428
+ if (existsSync(extractDir)) {
429
+ await fs.rm(extractDir, { recursive: true, force: true });
430
+ }
431
+ }
432
+ catch {
433
+ // 忽略清理错误
434
+ }
435
+ }
436
+ }
437
+ /**
438
+ * 解压 ZIP 文件
439
+ * Windows 使用 PowerShell,Unix 使用 unzip
440
+ */
441
+ async function extractZip(zipPath, destDir) {
442
+ const isWindows = platform() === 'win32';
443
+ try {
444
+ if (isWindows) {
445
+ const script = '& { param([string]$Zip,[string]$Dest) Expand-Archive -Path $Zip -DestinationPath $Dest -Force }';
446
+ await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script, zipPath, destDir], { timeout: 120000, windowsHide: true });
447
+ }
448
+ else {
449
+ await execFileAsync('unzip', ['-o', zipPath, '-d', destDir], { timeout: 120000 });
450
+ }
451
+ }
452
+ catch (err) {
453
+ // 如果系统命令失败,尝试使用 tar 命令(Windows 10+ 支持)
454
+ try {
455
+ await execFileAsync('tar', ['-xf', zipPath, '-C', destDir], { timeout: 120000 });
456
+ }
457
+ catch {
458
+ throw new Error(`解压失败: ${err instanceof Error ? err.message : String(err)}`);
459
+ }
460
+ }
461
+ }
462
+ /**
463
+ * 递归复制目录
464
+ */
465
+ async function copyDir(src, dest) {
466
+ await ensureDir(dest);
467
+ const entries = await fs.readdir(src, { withFileTypes: true });
468
+ for (const entry of entries) {
469
+ const srcPath = join(src, entry.name);
470
+ const destPath = join(dest, entry.name);
471
+ if (entry.isDirectory()) {
472
+ await copyDir(srcPath, destPath);
473
+ }
474
+ else {
475
+ await fs.copyFile(srcPath, destPath);
476
+ }
477
+ }
478
+ }
479
+ // ============================================================================
480
+ // 插件验证
481
+ // ============================================================================
482
+ /**
483
+ * 验证插件结构
484
+ * 检查必要文件是否存在:plugin.json 或 package.json, index.ts 或 index.js
485
+ *
486
+ * @param pluginDir 插件目录
487
+ * @returns 验证结果
488
+ */
489
+ export async function validatePlugin(pluginDir) {
490
+ const errors = [];
491
+ let manifest = null;
492
+ // 检查 plugin.json 或 package.json
493
+ const pluginJsonPath = join(pluginDir, 'plugin.json');
494
+ const packageJsonPath = join(pluginDir, 'package.json');
495
+ if (existsSync(pluginJsonPath)) {
496
+ try {
497
+ const content = await fs.readFile(pluginJsonPath, 'utf-8');
498
+ manifest = JSON.parse(content);
499
+ log.debug('找到 plugin.json');
500
+ // 验证必要字段
501
+ if (!manifest.name)
502
+ errors.push('plugin.json 缺少 name 字段');
503
+ if (!manifest.version)
504
+ errors.push('plugin.json 缺少 version 字段');
505
+ if (!manifest.main)
506
+ errors.push('plugin.json 缺少 main 字段');
507
+ if (!manifest.type)
508
+ errors.push('plugin.json 缺少 type 字段');
509
+ }
510
+ catch (err) {
511
+ errors.push(`plugin.json 解析失败: ${err instanceof Error ? err.message : String(err)}`);
512
+ }
513
+ }
514
+ else if (existsSync(packageJsonPath)) {
515
+ try {
516
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
517
+ manifest = JSON.parse(content);
518
+ log.debug('找到 package.json');
519
+ // 验证必要字段
520
+ if (!manifest.name)
521
+ errors.push('package.json 缺少 name 字段');
522
+ if (!manifest.version)
523
+ errors.push('package.json 缺少 version 字段');
524
+ }
525
+ catch (err) {
526
+ errors.push(`package.json 解析失败: ${err instanceof Error ? err.message : String(err)}`);
527
+ }
528
+ }
529
+ else {
530
+ errors.push('缺少 plugin.json 或 package.json');
531
+ }
532
+ // 检查入口文件
533
+ const indexTs = join(pluginDir, 'index.ts');
534
+ const indexJs = join(pluginDir, 'index.js');
535
+ const srcIndexTs = join(pluginDir, 'src', 'index.ts');
536
+ const srcIndexJs = join(pluginDir, 'src', 'index.js');
537
+ const hasEntryFile = existsSync(indexTs) ||
538
+ existsSync(indexJs) ||
539
+ existsSync(srcIndexTs) ||
540
+ existsSync(srcIndexJs);
541
+ if (!hasEntryFile && manifest?.main) {
542
+ if (!isSafeRelativePath(manifest.main)) {
543
+ errors.push(`入口文件路径不安全: ${manifest.main}`);
544
+ }
545
+ else {
546
+ // 检查 manifest 中指定的 main 文件
547
+ const mainPath = join(pluginDir, manifest.main);
548
+ const mainTsPath = mainPath.replace(/\.js$/, '.ts');
549
+ if (!existsSync(mainPath) && !existsSync(mainTsPath)) {
550
+ errors.push(`入口文件不存在: ${manifest.main}`);
551
+ }
552
+ }
553
+ }
554
+ else if (!hasEntryFile && !manifest?.main) {
555
+ errors.push('缺少入口文件 (index.ts/index.js)');
556
+ }
557
+ return {
558
+ valid: errors.length === 0,
559
+ errors,
560
+ manifest: manifest ? {
561
+ name: manifest.name,
562
+ version: manifest.version,
563
+ description: manifest.description,
564
+ type: manifest.type,
565
+ } : undefined,
566
+ };
567
+ }
568
+ // ============================================================================
569
+ // 已安装插件管理
570
+ // ============================================================================
571
+ /**
572
+ * 读取已安装插件元数据
573
+ */
574
+ async function readInstalledMeta() {
575
+ const metaPath = getInstalledMetaPath();
576
+ try {
577
+ if (existsSync(metaPath)) {
578
+ const content = await fs.readFile(metaPath, 'utf-8');
579
+ return JSON.parse(content);
580
+ }
581
+ }
582
+ catch {
583
+ // 忽略读取错误
584
+ }
585
+ return {};
586
+ }
587
+ /**
588
+ * 保存已安装插件元数据
589
+ */
590
+ async function saveInstalledMeta(meta) {
591
+ const metaPath = getInstalledMetaPath();
592
+ const dir = join(getFlashClawHome(), 'plugins');
593
+ await ensureDir(dir);
594
+ await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
595
+ }
596
+ // ============================================================================
597
+ // 主要功能
598
+ // ============================================================================
599
+ /**
600
+ * 从官方仓库安装插件(只提取特定目录)
601
+ */
602
+ async function downloadOfficialPlugin(registry, pluginName, targetDir) {
603
+ const repo = registry.officialRepo || 'GuLu9527/flashclaw';
604
+ const pluginsPath = registry.officialPluginsPath || 'community-plugins';
605
+ if (!isSafeRelativePath(pluginsPath)) {
606
+ throw new Error(`官方插件目录路径不安全: ${pluginsPath}`);
607
+ }
608
+ log.step(`从官方仓库安装: ${repo}/${pluginsPath}/${pluginName}`);
609
+ const tempDir = join(getFlashClawHome(), 'temp');
610
+ const tempZip = join(tempDir, `official-repo.zip`);
611
+ const extractDir = join(tempDir, 'extract-official');
612
+ await ensureDir(tempDir);
613
+ try {
614
+ // 下载主仓库 ZIP
615
+ const zipUrl = `https://github.com/${repo}/archive/refs/heads/main.zip`;
616
+ log.debug(`ZIP URL: ${zipUrl}`);
617
+ log.info('正在下载官方仓库...');
618
+ await downloadFile(zipUrl, tempZip);
619
+ // 解压
620
+ if (existsSync(extractDir)) {
621
+ await fs.rm(extractDir, { recursive: true, force: true });
622
+ }
623
+ await ensureDir(extractDir);
624
+ log.info('正在解压...');
625
+ await extractZip(tempZip, extractDir);
626
+ // 找到解压后的根目录
627
+ const entries = await fs.readdir(extractDir, { withFileTypes: true });
628
+ const rootEntry = entries.find((entry) => entry.isDirectory());
629
+ if (!rootEntry) {
630
+ throw new Error('解压后找不到仓库根目录');
631
+ }
632
+ const repoRoot = rootEntry.name; // 通常是 flashclaw-main
633
+ // 定位插件目录
634
+ const pluginsRoot = resolve(extractDir, repoRoot, pluginsPath);
635
+ const pluginSourceDir = resolve(pluginsRoot, pluginName);
636
+ if (!pluginSourceDir.startsWith(pluginsRoot + sep)) {
637
+ throw new Error(`插件路径不安全: ${pluginsPath}/${pluginName}`);
638
+ }
639
+ if (!existsSync(pluginSourceDir)) {
640
+ throw new Error(`插件 "${pluginName}" 在官方仓库中不存在 (路径: ${pluginsPath}/${pluginName})`);
641
+ }
642
+ // 复制插件到目标目录
643
+ log.info('正在安装插件...');
644
+ await ensureDir(targetDir);
645
+ await copyDir(pluginSourceDir, targetDir);
646
+ log.success(`插件已安装到: ${targetDir}`);
647
+ }
648
+ finally {
649
+ // 清理
650
+ try {
651
+ if (existsSync(tempZip))
652
+ await fs.unlink(tempZip);
653
+ if (existsSync(extractDir))
654
+ await fs.rm(extractDir, { recursive: true, force: true });
655
+ }
656
+ catch { /* ignore */ }
657
+ }
658
+ }
659
+ /**
660
+ * 安装插件
661
+ * 直接从 community-plugins 目录下载,无需预先检查注册表
662
+ *
663
+ * @param name 插件名称
664
+ * @returns 是否安装成功
665
+ */
666
+ export async function installPlugin(name) {
667
+ log.info(`准备安装插件: ${name}`);
668
+ const pluginsDir = getUserPluginsDir();
669
+ await ensureDir(pluginsDir);
670
+ if (!isValidPluginName(name)) {
671
+ log.error(`插件名称不合法: ${name}`);
672
+ log.info('插件名称只能包含小写字母、数字、- 或 _');
673
+ return false;
674
+ }
675
+ const targetDir = resolvePluginDir(pluginsDir, name);
676
+ if (!targetDir) {
677
+ log.error(`插件名称不合法: ${name}`);
678
+ return false;
679
+ }
680
+ // 检查是否已安装
681
+ if (existsSync(targetDir)) {
682
+ log.warn(`插件 "${name}" 已安装,将覆盖安装`);
683
+ await fs.rm(targetDir, { recursive: true, force: true });
684
+ }
685
+ // 构建默认 Registry 结构用于下载
686
+ const registry = {
687
+ version: '1.0.0',
688
+ updated: new Date().toISOString(),
689
+ officialRepo: OFFICIAL_REPO,
690
+ officialPluginsPath: COMMUNITY_PLUGINS_PATH,
691
+ plugins: {},
692
+ };
693
+ try {
694
+ // 从官方仓库安装(直接尝试下载,如果不存在会报错)
695
+ await downloadOfficialPlugin(registry, name, targetDir);
696
+ // 验证插件
697
+ log.step('验证插件结构...');
698
+ const validation = await validatePlugin(targetDir);
699
+ if (!validation.valid) {
700
+ log.error('插件验证失败:');
701
+ validation.errors.forEach(e => log.error(` - ${e}`));
702
+ // 回滚:删除已下载的文件
703
+ await fs.rm(targetDir, { recursive: true, force: true });
704
+ return false;
705
+ }
706
+ // 保存安装元数据
707
+ const meta = await readInstalledMeta();
708
+ meta[name] = {
709
+ name: name,
710
+ version: validation.manifest?.version || '1.0.0',
711
+ installedAt: new Date().toISOString(),
712
+ source: 'registry',
713
+ repo: `${OFFICIAL_REPO}/${COMMUNITY_PLUGINS_PATH}/${name}`,
714
+ };
715
+ await saveInstalledMeta(meta);
716
+ log.success(`插件 "${name}" 安装成功!`);
717
+ log.info(`安装位置: ${targetDir}`);
718
+ log.info('提示: 重启 FlashClaw 以加载新插件');
719
+ return true;
720
+ }
721
+ catch (err) {
722
+ log.error(`安装失败: ${err instanceof Error ? err.message : String(err)}`);
723
+ // 回滚:删除已下载的文件
724
+ try {
725
+ if (existsSync(targetDir)) {
726
+ await fs.rm(targetDir, { recursive: true, force: true });
727
+ }
728
+ }
729
+ catch {
730
+ // 忽略清理错误
731
+ }
732
+ return false;
733
+ }
734
+ }
735
+ /**
736
+ * 卸载插件
737
+ *
738
+ * @param name 插件名称
739
+ * @returns 是否卸载成功
740
+ */
741
+ export async function uninstallPlugin(name) {
742
+ log.info(`准备卸载插件: ${name}`);
743
+ const pluginsDir = getUserPluginsDir();
744
+ const targetDir = resolvePluginDir(pluginsDir, name);
745
+ if (!targetDir) {
746
+ log.error(`插件名称不合法: ${name}`);
747
+ return false;
748
+ }
749
+ if (!existsSync(targetDir)) {
750
+ log.error(`插件 "${name}" 未安装`);
751
+ return false;
752
+ }
753
+ try {
754
+ // 删除插件目录
755
+ await fs.rm(targetDir, { recursive: true, force: true });
756
+ // 更新安装元数据
757
+ const meta = await readInstalledMeta();
758
+ delete meta[name];
759
+ await saveInstalledMeta(meta);
760
+ log.success(`插件 "${name}" 已卸载`);
761
+ log.info('提示: 重启 FlashClaw 以应用更改');
762
+ return true;
763
+ }
764
+ catch (err) {
765
+ log.error(`卸载失败: ${err instanceof Error ? err.message : String(err)}`);
766
+ return false;
767
+ }
768
+ }
769
+ /**
770
+ * 列出已安装插件
771
+ *
772
+ * @returns 已安装插件列表
773
+ */
774
+ export async function listInstalledPlugins() {
775
+ const pluginsDir = getUserPluginsDir();
776
+ if (!existsSync(pluginsDir)) {
777
+ return [];
778
+ }
779
+ const installed = [];
780
+ const meta = await readInstalledMeta();
781
+ // 扫描插件目录
782
+ const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
783
+ for (const entry of entries) {
784
+ if (!entry.isDirectory())
785
+ continue;
786
+ if (entry.name.startsWith('.'))
787
+ continue;
788
+ const pluginDir = join(pluginsDir, entry.name);
789
+ const validation = await validatePlugin(pluginDir);
790
+ if (validation.valid && validation.manifest) {
791
+ const savedMeta = meta[entry.name];
792
+ installed.push({
793
+ name: validation.manifest.name,
794
+ version: validation.manifest.version,
795
+ installedAt: savedMeta?.installedAt || 'unknown',
796
+ source: savedMeta?.source || 'local',
797
+ repo: savedMeta?.repo,
798
+ });
799
+ }
800
+ }
801
+ return installed;
802
+ }
803
+ /**
804
+ * 列出可用插件(注册表中的插件)
805
+ *
806
+ * @returns 可用插件列表
807
+ */
808
+ export async function listAvailablePlugins() {
809
+ try {
810
+ const registry = await getRegistry();
811
+ return Object.values(registry.plugins);
812
+ }
813
+ catch (err) {
814
+ log.error(`获取注册表失败: ${err instanceof Error ? err.message : String(err)}`);
815
+ return [];
816
+ }
817
+ }
818
+ /**
819
+ * 更新插件
820
+ *
821
+ * @param name 插件名称
822
+ * @returns 是否更新成功
823
+ */
824
+ export async function updatePlugin(name) {
825
+ log.info(`准备更新插件: ${name}`);
826
+ const pluginsDir = getUserPluginsDir();
827
+ const targetDir = resolvePluginDir(pluginsDir, name);
828
+ if (!targetDir) {
829
+ log.error(`插件名称不合法: ${name}`);
830
+ return false;
831
+ }
832
+ if (!existsSync(targetDir)) {
833
+ log.error(`插件 "${name}" 未安装`);
834
+ return false;
835
+ }
836
+ const meta = await readInstalledMeta();
837
+ // 构建默认 Registry 结构用于下载
838
+ const registry = {
839
+ version: '1.0.0',
840
+ updated: new Date().toISOString(),
841
+ officialRepo: OFFICIAL_REPO,
842
+ officialPluginsPath: COMMUNITY_PLUGINS_PATH,
843
+ plugins: {},
844
+ };
845
+ const tempInstallDir = join(getFlashClawHome(), 'temp', `update-${name}-${Date.now()}`);
846
+ const backupDir = join(getFlashClawHome(), 'backup', `${name}-${Date.now()}`);
847
+ try {
848
+ // 下载新版本到临时目录
849
+ await downloadOfficialPlugin(registry, name, tempInstallDir);
850
+ // 验证新版本
851
+ const validation = await validatePlugin(tempInstallDir);
852
+ if (!validation.valid) {
853
+ log.error('新版本验证失败:');
854
+ validation.errors.forEach((e) => log.error(` - ${e}`));
855
+ await fs.rm(tempInstallDir, { recursive: true, force: true });
856
+ return false;
857
+ }
858
+ // 备份当前版本
859
+ log.step('备份当前版本...');
860
+ await ensureDir(backupDir);
861
+ await copyDir(targetDir, backupDir);
862
+ // 替换旧版本
863
+ await fs.rm(targetDir, { recursive: true, force: true });
864
+ try {
865
+ await fs.rename(tempInstallDir, targetDir);
866
+ }
867
+ catch {
868
+ await copyDir(tempInstallDir, targetDir);
869
+ await fs.rm(tempInstallDir, { recursive: true, force: true });
870
+ }
871
+ // 更新元数据
872
+ meta[name] = {
873
+ name,
874
+ version: validation.manifest?.version || '1.0.0',
875
+ installedAt: new Date().toISOString(),
876
+ source: 'registry',
877
+ repo: `${OFFICIAL_REPO}/${COMMUNITY_PLUGINS_PATH}/${name}`,
878
+ };
879
+ await saveInstalledMeta(meta);
880
+ // 清理备份
881
+ await fs.rm(backupDir, { recursive: true, force: true });
882
+ log.success(`插件 "${name}" 更新成功!`);
883
+ log.info(`新版本: ${validation.manifest?.version || 'unknown'}`);
884
+ log.info('提示: 重启 FlashClaw 以应用更新');
885
+ return true;
886
+ }
887
+ catch (err) {
888
+ log.error(`更新失败: ${err instanceof Error ? err.message : String(err)}`);
889
+ // 尝试回滚
890
+ try {
891
+ if (existsSync(backupDir)) {
892
+ if (existsSync(targetDir)) {
893
+ await fs.rm(targetDir, { recursive: true, force: true });
894
+ }
895
+ await copyDir(backupDir, targetDir);
896
+ await fs.rm(backupDir, { recursive: true, force: true });
897
+ log.info('已回滚到之前版本');
898
+ }
899
+ }
900
+ catch {
901
+ log.error('回滚失败,请手动恢复');
902
+ }
903
+ // 清理临时目录
904
+ try {
905
+ if (existsSync(tempInstallDir)) {
906
+ await fs.rm(tempInstallDir, { recursive: true, force: true });
907
+ }
908
+ }
909
+ catch {
910
+ // ignore
911
+ }
912
+ return false;
913
+ }
914
+ }
915
+ /**
916
+ * 显示插件详情
917
+ *
918
+ * @param name 插件名称
919
+ */
920
+ export async function showPluginInfo(name) {
921
+ // 检查注册表
922
+ try {
923
+ const registry = await getRegistry();
924
+ const pluginInfo = registry.plugins[name];
925
+ if (pluginInfo) {
926
+ console.log('');
927
+ console.log(`${colors.bright}${colors.cyan}插件信息${colors.reset}`);
928
+ console.log(`${colors.bright}名称:${colors.reset} ${pluginInfo.name}`);
929
+ console.log(`${colors.bright}版本:${colors.reset} ${pluginInfo.version}`);
930
+ console.log(`${colors.bright}作者:${colors.reset} ${pluginInfo.author}`);
931
+ console.log(`${colors.bright}类型:${colors.reset} ${pluginInfo.type}`);
932
+ console.log(`${colors.bright}描述:${colors.reset} ${pluginInfo.description}`);
933
+ console.log(`${colors.bright}标签:${colors.reset} ${pluginInfo.tags.join(', ')}`);
934
+ console.log('');
935
+ return;
936
+ }
937
+ }
938
+ catch {
939
+ // 忽略注册表错误
940
+ }
941
+ // 检查本地安装
942
+ const pluginsDir = getUserPluginsDir();
943
+ const targetDir = join(pluginsDir, name);
944
+ if (existsSync(targetDir)) {
945
+ const validation = await validatePlugin(targetDir);
946
+ if (validation.valid && validation.manifest) {
947
+ const meta = await readInstalledMeta();
948
+ const savedMeta = meta[name];
949
+ console.log('');
950
+ console.log(`${colors.bright}${colors.cyan}已安装插件信息${colors.reset}`);
951
+ console.log(`${colors.bright}名称:${colors.reset} ${validation.manifest.name}`);
952
+ console.log(`${colors.bright}版本:${colors.reset} ${validation.manifest.version}`);
953
+ console.log(`${colors.bright}类型:${colors.reset} ${validation.manifest.type || 'unknown'}`);
954
+ console.log(`${colors.bright}描述:${colors.reset} ${validation.manifest.description || 'N/A'}`);
955
+ console.log(`${colors.bright}安装时间:${colors.reset} ${savedMeta?.installedAt || 'unknown'}`);
956
+ console.log(`${colors.bright}来源:${colors.reset} ${savedMeta?.source || 'local'}`);
957
+ if (savedMeta?.repo) {
958
+ console.log(`${colors.bright}仓库:${colors.reset} https://github.com/${savedMeta.repo}`);
959
+ }
960
+ console.log('');
961
+ return;
962
+ }
963
+ }
964
+ log.error(`找不到插件: ${name}`);
965
+ }
966
+ // ============================================================================
967
+ // 命令行辅助
968
+ // ============================================================================
969
+ /**
970
+ * 列出插件(带格式化输出)
971
+ */
972
+ export async function printPluginList() {
973
+ console.log('');
974
+ console.log(`${colors.bright}${colors.cyan}=== 可用插件 ===${colors.reset}`);
975
+ console.log('');
976
+ const available = await listAvailablePlugins();
977
+ const installed = await listInstalledPlugins();
978
+ const installedNames = new Set(installed.map(p => p.name));
979
+ if (available.length === 0) {
980
+ log.warn('注册表中没有插件');
981
+ }
982
+ else {
983
+ for (const plugin of available) {
984
+ const isInstalled = installedNames.has(plugin.name);
985
+ const status = isInstalled
986
+ ? `${colors.green}[已安装]${colors.reset}`
987
+ : `${colors.dim}[未安装]${colors.reset}`;
988
+ console.log(` ${colors.bright}${plugin.name}${colors.reset} ${status}`);
989
+ console.log(` ${colors.dim}${plugin.description}${colors.reset}`);
990
+ console.log(` ${colors.dim}版本: ${plugin.version} | 作者: ${plugin.author}${colors.reset}`);
991
+ console.log('');
992
+ }
993
+ }
994
+ console.log(`${colors.bright}${colors.cyan}=== 已安装插件 ===${colors.reset}`);
995
+ console.log('');
996
+ if (installed.length === 0) {
997
+ log.info('暂无已安装的插件');
998
+ }
999
+ else {
1000
+ for (const plugin of installed) {
1001
+ console.log(` ${colors.bright}${plugin.name}${colors.reset} v${plugin.version}`);
1002
+ console.log(` ${colors.dim}安装时间: ${plugin.installedAt}${colors.reset}`);
1003
+ console.log(` ${colors.dim}来源: ${plugin.source}${plugin.repo ? ` (${plugin.repo})` : ''}${colors.reset}`);
1004
+ console.log('');
1005
+ }
1006
+ }
1007
+ }
1008
+ //# sourceMappingURL=installer.js.map