@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/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
|
+
}
|