yuangs 1.3.34 → 1.3.37

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.
File without changes
package/cli.js CHANGED
@@ -2,11 +2,43 @@
2
2
 
3
3
  const yuangs = require('./index.js');
4
4
  const chalk = require('chalk');
5
- const { version } = require('./package.json'); // 引入版本号
5
+ const { version } = require('./package.json');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const ora = require('ora').default;
9
+ const { marked } = require('marked');
10
+ const TerminalRenderer = require('marked-terminal').default;
11
+ const { exec } = require('child_process');
12
+
13
+ marked.setOptions({
14
+ renderer: new TerminalRenderer({
15
+ code: chalk.yellow,
16
+ heading: chalk.magenta.bold,
17
+ firstHeading: chalk.magenta.underline.bold,
18
+ listitem: chalk.cyan,
19
+ table: chalk.white,
20
+ strong: chalk.bold.red,
21
+ em: chalk.italic
22
+ })
23
+ });
6
24
 
7
25
  const args = process.argv.slice(2);
8
26
  const command = args[0];
9
27
 
28
+ async function readStdin() {
29
+ if (process.stdin.isTTY) return '';
30
+ return new Promise((resolve) => {
31
+ let data = '';
32
+ process.stdin.setEncoding('utf8');
33
+ process.stdin.on('data', (chunk) => {
34
+ data += chunk;
35
+ });
36
+ process.stdin.on('end', () => {
37
+ resolve(data);
38
+ });
39
+ });
40
+ }
41
+
10
42
  function printHelp() {
11
43
  console.log(chalk.bold.cyan('\n🎨 苑广山的个人应用启动器\n'));
12
44
  console.log(chalk.yellow(`当前版本: ${version}`)); // 显示版本号
@@ -35,6 +67,11 @@ function printHelp() {
35
67
  console.log(` ${chalk.gray('--model, -m <模型名称>')} 指定 AI 模型 (可选)`);
36
68
  console.log(` ${chalk.gray('-p -f -l')} 指定 pro,flash,lite 模型 (可选)`);
37
69
  console.log(` ${chalk.gray('-e <描述>')} 生成 Linux 命令`);
70
+ console.log(` ${chalk.green('config')} <key> <value> 设置配置项`);
71
+ console.log(` ${chalk.green('history')} 查看命令历史`);
72
+ console.log(` ${chalk.green('save')} <名称> 创建快捷指令`);
73
+ console.log(` ${chalk.green('run')} <名称> 执行快捷指令`);
74
+ console.log(` ${chalk.green('macros')} 查看所有快捷指令`);
38
75
  console.log(` ${chalk.green('help')} 显示帮助信息\n`);
39
76
  console.log(chalk.bold('AI 交互模式命令:'));
40
77
  console.log(` ${chalk.gray('/clear')} 清空对话历史`);
@@ -51,50 +88,83 @@ function printSuccess(app, url) {
51
88
  }
52
89
 
53
90
  async function askOnce(question, model) {
54
- const startTime = Date.now(); // Record start time
91
+ const startTime = Date.now();
55
92
  let i = 0;
56
93
  const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
57
94
  const interval = setInterval(() => {
58
- const elapsedTime = Math.floor((Date.now() - startTime) / 1000); // Calculate elapsed time in seconds
95
+ const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
59
96
  process.stdout.write(chalk.cyan(`\r${spinner[i++ % spinner.length]} 正在请求 AI,请稍候... (${elapsedTime}s}`));
60
97
  }, 100);
61
98
 
62
99
  try {
63
- // For single requests (non-interactive mode), we may want to include history
64
- // For now, use history for all requests, but we could make this configurable
65
100
  const answer = await yuangs.getAIAnswer(question, model, true);
66
101
  clearInterval(interval);
67
102
 
68
- // Clear the spinner line if possible
69
103
  if (process.stdout.clearLine) {
70
104
  process.stdout.clearLine(0);
71
105
  process.stdout.cursorTo(0);
72
106
  } else {
73
- process.stdout.write('\r'); // Fallback to just carriage return
107
+ process.stdout.write('\r');
74
108
  }
75
109
 
76
- const totalElapsedTime = (Date.now() - startTime) / 1000; // Calculate total elapsed time
110
+ const totalElapsedTime = (Date.now() - startTime) / 1000;
77
111
  if (answer && answer.explanation) {
78
- console.log(chalk.bold.green('🤖 AI 回答:\n'));
79
- console.log(answer.explanation);
112
+ console.log(marked(answer.explanation));
80
113
  } else {
81
114
  console.log(chalk.yellow('AI 未返回有效内容。'));
82
115
  }
83
- console.log(chalk.gray(`\n请求耗时: ${totalElapsedTime.toFixed(2)}s\n`)); // Display total elapsed time
116
+ console.log(chalk.gray(`\n请求耗时: ${totalElapsedTime.toFixed(2)}s\n`));
84
117
  } catch (error) {
85
118
  clearInterval(interval);
86
119
 
87
- // Clear the spinner line if possible
88
120
  if (process.stdout.clearLine) {
89
121
  process.stdout.clearLine(0);
90
122
  process.stdout.cursorTo(0);
91
123
  } else {
92
- process.stdout.write('\r'); // Fallback to just carriage return
124
+ process.stdout.write('\r');
93
125
  }
94
126
 
95
- const totalElapsedTime = (Date.now() - startTime) / 1000; // Calculate total elapsed time on error
127
+ const totalElapsedTime = (Date.now() - startTime) / 1000;
96
128
  console.error(chalk.red('处理 AI 请求时出错:'), error.message || error);
97
- console.log(chalk.gray(`\n请求耗时: ${totalElapsedTime.toFixed(2)}s\n`)); // Display total elapsed time on error
129
+ console.log(chalk.gray(`\n请求耗时: ${totalElapsedTime.toFixed(2)}s\n`));
130
+ }
131
+ }
132
+
133
+ async function askOnceStream(question, model) {
134
+ const startTime = Date.now();
135
+ let messages = [...yuangs.getConversationHistory()];
136
+ messages.push({ role: 'user', content: question });
137
+
138
+ const spinner = ora(chalk.cyan('AI 正在思考...')).start();
139
+ let fullResponse = '';
140
+ let firstChunk = true;
141
+
142
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
143
+
144
+ try {
145
+ await yuangs.callAI_Stream(messages, model, (chunk) => {
146
+ if (spinner.isSpinning) {
147
+ spinner.stop();
148
+ }
149
+ fullResponse += chunk;
150
+ process.stdout.write(chunk);
151
+ });
152
+
153
+ yuangs.addToConversationHistory('user', question);
154
+ yuangs.addToConversationHistory('assistant', fullResponse);
155
+
156
+ // 打印优美的分割线和 Markdown 渲染结果
157
+ console.log('');
158
+ console.log(chalk.gray('─'.repeat(Math.min(80, process.stdout.columns || 80))));
159
+ console.log(marked(fullResponse));
160
+
161
+ const totalElapsedTime = (Date.now() - startTime) / 1000;
162
+ console.log(chalk.gray(`\n\n请求耗时: ${totalElapsedTime.toFixed(2)}s\n`));
163
+ } catch (error) {
164
+ if (spinner.isSpinning) {
165
+ spinner.fail(chalk.red('AI 响应出错'));
166
+ }
167
+ console.error(error.message);
98
168
  }
99
169
  }
100
170
 
@@ -105,6 +175,12 @@ async function handleAICommand() {
105
175
  let questionParts = commandArgs;
106
176
  let isExecMode = false;
107
177
 
178
+ const stdinContent = await readStdin();
179
+
180
+ if (stdinContent) {
181
+ questionParts.unshift(stdinContent);
182
+ }
183
+
108
184
  // Check for -e flag
109
185
  const execIndex = commandArgs.indexOf('-e');
110
186
  if (execIndex !== -1) {
@@ -218,11 +294,13 @@ async function handleAICommand() {
218
294
 
219
295
  const { spawn } = require('child_process');
220
296
  console.log(chalk.gray('正在执行...'));
221
- // Use shell: true to support pipes, redirects, etc.
222
297
  const child = spawn(finalCommand, [], { shell: true, stdio: 'inherit' });
223
298
 
224
299
  child.on('close', (code) => {
225
- if (code !== 0) {
300
+ if (code === 0) {
301
+ yuangs.saveSuccessfulCommand(question, finalCommand);
302
+ console.log(chalk.green('\n✓ 执行成功并已存入历史库'));
303
+ } else {
226
304
  console.log(chalk.red(`\n命令执行失败 (退出码: ${code})`));
227
305
  }
228
306
  process.exit(code);
@@ -288,11 +366,10 @@ async function handleAICommand() {
288
366
  }
289
367
 
290
368
  if (!trimmed) {
291
- return askLoop(); // 空输入则重新询问
369
+ return askLoop();
292
370
  }
293
371
 
294
- // 等待回答完成后,再开启下一轮询问
295
- await askOnce(trimmed, model);
372
+ await askOnceStream(trimmed, model);
296
373
  askLoop();
297
374
  });
298
375
  };
@@ -303,7 +380,7 @@ async function handleAICommand() {
303
380
  }
304
381
 
305
382
  // 有问题时,直接请求一次
306
- await askOnce(question, model);
383
+ await askOnceStream(question, model);
307
384
  }
308
385
 
309
386
  // Check if the command matches one of the configured apps
@@ -333,13 +410,143 @@ switch (command) {
333
410
  case 'ai':
334
411
  handleAICommand();
335
412
  break;
413
+ case 'config':
414
+ const key = args[1];
415
+ const value = args[2];
416
+ if (!key || !value) {
417
+ console.log(chalk.cyan('\n⚙️ 配置帮助: yuangs config <key> <value>'));
418
+ console.log(chalk.gray('当前配置:'), yuangs.getUserConfig());
419
+ console.log(chalk.gray('\n可用配置:'));
420
+ console.log(chalk.gray(' defaultModel 默认AI模型 (如: Assistant, gemini-pro-latest)'));
421
+ console.log(chalk.gray(' aiProxyUrl AI代理地址'));
422
+ console.log(chalk.gray(' accountType 账户类型 (如: free, pro)\n'));
423
+ break;
424
+ }
425
+ const config = yuangs.getUserConfig();
426
+ config[key] = value;
427
+ fs.writeFileSync(path.join(require('os').homedir(), '.yuangs.json'), JSON.stringify(config, null, 2));
428
+ console.log(chalk.green(`✓ 已更新 ${key}`));
429
+ break;
430
+ case 'history':
431
+ const history = yuangs.getCommandHistory();
432
+ if (history.length === 0) {
433
+ console.log(chalk.gray('暂无命令历史\n'));
434
+ } else {
435
+ console.log(chalk.bold.cyan('\n📋 命令历史\n'));
436
+ console.log(chalk.gray('─────────────────────────────────────────────────'));
437
+ history.forEach((item, index) => {
438
+ console.log(chalk.white(`${index + 1}. ${item.command}`));
439
+ console.log(chalk.gray(` 问题: ${item.question}`));
440
+ console.log(chalk.gray(` 时间: ${item.time}\n`));
441
+ });
442
+ console.log(chalk.gray('─────────────────────────────────────────────────\n'));
443
+ }
444
+ break;
445
+ case 'save':
446
+ const macroName = args[1];
447
+ if (!macroName) {
448
+ console.log(chalk.red('\n错误: 请指定快捷指令名称'));
449
+ console.log(chalk.gray('用法: yuangs save <名称>\n'));
450
+ break;
451
+ }
452
+
453
+ const readline = require('readline');
454
+ const saveRl = readline.createInterface({
455
+ input: process.stdin,
456
+ output: process.stdout
457
+ });
458
+
459
+ console.log(chalk.cyan(`\n正在创建快捷指令 "${macroName}"...`));
460
+ console.log(chalk.gray('请输入要保存的命令(多行命令用 && 或 ; 分隔,输入空行结束):\n'));
461
+
462
+ let commandLines = [];
463
+ const askCommand = () => {
464
+ saveRl.question(chalk.green('> '), (line) => {
465
+ if (line.trim() === '') {
466
+ if (commandLines.length === 0) {
467
+ console.log(chalk.yellow('\n未输入命令,已取消'));
468
+ saveRl.close();
469
+ return;
470
+ }
471
+
472
+ saveRl.question(chalk.cyan('请输入描述(可选): '), (desc) => {
473
+ const commands = commandLines.map(cmd => cmd.trim()).join(' && ');
474
+ yuangs.saveMacro(macroName, commands, desc.trim());
475
+ console.log(chalk.green(`\n✓ 快捷指令 "${macroName}" 已保存\n`));
476
+ saveRl.close();
477
+ });
478
+ return;
479
+ }
480
+ commandLines.push(line);
481
+ askCommand();
482
+ });
483
+ };
484
+ askCommand();
485
+ break;
486
+ case 'run':
487
+ const runMacroName = args[1];
488
+ if (!runMacroName) {
489
+ console.log(chalk.red('\n错误: 请指定快捷指令名称'));
490
+ console.log(chalk.gray('用法: yuangs run <名称>\n'));
491
+ break;
492
+ }
493
+
494
+ const macros = yuangs.getMacros();
495
+ if (!macros[runMacroName]) {
496
+ console.log(chalk.red(`\n错误: 快捷指令 "${runMacroName}" 不存在`));
497
+ console.log(chalk.gray('使用 "yuangs macros" 查看所有快捷指令\n'));
498
+ break;
499
+ }
500
+
501
+ const macro = macros[runMacroName];
502
+ console.log(chalk.cyan(`\n执行快捷指令: ${runMacroName}`));
503
+ if (macro.description) {
504
+ console.log(chalk.gray(`描述: ${macro.description}`));
505
+ }
506
+ console.log(chalk.gray(`命令: ${macro.commands}\n`));
507
+
508
+ const { spawn } = require('child_process');
509
+ const child = spawn(macro.commands, [], { shell: true, stdio: 'inherit' });
510
+
511
+ child.on('close', (code) => {
512
+ if (code !== 0) {
513
+ console.error(chalk.red(`\n快捷指令执行失败 (退出码: ${code})`));
514
+ process.exit(code);
515
+ }
516
+ });
517
+ break;
518
+ case 'macros':
519
+ const allMacros = yuangs.getMacros();
520
+ const macroNames = Object.keys(allMacros);
521
+
522
+ if (macroNames.length === 0) {
523
+ console.log(chalk.gray('暂无快捷指令\n'));
524
+ console.log(chalk.gray('使用 "yuangs save <名称>" 创建快捷指令\n'));
525
+ break;
526
+ }
527
+
528
+ console.log(chalk.bold.cyan('\n🚀 快捷指令列表\n'));
529
+ console.log(chalk.gray('─────────────────────────────────────────────────'));
530
+ macroNames.forEach(name => {
531
+ const m = allMacros[name];
532
+ console.log(chalk.white(` ${name}`));
533
+ if (m.description) {
534
+ console.log(chalk.gray(` 描述: ${m.description}`));
535
+ }
536
+ console.log(chalk.gray(` 命令: ${m.commands}`));
537
+ console.log('');
538
+ });
539
+ console.log(chalk.gray('─────────────────────────────────────────────────\n'));
540
+ console.log(chalk.gray('使用:'));
541
+ console.log(chalk.gray(' yuangs run <名称> 执行快捷指令'));
542
+ console.log(chalk.gray(' yuangs save <名称> 创建新快捷指令\n'));
543
+ break;
336
544
  case 'help':
337
545
  case '--help':
338
546
  case '-h':
339
547
  printHelp();
340
548
  break;
341
549
  default:
342
- // If it's an app command but not one of the named ones, handle it with the dynamic function
343
550
  if (isAppCommand) {
344
551
  printSuccess(command, yuangs.urls[command]);
345
552
  yuangs.openApp(command);
package/index.js CHANGED
@@ -2,11 +2,72 @@ const { exec } = require('child_process');
2
2
  const axios = require('axios');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const os = require('os');
5
6
 
6
7
  // Store conversation history
7
8
  // 存储结构标准为: [{ role: 'user', content: '...' }, { role: 'assistant', content: '...' }]
8
9
  let conversationHistory = [];
9
10
 
11
+ const HISTORY_FILE = path.join(os.homedir(), '.yuangs_cmd_history.json');
12
+ const CONFIG_FILE = path.join(os.homedir(), '.yuangs.json');
13
+ const MACROS_FILE = path.join(os.homedir(), '.yuangs_macros.json');
14
+
15
+ function getUserConfig() {
16
+ if (fs.existsSync(CONFIG_FILE)) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
19
+ } catch (e) {}
20
+ }
21
+ return {};
22
+ }
23
+
24
+ function getCommandHistory() {
25
+ if (fs.existsSync(HISTORY_FILE)) {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
28
+ } catch (e) {}
29
+ }
30
+ return [];
31
+ }
32
+
33
+ function saveSuccessfulCommand(question, command) {
34
+ if (!command) return;
35
+ let history = getCommandHistory();
36
+ const newEntry = { question, command, time: new Date().toLocaleString() };
37
+ history = [newEntry, ...history.filter(item => item.command !== command)].slice(0, 5);
38
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
39
+ }
40
+
41
+ function getMacros() {
42
+ if (fs.existsSync(MACROS_FILE)) {
43
+ try {
44
+ return JSON.parse(fs.readFileSync(MACROS_FILE, 'utf8'));
45
+ } catch (e) {}
46
+ }
47
+ return {};
48
+ }
49
+
50
+ function saveMacro(name, commands, description = '') {
51
+ const macros = getMacros();
52
+ macros[name] = {
53
+ commands,
54
+ description,
55
+ createdAt: new Date().toISOString()
56
+ };
57
+ fs.writeFileSync(MACROS_FILE, JSON.stringify(macros, null, 2));
58
+ return true;
59
+ }
60
+
61
+ function deleteMacro(name) {
62
+ const macros = getMacros();
63
+ if (macros[name]) {
64
+ delete macros[name];
65
+ fs.writeFileSync(MACROS_FILE, JSON.stringify(macros, null, 2));
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+
10
71
  // Default apps (fallback if no config file exists)
11
72
  const DEFAULT_APPS = {
12
73
  shici: 'https://wealth.want.biz/shici/index.html',
@@ -103,21 +164,90 @@ function getConversationHistory() {
103
164
  /**
104
165
  * 通用 AI 调用函数 (OpenAI 兼容接口)
105
166
  */
167
+ async function callAI_Stream(messages, model, onChunk) {
168
+ const config = getUserConfig();
169
+ const url = config.aiProxyUrl || 'https://aiproxy.want.biz/v1/chat/completions';
170
+
171
+ const response = await axios({
172
+ method: 'post',
173
+ url: url,
174
+ data: {
175
+ model: model || config.defaultModel || 'Assistant',
176
+ messages: messages,
177
+ stream: true
178
+ },
179
+ responseType: 'stream',
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ 'X-Client-ID': 'npm_yuangs',
183
+ 'Origin': 'https://cli.want.biz',
184
+ 'Referer': 'https://cli.want.biz/',
185
+ 'account': config.accountType || 'free',
186
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
187
+ 'Accept': 'application/json'
188
+ }
189
+ });
190
+
191
+ return new Promise((resolve, reject) => {
192
+ let buffer = '';
193
+ response.data.on('data', chunk => {
194
+ buffer += chunk.toString();
195
+ let lines = buffer.split('\n');
196
+ // 数组的最后一个元素可能是不完整的行,留到下一次处理
197
+ buffer = lines.pop();
198
+
199
+ for (const line of lines) {
200
+ const trimmedLine = line.trim();
201
+ if (trimmedLine.startsWith('data: ')) {
202
+ const data = trimmedLine.slice(6);
203
+ if (data === '[DONE]') {
204
+ resolve();
205
+ return;
206
+ }
207
+ try {
208
+ const parsed = JSON.parse(data);
209
+ const content = parsed.choices[0]?.delta?.content || '';
210
+ if (content) onChunk(content);
211
+ } catch (e) {
212
+ // 如果这一行真的有问题,忽略它
213
+ }
214
+ }
215
+ }
216
+ });
217
+ response.data.on('error', reject);
218
+ response.data.on('end', () => {
219
+ // 处理缓冲区中剩余的内容(如果有)
220
+ if (buffer.trim().startsWith('data: ')) {
221
+ try {
222
+ const data = buffer.trim().slice(6);
223
+ if (data !== '[DONE]') {
224
+ const parsed = JSON.parse(data);
225
+ const content = parsed.choices[0]?.delta?.content || '';
226
+ if (content) onChunk(content);
227
+ }
228
+ } catch (e) {}
229
+ }
230
+ resolve();
231
+ });
232
+ });
233
+ }
234
+
106
235
  async function callAI_OpenAI(messages, model) {
107
- const url = 'https://aiproxy.want.biz/v1/chat/completions';
236
+ const config = getUserConfig();
237
+ const url = config.aiProxyUrl || 'https://aiproxy.want.biz/v1/chat/completions';
108
238
 
109
239
  const headers = {
110
240
  'Content-Type': 'application/json',
111
- 'X-Client-ID': 'npm_yuangs', // 客户端 标识
112
- 'Origin': 'https://cli.want.biz', // 配合后端白名单
241
+ 'X-Client-ID': 'npm_yuangs',
242
+ 'Origin': 'https://cli.want.biz',
113
243
  'Referer': 'https://cli.want.biz/',
114
- "account": "free",
244
+ 'account': config.accountType || 'free',
115
245
  'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
116
246
  'Accept': 'application/json'
117
247
  };
118
248
 
119
249
  const data = {
120
- model: model || "gemini-flash-lite-latest",
250
+ model: model || config.defaultModel || 'Assistant',
121
251
  messages: messages,
122
252
  stream: false
123
253
  };
@@ -162,8 +292,7 @@ async function getAIAnswer(question, model, includeHistory = true) {
162
292
  }
163
293
 
164
294
  async function generateCommand(instruction, model) {
165
- // 构造 System Prompt (通过 system role 或者直接在 user message 中强调)
166
- // Gemini 有时对 system role 支持不同,但在 OpenAI 兼容接口下通常支持 system
295
+ const config = getUserConfig();
167
296
  const messages = [
168
297
  {
169
298
  role: 'system',
@@ -177,20 +306,17 @@ async function generateCommand(instruction, model) {
177
306
  ];
178
307
 
179
308
  try {
180
- const response = await callAI_OpenAI(messages, model);
309
+ const response = await callAI_OpenAI(messages, model || config.defaultModel);
181
310
  const aiContent = response.data?.choices?.[0]?.message?.content;
182
311
 
183
312
  if (aiContent) {
184
- // Clean up the output just in case the AI adds markdown or whitespace
185
313
  let command = aiContent.trim();
186
- // Remove wrapping backticks if present
187
314
  if (command.startsWith('`') && command.endsWith('`')) {
188
315
  command = command.slice(1, -1);
189
316
  }
190
317
  if (command.startsWith('```') && command.endsWith('```')) {
191
318
  command = command.split('\n').filter(line => !line.startsWith('```')).join('\n').trim();
192
319
  }
193
- // If it starts with a shell prefix like "$ " or "> ", remove it
194
320
  if (command.startsWith('$ ')) command = command.slice(2);
195
321
  if (command.startsWith('> ')) command = command.slice(2);
196
322
 
@@ -198,15 +324,12 @@ async function generateCommand(instruction, model) {
198
324
  }
199
325
  return null;
200
326
  } catch (error) {
201
- // 命令生成失败不打印过多错误,返回 null 即可
202
- // console.error('AI 生成命令失败:', error.message);
203
327
  return null;
204
328
  }
205
329
  }
206
330
 
207
331
  module.exports = {
208
332
  urls: APPS,
209
- // Dynamic function to open any app by key
210
333
  openApp: (appKey) => {
211
334
  const url = APPS[appKey];
212
335
  if (url) {
@@ -216,7 +339,6 @@ module.exports = {
216
339
  console.error(`App '${appKey}' not found`);
217
340
  return false;
218
341
  },
219
- // Specific functions for default apps (for backward compatibility)
220
342
  openShici: () => openUrl(APPS.shici || DEFAULT_APPS.shici),
221
343
  openDict: () => openUrl(APPS.dict || DEFAULT_APPS.dict),
222
344
  openPong: () => openUrl(APPS.pong || DEFAULT_APPS.pong),
@@ -228,5 +350,12 @@ module.exports = {
228
350
  addToConversationHistory,
229
351
  clearConversationHistory,
230
352
  getConversationHistory,
231
- generateCommand
353
+ generateCommand,
354
+ getUserConfig,
355
+ getCommandHistory,
356
+ saveSuccessfulCommand,
357
+ callAI_Stream,
358
+ getMacros,
359
+ saveMacro,
360
+ deleteMacro
232
361
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuangs",
3
- "version": "1.3.34",
3
+ "version": "1.3.37",
4
4
  "description": "苑广山的个人应用集合 CLI(彩色版)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -28,7 +28,10 @@
28
28
  "dependencies": {
29
29
  "axios": "^1.13.2",
30
30
  "chalk": "^4.1.2",
31
- "js-yaml": "^4.1.0"
31
+ "js-yaml": "^4.1.0",
32
+ "marked": "^15.0.12",
33
+ "marked-terminal": "^7.3.0",
34
+ "ora": "^9.0.0"
32
35
  },
33
36
  "devDependencies": {
34
37
  "jest": "^29.7.0"
@@ -0,0 +1,91 @@
1
+ const fs = require('fs');
2
+ const yuangs = require('../index.js');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ jest.mock('fs');
7
+
8
+ describe('Module: Macros', () => {
9
+ const mockMacrosFile = path.join(os.homedir(), '.yuangs_macros.json');
10
+
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ // Setup default mock implementation
14
+ fs.existsSync.mockReturnValue(false);
15
+ fs.readFileSync.mockReturnValue('{}');
16
+ fs.writeFileSync.mockReturnValue(undefined);
17
+ // We need to unmock path and os if they were mocked, but we only mocked fs
18
+ });
19
+
20
+ test('should get empty macros when file does not exist', () => {
21
+ fs.existsSync.mockReturnValue(false);
22
+ const macros = yuangs.getMacros();
23
+ expect(macros).toEqual({});
24
+ expect(fs.existsSync).toHaveBeenCalledWith(mockMacrosFile);
25
+ });
26
+
27
+ test('should save a new macro', () => {
28
+ fs.existsSync.mockReturnValue(false); // File doesn't exist yet
29
+
30
+ const result = yuangs.saveMacro('test', 'echo hello', 'description');
31
+
32
+ expect(result).toBe(true);
33
+ expect(fs.writeFileSync).toHaveBeenCalled();
34
+
35
+ const [filePath, content] = fs.writeFileSync.mock.calls[0];
36
+ expect(filePath).toBe(mockMacrosFile);
37
+
38
+ const data = JSON.parse(content);
39
+ expect(data).toHaveProperty('test');
40
+ expect(data.test.commands).toBe('echo hello');
41
+ expect(data.test.description).toBe('description');
42
+ expect(data.test).toHaveProperty('createdAt');
43
+ });
44
+
45
+ test('should retrieve existing macros', () => {
46
+ const mockData = {
47
+ "demo": {
48
+ "commands": "ls -la",
49
+ "description": "list files"
50
+ }
51
+ };
52
+ fs.existsSync.mockReturnValue(true);
53
+ fs.readFileSync.mockReturnValue(JSON.stringify(mockData));
54
+
55
+ const macros = yuangs.getMacros();
56
+ expect(macros).toEqual(mockData);
57
+ });
58
+
59
+ test('should delete a macro', () => {
60
+ const mockData = {
61
+ "todelete": { "commands": "rm -rf /" },
62
+ "keep": { "commands": "echo safe" }
63
+ };
64
+ fs.existsSync.mockReturnValue(true);
65
+ fs.readFileSync.mockReturnValue(JSON.stringify(mockData));
66
+
67
+ const result = yuangs.deleteMacro('todelete');
68
+
69
+ expect(result).toBe(true);
70
+ expect(fs.writeFileSync).toHaveBeenCalled();
71
+
72
+ const [filePath, content] = fs.writeFileSync.mock.calls[0];
73
+ const savedData = JSON.parse(content);
74
+ expect(savedData).not.toHaveProperty('todelete');
75
+ expect(savedData).toHaveProperty('keep');
76
+ });
77
+
78
+ test('should return false when deleting non-existent macro', () => {
79
+ fs.existsSync.mockReturnValue(false); // Or true with empty object
80
+
81
+ const result = yuangs.deleteMacro('nonexistent');
82
+ expect(result).toBe(false);
83
+ // Should not write to disk if nothing changed (optional optimization, but current implementation reads first)
84
+ // Actually current implementation:
85
+ // const macros = getMacros();
86
+ // if (macros[name]) { ... }
87
+ // getMacros returns {} if file not exists. macros['nonexistent'] is undefined.
88
+ // So it returns false and does NOT call writeFileSync.
89
+ expect(fs.writeFileSync).not.toHaveBeenCalled();
90
+ });
91
+ });
package/todo.md ADDED
@@ -0,0 +1,273 @@
1
+ 功能1:历史记录功能
2
+ 没问题!我已经为你梳理好了完整的代码整合方案。通过这次整合,你的 yuangs 工具将拥有完整的配置管理、带语义的历史记录以及智能预填执行功能。
3
+ 1. index.js 完整核心逻辑
4
+ 请将以下函数添加到 index.js 中并确保在 module.exports 中导出:
5
+ const os = require(’os‘);
6
+ const HISTORY_FILE = path.join(os.homedir(), ’.yuangs_cmd_history.json‘);
7
+ const CONFIG_FILE = path.join(os.homedir(), ’.yuangs.json‘);
8
+
9
+ // — 配置管理 —
10
+ function getUserConfig() {
11
+ if (fs.existsSync(CONFIG_FILE)) {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, ’utf8‘));
14
+ } catch (e) {}
15
+ }
16
+ return {};
17
+ }
18
+
19
+ // — 历史记录管理 (带问题描述) —
20
+ function getCommandHistory() {
21
+ if (fs.existsSync(HISTORY_FILE)) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(HISTORY_FILE, ’utf8‘));
24
+ } catch (e) {}
25
+ }
26
+ return [];
27
+ }
28
+
29
+ function saveSuccessfulCommand(question, command) {
30
+ if (!command) return;
31
+ let history = getCommandHistory();
32
+ const newEntry = { question, command, time: new Date().toLocaleString() };
33
+ // 去重并保留最近 5 条
34
+ history = [newEntry, ...history.filter(item => item.command !== command)].slice(0, 5);
35
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
36
+ }
37
+
38
+ // 修改原有的 callAI_OpenAI 适配配置
39
+ async function callAI_OpenAI(messages, model) {
40
+ const config = getUserConfig();
41
+ const url = config.aiProxyUrl || ’https://aiproxy.want.biz/v1/chat/completions‘;
42
+
43
+ const headers = {
44
+ ’Content-Type‘: ’application/json‘,
45
+ ’X-Client-ID‘: ’npm_yuangs‘,
46
+ ’account‘: config.accountType || ’free‘,
47
+ ’User-Agent‘: ’Mozilla/5.0...‘,
48
+ ’Accept‘: ’application/json‘
49
+ };
50
+
51
+ const data = {
52
+ model: model || config.defaultModel || ”gemini-flash-lite-latest“,
53
+ messages: messages,
54
+ stream: false
55
+ };
56
+
57
+ return await axios.post(url, data, { headers });
58
+ }
59
+
60
+ 2. cli.js 交互层升级
61
+ 在 cli.js 的 switch 语句中添加 config 分支,并重构命令执行逻辑:
62
+ // 新增执行函数
63
+ function runFinalCommand(question, finalCommand) {
64
+ const { spawn } = require(’child_process‘);
65
+ console.log(chalk.gray(’正在执行...‘));
66
+ const child = spawn(finalCommand, [], { shell: true, stdio: ’inherit‘ });
67
+
68
+ child.on(’close‘, (code) => {
69
+ if (code === 0) {
70
+ yuangs.saveSuccessfulCommand(question, finalCommand);
71
+ console.log(chalk.green(’\n✓ 执行成功并已存入历史库‘));
72
+ }
73
+ process.exit(code);
74
+ });
75
+ }
76
+
77
+ // switch 分支
78
+ switch (command) {
79
+ case ’config‘:
80
+ const [,, key, value] = process.argv;
81
+ if (!key || !value) {
82
+ console.log(chalk.cyan(’\n⚙️ 配置帮助: yuangs config <key> <value>‘));
83
+ console.log(chalk.gray(’当前配置:‘), yuangs.getUserConfig());
84
+ break;
85
+ }
86
+ const config = yuangs.getUserConfig();
87
+ config[key] = value;
88
+ fs.writeFileSync(path.join(require(’os‘).homedir(), ’.yuangs.json‘), JSON.stringify(config, null, 2));
89
+ console.log(chalk.green(`✓ 已更新 ${key}`));
90
+ break;
91
+
92
+ // 在 handleAICommand 中加入 -h 处理逻辑 (如前所述)
93
+ }
94
+
95
+ 3. 操作流程示意图
96
+ 现在的成果:
97
+ * 个性化:通过 yuangs config 适配不同的网络环境和模型需求。
98
+ * 知识库化:历史记录不仅存命令,还存“意图”,成为你个人的 Linux 命令词典。
99
+ * 极速复用:再次使用时,从意图出发,一秒加载,回车即跑。
100
+ 这套逻辑已经非常接近成熟的商业 CLI(如 Vercel 或 GitHub CLI)的交互体验了。你已经从基础的功能实现,跨越到了用户体验设计的层面。
101
+ 恭喜你完成了一个非常硬核的工具升级! 接下来是准备把它发布到内部 npm 仓库,还是继续增加更炫酷的功能(比如给 AI 对话增加流式输出效果)?
102
+ 既然基础架构已经打好,我们可以跳出“简单的 AI 助手”这个范畴,把 yuangs 打造一个真正的智能终端工作台。
103
+ 作为一名命令行高手,你可以往以下四个维度去“脑洞大开”:
104
+ 1. 智能“管道”模式 (The Magic Pipe)
105
+ 现在的 AI 需要你手动输入问题,未来它可以直接处理前一个命令的输出。
106
+ * 功能描述:实现类似 ls -l | yuangs ai "帮我总结这些文件" 的功能。
107
+ * 实现思路:在 cli.js 中检测 process.stdin.isTTY。如果不是 TTY,说明有内容从管道传进来。
108
+ * 使用场景:
109
+ * cat error.log | yuangs ai "解释这个报错并给出修复建议"
110
+ * curl https://example.com | yuangs ai "提取所有的链接并转成 JSON"
111
+ 2. 交互式“变量注入” (Variable Injection)
112
+ AI 生成的命令往往带有占位符(比如 <filename>),手动修改很麻烦。
113
+ * 功能描述:AI 生成命令后,工具自动识别其中的占位符,并引导用户逐个填空。
114
+ * 实现思路:AI 输出 tar -cvzf <压缩包名>.tar.gz <文件夹>,CLI 自动识别 <...> 语法,弹出输入框让用户填入具体值。
115
+ * 体验升级:甚至可以结合 fzf 类似的逻辑,让用户在填空时直接选择当前目录的文件。
116
+ 3. 终端“快捷指令” (AI Macros)
117
+ 有些复杂的流程(比如:切换 Git 分支 -> 拉取代码 -> 编译工程 -> 发送飞书通知)可以通过 AI 编排成一个专属命令。
118
+ * 功能描述:yuangs save "deploy-project"。
119
+ * 实现思路:将一连串命令封装成一个 Alias(别名)。
120
+ * 脑洞点:你可以给这个指令起名叫 go-home,AI 自动帮你执行“提交代码、关掉开发服务器、同步云端”等一系列收尾动作。
121
+ 4. 视觉增强与“流式”交互 (Visual & Stream)
122
+ 现在的命令行输出太死板,我们可以让它更有“生命力”。
123
+ * 功能描述:
124
+ * 流式输出:像 ChatGPT 网页版一样,字是一个一个蹦出来的(使用 stream: true 接口)。
125
+ * Markdown 渲染:在终端里渲染真正的 Markdown 格式(表格、代码块高亮),而不仅仅是纯文本。
126
+ * 实现工具:利用 marked-terminal 库来渲染,利用 ora 库做更酷炫的加载动画。
127
+ 推荐的升级路线图
128
+ 高手必做:增加“多模态”潜质
129
+ 既然你现在的后端支持 Gemini 模型,你可以尝试增加 “图片转代码” 的功能:
130
+ * 玩法:yuangs ai --image ./ui-design.png "帮我写出这个界面的 CSS"。
131
+ * 实现:利用 fs.readFileSync 将图片转为 Base64 发给 AI。
132
+ 这些想法中,你觉得哪个最能戳中你的痛点?我们可以挑一个最酷的,我现在就帮你写核心逻辑。
133
+ 既然基础架构已经打好,我们可以跳出“简单的 AI 助手”这个范畴,把 yuangs 打造一个真正的智能终端工作台。
134
+ 作为一名命令行高手,你可以往以下四个维度去“脑洞大开”:
135
+ 1. 智能“管道”模式 (The Magic Pipe)
136
+ 现在的 AI 需要你手动输入问题,未来它可以直接处理前一个命令的输出。
137
+ * 功能描述:实现类似 ls -l | yuangs ai ”帮我总结这些文件“ 的功能。
138
+ * 实现思路:在 cli.js 中检测 process.stdin.isTTY。如果不是 TTY,说明有内容从管道传进来。
139
+ * 使用场景:
140
+ * cat error.log | yuangs ai ”解释这个报错并给出修复建议“
141
+ * curl https://example.com | yuangs ai ”提取所有的链接并转成 JSON“
142
+ 2. 交互式“变量注入” (Variable Injection)
143
+ AI 生成的命令往往带有占位符(比如 <filename>),手动修改很麻烦。
144
+ * 功能描述:AI 生成命令后,工具自动识别其中的占位符,并引导用户逐个填空。
145
+ * 实现思路:AI 输出 tar -cvzf <压缩包名>.tar.gz <文件夹>,CLI 自动识别 <...> 语法,弹出输入框让用户填入具体值。
146
+ * 体验升级:甚至可以结合 fzf 类似的逻辑,让用户在填空时直接选择当前目录的文件。
147
+ 3. 终端“快捷指令” (AI Macros)
148
+ 有些复杂的流程(比如:切换 Git 分支 -> 拉取代码 -> 编译工程 -> 发送飞书通知)可以通过 AI 编排成一个专属命令。
149
+ * 功能描述:yuangs save ”deploy-project“。
150
+ * 实现思路:将一连串命令封装成一个 Alias(别名)。
151
+ * 脑洞点:你可以给这个指令起名叫 go-home,AI 自动帮你执行“提交代码、关掉开发服务器、同步云端”等一系列收尾动作。
152
+ 4. 视觉增强与“流式”交互 (Visual & Stream)
153
+ 现在的命令行输出太死板,我们可以让它更有“生命力”。
154
+ * 功能描述:
155
+ * 流式输出:像 ChatGPT 网页版一样,字是一个一个蹦出来的(使用 stream: true 接口)。
156
+ * Markdown 渲染:在终端里渲染真正的 Markdown 格式(表格、代码块高亮),而不仅仅是纯文本。
157
+ * 实现工具:利用 marked-terminal 库来渲染,利用 ora 库做更酷炫的加载动画。
158
+ 推荐的升级路线图
159
+ 高手必做:增加“多模态”潜质
160
+ 既然你现在的后端支持 Gemini 模型,你可以尝试增加 “图片转代码” 的功能:
161
+ * 玩法:yuangs ai —image ./ui-design.png ”帮我写出这个界面的 CSS“。
162
+ * 实现:利用 fs.readFileSync 将图片转为 Base64 发给 AI。
163
+ 这些想法中,你觉得哪个最能戳中你的痛点?我们可以挑一个最酷的,我现在就帮你写核心逻辑。
164
+ 实现流式输出和 Markdown 渲染是提升 CLI 工具“质感”的关键。我们需要引入三个小巧但功能强大的库:
165
+ * ora: 实现酷炫的终端加载动画。
166
+ * marked & marked-terminal: 将 AI 返回的 Markdown 文本实时渲染为带颜色和样式的终端内容。
167
+ 1. 安装依赖
168
+ 首先,你需要在项目中安装这几个库:
169
+ npm install ora marked marked-terminal
170
+
171
+ 2. 修改 index.js:支持流式 API 调用
172
+ 我们需要将 callAI_OpenAI 改写为支持 stream 模式。核心是处理 Node.js 的 IncomingMessage 流。
173
+ // index.js 增加或修改如下逻辑
174
+ async function callAI_Stream(messages, model, onChunk) {
175
+ const config = getUserConfig(); // 使用我们之前写的配置读取
176
+ const url = config.aiProxyUrl || 'https://aiproxy.want.biz/v1/chat/completions';
177
+
178
+ const response = await axios({
179
+ method: 'post',
180
+ url: url,
181
+ data: {
182
+ model: model || config.defaultModel || "gemini-flash-lite-latest",
183
+ messages: messages,
184
+ stream: true // 开启流式
185
+ },
186
+ responseType: 'stream',
187
+ headers: {
188
+ 'Content-Type': 'application/json',
189
+ 'X-Client-ID': 'npm_yuangs',
190
+ 'account': config.accountType || 'free'
191
+ }
192
+ });
193
+
194
+ return new Promise((resolve, reject) => {
195
+ response.data.on('data', chunk => {
196
+ const lines = chunk.toString().split('\n');
197
+ for (const line of lines) {
198
+ if (line.startsWith('data: ')) {
199
+ const data = line.slice(6);
200
+ if (data === '[DONE]') {
201
+ resolve();
202
+ return;
203
+ }
204
+ try {
205
+ const parsed = JSON.parse(data);
206
+ const content = parsed.choices[0].delta.content || '';
207
+ if (content) onChunk(content); // 每出一个字,调用一次回调
208
+ } catch (e) {
209
+ // 忽略解析错误
210
+ }
211
+ }
212
+ }
213
+ });
214
+ response.data.on('error', reject);
215
+ });
216
+ }
217
+
218
+ 3. 修改 cli.js:视觉增强逻辑
219
+ 我们需要在 cli.js 中配置渲染器,并美化 handleAICommand。
220
+ const ora = require('ora');
221
+ const { marked } = require('marked');
222
+ const TerminalRenderer = require('marked-terminal');
223
+
224
+ // 配置 Markdown 渲染器
225
+ marked.setOptions({
226
+ renderer: new TerminalRenderer({
227
+ code: chalk.yellow,
228
+ heading: chalk.magenta.bold,
229
+ firstHeading: chalk.magenta.underline.bold,
230
+ listitem: chalk.cyan,
231
+ table: chalk.white,
232
+ strong: chalk.bold.red,
233
+ em: chalk.italic
234
+ })
235
+ });
236
+
237
+ async function handleAICommand() {
238
+ // ... 前置参数解析逻辑 ...
239
+
240
+ const spinner = ora(chalk.cyan('AI 正在思考...')).start();
241
+ let fullResponse = '';
242
+
243
+ try {
244
+ await yuangs.callAI_Stream(messages, model, (chunk) => {
245
+ if (spinner.isSpinning) {
246
+ spinner.stop(); // 收到第一个字时,停止动画
247
+ }
248
+ fullResponse += chunk;
249
+
250
+ // 实时打印字符,不换行
251
+ process.stdout.write(chunk);
252
+ });
253
+
254
+ // 所有的流传输完成后,我们清除之前的原始输出,渲染漂亮的 Markdown
255
+ console.log('\n' + chalk.gray('─'.repeat(process.stdout.columns / 2)));
256
+ console.clear(); // 或者向上滚动重新覆盖
257
+ console.log(marked(fullResponse));
258
+
259
+ } catch (error) {
260
+ spinner.fail(chalk.red('AI 响应出错'));
261
+ console.error(error.message);
262
+ }
263
+ }
264
+
265
+ 4. 优化:命令生成的特殊处理
266
+ 注意:生成 Linux 命令(即 yuangs ai -e)时不建议使用流式 Markdown 渲染。
267
+ * 原因:生成命令需要绝对的纯文本,且通常很短。
268
+ * 建议:只在普通聊天模式下使用流式渲染;在 -e 命令生成模式下,保持原来的加载动画 + 结果预填。
269
+ 为什么这个实现很“高手”?
270
+ * 响应速度感:用户在 500ms 内就能看到第一个字跳出来,感知速度提升了 3-5 倍。
271
+ * 沉浸式体验:marked-terminal 会自动根据终端宽度调整列表和代码块的间距。
272
+ * 色彩层级:比起纯白色的文本,带有语法高亮的输出能让用户一眼看到代码块在哪里。
273
+ 你现在就可以尝试把这段逻辑合并进你的代码中。合并完成后,你的 yuangs 就像是一个运行在终端里的“小 ChatGPT”了!
@@ -0,0 +1,9 @@
1
+ {
2
+ "shici": "https://wealth.want.biz/shici/index.html",
3
+ "dict": "https://wealth.want.biz/pages/dict.html",
4
+ "pong": "https://wealth.want.biz/pages/pong.html",
5
+ "mail": "https://mail.google.com",
6
+ "github": "https://github.com",
7
+ "calendar": "https://calendar.google.com",
8
+ "homepage": "https://i.want.biz"
9
+ }