@fivetu53/soul-chat 1.1.0 → 1.1.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.
Files changed (2) hide show
  1. package/bin/index.js +263 -11
  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
 
@@ -454,7 +462,12 @@ async function showChat() {
454
462
  try {
455
463
  const history = await getMessages(currentConversationId);
456
464
  history.forEach(m => {
457
- messages.push({ role: m.role === 'user' ? 'user' : 'bot', text: m.content, id: m.id });
465
+ messages.push({
466
+ role: m.role === 'user' ? 'user' : 'bot',
467
+ text: m.content,
468
+ id: m.id,
469
+ timestamp: m.created_at ? new Date(m.created_at) : null
470
+ });
458
471
  });
459
472
  } catch (err) {
460
473
  // 忽略
@@ -471,9 +484,15 @@ async function showChat() {
471
484
  console.log(c('secondary', c('bold', ` [*] 与 ${currentCharacter.name} 聊天`)));
472
485
  console.log();
473
486
 
474
- messages.slice(-12).forEach(msg => {
487
+ messages.slice(-20).forEach(msg => {
488
+ const timeStr = msg.timestamp
489
+ ? c('gray', ` [${msg.timestamp.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'})}]`)
490
+ : '';
475
491
  if (msg.role === 'user') {
476
- console.log(c('user', ` ${currentUser.username}: ${msg.text}`));
492
+ console.log(c('user', ` ${currentUser.username}${timeStr}: ${msg.text}`));
493
+ } else if (msg.role === 'system') {
494
+ // 系统消息(导出、搜索等)
495
+ console.log(c('cyan', ` ${msg.text}`));
477
496
  } else {
478
497
  // 渲染 markdown
479
498
  const rendered = marked.parse(msg.text).trim();
@@ -519,7 +538,107 @@ async function showChat() {
519
538
  continue;
520
539
  }
521
540
 
522
- messages.push({ role: 'user', text: input });
541
+ if (input === '/help') {
542
+ console.log(c('cyan', '\n 可用命令:'));
543
+ console.log(c('gray', ' /back - 返回主菜单'));
544
+ console.log(c('gray', ' /clear - 清空对话'));
545
+ console.log(c('gray', ' /delete - 撤回最后一轮'));
546
+ console.log(c('gray', ' /regen - 重新生成回复'));
547
+ console.log(c('gray', ' /export - 导出对话'));
548
+ console.log(c('gray', ' Ctrl+C - 清空输入 (双击退出)'));
549
+ console.log(c('gray', ' Ctrl+V - 粘贴图片'));
550
+ await sleep(2000);
551
+ continue;
552
+ }
553
+
554
+ if (input === '/regen') {
555
+ // 重新生成最后一条回复
556
+ if (messages.length >= 2) {
557
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
558
+ if (lastUserMsg) {
559
+ // 删除服务端最后两条
560
+ if (currentConversationId) {
561
+ try { await deleteLastMessages(currentConversationId); } catch {}
562
+ }
563
+ messages.splice(-2);
564
+
565
+ // 重新发送用户消息
566
+ const regenText = lastUserMsg.text;
567
+ messages.push({ role: 'user', text: regenText, timestamp: new Date() });
568
+ drawMessages();
569
+
570
+ // 显示思考中
571
+ process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '思考中...'));
572
+
573
+ try {
574
+ const response = await sendMessage(regenText, currentCharacter.id, currentConversationId, null);
575
+ process.stdout.write('\r\x1b[K');
576
+ process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
577
+
578
+ let fullResponse = '';
579
+ const reader = response.body.getReader();
580
+ const decoder = new TextDecoder();
581
+ let buffer = '';
582
+
583
+ while (true) {
584
+ const { done, value } = await reader.read();
585
+ if (done) break;
586
+ buffer += decoder.decode(value, { stream: true });
587
+ const lines = buffer.split('\n');
588
+ buffer = lines.pop() || '';
589
+
590
+ for (const line of lines) {
591
+ if (line.startsWith('data: ')) {
592
+ const data = line.slice(6);
593
+ if (data === '[DONE]') continue;
594
+ try {
595
+ const json = JSON.parse(data);
596
+ if (json.type === 'info') {
597
+ currentConversationId = json.conversationId;
598
+ } else if (json.type === 'content') {
599
+ fullResponse += json.content;
600
+ process.stdout.write(json.content);
601
+ }
602
+ } catch {}
603
+ }
604
+ }
605
+ }
606
+
607
+ console.log();
608
+ messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
609
+ drawMessages();
610
+ } catch (err) {
611
+ process.stdout.write('\r\x1b[K');
612
+ console.log(c('red', ` 错误: ${err.message}`));
613
+ await sleep(1500);
614
+ }
615
+ }
616
+ }
617
+ continue;
618
+ }
619
+
620
+ if (input === '/export') {
621
+ const filename = `soul-chat-${currentCharacter.name}-${Date.now()}.md`;
622
+ const filepath = `${os.homedir()}/Downloads/${filename}`;
623
+
624
+ let content = `# 与 ${currentCharacter.name} 的对话\n\n`;
625
+ content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n\n---\n\n`;
626
+
627
+ messages.filter(m => m.role !== 'system').forEach(msg => {
628
+ const role = msg.role === 'user' ? currentUser.username : currentCharacter.name;
629
+ content += `**${role}**:\n${msg.text}\n\n`;
630
+ });
631
+
632
+ try {
633
+ fs.writeFileSync(filepath, content);
634
+ messages.push({ role: 'system', text: `✓ 已导出到: ${filepath}` });
635
+ } catch (err) {
636
+ messages.push({ role: 'system', text: `✗ 导出失败: ${err.message}` });
637
+ }
638
+ continue;
639
+ }
640
+
641
+ messages.push({ role: 'user', text: input, timestamp: new Date() });
523
642
  drawMessages();
524
643
 
525
644
  // 显示思考中
@@ -562,14 +681,16 @@ async function showChat() {
562
681
  if (json.type === 'info') {
563
682
  currentConversationId = json.conversationId;
564
683
  } else if (json.type === 'tool_call') {
565
- // 显示搜索状态
684
+ // 显示搜索状态并添加到消息
566
685
  process.stdout.write('\r\x1b[K');
567
686
  process.stdout.write(c('cyan', ` 🔍 正在搜索: ${json.query}...`));
687
+ messages.push({ role: 'system', text: `🔍 正在搜索: ${json.query}` });
568
688
  } else if (json.type === 'tool_result') {
569
- // 显示搜索结果数量
689
+ // 显示搜索结果数量并添加到消息
570
690
  process.stdout.write('\r\x1b[K');
571
691
  process.stdout.write(c('green', ` 📋 找到 ${json.count} 条结果\n`));
572
692
  process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
693
+ messages.push({ role: 'system', text: `📋 找到 ${json.count} 条结果` });
573
694
  } else if (json.type === 'content') {
574
695
  fullResponse += json.content;
575
696
  // 打字机效果:直接输出内容
@@ -585,7 +706,7 @@ async function showChat() {
585
706
  }
586
707
 
587
708
  console.log(); // 结束当前行
588
- messages.push({ role: 'bot', text: fullResponse || '...' });
709
+ messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
589
710
  // 重绘消息列表(带 markdown 渲染)
590
711
  drawMessages();
591
712
  await sleep(100); // 短暂延迟让用户看到渲染效果
@@ -734,13 +855,24 @@ function prompt(query) {
734
855
 
735
856
  // 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
736
857
  let pendingImage = null; // 待发送的图片
858
+ let lastCtrlCTime = 0; // 上次 Ctrl+C 时间
859
+
860
+ // 斜杠命令列表
861
+ const slashCommands = [
862
+ { cmd: '/back', desc: '返回主菜单' },
863
+ { cmd: '/clear', desc: '清空对话' },
864
+ { cmd: '/delete', desc: '撤回最后一轮' },
865
+ { cmd: '/regen', desc: '重新生成回复' },
866
+ { cmd: '/export', desc: '导出对话' },
867
+ { cmd: '/help', desc: '显示帮助' },
868
+ ];
737
869
 
738
870
  function chatPrompt(query) {
739
871
  return new Promise((resolve) => {
740
872
  const stdin = process.stdin;
741
873
 
742
874
  // 先打印下方提示
743
- console.log(c('hint', ' /back 返回 /clear 清空 /delete 撤回 Ctrl+V 粘贴图片'));
875
+ console.log(c('hint', ' 输入 / 选择命令 Ctrl+C 清空 (双击退出) Ctrl+V 粘贴图片'));
744
876
  console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
745
877
  // 光标上移3行,显示输入提示
746
878
  process.stdout.write('\x1b[3A');
@@ -786,11 +918,29 @@ function chatPrompt(query) {
786
918
  return;
787
919
  }
788
920
 
789
- // Ctrl+C
921
+ // Ctrl+C - 单击清空,双击退出
790
922
  if (char === '\u0003') {
923
+ if (now - lastCtrlCTime < 500) {
924
+ // 双击退出
925
+ stdin.removeListener('data', onData);
926
+ stdin.setRawMode(false);
927
+ cleanup();
928
+ process.exit();
929
+ } else {
930
+ // 单击清空输入
931
+ lastCtrlCTime = now;
932
+ process.stdout.write('\r' + query + ' '.repeat(input.length) + '\r' + query);
933
+ input = '';
934
+ }
935
+ return;
936
+ }
937
+
938
+ // 斜杠命令选择器
939
+ if (char === '/' && input === '') {
791
940
  stdin.removeListener('data', onData);
792
- stdin.setRawMode(false);
793
- process.exit();
941
+ // 显示命令选择器
942
+ showCommandSelector(stdin, query, resolve);
943
+ return;
794
944
  }
795
945
 
796
946
  // 退格
@@ -816,6 +966,108 @@ function chatPrompt(query) {
816
966
  });
817
967
  }
818
968
 
969
+ // 斜杠命令选择器
970
+ function showCommandSelector(stdin, query, resolve) {
971
+ let selected = 0;
972
+
973
+ // 清除当前行并显示命令列表
974
+ process.stdout.write('\r\x1b[K');
975
+ console.log();
976
+
977
+ const drawCommands = () => {
978
+ // 上移到命令列表开始位置
979
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
980
+ slashCommands.forEach((cmd, i) => {
981
+ const prefix = i === selected ? c('cyan', '> ') : ' ';
982
+ const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
983
+ console.log(` ${prefix}${cmdText} ${c('gray', cmd.desc)}\x1b[K`);
984
+ });
985
+ };
986
+
987
+ // 初始绘制
988
+ slashCommands.forEach((cmd, i) => {
989
+ const prefix = i === selected ? c('cyan', '> ') : ' ';
990
+ const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
991
+ console.log(` ${prefix}${cmdText} ${c('gray', cmd.desc)}`);
992
+ });
993
+
994
+ const onSelect = (key) => {
995
+ // 上下键
996
+ if (key[0] === 27 && key[1] === 91) {
997
+ if (key[2] === 65) { // 上
998
+ selected = selected > 0 ? selected - 1 : slashCommands.length - 1;
999
+ drawCommands();
1000
+ } else if (key[2] === 66) { // 下
1001
+ selected = selected < slashCommands.length - 1 ? selected + 1 : 0;
1002
+ drawCommands();
1003
+ }
1004
+ return;
1005
+ }
1006
+
1007
+ // Enter - 选择命令
1008
+ if (key[0] === 13) {
1009
+ stdin.removeListener('data', onSelect);
1010
+ stdin.setRawMode(false);
1011
+ // 清除命令列表
1012
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
1013
+ for (let i = 0; i < slashCommands.length; i++) {
1014
+ console.log('\x1b[K');
1015
+ }
1016
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
1017
+ resolve(slashCommands[selected].cmd);
1018
+ return;
1019
+ }
1020
+
1021
+ // Esc - 取消
1022
+ if (key[0] === 27 && key.length === 1) {
1023
+ stdin.removeListener('data', onSelect);
1024
+ // 清除命令列表,重新开始输入
1025
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
1026
+ for (let i = 0; i < slashCommands.length; i++) {
1027
+ console.log('\x1b[K');
1028
+ }
1029
+ process.stdout.write(`\x1b[${slashCommands.length}A`);
1030
+ process.stdout.write(query);
1031
+ // 重新监听输入
1032
+ let input = '';
1033
+ const onData = (chunk) => {
1034
+ const char = chunk.toString();
1035
+ if (char === '\r' || char === '\n') {
1036
+ stdin.removeListener('data', onData);
1037
+ stdin.setRawMode(false);
1038
+ console.log();
1039
+ resolve(input);
1040
+ } else if (char === '\u0003') {
1041
+ stdin.removeListener('data', onData);
1042
+ stdin.setRawMode(false);
1043
+ cleanup();
1044
+ process.exit();
1045
+ } else if (char === '\u007f' || char === '\b') {
1046
+ if (input.length > 0) {
1047
+ input = input.slice(0, -1);
1048
+ process.stdout.write('\b \b');
1049
+ }
1050
+ } else if (char >= ' ') {
1051
+ input += char;
1052
+ process.stdout.write(char);
1053
+ }
1054
+ };
1055
+ stdin.on('data', onData);
1056
+ return;
1057
+ }
1058
+
1059
+ // Ctrl+C - 退出
1060
+ if (key[0] === 3) {
1061
+ stdin.removeListener('data', onSelect);
1062
+ stdin.setRawMode(false);
1063
+ cleanup();
1064
+ process.exit();
1065
+ }
1066
+ };
1067
+
1068
+ stdin.on('data', onSelect);
1069
+ }
1070
+
819
1071
  function cleanup() {
820
1072
  showCursor();
821
1073
  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.2",
4
4
  "description": "Soul Chat - 智能 AI 伴侣命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {