@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.
Files changed (3) hide show
  1. package/bin/api.js +12 -2
  2. package/bin/index.js +299 -45
  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,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
- while (true) {
682
- const { done, value } = await reader.read();
683
- if (done) break;
684
-
685
- buffer += decoder.decode(value, { stream: true });
686
- const lines = buffer.split('\n');
687
- buffer = lines.pop() || '';
688
-
689
- for (const line of lines) {
690
- if (line.startsWith('data: ')) {
691
- const data = line.slice(6);
692
- if (data === '[DONE]') continue;
693
-
694
- try {
695
- const json = JSON.parse(data);
696
-
697
- if (json.type === 'info') {
698
- currentConversationId = json.conversationId;
699
- } else if (json.type === 'tool_call') {
700
- // 显示搜索状态并添加到消息
701
- process.stdout.write('\r\x1b[K');
702
- process.stdout.write(c('cyan', ` 🔍 正在搜索: ${json.query}...`));
703
- messages.push({ role: 'system', text: `🔍 正在搜索: ${json.query}` });
704
- } else if (json.type === 'tool_result') {
705
- // 显示搜索结果数量并添加到消息
706
- process.stdout.write('\r\x1b[K');
707
- process.stdout.write(c('green', ` 📋 找到 ${json.count} 条结果\n`));
708
- process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
709
- messages.push({ role: 'system', text: `📋 找到 ${json.count} 条结果` });
710
- } else if (json.type === 'content') {
711
- fullResponse += json.content;
712
- // 打字机效果:直接输出内容
713
- process.stdout.write(json.content);
714
- // 统计换行数
715
- 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
+ // 忽略解析错误
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
- 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
+
726
772
  // 重绘消息列表(带 markdown 渲染)
727
773
  drawMessages();
728
774
  await sleep(100); // 短暂延迟让用户看到渲染效果
729
775
  } catch (err) {
730
776
  process.stdout.write('\r\x1b[K');
731
- console.log(c('red', ` 错误: ${err.message}`));
732
- 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;
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+C 清空 (双击退出) Ctrl+V 粘贴图片'));
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
- // 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 键 - 发送消息
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivetu53/soul-chat",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Soul Chat - 智能 AI 伴侣命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {