@fivetu53/soul-chat 1.1.5 → 1.1.6

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 (2) hide show
  1. package/bin/index.js +103 -172
  2. package/package.json +8 -7
package/bin/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import readline from 'readline';
3
+ import termkit from 'terminal-kit';
4
4
  import { execSync } from 'child_process';
5
5
  import fs from 'fs';
6
6
  import os from 'os';
7
7
  import { Marked } from 'marked';
8
8
  import { markedTerminal } from 'marked-terminal';
9
+
10
+ const term = termkit.terminal;
9
11
  import { loadConfig, saveConfig, getConfigPath } from './config.js';
10
12
  import { checkAuth, showAuthScreen } from './auth.js';
11
13
  import {
@@ -940,174 +942,144 @@ const slashCommands = [
940
942
  { cmd: '/help', desc: '显示帮助' },
941
943
  ];
942
944
 
945
+ // 聊天输入历史
946
+ const chatHistory = [];
947
+
943
948
  function chatPrompt(query) {
944
949
  return new Promise((resolve) => {
945
- const stdin = process.stdin;
946
-
947
- // 先打印下方提示
948
- console.log(c('hint', ' 输入 / 选择命令 Ctrl+J 换行 Esc 双击回滚 Ctrl+C 双击退出'));
950
+ // 打印提示
951
+ console.log(c('hint', ' 输入 / 选择命令 Esc 双击回滚 Ctrl+C 双击退出 Ctrl+J 换行'));
949
952
  console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
950
- // 光标上移3行,显示输入提示
951
953
  process.stdout.write('\x1b[3A');
952
954
  process.stdout.write(query);
953
955
 
954
956
  let input = '';
955
- let cursorPos = 0; // 光标位置
956
- let lastInputTime = Date.now();
957
-
958
- // 计算字符显示宽度(中文2,英文1)
959
- const charWidth = (ch) => ch.charCodeAt(0) > 127 ? 2 : 1;
960
- const strWidth = (str) => [...str].reduce((w, ch) => w + charWidth(ch), 0);
957
+ let lastCtrlC = 0;
958
+ let lastEsc = 0;
959
+ let historyIndex = chatHistory.length;
960
+ let savedInput = '';
961
961
 
962
+ const stdin = process.stdin;
962
963
  stdin.setRawMode(true);
963
964
  stdin.resume();
964
965
 
965
- const onData = (chunk) => {
966
- const char = chunk.toString();
966
+ const onData = (key) => {
967
967
  const now = Date.now();
968
- const timeDiff = now - lastInputTime;
969
- lastInputTime = now;
970
-
971
- // 方向键处理 (ESC [ A/B/C/D)
972
- if (chunk[0] === 27 && chunk[1] === 91) {
973
- if (chunk[2] === 68) { // 左
974
- if (cursorPos > 0) {
975
- const ch = input[cursorPos - 1];
976
- if (ch === '\n') {
977
- // 跨行移动太复杂,暂不支持
978
- } else {
979
- cursorPos--;
980
- process.stdout.write('\x1b[' + charWidth(ch) + 'D');
981
- }
982
- }
983
- return;
984
- }
985
- if (chunk[2] === 67) { // 右
986
- if (cursorPos < input.length) {
987
- const ch = input[cursorPos];
988
- if (ch === '\n') {
989
- // 跨行移动太复杂,暂不支持
990
- } else {
991
- cursorPos++;
992
- process.stdout.write('\x1b[' + charWidth(ch) + 'C');
993
- }
994
- }
995
- return;
996
- }
997
- // 上下键忽略
998
- return;
999
- }
1000
968
 
1001
- // Ctrl+V - 粘贴图片
1002
- if (char === '\x16') {
1003
- const img = getClipboardImage();
1004
- if (img) {
1005
- pendingImage = img;
1006
- const hint = `[图片 ${img.width}x${img.height}] `;
1007
- input = input.slice(0, cursorPos) + hint + input.slice(cursorPos);
1008
- cursorPos += hint.length;
1009
- process.stdout.write(c('cyan', hint));
1010
- }
969
+ // 检测粘贴(多字符同时到达)
970
+ if (key.length > 1 && key[0] !== 27) {
971
+ // 粘贴模式:将换行替换为 符号
972
+ let text = key.toString().replace(/\r\n|\r|\n/g, '↵');
973
+ input += text;
974
+ process.stdout.write(text);
1011
975
  return;
1012
976
  }
1013
977
 
1014
- // Ctrl+J - 插入换行(只在行尾支持)
1015
- if (char === '\x0a' && chunk.length === 1 && chunk[0] === 10) {
1016
- if (cursorPos === input.length) {
1017
- input += '\n';
1018
- cursorPos++;
1019
- process.stdout.write('\n ');
978
+ // Ctrl+C
979
+ if (key[0] === 3) {
980
+ if (now - lastCtrlC < 500) {
981
+ stdin.setRawMode(false);
982
+ cleanup();
983
+ process.exit();
1020
984
  }
985
+ lastCtrlC = now;
986
+ // 清空当前行
987
+ process.stdout.write('\r' + query + '\x1b[K');
988
+ input = '';
1021
989
  return;
1022
990
  }
1023
991
 
1024
- // Enter 键 - 发送消息
1025
- if (char === '\r' || char === '\n') {
1026
- if (timeDiff < 10 && input.length > 0) {
1027
- input = input.slice(0, cursorPos) + ' ' + input.slice(cursorPos);
1028
- cursorPos++;
1029
- process.stdout.write(' ');
1030
- } else {
992
+ // Esc
993
+ if (key[0] === 27 && key.length === 1) {
994
+ if (now - lastEsc < 500) {
1031
995
  stdin.removeListener('data', onData);
1032
996
  stdin.setRawMode(false);
1033
997
  console.log();
1034
- resolve(input);
998
+ resolve('/rollback');
999
+ return;
1035
1000
  }
1036
- return;
1037
- }
1038
-
1039
- // Ctrl+C - 单击清空,双击退出
1040
- if (char === '\u0003') {
1041
- if (now - lastCtrlCTime < 500) {
1042
- stdin.removeListener('data', onData);
1043
- stdin.setRawMode(false);
1044
- cleanup();
1045
- process.exit();
1046
- } else {
1047
- lastCtrlCTime = now;
1048
- // 清空输入
1049
- process.stdout.write('\r' + query + ' '.repeat(strWidth(input)) + '\r' + query);
1050
- input = '';
1051
- cursorPos = 0;
1001
+ lastEsc = now;
1002
+ if (currentAbortController) {
1003
+ currentAbortController.abort();
1052
1004
  }
1053
1005
  return;
1054
1006
  }
1055
1007
 
1056
- // Esc - 单击打断回复,双击触发回滚
1057
- if (chunk[0] === 27 && chunk.length === 1) {
1058
- if (now - lastEscTime < 500) {
1059
- stdin.removeListener('data', onData);
1060
- stdin.setRawMode(false);
1061
- console.log();
1062
- resolve('/rollback');
1063
- } else {
1064
- lastEscTime = now;
1065
- if (currentAbortController) {
1066
- currentAbortController.abort();
1067
- }
1008
+ // 上下方向键 - 历史
1009
+ if (key[0] === 27 && key[1] === 91) {
1010
+ if (key[2] === 65 && historyIndex > 0) { // 上
1011
+ if (historyIndex === chatHistory.length) savedInput = input;
1012
+ historyIndex--;
1013
+ process.stdout.write('\r' + query + '\x1b[K');
1014
+ input = chatHistory[historyIndex];
1015
+ process.stdout.write(input);
1016
+ } else if (key[2] === 66 && historyIndex < chatHistory.length) { // 下
1017
+ historyIndex++;
1018
+ process.stdout.write('\r' + query + '\x1b[K');
1019
+ input = historyIndex === chatHistory.length ? savedInput : chatHistory[historyIndex];
1020
+ process.stdout.write(input);
1068
1021
  }
1069
1022
  return;
1070
1023
  }
1071
1024
 
1072
- // 斜杠命令选择器
1073
- if (char === '/' && input === '') {
1025
+ // Enter - 发送
1026
+ if (key[0] === 13) {
1074
1027
  stdin.removeListener('data', onData);
1075
- showCommandSelector(stdin, query, resolve);
1028
+ stdin.setRawMode(false);
1029
+ console.log();
1030
+ const finalInput = input.replace(/↵/g, '\n');
1031
+ if (finalInput.trim()) chatHistory.push(input);
1032
+ resolve(finalInput);
1076
1033
  return;
1077
1034
  }
1078
1035
 
1079
- // 退格
1080
- if (char === '\u007f' || char === '\b') {
1081
- if (cursorPos > 0) {
1082
- const delChar = input[cursorPos - 1];
1083
- input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
1084
- cursorPos--;
1036
+ // Ctrl+J - 换行符号
1037
+ if (key[0] === 10) {
1038
+ input += '↵';
1039
+ process.stdout.write('↵');
1040
+ return;
1041
+ }
1085
1042
 
1086
- if (delChar === '\n') {
1087
- process.stdout.write('\x1b[A\x1b[999C');
1043
+ // Backspace
1044
+ if (key[0] === 127) {
1045
+ if (input.length > 0) {
1046
+ const deleted = input[input.length - 1];
1047
+ input = input.slice(0, -1);
1048
+ // 中文等宽字符占2列
1049
+ const isWide = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/.test(deleted);
1050
+ if (isWide) {
1051
+ process.stdout.write('\b\b \b\b');
1088
1052
  } else {
1089
- const w = charWidth(delChar);
1090
- const afterCursor = input.slice(cursorPos);
1091
- // 左移,重绘后面的内容,清除末尾,回到光标位置
1092
- process.stdout.write('\x1b[' + w + 'D');
1093
- process.stdout.write(afterCursor + ' '.repeat(w));
1094
- const moveBack = strWidth(afterCursor) + w;
1095
- if (moveBack > 0) process.stdout.write('\x1b[' + moveBack + 'D');
1053
+ process.stdout.write('\b \b');
1096
1054
  }
1097
1055
  }
1098
1056
  return;
1099
1057
  }
1100
1058
 
1059
+ // Ctrl+V - 粘贴图片
1060
+ if (key[0] === 22) {
1061
+ const img = getClipboardImage();
1062
+ if (img) {
1063
+ pendingImage = img;
1064
+ const hint = `[图片 ${img.width}x${img.height}] `;
1065
+ input += hint;
1066
+ process.stdout.write(hint);
1067
+ }
1068
+ return;
1069
+ }
1070
+
1071
+ // 斜杠命令选择
1072
+ if (key[0] === 47 && input === '') { // /
1073
+ stdin.removeListener('data', onData);
1074
+ showCommandSelector(stdin, query, resolve);
1075
+ return;
1076
+ }
1077
+
1101
1078
  // 普通字符
1102
- if (char >= ' ' || char === '\t') {
1103
- input = input.slice(0, cursorPos) + char + input.slice(cursorPos);
1104
- const afterCursor = input.slice(cursorPos + 1);
1105
- cursorPos++;
1106
- // 输出字符和后面的内容
1107
- process.stdout.write(char + afterCursor);
1108
- // 光标回到正确位置
1109
- const moveBack = strWidth(afterCursor);
1110
- if (moveBack > 0) process.stdout.write('\x1b[' + moveBack + 'D');
1079
+ if (key[0] >= 32 || key[0] === 9) {
1080
+ const char = key.toString();
1081
+ input += char;
1082
+ process.stdout.write(char);
1111
1083
  }
1112
1084
  };
1113
1085
 
@@ -1119,12 +1091,10 @@ function chatPrompt(query) {
1119
1091
  function showCommandSelector(stdin, query, resolve) {
1120
1092
  let selected = 0;
1121
1093
 
1122
- // 清除当前行并显示命令列表
1123
1094
  process.stdout.write('\r\x1b[K');
1124
1095
  console.log();
1125
1096
 
1126
1097
  const drawCommands = () => {
1127
- // 上移到命令列表开始位置
1128
1098
  process.stdout.write(`\x1b[${slashCommands.length}A`);
1129
1099
  slashCommands.forEach((cmd, i) => {
1130
1100
  const prefix = i === selected ? c('cyan', '> ') : ' ';
@@ -1133,7 +1103,6 @@ function showCommandSelector(stdin, query, resolve) {
1133
1103
  });
1134
1104
  };
1135
1105
 
1136
- // 初始绘制
1137
1106
  slashCommands.forEach((cmd, i) => {
1138
1107
  const prefix = i === selected ? c('cyan', '> ') : ' ';
1139
1108
  const cmdText = i === selected ? c('cyan', cmd.cmd.padEnd(10)) : cmd.cmd.padEnd(10);
@@ -1141,71 +1110,33 @@ function showCommandSelector(stdin, query, resolve) {
1141
1110
  });
1142
1111
 
1143
1112
  const onSelect = (key) => {
1144
- // 上下键
1145
1113
  if (key[0] === 27 && key[1] === 91) {
1146
- if (key[2] === 65) { //
1147
- selected = selected > 0 ? selected - 1 : slashCommands.length - 1;
1148
- drawCommands();
1149
- } else if (key[2] === 66) { // 下
1150
- selected = selected < slashCommands.length - 1 ? selected + 1 : 0;
1151
- drawCommands();
1152
- }
1114
+ if (key[2] === 65) selected = selected > 0 ? selected - 1 : slashCommands.length - 1;
1115
+ else if (key[2] === 66) selected = selected < slashCommands.length - 1 ? selected + 1 : 0;
1116
+ drawCommands();
1153
1117
  return;
1154
1118
  }
1155
1119
 
1156
- // Enter - 选择命令
1157
1120
  if (key[0] === 13) {
1158
1121
  stdin.removeListener('data', onSelect);
1159
1122
  stdin.setRawMode(false);
1160
- // 清除命令列表
1161
1123
  process.stdout.write(`\x1b[${slashCommands.length}A`);
1162
- for (let i = 0; i < slashCommands.length; i++) {
1163
- console.log('\x1b[K');
1164
- }
1124
+ for (let i = 0; i < slashCommands.length; i++) console.log('\x1b[K');
1165
1125
  process.stdout.write(`\x1b[${slashCommands.length}A`);
1166
1126
  resolve(slashCommands[selected].cmd);
1167
1127
  return;
1168
1128
  }
1169
1129
 
1170
- // Esc - 取消
1171
1130
  if (key[0] === 27 && key.length === 1) {
1172
1131
  stdin.removeListener('data', onSelect);
1173
- // 清除命令列表,重新开始输入
1174
1132
  process.stdout.write(`\x1b[${slashCommands.length}A`);
1175
- for (let i = 0; i < slashCommands.length; i++) {
1176
- console.log('\x1b[K');
1177
- }
1133
+ for (let i = 0; i < slashCommands.length; i++) console.log('\x1b[K');
1178
1134
  process.stdout.write(`\x1b[${slashCommands.length}A`);
1179
1135
  process.stdout.write(query);
1180
- // 重新监听输入
1181
- let input = '';
1182
- const onData = (chunk) => {
1183
- const char = chunk.toString();
1184
- if (char === '\r' || char === '\n') {
1185
- stdin.removeListener('data', onData);
1186
- stdin.setRawMode(false);
1187
- console.log();
1188
- resolve(input);
1189
- } else if (char === '\u0003') {
1190
- stdin.removeListener('data', onData);
1191
- stdin.setRawMode(false);
1192
- cleanup();
1193
- process.exit();
1194
- } else if (char === '\u007f' || char === '\b') {
1195
- if (input.length > 0) {
1196
- input = input.slice(0, -1);
1197
- process.stdout.write('\b \b');
1198
- }
1199
- } else if (char >= ' ') {
1200
- input += char;
1201
- process.stdout.write(char);
1202
- }
1203
- };
1204
- stdin.on('data', onData);
1136
+ chatPrompt(query).then(resolve);
1205
1137
  return;
1206
1138
  }
1207
1139
 
1208
- // Ctrl+C - 退出
1209
1140
  if (key[0] === 3) {
1210
1141
  stdin.removeListener('data', onSelect);
1211
1142
  stdin.setRawMode(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivetu53/soul-chat",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Soul Chat - 智能 AI 伴侣命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,12 @@
12
12
  "engines": {
13
13
  "node": ">=18.0.0"
14
14
  },
15
- "keywords": ["ai", "chat", "cli", "soul-chat"],
15
+ "keywords": [
16
+ "ai",
17
+ "chat",
18
+ "cli",
19
+ "soul-chat"
20
+ ],
16
21
  "author": "u53",
17
22
  "license": "UNLICENSED",
18
23
  "repository": {
@@ -25,12 +30,8 @@
25
30
  "dev": "node --watch bin/index.js"
26
31
  },
27
32
  "dependencies": {
28
- "ink": "^4.4.1",
29
- "ink-select-input": "^5.0.0",
30
- "ink-spinner": "^5.0.0",
31
- "ink-text-input": "^5.0.1",
32
33
  "marked": "^15.0.12",
33
34
  "marked-terminal": "^7.3.0",
34
- "react": "^18.2.0"
35
+ "terminal-kit": "^3.1.2"
35
36
  }
36
37
  }