ai-account-switch 1.5.6 → 1.6.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/src/ui-server.js CHANGED
@@ -99,6 +99,8 @@ class UIServer {
99
99
  this.handleExportAll(req, res);
100
100
  } else if (pathname === '/api/import' && req.method === 'POST') {
101
101
  this.handleImportAccounts(req, res);
102
+ } else if (pathname.startsWith('/api/accounts/') && pathname.endsWith('/check') && req.method === 'POST') {
103
+ this.handleCheckAccount(req, res, pathname);
102
104
  } else {
103
105
  res.writeHead(404, { 'Content-Type': 'application/json' });
104
106
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -247,6 +249,146 @@ class UIServer {
247
249
  });
248
250
  }
249
251
 
252
+ handleCheckAccount(req, res, pathname) {
253
+ const name = decodeURIComponent(pathname.split('/api/accounts/')[1].replace('/check', ''));
254
+
255
+ const saveCheckResult = (status, error = null) => {
256
+ const account = this.config.getAccount(name);
257
+ if (account) {
258
+ account.lastCheck = {
259
+ status,
260
+ error,
261
+ timestamp: new Date().toISOString()
262
+ };
263
+ this.config.addAccount(name, account);
264
+ }
265
+ };
266
+
267
+ try {
268
+ const account = this.config.getAccount(name);
269
+ if (!account) {
270
+ res.writeHead(404, { 'Content-Type': 'application/json' });
271
+ res.end(JSON.stringify({ error: 'Account not found' }));
272
+ return;
273
+ }
274
+
275
+ // Basic check - if API key exists, consider it potentially available
276
+ if (!account.apiKey) {
277
+ saveCheckResult('unavailable', 'No API key configured');
278
+ res.writeHead(200, { 'Content-Type': 'application/json' });
279
+ res.end(JSON.stringify({
280
+ success: true,
281
+ status: 'unavailable',
282
+ error: 'No API key configured'
283
+ }));
284
+ return;
285
+ }
286
+
287
+ // For now, just check if the configuration is complete
288
+ let status = 'available';
289
+
290
+ // Check if API URL is accessible (for CCR and custom endpoints)
291
+ if (account.apiUrl) {
292
+ const https = require('https');
293
+ const http = require('http');
294
+
295
+ try {
296
+ const url = new URL(account.apiUrl);
297
+ const client = url.protocol === 'https:' ? https : http;
298
+
299
+ const options = {
300
+ hostname: url.hostname,
301
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
302
+ path: '/',
303
+ method: 'HEAD',
304
+ timeout: 3000
305
+ };
306
+
307
+ let responded = false;
308
+
309
+ const request = client.request(options, (response) => {
310
+ if (responded) return;
311
+ responded = true;
312
+
313
+ if (response.statusCode >= 200 && response.statusCode < 500) {
314
+ status = 'available';
315
+ } else {
316
+ status = 'unstable';
317
+ }
318
+
319
+ saveCheckResult(status);
320
+ res.writeHead(200, { 'Content-Type': 'application/json' });
321
+ res.end(JSON.stringify({
322
+ success: true,
323
+ status,
324
+ statusCode: response.statusCode
325
+ }));
326
+ });
327
+
328
+ request.on('error', (error) => {
329
+ if (responded) return;
330
+ responded = true;
331
+
332
+ // Connection refused, host not found, etc. = unavailable
333
+ const isUnavailable = error.code === 'ECONNREFUSED' ||
334
+ error.code === 'ENOTFOUND' ||
335
+ error.code === 'EHOSTUNREACH';
336
+
337
+ const status = isUnavailable ? 'unavailable' : 'unstable';
338
+ saveCheckResult(status, error.message);
339
+ res.writeHead(200, { 'Content-Type': 'application/json' });
340
+ res.end(JSON.stringify({
341
+ success: true,
342
+ status,
343
+ error: error.message
344
+ }));
345
+ });
346
+
347
+ request.on('timeout', () => {
348
+ if (responded) return;
349
+ responded = true;
350
+
351
+ request.destroy();
352
+ saveCheckResult('unavailable', 'Connection timeout');
353
+ res.writeHead(200, { 'Content-Type': 'application/json' });
354
+ res.end(JSON.stringify({
355
+ success: true,
356
+ status: 'unavailable',
357
+ error: 'Connection timeout'
358
+ }));
359
+ });
360
+
361
+ request.end();
362
+ } catch (e) {
363
+ saveCheckResult('available');
364
+ res.writeHead(200, { 'Content-Type': 'application/json' });
365
+ res.end(JSON.stringify({
366
+ success: true,
367
+ status: 'available',
368
+ message: 'Configuration looks valid'
369
+ }));
370
+ }
371
+ } else {
372
+ // No API URL, just check if key exists
373
+ saveCheckResult('available');
374
+ res.writeHead(200, { 'Content-Type': 'application/json' });
375
+ res.end(JSON.stringify({
376
+ success: true,
377
+ status: 'available',
378
+ message: 'Configuration looks valid'
379
+ }));
380
+ }
381
+ } catch (error) {
382
+ saveCheckResult('unavailable', error.message);
383
+ res.writeHead(200, { 'Content-Type': 'application/json' });
384
+ res.end(JSON.stringify({
385
+ success: true,
386
+ status: 'unavailable',
387
+ error: error.message
388
+ }));
389
+ }
390
+ }
391
+
250
392
  getHTMLContent() {
251
393
  return `<!DOCTYPE html>
252
394
  <html lang="zh-CN">
@@ -482,6 +624,21 @@ class UIServer {
482
624
  color: var(--text-primary);
483
625
  }
484
626
 
627
+ .filter-box {
628
+ min-width: 150px;
629
+ }
630
+
631
+ .filter-box select {
632
+ width: 100%;
633
+ padding: 10px;
634
+ border: 1px solid var(--input-border);
635
+ border-radius: 5px;
636
+ font-size: 14px;
637
+ background: var(--input-bg);
638
+ color: var(--text-primary);
639
+ cursor: pointer;
640
+ }
641
+
485
642
  .accounts-grid {
486
643
  display: grid;
487
644
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
@@ -497,6 +654,7 @@ class UIServer {
497
654
  display: flex;
498
655
  flex-direction: column;
499
656
  min-height: 200px;
657
+ border-left: 4px solid transparent;
500
658
  }
501
659
 
502
660
  .account-card:hover {
@@ -504,6 +662,26 @@ class UIServer {
504
662
  box-shadow: 0 6px 12px var(--card-shadow-hover);
505
663
  }
506
664
 
665
+ .account-card.type-claude {
666
+ border-left-color: #1976d2;
667
+ }
668
+
669
+ .account-card.type-codex {
670
+ border-left-color: #7b1fa2;
671
+ }
672
+
673
+ .account-card.type-droids {
674
+ border-left-color: #388e3c;
675
+ }
676
+
677
+ .account-card.type-ccr {
678
+ border-left-color: #ff9800;
679
+ }
680
+
681
+ .account-card.type-other {
682
+ border-left-color: #f57c00;
683
+ }
684
+
507
685
  .account-content {
508
686
  flex: 1;
509
687
  }
@@ -515,6 +693,12 @@ class UIServer {
515
693
  margin-bottom: 15px;
516
694
  }
517
695
 
696
+ .account-name-wrapper {
697
+ display: flex;
698
+ align-items: center;
699
+ gap: 8px;
700
+ }
701
+
518
702
  .account-name {
519
703
  font-size: 1.4rem;
520
704
  font-weight: 600;
@@ -527,10 +711,62 @@ class UIServer {
527
711
  border-radius: 4px;
528
712
  font-size: 12px;
529
713
  font-weight: 500;
714
+ }
715
+
716
+ .account-status-wrapper {
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 8px;
720
+ }
721
+
722
+ .account-status {
723
+ display: inline-block;
724
+ width: 12px;
725
+ height: 12px;
726
+ border-radius: 50%;
727
+ }
728
+
729
+ .account-status.available {
730
+ background: #4CAF50;
731
+ }
732
+
733
+ .account-status.unstable {
734
+ background: #FF9800;
735
+ }
736
+
737
+ .account-status.unavailable {
738
+ background: #f44336;
739
+ }
740
+
741
+ .account-status.unknown {
742
+ background: #9E9E9E;
743
+ }
744
+
745
+ .account-type.type-claude {
530
746
  background: #e3f2fd;
531
747
  color: #1976d2;
532
748
  }
533
749
 
750
+ .account-type.type-codex {
751
+ background: #f3e5f5;
752
+ color: #7b1fa2;
753
+ }
754
+
755
+ .account-type.type-droids {
756
+ background: #e8f5e9;
757
+ color: #388e3c;
758
+ }
759
+
760
+ .account-type.type-ccr {
761
+ background: #fff3e0;
762
+ color: #ff9800;
763
+ }
764
+
765
+ .account-type.type-other {
766
+ background: #fafafa;
767
+ color: #757575;
768
+ }
769
+
534
770
  .account-info {
535
771
  margin-bottom: 10px;
536
772
  }
@@ -807,6 +1043,16 @@ class UIServer {
807
1043
  <div class="search-box">
808
1044
  <input type="text" id="searchInput" data-i18n-placeholder="searchPlaceholder" placeholder="搜索账号...">
809
1045
  </div>
1046
+ <div class="filter-box">
1047
+ <select id="typeFilter" onchange="renderAccounts()">
1048
+ <option value="" data-i18n="allTypes">所有类型</option>
1049
+ <option value="Claude">Claude</option>
1050
+ <option value="Codex">Codex</option>
1051
+ <option value="CCR">CCR</option>
1052
+ <option value="Droids">Droids</option>
1053
+ <option value="Other" data-i18n="other">其他</option>
1054
+ </select>
1055
+ </div>
810
1056
  <button class="btn btn-primary" onclick="showAddModal()" data-i18n="addAccount">+ 添加账号</button>
811
1057
  <button class="btn btn-secondary" onclick="exportAccounts()" data-i18n="exportAll">导出全部</button>
812
1058
  <button class="btn btn-secondary" onclick="document.getElementById('importFile').click()" data-i18n="import">导入</button>
@@ -832,9 +1078,11 @@ class UIServer {
832
1078
  </div>
833
1079
  <div class="form-group">
834
1080
  <label for="accountType" data-i18n="type">类型 *</label>
835
- <select id="accountType" required>
1081
+ <select id="accountType" required onchange="toggleModelFields()">
836
1082
  <option value="Claude">Claude</option>
837
1083
  <option value="Codex">Codex</option>
1084
+ <option value="CCR">CCR</option>
1085
+ <option value="Droids">Droids</option>
838
1086
  <option value="Other" data-i18n="other">其他</option>
839
1087
  </select>
840
1088
  </div>
@@ -854,19 +1102,50 @@ class UIServer {
854
1102
  <label for="description" data-i18n="description">描述 (可选)</label>
855
1103
  <textarea id="description" data-i18n-placeholder="descriptionPlaceholder" placeholder="用于生产环境的主账号"></textarea>
856
1104
  </div>
857
- <div class="form-group">
858
- <label data-i18n="customEnv">自定义环境变量 (可选)</label>
859
- <div id="envVarsList"></div>
860
- <button type="button" class="btn btn-secondary btn-small" onclick="addEnvVar()" data-i18n="addVariable">+ 添加变量</button>
861
- </div>
862
1105
  <div class="form-group">
863
1106
  <div class="advanced-toggle" onclick="toggleAdvancedSettings()">
864
1107
  <span class="advanced-toggle-icon" id="advancedToggleIcon">▶</span>
865
- <span data-i18n="advancedSettings">高级设置 - 模型组</span>
1108
+ <span data-i18n="advancedSettings">高级配置</span>
866
1109
  </div>
867
1110
  <div class="advanced-content" id="advancedContent">
868
- <div id="modelGroupsList" style="margin-bottom: 10px;"></div>
869
- <button type="button" class="btn btn-secondary btn-small" onclick="addModelGroupUI()" data-i18n="addModelGroup">+ 添加模型组</button>
1111
+ <!-- Custom Environment Variables -->
1112
+ <div class="form-group">
1113
+ <label data-i18n="customEnv">自定义环境变量</label>
1114
+ <div id="envVarsList"></div>
1115
+ <button type="button" class="btn btn-secondary btn-small" onclick="addEnvVar()" data-i18n="addVariable">+ 添加变量</button>
1116
+ </div>
1117
+ <!-- Model Configuration -->
1118
+ <div class="form-group">
1119
+ <label data-i18n="modelConfig">模型配置</label>
1120
+ <!-- Simple model field for Codex/Droids -->
1121
+ <div id="simpleModelGroup" style="display: none;">
1122
+ <input type="text" id="simpleModel" data-i18n-placeholder="simpleModelPlaceholder" placeholder="例如: gpt-4, droids-model-v1">
1123
+ </div>
1124
+ <!-- CCR model fields -->
1125
+ <div id="ccrModelGroup" style="display: none;">
1126
+ <div style="margin-bottom: 10px;">
1127
+ <label>Provider Name</label>
1128
+ <input type="text" id="ccrProviderName" placeholder="例如: Local-new-api">
1129
+ </div>
1130
+ <div style="margin-bottom: 10px;">
1131
+ <label>Default Model</label>
1132
+ <input type="text" id="ccrDefaultModel" placeholder="例如: gemini-2.5-flash">
1133
+ </div>
1134
+ <div style="margin-bottom: 10px;">
1135
+ <label>Background Model</label>
1136
+ <input type="text" id="ccrBackgroundModel" placeholder="例如: gemini-2.5-flash">
1137
+ </div>
1138
+ <div style="margin-bottom: 10px;">
1139
+ <label>Think Model</label>
1140
+ <input type="text" id="ccrThinkModel" placeholder="例如: gemini-2.5-pro">
1141
+ </div>
1142
+ </div>
1143
+ <!-- Model groups for Claude -->
1144
+ <div id="claudeModelGroup" style="display: none;">
1145
+ <div id="modelGroupsList" style="margin-bottom: 10px;"></div>
1146
+ <button type="button" class="btn btn-secondary btn-small" onclick="addModelGroupUI()" data-i18n="addModelGroup">+ 添加模型组</button>
1147
+ </div>
1148
+ </div>
870
1149
  </div>
871
1150
  </div>
872
1151
  <div class="form-actions">
@@ -885,6 +1164,7 @@ class UIServer {
885
1164
  subtitle: '管理你的 AI 账号配置',
886
1165
  themeLabel: '主题',
887
1166
  searchPlaceholder: '搜索账号...',
1167
+ allTypes: '所有类型',
888
1168
  addAccount: '+ 添加账号',
889
1169
  exportAll: '导出全部',
890
1170
  import: '导入',
@@ -904,9 +1184,12 @@ class UIServer {
904
1184
  emailPlaceholder: 'user@example.com',
905
1185
  description: '描述 (可选)',
906
1186
  descriptionPlaceholder: '用于生产环境的主账号',
907
- customEnv: '自定义环境变量 (可选)',
1187
+ advancedSettings: '高级配置',
1188
+ customEnv: '自定义环境变量',
908
1189
  addVariable: '+ 添加变量',
909
- advancedSettings: '高级设置 - 模型组',
1190
+ modelConfig: '模型配置',
1191
+ simpleModel: '模型',
1192
+ simpleModelPlaceholder: '例如: gpt-4, droids-model-v1',
910
1193
  addModelGroup: '+ 添加模型组',
911
1194
  modelGroupName: '模型组名称',
912
1195
  setActive: '设为激活',
@@ -937,6 +1220,7 @@ class UIServer {
937
1220
  subtitle: 'Manage your AI account configurations',
938
1221
  themeLabel: 'Theme',
939
1222
  searchPlaceholder: 'Search accounts...',
1223
+ allTypes: 'All Types',
940
1224
  addAccount: '+ Add Account',
941
1225
  exportAll: 'Export All',
942
1226
  import: 'Import',
@@ -956,9 +1240,12 @@ class UIServer {
956
1240
  emailPlaceholder: 'user@example.com',
957
1241
  description: 'Description (optional)',
958
1242
  descriptionPlaceholder: 'Main account for production environment',
959
- customEnv: 'Custom Environment Variables (optional)',
1243
+ advancedSettings: 'Advanced Configuration',
1244
+ customEnv: 'Custom Environment Variables',
960
1245
  addVariable: '+ Add Variable',
961
- advancedSettings: 'Advanced Settings - Model Groups',
1246
+ modelConfig: 'Model Configuration',
1247
+ simpleModel: 'Model',
1248
+ simpleModelPlaceholder: 'e.g., gpt-4, droids-model-v1',
962
1249
  addModelGroup: '+ Add Model Group',
963
1250
  modelGroupName: 'Model Group Name',
964
1251
  setActive: 'Set Active',
@@ -1068,11 +1355,18 @@ class UIServer {
1068
1355
  const container = document.getElementById('accountsContainer');
1069
1356
  const emptyState = document.getElementById('emptyState');
1070
1357
  const searchTerm = document.getElementById('searchInput').value.toLowerCase();
1358
+ const typeFilter = document.getElementById('typeFilter').value;
1071
1359
 
1072
1360
  const filteredAccounts = Object.entries(accounts).filter(([name, data]) => {
1073
- return name.toLowerCase().includes(searchTerm) ||
1361
+ // Search filter
1362
+ const matchesSearch = name.toLowerCase().includes(searchTerm) ||
1074
1363
  (data.email && data.email.toLowerCase().includes(searchTerm)) ||
1075
1364
  (data.type && data.type.toLowerCase().includes(searchTerm));
1365
+
1366
+ // Type filter
1367
+ const matchesType = !typeFilter || data.type === typeFilter;
1368
+
1369
+ return matchesSearch && matchesType;
1076
1370
  });
1077
1371
 
1078
1372
  if (filteredAccounts.length === 0) {
@@ -1082,12 +1376,20 @@ class UIServer {
1082
1376
  }
1083
1377
 
1084
1378
  emptyState.classList.add('hidden');
1085
- container.innerHTML = filteredAccounts.map(([name, data]) => \`
1086
- <div class="account-card">
1379
+ container.innerHTML = filteredAccounts.map(([name, data]) => {
1380
+ const typeClass = data.type ? \`type-\${data.type.toLowerCase()}\` : 'type-other';
1381
+ return \`
1382
+ <div class="account-card \${typeClass}">
1087
1383
  <div class="account-content">
1088
1384
  <div class="account-header">
1089
- <div class="account-name">\${name}</div>
1090
- <div class="account-type">\${data.type || 'N/A'}</div>
1385
+ <div class="account-name-wrapper">
1386
+ <div class="account-name">\${name}</div>
1387
+ <span class="account-type \${typeClass}">\${data.type || 'N/A'}</span>
1388
+ </div>
1389
+ <div class="account-status-wrapper">
1390
+ <span class="account-status \${data.lastCheck ? data.lastCheck.status : 'unknown'}" id="status_\${name}" title="\${data.lastCheck ? (data.lastCheck.status === 'available' ? '可用' : data.lastCheck.status === 'unstable' ? '不稳定' : '不可用') : '未检查'}"></span>
1391
+ <button class="btn btn-secondary btn-small" onclick="checkAccount('\${name}')" id="checkBtn_\${name}">状态检查</button>
1392
+ </div>
1091
1393
  </div>
1092
1394
  <div class="account-info">
1093
1395
  <div class="info-label">\${t('apiKeyLabel')}</div>
@@ -1117,19 +1419,36 @@ class UIServer {
1117
1419
  <div class="info-value">\${Object.keys(data.customEnv).join(', ')}</div>
1118
1420
  </div>
1119
1421
  \` : ''}
1120
- \${data.modelGroups && Object.keys(data.modelGroups).length > 0 ? \`
1422
+ \${data.type === 'Claude' && data.modelGroups && Object.keys(data.modelGroups).length > 0 ? \`
1121
1423
  <div class="account-info">
1122
1424
  <div class="info-label">Model Groups</div>
1123
1425
  <div class="info-value">\${Object.keys(data.modelGroups).join(', ')} \${data.activeModelGroup ? '(active: ' + data.activeModelGroup + ')' : ''}</div>
1124
1426
  </div>
1125
1427
  \` : ''}
1428
+ \${(data.type === 'Codex' || data.type === 'Droids') && data.model ? \`
1429
+ <div class="account-info">
1430
+ <div class="info-label">Model</div>
1431
+ <div class="info-value">\${data.model}</div>
1432
+ </div>
1433
+ \` : ''}
1434
+ \${data.type === 'CCR' && data.ccrConfig ? \`
1435
+ <div class="account-info">
1436
+ <div class="info-label">CCR Provider</div>
1437
+ <div class="info-value">\${data.ccrConfig.providerName}</div>
1438
+ </div>
1439
+ <div class="account-info">
1440
+ <div class="info-label">Models</div>
1441
+ <div class="info-value">default: \${data.ccrConfig.defaultModel}, background: \${data.ccrConfig.backgroundModel}, think: \${data.ccrConfig.thinkModel}</div>
1442
+ </div>
1443
+ \` : ''}
1126
1444
  </div>
1127
1445
  <div class="account-actions">
1128
1446
  <button class="btn btn-secondary btn-small" onclick="editAccount('\${name}')">\${t('edit')}</button>
1129
1447
  <button class="btn btn-danger btn-small" onclick="deleteAccount('\${name}')">\${t('delete')}</button>
1130
1448
  </div>
1131
1449
  </div>
1132
- \`).join('');
1450
+ \`;
1451
+ }).join('');
1133
1452
  }
1134
1453
 
1135
1454
  function maskApiKey(key) {
@@ -1137,6 +1456,27 @@ class UIServer {
1137
1456
  return key.substring(0, 4) + '****' + key.substring(key.length - 4);
1138
1457
  }
1139
1458
 
1459
+ function toggleModelFields() {
1460
+ const accountType = document.getElementById('accountType').value;
1461
+ const simpleModelGroup = document.getElementById('simpleModelGroup');
1462
+ const claudeModelGroup = document.getElementById('claudeModelGroup');
1463
+ const ccrModelGroup = document.getElementById('ccrModelGroup');
1464
+
1465
+ if (accountType === 'Codex' || accountType === 'Droids') {
1466
+ simpleModelGroup.style.display = 'block';
1467
+ claudeModelGroup.style.display = 'none';
1468
+ if (ccrModelGroup) ccrModelGroup.style.display = 'none';
1469
+ } else if (accountType === 'CCR') {
1470
+ simpleModelGroup.style.display = 'none';
1471
+ claudeModelGroup.style.display = 'none';
1472
+ if (ccrModelGroup) ccrModelGroup.style.display = 'block';
1473
+ } else {
1474
+ simpleModelGroup.style.display = 'none';
1475
+ claudeModelGroup.style.display = 'block';
1476
+ if (ccrModelGroup) ccrModelGroup.style.display = 'none';
1477
+ }
1478
+ }
1479
+
1140
1480
  function showAddModal() {
1141
1481
  editingAccount = null;
1142
1482
  document.getElementById('modalTitle').textContent = t('addAccountTitle');
@@ -1148,9 +1488,13 @@ class UIServer {
1148
1488
  document.getElementById('modelGroupsList').innerHTML = '';
1149
1489
  modelGroupCount = 0;
1150
1490
  activeModelGroup = null;
1491
+ // Clear simple model
1492
+ document.getElementById('simpleModel').value = '';
1151
1493
  // Collapse advanced settings
1152
1494
  document.getElementById('advancedContent').classList.remove('expanded');
1153
1495
  document.getElementById('advancedToggleIcon').classList.remove('expanded');
1496
+ // Toggle model fields based on default type (Claude)
1497
+ toggleModelFields();
1154
1498
  document.getElementById('accountModal').classList.add('active');
1155
1499
  }
1156
1500
 
@@ -1176,13 +1520,44 @@ class UIServer {
1176
1520
  });
1177
1521
  }
1178
1522
 
1179
- // Load model groups
1180
- document.getElementById('modelGroupsList').innerHTML = '';
1181
- modelGroupCount = 0;
1182
- activeModelGroup = null;
1183
-
1184
- const hasModelGroups = account.modelGroups && Object.keys(account.modelGroups).length > 0;
1185
- if (hasModelGroups) {
1523
+ // Toggle model fields based on account type
1524
+ toggleModelFields();
1525
+
1526
+ // Load model configuration based on account type
1527
+ if (account.type === 'Codex' || account.type === 'Droids') {
1528
+ // Load simple model field
1529
+ document.getElementById('simpleModel').value = account.model || '';
1530
+ // Clear model groups and CCR config
1531
+ document.getElementById('modelGroupsList').innerHTML = '';
1532
+ modelGroupCount = 0;
1533
+ activeModelGroup = null;
1534
+ document.getElementById('ccrProviderName').value = '';
1535
+ document.getElementById('ccrDefaultModel').value = '';
1536
+ document.getElementById('ccrBackgroundModel').value = '';
1537
+ document.getElementById('ccrThinkModel').value = '';
1538
+ } else if (account.type === 'CCR') {
1539
+ // Load CCR config
1540
+ if (account.ccrConfig) {
1541
+ document.getElementById('ccrProviderName').value = account.ccrConfig.providerName || '';
1542
+ document.getElementById('ccrDefaultModel').value = account.ccrConfig.defaultModel || '';
1543
+ document.getElementById('ccrBackgroundModel').value = account.ccrConfig.backgroundModel || '';
1544
+ document.getElementById('ccrThinkModel').value = account.ccrConfig.thinkModel || '';
1545
+ }
1546
+ // Clear simple model and model groups
1547
+ document.getElementById('simpleModel').value = '';
1548
+ document.getElementById('modelGroupsList').innerHTML = '';
1549
+ modelGroupCount = 0;
1550
+ activeModelGroup = null;
1551
+ } else {
1552
+ // Load model groups for Claude
1553
+ document.getElementById('modelGroupsList').innerHTML = '';
1554
+ modelGroupCount = 0;
1555
+ activeModelGroup = null;
1556
+ // Clear simple model
1557
+ document.getElementById('simpleModel').value = '';
1558
+
1559
+ const hasModelGroups = account.modelGroups && Object.keys(account.modelGroups).length > 0;
1560
+ if (hasModelGroups) {
1186
1561
  Object.entries(account.modelGroups).forEach(([groupName, groupConfig]) => {
1187
1562
  const groupId = modelGroupCount++;
1188
1563
  const isActive = account.activeModelGroup === groupName;
@@ -1237,13 +1612,14 @@ class UIServer {
1237
1612
  container.appendChild(div);
1238
1613
  });
1239
1614
 
1240
- // Expand advanced settings if model groups exist
1241
- document.getElementById('advancedContent').classList.add('expanded');
1242
- document.getElementById('advancedToggleIcon').classList.add('expanded');
1243
- } else {
1244
- // Collapse advanced settings if no model groups
1245
- document.getElementById('advancedContent').classList.remove('expanded');
1246
- document.getElementById('advancedToggleIcon').classList.remove('expanded');
1615
+ // Expand advanced settings if model groups exist
1616
+ document.getElementById('advancedContent').classList.add('expanded');
1617
+ document.getElementById('advancedToggleIcon').classList.add('expanded');
1618
+ } else {
1619
+ // Collapse advanced settings if no model groups
1620
+ document.getElementById('advancedContent').classList.remove('expanded');
1621
+ document.getElementById('advancedToggleIcon').classList.remove('expanded');
1622
+ }
1247
1623
  }
1248
1624
 
1249
1625
  document.getElementById('accountModal').classList.add('active');
@@ -1404,12 +1780,41 @@ class UIServer {
1404
1780
  delete accountData.customEnv;
1405
1781
  }
1406
1782
 
1407
- // Collect model groups
1408
- accountData.modelGroups = {};
1409
- accountData.activeModelGroup = null;
1783
+ // Collect model configuration based on account type
1784
+ const accountType = document.getElementById('accountType').value;
1410
1785
 
1411
- const modelGroupsList = document.getElementById('modelGroupsList');
1412
- modelGroupsList.querySelectorAll('.model-group-item').forEach(item => {
1786
+ if (accountType === 'Codex' || accountType === 'Droids') {
1787
+ // Use simple model field
1788
+ const simpleModel = document.getElementById('simpleModel').value.trim();
1789
+ if (simpleModel) {
1790
+ accountData.model = simpleModel;
1791
+ }
1792
+ } else if (accountType === 'CCR') {
1793
+ // Collect CCR config
1794
+ const providerName = document.getElementById('ccrProviderName').value.trim();
1795
+ const defaultModel = document.getElementById('ccrDefaultModel').value.trim();
1796
+ const backgroundModel = document.getElementById('ccrBackgroundModel').value.trim();
1797
+ const thinkModel = document.getElementById('ccrThinkModel').value.trim();
1798
+
1799
+ if (providerName && defaultModel && backgroundModel && thinkModel) {
1800
+ const models = [defaultModel, backgroundModel, thinkModel];
1801
+ const uniqueModels = [...new Set(models)];
1802
+
1803
+ accountData.ccrConfig = {
1804
+ providerName,
1805
+ models: uniqueModels,
1806
+ defaultModel,
1807
+ backgroundModel,
1808
+ thinkModel
1809
+ };
1810
+ }
1811
+ } else {
1812
+ // Collect model groups for Claude
1813
+ accountData.modelGroups = {};
1814
+ accountData.activeModelGroup = null;
1815
+
1816
+ const modelGroupsList = document.getElementById('modelGroupsList');
1817
+ modelGroupsList.querySelectorAll('.model-group-item').forEach(item => {
1413
1818
  const groupId = parseInt(item.id.replace('modelGroup', ''));
1414
1819
  const groupName = document.getElementById(\`groupName\${groupId}\`).value;
1415
1820
 
@@ -1430,15 +1835,16 @@ class UIServer {
1430
1835
 
1431
1836
  accountData.modelGroups[groupName] = groupConfig;
1432
1837
 
1433
- // Check if this is the active group
1434
- if (activeModelGroup === groupId) {
1435
- accountData.activeModelGroup = groupName;
1436
- }
1437
- });
1838
+ // Check if this is the active group
1839
+ if (activeModelGroup === groupId) {
1840
+ accountData.activeModelGroup = groupName;
1841
+ }
1842
+ });
1438
1843
 
1439
- if (Object.keys(accountData.modelGroups).length === 0) {
1440
- delete accountData.modelGroups;
1441
- delete accountData.activeModelGroup;
1844
+ if (Object.keys(accountData.modelGroups).length === 0) {
1845
+ delete accountData.modelGroups;
1846
+ delete accountData.activeModelGroup;
1847
+ }
1442
1848
  }
1443
1849
 
1444
1850
  try {
@@ -1551,6 +1957,47 @@ class UIServer {
1551
1957
  }, 3000);
1552
1958
  }
1553
1959
 
1960
+ async function checkAccount(name) {
1961
+ const checkBtn = document.getElementById(\`checkBtn_\${name}\`);
1962
+ const statusBadge = document.getElementById(\`status_\${name}\`);
1963
+
1964
+ if (!checkBtn || !statusBadge) return;
1965
+
1966
+ checkBtn.disabled = true;
1967
+ checkBtn.textContent = '检查中...';
1968
+ statusBadge.title = '检查中...';
1969
+ statusBadge.className = 'account-status unknown';
1970
+
1971
+ try {
1972
+ const response = await fetch(\`/api/accounts/\${encodeURIComponent(name)}/check\`, {
1973
+ method: 'POST'
1974
+ });
1975
+
1976
+ const result = await response.json();
1977
+
1978
+ if (result.success) {
1979
+ const statusMap = {
1980
+ 'available': '可用',
1981
+ 'unstable': '不稳定',
1982
+ 'unavailable': '不可用'
1983
+ };
1984
+
1985
+ statusBadge.title = statusMap[result.status] || '未知';
1986
+ statusBadge.className = \`account-status \${result.status}\`;
1987
+ } else {
1988
+ statusBadge.title = '检查失败';
1989
+ statusBadge.className = 'account-status unavailable';
1990
+ }
1991
+ } catch (error) {
1992
+ statusBadge.title = '检查失败';
1993
+ statusBadge.className = 'account-status unavailable';
1994
+ showToast('检查失败: ' + error.message, 'error');
1995
+ } finally {
1996
+ checkBtn.disabled = false;
1997
+ checkBtn.textContent = '状态检查';
1998
+ }
1999
+ }
2000
+
1554
2001
  // Event listeners
1555
2002
  document.getElementById('searchInput').addEventListener('input', renderAccounts);
1556
2003