@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.
- package/bin/index.js +103 -172
- package/package.json +8 -7
package/bin/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
|
956
|
-
let
|
|
957
|
-
|
|
958
|
-
|
|
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 = (
|
|
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
|
-
//
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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+
|
|
1015
|
-
if (
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
process.
|
|
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
|
-
//
|
|
1025
|
-
if (
|
|
1026
|
-
if (
|
|
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(
|
|
998
|
+
resolve('/rollback');
|
|
999
|
+
return;
|
|
1035
1000
|
}
|
|
1036
|
-
|
|
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
|
-
//
|
|
1057
|
-
if (
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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 (
|
|
1025
|
+
// Enter - 发送
|
|
1026
|
+
if (key[0] === 13) {
|
|
1074
1027
|
stdin.removeListener('data', onData);
|
|
1075
|
-
|
|
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 (
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1036
|
+
// Ctrl+J - 换行符号
|
|
1037
|
+
if (key[0] === 10) {
|
|
1038
|
+
input += '↵';
|
|
1039
|
+
process.stdout.write('↵');
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1085
1042
|
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
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 (
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
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.
|
|
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": [
|
|
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
|
-
"
|
|
35
|
+
"terminal-kit": "^3.1.2"
|
|
35
36
|
}
|
|
36
37
|
}
|