@adversity/coding-tool-x 3.0.5 → 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 +42 -0
- package/dist/web/assets/{icons-BlzwYoRU.js → icons-CO_2OFES.js} +1 -1
- package/dist/web/assets/index-DI8QOi-E.js +14 -0
- package/dist/web/assets/index-uLHGdeZh.css +41 -0
- package/dist/web/assets/{naive-ui-B1TP-0TP.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/channels.js +3 -0
- package/src/server/api/codex-channels.js +40 -0
- package/src/server/api/config-registry.js +341 -0
- package/src/server/api/gemini-channels.js +40 -0
- package/src/server/api/oauth.js +294 -0
- package/src/server/api/permissions.js +30 -15
- package/src/server/codex-proxy-server.js +30 -4
- package/src/server/config/oauth-providers.js +68 -0
- package/src/server/gemini-proxy-server.js +64 -2
- package/src/server/index.js +15 -3
- package/src/server/proxy-server.js +31 -4
- package/src/server/services/channels.js +33 -2
- package/src/server/services/codex-channels.js +35 -4
- package/src/server/services/config-registry-service.js +762 -0
- package/src/server/services/config-sync-manager.js +456 -0
- package/src/server/services/config-templates-service.js +38 -3
- package/src/server/services/gemini-channels.js +40 -1
- package/src/server/services/model-detector.js +116 -23
- 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
- package/src/server/services/permission-templates-service.js +0 -31
- package/dist/web/assets/index-19ZPjh5b.css +0 -41
- package/dist/web/assets/index-B4w1yh7H.js +0 -14
|
@@ -9,21 +9,37 @@ const os = require('os');
|
|
|
9
9
|
const https = require('https');
|
|
10
10
|
const http = require('http');
|
|
11
11
|
const { URL } = require('url');
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
|
|
13
14
|
// Model priority by channel type
|
|
14
15
|
const MODEL_PRIORITY = {
|
|
15
16
|
claude: [
|
|
16
|
-
'claude-opus-4-5-
|
|
17
|
+
'claude-opus-4-5-20251101',
|
|
17
18
|
'claude-sonnet-4-5-20250929',
|
|
18
|
-
'claude-haiku-4-5-
|
|
19
|
+
'claude-haiku-4-5-20251001',
|
|
19
20
|
'claude-sonnet-4-20250514',
|
|
20
|
-
'claude-opus-4-20250514'
|
|
21
|
-
'claude-haiku-3-5-20241022',
|
|
22
|
-
'claude-3-5-haiku-20241022'
|
|
21
|
+
'claude-opus-4-20250514'
|
|
23
22
|
],
|
|
24
|
-
codex: [
|
|
25
|
-
|
|
23
|
+
codex: [
|
|
24
|
+
'gpt-5.2-codex',
|
|
25
|
+
'gpt-5.1-codex-max',
|
|
26
|
+
'gpt-5.1-codex-mini',
|
|
27
|
+
'gpt-5.1-codex',
|
|
28
|
+
'gpt-5-codex',
|
|
29
|
+
'gpt-5.2',
|
|
30
|
+
'gpt-5.1',
|
|
31
|
+
'gpt-5'
|
|
32
|
+
],
|
|
33
|
+
gemini: [
|
|
34
|
+
'gemini-3-pro',
|
|
35
|
+
'gemini-3-flash',
|
|
36
|
+
'gemini-3-deep-think',
|
|
37
|
+
'gemini-2.5-pro',
|
|
38
|
+
'gemini-2.5-flash'
|
|
39
|
+
]
|
|
26
40
|
};
|
|
41
|
+
// openai_compatible 复用 codex 的模型列表
|
|
42
|
+
MODEL_PRIORITY.openai_compatible = MODEL_PRIORITY.codex;
|
|
27
43
|
|
|
28
44
|
const PROVIDER_CAPABILITIES = {
|
|
29
45
|
claude: {
|
|
@@ -132,6 +148,63 @@ const MODEL_ALIASES = {
|
|
|
132
148
|
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
133
149
|
const TEST_TIMEOUT_MS = 10000; // 10 seconds per model test
|
|
134
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Generate realistic User-Agent strings that mimic official SDKs
|
|
153
|
+
* @param {string} channelType - 'claude' | 'codex' | 'gemini' | 'openai_compatible'
|
|
154
|
+
* @returns {string} - User-Agent string
|
|
155
|
+
*/
|
|
156
|
+
function getRealisticUserAgent(channelType) {
|
|
157
|
+
const nodeVersion = process.version.slice(1); // e.g., "18.17.0"
|
|
158
|
+
const platform = process.platform; // e.g., "darwin", "linux", "win32"
|
|
159
|
+
|
|
160
|
+
switch (channelType) {
|
|
161
|
+
case 'claude':
|
|
162
|
+
// Mimics official Anthropic Python SDK
|
|
163
|
+
return `anthropic-sdk-python/0.39.0 python/3.11.4 ${platform}`;
|
|
164
|
+
case 'gemini':
|
|
165
|
+
// Mimics official Google SDK
|
|
166
|
+
return `google-generativeai/0.8.2 python/3.11.4 ${platform}`;
|
|
167
|
+
case 'codex':
|
|
168
|
+
case 'openai_compatible':
|
|
169
|
+
default:
|
|
170
|
+
// Mimics official OpenAI Python SDK
|
|
171
|
+
return `OpenAI/Python/1.56.0`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Add a small random delay between requests to avoid rate limiting
|
|
177
|
+
* and appear more human-like (100-300ms)
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
async function randomDelay() {
|
|
181
|
+
const delay = 100 + Math.random() * 200;
|
|
182
|
+
return new Promise(resolve => setTimeout(resolve, delay));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build common headers for API requests that look like legitimate SDK clients
|
|
187
|
+
* @param {string} channelType - Channel type
|
|
188
|
+
* @param {Object} channel - Channel configuration
|
|
189
|
+
* @returns {Object} - Headers object
|
|
190
|
+
*/
|
|
191
|
+
function buildRequestHeaders(channelType, channel) {
|
|
192
|
+
const headers = {
|
|
193
|
+
'User-Agent': getRealisticUserAgent(channelType),
|
|
194
|
+
'Accept': 'application/json',
|
|
195
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
196
|
+
'Connection': 'keep-alive',
|
|
197
|
+
'X-Request-Id': crypto.randomUUID()
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// For OpenAI-compatible APIs, add additional headers
|
|
201
|
+
if (channelType === 'codex' || channelType === 'openai_compatible') {
|
|
202
|
+
headers['OpenAI-Beta'] = 'assistants=v2';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return headers;
|
|
206
|
+
}
|
|
207
|
+
|
|
135
208
|
/**
|
|
136
209
|
* Get cache file path
|
|
137
210
|
*/
|
|
@@ -210,9 +283,10 @@ async function testModelAvailability(channel, channelType, model) {
|
|
|
210
283
|
const baseUrl = channel.baseUrl.trim().replace(/\/+$/, '');
|
|
211
284
|
let testUrl;
|
|
212
285
|
let requestBody;
|
|
286
|
+
// Start with common headers that look like legitimate SDK clients
|
|
213
287
|
let headers = {
|
|
214
|
-
|
|
215
|
-
'
|
|
288
|
+
...buildRequestHeaders(channelType, channel),
|
|
289
|
+
'Content-Type': 'application/json'
|
|
216
290
|
};
|
|
217
291
|
|
|
218
292
|
// Construct API endpoint and request based on channel type
|
|
@@ -225,8 +299,11 @@ async function testModelAvailability(channel, channelType, model) {
|
|
|
225
299
|
max_tokens: 1,
|
|
226
300
|
messages: [{ role: 'user', content: 'test' }]
|
|
227
301
|
});
|
|
228
|
-
} else if (channelType === 'codex') {
|
|
229
|
-
|
|
302
|
+
} else if (channelType === 'codex' || channelType === 'openai_compatible') {
|
|
303
|
+
// 处理 baseUrl 已包含 /v1 的情况
|
|
304
|
+
testUrl = baseUrl.endsWith('/v1')
|
|
305
|
+
? `${baseUrl}/chat/completions`
|
|
306
|
+
: `${baseUrl}/v1/chat/completions`;
|
|
230
307
|
headers['Authorization'] = `Bearer ${channel.apiKey}`;
|
|
231
308
|
requestBody = JSON.stringify({
|
|
232
309
|
model: model,
|
|
@@ -344,9 +421,16 @@ async function probeModelAvailability(channel, channelType) {
|
|
|
344
421
|
console.log(`[ModelDetector] Testing models for channel ${channel.name} (${channelType})...`);
|
|
345
422
|
|
|
346
423
|
const availableModels = [];
|
|
424
|
+
let isFirstModel = true;
|
|
347
425
|
|
|
348
426
|
// Test models in priority order
|
|
349
427
|
for (const model of modelsToTest) {
|
|
428
|
+
// Add delay between model tests to avoid rate limiting (skip first)
|
|
429
|
+
if (!isFirstModel) {
|
|
430
|
+
await randomDelay();
|
|
431
|
+
}
|
|
432
|
+
isFirstModel = false;
|
|
433
|
+
|
|
350
434
|
const isAvailable = await testModelAvailability(channel, channelType, model);
|
|
351
435
|
|
|
352
436
|
if (isAvailable) {
|
|
@@ -421,8 +505,12 @@ function getCachedModelInfo(channelId) {
|
|
|
421
505
|
* @returns {Promise<Object>} { models: string[], supported: boolean, cached: boolean, error: string|null, fallbackUsed: boolean }
|
|
422
506
|
*/
|
|
423
507
|
async function fetchModelsFromProvider(channel, channelType) {
|
|
424
|
-
//
|
|
425
|
-
|
|
508
|
+
// PRESERVE original channel type for fallback model selection
|
|
509
|
+
const originalChannelType = channelType;
|
|
510
|
+
|
|
511
|
+
// Only auto-detect if channelType is NOT specified at all
|
|
512
|
+
// DO NOT auto-detect when channelType is 'claude' - respect the caller's intent
|
|
513
|
+
if (!channelType) {
|
|
426
514
|
channelType = detectChannelType(channel);
|
|
427
515
|
console.log(`[ModelDetector] Auto-detected channel type: ${channelType} for ${channel.name}`);
|
|
428
516
|
}
|
|
@@ -457,17 +545,18 @@ async function fetchModelsFromProvider(channel, channelType) {
|
|
|
457
545
|
return new Promise((resolve) => {
|
|
458
546
|
try {
|
|
459
547
|
const baseUrl = channel.baseUrl.trim().replace(/\/+$/, '');
|
|
460
|
-
const endpoint = capability.modelListEndpoint;
|
|
461
|
-
|
|
548
|
+
const endpoint = capability.modelListEndpoint; // e.g. '/v1/models'
|
|
549
|
+
// 避免路径重复:如果 baseUrl 已包含 /v1,则只拼接 /models
|
|
550
|
+
const requestUrl = baseUrl.endsWith('/v1') && endpoint.startsWith('/v1/')
|
|
551
|
+
? `${baseUrl}${endpoint.slice(3)}`
|
|
552
|
+
: `${baseUrl}${endpoint}`;
|
|
462
553
|
|
|
463
554
|
const parsedUrl = new URL(requestUrl);
|
|
464
555
|
const isHttps = parsedUrl.protocol === 'https:';
|
|
465
556
|
const httpModule = isHttps ? https : http;
|
|
466
557
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
'Accept': 'application/json'
|
|
470
|
-
};
|
|
558
|
+
// Use realistic SDK headers to avoid anti-crawler detection
|
|
559
|
+
const headers = buildRequestHeaders(channelType, channel);
|
|
471
560
|
|
|
472
561
|
// Add authentication header
|
|
473
562
|
if (capability.authHeader && channel.apiKey) {
|
|
@@ -540,11 +629,15 @@ async function fetchModelsFromProvider(channel, channelType) {
|
|
|
540
629
|
let errorHint;
|
|
541
630
|
|
|
542
631
|
if (isCloudflare) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
632
|
+
// Use originalChannelType for fallback to ensure correct models
|
|
633
|
+
// This prevents Claude channels from getting Codex models when using third-party proxies
|
|
634
|
+
const fallbackModels = MODEL_PRIORITY[originalChannelType || channelType] || MODEL_PRIORITY.claude;
|
|
635
|
+
const fallbackLabel = fallbackModels[0] || 'unknown';
|
|
636
|
+
errorMessage = 'Cloudflare 防护拦截,已使用默认模型列表';
|
|
637
|
+
errorHint = `该 API 端点受 Cloudflare 保护,已自动使用默认模型列表`;
|
|
638
|
+
console.warn(`[ModelDetector] Cloudflare protection detected for ${channel.name}, using default models for ${originalChannelType || channelType}`);
|
|
546
639
|
resolve({
|
|
547
|
-
models:
|
|
640
|
+
models: fallbackModels,
|
|
548
641
|
supported: true,
|
|
549
642
|
cached: false,
|
|
550
643
|
fallbackUsed: true,
|
|
@@ -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
|
+
};
|