@fivetu53/soul-chat 1.1.0 → 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.
- package/bin/index.js +206 -8
- 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
|
|
|
@@ -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(-
|
|
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();
|
|
@@ -519,7 +530,63 @@ async function showChat() {
|
|
|
519
530
|
continue;
|
|
520
531
|
}
|
|
521
532
|
|
|
522
|
-
|
|
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
|
+
// 重新生成最后一条回复
|
|
548
|
+
if (messages.length >= 2) {
|
|
549
|
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
550
|
+
if (lastUserMsg) {
|
|
551
|
+
// 删除服务端最后两条
|
|
552
|
+
if (currentConversationId) {
|
|
553
|
+
try { await deleteLastMessages(currentConversationId); } catch {}
|
|
554
|
+
}
|
|
555
|
+
messages.splice(-2);
|
|
556
|
+
// 重新发送用户消息
|
|
557
|
+
input = lastUserMsg.text;
|
|
558
|
+
// 不 continue,让后续代码处理发送
|
|
559
|
+
} else {
|
|
560
|
+
continue;
|
|
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}`));
|
|
584
|
+
}
|
|
585
|
+
await sleep(1500);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
messages.push({ role: 'user', text: input, timestamp: new Date() });
|
|
523
590
|
drawMessages();
|
|
524
591
|
|
|
525
592
|
// 显示思考中
|
|
@@ -585,7 +652,7 @@ async function showChat() {
|
|
|
585
652
|
}
|
|
586
653
|
|
|
587
654
|
console.log(); // 结束当前行
|
|
588
|
-
messages.push({ role: 'bot', text: fullResponse || '...' });
|
|
655
|
+
messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
|
|
589
656
|
// 重绘消息列表(带 markdown 渲染)
|
|
590
657
|
drawMessages();
|
|
591
658
|
await sleep(100); // 短暂延迟让用户看到渲染效果
|
|
@@ -734,13 +801,24 @@ function prompt(query) {
|
|
|
734
801
|
|
|
735
802
|
// 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
|
|
736
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
|
+
];
|
|
737
815
|
|
|
738
816
|
function chatPrompt(query) {
|
|
739
817
|
return new Promise((resolve) => {
|
|
740
818
|
const stdin = process.stdin;
|
|
741
819
|
|
|
742
820
|
// 先打印下方提示
|
|
743
|
-
console.log(c('hint', ' /
|
|
821
|
+
console.log(c('hint', ' 输入 / 选择命令 Ctrl+C 清空 (双击退出) Ctrl+V 粘贴图片'));
|
|
744
822
|
console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
|
|
745
823
|
// 光标上移3行,显示输入提示
|
|
746
824
|
process.stdout.write('\x1b[3A');
|
|
@@ -786,11 +864,29 @@ function chatPrompt(query) {
|
|
|
786
864
|
return;
|
|
787
865
|
}
|
|
788
866
|
|
|
789
|
-
// Ctrl+C
|
|
867
|
+
// Ctrl+C - 单击清空,双击退出
|
|
790
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 === '') {
|
|
791
886
|
stdin.removeListener('data', onData);
|
|
792
|
-
|
|
793
|
-
|
|
887
|
+
// 显示命令选择器
|
|
888
|
+
showCommandSelector(stdin, query, resolve);
|
|
889
|
+
return;
|
|
794
890
|
}
|
|
795
891
|
|
|
796
892
|
// 退格
|
|
@@ -816,6 +912,108 @@ function chatPrompt(query) {
|
|
|
816
912
|
});
|
|
817
913
|
}
|
|
818
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
|
+
|
|
819
1017
|
function cleanup() {
|
|
820
1018
|
showCursor();
|
|
821
1019
|
clearScreen();
|