@fivetu53/soul-chat 1.1.0 → 1.1.2
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 +263 -11
- 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
|
|
|
@@ -454,7 +462,12 @@ async function showChat() {
|
|
|
454
462
|
try {
|
|
455
463
|
const history = await getMessages(currentConversationId);
|
|
456
464
|
history.forEach(m => {
|
|
457
|
-
messages.push({
|
|
465
|
+
messages.push({
|
|
466
|
+
role: m.role === 'user' ? 'user' : 'bot',
|
|
467
|
+
text: m.content,
|
|
468
|
+
id: m.id,
|
|
469
|
+
timestamp: m.created_at ? new Date(m.created_at) : null
|
|
470
|
+
});
|
|
458
471
|
});
|
|
459
472
|
} catch (err) {
|
|
460
473
|
// 忽略
|
|
@@ -471,9 +484,15 @@ async function showChat() {
|
|
|
471
484
|
console.log(c('secondary', c('bold', ` [*] 与 ${currentCharacter.name} 聊天`)));
|
|
472
485
|
console.log();
|
|
473
486
|
|
|
474
|
-
messages.slice(-
|
|
487
|
+
messages.slice(-20).forEach(msg => {
|
|
488
|
+
const timeStr = msg.timestamp
|
|
489
|
+
? c('gray', ` [${msg.timestamp.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'})}]`)
|
|
490
|
+
: '';
|
|
475
491
|
if (msg.role === 'user') {
|
|
476
|
-
console.log(c('user', ` ${currentUser.username}: ${msg.text}`));
|
|
492
|
+
console.log(c('user', ` ${currentUser.username}${timeStr}: ${msg.text}`));
|
|
493
|
+
} else if (msg.role === 'system') {
|
|
494
|
+
// 系统消息(导出、搜索等)
|
|
495
|
+
console.log(c('cyan', ` ${msg.text}`));
|
|
477
496
|
} else {
|
|
478
497
|
// 渲染 markdown
|
|
479
498
|
const rendered = marked.parse(msg.text).trim();
|
|
@@ -519,7 +538,107 @@ async function showChat() {
|
|
|
519
538
|
continue;
|
|
520
539
|
}
|
|
521
540
|
|
|
522
|
-
|
|
541
|
+
if (input === '/help') {
|
|
542
|
+
console.log(c('cyan', '\n 可用命令:'));
|
|
543
|
+
console.log(c('gray', ' /back - 返回主菜单'));
|
|
544
|
+
console.log(c('gray', ' /clear - 清空对话'));
|
|
545
|
+
console.log(c('gray', ' /delete - 撤回最后一轮'));
|
|
546
|
+
console.log(c('gray', ' /regen - 重新生成回复'));
|
|
547
|
+
console.log(c('gray', ' /export - 导出对话'));
|
|
548
|
+
console.log(c('gray', ' Ctrl+C - 清空输入 (双击退出)'));
|
|
549
|
+
console.log(c('gray', ' Ctrl+V - 粘贴图片'));
|
|
550
|
+
await sleep(2000);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (input === '/regen') {
|
|
555
|
+
// 重新生成最后一条回复
|
|
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);
|
|
564
|
+
|
|
565
|
+
// 重新发送用户消息
|
|
566
|
+
const regenText = lastUserMsg.text;
|
|
567
|
+
messages.push({ role: 'user', text: regenText, timestamp: new Date() });
|
|
568
|
+
drawMessages();
|
|
569
|
+
|
|
570
|
+
// 显示思考中
|
|
571
|
+
process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '思考中...'));
|
|
572
|
+
|
|
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 {}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
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
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (input === '/export') {
|
|
621
|
+
const filename = `soul-chat-${currentCharacter.name}-${Date.now()}.md`;
|
|
622
|
+
const filepath = `${os.homedir()}/Downloads/${filename}`;
|
|
623
|
+
|
|
624
|
+
let content = `# 与 ${currentCharacter.name} 的对话\n\n`;
|
|
625
|
+
content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n\n---\n\n`;
|
|
626
|
+
|
|
627
|
+
messages.filter(m => m.role !== 'system').forEach(msg => {
|
|
628
|
+
const role = msg.role === 'user' ? currentUser.username : currentCharacter.name;
|
|
629
|
+
content += `**${role}**:\n${msg.text}\n\n`;
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
fs.writeFileSync(filepath, content);
|
|
634
|
+
messages.push({ role: 'system', text: `✓ 已导出到: ${filepath}` });
|
|
635
|
+
} catch (err) {
|
|
636
|
+
messages.push({ role: 'system', text: `✗ 导出失败: ${err.message}` });
|
|
637
|
+
}
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
messages.push({ role: 'user', text: input, timestamp: new Date() });
|
|
523
642
|
drawMessages();
|
|
524
643
|
|
|
525
644
|
// 显示思考中
|
|
@@ -562,14 +681,16 @@ async function showChat() {
|
|
|
562
681
|
if (json.type === 'info') {
|
|
563
682
|
currentConversationId = json.conversationId;
|
|
564
683
|
} else if (json.type === 'tool_call') {
|
|
565
|
-
//
|
|
684
|
+
// 显示搜索状态并添加到消息
|
|
566
685
|
process.stdout.write('\r\x1b[K');
|
|
567
686
|
process.stdout.write(c('cyan', ` 🔍 正在搜索: ${json.query}...`));
|
|
687
|
+
messages.push({ role: 'system', text: `🔍 正在搜索: ${json.query}` });
|
|
568
688
|
} else if (json.type === 'tool_result') {
|
|
569
|
-
//
|
|
689
|
+
// 显示搜索结果数量并添加到消息
|
|
570
690
|
process.stdout.write('\r\x1b[K');
|
|
571
691
|
process.stdout.write(c('green', ` 📋 找到 ${json.count} 条结果\n`));
|
|
572
692
|
process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
|
|
693
|
+
messages.push({ role: 'system', text: `📋 找到 ${json.count} 条结果` });
|
|
573
694
|
} else if (json.type === 'content') {
|
|
574
695
|
fullResponse += json.content;
|
|
575
696
|
// 打字机效果:直接输出内容
|
|
@@ -585,7 +706,7 @@ async function showChat() {
|
|
|
585
706
|
}
|
|
586
707
|
|
|
587
708
|
console.log(); // 结束当前行
|
|
588
|
-
messages.push({ role: 'bot', text: fullResponse || '...' });
|
|
709
|
+
messages.push({ role: 'bot', text: fullResponse || '...', timestamp: new Date() });
|
|
589
710
|
// 重绘消息列表(带 markdown 渲染)
|
|
590
711
|
drawMessages();
|
|
591
712
|
await sleep(100); // 短暂延迟让用户看到渲染效果
|
|
@@ -734,13 +855,24 @@ function prompt(query) {
|
|
|
734
855
|
|
|
735
856
|
// 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
|
|
736
857
|
let pendingImage = null; // 待发送的图片
|
|
858
|
+
let lastCtrlCTime = 0; // 上次 Ctrl+C 时间
|
|
859
|
+
|
|
860
|
+
// 斜杠命令列表
|
|
861
|
+
const slashCommands = [
|
|
862
|
+
{ cmd: '/back', desc: '返回主菜单' },
|
|
863
|
+
{ cmd: '/clear', desc: '清空对话' },
|
|
864
|
+
{ cmd: '/delete', desc: '撤回最后一轮' },
|
|
865
|
+
{ cmd: '/regen', desc: '重新生成回复' },
|
|
866
|
+
{ cmd: '/export', desc: '导出对话' },
|
|
867
|
+
{ cmd: '/help', desc: '显示帮助' },
|
|
868
|
+
];
|
|
737
869
|
|
|
738
870
|
function chatPrompt(query) {
|
|
739
871
|
return new Promise((resolve) => {
|
|
740
872
|
const stdin = process.stdin;
|
|
741
873
|
|
|
742
874
|
// 先打印下方提示
|
|
743
|
-
console.log(c('hint', ' /
|
|
875
|
+
console.log(c('hint', ' 输入 / 选择命令 Ctrl+C 清空 (双击退出) Ctrl+V 粘贴图片'));
|
|
744
876
|
console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
|
|
745
877
|
// 光标上移3行,显示输入提示
|
|
746
878
|
process.stdout.write('\x1b[3A');
|
|
@@ -786,11 +918,29 @@ function chatPrompt(query) {
|
|
|
786
918
|
return;
|
|
787
919
|
}
|
|
788
920
|
|
|
789
|
-
// Ctrl+C
|
|
921
|
+
// Ctrl+C - 单击清空,双击退出
|
|
790
922
|
if (char === '\u0003') {
|
|
923
|
+
if (now - lastCtrlCTime < 500) {
|
|
924
|
+
// 双击退出
|
|
925
|
+
stdin.removeListener('data', onData);
|
|
926
|
+
stdin.setRawMode(false);
|
|
927
|
+
cleanup();
|
|
928
|
+
process.exit();
|
|
929
|
+
} else {
|
|
930
|
+
// 单击清空输入
|
|
931
|
+
lastCtrlCTime = now;
|
|
932
|
+
process.stdout.write('\r' + query + ' '.repeat(input.length) + '\r' + query);
|
|
933
|
+
input = '';
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// 斜杠命令选择器
|
|
939
|
+
if (char === '/' && input === '') {
|
|
791
940
|
stdin.removeListener('data', onData);
|
|
792
|
-
|
|
793
|
-
|
|
941
|
+
// 显示命令选择器
|
|
942
|
+
showCommandSelector(stdin, query, resolve);
|
|
943
|
+
return;
|
|
794
944
|
}
|
|
795
945
|
|
|
796
946
|
// 退格
|
|
@@ -816,6 +966,108 @@ function chatPrompt(query) {
|
|
|
816
966
|
});
|
|
817
967
|
}
|
|
818
968
|
|
|
969
|
+
// 斜杠命令选择器
|
|
970
|
+
function showCommandSelector(stdin, query, resolve) {
|
|
971
|
+
let selected = 0;
|
|
972
|
+
|
|
973
|
+
// 清除当前行并显示命令列表
|
|
974
|
+
process.stdout.write('\r\x1b[K');
|
|
975
|
+
console.log();
|
|
976
|
+
|
|
977
|
+
const drawCommands = () => {
|
|
978
|
+
// 上移到命令列表开始位置
|
|
979
|
+
process.stdout.write(`\x1b[${slashCommands.length}A`);
|
|
980
|
+
slashCommands.forEach((cmd, i) => {
|
|
981
|
+
const prefix = i === selected ? c('cyan', '> ') : ' ';
|
|
982
|
+
const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
|
|
983
|
+
console.log(` ${prefix}${cmdText} ${c('gray', cmd.desc)}\x1b[K`);
|
|
984
|
+
});
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// 初始绘制
|
|
988
|
+
slashCommands.forEach((cmd, i) => {
|
|
989
|
+
const prefix = i === selected ? c('cyan', '> ') : ' ';
|
|
990
|
+
const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
|
|
991
|
+
console.log(` ${prefix}${cmdText} ${c('gray', cmd.desc)}`);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
const onSelect = (key) => {
|
|
995
|
+
// 上下键
|
|
996
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
997
|
+
if (key[2] === 65) { // 上
|
|
998
|
+
selected = selected > 0 ? selected - 1 : slashCommands.length - 1;
|
|
999
|
+
drawCommands();
|
|
1000
|
+
} else if (key[2] === 66) { // 下
|
|
1001
|
+
selected = selected < slashCommands.length - 1 ? selected + 1 : 0;
|
|
1002
|
+
drawCommands();
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Enter - 选择命令
|
|
1008
|
+
if (key[0] === 13) {
|
|
1009
|
+
stdin.removeListener('data', onSelect);
|
|
1010
|
+
stdin.setRawMode(false);
|
|
1011
|
+
// 清除命令列表
|
|
1012
|
+
process.stdout.write(`\x1b[${slashCommands.length}A`);
|
|
1013
|
+
for (let i = 0; i < slashCommands.length; i++) {
|
|
1014
|
+
console.log('\x1b[K');
|
|
1015
|
+
}
|
|
1016
|
+
process.stdout.write(`\x1b[${slashCommands.length}A`);
|
|
1017
|
+
resolve(slashCommands[selected].cmd);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Esc - 取消
|
|
1022
|
+
if (key[0] === 27 && key.length === 1) {
|
|
1023
|
+
stdin.removeListener('data', onSelect);
|
|
1024
|
+
// 清除命令列表,重新开始输入
|
|
1025
|
+
process.stdout.write(`\x1b[${slashCommands.length}A`);
|
|
1026
|
+
for (let i = 0; i < slashCommands.length; i++) {
|
|
1027
|
+
console.log('\x1b[K');
|
|
1028
|
+
}
|
|
1029
|
+
process.stdout.write(`\x1b[${slashCommands.length}A`);
|
|
1030
|
+
process.stdout.write(query);
|
|
1031
|
+
// 重新监听输入
|
|
1032
|
+
let input = '';
|
|
1033
|
+
const onData = (chunk) => {
|
|
1034
|
+
const char = chunk.toString();
|
|
1035
|
+
if (char === '\r' || char === '\n') {
|
|
1036
|
+
stdin.removeListener('data', onData);
|
|
1037
|
+
stdin.setRawMode(false);
|
|
1038
|
+
console.log();
|
|
1039
|
+
resolve(input);
|
|
1040
|
+
} else if (char === '\u0003') {
|
|
1041
|
+
stdin.removeListener('data', onData);
|
|
1042
|
+
stdin.setRawMode(false);
|
|
1043
|
+
cleanup();
|
|
1044
|
+
process.exit();
|
|
1045
|
+
} else if (char === '\u007f' || char === '\b') {
|
|
1046
|
+
if (input.length > 0) {
|
|
1047
|
+
input = input.slice(0, -1);
|
|
1048
|
+
process.stdout.write('\b \b');
|
|
1049
|
+
}
|
|
1050
|
+
} else if (char >= ' ') {
|
|
1051
|
+
input += char;
|
|
1052
|
+
process.stdout.write(char);
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
stdin.on('data', onData);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Ctrl+C - 退出
|
|
1060
|
+
if (key[0] === 3) {
|
|
1061
|
+
stdin.removeListener('data', onSelect);
|
|
1062
|
+
stdin.setRawMode(false);
|
|
1063
|
+
cleanup();
|
|
1064
|
+
process.exit();
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
stdin.on('data', onSelect);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
819
1071
|
function cleanup() {
|
|
820
1072
|
showCursor();
|
|
821
1073
|
clearScreen();
|