@fivetu53/soul-chat 1.1.0 → 1.1.1

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 (2) hide show
  1. package/bin/index.js +206 -8
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -267,6 +267,14 @@ async function showMenu() {
267
267
  } else if (key[0] === 3) { // Ctrl+C
268
268
  cleanup();
269
269
  process.exit();
270
+ } else {
271
+ // 数字快捷键 1-6
272
+ const num = key[0] - 48;
273
+ if (num >= 1 && num <= menuItems.length) {
274
+ process.stdin.setRawMode(false);
275
+ process.stdin.removeListener('data', onKeyPress);
276
+ resolve(menuItems[num - 1].value);
277
+ }
270
278
  }
271
279
  };
272
280
 
@@ -471,9 +479,12 @@ async function showChat() {
471
479
  console.log(c('secondary', c('bold', ` [*] 与 ${currentCharacter.name} 聊天`)));
472
480
  console.log();
473
481
 
474
- messages.slice(-12).forEach(msg => {
482
+ messages.slice(-20).forEach(msg => {
483
+ const timeStr = msg.timestamp
484
+ ? c('gray', ` [${msg.timestamp.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'})}]`)
485
+ : '';
475
486
  if (msg.role === 'user') {
476
- console.log(c('user', ` ${currentUser.username}: ${msg.text}`));
487
+ console.log(c('user', ` ${currentUser.username}${timeStr}: ${msg.text}`));
477
488
  } else {
478
489
  // 渲染 markdown
479
490
  const rendered = marked.parse(msg.text).trim();
@@ -519,7 +530,63 @@ async function showChat() {
519
530
  continue;
520
531
  }
521
532
 
522
- messages.push({ role: 'user', text: input });
533
+ if (input === '/help') {
534
+ console.log(c('cyan', '\n 可用命令:'));
535
+ console.log(c('gray', ' /back - 返回主菜单'));
536
+ console.log(c('gray', ' /clear - 清空对话'));
537
+ console.log(c('gray', ' /delete - 撤回最后一轮'));
538
+ console.log(c('gray', ' /regen - 重新生成回复'));
539
+ console.log(c('gray', ' /export - 导出对话'));
540
+ console.log(c('gray', ' Ctrl+C - 清空输入 (双击退出)'));
541
+ console.log(c('gray', ' Ctrl+V - 粘贴图片'));
542
+ await sleep(2000);
543
+ continue;
544
+ }
545
+
546
+ if (input === '/regen') {
547
+ // 重新生成最后一条回复
548
+ if (messages.length >= 2) {
549
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
550
+ if (lastUserMsg) {
551
+ // 删除服务端最后两条
552
+ if (currentConversationId) {
553
+ try { await deleteLastMessages(currentConversationId); } catch {}
554
+ }
555
+ messages.splice(-2);
556
+ // 重新发送用户消息
557
+ input = lastUserMsg.text;
558
+ // 不 continue,让后续代码处理发送
559
+ } else {
560
+ continue;
561
+ }
562
+ } else {
563
+ continue;
564
+ }
565
+ }
566
+
567
+ if (input === '/export') {
568
+ const filename = `soul-chat-${currentCharacter.name}-${Date.now()}.md`;
569
+ const filepath = `${os.homedir()}/Downloads/${filename}`;
570
+
571
+ let content = `# 与 ${currentCharacter.name} 的对话\n\n`;
572
+ content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n\n---\n\n`;
573
+
574
+ messages.forEach(msg => {
575
+ const role = msg.role === 'user' ? currentUser.username : currentCharacter.name;
576
+ content += `**${role}**:\n${msg.text}\n\n`;
577
+ });
578
+
579
+ try {
580
+ fs.writeFileSync(filepath, content);
581
+ console.log(c('green', ` ✓ 已导出到: ${filepath}`));
582
+ } catch (err) {
583
+ console.log(c('red', ` 导出失败: ${err.message}`));
584
+ }
585
+ await sleep(1500);
586
+ continue;
587
+ }
588
+
589
+ messages.push({ role: 'user', text: input, timestamp: new Date() });
523
590
  drawMessages();
524
591
 
525
592
  // 显示思考中
@@ -585,7 +652,7 @@ async function showChat() {
585
652
  }
586
653
 
587
654
  console.log(); // 结束当前行
588
- messages.push({ role: 'bot', text: fullResponse || '...' });
655
+ messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
589
656
  // 重绘消息列表(带 markdown 渲染)
590
657
  drawMessages();
591
658
  await sleep(100); // 短暂延迟让用户看到渲染效果
@@ -734,13 +801,24 @@ function prompt(query) {
734
801
 
735
802
  // 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
736
803
  let pendingImage = null; // 待发送的图片
804
+ let lastCtrlCTime = 0; // 上次 Ctrl+C 时间
805
+
806
+ // 斜杠命令列表
807
+ const slashCommands = [
808
+ { cmd: '/back', desc: '返回主菜单' },
809
+ { cmd: '/clear', desc: '清空对话' },
810
+ { cmd: '/delete', desc: '撤回最后一轮' },
811
+ { cmd: '/regen', desc: '重新生成回复' },
812
+ { cmd: '/export', desc: '导出对话' },
813
+ { cmd: '/help', desc: '显示帮助' },
814
+ ];
737
815
 
738
816
  function chatPrompt(query) {
739
817
  return new Promise((resolve) => {
740
818
  const stdin = process.stdin;
741
819
 
742
820
  // 先打印下方提示
743
- console.log(c('hint', ' /back 返回 /clear 清空 /delete 撤回 Ctrl+V 粘贴图片'));
821
+ console.log(c('hint', ' 输入 / 选择命令 Ctrl+C 清空 (双击退出) Ctrl+V 粘贴图片'));
744
822
  console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
745
823
  // 光标上移3行,显示输入提示
746
824
  process.stdout.write('\x1b[3A');
@@ -786,11 +864,29 @@ function chatPrompt(query) {
786
864
  return;
787
865
  }
788
866
 
789
- // Ctrl+C
867
+ // Ctrl+C - 单击清空,双击退出
790
868
  if (char === '\u0003') {
869
+ if (now - lastCtrlCTime < 500) {
870
+ // 双击退出
871
+ stdin.removeListener('data', onData);
872
+ stdin.setRawMode(false);
873
+ cleanup();
874
+ process.exit();
875
+ } else {
876
+ // 单击清空输入
877
+ lastCtrlCTime = now;
878
+ process.stdout.write('\r' + query + ' '.repeat(input.length) + '\r' + query);
879
+ input = '';
880
+ }
881
+ return;
882
+ }
883
+
884
+ // 斜杠命令选择器
885
+ if (char === '/' && input === '') {
791
886
  stdin.removeListener('data', onData);
792
- stdin.setRawMode(false);
793
- process.exit();
887
+ // 显示命令选择器
888
+ showCommandSelector(stdin, query, resolve);
889
+ return;
794
890
  }
795
891
 
796
892
  // 退格
@@ -816,6 +912,108 @@ function chatPrompt(query) {
816
912
  });
817
913
  }
818
914
 
915
+ // 斜杠命令选择器
916
+ function showCommandSelector(stdin, query, resolve) {
917
+ let selected = 0;
918
+
919
+ // 清除当前行并显示命令列表
920
+ process.stdout.write('\r\x1b[K');
921
+ console.log();
922
+
923
+ const drawCommands = () => {
924
+ // 上移到命令列表开始位置
925
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
926
+ slashCommands.forEach((cmd, i) => {
927
+ const prefix = i === selected ? c('cyan', '> ') : ' ';
928
+ const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
929
+ console.log(` ${prefix}${cmdText} ${c('gray', cmd.desc)}\x1b[K`);
930
+ });
931
+ };
932
+
933
+ // 初始绘制
934
+ slashCommands.forEach((cmd, i) => {
935
+ const prefix = i === selected ? c('cyan', '> ') : ' ';
936
+ const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
937
+ console.log(` ${prefix}${cmdText} ${c('gray', cmd.desc)}`);
938
+ });
939
+
940
+ const onSelect = (key) => {
941
+ // 上下键
942
+ if (key[0] === 27 && key[1] === 91) {
943
+ if (key[2] === 65) { // 上
944
+ selected = selected > 0 ? selected - 1 : slashCommands.length - 1;
945
+ drawCommands();
946
+ } else if (key[2] === 66) { // 下
947
+ selected = selected < slashCommands.length - 1 ? selected + 1 : 0;
948
+ drawCommands();
949
+ }
950
+ return;
951
+ }
952
+
953
+ // Enter - 选择命令
954
+ if (key[0] === 13) {
955
+ stdin.removeListener('data', onSelect);
956
+ stdin.setRawMode(false);
957
+ // 清除命令列表
958
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
959
+ for (let i = 0; i < slashCommands.length; i++) {
960
+ console.log('\x1b[K');
961
+ }
962
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
963
+ resolve(slashCommands[selected].cmd);
964
+ return;
965
+ }
966
+
967
+ // Esc - 取消
968
+ if (key[0] === 27 && key.length === 1) {
969
+ stdin.removeListener('data', onSelect);
970
+ // 清除命令列表,重新开始输入
971
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
972
+ for (let i = 0; i < slashCommands.length; i++) {
973
+ console.log('\x1b[K');
974
+ }
975
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
976
+ process.stdout.write(query);
977
+ // 重新监听输入
978
+ let input = '';
979
+ const onData = (chunk) => {
980
+ const char = chunk.toString();
981
+ if (char === '\r' || char === '\n') {
982
+ stdin.removeListener('data', onData);
983
+ stdin.setRawMode(false);
984
+ console.log();
985
+ resolve(input);
986
+ } else if (char === '\u0003') {
987
+ stdin.removeListener('data', onData);
988
+ stdin.setRawMode(false);
989
+ cleanup();
990
+ process.exit();
991
+ } else if (char === '\u007f' || char === '\b') {
992
+ if (input.length > 0) {
993
+ input = input.slice(0, -1);
994
+ process.stdout.write('\b \b');
995
+ }
996
+ } else if (char >= ' ') {
997
+ input += char;
998
+ process.stdout.write(char);
999
+ }
1000
+ };
1001
+ stdin.on('data', onData);
1002
+ return;
1003
+ }
1004
+
1005
+ // Ctrl+C - 退出
1006
+ if (key[0] === 3) {
1007
+ stdin.removeListener('data', onSelect);
1008
+ stdin.setRawMode(false);
1009
+ cleanup();
1010
+ process.exit();
1011
+ }
1012
+ };
1013
+
1014
+ stdin.on('data', onSelect);
1015
+ }
1016
+
819
1017
  function cleanup() {
820
1018
  showCursor();
821
1019
  clearScreen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivetu53/soul-chat",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Soul Chat - 智能 AI 伴侣命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {