@fivetu53/soul-chat 1.0.9 → 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 (3) hide show
  1. package/bin/api.js +9 -0
  2. package/bin/index.js +210 -22
  3. package/package.json +1 -1
package/bin/api.js CHANGED
@@ -228,6 +228,15 @@ export async function deleteMessage(messageId) {
228
228
  return data;
229
229
  }
230
230
 
231
+ export async function deleteLastMessages(conversationId) {
232
+ const response = await request(`/api/chat/conversations/${conversationId}/last`, {
233
+ method: 'DELETE'
234
+ });
235
+ const data = await response.json();
236
+ if (!response.ok) throw new Error(data.error);
237
+ return data;
238
+ }
239
+
231
240
  // Memory APIs
232
241
  export async function getMemories(characterId) {
233
242
  const response = await request(`/api/memories/${characterId}`);
package/bin/index.js CHANGED
@@ -10,7 +10,7 @@ import { loadConfig, saveConfig, getConfigPath } from './config.js';
10
10
  import { checkAuth, showAuthScreen } from './auth.js';
11
11
  import {
12
12
  isLoggedIn, getCurrentUser, logout, updateProfile,
13
- sendMessage, getCharacters, createCharacter, getConversations, getMessages, clearConversation, deleteMessage, getMemories, pinMemory, deleteMemory
13
+ sendMessage, getCharacters, createCharacter, getConversations, getMessages, clearConversation, deleteLastMessages, getMemories, pinMemory, deleteMemory
14
14
  } from './api.js';
15
15
 
16
16
  // Setup marked with terminal renderer
@@ -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();
@@ -508,28 +519,74 @@ async function showChat() {
508
519
 
509
520
  if (input === '/delete') {
510
521
  // 删除最后一轮对话(用户消息+AI回复)
522
+ if (messages.length >= 2 && currentConversationId) {
523
+ try {
524
+ await deleteLastMessages(currentConversationId);
525
+ } catch (err) {
526
+ // 忽略服务端错误
527
+ }
528
+ messages.splice(-2);
529
+ }
530
+ continue;
531
+ }
532
+
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
+ // 重新生成最后一条回复
511
548
  if (messages.length >= 2) {
512
- const lastUserMsg = [...messages].reverse().find(m => m.role === 'user' && m.id);
513
- if (lastUserMsg?.id) {
514
- try {
515
- await deleteMessage(lastUserMsg.id);
516
- // 从本地移除最后一轮对话
517
- const userIdx = messages.lastIndexOf(lastUserMsg);
518
- if (userIdx !== -1) {
519
- messages.splice(userIdx, 2); // 删除用户消息和AI回复
520
- }
521
- } catch (err) {
522
- // 忽略
549
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
550
+ if (lastUserMsg) {
551
+ // 删除服务端最后两条
552
+ if (currentConversationId) {
553
+ try { await deleteLastMessages(currentConversationId); } catch {}
523
554
  }
555
+ messages.splice(-2);
556
+ // 重新发送用户消息
557
+ input = lastUserMsg.text;
558
+ // 不 continue,让后续代码处理发送
524
559
  } else {
525
- // 没有ID,只删除本地
526
- messages.splice(-2, 2);
560
+ continue;
527
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}`));
528
584
  }
585
+ await sleep(1500);
529
586
  continue;
530
587
  }
531
588
 
532
- messages.push({ role: 'user', text: input });
589
+ messages.push({ role: 'user', text: input, timestamp: new Date() });
533
590
  drawMessages();
534
591
 
535
592
  // 显示思考中
@@ -595,7 +652,7 @@ async function showChat() {
595
652
  }
596
653
 
597
654
  console.log(); // 结束当前行
598
- messages.push({ role: 'bot', text: fullResponse || '...' });
655
+ messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
599
656
  // 重绘消息列表(带 markdown 渲染)
600
657
  drawMessages();
601
658
  await sleep(100); // 短暂延迟让用户看到渲染效果
@@ -744,13 +801,24 @@ function prompt(query) {
744
801
 
745
802
  // 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
746
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
+ ];
747
815
 
748
816
  function chatPrompt(query) {
749
817
  return new Promise((resolve) => {
750
818
  const stdin = process.stdin;
751
819
 
752
820
  // 先打印下方提示
753
- console.log(c('hint', ' /back 返回 /clear 清空 /delete 撤回 Ctrl+V 粘贴图片'));
821
+ console.log(c('hint', ' 输入 / 选择命令 Ctrl+C 清空 (双击退出) Ctrl+V 粘贴图片'));
754
822
  console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
755
823
  // 光标上移3行,显示输入提示
756
824
  process.stdout.write('\x1b[3A');
@@ -796,11 +864,29 @@ function chatPrompt(query) {
796
864
  return;
797
865
  }
798
866
 
799
- // Ctrl+C
867
+ // Ctrl+C - 单击清空,双击退出
800
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 === '') {
801
886
  stdin.removeListener('data', onData);
802
- stdin.setRawMode(false);
803
- process.exit();
887
+ // 显示命令选择器
888
+ showCommandSelector(stdin, query, resolve);
889
+ return;
804
890
  }
805
891
 
806
892
  // 退格
@@ -826,6 +912,108 @@ function chatPrompt(query) {
826
912
  });
827
913
  }
828
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
+
829
1017
  function cleanup() {
830
1018
  showCursor();
831
1019
  clearScreen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivetu53/soul-chat",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "Soul Chat - 智能 AI 伴侣命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {