@fivetu53/soul-chat 1.1.2 → 1.1.4

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 +25 -2
  2. package/bin/index.js +368 -98
  3. package/package.json +1 -1
package/bin/api.js CHANGED
@@ -182,10 +182,11 @@ export async function createCharacter(character) {
182
182
  }
183
183
 
184
184
  // Chat APIs
185
- export async function sendMessage(message, characterId, conversationId, image = null) {
185
+ export async function sendMessage(message, characterId, conversationId, image = null, signal = null) {
186
186
  const response = await request('/api/chat', {
187
187
  method: 'POST',
188
- body: JSON.stringify({ message, characterId, conversationId, image })
188
+ body: JSON.stringify({ message, characterId, conversationId, image }),
189
+ signal
189
190
  });
190
191
 
191
192
  if (!response.ok) {
@@ -237,6 +238,28 @@ export async function deleteLastMessages(conversationId) {
237
238
  return data;
238
239
  }
239
240
 
241
+ export async function deleteMessagesAfter(conversationId, messageId) {
242
+ const response = await request(`/api/chat/conversations/${conversationId}/after/${messageId}`, {
243
+ method: 'DELETE'
244
+ });
245
+ const data = await response.json();
246
+ if (!response.ok) throw new Error(data.error);
247
+ return data;
248
+ }
249
+
250
+ export async function regenerateMessage(conversationId) {
251
+ const response = await request(`/api/chat/conversations/${conversationId}/regenerate`, {
252
+ method: 'POST'
253
+ });
254
+
255
+ if (!response.ok) {
256
+ const data = await response.json();
257
+ throw new Error(data.error);
258
+ }
259
+
260
+ return response;
261
+ }
262
+
240
263
  // Memory APIs
241
264
  export async function getMemories(characterId) {
242
265
  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, deleteLastMessages, getMemories, pinMemory, deleteMemory
13
+ sendMessage, getCharacters, createCharacter, getConversations, getMessages, clearConversation, deleteLastMessages, deleteMessagesAfter, regenerateMessage, getMemories, pinMemory, deleteMemory
14
14
  } from './api.js';
15
15
 
16
16
  // Setup marked with terminal renderer
@@ -538,6 +538,31 @@ async function showChat() {
538
538
  continue;
539
539
  }
540
540
 
541
+ if (input === '/rollback') {
542
+ // 回滚到指定消息
543
+ if (!currentConversationId) {
544
+ continue;
545
+ }
546
+ const selectedMsg = await selectMessageToRollback(messages);
547
+ if (selectedMsg) {
548
+ const confirmed = await confirmRollback();
549
+ if (confirmed) {
550
+ try {
551
+ await deleteMessagesAfter(currentConversationId, selectedMsg.id);
552
+ // 从本地 messages 数组中移除选中消息之后的所有消息
553
+ const idx = messages.findIndex(m => m.id === selectedMsg.id);
554
+ if (idx !== -1) {
555
+ messages.splice(idx + 1);
556
+ }
557
+ messages.push({ role: 'system', text: '✓ 已回滚到该消息' });
558
+ } catch (err) {
559
+ messages.push({ role: 'system', text: `✗ 回滚失败: ${err.message}` });
560
+ }
561
+ }
562
+ }
563
+ continue;
564
+ }
565
+
541
566
  if (input === '/help') {
542
567
  console.log(c('cyan', '\n 可用命令:'));
543
568
  console.log(c('gray', ' /back - 返回主菜单'));
@@ -547,72 +572,89 @@ async function showChat() {
547
572
  console.log(c('gray', ' /export - 导出对话'));
548
573
  console.log(c('gray', ' Ctrl+C - 清空输入 (双击退出)'));
549
574
  console.log(c('gray', ' Ctrl+V - 粘贴图片'));
575
+ console.log(c('gray', ' Esc - 打断回复 (双击回滚)'));
550
576
  await sleep(2000);
551
577
  continue;
552
578
  }
553
579
 
554
580
  if (input === '/regen') {
555
581
  // 重新生成最后一条回复
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);
582
+ if (!currentConversationId) {
583
+ console.log(c('yellow', ' 没有可重新生成的对话'));
584
+ await sleep(1000);
585
+ continue;
586
+ }
564
587
 
565
- // 重新发送用户消息
566
- const regenText = lastUserMsg.text;
567
- messages.push({ role: 'user', text: regenText, timestamp: new Date() });
568
- drawMessages();
588
+ // 找到最后一条 AI 回复的索引
589
+ let lastBotIdx = -1;
590
+ for (let i = messages.length - 1; i >= 0; i--) {
591
+ if (messages[i].role === 'bot') {
592
+ lastBotIdx = i;
593
+ break;
594
+ }
595
+ }
596
+
597
+ if (lastBotIdx === -1) {
598
+ console.log(c('yellow', ' 没有可重新生成的回复'));
599
+ await sleep(1000);
600
+ continue;
601
+ }
602
+
603
+ // 删除客户端的 AI 回复(以及之后的 system 消息)
604
+ messages.splice(lastBotIdx);
605
+ drawMessages();
569
606
 
570
- // 显示思考中
571
- process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '思考中...'));
607
+ // 显示思考中
608
+ process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '重新生成中...'));
572
609
 
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 {}
610
+ try {
611
+ const response = await regenerateMessage(currentConversationId);
612
+ process.stdout.write('\r\x1b[K');
613
+ process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
614
+
615
+ let fullResponse = '';
616
+ const reader = response.body.getReader();
617
+ const decoder = new TextDecoder();
618
+ let buffer = '';
619
+
620
+ while (true) {
621
+ const { done, value } = await reader.read();
622
+ if (done) break;
623
+ buffer += decoder.decode(value, { stream: true });
624
+ const lines = buffer.split('\n');
625
+ buffer = lines.pop() || '';
626
+
627
+ for (const line of lines) {
628
+ if (line.startsWith('data: ')) {
629
+ const data = line.slice(6);
630
+ if (data === '[DONE]') continue;
631
+ try {
632
+ const json = JSON.parse(data);
633
+ if (json.type === 'tool_call') {
634
+ process.stdout.write('\r\x1b[K');
635
+ process.stdout.write(c('cyan', ` 🔍 正在搜索: ${json.query}...`));
636
+ messages.push({ role: 'system', text: `🔍 正在搜索: ${json.query}` });
637
+ } else if (json.type === 'tool_result') {
638
+ process.stdout.write('\r\x1b[K');
639
+ process.stdout.write(c('green', ` 📋 找到 ${json.count} 条结果\n`));
640
+ process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
641
+ messages.push({ role: 'system', text: `📋 找到 ${json.count} 条结果` });
642
+ } else if (json.type === 'content') {
643
+ fullResponse += json.content;
644
+ process.stdout.write(json.content);
603
645
  }
604
- }
646
+ } catch {}
605
647
  }
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
648
  }
615
649
  }
650
+
651
+ console.log();
652
+ messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
653
+ drawMessages();
654
+ } catch (err) {
655
+ process.stdout.write('\r\x1b[K');
656
+ console.log(c('red', ` 错误: ${err.message}`));
657
+ await sleep(1500);
616
658
  }
617
659
  continue;
618
660
  }
@@ -644,11 +686,15 @@ async function showChat() {
644
686
  // 显示思考中
645
687
  process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '思考中...'));
646
688
 
689
+ // 创建 AbortController 用于打断请求
690
+ currentAbortController = new AbortController();
691
+ let aborted = false;
692
+
647
693
  try {
648
- // 发送消息(带图片)
694
+ // 发送消息(带图片和 abort signal)
649
695
  const imageBase64 = pendingImage?.base64 || null;
650
696
  pendingImage = null; // 清空待发送图片
651
- const response = await sendMessage(input, currentCharacter.id, currentConversationId, imageBase64);
697
+ const response = await sendMessage(input, currentCharacter.id, currentConversationId, imageBase64, currentAbortController.signal);
652
698
 
653
699
  // 清除思考中,开始打字机效果
654
700
  process.stdout.write('\r\x1b[K');
@@ -662,58 +708,83 @@ async function showChat() {
662
708
  const decoder = new TextDecoder();
663
709
  let buffer = '';
664
710
 
665
- while (true) {
666
- const { done, value } = await reader.read();
667
- if (done) break;
668
-
669
- buffer += decoder.decode(value, { stream: true });
670
- const lines = buffer.split('\n');
671
- buffer = lines.pop() || '';
672
-
673
- for (const line of lines) {
674
- if (line.startsWith('data: ')) {
675
- const data = line.slice(6);
676
- if (data === '[DONE]') continue;
677
-
678
- try {
679
- const json = JSON.parse(data);
680
-
681
- if (json.type === 'info') {
682
- currentConversationId = json.conversationId;
683
- } else if (json.type === 'tool_call') {
684
- // 显示搜索状态并添加到消息
685
- process.stdout.write('\r\x1b[K');
686
- process.stdout.write(c('cyan', ` 🔍 正在搜索: ${json.query}...`));
687
- messages.push({ role: 'system', text: `🔍 正在搜索: ${json.query}` });
688
- } else if (json.type === 'tool_result') {
689
- // 显示搜索结果数量并添加到消息
690
- process.stdout.write('\r\x1b[K');
691
- process.stdout.write(c('green', ` 📋 找到 ${json.count} 条结果\n`));
692
- process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
693
- messages.push({ role: 'system', text: `📋 找到 ${json.count} 条结果` });
694
- } else if (json.type === 'content') {
695
- fullResponse += json.content;
696
- // 打字机效果:直接输出内容
697
- process.stdout.write(json.content);
698
- // 统计换行数
699
- lineCount += (json.content.match(/\n/g) || []).length;
711
+ try {
712
+ while (true) {
713
+ const { done, value } = await reader.read();
714
+ if (done) break;
715
+
716
+ buffer += decoder.decode(value, { stream: true });
717
+ const lines = buffer.split('\n');
718
+ buffer = lines.pop() || '';
719
+
720
+ for (const line of lines) {
721
+ if (line.startsWith('data: ')) {
722
+ const data = line.slice(6);
723
+ if (data === '[DONE]') continue;
724
+
725
+ try {
726
+ const json = JSON.parse(data);
727
+
728
+ if (json.type === 'info') {
729
+ currentConversationId = json.conversationId;
730
+ } else if (json.type === 'tool_call') {
731
+ // 显示搜索状态并添加到消息
732
+ process.stdout.write('\r\x1b[K');
733
+ process.stdout.write(c('cyan', ` 🔍 正在搜索: ${json.query}...`));
734
+ messages.push({ role: 'system', text: `🔍 正在搜索: ${json.query}` });
735
+ } else if (json.type === 'tool_result') {
736
+ // 显示搜索结果数量并添加到消息
737
+ process.stdout.write('\r\x1b[K');
738
+ process.stdout.write(c('green', ` 📋 找到 ${json.count} 条结果\n`));
739
+ process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
740
+ messages.push({ role: 'system', text: `📋 找到 ${json.count} 条结果` });
741
+ } else if (json.type === 'content') {
742
+ fullResponse += json.content;
743
+ // 打字机效果:直接输出内容
744
+ process.stdout.write(json.content);
745
+ // 统计换行数
746
+ lineCount += (json.content.match(/\n/g) || []).length;
747
+ }
748
+ } catch (e) {
749
+ // 忽略解析错误
700
750
  }
701
- } catch (e) {
702
- // 忽略解析错误
703
751
  }
704
752
  }
705
753
  }
754
+ } catch (readErr) {
755
+ if (readErr.name === 'AbortError') {
756
+ aborted = true;
757
+ } else {
758
+ throw readErr;
759
+ }
706
760
  }
707
761
 
708
762
  console.log(); // 结束当前行
709
- messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
763
+
764
+ if (aborted) {
765
+ // 被打断,显示提示,删除用户消息
766
+ messages.pop(); // 删除刚添加的用户消息
767
+ messages.push({ role: 'system', text: '⚠️ 已打断回复' });
768
+ } else {
769
+ messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
770
+ }
771
+
710
772
  // 重绘消息列表(带 markdown 渲染)
711
773
  drawMessages();
712
774
  await sleep(100); // 短暂延迟让用户看到渲染效果
713
775
  } catch (err) {
714
776
  process.stdout.write('\r\x1b[K');
715
- console.log(c('red', ` 错误: ${err.message}`));
716
- await sleep(1500);
777
+ if (err.name === 'AbortError') {
778
+ // 被打断
779
+ messages.pop(); // 删除用户消息
780
+ messages.push({ role: 'system', text: '⚠️ 已打断回复' });
781
+ drawMessages();
782
+ } else {
783
+ console.log(c('red', ` 错误: ${err.message}`));
784
+ await sleep(1500);
785
+ }
786
+ } finally {
787
+ currentAbortController = null;
717
788
  }
718
789
  }
719
790
  }
@@ -856,6 +927,8 @@ function prompt(query) {
856
927
  // 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
857
928
  let pendingImage = null; // 待发送的图片
858
929
  let lastCtrlCTime = 0; // 上次 Ctrl+C 时间
930
+ let lastEscTime = 0; // 上次 Esc 时间
931
+ let currentAbortController = null; // 用于打断请求
859
932
 
860
933
  // 斜杠命令列表
861
934
  const slashCommands = [
@@ -872,7 +945,7 @@ function chatPrompt(query) {
872
945
  const stdin = process.stdin;
873
946
 
874
947
  // 先打印下方提示
875
- console.log(c('hint', ' 输入 / 选择命令 Ctrl+C 清空 (双击退出) Ctrl+V 粘贴图片'));
948
+ console.log(c('hint', ' 输入 / 选择命令 Ctrl+J 换行 Esc 双击回滚 Ctrl+C 双击退出'));
876
949
  console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
877
950
  // 光标上移3行,显示输入提示
878
951
  process.stdout.write('\x1b[3A');
@@ -902,7 +975,14 @@ function chatPrompt(query) {
902
975
  return;
903
976
  }
904
977
 
905
- // Enter
978
+ // Ctrl+J - 插入换行
979
+ if (char === '\x0a' && chunk.length === 1 && chunk[0] === 10) {
980
+ input += '\n';
981
+ process.stdout.write('\n '); // 换行并缩进
982
+ return;
983
+ }
984
+
985
+ // Enter 键 - 发送消息
906
986
  if (char === '\r' || char === '\n') {
907
987
  // 如果是快速输入(粘贴),把换行当作空格
908
988
  if (timeDiff < 10 && input.length > 0) {
@@ -935,6 +1015,25 @@ function chatPrompt(query) {
935
1015
  return;
936
1016
  }
937
1017
 
1018
+ // Esc - 单击打断回复,双击触发回滚
1019
+ if (chunk[0] === 27 && chunk.length === 1) {
1020
+ const now = Date.now();
1021
+ if (now - lastEscTime < 500) {
1022
+ // 双击 Esc - 触发回滚流程
1023
+ stdin.removeListener('data', onData);
1024
+ stdin.setRawMode(false);
1025
+ console.log();
1026
+ resolve('/rollback');
1027
+ } else {
1028
+ // 单击 Esc - 打断回复(如果正在生成)
1029
+ lastEscTime = now;
1030
+ if (currentAbortController) {
1031
+ currentAbortController.abort();
1032
+ }
1033
+ }
1034
+ return;
1035
+ }
1036
+
938
1037
  // 斜杠命令选择器
939
1038
  if (char === '/' && input === '') {
940
1039
  stdin.removeListener('data', onData);
@@ -1068,6 +1167,177 @@ function showCommandSelector(stdin, query, resolve) {
1068
1167
  stdin.on('data', onSelect);
1069
1168
  }
1070
1169
 
1170
+ // 消息选择器(用于回滚)
1171
+ function selectMessageToRollback(messages) {
1172
+ return new Promise((resolve) => {
1173
+ const stdin = process.stdin;
1174
+ // 只显示有 id 的消息(排除系统消息和初始欢迎消息)
1175
+ const selectableMessages = messages.filter(m => m.id);
1176
+ if (selectableMessages.length === 0) {
1177
+ resolve(null);
1178
+ return;
1179
+ }
1180
+
1181
+ let selected = selectableMessages.length - 1; // 默认选中最后一条
1182
+ const maxDisplay = 10; // 最多显示10条
1183
+
1184
+ const getDisplayMessages = () => {
1185
+ // 显示最后 maxDisplay 条
1186
+ const start = Math.max(0, selectableMessages.length - maxDisplay);
1187
+ return selectableMessages.slice(start);
1188
+ };
1189
+
1190
+ const drawList = () => {
1191
+ const displayMsgs = getDisplayMessages();
1192
+ const startIdx = Math.max(0, selectableMessages.length - maxDisplay);
1193
+
1194
+ // 上移到列表开始位置
1195
+ process.stdout.write(`\x1b[${displayMsgs.length + 2}A`);
1196
+
1197
+ console.log(c('yellow', ' 选择要保留到哪条消息(之后的消息将被删除):') + '\x1b[K');
1198
+ displayMsgs.forEach((msg, i) => {
1199
+ const globalIdx = startIdx + i;
1200
+ const isSelected = globalIdx === selected;
1201
+ const prefix = isSelected ? c('cyan', '> ') : ' ';
1202
+ const roleIcon = msg.role === 'user' ? '👤' : '🤖';
1203
+ const preview = msg.text.slice(0, 30).replace(/\n/g, ' ') + (msg.text.length > 30 ? '...' : '');
1204
+ const line = isSelected ? c('cyan', `${roleIcon} ${preview}`) : `${roleIcon} ${preview}`;
1205
+ console.log(` ${prefix}${line}\x1b[K`);
1206
+ });
1207
+ console.log(c('gray', ' ↑↓选择 Enter确认 Esc取消') + '\x1b[K');
1208
+ };
1209
+
1210
+ // 初始绘制
1211
+ console.log();
1212
+ console.log(c('yellow', ' 选择要保留到哪条消息(之后的消息将被删除):'));
1213
+ const displayMsgs = getDisplayMessages();
1214
+ const startIdx = Math.max(0, selectableMessages.length - maxDisplay);
1215
+ displayMsgs.forEach((msg, i) => {
1216
+ const globalIdx = startIdx + i;
1217
+ const isSelected = globalIdx === selected;
1218
+ const prefix = isSelected ? c('cyan', '> ') : ' ';
1219
+ const roleIcon = msg.role === 'user' ? '👤' : '🤖';
1220
+ const preview = msg.text.slice(0, 30).replace(/\n/g, ' ') + (msg.text.length > 30 ? '...' : '');
1221
+ const line = isSelected ? c('cyan', `${roleIcon} ${preview}`) : `${roleIcon} ${preview}`;
1222
+ console.log(` ${prefix}${line}`);
1223
+ });
1224
+ console.log(c('gray', ' ↑↓选择 Enter确认 Esc取消'));
1225
+
1226
+ stdin.setRawMode(true);
1227
+ stdin.resume();
1228
+
1229
+ const onKey = (key) => {
1230
+ // 上下键
1231
+ if (key[0] === 27 && key[1] === 91) {
1232
+ if (key[2] === 65 && selected > 0) { // 上
1233
+ selected--;
1234
+ drawList();
1235
+ } else if (key[2] === 66 && selected < selectableMessages.length - 1) { // 下
1236
+ selected++;
1237
+ drawList();
1238
+ }
1239
+ return;
1240
+ }
1241
+
1242
+ // Enter - 确认选择
1243
+ if (key[0] === 13) {
1244
+ stdin.removeListener('data', onKey);
1245
+ stdin.setRawMode(false);
1246
+ // 清除选择列表
1247
+ const lines = getDisplayMessages().length + 2;
1248
+ process.stdout.write(`\x1b[${lines}A`);
1249
+ for (let i = 0; i < lines; i++) console.log('\x1b[K');
1250
+ process.stdout.write(`\x1b[${lines}A`);
1251
+ resolve(selectableMessages[selected]);
1252
+ return;
1253
+ }
1254
+
1255
+ // Esc - 取消
1256
+ if (key[0] === 27 && key.length === 1) {
1257
+ stdin.removeListener('data', onKey);
1258
+ stdin.setRawMode(false);
1259
+ // 清除选择列表
1260
+ const lines = getDisplayMessages().length + 2;
1261
+ process.stdout.write(`\x1b[${lines}A`);
1262
+ for (let i = 0; i < lines; i++) console.log('\x1b[K');
1263
+ process.stdout.write(`\x1b[${lines}A`);
1264
+ resolve(null);
1265
+ return;
1266
+ }
1267
+ };
1268
+
1269
+ stdin.on('data', onKey);
1270
+ });
1271
+ }
1272
+
1273
+ // 确认对话框
1274
+ function confirmRollback() {
1275
+ return new Promise((resolve) => {
1276
+ const stdin = process.stdin;
1277
+ const options = ['确认删除', '取消'];
1278
+ let selected = 1; // 默认选中取消
1279
+
1280
+ const drawOptions = () => {
1281
+ process.stdout.write('\x1b[3A');
1282
+ console.log(c('yellow', ' 确定删除此消息之后的所有内容?') + '\x1b[K');
1283
+ options.forEach((opt, i) => {
1284
+ const prefix = i === selected ? c('cyan', '> ') : ' ';
1285
+ const text = i === selected ? c('cyan', opt) : opt;
1286
+ console.log(` ${prefix}${text}\x1b[K`);
1287
+ });
1288
+ };
1289
+
1290
+ // 初始绘制
1291
+ console.log();
1292
+ console.log(c('yellow', ' 确定删除此消息之后的所有内容?'));
1293
+ options.forEach((opt, i) => {
1294
+ const prefix = i === selected ? c('cyan', '> ') : ' ';
1295
+ const text = i === selected ? c('cyan', opt) : opt;
1296
+ console.log(` ${prefix}${text}`);
1297
+ });
1298
+
1299
+ stdin.setRawMode(true);
1300
+ stdin.resume();
1301
+
1302
+ const onKey = (key) => {
1303
+ // 上下键
1304
+ if (key[0] === 27 && key[1] === 91) {
1305
+ if (key[2] === 65 || key[2] === 66) { // 上或下
1306
+ selected = selected === 0 ? 1 : 0;
1307
+ drawOptions();
1308
+ }
1309
+ return;
1310
+ }
1311
+
1312
+ // Enter - 确认
1313
+ if (key[0] === 13) {
1314
+ stdin.removeListener('data', onKey);
1315
+ stdin.setRawMode(false);
1316
+ // 清除对话框
1317
+ process.stdout.write('\x1b[3A');
1318
+ for (let i = 0; i < 3; i++) console.log('\x1b[K');
1319
+ process.stdout.write('\x1b[3A');
1320
+ resolve(selected === 0);
1321
+ return;
1322
+ }
1323
+
1324
+ // Esc - 取消
1325
+ if (key[0] === 27 && key.length === 1) {
1326
+ stdin.removeListener('data', onKey);
1327
+ stdin.setRawMode(false);
1328
+ // 清除对话框
1329
+ process.stdout.write('\x1b[3A');
1330
+ for (let i = 0; i < 3; i++) console.log('\x1b[K');
1331
+ process.stdout.write('\x1b[3A');
1332
+ resolve(false);
1333
+ return;
1334
+ }
1335
+ };
1336
+
1337
+ stdin.on('data', onKey);
1338
+ });
1339
+ }
1340
+
1071
1341
  function cleanup() {
1072
1342
  showCursor();
1073
1343
  clearScreen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivetu53/soul-chat",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Soul Chat - 智能 AI 伴侣命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {