protocol-proxy 2.5.1 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protocol-proxy",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "OpenAI / Anthropic 协议转换透明代理",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -587,6 +587,59 @@ function initModelDropdown() {
587
587
  });
588
588
  }
589
589
 
590
+ async function importModels() {
591
+ const providerId = document.getElementById('provider-id').value;
592
+ if (!providerId) {
593
+ showToast('请先选择供应商', true);
594
+ return;
595
+ }
596
+ const btn = document.getElementById('model-import-btn');
597
+ btn.disabled = true;
598
+ btn.textContent = '导入中...';
599
+ try {
600
+ const apiKeys = collectApiKeys();
601
+ const res = await fetch(`/api/providers/${providerId}/available-models`, {
602
+ method: 'POST',
603
+ headers: { 'Content-Type': 'application/json' },
604
+ body: JSON.stringify({ apiKeys }),
605
+ });
606
+ const data = await res.json();
607
+ if (!data.models || data.models.length === 0) {
608
+ showToast(data.message || '未获取到模型', true);
609
+ return;
610
+ }
611
+ const provider = providers.find(p => p.id === providerId);
612
+ const existing = new Set(provider?.models || []);
613
+ const newModels = data.models.filter(m => !existing.has(m));
614
+ if (newModels.length === 0) {
615
+ showToast(`已全部存在,共 ${data.models.length} 个模型`);
616
+ // 即使没有新模型,也尝试自动选择第一个
617
+ if (!document.getElementById('target-model').value && data.models.length > 0) {
618
+ selectModel(data.models[0]);
619
+ }
620
+ return;
621
+ }
622
+ const merged = [...(provider?.models || []), ...newModels];
623
+ await fetch(`/api/providers/${providerId}`, {
624
+ method: 'PUT',
625
+ headers: { 'Content-Type': 'application/json' },
626
+ body: JSON.stringify({ models: merged }),
627
+ });
628
+ await loadProviders();
629
+ renderModelOptions();
630
+ // 自动选择默认模型
631
+ if (!document.getElementById('target-model').value) {
632
+ selectModel(newModels[0] || data.models[0]);
633
+ }
634
+ showToast(`已导入 ${newModels.length} 个新模型(共 ${data.models.length} 个)`);
635
+ } catch (err) {
636
+ showToast('导入失败: ' + err.message, true);
637
+ } finally {
638
+ btn.disabled = false;
639
+ btn.textContent = '自动导入';
640
+ }
641
+ }
642
+
590
643
  function renderModelOptions() {
591
644
  const container = document.getElementById('model-dropdown-options');
592
645
  const providerId = document.getElementById('provider-id').value;
@@ -1201,6 +1254,140 @@ function closeModal() {
1201
1254
  editingProviderId = null;
1202
1255
  }
1203
1256
 
1257
+ function showTestResultModal(data) {
1258
+ const modal = document.getElementById('test-result-modal');
1259
+ const icon = document.getElementById('test-result-icon');
1260
+ const summary = document.getElementById('test-result-summary');
1261
+ const list = document.getElementById('test-result-list');
1262
+ const closeBtn = document.getElementById('test-result-close');
1263
+
1264
+ if (data.failed === 0) {
1265
+ icon.textContent = '✓';
1266
+ icon.style.background = 'rgba(6, 78, 59, 0.4)';
1267
+ icon.style.color = '#34d399';
1268
+ icon.style.borderColor = 'rgba(52, 211, 153, 0.15)';
1269
+ summary.innerHTML = `<strong>${data.total}</strong> 条 API Key 全部测试通过`;
1270
+ } else if (data.passed === 0) {
1271
+ icon.textContent = '✗';
1272
+ icon.style.background = 'rgba(127, 29, 29, 0.4)';
1273
+ icon.style.color = '#f87171';
1274
+ icon.style.borderColor = 'rgba(248, 113, 113, 0.15)';
1275
+ summary.innerHTML = `<strong>${data.total}</strong> 条 API Key 全部测试失败`;
1276
+ } else {
1277
+ icon.textContent = '!';
1278
+ icon.style.background = 'rgba(69, 26, 3, 0.4)';
1279
+ icon.style.color = '#fbbf24';
1280
+ icon.style.borderColor = 'rgba(251, 191, 36, 0.15)';
1281
+ summary.innerHTML = `<strong>${data.passed}</strong> 条通过,<strong>${data.failed}</strong> 条失败`;
1282
+ }
1283
+
1284
+ list.innerHTML = data.results.map(r => `
1285
+ <div class="test-result-item ${r.ok ? 'test-ok' : 'test-fail'}">
1286
+ <div class="test-result-row">
1287
+ <span class="test-result-status">${r.ok ? '✓' : '✗'}</span>
1288
+ <span class="test-result-alias">${escapeHtml(r.alias || `Key #${r.index + 1}`)}</span>
1289
+ ${r.latencyMs != null ? `<span class="test-result-latency">${r.latencyMs}ms</span>` : ''}
1290
+ </div>
1291
+ ${r.message ? `<div class="test-result-error">${escapeHtml(r.message)}</div>` : ''}
1292
+ </div>
1293
+ `).join('');
1294
+
1295
+ modal.classList.add('active');
1296
+ closeBtn.onclick = () => modal.classList.remove('active');
1297
+ }
1298
+
1299
+ function clearKeyErrors() {
1300
+ document.querySelectorAll('#api-keys-list .api-key-entry').forEach(row => {
1301
+ row.querySelector('.api-key-input')?.style.removeProperty('border-color');
1302
+ row.querySelector('.api-key-display')?.style.removeProperty('border-color');
1303
+ row.querySelector('.api-key-error')?.remove();
1304
+ });
1305
+ }
1306
+
1307
+ function markKeyErrors(data) {
1308
+ const rows = document.querySelectorAll('#api-keys-list .api-key-entry');
1309
+ for (const r of data.results) {
1310
+ if (!r.ok) {
1311
+ const row = rows[r.index];
1312
+ if (row) {
1313
+ const el = row.querySelector('.api-key-input') || row.querySelector('.api-key-display');
1314
+ if (el) el.style.borderColor = '#ef4444';
1315
+ if (r.message) {
1316
+ const errDiv = document.createElement('div');
1317
+ errDiv.className = 'api-key-error';
1318
+ errDiv.textContent = r.message;
1319
+ // Insert after the API Key form-group
1320
+ const keyGroup = row.querySelectorAll('.form-group')[1];
1321
+ if (keyGroup) keyGroup.appendChild(errDiv);
1322
+ }
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ async function testConnection() {
1329
+ const providerId = document.getElementById('provider-id').value;
1330
+ if (!providerId) {
1331
+ showToast('请先选择供应商', true);
1332
+ return;
1333
+ }
1334
+ const protocol = document.getElementById('target-protocol').value;
1335
+ const apiKeys = collectApiKeys();
1336
+ const model = document.getElementById('target-model').value.trim() || '';
1337
+ const btn = document.getElementById('test-connection-btn');
1338
+ btn.disabled = true;
1339
+ btn.textContent = '测试中...';
1340
+ clearKeyErrors();
1341
+ try {
1342
+ const res = await fetch(`/api/providers/${providerId}/test`, {
1343
+ method: 'POST',
1344
+ headers: { 'Content-Type': 'application/json' },
1345
+ body: JSON.stringify({ protocol, apiKeys, model }),
1346
+ });
1347
+ const data = await res.json();
1348
+ if (!data.results || data.results.length === 0) {
1349
+ showToast(data.message || '没有可用的 API Key', true);
1350
+ return;
1351
+ }
1352
+ markKeyErrors(data);
1353
+ showTestResultModal(data);
1354
+ } catch (err) {
1355
+ showToast('测试请求失败: ' + err.message, true);
1356
+ } finally {
1357
+ btn.disabled = false;
1358
+ btn.textContent = '测试连接';
1359
+ }
1360
+ }
1361
+
1362
+ async function autoTestForSave() {
1363
+ const providerId = document.getElementById('provider-id').value;
1364
+ if (!providerId) return true;
1365
+ const protocol = document.getElementById('target-protocol').value;
1366
+ const apiKeys = collectApiKeys();
1367
+ const model = document.getElementById('target-model').value.trim() || '';
1368
+ clearKeyErrors();
1369
+ try {
1370
+ const res = await fetch(`/api/providers/${providerId}/test`, {
1371
+ method: 'POST',
1372
+ headers: { 'Content-Type': 'application/json' },
1373
+ body: JSON.stringify({ protocol, apiKeys, model }),
1374
+ });
1375
+ const data = await res.json();
1376
+ if (!data.results || data.results.length === 0) return true;
1377
+ if (data.failed === 0) {
1378
+ showToast(`${data.total} 条 API Key 测试通过`);
1379
+ return true;
1380
+ }
1381
+ markKeyErrors(data);
1382
+ return await showConfirm(
1383
+ `${data.passed} 条通过,${data.failed} 条失败。<br><br>是否仍然保存?`,
1384
+ '仍然保存'
1385
+ );
1386
+ } catch (err) {
1387
+ return true;
1388
+ }
1389
+ }
1390
+
1204
1391
  async function handleSubmit(e) {
1205
1392
  e.preventDefault();
1206
1393
 
@@ -1210,6 +1397,21 @@ async function handleSubmit(e) {
1210
1397
  return;
1211
1398
  }
1212
1399
 
1400
+ // 保存前自动测试:如果有 API Key 被修改,先测试连接
1401
+ const hasModifiedKeys = !!document.querySelector('#api-keys-list .api-key-entry[data-masked="false"], #api-keys-list .api-key-entry[data-new="true"]');
1402
+ if (hasModifiedKeys) {
1403
+ const saveBtn = document.querySelector('.modal-footer .btn-primary');
1404
+ saveBtn.disabled = true;
1405
+ saveBtn.textContent = '测试中...';
1406
+ try {
1407
+ const canProceed = await autoTestForSave();
1408
+ if (!canProceed) return;
1409
+ } finally {
1410
+ saveBtn.disabled = false;
1411
+ saveBtn.textContent = '保存';
1412
+ }
1413
+ }
1414
+
1213
1415
  const port = parseInt(document.getElementById('proxy-port').value);
1214
1416
 
1215
1417
  const conflict = proxies.find(p => p.id !== editingId && p.port === port);
package/public/index.html CHANGED
@@ -184,6 +184,7 @@
184
184
  <div class="model-add-section" id="model-add-section">
185
185
  <input type="text" class="model-add-input" id="model-add-input" placeholder="输入模型名称">
186
186
  <button type="button" class="btn btn-primary btn-sm" id="model-add-btn">添加</button>
187
+ <button type="button" class="btn btn-sm" id="model-import-btn" onclick="importModels()">自动导入</button>
187
188
  </div>
188
189
  </div>
189
190
  </div>
@@ -252,6 +253,7 @@
252
253
 
253
254
  <div class="modal-footer">
254
255
  <button type="button" class="btn" onclick="closeModal()">取消</button>
256
+ <button type="button" class="btn" id="test-connection-btn" onclick="testConnection()">测试连接</button>
255
257
  <button type="submit" class="btn btn-primary">保存</button>
256
258
  </div>
257
259
  </form>
@@ -271,6 +273,18 @@
271
273
  </div>
272
274
  </div>
273
275
 
276
+ <!-- 测试结果弹窗 -->
277
+ <div class="modal confirm-modal" id="test-result-modal">
278
+ <div class="confirm-box test-result-box">
279
+ <div class="confirm-icon" id="test-result-icon">!</div>
280
+ <p class="confirm-text" id="test-result-summary"></p>
281
+ <div class="test-result-list" id="test-result-list"></div>
282
+ <div class="confirm-actions">
283
+ <button class="btn btn-primary" id="test-result-close">知道了</button>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
274
288
  <!-- 导入预览弹窗 -->
275
289
  <div class="modal" id="import-modal">
276
290
  <div class="modal-content" style="max-width:500px">
package/public/style.css CHANGED
@@ -611,8 +611,9 @@ form {
611
611
  /* Toast */
612
612
  .toast {
613
613
  position: fixed;
614
- bottom: 28px;
615
- right: 28px;
614
+ top: 50%;
615
+ left: 50%;
616
+ transform: translate(-50%, -50%);
616
617
  padding: 12px 24px;
617
618
  background: linear-gradient(135deg, #22c55e, #16a34a);
618
619
  color: white;
@@ -626,8 +627,8 @@ form {
626
627
  }
627
628
 
628
629
  @keyframes toast-in {
629
- from { opacity: 0; transform: translateY(16px) scale(0.95); }
630
- to { opacity: 1; transform: translateY(0) scale(1); }
630
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
631
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
631
632
  }
632
633
 
633
634
  @keyframes toast-out {
@@ -1457,3 +1458,103 @@ form {
1457
1458
  background: rgba(127, 29, 29, 0.35);
1458
1459
  border-color: rgba(248, 113, 113, 0.8);
1459
1460
  }
1461
+
1462
+ /* Inline API Key error */
1463
+ .api-key-error {
1464
+ margin-top: 6px;
1465
+ padding: 6px 10px;
1466
+ background: rgba(127, 29, 29, 0.15);
1467
+ border: 1px solid rgba(248, 113, 113, 0.2);
1468
+ border-radius: 6px;
1469
+ color: #fca5a5;
1470
+ font-size: 0.78rem;
1471
+ line-height: 1.4;
1472
+ }
1473
+
1474
+ /* Test result modal items */
1475
+ .test-result-box {
1476
+ max-width: 460px !important;
1477
+ }
1478
+
1479
+ .test-result-list {
1480
+ display: flex;
1481
+ flex-direction: column;
1482
+ gap: 8px;
1483
+ margin-bottom: 20px;
1484
+ max-height: 300px;
1485
+ overflow-y: auto;
1486
+ }
1487
+
1488
+ .test-result-item {
1489
+ padding: 10px 14px;
1490
+ border-radius: 10px;
1491
+ background: rgba(6, 8, 15, 0.4);
1492
+ border: 1px solid rgba(51, 65, 85, 0.3);
1493
+ }
1494
+
1495
+ .test-result-item.test-ok {
1496
+ border-color: rgba(52, 211, 153, 0.2);
1497
+ }
1498
+
1499
+ .test-result-item.test-fail {
1500
+ border-color: rgba(248, 113, 113, 0.3);
1501
+ background: rgba(127, 29, 29, 0.1);
1502
+ }
1503
+
1504
+ .test-result-row {
1505
+ display: flex;
1506
+ align-items: center;
1507
+ gap: 10px;
1508
+ }
1509
+
1510
+ .test-result-status {
1511
+ font-size: 1rem;
1512
+ font-weight: 700;
1513
+ width: 20px;
1514
+ text-align: center;
1515
+ }
1516
+
1517
+ .test-ok .test-result-status {
1518
+ color: #34d399;
1519
+ }
1520
+
1521
+ .test-fail .test-result-status {
1522
+ color: #f87171;
1523
+ }
1524
+
1525
+ .test-result-alias {
1526
+ flex: 1;
1527
+ color: #e2e8f0;
1528
+ font-size: 0.9rem;
1529
+ font-weight: 500;
1530
+ }
1531
+
1532
+ .test-result-latency {
1533
+ color: #60a5fa;
1534
+ font-size: 0.82rem;
1535
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
1536
+ }
1537
+
1538
+ .test-result-error {
1539
+ margin-top: 6px;
1540
+ padding: 5px 10px;
1541
+ color: #fca5a5;
1542
+ font-size: 0.78rem;
1543
+ background: rgba(127, 29, 29, 0.15);
1544
+ border-radius: 6px;
1545
+ border: 1px solid rgba(248, 113, 113, 0.1);
1546
+ }
1547
+
1548
+ /* test-result-list scrollbar */
1549
+ .test-result-list::-webkit-scrollbar {
1550
+ width: 6px;
1551
+ }
1552
+
1553
+ .test-result-list::-webkit-scrollbar-track {
1554
+ background: transparent;
1555
+ }
1556
+
1557
+ .test-result-list::-webkit-scrollbar-thumb {
1558
+ background: rgba(71, 85, 105, 0.5);
1559
+ border-radius: 3px;
1560
+ }
package/server.js CHANGED
@@ -329,6 +329,169 @@ async function init() {
329
329
  res.json(updated);
330
330
  });
331
331
 
332
+ app.post('/api/providers/:id/test', async (req, res) => {
333
+ const provider = configStore.getProviderById(req.params.id);
334
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
335
+
336
+ const existingKeys = provider.apiKeys || [];
337
+ const reqKeys = Array.isArray(req.body.apiKeys) ? req.body.apiKeys : [];
338
+ const resolved = reqKeys
339
+ .map((k, i) => {
340
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
341
+ const ex = existingKeys[k.index];
342
+ return ex ? { key: ex.key, alias: k.alias || ex.alias || '', domIndex: i } : null;
343
+ }
344
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
345
+ return { key: k.key.trim(), alias: k.alias || '', domIndex: i };
346
+ }
347
+ if (typeof k === 'string' && k.trim()) return { key: k.trim(), alias: '', domIndex: i };
348
+ return null;
349
+ })
350
+ .filter(Boolean);
351
+
352
+ if (resolved.length === 0) {
353
+ return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
354
+ }
355
+
356
+ const protocol = req.body.protocol || provider.protocol || 'openai';
357
+ const base = provider.url.replace(/\/$/, '');
358
+ const hasV1Suffix = base.endsWith('/v1');
359
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
360
+
361
+ function buildTestOpts(key) {
362
+ if (protocol === 'openai') {
363
+ if (isAzure) {
364
+ const ver = provider.azureApiVersion || '2024-02-01';
365
+ return {
366
+ url: `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`,
367
+ opts: { headers: { 'api-key': key } },
368
+ };
369
+ }
370
+ return {
371
+ url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`,
372
+ opts: { headers: { 'Authorization': `Bearer ${key}` } },
373
+ };
374
+ }
375
+ if (protocol === 'anthropic') {
376
+ const testModel = req.body.model || 'claude-3-haiku-20240307';
377
+ return {
378
+ url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
379
+ opts: {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' },
382
+ body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
383
+ },
384
+ };
385
+ }
386
+ if (protocol === 'gemini') {
387
+ return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
388
+ }
389
+ return null;
390
+ }
391
+
392
+ if (protocol !== 'openai' && protocol !== 'anthropic' && protocol !== 'gemini') {
393
+ return res.json({ ok: false, message: `不支持的协议: ${protocol}`, results: [] });
394
+ }
395
+
396
+ const results = await Promise.all(resolved.map(async entry => {
397
+ const { url: testUrl, opts: fetchOpts } = buildTestOpts(entry.key);
398
+ try {
399
+ const startedAt = Date.now();
400
+ const fetchRes = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
401
+ const latencyMs = Date.now() - startedAt;
402
+ if (!fetchRes.ok) {
403
+ const errText = await fetchRes.text().catch(() => '');
404
+ const hint = fetchRes.status === 401 || fetchRes.status === 403
405
+ ? 'API Key 无效或无权限'
406
+ : `HTTP ${fetchRes.status}: ${errText.slice(0, 200) || '未知错误'}`;
407
+ return { ok: false, alias: entry.alias, index: entry.domIndex, message: hint, latencyMs };
408
+ }
409
+ return { ok: true, alias: entry.alias, index: entry.domIndex, latencyMs };
410
+ } catch (err) {
411
+ const msg = err.name === 'TimeoutError' ? '连接超时 (15s)' : `连接失败: ${err.message}`;
412
+ return { ok: false, alias: entry.alias, index: entry.domIndex, message: msg };
413
+ }
414
+ }));
415
+
416
+ const passed = results.filter(r => r.ok).length;
417
+ const failed = results.length - passed;
418
+ res.json({ ok: failed === 0, passed, failed, total: results.length, results });
419
+ });
420
+
421
+ app.post('/api/providers/:id/available-models', async (req, res) => {
422
+ const provider = configStore.getProviderById(req.params.id);
423
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
424
+
425
+ // Support unsaved API keys from form
426
+ let keys;
427
+ const reqKeys = Array.isArray(req.body?.apiKeys) ? req.body.apiKeys : [];
428
+ if (reqKeys.length > 0) {
429
+ const existingKeys = provider.apiKeys || [];
430
+ keys = reqKeys
431
+ .map(k => {
432
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
433
+ return existingKeys[k.index]?.key || null;
434
+ }
435
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
436
+ return k.key.trim();
437
+ }
438
+ return null;
439
+ })
440
+ .filter(Boolean);
441
+ } else {
442
+ keys = (provider.apiKeys || []).map(k => k.key).filter(Boolean);
443
+ }
444
+ if (keys.length === 0) return res.json({ models: [], message: '没有可用的 API Key' });
445
+
446
+ const protocol = provider.protocol || 'openai';
447
+ const base = provider.url.replace(/\/$/, '');
448
+ const hasV1Suffix = base.endsWith('/v1');
449
+ const key = keys[0];
450
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
451
+
452
+ try {
453
+ let fetchUrl, fetchOpts;
454
+ if (protocol === 'openai') {
455
+ if (isAzure) {
456
+ const ver = provider.azureApiVersion || '2024-02-01';
457
+ fetchUrl = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
458
+ fetchOpts = { headers: { 'api-key': key } };
459
+ } else {
460
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
461
+ fetchOpts = { headers: { 'Authorization': `Bearer ${key}` } };
462
+ }
463
+ } else if (protocol === 'gemini') {
464
+ fetchUrl = `${base}/v1beta/models?key=${key}`;
465
+ fetchOpts = {};
466
+ } else if (protocol === 'anthropic') {
467
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
468
+ fetchOpts = { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } };
469
+ } else {
470
+ return res.json({ models: [], message: `不支持的协议: ${protocol}` });
471
+ }
472
+
473
+ const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
474
+ if (!fetchRes.ok) {
475
+ const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
476
+ return res.json({ models: [], message: hint });
477
+ }
478
+
479
+ const data = await fetchRes.json().catch(() => null);
480
+ let models = [];
481
+ if (Array.isArray(data?.data)) {
482
+ // OpenAI 格式(含第三方 Anthropic 兼容供应商)
483
+ models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
484
+ } else if (Array.isArray(data?.models)) {
485
+ // Gemini 格式
486
+ models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
487
+ }
488
+
489
+ res.json({ models });
490
+ } catch (err) {
491
+ res.json({ models: [], message: `获取失败: ${err.message}` });
492
+ }
493
+ });
494
+
332
495
  app.delete('/api/providers/:id', (req, res) => {
333
496
  const existing = configStore.getProviderById(req.params.id);
334
497
  if (!existing) return res.status(404).json({ error: 'Provider not found' });