@adversity/coding-tool-x 3.0.6 → 3.1.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/CHANGELOG.md +23 -0
- package/dist/web/assets/{icons-BxudHPiX.js → icons-CO_2OFES.js} +1 -1
- package/dist/web/assets/{index-D2VfwJBa.js → index-DI8QOi-E.js} +2 -2
- package/dist/web/assets/{index-oXBzu0bd.css → index-uLHGdeZh.css} +1 -1
- package/dist/web/assets/{naive-ui-DT-Uur8K.js → naive-ui-B1re3c-e.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/src/commands/daemon.js +11 -1
- package/src/commands/ui.js +8 -1
- package/src/index.js +3 -1
- package/src/server/api/oauth.js +294 -0
- package/src/server/codex-proxy-server.js +3 -2
- package/src/server/config/oauth-providers.js +68 -0
- package/src/server/gemini-proxy-server.js +3 -1
- package/src/server/index.js +12 -3
- package/src/server/proxy-server.js +4 -2
- package/src/server/services/channels.js +33 -2
- package/src/server/services/codex-channels.js +27 -2
- package/src/server/services/gemini-channels.js +34 -1
- package/src/server/services/oauth-callback-server.js +284 -0
- package/src/server/services/oauth-service.js +378 -0
- package/src/server/services/oauth-token-storage.js +135 -0
|
@@ -72,7 +72,8 @@ function loadChannels() {
|
|
|
72
72
|
weight: ch.weight || 1,
|
|
73
73
|
maxConcurrency: ch.maxConcurrency || null,
|
|
74
74
|
modelRedirects: ch.modelRedirects || [],
|
|
75
|
-
speedTestModel: ch.speedTestModel || null
|
|
75
|
+
speedTestModel: ch.speedTestModel || null,
|
|
76
|
+
authType: ch.authType || 'apiKey'
|
|
76
77
|
}));
|
|
77
78
|
}
|
|
78
79
|
return data;
|
|
@@ -118,6 +119,7 @@ function initializeFromEnv() {
|
|
|
118
119
|
enabled: true,
|
|
119
120
|
weight: 1,
|
|
120
121
|
maxConcurrency: null,
|
|
122
|
+
authType: 'apiKey',
|
|
121
123
|
createdAt: Date.now(),
|
|
122
124
|
updatedAt: Date.now()
|
|
123
125
|
};
|
|
@@ -175,6 +177,9 @@ function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraCon
|
|
|
175
177
|
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
176
178
|
modelRedirects: extraConfig.modelRedirects || [],
|
|
177
179
|
speedTestModel: extraConfig.speedTestModel || null,
|
|
180
|
+
authType: extraConfig.authType || 'apiKey',
|
|
181
|
+
oauthProvider: extraConfig.oauthProvider || null,
|
|
182
|
+
oauthTokenId: extraConfig.oauthTokenId || null,
|
|
178
183
|
createdAt: Date.now(),
|
|
179
184
|
updatedAt: Date.now()
|
|
180
185
|
};
|
|
@@ -392,6 +397,33 @@ function getEnabledChannels() {
|
|
|
392
397
|
return data.channels.filter(c => c.enabled !== false);
|
|
393
398
|
}
|
|
394
399
|
|
|
400
|
+
/**
|
|
401
|
+
* 获取渠道的有效 API Key
|
|
402
|
+
* 如果是 OAuth 认证,尝试从 token 存储获取 access token
|
|
403
|
+
* 否则返回渠道配置的 apiKey
|
|
404
|
+
*
|
|
405
|
+
* @param {Object} channel - 渠道对象
|
|
406
|
+
* @returns {string|null} 有效的 API key 或 access token,OAuth 令牌无效/过期时返回 null
|
|
407
|
+
*/
|
|
408
|
+
function getEffectiveApiKey(channel) {
|
|
409
|
+
if (channel.authType === 'oauth' && channel.oauthTokenId) {
|
|
410
|
+
try {
|
|
411
|
+
const { getToken, isTokenExpired } = require('./oauth-token-storage');
|
|
412
|
+
const token = getToken(channel.oauthTokenId);
|
|
413
|
+
if (token && !isTokenExpired(token)) {
|
|
414
|
+
return token.accessToken;
|
|
415
|
+
}
|
|
416
|
+
// OAuth 令牌无效或已过期,返回 null(调用方应处理刷新或报错)
|
|
417
|
+
console.warn(`[Gemini Channels] OAuth token expired or not found for channel ${channel.name}`);
|
|
418
|
+
return null;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error('[Gemini Channels] Failed to get OAuth token:', err.message);
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return channel.apiKey;
|
|
425
|
+
}
|
|
426
|
+
|
|
395
427
|
// 保存渠道顺序
|
|
396
428
|
function saveChannelOrder(order) {
|
|
397
429
|
const data = loadChannels();
|
|
@@ -422,6 +454,7 @@ module.exports = {
|
|
|
422
454
|
updateChannel,
|
|
423
455
|
deleteChannel,
|
|
424
456
|
getEnabledChannels,
|
|
457
|
+
getEffectiveApiKey,
|
|
425
458
|
saveChannelOrder,
|
|
426
459
|
isProxyConfig,
|
|
427
460
|
getGeminiDir,
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const url = require('url');
|
|
5
|
+
|
|
6
|
+
// Store active servers by port
|
|
7
|
+
const activeServers = new Map();
|
|
8
|
+
|
|
9
|
+
// Default timeout for auto-close (30 seconds)
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
11
|
+
|
|
12
|
+
// Delay before closing server after callback (1 second)
|
|
13
|
+
const CLOSE_DELAY_MS = 1000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate HTML response page
|
|
17
|
+
* @param {boolean} success - Whether authorization was successful
|
|
18
|
+
* @param {string} [errorMessage] - Error message if failed
|
|
19
|
+
* @returns {string} HTML content
|
|
20
|
+
*/
|
|
21
|
+
function generateHtmlResponse(success, errorMessage) {
|
|
22
|
+
const title = success ? 'Authorization Successful' : 'Authorization Failed';
|
|
23
|
+
const titleCn = success ? '授权成功' : '授权失败';
|
|
24
|
+
const message = success
|
|
25
|
+
? 'You can close this window now.'
|
|
26
|
+
: `Error: ${errorMessage || 'Unknown error'}`;
|
|
27
|
+
const messageCn = success
|
|
28
|
+
? '您现在可以关闭此窗口。'
|
|
29
|
+
: `错误: ${errorMessage || '未知错误'}`;
|
|
30
|
+
const icon = success ? '✔' : '✖';
|
|
31
|
+
const color = success ? '#4CAF50' : '#f44336';
|
|
32
|
+
|
|
33
|
+
return `<!DOCTYPE html>
|
|
34
|
+
<html lang="en">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="UTF-8">
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
38
|
+
<title>${title}</title>
|
|
39
|
+
<style>
|
|
40
|
+
* {
|
|
41
|
+
margin: 0;
|
|
42
|
+
padding: 0;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
}
|
|
45
|
+
body {
|
|
46
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
47
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
48
|
+
min-height: 100vh;
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
align-items: center;
|
|
52
|
+
padding: 20px;
|
|
53
|
+
}
|
|
54
|
+
.container {
|
|
55
|
+
background: white;
|
|
56
|
+
border-radius: 16px;
|
|
57
|
+
padding: 48px;
|
|
58
|
+
text-align: center;
|
|
59
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
60
|
+
max-width: 400px;
|
|
61
|
+
width: 100%;
|
|
62
|
+
}
|
|
63
|
+
.icon {
|
|
64
|
+
width: 80px;
|
|
65
|
+
height: 80px;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
background: ${color};
|
|
68
|
+
color: white;
|
|
69
|
+
font-size: 40px;
|
|
70
|
+
line-height: 80px;
|
|
71
|
+
margin: 0 auto 24px;
|
|
72
|
+
}
|
|
73
|
+
h1 {
|
|
74
|
+
color: #333;
|
|
75
|
+
font-size: 24px;
|
|
76
|
+
margin-bottom: 8px;
|
|
77
|
+
}
|
|
78
|
+
.subtitle {
|
|
79
|
+
color: #666;
|
|
80
|
+
font-size: 14px;
|
|
81
|
+
margin-bottom: 16px;
|
|
82
|
+
}
|
|
83
|
+
p {
|
|
84
|
+
color: #555;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
line-height: 1.5;
|
|
87
|
+
}
|
|
88
|
+
.error-detail {
|
|
89
|
+
background: #fff3f3;
|
|
90
|
+
border: 1px solid #ffcdd2;
|
|
91
|
+
border-radius: 8px;
|
|
92
|
+
padding: 12px;
|
|
93
|
+
margin-top: 16px;
|
|
94
|
+
color: #c62828;
|
|
95
|
+
font-size: 14px;
|
|
96
|
+
word-break: break-word;
|
|
97
|
+
}
|
|
98
|
+
.auto-close {
|
|
99
|
+
margin-top: 24px;
|
|
100
|
+
color: #999;
|
|
101
|
+
font-size: 12px;
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<div class="container">
|
|
107
|
+
<div class="icon">${icon}</div>
|
|
108
|
+
<h1>${title}</h1>
|
|
109
|
+
<p class="subtitle">${titleCn}</p>
|
|
110
|
+
<p>${message}</p>
|
|
111
|
+
<p style="color: #888; font-size: 14px; margin-top: 8px;">${messageCn}</p>
|
|
112
|
+
${!success && errorMessage ? `<div class="error-detail">${errorMessage}</div>` : ''}
|
|
113
|
+
<p class="auto-close">This window will close automatically...</p>
|
|
114
|
+
</div>
|
|
115
|
+
<script>
|
|
116
|
+
setTimeout(function() { window.close(); }, 3000);
|
|
117
|
+
</script>
|
|
118
|
+
</body>
|
|
119
|
+
</html>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start OAuth callback server
|
|
124
|
+
* @param {number} port - Port to listen on
|
|
125
|
+
* @param {string} callbackPath - Path to handle callbacks (e.g., '/callback')
|
|
126
|
+
* @param {Function} onCallback - Callback function with { code, state, error, errorDescription }
|
|
127
|
+
* @returns {Promise<http.Server>} The HTTP server instance
|
|
128
|
+
*/
|
|
129
|
+
async function startCallbackServer(port, callbackPath, onCallback) {
|
|
130
|
+
// Check if server is already running on this port
|
|
131
|
+
if (activeServers.has(port)) {
|
|
132
|
+
throw new Error(`OAuth callback server already running on port ${port}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Normalize callback path
|
|
136
|
+
const normalizedPath = callbackPath.startsWith('/') ? callbackPath : `/${callbackPath}`;
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const server = http.createServer((req, res) => {
|
|
140
|
+
const parsedUrl = url.parse(req.url, true);
|
|
141
|
+
const pathname = parsedUrl.pathname;
|
|
142
|
+
|
|
143
|
+
// Only handle the callback path
|
|
144
|
+
if (pathname !== normalizedPath) {
|
|
145
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
146
|
+
res.end('Not Found');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Parse query parameters
|
|
151
|
+
const query = parsedUrl.query;
|
|
152
|
+
const code = query.code || null;
|
|
153
|
+
const state = query.state || null;
|
|
154
|
+
const error = query.error || null;
|
|
155
|
+
const errorDescription = query.error_description || null;
|
|
156
|
+
|
|
157
|
+
// Determine success/failure
|
|
158
|
+
const success = !error && code;
|
|
159
|
+
const errorMessage = error
|
|
160
|
+
? `${error}${errorDescription ? `: ${errorDescription}` : ''}`
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
// Send HTML response
|
|
164
|
+
res.writeHead(success ? 200 : 400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
165
|
+
res.end(generateHtmlResponse(success, errorMessage));
|
|
166
|
+
|
|
167
|
+
// Call the callback
|
|
168
|
+
try {
|
|
169
|
+
onCallback({
|
|
170
|
+
code,
|
|
171
|
+
state,
|
|
172
|
+
error,
|
|
173
|
+
errorDescription
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('Error in OAuth callback handler:', err);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Auto-close server after delay
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
stopCallbackServer(port);
|
|
182
|
+
}, CLOSE_DELAY_MS);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Set up timeout for auto-close
|
|
186
|
+
const timeoutId = setTimeout(() => {
|
|
187
|
+
if (activeServers.has(port)) {
|
|
188
|
+
console.log(`OAuth callback server on port ${port} timed out, closing...`);
|
|
189
|
+
stopCallbackServer(port);
|
|
190
|
+
}
|
|
191
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
192
|
+
|
|
193
|
+
// Handle server errors
|
|
194
|
+
server.on('error', (err) => {
|
|
195
|
+
clearTimeout(timeoutId);
|
|
196
|
+
activeServers.delete(port);
|
|
197
|
+
|
|
198
|
+
if (err.code === 'EADDRINUSE') {
|
|
199
|
+
reject(new Error(`Port ${port} is already in use. Please try a different port or close the application using it.`));
|
|
200
|
+
} else if (err.code === 'EACCES') {
|
|
201
|
+
reject(new Error(`Permission denied to bind to port ${port}. Try a port number above 1024.`));
|
|
202
|
+
} else {
|
|
203
|
+
reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Start listening
|
|
208
|
+
server.listen(port, '127.0.0.1', () => {
|
|
209
|
+
// Store server info
|
|
210
|
+
activeServers.set(port, {
|
|
211
|
+
server,
|
|
212
|
+
timeoutId,
|
|
213
|
+
callbackPath: normalizedPath
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
console.log(`OAuth callback server started on http://127.0.0.1:${port}${normalizedPath}`);
|
|
217
|
+
resolve(server);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Stop OAuth callback server
|
|
224
|
+
* @param {number} port - Port of the server to stop
|
|
225
|
+
* @returns {boolean} True if server was stopped, false if not found
|
|
226
|
+
*/
|
|
227
|
+
function stopCallbackServer(port) {
|
|
228
|
+
const serverInfo = activeServers.get(port);
|
|
229
|
+
|
|
230
|
+
if (!serverInfo) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Clear timeout
|
|
235
|
+
if (serverInfo.timeoutId) {
|
|
236
|
+
clearTimeout(serverInfo.timeoutId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Close server
|
|
240
|
+
try {
|
|
241
|
+
serverInfo.server.close();
|
|
242
|
+
console.log(`OAuth callback server on port ${port} stopped`);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(`Error stopping OAuth callback server on port ${port}:`, err.message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
activeServers.delete(port);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if server is running on port
|
|
253
|
+
* @param {number} port - Port to check
|
|
254
|
+
* @returns {boolean} True if server is running
|
|
255
|
+
*/
|
|
256
|
+
function isServerRunning(port) {
|
|
257
|
+
return activeServers.has(port);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get all active server ports
|
|
262
|
+
* @returns {number[]} Array of active ports
|
|
263
|
+
*/
|
|
264
|
+
function getActiveServers() {
|
|
265
|
+
return Array.from(activeServers.keys());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Stop all active servers
|
|
270
|
+
* @returns {number} Number of servers stopped
|
|
271
|
+
*/
|
|
272
|
+
function stopAllServers() {
|
|
273
|
+
const ports = getActiveServers();
|
|
274
|
+
ports.forEach(port => stopCallbackServer(port));
|
|
275
|
+
return ports.length;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
startCallbackServer,
|
|
280
|
+
stopCallbackServer,
|
|
281
|
+
isServerRunning,
|
|
282
|
+
getActiveServers,
|
|
283
|
+
stopAllServers
|
|
284
|
+
};
|