@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.
- package/bin/api.js +25 -2
- package/bin/index.js +368 -98
- 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 (
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
607
|
+
// 显示思考中
|
|
608
|
+
process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '重新生成中...'));
|
|
572
609
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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+
|
|
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
|
-
//
|
|
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();
|