@@ -45,6 +45,10 @@ const i18n = {
4545 btn_test : '测试连接' ,
4646 btn_ping : '连通检测' ,
4747 btn_save : '保存服务商' ,
48+ btn_testall : '测试全部' ,
49+ btn_export_all : '导出全部' ,
50+ btn_import : '导入' ,
51+ btn_delete_all : '删除全部' ,
4852 btn_copy : '复制' ,
4953 btn_download : '下载' ,
5054 saved_title : '服务商' ,
@@ -68,6 +72,7 @@ const i18n = {
6872 onboarding_next : '下一步' ,
6973 toast_saved : '✓ 已保存(密钥已加密)' ,
7074 toast_deleted : '✓ 已删除' ,
75+ toast_deleted_all : '✓ 已清空全部服务商' ,
7176 toast_loaded : '✓ 已加载' ,
7277 toast_copied : '✓ 已复制' ,
7378 toast_imported : '✓ 导入并保存成功' ,
@@ -81,6 +86,7 @@ const i18n = {
8186 prompt_name : '为该服务商命名:' ,
8287 prompt_rename : '输入新名称:' ,
8388 confirm_delete : '确认删除该服务商及其所有配置?' ,
89+ confirm_delete_all : '确认删除全部已保存服务商?' ,
8490 status_ok : '正常' ,
8591 status_fail : '失败' ,
8692 ping_dns : 'DNS 解析' ,
@@ -151,6 +157,10 @@ const i18n = {
151157 btn_test : 'Test' ,
152158 btn_ping : 'Ping' ,
153159 btn_save : 'Save Provider' ,
160+ btn_testall : 'Test All' ,
161+ btn_export_all : 'Export All' ,
162+ btn_import : 'Import' ,
163+ btn_delete_all : 'Delete All' ,
154164 btn_copy : 'Copy' ,
155165 btn_download : 'Download' ,
156166 saved_title : 'PROVIDERS' ,
@@ -174,6 +184,7 @@ const i18n = {
174184 onboarding_next : 'Next' ,
175185 toast_saved : '✓ Saved (key encrypted)' ,
176186 toast_deleted : '✓ Deleted' ,
187+ toast_deleted_all : '✓ All providers cleared' ,
177188 toast_loaded : '✓ Loaded' ,
178189 toast_copied : '✓ Copied' ,
179190 toast_imported : '✓ Imported & saved' ,
@@ -187,6 +198,7 @@ const i18n = {
187198 prompt_name : 'Name this provider:' ,
188199 prompt_rename : 'Enter new name:' ,
189200 confirm_delete : 'Delete this provider and all its config?' ,
201+ confirm_delete_all : 'Delete all saved providers?' ,
190202 status_ok : 'OK' ,
191203 status_fail : 'FAIL' ,
192204 ping_dns : 'DNS Resolution' ,
@@ -236,6 +248,10 @@ function applyLang() {
236248 const key = el . getAttribute ( 'data-i18n' ) ;
237249 if ( i18n [ currentLang ] [ key ] ) el . textContent = i18n [ currentLang ] [ key ] ;
238250 } ) ;
251+ document . querySelectorAll ( '[data-i18n-title]' ) . forEach ( el => {
252+ const key = el . getAttribute ( 'data-i18n-title' ) ;
253+ if ( i18n [ currentLang ] [ key ] ) el . title = i18n [ currentLang ] [ key ] ;
254+ } ) ;
239255 document . querySelectorAll ( '[data-i18n-placeholder]' ) . forEach ( el => {
240256 const key = el . getAttribute ( 'data-i18n-placeholder' ) ;
241257 if ( i18n [ currentLang ] [ key ] ) el . placeholder = i18n [ currentLang ] [ key ] ;
@@ -596,6 +612,9 @@ async function renderProviders() {
596612 </div>
597613 </div>
598614 <div class="provider-actions" onclick="event.stopPropagation()">
615+ <button class="btn-icon btn-icon-sm" onclick="testProviderAllModels(${ p . id } )" title="${ t ( 'btn_testall' ) } ">
616+ <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 8h12"/><path d="M9 3l5 5-5 5"/></svg>
617+ </button>
599618 <button class="btn-icon btn-icon-sm" onclick="refreshProviderModels(${ p . id } )" title="${ t ( 'provider_refresh' ) } ">
600619 <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 8a6 6 0 0111.5-2.3"/><path d="M14 8a6 6 0 01-11.5 2.3"/><path d="M13 2v4h-4"/><path d="M3 14v-4h4"/></svg>
601620 </button>
@@ -663,10 +682,23 @@ async function deleteProvider(id) {
663682 expandedProviders . delete ( id ) ;
664683 setProviders ( raw ) ;
665684 renderProviders ( ) ;
685+ syncProbeMatchesAfterProviderChange ( ) ;
666686 showToast ( t ( 'toast_deleted' ) ) ;
667687 } ) ;
668688}
669689
690+ function deleteAllProviders ( ) {
691+ const raw = getRawProviders ( ) ;
692+ if ( ! raw . length ) return ;
693+ showConfirmModal ( t ( 'confirm_delete_all' ) , ( ) => {
694+ expandedProviders . clear ( ) ;
695+ setProviders ( [ ] ) ;
696+ renderProviders ( ) ;
697+ syncProbeMatchesAfterProviderChange ( ) ;
698+ showToast ( t ( 'toast_deleted_all' ) ) ;
699+ } ) ;
700+ }
701+
670702async function refreshProviderModels ( id ) {
671703 const providers = await getProviders ( ) ;
672704 const p = providers . find ( x => x . id === id ) ;
@@ -919,13 +951,39 @@ async function testAllProviders() {
919951 const providers = await getProviders ( ) ;
920952 if ( ! providers . length ) { showToast ( t ( 'testall_empty' ) ) ; return ; }
921953
954+ await runProviderModelTests ( providers . map ( p => ( {
955+ id : p . id ,
956+ name : p . name ,
957+ baseUrl : p . baseUrl ,
958+ apiKey : p . _key ,
959+ models : ( p . models || [ ] ) . slice ( )
960+ } ) ) ) ;
961+ }
962+
963+ async function testProviderAllModels ( id ) {
964+ const providers = await getProviders ( ) ;
965+ const p = providers . find ( x => x . id === id ) ;
966+ if ( ! p ) return ;
967+
968+ await runProviderModelTests ( [ {
969+ id : p . id ,
970+ name : p . name ,
971+ baseUrl : p . baseUrl ,
972+ apiKey : p . _key ,
973+ models : ( p . models || [ ] ) . slice ( )
974+ } ] ) ;
975+ }
976+
977+ async function runProviderModelTests ( providerList ) {
978+ if ( ! providerList . length ) { showToast ( t ( 'testall_empty' ) ) ; return ; }
979+
922980 testAllAbort = false ;
923981 document . getElementById ( 'testall-modal' ) . classList . remove ( 'hidden' ) ;
924982 const container = document . getElementById ( 'testall-results' ) ;
925983 const summaryEl = document . getElementById ( 'testall-summary' ) ;
926984 let totalOk = 0 , totalFail = 0 , totalSkip = 0 ;
927985
928- const pData = providers . map ( p => ( {
986+ const pData = providerList . map ( p => ( {
929987 id : p . id , name : p . name , baseUrl : p . baseUrl , apiKey : p . _key ,
930988 models : ( p . models || [ ] ) . slice ( ) , // ALL models, no limit
931989 status : 'run' , results : [ ]
@@ -1032,22 +1090,64 @@ function importConfigs() { document.getElementById('import-file').click(); }
10321090function handleImportFile ( e ) {
10331091 const file = e . target . files [ 0 ] ;
10341092 if ( ! file ) return ;
1093+ e . target . value = '' ;
1094+
1095+ if ( file . name . toLowerCase ( ) . endsWith ( '.zip' ) ) {
1096+ importZipFile ( file ) . catch ( ( ) => showToast ( t ( 'toast_import_fail' ) ) ) ;
1097+ return ;
1098+ }
1099+
10351100 const reader = new FileReader ( ) ;
10361101 reader . onload = ev => {
10371102 try { parseAndAutoSave ( ev . target . result , file . name ) ; }
10381103 catch { showToast ( t ( 'toast_import_fail' ) ) ; }
10391104 } ;
10401105 reader . readAsText ( file ) ;
1041- e . target . value = '' ;
1106+ }
1107+
1108+ async function importZipFile ( file ) {
1109+ if ( typeof JSZip === 'undefined' ) {
1110+ showToast ( currentLang === 'zh' ? 'JSZip 未加载,请刷新页面' : 'JSZip not loaded' ) ;
1111+ return ;
1112+ }
1113+
1114+ const zip = await JSZip . loadAsync ( file ) ;
1115+ const entries = Object . values ( zip . files ) . filter ( entry => ! entry . dir && entry . name . toLowerCase ( ) . endsWith ( '.json' ) ) ;
1116+ if ( entries . length === 0 ) {
1117+ showToast ( t ( 'toast_import_fail' ) ) ;
1118+ return ;
1119+ }
1120+
1121+ let importedCount = 0 ;
1122+ for ( const entry of entries ) {
1123+ const text = await entry . async ( 'string' ) ;
1124+ try {
1125+ await parseAndAutoSave ( text , entry . name ) ;
1126+ importedCount ++ ;
1127+ } catch {
1128+ // Skip invalid entry and continue importing remaining JSON files
1129+ }
1130+ }
1131+
1132+ if ( importedCount === 0 ) {
1133+ showToast ( t ( 'toast_import_fail' ) ) ;
1134+ return ;
1135+ }
1136+
1137+ showToast ( currentLang === 'zh'
1138+ ? `✓ 已导入 ZIP(${ importedCount } 个配置)`
1139+ : `✓ ZIP imported (${ importedCount } configs)` ) ;
10421140}
10431141
10441142async function parseAndAutoSave ( text , fname ) {
10451143 let cfg = { } ;
10461144 if ( fname . endsWith ( '.json' ) ) {
10471145 const j = JSON . parse ( text ) ;
1146+ cfg . name = j . name ;
10481147 cfg . baseUrl = j . base_url || j . baseUrl || j . OPENAI_BASE_URL || j . api_base || '' ;
10491148 cfg . apiKey = j . api_key || j . apiKey || j . OPENAI_API_KEY || '' ;
1050- cfg . model = j . model || j . MODEL || j . default_model || '' ;
1149+ cfg . model = j . model || '' ;
1150+ cfg . models = Array . isArray ( j . models ) ? j . models : [ ] ;
10511151 } else if ( fname . endsWith ( '.env' ) ) {
10521152 text . split ( '\n' ) . forEach ( line => {
10531153 const m = line . match ( / ^ ( [ ^ # = ] + ) = ( .* ) $ / ) ;
@@ -1082,16 +1182,24 @@ async function parseAndAutoSave(text, fname) {
10821182 // Fill form
10831183 document . getElementById ( 'base-url' ) . value = cfg . baseUrl ;
10841184 document . getElementById ( 'api-key' ) . value = cfg . apiKey ;
1085- if ( cfg . model ) document . getElementById ( 'model-name' ) . value = cfg . model ;
1185+ const modelNameEl = document . getElementById ( 'model-name' ) ;
1186+ // Auto-save: build model list from imported fields first
1187+ let modelList = [ ] ;
1188+ if ( cfg . models ) {
1189+ modelList = cfg . models . filter ( m => typeof m === 'string' && m . trim ( ) ) ;
1190+ } else if ( cfg . model ) {
1191+ modelList = [ cfg . model ] ;
1192+ } else {
1193+ showToast ( t ( 'toast_fetching_models' ) ) ;
1194+ modelList = await fetchModelsRaw ( cfg . baseUrl , cfg . apiKey ) ;
1195+ }
10861196
1087- // Auto-save: fetch models then save
1088- showToast ( t ( 'toast_fetching_models' ) ) ;
1089- const models = await fetchModelsRaw ( cfg . baseUrl , cfg . apiKey ) ;
1090- const modelList = models . length > 0 ? models : ( cfg . model ? [ cfg . model ] : [ ] ) ;
1197+ modelNameEl . value = ( cfg . model && modelList . includes ( cfg . model ) ) ? cfg . model : modelList ? modelList [ 0 ] : '' ;
10911198
10921199 const encKey = await encryptText ( cfg . apiKey ) ;
10931200 const raw = getRawProviders ( ) ;
1094- const name = extractHost ( cfg . baseUrl ) ;
1201+ const importedName = ( cfg . name || '' ) . trim ( ) ;
1202+ const name = importedName || extractHost ( cfg . baseUrl ) ;
10951203 raw . push ( {
10961204 id : Date . now ( ) ,
10971205 name,
@@ -1102,6 +1210,7 @@ async function parseAndAutoSave(text, fname) {
11021210 } ) ;
11031211 setProviders ( raw ) ;
11041212 renderProviders ( ) ;
1213+ await syncProbeMatchesAfterProviderChange ( ) ;
11051214 showToast ( t ( 'toast_imported' ) + ` — ${ name } (${ modelList . length } ${ currentLang === 'zh' ? '个模型' : 'models' } )` ) ;
11061215}
11071216
@@ -1125,7 +1234,7 @@ function generateFormatContent(format, cfg) {
11251234 case 'curl' : return { title : 'cURL' , filename : 'test_api.sh' , content : `curl "${ base } /chat/completions" \\\n -H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${ apiKey } " \\\n -d '{\n "model": "${ model } ",\n "messages": [{"role":"user","content":"${ prompt . replace ( / " / g, '\\"' ) } "}],\n "max_tokens": 256\n }'` } ;
11261235 case 'python' : return { title : 'Python' , filename : 'test_api.py' , content : `from openai import OpenAI\n\nclient = OpenAI(\n api_key="${ apiKey } ",\n base_url="${ base } "\n)\n\nres = client.chat.completions.create(\n model="${ model } ",\n messages=[{"role":"user","content":"${ prompt . replace ( / " / g, '\\"' ) } "}],\n max_tokens=256\n)\nprint(res.choices[0].message.content)` } ;
11271236 case 'json' : return { title : 'JSON' , filename : `${ name . replace ( / [ ^ a - z A - Z 0 - 9 \u4e00 - \u9fff _ - ] / g, '_' ) } _config.json` , content : JSON . stringify ( { name, base_url : base , api_key : apiKey , model, models : cfg . models || [ model ] , created_at : new Date ( ) . toISOString ( ) } , null , 2 ) } ;
1128- case 'provider-full' : return { title : name , filename : `${ name . replace ( / [ ^ a - z A - Z 0 - 9 \u4e00 - \u9fff _ - ] / g, '_' ) } .json` , content : JSON . stringify ( { name, base_url : base , api_key : apiKey , models : cfg . models || [ ] , created_at : cfg . ts || new Date ( ) . toISOString ( ) } , null , 2 ) } ;
1237+ case 'provider-full' : return { title : name , filename : `${ name . replace ( / [ ^ a - z A - Z 0 - 9 \u4e00 - \u9fff _ - ] / g, '_' ) } .json` , content : JSON . stringify ( { name, base_url : base , api_key : apiKey , model , models : cfg . models || [ ] , created_at : cfg . ts || new Date ( ) . toISOString ( ) } , null , 2 ) } ;
11291238 default : return { title : '' , filename : '' , content : '' } ;
11301239 }
11311240}
@@ -1243,6 +1352,39 @@ function downloadExport() {
12431352let probeData = [ ] ;
12441353let probeAbort = false ;
12451354
1355+ async function syncProbeMatchesAfterProviderChange ( ) {
1356+ const model = document . getElementById ( 'probe-select' ) . value ;
1357+ const btn = document . getElementById ( 'btn-probe-all' ) ;
1358+
1359+ if ( ! model ) {
1360+ btn . disabled = true ;
1361+ const container = document . getElementById ( 'probe-results' ) . innerHTML = '' ;
1362+ probeData = [ ] ;
1363+ return ;
1364+ }
1365+
1366+ const providers = await getProviders ( ) ;
1367+ const previousStatus = new Map ( probeData . map ( item => [ item . providerId , { status : item . status , latency : item . latency } ] ) ) ;
1368+
1369+ probeData = providers
1370+ . filter ( p => ( p . models || [ ] ) . includes ( model ) )
1371+ . map ( p => {
1372+ const previous = previousStatus . get ( p . id ) ;
1373+ return {
1374+ providerId : p . id ,
1375+ providerName : p . name ,
1376+ baseUrl : p . baseUrl ,
1377+ apiKey : p . _key ,
1378+ model : model ,
1379+ status : previous ? previous . status : 'wait' ,
1380+ latency : previous ? previous . latency : null
1381+ } ;
1382+ } ) ;
1383+
1384+ btn . disabled = probeData . length === 0 ;
1385+ renderProbeResults ( ) ;
1386+ }
1387+
12461388// Populate the dropdown with all unique models from all providers
12471389async function refreshProbeDropdown ( ) {
12481390 const providers = await getProviders ( ) ;
@@ -1265,33 +1407,7 @@ async function refreshProbeDropdown() {
12651407
12661408// When user selects a model from dropdown
12671409async function onProbeSelect ( ) {
1268- const model = document . getElementById ( 'probe-select' ) . value ;
1269- const btn = document . getElementById ( 'btn-probe-all' ) ;
1270- if ( ! model ) {
1271- btn . disabled = true ;
1272- document . getElementById ( 'probe-results' ) . innerHTML = '' ;
1273- probeData = [ ] ;
1274- return ;
1275- }
1276-
1277- const providers = await getProviders ( ) ;
1278- probeData = [ ] ;
1279- for ( const p of providers ) {
1280- if ( ( p . models || [ ] ) . includes ( model ) ) {
1281- probeData . push ( {
1282- providerId : p . id ,
1283- providerName : p . name ,
1284- baseUrl : p . baseUrl ,
1285- apiKey : p . _key ,
1286- model,
1287- status : 'wait' ,
1288- latency : null
1289- } ) ;
1290- }
1291- }
1292-
1293- btn . disabled = probeData . length === 0 ;
1294- renderProbeResults ( ) ;
1410+ await syncProbeMatchesAfterProviderChange ( ) ;
12951411}
12961412
12971413function renderProbeResults ( ) {
0 commit comments