yuangs 1.3.34 → 1.3.38

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/README.md CHANGED
@@ -51,15 +51,61 @@ yuangs ai
51
51
  - 直接输入问题进行对话
52
52
  - 输入 `/clear` 清空对话历史
53
53
  - 输入 `/history` 查看对话历史
54
- ### 命令生成模式(v1.3.32)
54
+ ### 命令生成模式
55
55
 
56
- 使用 `-e` 参数让 AI 为你生成 Linux 命令:
56
+ 使用 `-e` 参数让 AI 为你生成 Linux 命令。现在支持生成后自动复制到剪贴板,并预填到输入行供你确认执行:
57
57
 
58
58
  ```bash
59
59
  yuangs ai -e "查看当前目录下大于100M的文件"
60
- # 输出: > find . -type f -size +100M
60
+ # 1. 自动生成: > find . -type f -size +100M
61
+ # 2. 自动复制到剪贴板
62
+ # 3. 预填到下方,按回车即可执行
61
63
  ```
62
64
 
65
+ ### 管道模式 (Pipe Mode)
66
+
67
+ 支持将前一个命令的输出通过管道传给 AI 进行分析:
68
+
69
+ ```bash
70
+ cat error.log | yuangs ai "解释这个报错"
71
+ ls -la | yuangs ai "帮我总结这些文件"
72
+ ```
73
+
74
+ ### 流式输出与 Markdown 渲染
75
+
76
+ 所有 AI 回复默认开启流式输出(打字机效果),并在回答结束后自动进行 Markdown 渲染(包含代码高亮、表格格式化等)。
77
+
78
+ ## 快捷指令 (Macros)
79
+
80
+ 可以将常用的复杂命令保存为快捷指令:
81
+
82
+ ```bash
83
+ # 创建快捷指令 (支持多行命令)
84
+ yuangs save deploy
85
+ # > 请输入要保存的命令:
86
+ # > npm run build && git add . && git commit -m "deploy" && git push
87
+
88
+ # 执行快捷指令
89
+ yuangs run deploy
90
+
91
+ # 查看所有指令
92
+ yuangs macros
93
+ ```
94
+
95
+ ## 配置管理
96
+
97
+ 使用 `config` 命令快速修改 AI 配置:
98
+
99
+ ```bash
100
+ yuangs config defaultModel Assistant
101
+ yuangs config accountType pro
102
+ ```
103
+
104
+ 配置项说明:
105
+ - `defaultModel`: 默认使用的 AI 模型 (默认为 `Assistant`)
106
+ - `aiProxyUrl`: 自定义 AI 接口地址
107
+ - `accountType`: 账户类型 (free/pro)
108
+
63
109
  ## 应用列表
64
110
 
65
111
  以下是 CLI 中可以通过相应命令直接打开的应用程序链接:
@@ -126,9 +172,12 @@ yuangs mail # 打开邮箱
126
172
 
127
173
  ## 近期主要更新日志
128
174
 
129
- ### v1.3.32 (2026-01-16)
130
- - **新增** AI 命令生成模式:使用 `yuangs ai -e <描述>` 快速生成 Linux 命令。
131
- - **优化** AI 接口升级:全面迁移至 OpenAI 兼容接口,提升稳定性。
175
+ ### v1.3.38 (2026-01-16)
176
+ - **新增** 快捷指令系统 (`save`/`run`/`macros`),支持保存复杂命令。
177
+ - **新增** 管道模式:支持 `cat file | yuangs ai` 分析内容。
178
+ - **新增** AI 流式输出 + Markdown 终端渲染,体验对齐 ChatGPT 网页版。
179
+ - **新增** `config` 命令,方便管理 AI 配置。
180
+ - **优化** 命令生成模式 (`-e`) 增加剪贴板自动复制和预填执行功能。
132
181
 
133
182
  ### v1.3.22 (2025-11-30)
134
183
  - **新增** AI 命令支持 `-p` `-f` `-l` 简写,快速选择gemini默认模型
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.38",
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
+ });
@@ -4,5 +4,8 @@
4
4
  "pong": "https://wealth.want.biz/pages/pong.html",
5
5
  "github": "https://github.com",
6
6
  "calendar": "https://calendar.google.com",
7
- "mail": "https://mail.google.com"
7
+ "mail": "https://mail.google.com",
8
+ "aiProxyUrl": "https://aiproxy.want.biz/v1/chat/completions",
9
+ "defaultModel": "Assistant",
10
+ "accountType": "free"
8
11
  }
@@ -8,6 +8,11 @@ github: "https://github.com"
8
8
  calendar: "https://calendar.google.com"
9
9
  mail: "https://mail.google.com"
10
10
 
11
+ # AI Configuration
12
+ aiProxyUrl: "https://aiproxy.want.biz/v1/chat/completions"
13
+ defaultModel: "Assistant"
14
+ accountType: "free"
15
+
11
16
  # You can also use the apps property if you prefer to group them
12
17
  # apps:
13
18
  # shici: "https://wealth.want.biz/shici/index.html"
@@ -15,4 +20,4 @@ mail: "https://mail.google.com"
15
20
  # pong: "https://wealth.want.biz/pages/pong.html"
16
21
  # github: "https://github.com"
17
22
  # calendar: "https://calendar.google.com"
18
- # mail: "https://mail.google.com"
23
+ # mail: "https://mail.google.com"
@@ -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
+ }
@@ -1,54 +0,0 @@
1
- name: Auto Bump & Publish
2
-
3
- on:
4
- push:
5
- branches:
6
- - main # 如果你的默认分支不是 main,记得改成对应名字
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
-
12
- # 权限:允许 Action 修改仓库代码 + 生成 provenance
13
- permissions:
14
- contents: write
15
- id-token: write
16
-
17
- steps:
18
- - name: Checkout repository
19
- uses: actions/checkout@v4
20
- with:
21
- token: ${{ secrets.GITHUB_TOKEN }}
22
- fetch-depth: 0 # 完整历史,方便推 tag
23
-
24
- - name: Set up Node.js
25
- uses: actions/setup-node@v4
26
- with:
27
- node-version: "18"
28
- registry-url: "https://registry.npmjs.org/"
29
-
30
- - name: Configure Git
31
- run: |
32
- git config user.name "GitHub Action"
33
- git config user.email "action@github.com"
34
-
35
- # 自动 bump patch 版本:1.1.5 -> 1.1.6
36
- # [skip ci] 防止这个 commit 再次触发 workflow 形成死循环
37
- - name: Bump version
38
- run: |
39
- npm version patch -m "苑广山注:自动升版本号%s[skip ci]"
40
-
41
- # 推送 commit + tag 回 GitHub
42
- - name: Push changes
43
- run: |
44
- git push --follow-tags
45
-
46
- # 安装依赖
47
- - name: Install dependencies
48
- run: npm ci
49
-
50
- # 发布到 npm
51
- - name: Publish to npm
52
- run: npm publish --provenance
53
- env:
54
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}