@fivetu53/soul-chat 1.0.0
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/api.js +262 -0
- package/bin/auth.js +363 -0
- package/bin/config.js +110 -0
- package/bin/index.js +1076 -0
- package/package.json +36 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { Marked } from 'marked';
|
|
7
|
+
import { markedTerminal } from 'marked-terminal';
|
|
8
|
+
import { loadConfig, saveConfig, getConfigPath } from './config.js';
|
|
9
|
+
import { checkAuth, showAuthScreen } from './auth.js';
|
|
10
|
+
import {
|
|
11
|
+
isLoggedIn, getCurrentUser, logout, updateProfile,
|
|
12
|
+
sendMessage, getCharacters, createCharacter, getConversations, getMessages, clearConversation, getMemories, pinMemory, deleteMemory
|
|
13
|
+
} from './api.js';
|
|
14
|
+
|
|
15
|
+
// Setup marked with terminal renderer
|
|
16
|
+
const marked = new Marked(markedTerminal());
|
|
17
|
+
|
|
18
|
+
// Load global config
|
|
19
|
+
let config = loadConfig();
|
|
20
|
+
let currentUser = null;
|
|
21
|
+
let currentCharacter = null;
|
|
22
|
+
let currentConversationId = null;
|
|
23
|
+
|
|
24
|
+
// ANSI 颜色码
|
|
25
|
+
const colors = {
|
|
26
|
+
reset: '\x1b[0m',
|
|
27
|
+
bold: '\x1b[1m',
|
|
28
|
+
dim: '\x1b[2m',
|
|
29
|
+
italic: '\x1b[3m',
|
|
30
|
+
underline: '\x1b[4m',
|
|
31
|
+
red: '\x1b[31m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
blue: '\x1b[34m',
|
|
35
|
+
magenta: '\x1b[35m',
|
|
36
|
+
cyan: '\x1b[36m',
|
|
37
|
+
white: '\x1b[37m',
|
|
38
|
+
gray: '\x1b[90m',
|
|
39
|
+
bgRed: '\x1b[41m',
|
|
40
|
+
bgGreen: '\x1b[42m',
|
|
41
|
+
bgYellow: '\x1b[43m',
|
|
42
|
+
bgBlue: '\x1b[44m',
|
|
43
|
+
bgMagenta: '\x1b[45m',
|
|
44
|
+
bgCyan: '\x1b[46m',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 主题配色方案
|
|
48
|
+
const themes = {
|
|
49
|
+
default: { // 黑金主题
|
|
50
|
+
primary: 'yellow', // 主色调
|
|
51
|
+
secondary: 'cyan', // 次要色
|
|
52
|
+
user: 'green', // 用户消息
|
|
53
|
+
bot: 'cyan', // AI消息
|
|
54
|
+
hint: 'gray', // 提示文字
|
|
55
|
+
success: 'green',
|
|
56
|
+
error: 'red',
|
|
57
|
+
warning: 'yellow',
|
|
58
|
+
},
|
|
59
|
+
dark: { // 暗夜主题
|
|
60
|
+
primary: 'blue',
|
|
61
|
+
secondary: 'magenta',
|
|
62
|
+
user: 'white',
|
|
63
|
+
bot: 'magenta',
|
|
64
|
+
hint: 'gray',
|
|
65
|
+
success: 'green',
|
|
66
|
+
error: 'red',
|
|
67
|
+
warning: 'yellow',
|
|
68
|
+
},
|
|
69
|
+
light: { // 清新主题
|
|
70
|
+
primary: 'magenta',
|
|
71
|
+
secondary: 'cyan',
|
|
72
|
+
user: 'blue',
|
|
73
|
+
bot: 'cyan',
|
|
74
|
+
hint: 'gray',
|
|
75
|
+
success: 'green',
|
|
76
|
+
error: 'red',
|
|
77
|
+
warning: 'yellow',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 获取当前主题色
|
|
82
|
+
const getThemeColor = (semantic) => {
|
|
83
|
+
const theme = themes[config.theme] || themes.default;
|
|
84
|
+
return theme[semantic] || semantic;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const c = (color, text) => {
|
|
88
|
+
// 支持语义化颜色名
|
|
89
|
+
const actualColor = getThemeColor(color);
|
|
90
|
+
return `${colors[actualColor] || colors[color] || ''}${text}${colors.reset}`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// 工具函数
|
|
94
|
+
const clearScreen = () => process.stdout.write('\x1b[2J\x1b[H');
|
|
95
|
+
const moveCursor = (row, col) => process.stdout.write(`\x1b[${row};${col}H`);
|
|
96
|
+
const hideCursor = () => process.stdout.write('\x1b[?25l');
|
|
97
|
+
const showCursor = () => process.stdout.write('\x1b[?25h');
|
|
98
|
+
|
|
99
|
+
// 获取剪贴板图片 (跨平台)
|
|
100
|
+
function getClipboardImage() {
|
|
101
|
+
const tmpFile = process.platform === 'win32' ? 'C:\\Temp\\soul_clipboard.png' : '/tmp/soul_clipboard.png';
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
if (process.platform === 'darwin') {
|
|
105
|
+
// macOS: 用 osascript 导出剪贴板图片
|
|
106
|
+
const script = `set theFile to POSIX file "${tmpFile}"
|
|
107
|
+
try
|
|
108
|
+
set imgData to the clipboard as «class PNGf»
|
|
109
|
+
set fileRef to open for access theFile with write permission
|
|
110
|
+
write imgData to fileRef
|
|
111
|
+
close access fileRef
|
|
112
|
+
return "ok"
|
|
113
|
+
on error
|
|
114
|
+
return "no"
|
|
115
|
+
end try`;
|
|
116
|
+
const result = execSync(`osascript -e '${script}'`, { encoding: 'utf8' }).trim();
|
|
117
|
+
if (result !== 'ok') return null;
|
|
118
|
+
} else if (process.platform === 'win32') {
|
|
119
|
+
// Windows: 用 PowerShell 导出剪贴板图片
|
|
120
|
+
const ps = `$img = Get-Clipboard -Format Image; if ($img) { $img.Save('${tmpFile}'); 'ok' } else { 'no' }`;
|
|
121
|
+
const result = execSync(`powershell -Command "${ps}"`, { encoding: 'utf8' }).trim();
|
|
122
|
+
if (result !== 'ok') return null;
|
|
123
|
+
} else {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const buffer = fs.readFileSync(tmpFile);
|
|
128
|
+
fs.unlinkSync(tmpFile);
|
|
129
|
+
|
|
130
|
+
// 解析 PNG 尺寸
|
|
131
|
+
let width = 0, height = 0;
|
|
132
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50) {
|
|
133
|
+
width = buffer.readUInt32BE(16);
|
|
134
|
+
height = buffer.readUInt32BE(20);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { base64: buffer.toString('base64'), width, height };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// UI 组件 - 画边框
|
|
144
|
+
function drawBox(title, width = 50) {
|
|
145
|
+
const top = '+' + '-'.repeat(width - 2) + '+';
|
|
146
|
+
const bottom = '+' + '-'.repeat(width - 2) + '+';
|
|
147
|
+
const padding = Math.floor((width - 2 - title.length) / 2);
|
|
148
|
+
const middle = '|' + ' '.repeat(padding) + title + ' '.repeat(width - 2 - padding - title.length) + '|';
|
|
149
|
+
|
|
150
|
+
console.log(c('cyan', top));
|
|
151
|
+
console.log(c('cyan', middle));
|
|
152
|
+
console.log(c('cyan', bottom));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 加载动画帧
|
|
156
|
+
const spinnerFrames = ['|', '/', '-', '\\'];
|
|
157
|
+
|
|
158
|
+
// 页面:欢迎
|
|
159
|
+
async function showWelcome() {
|
|
160
|
+
clearScreen();
|
|
161
|
+
console.log();
|
|
162
|
+
drawBox('[*] Soul Chat', 40);
|
|
163
|
+
console.log();
|
|
164
|
+
|
|
165
|
+
// 显示当前用户
|
|
166
|
+
console.log(c('user', ` 你好, ${currentUser.username}!`));
|
|
167
|
+
console.log();
|
|
168
|
+
|
|
169
|
+
// 显示角色信息
|
|
170
|
+
if (currentCharacter) {
|
|
171
|
+
console.log(c('secondary', ` 当前角色: ${currentCharacter.name}`));
|
|
172
|
+
if (currentCharacter.description) {
|
|
173
|
+
console.log(c('hint', ` ${currentCharacter.description}`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(c('hint', ' 按 Enter 键继续...'));
|
|
181
|
+
|
|
182
|
+
// 更新最后访问时间
|
|
183
|
+
config.lastVisit = new Date().toLocaleString('zh-CN');
|
|
184
|
+
saveConfig(config);
|
|
185
|
+
|
|
186
|
+
await waitForEnter();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 页面:菜单
|
|
190
|
+
async function showMenu() {
|
|
191
|
+
const menuItems = [
|
|
192
|
+
{ label: '[1] 开始聊天', value: 'chat' },
|
|
193
|
+
{ label: '[2] 角色管理', value: 'characters' },
|
|
194
|
+
{ label: '[3] 记忆查看', value: 'memories' },
|
|
195
|
+
{ label: '[4] 设置', value: 'settings' },
|
|
196
|
+
{ label: '[5] 退出登录', value: 'logout' },
|
|
197
|
+
{ label: '[6] 退出程序', value: 'exit' },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
let selected = 0;
|
|
201
|
+
|
|
202
|
+
const draw = () => {
|
|
203
|
+
clearScreen();
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(c('primary', c('bold', ` ${config.username},请选择一个功能:`)));
|
|
206
|
+
console.log();
|
|
207
|
+
|
|
208
|
+
menuItems.forEach((item, i) => {
|
|
209
|
+
if (i === selected) {
|
|
210
|
+
console.log(c('secondary', ` > ${item.label}`));
|
|
211
|
+
} else {
|
|
212
|
+
console.log(` ${item.label}`);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
console.log();
|
|
217
|
+
console.log(c('hint', ' 使用方向键选择,Enter 确认'));
|
|
218
|
+
console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
draw();
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
process.stdin.setRawMode(true);
|
|
225
|
+
process.stdin.resume();
|
|
226
|
+
|
|
227
|
+
const onKeyPress = (key) => {
|
|
228
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
229
|
+
if (key[2] === 65) { // 上
|
|
230
|
+
selected = selected > 0 ? selected - 1 : menuItems.length - 1;
|
|
231
|
+
draw();
|
|
232
|
+
} else if (key[2] === 66) { // 下
|
|
233
|
+
selected = selected < menuItems.length - 1 ? selected + 1 : 0;
|
|
234
|
+
draw();
|
|
235
|
+
}
|
|
236
|
+
} else if (key[0] === 13) { // Enter
|
|
237
|
+
process.stdin.setRawMode(false);
|
|
238
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
239
|
+
resolve(menuItems[selected].value);
|
|
240
|
+
} else if (key[0] === 3) { // Ctrl+C
|
|
241
|
+
cleanup();
|
|
242
|
+
process.exit();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
process.stdin.on('data', onKeyPress);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 页面:设置
|
|
251
|
+
async function showSettings() {
|
|
252
|
+
const settingsItems = [
|
|
253
|
+
{ label: '修改用户名', key: 'username' },
|
|
254
|
+
{ label: '修改主题', key: 'theme' },
|
|
255
|
+
{ label: '查看配置文件路径', key: 'path' },
|
|
256
|
+
{ label: '返回主菜单', key: 'back' },
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
let selected = 0;
|
|
260
|
+
|
|
261
|
+
const draw = () => {
|
|
262
|
+
clearScreen();
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(c('yellow', c('bold', ' [*] 设置')));
|
|
265
|
+
console.log();
|
|
266
|
+
console.log(c('gray', ` 配置文件: ${getConfigPath()}`));
|
|
267
|
+
console.log();
|
|
268
|
+
console.log(c('white', ' 当前设置:'));
|
|
269
|
+
console.log(c('cyan', ` 用户名: ${config.username}`));
|
|
270
|
+
console.log(c('cyan', ` 主题: ${config.theme}`));
|
|
271
|
+
console.log(c('cyan', ` 语言: ${config.language}`));
|
|
272
|
+
console.log();
|
|
273
|
+
|
|
274
|
+
settingsItems.forEach((item, i) => {
|
|
275
|
+
if (i === selected) {
|
|
276
|
+
console.log(c('cyan', ` > ${item.label}`));
|
|
277
|
+
} else {
|
|
278
|
+
console.log(` ${item.label}`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
console.log();
|
|
283
|
+
console.log(c('gray', ' 使用方向键选择,Enter 确认'));
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
draw();
|
|
287
|
+
|
|
288
|
+
while (true) {
|
|
289
|
+
const choice = await new Promise((resolve) => {
|
|
290
|
+
process.stdin.setRawMode(true);
|
|
291
|
+
process.stdin.resume();
|
|
292
|
+
|
|
293
|
+
const onKeyPress = (key) => {
|
|
294
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
295
|
+
if (key[2] === 65) { // 上
|
|
296
|
+
selected = selected > 0 ? selected - 1 : settingsItems.length - 1;
|
|
297
|
+
draw();
|
|
298
|
+
} else if (key[2] === 66) { // 下
|
|
299
|
+
selected = selected < settingsItems.length - 1 ? selected + 1 : 0;
|
|
300
|
+
draw();
|
|
301
|
+
}
|
|
302
|
+
} else if (key[0] === 13) { // Enter
|
|
303
|
+
process.stdin.setRawMode(false);
|
|
304
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
305
|
+
resolve(settingsItems[selected].key);
|
|
306
|
+
} else if (key[0] === 3) { // Ctrl+C
|
|
307
|
+
cleanup();
|
|
308
|
+
process.exit();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
process.stdin.on('data', onKeyPress);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (choice === 'back') {
|
|
316
|
+
return;
|
|
317
|
+
} else if (choice === 'username') {
|
|
318
|
+
clearScreen();
|
|
319
|
+
console.log();
|
|
320
|
+
console.log(c('yellow', ' 请输入新的用户名:'));
|
|
321
|
+
const newName = await prompt(c('cyan', ' > '));
|
|
322
|
+
if (newName.trim() && newName.trim().length >= 2) {
|
|
323
|
+
try {
|
|
324
|
+
console.log(c('gray', ' 更新中...'));
|
|
325
|
+
const result = await updateProfile(newName.trim());
|
|
326
|
+
config.username = result.user.username;
|
|
327
|
+
currentUser = result.user;
|
|
328
|
+
saveConfig(config);
|
|
329
|
+
console.log(c('green', ' 用户名已更新!'));
|
|
330
|
+
await sleep(1000);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.log(c('red', ` 错误: ${err.message}`));
|
|
333
|
+
await sleep(1500);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
draw();
|
|
337
|
+
} else if (choice === 'theme') {
|
|
338
|
+
clearScreen();
|
|
339
|
+
console.log();
|
|
340
|
+
console.log(c('yellow', ' 可用主题: default, dark, light'));
|
|
341
|
+
const newTheme = await prompt(c('cyan', ' > '));
|
|
342
|
+
if (['default', 'dark', 'light'].includes(newTheme.trim())) {
|
|
343
|
+
config.theme = newTheme.trim();
|
|
344
|
+
saveConfig(config);
|
|
345
|
+
}
|
|
346
|
+
draw();
|
|
347
|
+
} else if (choice === 'path') {
|
|
348
|
+
clearScreen();
|
|
349
|
+
console.log();
|
|
350
|
+
console.log(c('yellow', ' 配置文件位置:'));
|
|
351
|
+
console.log();
|
|
352
|
+
console.log(c('green', ` ${getConfigPath()}`));
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(c('gray', ' 按 Enter 返回'));
|
|
355
|
+
await waitForEnter();
|
|
356
|
+
draw();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 页面:进度条
|
|
362
|
+
async function showProgress() {
|
|
363
|
+
clearScreen();
|
|
364
|
+
console.log();
|
|
365
|
+
console.log(c('magenta', c('bold', ' [*] 进度条演示')));
|
|
366
|
+
console.log();
|
|
367
|
+
|
|
368
|
+
hideCursor();
|
|
369
|
+
|
|
370
|
+
for (let progress = 0; progress <= 100; progress += 2) {
|
|
371
|
+
const width = 40;
|
|
372
|
+
const filled = Math.round((progress / 100) * width);
|
|
373
|
+
const empty = width - filled;
|
|
374
|
+
const bar = '#'.repeat(filled) + '-'.repeat(empty);
|
|
375
|
+
|
|
376
|
+
const frame = spinnerFrames[Math.floor(progress / 2) % spinnerFrames.length];
|
|
377
|
+
|
|
378
|
+
moveCursor(4, 3);
|
|
379
|
+
if (progress < 100) {
|
|
380
|
+
process.stdout.write(c('green', `${frame} 加载中...`));
|
|
381
|
+
} else {
|
|
382
|
+
process.stdout.write(c('green', '[OK] 完成! '));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
moveCursor(6, 3);
|
|
386
|
+
process.stdout.write(c('cyan', `[${bar}] ${progress}%`));
|
|
387
|
+
|
|
388
|
+
await sleep(50);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
showCursor();
|
|
392
|
+
|
|
393
|
+
console.log();
|
|
394
|
+
console.log();
|
|
395
|
+
console.log(c('gray', ' 按 Enter 返回'));
|
|
396
|
+
await waitForEnter();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 页面:聊天
|
|
400
|
+
async function showChat() {
|
|
401
|
+
if (!currentCharacter) {
|
|
402
|
+
clearScreen();
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(c('yellow', ' 请先选择一个角色'));
|
|
405
|
+
console.log(c('gray', ' 按 Enter 返回'));
|
|
406
|
+
await waitForEnter();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const messages = [];
|
|
411
|
+
|
|
412
|
+
// 如果没有会话ID,先获取现有会话
|
|
413
|
+
if (!currentConversationId) {
|
|
414
|
+
try {
|
|
415
|
+
const conversations = await getConversations();
|
|
416
|
+
const conv = conversations.find(c => c.character_id === currentCharacter.id);
|
|
417
|
+
if (conv) {
|
|
418
|
+
currentConversationId = conv.id;
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
// 忽略
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 加载历史消息
|
|
426
|
+
if (currentConversationId) {
|
|
427
|
+
try {
|
|
428
|
+
const history = await getMessages(currentConversationId);
|
|
429
|
+
history.forEach(m => {
|
|
430
|
+
messages.push({ role: m.role === 'user' ? 'user' : 'bot', text: m.content, id: m.id });
|
|
431
|
+
});
|
|
432
|
+
} catch (err) {
|
|
433
|
+
// 忽略
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (messages.length === 0) {
|
|
438
|
+
messages.push({ role: 'bot', text: `你好! 我是 ${currentCharacter.name}。有什么我可以帮你的吗?` });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const drawMessages = () => {
|
|
442
|
+
clearScreen();
|
|
443
|
+
console.log();
|
|
444
|
+
console.log(c('secondary', c('bold', ` [*] 与 ${currentCharacter.name} 聊天`)));
|
|
445
|
+
console.log();
|
|
446
|
+
|
|
447
|
+
messages.slice(-12).forEach(msg => {
|
|
448
|
+
if (msg.role === 'user') {
|
|
449
|
+
console.log(c('user', ` ${currentUser.username}: ${msg.text}`));
|
|
450
|
+
} else {
|
|
451
|
+
// 渲染 markdown
|
|
452
|
+
const rendered = marked.parse(msg.text).trim();
|
|
453
|
+
const indented = rendered.split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n');
|
|
454
|
+
console.log(c('bot', ` ${currentCharacter.name}: `) + indented);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
console.log();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
while (true) {
|
|
462
|
+
drawMessages();
|
|
463
|
+
const input = await chatPrompt(c('cyan', ' > '));
|
|
464
|
+
|
|
465
|
+
if (input === '/back') break;
|
|
466
|
+
if (!input.trim()) continue;
|
|
467
|
+
|
|
468
|
+
if (input === '/clear') {
|
|
469
|
+
if (currentConversationId) {
|
|
470
|
+
try {
|
|
471
|
+
await clearConversation(currentConversationId);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
// 忽略
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
messages.length = 0;
|
|
477
|
+
currentConversationId = null;
|
|
478
|
+
messages.push({ role: 'bot', text: `对话已清空,记忆仍然保留。有什么我可以帮你的吗?` });
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
messages.push({ role: 'user', text: input });
|
|
483
|
+
drawMessages();
|
|
484
|
+
|
|
485
|
+
// 显示思考中
|
|
486
|
+
process.stdout.write(c('bot', ` ${currentCharacter.name}: `) + c('hint', '思考中...'));
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
// 发送消息(带图片)
|
|
490
|
+
const imageBase64 = pendingImage?.base64 || null;
|
|
491
|
+
pendingImage = null; // 清空待发送图片
|
|
492
|
+
const response = await sendMessage(input, currentCharacter.id, currentConversationId, imageBase64);
|
|
493
|
+
|
|
494
|
+
// 清除思考中,开始打字机效果
|
|
495
|
+
process.stdout.write('\r\x1b[K');
|
|
496
|
+
process.stdout.write(c('bot', ` ${currentCharacter.name}: `));
|
|
497
|
+
|
|
498
|
+
let fullResponse = '';
|
|
499
|
+
let lineCount = 0;
|
|
500
|
+
|
|
501
|
+
// 解析 SSE 流
|
|
502
|
+
const reader = response.body.getReader();
|
|
503
|
+
const decoder = new TextDecoder();
|
|
504
|
+
let buffer = '';
|
|
505
|
+
|
|
506
|
+
while (true) {
|
|
507
|
+
const { done, value } = await reader.read();
|
|
508
|
+
if (done) break;
|
|
509
|
+
|
|
510
|
+
buffer += decoder.decode(value, { stream: true });
|
|
511
|
+
const lines = buffer.split('\n');
|
|
512
|
+
buffer = lines.pop() || '';
|
|
513
|
+
|
|
514
|
+
for (const line of lines) {
|
|
515
|
+
if (line.startsWith('data: ')) {
|
|
516
|
+
const data = line.slice(6);
|
|
517
|
+
if (data === '[DONE]') continue;
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const json = JSON.parse(data);
|
|
521
|
+
|
|
522
|
+
if (json.type === 'info') {
|
|
523
|
+
currentConversationId = json.conversationId;
|
|
524
|
+
} else if (json.type === 'content') {
|
|
525
|
+
fullResponse += json.content;
|
|
526
|
+
// 打字机效果:直接输出内容
|
|
527
|
+
process.stdout.write(json.content);
|
|
528
|
+
// 统计换行数
|
|
529
|
+
lineCount += (json.content.match(/\n/g) || []).length;
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
// 忽略解析错误
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log(); // 结束当前行
|
|
539
|
+
messages.push({ role: 'bot', text: fullResponse || '...' });
|
|
540
|
+
// 重绘消息列表(带 markdown 渲染)
|
|
541
|
+
drawMessages();
|
|
542
|
+
await sleep(100); // 短暂延迟让用户看到渲染效果
|
|
543
|
+
} catch (err) {
|
|
544
|
+
process.stdout.write('\r\x1b[K');
|
|
545
|
+
console.log(c('red', ` 错误: ${err.message}`));
|
|
546
|
+
await sleep(1500);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// 页面:任务列表
|
|
552
|
+
async function showTodo() {
|
|
553
|
+
const todos = [
|
|
554
|
+
{ text: '学习终端 UI', done: true },
|
|
555
|
+
{ text: '创建第一个 CLI 工具', done: false },
|
|
556
|
+
{ text: '发布到 npm', done: false },
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
let selected = 0;
|
|
560
|
+
|
|
561
|
+
const draw = () => {
|
|
562
|
+
clearScreen();
|
|
563
|
+
console.log();
|
|
564
|
+
console.log(c('yellow', c('bold', ' [*] 任务列表')));
|
|
565
|
+
console.log(c('gray', ' Space/Enter 切换状态,q 返回'));
|
|
566
|
+
console.log();
|
|
567
|
+
|
|
568
|
+
todos.forEach((todo, i) => {
|
|
569
|
+
const cursor = i === selected ? c('cyan', '> ') : ' ';
|
|
570
|
+
const checkbox = todo.done ? '[x]' : '[ ]';
|
|
571
|
+
const text = i === selected ? c('cyan', todo.text) : todo.text;
|
|
572
|
+
console.log(` ${cursor}${checkbox} ${text}`);
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
draw();
|
|
577
|
+
|
|
578
|
+
return new Promise((resolve) => {
|
|
579
|
+
process.stdin.setRawMode(true);
|
|
580
|
+
process.stdin.resume();
|
|
581
|
+
|
|
582
|
+
const onKeyPress = (key) => {
|
|
583
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
584
|
+
if (key[2] === 65) { // 上
|
|
585
|
+
selected = selected > 0 ? selected - 1 : todos.length - 1;
|
|
586
|
+
draw();
|
|
587
|
+
} else if (key[2] === 66) { // 下
|
|
588
|
+
selected = selected < todos.length - 1 ? selected + 1 : 0;
|
|
589
|
+
draw();
|
|
590
|
+
}
|
|
591
|
+
} else if (key[0] === 13 || key[0] === 32) { // Enter 或 Space
|
|
592
|
+
todos[selected].done = !todos[selected].done;
|
|
593
|
+
draw();
|
|
594
|
+
} else if (key[0] === 113 || key[0] === 27) { // q 或 Esc
|
|
595
|
+
process.stdin.setRawMode(false);
|
|
596
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
597
|
+
resolve();
|
|
598
|
+
} else if (key[0] === 3) { // Ctrl+C
|
|
599
|
+
cleanup();
|
|
600
|
+
process.exit();
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
process.stdin.on('data', onKeyPress);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 页面:颜色展示
|
|
609
|
+
async function showColors() {
|
|
610
|
+
clearScreen();
|
|
611
|
+
console.log();
|
|
612
|
+
console.log(c('white', c('bold', ' [*] 终端颜色展示')));
|
|
613
|
+
console.log();
|
|
614
|
+
|
|
615
|
+
const colorList = [
|
|
616
|
+
{ name: 'red', label: '红色' },
|
|
617
|
+
{ name: 'green', label: '绿色' },
|
|
618
|
+
{ name: 'yellow', label: '黄色' },
|
|
619
|
+
{ name: 'blue', label: '蓝色' },
|
|
620
|
+
{ name: 'magenta', label: '紫色' },
|
|
621
|
+
{ name: 'cyan', label: '青色' },
|
|
622
|
+
{ name: 'white', label: '白色' },
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
colorList.forEach(color => {
|
|
626
|
+
const bgColor = 'bg' + color.name.charAt(0).toUpperCase() + color.name.slice(1);
|
|
627
|
+
console.log(` ${c(color.name, '####')} ${color.label.padEnd(4)} ${colors[bgColor] || ''} ${color.label}背景 ${colors.reset}`);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
console.log();
|
|
631
|
+
console.log(` ${c('bold', '这是粗体')}`);
|
|
632
|
+
console.log(` ${c('italic', '这是斜体')}`);
|
|
633
|
+
console.log(` ${c('underline', '这是下划线')}`);
|
|
634
|
+
console.log(` ${c('dim', '这是暗淡文字')}`);
|
|
635
|
+
|
|
636
|
+
console.log();
|
|
637
|
+
console.log(c('gray', ' 按 Enter 返回'));
|
|
638
|
+
await waitForEnter();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 工具函数
|
|
642
|
+
function sleep(ms) {
|
|
643
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function waitForEnter() {
|
|
647
|
+
return new Promise((resolve) => {
|
|
648
|
+
const stdin = process.stdin;
|
|
649
|
+
|
|
650
|
+
// 确保 stdin 处于正确状态
|
|
651
|
+
if (stdin.isPaused()) {
|
|
652
|
+
stdin.resume();
|
|
653
|
+
}
|
|
654
|
+
stdin.setRawMode(true);
|
|
655
|
+
|
|
656
|
+
const onKeyPress = (key) => {
|
|
657
|
+
if (key[0] === 13 || key[0] === 3) {
|
|
658
|
+
stdin.removeListener('data', onKeyPress);
|
|
659
|
+
stdin.setRawMode(false);
|
|
660
|
+
if (key[0] === 3) {
|
|
661
|
+
cleanup();
|
|
662
|
+
process.exit();
|
|
663
|
+
}
|
|
664
|
+
resolve();
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
stdin.on('data', onKeyPress);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function prompt(query) {
|
|
673
|
+
const rl = readline.createInterface({
|
|
674
|
+
input: process.stdin,
|
|
675
|
+
output: process.stdout
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
return new Promise((resolve) => {
|
|
679
|
+
rl.question(query, (answer) => {
|
|
680
|
+
rl.close();
|
|
681
|
+
resolve(answer);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 聊天输入(处理粘贴带换行的情况,支持图片粘贴)
|
|
687
|
+
let pendingImage = null; // 待发送的图片
|
|
688
|
+
|
|
689
|
+
function chatPrompt(query) {
|
|
690
|
+
return new Promise((resolve) => {
|
|
691
|
+
const stdin = process.stdin;
|
|
692
|
+
|
|
693
|
+
// 先打印下方提示
|
|
694
|
+
console.log(c('hint', ' /back 返回 /clear 清空对话 Ctrl+V 粘贴图片'));
|
|
695
|
+
console.log(c('primary', ' 门户网站: https://soul-chat.jdctools.com.cn'));
|
|
696
|
+
// 光标上移3行,显示输入提示
|
|
697
|
+
process.stdout.write('\x1b[3A');
|
|
698
|
+
process.stdout.write(query);
|
|
699
|
+
|
|
700
|
+
let input = '';
|
|
701
|
+
let lastInputTime = Date.now();
|
|
702
|
+
|
|
703
|
+
stdin.setRawMode(true);
|
|
704
|
+
stdin.resume();
|
|
705
|
+
|
|
706
|
+
const onData = (chunk) => {
|
|
707
|
+
const char = chunk.toString();
|
|
708
|
+
const now = Date.now();
|
|
709
|
+
const timeDiff = now - lastInputTime;
|
|
710
|
+
lastInputTime = now;
|
|
711
|
+
|
|
712
|
+
// Ctrl+V - 粘贴图片
|
|
713
|
+
if (char === '\x16') {
|
|
714
|
+
const img = getClipboardImage();
|
|
715
|
+
if (img) {
|
|
716
|
+
pendingImage = img;
|
|
717
|
+
const hint = `[图片 ${img.width}x${img.height}] `;
|
|
718
|
+
input += hint;
|
|
719
|
+
process.stdout.write(c('cyan', hint));
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Enter 键
|
|
725
|
+
if (char === '\r' || char === '\n') {
|
|
726
|
+
// 如果是快速输入(粘贴),把换行当作空格
|
|
727
|
+
if (timeDiff < 10 && input.length > 0) {
|
|
728
|
+
input += ' ';
|
|
729
|
+
process.stdout.write(' ');
|
|
730
|
+
} else {
|
|
731
|
+
// 正常回车,结束输入
|
|
732
|
+
stdin.removeListener('data', onData);
|
|
733
|
+
stdin.setRawMode(false);
|
|
734
|
+
console.log();
|
|
735
|
+
resolve(input);
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Ctrl+C
|
|
741
|
+
if (char === '\u0003') {
|
|
742
|
+
stdin.removeListener('data', onData);
|
|
743
|
+
stdin.setRawMode(false);
|
|
744
|
+
process.exit();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 退格
|
|
748
|
+
if (char === '\u007f' || char === '\b') {
|
|
749
|
+
if (input.length > 0) {
|
|
750
|
+
const lastChar = input.slice(-1);
|
|
751
|
+
input = input.slice(0, -1);
|
|
752
|
+
// 中文字符占2个宽度
|
|
753
|
+
const width = lastChar.charCodeAt(0) > 127 ? 2 : 1;
|
|
754
|
+
process.stdout.write('\b \b'.repeat(width));
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// 普通字符
|
|
760
|
+
if (char >= ' ' || char === '\t') {
|
|
761
|
+
input += char;
|
|
762
|
+
process.stdout.write(char);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
stdin.on('data', onData);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function cleanup() {
|
|
771
|
+
showCursor();
|
|
772
|
+
clearScreen();
|
|
773
|
+
const name = currentUser?.username || config.username;
|
|
774
|
+
console.log(`再见, ${name}!`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// 页面:角色管理
|
|
778
|
+
async function showCharacters() {
|
|
779
|
+
let characters = [];
|
|
780
|
+
|
|
781
|
+
const loadCharacters = async () => {
|
|
782
|
+
try {
|
|
783
|
+
characters = await getCharacters();
|
|
784
|
+
} catch (err) {
|
|
785
|
+
console.log(c('red', ` 错误: ${err.message}`));
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
await loadCharacters();
|
|
790
|
+
|
|
791
|
+
const menuItems = [
|
|
792
|
+
{ label: '选择角色', value: 'select' },
|
|
793
|
+
{ label: '创建角色', value: 'create' },
|
|
794
|
+
{ label: '返回主菜单', value: 'back' },
|
|
795
|
+
];
|
|
796
|
+
|
|
797
|
+
let selected = 0;
|
|
798
|
+
|
|
799
|
+
const draw = () => {
|
|
800
|
+
clearScreen();
|
|
801
|
+
console.log();
|
|
802
|
+
console.log(c('cyan', c('bold', ' [*] 角色管理')));
|
|
803
|
+
console.log();
|
|
804
|
+
|
|
805
|
+
if (characters.length === 0) {
|
|
806
|
+
console.log(c('gray', ' 暂无角色'));
|
|
807
|
+
} else {
|
|
808
|
+
characters.forEach((char, i) => {
|
|
809
|
+
const marker = currentCharacter?.id === char.id ? c('green', ' [当前]') : '';
|
|
810
|
+
const def = char.is_default ? c('yellow', ' [默认]') : '';
|
|
811
|
+
console.log(` ${i + 1}. ${char.avatar || '🎭'} ${char.name}${marker}${def}`);
|
|
812
|
+
if (char.description) {
|
|
813
|
+
console.log(c('gray', ` ${char.description}`));
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
console.log();
|
|
819
|
+
menuItems.forEach((item, i) => {
|
|
820
|
+
if (i === selected) {
|
|
821
|
+
console.log(c('cyan', ` > ${item.label}`));
|
|
822
|
+
} else {
|
|
823
|
+
console.log(` ${item.label}`);
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
console.log();
|
|
827
|
+
console.log(c('gray', ' 方向键选择,Enter 确认'));
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
draw();
|
|
831
|
+
|
|
832
|
+
while (true) {
|
|
833
|
+
const choice = await new Promise((resolve) => {
|
|
834
|
+
process.stdin.setRawMode(true);
|
|
835
|
+
process.stdin.resume();
|
|
836
|
+
|
|
837
|
+
const onKeyPress = (key) => {
|
|
838
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
839
|
+
if (key[2] === 65 && selected > 0) {
|
|
840
|
+
selected--;
|
|
841
|
+
draw();
|
|
842
|
+
} else if (key[2] === 66 && selected < menuItems.length - 1) {
|
|
843
|
+
selected++;
|
|
844
|
+
draw();
|
|
845
|
+
}
|
|
846
|
+
} else if (key[0] === 13) {
|
|
847
|
+
process.stdin.setRawMode(false);
|
|
848
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
849
|
+
resolve(menuItems[selected].value);
|
|
850
|
+
} else if (key[0] === 3) {
|
|
851
|
+
cleanup();
|
|
852
|
+
process.exit();
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
process.stdin.on('data', onKeyPress);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
if (choice === 'back') return;
|
|
860
|
+
|
|
861
|
+
if (choice === 'select') {
|
|
862
|
+
if (characters.length === 0) {
|
|
863
|
+
console.log(c('yellow', ' 没有可选择的角色'));
|
|
864
|
+
await sleep(1000);
|
|
865
|
+
draw();
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
clearScreen();
|
|
870
|
+
console.log();
|
|
871
|
+
console.log(c('cyan', ' 选择角色 (输入序号):'));
|
|
872
|
+
console.log();
|
|
873
|
+
characters.forEach((char, i) => {
|
|
874
|
+
console.log(` ${i + 1}. ${char.avatar || '🎭'} ${char.name}`);
|
|
875
|
+
});
|
|
876
|
+
console.log();
|
|
877
|
+
|
|
878
|
+
const num = await prompt(c('cyan', ' > '));
|
|
879
|
+
const idx = parseInt(num) - 1;
|
|
880
|
+
|
|
881
|
+
if (idx >= 0 && idx < characters.length) {
|
|
882
|
+
currentCharacter = characters[idx];
|
|
883
|
+
currentConversationId = null;
|
|
884
|
+
console.log(c('green', ` 已切换到: ${currentCharacter.name}`));
|
|
885
|
+
await sleep(1000);
|
|
886
|
+
}
|
|
887
|
+
draw();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (choice === 'create') {
|
|
891
|
+
clearScreen();
|
|
892
|
+
console.log();
|
|
893
|
+
console.log(c('cyan', c('bold', ' [*] 创建角色')));
|
|
894
|
+
console.log();
|
|
895
|
+
|
|
896
|
+
const name = await prompt(c('gray', ' 名称: '));
|
|
897
|
+
if (!name.trim()) {
|
|
898
|
+
console.log(c('red', ' 名称不能为空'));
|
|
899
|
+
await sleep(1000);
|
|
900
|
+
draw();
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const avatar = await prompt(c('gray', ' 头像 (emoji): '));
|
|
905
|
+
const description = await prompt(c('gray', ' 描述: '));
|
|
906
|
+
const system_prompt = await prompt(c('gray', ' 系统提示词: '));
|
|
907
|
+
|
|
908
|
+
console.log();
|
|
909
|
+
console.log(c('yellow', ' 创建中...'));
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const newChar = await createCharacter({
|
|
913
|
+
name: name.trim(),
|
|
914
|
+
avatar: avatar.trim() || '🎭',
|
|
915
|
+
description: description.trim(),
|
|
916
|
+
system_prompt: system_prompt.trim()
|
|
917
|
+
});
|
|
918
|
+
console.log(c('green', ` 角色 "${newChar.name}" 创建成功!`));
|
|
919
|
+
await loadCharacters();
|
|
920
|
+
await sleep(1500);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
console.log(c('red', ` 创建失败: ${err.message}`));
|
|
923
|
+
await sleep(1500);
|
|
924
|
+
}
|
|
925
|
+
draw();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// 页面:记忆查看
|
|
931
|
+
async function showMemories() {
|
|
932
|
+
if (!currentCharacter) {
|
|
933
|
+
clearScreen();
|
|
934
|
+
console.log();
|
|
935
|
+
console.log(c('yellow', ' 请先选择一个角色'));
|
|
936
|
+
console.log(c('gray', ' 按 Enter 返回'));
|
|
937
|
+
await waitForEnter();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let memories = [];
|
|
942
|
+
|
|
943
|
+
const loadMemories = async () => {
|
|
944
|
+
try {
|
|
945
|
+
memories = await getMemories(currentCharacter.id);
|
|
946
|
+
} catch (err) {
|
|
947
|
+
console.log(c('red', ` 错误: ${err.message}`));
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
await loadMemories();
|
|
952
|
+
|
|
953
|
+
let selected = 0;
|
|
954
|
+
|
|
955
|
+
const draw = () => {
|
|
956
|
+
clearScreen();
|
|
957
|
+
console.log();
|
|
958
|
+
console.log(c('cyan', c('bold', ` [*] ${currentCharacter.name} 的记忆`)));
|
|
959
|
+
console.log(c('gray', ' ↑↓选择 p置顶 d删除 q返回'));
|
|
960
|
+
console.log();
|
|
961
|
+
|
|
962
|
+
if (memories.length === 0) {
|
|
963
|
+
console.log(c('gray', ' 暂无记忆'));
|
|
964
|
+
} else {
|
|
965
|
+
memories.forEach((mem, i) => {
|
|
966
|
+
const cursor = i === selected ? c('cyan', '> ') : ' ';
|
|
967
|
+
const pinned = mem.pinned ? c('yellow', '📌') : ' ';
|
|
968
|
+
const importance = c('cyan', `[${mem.importance}]`);
|
|
969
|
+
const type = c('gray', `(${mem.type || 'fact'})`);
|
|
970
|
+
console.log(` ${cursor}${pinned} ${importance} ${mem.content.slice(0, 50)}${mem.content.length > 50 ? '...' : ''} ${type}`);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
draw();
|
|
976
|
+
|
|
977
|
+
return new Promise((resolve) => {
|
|
978
|
+
process.stdin.setRawMode(true);
|
|
979
|
+
process.stdin.resume();
|
|
980
|
+
|
|
981
|
+
const onKeyPress = async (key) => {
|
|
982
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
983
|
+
if (key[2] === 65 && selected > 0) {
|
|
984
|
+
selected--;
|
|
985
|
+
draw();
|
|
986
|
+
} else if (key[2] === 66 && selected < memories.length - 1) {
|
|
987
|
+
selected++;
|
|
988
|
+
draw();
|
|
989
|
+
}
|
|
990
|
+
} else if (key[0] === 113 || key[0] === 27) { // q 或 Esc
|
|
991
|
+
process.stdin.setRawMode(false);
|
|
992
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
993
|
+
resolve();
|
|
994
|
+
} else if (key[0] === 112 && memories.length > 0) { // p - 置顶
|
|
995
|
+
const mem = memories[selected];
|
|
996
|
+
try {
|
|
997
|
+
await pinMemory(mem.id, !mem.pinned);
|
|
998
|
+
await loadMemories();
|
|
999
|
+
draw();
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
console.log(c('red', ` 置顶失败: ${err.message}`));
|
|
1002
|
+
}
|
|
1003
|
+
} else if (key[0] === 100 && memories.length > 0) { // d - 删除
|
|
1004
|
+
const mem = memories[selected];
|
|
1005
|
+
try {
|
|
1006
|
+
await deleteMemory(mem.id);
|
|
1007
|
+
await loadMemories();
|
|
1008
|
+
if (selected >= memories.length) selected = Math.max(0, memories.length - 1);
|
|
1009
|
+
draw();
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
console.log(c('red', ` 删除失败: ${err.message}`));
|
|
1012
|
+
}
|
|
1013
|
+
} else if (key[0] === 3) {
|
|
1014
|
+
cleanup();
|
|
1015
|
+
process.exit();
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
process.stdin.on('data', onKeyPress);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// 主程序
|
|
1024
|
+
async function main() {
|
|
1025
|
+
process.on('SIGINT', () => {
|
|
1026
|
+
cleanup();
|
|
1027
|
+
process.exit();
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// 检查认证状态
|
|
1031
|
+
const isAuthenticated = await checkAuth();
|
|
1032
|
+
|
|
1033
|
+
if (isAuthenticated) {
|
|
1034
|
+
currentUser = getCurrentUser();
|
|
1035
|
+
} else {
|
|
1036
|
+
currentUser = await showAuthScreen();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 获取默认角色
|
|
1040
|
+
try {
|
|
1041
|
+
const characters = await getCharacters();
|
|
1042
|
+
currentCharacter = characters.find(c => c.is_default) || characters[0];
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
console.log(c('yellow', ' 无法获取角色列表,请确保服务器已启动'));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
await showWelcome();
|
|
1048
|
+
|
|
1049
|
+
while (true) {
|
|
1050
|
+
const choice = await showMenu();
|
|
1051
|
+
|
|
1052
|
+
switch (choice) {
|
|
1053
|
+
case 'chat':
|
|
1054
|
+
await showChat();
|
|
1055
|
+
break;
|
|
1056
|
+
case 'characters':
|
|
1057
|
+
await showCharacters();
|
|
1058
|
+
break;
|
|
1059
|
+
case 'memories':
|
|
1060
|
+
await showMemories();
|
|
1061
|
+
break;
|
|
1062
|
+
case 'settings':
|
|
1063
|
+
await showSettings();
|
|
1064
|
+
break;
|
|
1065
|
+
case 'logout':
|
|
1066
|
+
await logout();
|
|
1067
|
+
currentUser = await showAuthScreen();
|
|
1068
|
+
break;
|
|
1069
|
+
case 'exit':
|
|
1070
|
+
cleanup();
|
|
1071
|
+
process.exit(0);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
main().catch(console.error);
|