@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/web/assets/{icons-BlzwYoRU.js → icons-CO_2OFES.js} +1 -1
  3. package/dist/web/assets/index-DI8QOi-E.js +14 -0
  4. package/dist/web/assets/index-uLHGdeZh.css +41 -0
  5. package/dist/web/assets/{naive-ui-B1TP-0TP.js → naive-ui-B1re3c-e.js} +1 -1
  6. package/dist/web/index.html +4 -4
  7. package/package.json +1 -1
  8. package/src/commands/daemon.js +11 -1
  9. package/src/commands/ui.js +8 -1
  10. package/src/index.js +3 -1
  11. package/src/server/api/channels.js +3 -0
  12. package/src/server/api/codex-channels.js +40 -0
  13. package/src/server/api/config-registry.js +341 -0
  14. package/src/server/api/gemini-channels.js +40 -0
  15. package/src/server/api/oauth.js +294 -0
  16. package/src/server/api/permissions.js +30 -15
  17. package/src/server/codex-proxy-server.js +30 -4
  18. package/src/server/config/oauth-providers.js +68 -0
  19. package/src/server/gemini-proxy-server.js +64 -2
  20. package/src/server/index.js +15 -3
  21. package/src/server/proxy-server.js +31 -4
  22. package/src/server/services/channels.js +33 -2
  23. package/src/server/services/codex-channels.js +35 -4
  24. package/src/server/services/config-registry-service.js +762 -0
  25. package/src/server/services/config-sync-manager.js +456 -0
  26. package/src/server/services/config-templates-service.js +38 -3
  27. package/src/server/services/gemini-channels.js +40 -1
  28. package/src/server/services/model-detector.js +116 -23
  29. package/src/server/services/oauth-callback-server.js +284 -0
  30. package/src/server/services/oauth-service.js +378 -0
  31. package/src/server/services/oauth-token-storage.js +135 -0
  32. package/src/server/services/permission-templates-service.js +0 -31
  33. package/dist/web/assets/index-19ZPjh5b.css +0 -41
  34. 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-20250929',
17
+ 'claude-opus-4-5-20251101',
17
18
  'claude-sonnet-4-5-20250929',
18
- 'claude-haiku-4-5-20250929',
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: ['gpt-4o-mini', 'gpt-4o', 'gpt-5-codex', 'o3'],
25
- gemini: ['gemini-2.5-flash', 'gemini-2.5-pro']
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
- 'Content-Type': 'application/json',
215
- 'User-Agent': 'Coding-Tool-ModelDetector/1.0'
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
- testUrl = `${baseUrl}/v1/chat/completions`;
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
- // If no type specified or type is 'claude', auto-detect
425
- if (!channelType || channelType === 'claude') {
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
- const requestUrl = `${baseUrl}${endpoint}`;
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
- const headers = {
468
- 'User-Agent': 'Coding-Tool-ModelDetector/1.0',
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
- errorMessage = 'Cloudflare 防护拦截,已使用默认模型';
544
- errorHint = '该 API 端点受 Cloudflare 保护,已自动使用默认模型 claude-sonnet-4-5';
545
- console.warn(`[ModelDetector] Cloudflare protection detected for ${channel.name}, using default model`);
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: ['claude-sonnet-4-5'],
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 ? '&#10004;' : '&#10006;';
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
+ };