@xubill/xx-cli 2.0.0 → 2.0.2

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * AI 智能解释插件
3
+ * 用于解释 xx-cli 命令的含义,将自然语言转换为命令
4
+ *
5
+ * 命令说明:
6
+ * - ai <query>:解释用户输入的自然语言,转换为对应的 xx 命令
7
+ * - 示例:
8
+ * - xx ai 关闭微信
9
+ * - xx ai 打开浏览器
10
+ * - xx ai 端口占用
11
+ */
12
+
13
+ const axios = require('axios');
14
+ const BasePlugin = require('../core/base-plugin');
15
+
16
+ class AIPlugin extends BasePlugin {
17
+ constructor() {
18
+ super();
19
+ this.pluginName = 'ai';
20
+ }
21
+
22
+ registerCommands(program) {
23
+ const aiCommand = program.command('ai')
24
+ .description('AI 智能解释命令,将自然语言转换为 xx 命令')
25
+ .option('-c, --config <path>', '指定配置文件路径')
26
+ .arguments('[query...]')
27
+ .action(async (query, options) => {
28
+ await this.handleAI(query || [], options);
29
+ });
30
+
31
+ // 添加生成配置文件的子命令
32
+ aiCommand.command('config')
33
+ .description('生成 AI 插件配置文件')
34
+ .option('-o, --output <path>', '输出配置文件路径', '.ai-config.json')
35
+ .action(async (options) => {
36
+ await this.generateConfig(options);
37
+ });
38
+
39
+ // 添加快捷添加 API key 的子命令
40
+ aiCommand.command('add')
41
+ .description('快捷添加 API key')
42
+ .arguments('<apiKey>')
43
+ .action(async (apiKey) => {
44
+ await this.addApiKey(apiKey);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * 快捷添加 API key
50
+ * @param {string} apiKey - API key
51
+ */
52
+ async addApiKey(apiKey) {
53
+ try {
54
+ this.startLoading('正在保存 API key...');
55
+
56
+ const defaultConfig = {
57
+ apiUrl: 'https://api.edgefn.net/v1/chat/completions',
58
+ apiKey: apiKey,
59
+ model: 'DeepSeek-R1-0528-Qwen3-8B'
60
+ };
61
+
62
+ // 保存到全局配置
63
+ this.saveConfig(defaultConfig, 'config', true);
64
+
65
+ this.stopLoading('API key 保存成功');
66
+ this.showSuccess('API key 已保存到全局配置');
67
+ this.showInfo('下次使用无需指定 --config 选项');
68
+
69
+ console.log('\n使用示例:');
70
+ console.log(' xx ai 关闭3000端口');
71
+ console.log(' xx ai 生成readme');
72
+ } catch (error) {
73
+ this.stopLoading();
74
+ this.showError(`保存 API key 失败: ${error.message}`);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 生成配置文件
80
+ * @param {Object} options - 命令选项
81
+ */
82
+ async generateConfig(options) {
83
+ const defaultConfig = {
84
+ apiUrl: 'https://api.edgefn.net/v1/chat/completions',
85
+ apiKey: '',
86
+ model: 'DeepSeek-R1-0528-Qwen3-8B'
87
+ };
88
+
89
+ const outputPath = options.output;
90
+
91
+ try {
92
+ this.startLoading(`正在生成配置文件到 ${outputPath}...`);
93
+
94
+ // 写入配置文件
95
+ const fs = require('fs');
96
+ fs.writeFileSync(outputPath, JSON.stringify(defaultConfig, null, 2));
97
+
98
+ this.stopLoading(`配置文件已生成到 ${outputPath}`);
99
+ this.showSuccess(`配置文件已生成,请根据需要修改其中的参数`);
100
+
101
+ console.log('\n配置文件格式说明:');
102
+ console.log(' apiUrl: AI API 接口地址');
103
+ console.log(' apiKey: API 密钥');
104
+ console.log(' model: 使用的模型名称');
105
+ } catch (error) {
106
+ this.stopLoading();
107
+ this.showError(`生成配置文件失败: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 获取所有可用命令列表
113
+ * @returns {Promise<Array>} 命令数组
114
+ */
115
+ async getAllCommands() {
116
+ const core = require('../core');
117
+ const { Command } = require('commander');
118
+ const tempProgram = new Command();
119
+ tempProgram.name('xx').version('2.0.0');
120
+
121
+ // 注册所有命令
122
+ core.registerCommands(tempProgram);
123
+
124
+ // 收集命令
125
+ const commands = [];
126
+ if (tempProgram.commands) {
127
+ tempProgram.commands.forEach((command) => {
128
+ const commandName = command._name;
129
+ if (commandName && commandName !== '*') {
130
+ // 获取命令描述
131
+ const description = command.description() || '';
132
+ commands.push({ name: commandName, description });
133
+ }
134
+ });
135
+ }
136
+
137
+ return commands;
138
+ }
139
+
140
+ /**
141
+ * 加载配置
142
+ * @param {Object} options - 命令行选项
143
+ * @returns {Object} 配置对象
144
+ */
145
+ loadAIConfig(options) {
146
+ const defaultConfig = {
147
+ apiUrl: 'https://api.edgefn.net/v1/chat/completions',
148
+ apiKey: '',
149
+ model: 'DeepSeek-R1-0528-Qwen3-8B'
150
+ };
151
+
152
+ // 检查是否通过 --config 指定了配置文件
153
+ if (options.config) {
154
+ try {
155
+ const fs = require('fs');
156
+ const customConfig = JSON.parse(fs.readFileSync(options.config, 'utf8'));
157
+ // 保存到全局配置
158
+ this.saveConfig(customConfig, 'config', true);
159
+ this.showInfo(`配置已保存到全局,下次使用无需指定 --config 选项`);
160
+ } catch (error) {
161
+ this.showError(`读取配置文件失败: ${error.message}`);
162
+ }
163
+ }
164
+
165
+ // 加载配置文件(此时会包含全局配置)
166
+ const config = this.loadConfig('config', options);
167
+
168
+ const aiConfig = {
169
+ apiUrl: config.apiUrl || defaultConfig.apiUrl,
170
+ apiKey: config.apiKey || defaultConfig.apiKey,
171
+ model: config.model || defaultConfig.model
172
+ };
173
+
174
+ // 检查是否有 API key
175
+ if (!aiConfig.apiKey) {
176
+ this.showWarning('未配置 API key');
177
+ console.log('请访问以下链接获取 API key:');
178
+ console.log('https://ai.baishan.com/auth/login?referralCode=9CbeQycJJP');
179
+
180
+ // 生成配置文件
181
+ const fs = require('fs');
182
+ const configPath = '.ai-config.json';
183
+ try {
184
+ fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
185
+ this.showInfo(`已生成配置文件到 ${configPath}`);
186
+ console.log('请在配置文件中填写您的 API key:');
187
+ console.log(` $ vim ${configPath}`);
188
+ } catch (error) {
189
+ this.showError(`生成配置文件失败: ${error.message}`);
190
+ }
191
+
192
+ // 尝试打开浏览器
193
+ try {
194
+ const open = require('open');
195
+ open('https://ai.baishan.com/auth/login?referralCode=9CbeQycJJP');
196
+ this.showInfo('正在打开浏览器...');
197
+ } catch (error) {
198
+ // 忽略打开浏览器的错误
199
+ }
200
+ console.log('\n快捷使用:');
201
+ console.log(' 直接添加 API key: xx ai add <apiKey>');
202
+ console.log(' 示例: xx ai add sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
203
+
204
+ console.log('\n完全自定义使用方法:');
205
+ console.log(' 1. 编辑配置文件');
206
+ console.log(' 2. 使用命令: xx ai --config .ai-config.json <查询内容>');
207
+ console.log(' 3. 配置会自动保存,下次使用无需指定 --config 选项');
208
+
209
+ process.exit(1);
210
+ }
211
+
212
+ return aiConfig;
213
+ }
214
+
215
+ /**
216
+ * 调用 AI API 解释命令
217
+ * @param {string} query - 用户查询
218
+ * @param {Array} commands - 可用命令列表
219
+ * @param {Object} config - AI 配置
220
+ * @returns {Promise<string>} AI 返回的解释
221
+ */
222
+ async callAI(query, commands, config) {
223
+ const commandList = commands.map(c => ` - ${c.name}: ${c.description}`).join('\n');
224
+
225
+ const prompt = `你是一个命令行工具助手。用户使用 xx-cli 工具集,你需要解释用户的意图并给出对应的 xx 命令。\n\n可用命令列表:\n${commandList}\n\n规则:\n1. 如果用户的意图可以匹配到上述命令,返回对应的 xx 命令和简要说明,命令必须以 "xx" 开头\n2. 如果无法匹配,返回 "无法匹配" 并给出最相关的命令建议,建议的命令也必须以 "xx" 开头\n3. 直接返回结果,不需要额外解释\n\n用户输入: ${query}`;
226
+
227
+ const response = await axios.post(
228
+ config.apiUrl,
229
+ {
230
+ model: config.model,
231
+ messages: [{ role: 'user', content: prompt }]
232
+ },
233
+ {
234
+ headers: {
235
+ 'Authorization': `Bearer ${config.apiKey}`,
236
+ 'Content-Type': 'application/json'
237
+ }
238
+ }
239
+ );
240
+
241
+ return response.data.choices[0].message.content;
242
+ }
243
+
244
+ /**
245
+ * 主处理函数
246
+ * @param {Array} args - 命令参数
247
+ * @param {Object} options - 命令选项
248
+ */
249
+ async handleAI(args, options) {
250
+ // 处理参数,确保 args 是数组
251
+ let queryArgs = args;
252
+ if (!Array.isArray(args)) {
253
+ // 如果 args 不是数组,可能是因为 Commander.js 版本差异
254
+ // 检查 options 是否包含 args
255
+ if (options && options.args) {
256
+ queryArgs = options.args;
257
+ } else {
258
+ queryArgs = [];
259
+ }
260
+ }
261
+
262
+ if (!queryArgs || queryArgs.length === 0) {
263
+ this.showError('请输入要解释的内容,例如: xx ai 关闭微信');
264
+ console.log('\n用法: xx ai <查询内容>');
265
+ console.log('示例:');
266
+ console.log(' xx ai 关闭占用3000端口的进程');
267
+ console.log(' xx ai 生成readme');
268
+ console.log(' xx ai 查看ip地址');
269
+ return;
270
+ }
271
+
272
+ const query = queryArgs.join(' ');
273
+
274
+ try {
275
+ this.startLoading('正在获取命令列表...');
276
+ const commands = await this.getAllCommands();
277
+ this.stopLoading('命令列表获取成功');
278
+
279
+ this.startLoading('正在加载配置...');
280
+ const config = this.loadAIConfig(options);
281
+ this.stopLoading('配置加载成功');
282
+
283
+ this.startLoading('正在调用 AI 解释...');
284
+ const result = await this.callAI(query, commands, config);
285
+ this.stopLoading('解释完成');
286
+
287
+ console.log('\n' + result + '\n');
288
+ } catch (error) {
289
+ this.stopLoading();
290
+ this.showError(`调用失败: ${error.message}`);
291
+ }
292
+ }
293
+ }
294
+
295
+ module.exports = AIPlugin;
@@ -150,7 +150,7 @@ class PluginManager {
150
150
  */
151
151
  toCamelCase(str) {
152
152
  if (!str) {
153
- return 'Plugin';
153
+ return "Plugin";
154
154
  }
155
155
  return str
156
156
  .replace(/-([a-zA-Z])/g, (g) => g[1].toUpperCase())
@@ -166,14 +166,14 @@ class PluginManager {
166
166
  generatePluginTemplate(name, format) {
167
167
  // 参数检查
168
168
  if (!name) {
169
- throw new Error('插件名称不能为空');
169
+ throw new Error("插件名称不能为空");
170
170
  }
171
-
171
+
172
172
  // 生成插件命令名称(去除后缀,转小写,连字符转驼峰)
173
173
  const commandName = name
174
174
  .replace(".js", "")
175
175
  .toLowerCase()
176
- .replace(/-(\w)/g, (g) => g[1].toUpperCase());
176
+ .replace(/-([a-zA-Z])/g, (g) => g[1].toUpperCase());
177
177
 
178
178
  // 生成驼峰命名的类名
179
179
  const className = this.toCamelCase(name);
@@ -302,7 +302,9 @@ module.exports = {
302
302
  }
303
303
  });
304
304
  } catch (error) {
305
- helper.showInfo("提示:无法自动打开 vscode,请手动打开插件文件进行编辑");
305
+ helper.showInfo(
306
+ "提示:无法自动打开 vscode,请手动打开插件文件进行编辑",
307
+ );
306
308
  }
307
309
  } catch (error) {
308
310
  helper.showError(`创建插件模板失败: ${error.message || error}`);
@@ -400,7 +402,10 @@ module.exports = {
400
402
  .get(url, (response) => {
401
403
  if (response.statusCode === 200) {
402
404
  // 获取文件大小
403
- const fileSize = parseInt(response.headers["content-length"], 10);
405
+ const fileSize = parseInt(
406
+ response.headers["content-length"],
407
+ 10,
408
+ );
404
409
  let downloadedSize = 0;
405
410
 
406
411
  // 创建写入流
@@ -468,6 +473,67 @@ module.exports = {
468
473
  helper.showError("添加插件失败:", error.message);
469
474
  }
470
475
  }
476
+
477
+ /**
478
+ * 同步开发插件
479
+ * 将当前目录下的 lib/myplugins 同步到 CLI 配置目录的 plugins
480
+ * @param {Object} helper - 插件帮助器
481
+ */
482
+ async syncDevPlugins(helper) {
483
+ try {
484
+ // 确保用户插件目录存在
485
+ fs.ensureDirSync(this.userPluginsDir);
486
+
487
+ // 开发插件目录路径
488
+ const devPluginsDir = path.join(process.cwd(), "lib/myplugins");
489
+
490
+ // 检查开发插件目录是否存在
491
+ if (!fs.existsSync(devPluginsDir)) {
492
+ helper.showError(`开发插件目录不存在: ${devPluginsDir}`);
493
+ return;
494
+ }
495
+
496
+ // 读取开发插件目录中的文件
497
+ const devPluginFiles = fs
498
+ .readdirSync(devPluginsDir)
499
+ .filter((file) => file.endsWith(".js"));
500
+
501
+ if (devPluginFiles.length === 0) {
502
+ helper.showInfo("开发插件目录中没有 JavaScript 文件");
503
+ return;
504
+ }
505
+
506
+ helper.showInfo(
507
+ `找到 ${devPluginFiles.length} 个开发插件文件,开始同步...`,
508
+ );
509
+
510
+ // 同步每个插件文件
511
+ let syncedCount = 0;
512
+ for (const file of devPluginFiles) {
513
+ const sourcePath = path.join(devPluginsDir, file);
514
+ const targetPath = path.join(this.userPluginsDir, file);
515
+
516
+ // 检查源文件是否存在
517
+ if (fs.existsSync(sourcePath)) {
518
+ // 复制文件
519
+ await fs.copy(sourcePath, targetPath, { overwrite: true });
520
+ helper.showSuccess(`已同步: ${file}`);
521
+ syncedCount++;
522
+ } else {
523
+ helper.showWarning(`跳过不存在的文件: ${file}`);
524
+ }
525
+ }
526
+
527
+ helper.showSuccess(`同步完成,共同步 ${syncedCount} 个插件文件`);
528
+ helper.showInfo(`插件已同步到: ${this.userPluginsDir}`);
529
+ helper.showInfo("\n提示:");
530
+ helper.showInfo("- 插件已自动注册到命令系统");
531
+ helper.showInfo('- 可以使用 "xx plugin list" 命令查看已安装的插件');
532
+ helper.showInfo('- 可以使用 "xx <plugin-command>" 命令使用插件功能');
533
+ } catch (error) {
534
+ helper.showError(`同步开发插件失败: ${error.message}`);
535
+ }
536
+ }
471
537
  }
472
538
 
473
539
  // 创建插件管理实例
@@ -567,21 +633,21 @@ module.exports = {
567
633
  action: async (args, options, helper) => {
568
634
  helper.showInfo(`args 对象: ${JSON.stringify(args)}`);
569
635
  helper.showInfo(`options 对象: ${JSON.stringify(options)}`);
570
-
636
+
571
637
  // 提取插件名称
572
- let pluginName = 'unnamed';
638
+ let pluginName = "unnamed";
573
639
  if (Array.isArray(args)) {
574
640
  // 遍历数组,找到非对象的字符串参数
575
641
  for (const arg of args) {
576
- if (typeof arg === 'string') {
642
+ if (typeof arg === "string") {
577
643
  pluginName = arg;
578
644
  break;
579
645
  }
580
646
  }
581
- } else if (typeof args === 'string') {
647
+ } else if (typeof args === "string") {
582
648
  pluginName = args;
583
649
  }
584
-
650
+
585
651
  helper.showInfo(`提取的插件名称: ${pluginName}`);
586
652
  await pluginManager.createPlugin(pluginName, options, helper);
587
653
  },
@@ -608,7 +674,20 @@ module.exports = {
608
674
  },
609
675
  ],
610
676
  action: (args, options, helper) => {
611
- pluginManager.managePluginConfig(args.plugin, args.key, args.value, helper);
677
+ pluginManager.managePluginConfig(
678
+ args.plugin,
679
+ args.key,
680
+ args.value,
681
+ helper,
682
+ );
683
+ },
684
+ },
685
+ {
686
+ name: "sync",
687
+ alias: "s",
688
+ description: "将当前目录下的 lib/myplugins 同步到 CLI 配置目录的 plugins",
689
+ action: async (args, options, helper) => {
690
+ await pluginManager.syncDevPlugins(helper);
612
691
  },
613
692
  },
614
693
  ],
@@ -618,4 +697,4 @@ module.exports = {
618
697
  description: "显示帮助信息",
619
698
  },
620
699
  ],
621
- };
700
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xubill/xx-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "个人工具集",
5
5
  "main": "lib/core/index.js",
6
6
  "bin": {
@@ -55,8 +55,13 @@
55
55
  "zx": "^8.8.5"
56
56
  },
57
57
  "devDependencies": {
58
+ "medium-zoom": "^1.1.0",
59
+ "nprogress-v2": "^1.1.10",
58
60
  "standard-version": "^9.5.0",
59
61
  "vitepress": "^1.6.4",
62
+ "vitepress-plugin-announcement": "^0.1.7",
63
+ "vitepress-plugin-group-icons": "^1.7.1",
64
+ "vitepress-sidebar": "^1.33.1",
60
65
  "vue": "^3.5.26"
61
66
  }
62
67
  }