@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 ADDED
@@ -0,0 +1,262 @@
1
+ import { loadAuth, saveAuth, clearAuth, loadConfig } from './config.js';
2
+
3
+ // Backend URL (优先级: 环境变量 > 配置文件 > 默认值)
4
+ const config = loadConfig();
5
+ const BASE_URL = process.env.SOUL_API_URL || config.apiUrl;
6
+
7
+ // HTTP request with auth
8
+ async function request(path, options = {}) {
9
+ const auth = loadAuth();
10
+ const headers = {
11
+ 'Content-Type': 'application/json',
12
+ ...options.headers
13
+ };
14
+
15
+ if (auth?.accessToken) {
16
+ headers['Authorization'] = `Bearer ${auth.accessToken}`;
17
+ }
18
+
19
+ const response = await fetch(`${BASE_URL}${path}`, {
20
+ ...options,
21
+ headers
22
+ });
23
+
24
+ // Handle 401 - try refresh token
25
+ if (response.status === 401 && auth?.refreshToken) {
26
+ const refreshed = await refreshToken();
27
+ if (refreshed) {
28
+ // Retry with new token
29
+ const newAuth = loadAuth();
30
+ headers['Authorization'] = `Bearer ${newAuth.accessToken}`;
31
+ return fetch(`${BASE_URL}${path}`, { ...options, headers });
32
+ }
33
+ }
34
+
35
+ return response;
36
+ }
37
+
38
+ // Refresh access token
39
+ async function refreshToken() {
40
+ const auth = loadAuth();
41
+ if (!auth?.refreshToken) return false;
42
+
43
+ try {
44
+ const response = await fetch(`${BASE_URL}/api/auth/refresh`, {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ refreshToken: auth.refreshToken })
48
+ });
49
+
50
+ if (response.ok) {
51
+ const data = await response.json();
52
+ saveAuth({
53
+ ...auth,
54
+ accessToken: data.accessToken
55
+ });
56
+ return true;
57
+ }
58
+ } catch (err) {
59
+ console.error('Refresh token failed:', err.message);
60
+ }
61
+
62
+ clearAuth();
63
+ return false;
64
+ }
65
+
66
+ // Auth APIs
67
+ export async function sendVerificationCode(email, type = 'register') {
68
+ const response = await fetch(`${BASE_URL}/api/auth/send-code`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ email, type })
72
+ });
73
+
74
+ const data = await response.json();
75
+ if (!response.ok) {
76
+ throw new Error(data.error || '发送验证码失败');
77
+ }
78
+
79
+ return data;
80
+ }
81
+
82
+ export async function register(username, email, password, code) {
83
+ const response = await fetch(`${BASE_URL}/api/auth/register`, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({ username, email, password, code })
87
+ });
88
+
89
+ const data = await response.json();
90
+ if (!response.ok) {
91
+ throw new Error(data.error || 'Registration failed');
92
+ }
93
+
94
+ saveAuth({
95
+ accessToken: data.accessToken,
96
+ refreshToken: data.refreshToken,
97
+ user: data.user
98
+ });
99
+
100
+ return data;
101
+ }
102
+
103
+ export async function login(email, password) {
104
+ const response = await fetch(`${BASE_URL}/api/auth/login`, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({ email, password })
108
+ });
109
+
110
+ const data = await response.json();
111
+ if (!response.ok) {
112
+ throw new Error(data.error || 'Login failed');
113
+ }
114
+
115
+ saveAuth({
116
+ accessToken: data.accessToken,
117
+ refreshToken: data.refreshToken,
118
+ user: data.user
119
+ });
120
+
121
+ return data;
122
+ }
123
+
124
+ export async function logout() {
125
+ try {
126
+ await request('/api/auth/logout', { method: 'POST' });
127
+ } catch (err) {
128
+ // Ignore errors
129
+ }
130
+ clearAuth();
131
+ }
132
+
133
+ export async function updateProfile(username) {
134
+ const response = await request('/api/auth/profile', {
135
+ method: 'PUT',
136
+ body: JSON.stringify({ username })
137
+ });
138
+
139
+ const data = await response.json();
140
+ if (!response.ok) throw new Error(data.error);
141
+
142
+ // Update local auth with new tokens
143
+ const auth = loadAuth();
144
+ saveAuth({
145
+ ...auth,
146
+ accessToken: data.accessToken,
147
+ refreshToken: data.refreshToken,
148
+ user: data.user
149
+ });
150
+
151
+ return data;
152
+ }
153
+
154
+ export async function verifyToken() {
155
+ const auth = loadAuth();
156
+ if (!auth?.accessToken) return false;
157
+
158
+ try {
159
+ const response = await request('/api/auth/verify');
160
+ return response.ok;
161
+ } catch (err) {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ // Character APIs
167
+ export async function getCharacters() {
168
+ const response = await request('/api/characters');
169
+ const data = await response.json();
170
+ if (!response.ok) throw new Error(data.error);
171
+ return data.characters;
172
+ }
173
+
174
+ export async function createCharacter(character) {
175
+ const response = await request('/api/characters', {
176
+ method: 'POST',
177
+ body: JSON.stringify(character)
178
+ });
179
+ const data = await response.json();
180
+ if (!response.ok) throw new Error(data.error);
181
+ return data.character;
182
+ }
183
+
184
+ // Chat APIs
185
+ export async function sendMessage(message, characterId, conversationId, image = null) {
186
+ const response = await request('/api/chat', {
187
+ method: 'POST',
188
+ body: JSON.stringify({ message, characterId, conversationId, image })
189
+ });
190
+
191
+ if (!response.ok) {
192
+ const data = await response.json();
193
+ throw new Error(data.error);
194
+ }
195
+
196
+ return response;
197
+ }
198
+
199
+ export async function getConversations() {
200
+ const response = await request('/api/chat/conversations');
201
+ const data = await response.json();
202
+ if (!response.ok) throw new Error(data.error);
203
+ return data.conversations;
204
+ }
205
+
206
+ export async function getMessages(conversationId) {
207
+ const response = await request(`/api/chat/conversations/${conversationId}/messages`);
208
+ const data = await response.json();
209
+ if (!response.ok) throw new Error(data.error);
210
+ return data.messages;
211
+ }
212
+
213
+ export async function clearConversation(conversationId) {
214
+ const response = await request(`/api/chat/conversations/${conversationId}`, {
215
+ method: 'DELETE'
216
+ });
217
+ const data = await response.json();
218
+ if (!response.ok) throw new Error(data.error);
219
+ return data;
220
+ }
221
+
222
+ // Memory APIs
223
+ export async function getMemories(characterId) {
224
+ const response = await request(`/api/memories/${characterId}`);
225
+ const data = await response.json();
226
+ if (!response.ok) throw new Error(data.error);
227
+ return data.memories;
228
+ }
229
+
230
+ export async function searchMemories(characterId, query) {
231
+ const response = await request(`/api/memories/${characterId}/search?q=${encodeURIComponent(query)}`);
232
+ const data = await response.json();
233
+ if (!response.ok) throw new Error(data.error);
234
+ return data.memories;
235
+ }
236
+
237
+ export async function pinMemory(memoryId, pinned = true) {
238
+ const response = await request(`/api/memories/${memoryId}/pin`, {
239
+ method: 'POST',
240
+ body: JSON.stringify({ pinned })
241
+ });
242
+ return response.ok;
243
+ }
244
+
245
+ export async function deleteMemory(memoryId) {
246
+ const response = await request(`/api/memories/${memoryId}`, {
247
+ method: 'DELETE'
248
+ });
249
+ return response.ok;
250
+ }
251
+
252
+ // Check if logged in
253
+ export function isLoggedIn() {
254
+ const auth = loadAuth();
255
+ return !!auth?.accessToken;
256
+ }
257
+
258
+ // Get current user
259
+ export function getCurrentUser() {
260
+ const auth = loadAuth();
261
+ return auth?.user || null;
262
+ }
package/bin/auth.js ADDED
@@ -0,0 +1,363 @@
1
+ import readline from 'readline';
2
+ import http from 'http';
3
+ import crypto from 'crypto';
4
+ import { login, verifyToken, isLoggedIn } from './api.js';
5
+ import { saveAuth, loadConfig } from './config.js';
6
+
7
+ // ANSI colors
8
+ const colors = {
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ red: '\x1b[31m',
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ cyan: '\x1b[36m',
15
+ gray: '\x1b[90m',
16
+ };
17
+
18
+ const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
19
+
20
+ const clearScreen = () => process.stdout.write('\x1b[2J\x1b[H');
21
+
22
+ function prompt(query) {
23
+ const rl = readline.createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout
26
+ });
27
+
28
+ return new Promise((resolve) => {
29
+ rl.question(query, (answer) => {
30
+ rl.close();
31
+ resolve(answer);
32
+ });
33
+ });
34
+ }
35
+
36
+ function promptPassword(query) {
37
+ return new Promise((resolve) => {
38
+ const stdin = process.stdin;
39
+
40
+ process.stdout.write(query);
41
+
42
+ let password = '';
43
+ stdin.setRawMode(true);
44
+ stdin.resume();
45
+
46
+ const onData = (chunk) => {
47
+ const char = chunk.toString();
48
+ if (char === '\n' || char === '\r' || char === '\u0004') {
49
+ stdin.removeListener('data', onData);
50
+ stdin.setRawMode(false);
51
+ console.log();
52
+ resolve(password);
53
+ } else if (char === '\u0003') {
54
+ process.exit();
55
+ } else if (char === '\u007F' || char === '\b') {
56
+ if (password.length > 0) {
57
+ password = password.slice(0, -1);
58
+ process.stdout.write('\b \b');
59
+ }
60
+ } else if (char.charCodeAt(0) >= 32) {
61
+ password += char;
62
+ process.stdout.write('*');
63
+ }
64
+ };
65
+
66
+ stdin.on('data', onData);
67
+ });
68
+ }
69
+
70
+ // Check if already logged in
71
+ export async function checkAuth() {
72
+ if (!isLoggedIn()) {
73
+ return false;
74
+ }
75
+
76
+ // Verify token is still valid
77
+ const valid = await verifyToken();
78
+ return valid;
79
+ }
80
+
81
+ // 认证界面 - 登录或注册
82
+ export async function showAuthScreen() {
83
+ clearScreen();
84
+ console.log();
85
+ console.log(c('cyan', c('bold', ' +-----------------------------+')));
86
+ console.log(c('cyan', c('bold', ' | Soul Chat 登录 |')));
87
+ console.log(c('cyan', c('bold', ' +-----------------------------+')));
88
+ console.log();
89
+
90
+ const menuItems = [
91
+ { label: '[1] 浏览器登录 (推荐)', value: 'browser' },
92
+ { label: '[2] 邮箱密码登录', value: 'login' },
93
+ { label: '[3] 注册新账号', value: 'register' },
94
+ { label: '[4] 退出', value: 'exit' },
95
+ ];
96
+
97
+ let selected = 0;
98
+
99
+ const draw = () => {
100
+ process.stdout.write('\x1b[6;0H'); // Move to line 6
101
+ menuItems.forEach((item, i) => {
102
+ if (i === selected) {
103
+ console.log(c('cyan', ` > ${item.label} `));
104
+ } else {
105
+ console.log(` ${item.label} `);
106
+ }
107
+ });
108
+ console.log();
109
+ console.log(c('gray', ' 方向键选择,Enter 确认'));
110
+ console.log(c('yellow', ' 门户网站: https://soul-chat.jdctools.com.cn'));
111
+ };
112
+
113
+ draw();
114
+
115
+ const choice = await new Promise((resolve) => {
116
+ process.stdin.setRawMode(true);
117
+ process.stdin.resume();
118
+
119
+ const onKeyPress = (key) => {
120
+ if (key[0] === 27 && key[1] === 91) {
121
+ if (key[2] === 65 && selected > 0) {
122
+ selected--;
123
+ draw();
124
+ } else if (key[2] === 66 && selected < menuItems.length - 1) {
125
+ selected++;
126
+ draw();
127
+ }
128
+ } else if (key[0] === 13) {
129
+ process.stdin.setRawMode(false);
130
+ process.stdin.removeListener('data', onKeyPress);
131
+ resolve(menuItems[selected].value);
132
+ } else if (key[0] === 3) {
133
+ process.exit();
134
+ }
135
+ };
136
+
137
+ process.stdin.on('data', onKeyPress);
138
+ });
139
+
140
+ if (choice === 'exit') {
141
+ console.log();
142
+ console.log(c('gray', ' 再见!'));
143
+ process.exit(0);
144
+ }
145
+
146
+ if (choice === 'browser') {
147
+ return await browserAuth();
148
+ } else if (choice === 'login') {
149
+ return await showLoginForm();
150
+ } else {
151
+ return await showRegisterForm();
152
+ }
153
+ }
154
+
155
+ // 浏览器认证
156
+ async function browserAuth() {
157
+ clearScreen();
158
+ console.log();
159
+ console.log(c('cyan', c('bold', ' [*] 浏览器登录')));
160
+ console.log();
161
+
162
+ // 生成随机 state
163
+ const state = crypto.randomUUID();
164
+
165
+ // 查找可用端口
166
+ const port = await findAvailablePort(9876);
167
+ if (!port) {
168
+ console.log(c('red', ' 无法启动本地服务器,请使用邮箱密码登录'));
169
+ await sleep(2000);
170
+ return await showAuthScreen();
171
+ }
172
+
173
+ console.log(c('gray', ` 正在启动本地认证服务器 (端口 ${port})...`));
174
+
175
+ // 创建 Promise 来等待认证结果
176
+ const authResult = await new Promise((resolve) => {
177
+ let resolved = false;
178
+
179
+ // 创建本地 HTTP 服务器
180
+ const server = http.createServer((req, res) => {
181
+ const url = new URL(req.url, `http://localhost:${port}`);
182
+
183
+ if (url.pathname === '/callback') {
184
+ const receivedState = url.searchParams.get('state');
185
+ const accessToken = url.searchParams.get('accessToken');
186
+ const refreshToken = url.searchParams.get('refreshToken');
187
+ const userJson = url.searchParams.get('user');
188
+
189
+ // 验证 state
190
+ if (receivedState !== state) {
191
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
192
+ res.end('<html><body style="font-family:sans-serif;text-align:center;padding:50px;"><h1>认证失败</h1><p>无效的 state 参数</p></body></html>');
193
+ return;
194
+ }
195
+
196
+ if (!accessToken || !refreshToken || !userJson) {
197
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
198
+ res.end('<html><body style="font-family:sans-serif;text-align:center;padding:50px;"><h1>认证失败</h1><p>缺少必要参数</p></body></html>');
199
+ return;
200
+ }
201
+
202
+ try {
203
+ const user = JSON.parse(userJson);
204
+
205
+ // 保存认证信息
206
+ saveAuth({ accessToken, refreshToken, user });
207
+
208
+ // 返回成功页面
209
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
210
+ res.end(`
211
+ <html>
212
+ <body style="font-family:sans-serif;text-align:center;padding:50px;background:#1a1a2e;color:#fff;">
213
+ <div style="max-width:400px;margin:0 auto;">
214
+ <div style="font-size:60px;margin-bottom:20px;">✓</div>
215
+ <h1 style="color:#f5c842;">认证成功!</h1>
216
+ <p style="color:#888;">欢迎回来, ${user.username}</p>
217
+ <p style="color:#666;margin-top:30px;">你可以关闭此页面,返回 CLI 继续使用</p>
218
+ </div>
219
+ </body>
220
+ </html>
221
+ `);
222
+
223
+ resolved = true;
224
+ resolve({ success: true, user });
225
+
226
+ // 延迟关闭服务器
227
+ setTimeout(() => server.close(), 500);
228
+ } catch (err) {
229
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
230
+ res.end('<html><body style="font-family:sans-serif;text-align:center;padding:50px;"><h1>认证失败</h1><p>解析用户信息失败</p></body></html>');
231
+ }
232
+ } else {
233
+ res.writeHead(404);
234
+ res.end('Not Found');
235
+ }
236
+ });
237
+
238
+ server.listen(port, '127.0.0.1', () => {
239
+ // 构建认证 URL
240
+ const config = loadConfig();
241
+ const baseUrl = config.apiUrl || 'https://soul-chat.jdctools.com.cn';
242
+ const authUrl = `${baseUrl}/cli-auth?port=${port}&state=${state}`;
243
+
244
+ console.log(c('yellow', ' 正在打开浏览器...'));
245
+ console.log();
246
+ console.log(c('gray', ` 如果浏览器没有自动打开,请手动访问:`));
247
+ console.log(c('cyan', ` ${authUrl}`));
248
+ console.log();
249
+ console.log(c('gray', ' 等待浏览器认证... (按 Ctrl+C 取消)'));
250
+
251
+ // 打开浏览器
252
+ import('child_process').then(({ exec }) => {
253
+ const cmd = process.platform === 'darwin' ? 'open' :
254
+ process.platform === 'win32' ? 'start' : 'xdg-open';
255
+ exec(`${cmd} "${authUrl}"`);
256
+ });
257
+ });
258
+
259
+ // 超时处理 (2 分钟)
260
+ setTimeout(() => {
261
+ if (!resolved) {
262
+ server.close();
263
+ resolve({ success: false, error: '认证超时' });
264
+ }
265
+ }, 120000);
266
+
267
+ // 处理服务器错误
268
+ server.on('error', (err) => {
269
+ if (!resolved) {
270
+ resolve({ success: false, error: err.message });
271
+ }
272
+ });
273
+ });
274
+
275
+ console.log();
276
+
277
+ if (authResult.success) {
278
+ console.log(c('green', ` 欢迎回来, ${authResult.user.username}!`));
279
+ await sleep(1000);
280
+ return authResult.user;
281
+ } else {
282
+ console.log(c('red', ` 认证失败: ${authResult.error}`));
283
+ await sleep(2000);
284
+ return await showAuthScreen();
285
+ }
286
+ }
287
+
288
+ // 查找可用端口
289
+ async function findAvailablePort(startPort) {
290
+ for (let port = startPort; port < startPort + 10; port++) {
291
+ const available = await checkPort(port);
292
+ if (available) return port;
293
+ }
294
+ return null;
295
+ }
296
+
297
+ function checkPort(port) {
298
+ return new Promise((resolve) => {
299
+ const server = http.createServer();
300
+ server.listen(port, '127.0.0.1');
301
+ server.on('listening', () => {
302
+ server.close();
303
+ resolve(true);
304
+ });
305
+ server.on('error', () => {
306
+ resolve(false);
307
+ });
308
+ });
309
+ }
310
+
311
+ // 登录表单
312
+ async function showLoginForm() {
313
+ clearScreen();
314
+ console.log();
315
+ console.log(c('cyan', c('bold', ' [*] 邮箱密码登录')));
316
+ console.log();
317
+
318
+ const email = await prompt(c('gray', ' 邮箱: '));
319
+ const password = await promptPassword(c('gray', ' 密码: '));
320
+
321
+ if (!email || !password) {
322
+ console.log();
323
+ console.log(c('red', ' 邮箱和密码不能为空'));
324
+ await sleep(1500);
325
+ return await showAuthScreen();
326
+ }
327
+
328
+ console.log();
329
+ console.log(c('yellow', ' 登录中...'));
330
+
331
+ try {
332
+ const result = await login(email, password);
333
+ console.log(c('green', ` 欢迎回来, ${result.user.username}!`));
334
+ await sleep(1000);
335
+ return result.user;
336
+ } catch (err) {
337
+ console.log(c('red', ` 错误: ${err.message}`));
338
+ await sleep(1500);
339
+ return await showAuthScreen();
340
+ }
341
+ }
342
+
343
+ // 注册 - 打开浏览器
344
+ async function showRegisterForm() {
345
+ const config = loadConfig();
346
+ const url = config.apiUrl || 'https://soul-chat.jdctools.com.cn';
347
+ console.log();
348
+ console.log(c('yellow', ` 正在打开浏览器: ${url}/register`));
349
+
350
+ // 根据系统打开浏览器
351
+ const { exec } = await import('child_process');
352
+ const cmd = process.platform === 'darwin' ? 'open' :
353
+ process.platform === 'win32' ? 'start' : 'xdg-open';
354
+ exec(`${cmd} ${url}/register`);
355
+
356
+ console.log(c('green', ' 请在浏览器中完成注册,然后返回此处登录'));
357
+ await sleep(2000);
358
+ return await showAuthScreen();
359
+ }
360
+
361
+ function sleep(ms) {
362
+ return new Promise(resolve => setTimeout(resolve, ms));
363
+ }