@fivetu53/soul-chat 1.1.3 → 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 +12 -2
- package/bin/index.js +299 -45
- 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,15 @@ 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
|
+
|
|
240
250
|
export async function regenerateMessage(conversationId) {
|
|
241
251
|
const response = await request(`/api/chat/conversations/${conversationId}/regenerate`, {
|
|
242
252
|
method: 'POST'
|
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, regenerateMessage, 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,6 +572,7 @@ 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
|
}
|
|
@@ -660,11 +686,15 @@ async function showChat() {
|
|
|
660
686
|
// 显示思考中
|
|
661
687
|
process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '思考中...'));
|
|
662
688
|
|
|
689
|
+
// 创建 AbortController 用于打断请求
|
|
690
|
+
currentAbortController = new AbortController();
|
|
691
|
+
let aborted = false;
|
|
692
|
+
|
|
663
693
|
try {
|
|
664
|
-
//
|
|
694
|
+
// 发送消息(带图片和 abort signal)
|
|
665
695
|
const imageBase64 = pendingImage?.base64 || null;
|
|
666
696
|
pendingImage = null; // 清空待发送图片
|
|
667
|
-
const response = await sendMessage(input, currentCharacter.id, currentConversationId, imageBase64);
|
|
697
|
+
const response = await sendMessage(input, currentCharacter.id, currentConversationId, imageBase64, currentAbortController.signal);
|
|
668
698
|
|
|
669
699
|
// 清除思考中,开始打字机效果
|
|
670
700
|
process.stdout.write('\r\x1b[K');
|
|
@@ -678,58 +708,83 @@ async function showChat() {
|
|
|
678
708
|
const decoder = new TextDecoder();
|
|
679
709
|
let buffer = '';
|
|
680
710
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
+
// 忽略解析错误
|
|
716
750
|
}
|
|
717
|
-
} catch (e) {
|
|
718
|
-
// 忽略解析错误
|
|
719
751
|
}
|
|
720
752
|
}
|
|
721
753
|
}
|
|
754
|
+
} catch (readErr) {
|
|
755
|
+
if (readErr.name === 'AbortError') {
|
|
756
|
+
aborted = true;
|
|
757
|
+
} else {
|
|
758
|
+
throw readErr;
|
|
759
|
+
}
|
|
722
760
|
}
|
|
723
761
|
|
|
724
762
|
console.log(); // 结束当前行
|
|
725
|
-
|
|
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
|
+
|
|
726
772
|
// 重绘消息列表(带 markdown 渲染)
|
|
727
773
|
drawMessages();
|
|
728
774
|
await sleep(100); // 短暂延迟让用户看到渲染效果
|
|
729
775
|
} catch (err) {
|
|
730
776
|
process.stdout.write('\r\x1b[K');
|
|
731
|
-
|
|
732
|
-
|
|
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;
|
|
733
788
|
}
|
|
734
789
|
}
|
|
735
790
|
}
|
|
@@ -872,6 +927,8 @@ function prompt(query) {
|
|
|
872
927
|
// 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
|
|
873
928
|
let pendingImage = null; // 待发送的图片
|
|
874
929
|
let lastCtrlCTime = 0; // 上次 Ctrl+C 时间
|
|
930
|
+
let lastEscTime = 0; // 上次 Esc 时间
|
|
931
|
+
let currentAbortController = null; // 用于打断请求
|
|
875
932
|
|
|
876
933
|
// 斜杠命令列表
|
|
877
934
|
const slashCommands = [
|
|
@@ -888,7 +945,7 @@ function chatPrompt(query) {
|
|
|
888
945
|
const stdin = process.stdin;
|
|
889
946
|
|
|
890
947
|
// 先打印下方提示
|
|
891
|
-
console.log(c('hint', ' 输入 / 选择命令 Ctrl+
|
|
948
|
+
console.log(c('hint', ' 输入 / 选择命令 Ctrl+J 换行 Esc 双击回滚 Ctrl+C 双击退出'));
|
|
892
949
|
console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
|
|
893
950
|
// 光标上移3行,显示输入提示
|
|
894
951
|
process.stdout.write('\x1b[3A');
|
|
@@ -918,7 +975,14 @@ function chatPrompt(query) {
|
|
|
918
975
|
return;
|
|
919
976
|
}
|
|
920
977
|
|
|
921
|
-
//
|
|
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 键 - 发送消息
|
|
922
986
|
if (char === '\r' || char === '\n') {
|
|
923
987
|
// 如果是快速输入(粘贴),把换行当作空格
|
|
924
988
|
if (timeDiff < 10 && input.length > 0) {
|
|
@@ -951,6 +1015,25 @@ function chatPrompt(query) {
|
|
|
951
1015
|
return;
|
|
952
1016
|
}
|
|
953
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
|
+
|
|
954
1037
|
// 斜杠命令选择器
|
|
955
1038
|
if (char === '/' && input === '') {
|
|
956
1039
|
stdin.removeListener('data', onData);
|
|
@@ -1084,6 +1167,177 @@ function showCommandSelector(stdin, query, resolve) {
|
|
|
1084
1167
|
stdin.on('data', onSelect);
|
|
1085
1168
|
}
|
|
1086
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
|
+
|
|
1087
1341
|
function cleanup() {
|
|
1088
1342
|
showCursor();
|
|
1089
1343
|
clearScreen();
|