1// å·¥å ·å½æ° 2import { t, getCurrentLanguage } from './i18n.js'; 3import { apiClient } from './auth.js'; 4 5/** 6 * è·åæææ¯æçæä¾åé ç½®å表 7 * @param {string[]} supportedProviders - 已注åçæä¾åç±»åå表 8 * @returns {Object[]} æä¾åé 置对象æ°ç» 9 */ 10/** 11 * è·åææåºç¡æä¾åé ç½®ï¼æ¯çï¼ 12 * @returns {Object[]} åºç¡æä¾åé ç½®æ°ç» 13 */ 14function getBaseProviderConfigs() { 15 return [ 16 { 17 id: 'forward-api', 18 name: 'NewAPI', 19 icon: 'fa-share-square' 20 }, 21 { 22 id: 'gemini-cli-oauth', 23 name: t('dashboard.routing.nodeName.gemini'), 24 icon: 'fa-robot', 25 defaultPath: 'configs/gemini/' 26 }, 27 { 28 id: 'gemini-antigravity', 29 name: t('dashboard.routing.nodeName.antigravity'), 30 icon: 'fa-rocket', 31 defaultPath: 'configs/antigravity/' 32 }, 33 { 34 id: 'claude-kiro-oauth', 35 name: t('dashboard.routing.nodeName.kiro'), 36 icon: 'fa-key', 37 defaultPath: 'configs/kiro/' 38 }, 39 { 40 id: 'openai-codex-oauth', 41 name: t('dashboard.routing.nodeName.codex'), 42 icon: 'fa-code', 43 defaultPath: 'configs/codex/' 44 }, 45 { 46 id: 'grok-cli-oauth', 47 name: t('dashboard.routing.nodeName.grokCli'), 48 icon: 'fa-terminal', 49 defaultPath: 'configs/grok-cli/' 50 }, 51 { 52 id: 'openai-qwen-oauth', 53 name: t('dashboard.routing.nodeName.qwen'), 54 icon: 'fa-cloud', 55 defaultPath: 'configs/qwen/' 56 }, 57 { 58 id: 'openai-iflow', 59 name: t('dashboard.routing.nodeName.iflow'), 60 icon: 'fa-stream', 61 defaultPath: 'configs/iflow/' 62 }, 63 { 64 id: 'grok-web', 65 name: t('dashboard.routing.nodeName.grok'), 66 icon: 'fa-user-secret' 67 }, 68 { 69 id: 'openai-custom', 70 name: t('dashboard.routing.nodeName.openai'), 71 icon: 'fa-microchip' 72 }, 73 { 74 id: 'claude-custom', 75 name: t('dashboard.routing.nodeName.claude'), 76 icon: 'fa-brain' 77 }, 78 { 79 id: 'openaiResponses-custom', 80 name: 'OpenAI Responses', 81 icon: 'fa-reply-all' 82 }, 83 { 84 id: 'atlascloud', 85 name: 'AtlasCloud', 86 icon: 'fa-cloud' 87 }, 88 ]; 89} 90 91/** 92 * è·åæææ¯æçæä¾åé ç½®å表 93 * @param {string[]} supportedProviders - 已注åçæä¾åç±»åå表 94 * @returns {Object[]} æä¾åé 置对象æ°ç» 95 */ 96function getProviderConfigs(supportedProviders = []) { 97 const baseConfigs = getBaseProviderConfigs(); 98 99 const result = []; 100 const usedIds = new Set(); 101 102 // 1. éååºç¡é ç½®ï¼ä½¿å ¶æç §å®ä¹é¡ºåºæåï¼å¹¶ä¸è®©å¸¦åç¼çèªå®ä¹æä¾åç´§è·å¨å®çåºç¡æ¯çåé¢ 103 baseConfigs.forEach(baseConfig => { 104 const isSupported = supportedProviders.includes(baseConfig.id); 105 result.push({ ...baseConfig, visible: isSupported }); 106 usedIds.add(baseConfig.id); 107 108 // ç´§æ¥ç寻æ¾å¹é 该åºç¡ ID ç带åç¼çèªå®ä¹ç±»åï¼ä¾å¦ openai-custom-testï¼ 109 supportedProviders.forEach(providerId => { 110 if (usedIds.has(providerId)) return; 111 112 if (providerId.startsWith(baseConfig.id + '-')) { 113 const suffix = providerId.substring(baseConfig.id.length + 1); 114 result.push({ 115 ...baseConfig, 116 id: providerId, 117 name: `${baseConfig.name} (${suffix})`, 118 visible: true 119 }); 120 usedIds.add(providerId); 121 } 122 }); 123 }); 124 125 // 2. å®å ¨å åºï¼å¦æè¿æä¸äº supportedProviders æ¢ä¸å¹é ä»»ä½åºç¡ ID ä¹ä¸å¹é ä»»ä½åç¼ï¼å°±è¿½å å°æå 126 supportedProviders.forEach(providerId => { 127 if (!usedIds.has(providerId)) { 128 result.push({ 129 id: providerId, 130 name: providerId, 131 icon: 'fa-server', 132 visible: true 133 }); 134 usedIds.add(providerId); 135 } 136 }); 137 138 return result; 139} 140 141/** 142 * æ ¼å¼åè¿è¡æ¶é´ 143 * @param {number} seconds - ç§æ° 144 * @returns {string} æ ¼å¼åçæ¶é´å符串 145 */ 146function formatUptime(seconds) { 147 const days = Math.floor(seconds / 86400); 148 const hours = Math.floor((seconds % 86400) / 3600); 149 const minutes = Math.floor((seconds % 3600) / 60); 150 const secs = Math.floor(seconds % 60); 151 152 if (getCurrentLanguage() === 'en-US') { 153 return `${days}d ${hours}h ${minutes}m ${secs}s`; 154 } 155 return `${days}天 ${hours}å°æ¶ ${minutes}å ${secs}ç§`; 156} 157 158/** 159 * HTMLè½¬ä¹ 160 * @param {string} text - è¦è½¬ä¹çææ¬ 161 * @returns {string} 转ä¹åçææ¬ 162 */ 163function escapeHtml(text) { 164 const div = document.createElement('div'); 165 div.textContent = text; 166 return div.innerHTML; 167} 168 169/** 170 * æ¾ç¤ºæç¤ºæ¶æ¯ 171 * @param {string} title - æç¤ºæ é¢ (å¯éï¼æ§æ¥å£ä¸º message) 172 * @param {string} message - æç¤ºæ¶æ¯ 173 * @param {string} type - æ¶æ¯ç±»å (info, success, error) 174 */ 175function showToast(title, message, type = 'info') { 176 // å ¼å®¹æ§æ¥å£ (message, type) 177 if (arguments.length === 2 && (message === 'success' || message === 'error' || message === 'info' || message === 'warning')) { 178 type = message; 179 message = title; 180 title = t(`common.${type}`); 181 } 182 183 const toast = document.createElement('div'); 184 toast.className = `toast ${type}`; 185 toast.innerHTML = ` 186 <div style="font-weight: 600; margin-bottom: 4px;">${escapeHtml(title)}</div> 187 <div>${escapeHtml(message)}</div> 188 `; 189 190 // è·åtoastå®¹å¨ 191 const toastContainer = document.getElementById('toastContainer') || document.querySelector('.toast-container'); 192 if (toastContainer) { 193 toastContainer.appendChild(toast); 194 195 setTimeout(() => { 196 toast.remove(); 197 }, 3000); 198 } 199} 200 201/** 202 * è·ååæ®µæ¾ç¤ºææ¡ 203 * @param {string} key - åæ®µé® 204 * @returns {string} æ¾ç¤ºææ¡ 205 */
206function getFieldLabel(key) { 207 const labelMap = { 208 'customName': t('modal.provider.customName') + ' ' + t('config.optional'), 209 'checkModelName': t('modal.provider.checkModelName') + ' ' + t('config.optional'), 210 'checkHealth': t('modal.provider.healthCheckLabel'), 211 'concurrencyLimit': t('modal.provider.concurrencyLimit') + ' ' + t('config.optional'), 212 'queueLimit': t('modal.provider.queueLimit') + ' ' + t('config.optional'), 213 'OPENAI_API_KEY': 'OpenAI API Key', 214 'OPENAI_BASE_URL': 'OpenAI Base URL', 215 'CLAUDE_API_KEY': 'Claude API Key', 216 'CLAUDE_BASE_URL': 'Claude Base URL', 217 'PROJECT_ID': t('modal.provider.field.projectId'), 218 'GEMINI_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 219 'KIRO_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 220 'QWEN_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 221 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 222 'IFLOW_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 223 'CODEX_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 224 'GROK_CLI_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'), 225 'GROK_COOKIE_TOKEN': t('modal.provider.field.ssoToken'), 226 'GROK_CF_CLEARANCE': t('modal.provider.field.cfClearance'), 227 'GROK_CF_BM': t('modal.provider.field.cfBm'), 228 229 'GROK_USER_AGENT': t('modal.provider.field.userAgent'), 230 'GEMINI_BASE_URL': 'Gemini Base URL', 231 'KIRO_BASE_URL': t('modal.provider.field.baseUrl'), 232 'KIRO_REFRESH_URL': t('modal.provider.field.refreshUrl'), 233 'KIRO_REFRESH_IDC_URL': t('modal.provider.field.refreshIdcUrl'), 234 'QWEN_BASE_URL': 'Qwen Base URL', 235 'QWEN_OAUTH_BASE_URL': t('modal.provider.field.oauthBaseUrl'), 236 'ANTIGRAVITY_BASE_URL_DAILY': t('modal.provider.field.dailyBaseUrl'), 237 'ANTIGRAVITY_BASE_URL_AUTOPUSH': t('modal.provider.field.autopushBaseUrl'), 238 'IFLOW_BASE_URL': t('modal.provider.field.iflowBaseUrl'), 239 'CODEX_BASE_URL': t('modal.provider.field.codexBaseUrl'), 240 'GROK_BASE_URL': t('modal.provider.field.grokBaseUrl'), 241 'FORWARD_API_KEY': 'Forward API Key', 242 'FORWARD_BASE_URL': 'Forward Base URL', 243 'FORWARD_HEADER_NAME': t('modal.provider.field.headerName'), 244 'FORWARD_HEADER_VALUE_PREFIX': t('modal.provider.field.headerPrefix'), 245 'USE_SYSTEM_PROXY_FORWARD': t('modal.provider.field.useSystemProxy') 246 }; 247 248 return labelMap[key] || key; 249} 250 251/** 252 * è·åæä¾åç±»åçåæ®µé ç½® 253 * @param {string} providerType - æä¾åç±»å 254 * @returns {Array} åæ®µé ç½®æ°ç» 255 */ 256function getProviderTypeFields(providerType) { 257 // åºç¡é ç½®åæ®µå®ä¹ 258 const fieldConfigs = { 259 'openai-custom': [ 260 { 261 id: 'OPENAI_API_KEY', 262 label: t('modal.provider.field.apiKey'), 263 type: 'password', 264 placeholder: 'sk-...' 265 }, 266 { 267 id: 'OPENAI_BASE_URL', 268 label: 'OpenAI Base URL', 269 type: 'text', 270 placeholder: 'https://api.openai.com/v1' 271 } 272 ], 273 'atlascloud': [ 274 { 275 id: 'OPENAI_API_KEY', 276 label: t('modal.provider.field.apiKey'), 277 type: 'password', 278 placeholder: 'sk-...' 279 }, 280 { 281 id: 'OPENAI_BASE_URL', 282 label: 'OpenAI Base URL', 283 type: 'text', 284 placeholder: 'https://api.openai.com/v1' 285 } 286 ], 287 'openaiResponses-custom': [ 288 { 289 id: 'OPENAI_API_KEY', 290 label: t('modal.provider.field.apiKey'), 291 type: 'password', 292 placeholder: 'sk-...' 293 }, 294 { 295 id: 'OPENAI_BASE_URL', 296 label: 'OpenAI Base URL', 297 type: 'text', 298 placeholder: 'https://api.openai.com/v1' 299 } 300 ], 301 'claude-custom': [ 302 { 303 id: 'CLAUDE_API_KEY', 304 label: 'Claude API Key', 305 type: 'password', 306 placeholder: 'sk-ant-...' 307 }, 308 { 309 id: 'CLAUDE_BASE_URL', 310 label: 'Claude Base URL', 311 type: 'text', 312 placeholder: 'https://api.anthropic.com' 313 } 314 ], 315 'gemini-cli-oauth': [ 316 { 317 id: 'PROJECT_ID', 318 label: t('modal.provider.field.projectId'), 319 type: 'text', 320 placeholder: t('modal.provider.field.projectId.placeholder') 321 }, 322 { 323 id: 'GEMINI_OAUTH_CREDS_FILE_PATH', 324 label: t('modal.provider.field.oauthPath'),
325 type: 'text', 326 placeholder: t('modal.provider.field.oauthPath.gemini.placeholder') 327 }, 328 { 329 id: 'GEMINI_BASE_URL', 330 label: `Gemini Base URL <span class="optional-tag">${t('config.optional')}</span>`, 331 type: 'text', 332 placeholder: 'https://cloudcode-pa.googleapis.com' 333 } 334 ], 335 'claude-kiro-oauth': [ 336 { 337 id: 'KIRO_OAUTH_CREDS_FILE_PATH', 338 label: t('modal.provider.field.oauthPath'), 339 type: 'text', 340 placeholder: t('modal.provider.field.oauthPath.kiro.placeholder') 341 }, 342 { 343 id: 'KIRO_BASE_URL', 344 label: `${t('modal.provider.field.baseUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 345 type: 'text', 346 placeholder: 'https://q.{{region}}.amazonaws.com/generateAssistantResponse' 347 }, 348 { 349 id: 'KIRO_REFRESH_URL', 350 label: `${t('modal.provider.field.refreshUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 351 type: 'text', 352 placeholder: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken' 353 }, 354 { 355 id: 'KIRO_REFRESH_IDC_URL', 356 label: `${t('modal.provider.field.refreshIdcUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 357 type: 'text', 358 placeholder: 'https://oidc.{{region}}.amazonaws.com/token' 359 } 360 ], 361 'openai-qwen-oauth': [ 362 { 363 id: 'QWEN_OAUTH_CREDS_FILE_PATH', 364 label: t('modal.provider.field.oauthPath'), 365 type: 'text', 366 placeholder: t('modal.provider.field.oauthPath.qwen.placeholder') 367 }, 368 { 369 id: 'QWEN_BASE_URL', 370 label: `Qwen Base URL <span class="optional-tag">${t('config.optional')}</span>`, 371 type: 'text', 372 placeholder: 'https://portal.qwen.ai/v1' 373 }, 374 { 375 id: 'QWEN_OAUTH_BASE_URL', 376 label: `${t('modal.provider.field.oauthBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 377 type: 'text', 378 placeholder: 'https://chat.qwen.ai' 379 } 380 ], 381 'gemini-antigravity': [ 382 { 383 id: 'PROJECT_ID', 384 label: `${t('modal.provider.field.projectId')} <span class="optional-tag">${t('config.optional')}</span>`, 385 type: 'text', 386 placeholder: t('modal.provider.field.projectId.optional.placeholder') 387 }, 388 { 389 id: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 390 label: t('modal.provider.field.oauthPath'), 391 type: 'text', 392 placeholder: t('modal.provider.field.oauthPath.antigravity.placeholder') 393 }, 394 { 395 id: 'ANTIGRAVITY_BASE_URL_DAILY', 396 label: `${t('modal.provider.field.dailyBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 397 type: 'text', 398 placeholder: 'https://daily-cloudcode-pa.sandbox.googleapis.com' 399 }, 400 { 401 id: 'ANTIGRAVITY_BASE_URL_AUTOPUSH', 402 label: `${t('modal.provider.field.autopushBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 403 type: 'text', 404 placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com' 405 } 406 ], 407 'openai-iflow': [ 408 { 409 id: 'IFLOW_OAUTH_CREDS_FILE_PATH', 410 label: t('modal.provider.field.oauthPath'), 411 type: 'text', 412 placeholder: t('modal.provider.field.oauthPath.iflow.placeholder') 413 }, 414 { 415 id: 'IFLOW_BASE_URL', 416 label: `iFlow Base URL <span class="optional-tag">${t('config.optional')}</span>`, 417 type: 'text', 418 placeholder: 'https://iflow.cn/api' 419 } 420 ], 421 'openai-codex-oauth': [ 422 { 423 id: 'CODEX_OAUTH_CREDS_FILE_PATH', 424 label: t('modal.provider.field.oauthPath'),
425 type: 'text', 426 placeholder: t('modal.provider.field.oauthPath.codex.placeholder') 427 }, 428 { 429 id: 'CODEX_EMAIL', 430 label: `${t('modal.provider.field.email')} <span class="optional-tag">${t('config.optional')}</span>`, 431 type: 'email', 432 placeholder: t('modal.provider.field.email.placeholder') 433 }, 434 { 435 id: 'CODEX_BASE_URL', 436 label: `${t('modal.provider.field.codexBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 437 type: 'text', 438 placeholder: 'https://api.openai.com/v1/codex' 439 } 440 ], 441 'grok-cli-oauth': [ 442 { 443 id: 'GROK_CLI_OAUTH_CREDS_FILE_PATH', 444 label: t('modal.provider.field.oauthPath'), 445 type: 'text', 446 placeholder: 'configs/grok-cli/..._xai-..._oauth_creds.json' 447 }, 448 { 449 id: 'GROK_CLI_EMAIL', 450 label: `${t('modal.provider.field.email')} <span class="optional-tag">${t('config.optional')}</span>`, 451 type: 'email', 452 placeholder: t('modal.provider.field.email.placeholder') 453 }, 454 { 455 id: 'GROK_CLI_BASE_URL', 456 label: `xAI Base URL <span class="optional-tag">${t('config.optional')}</span>`, 457 type: 'text', 458 placeholder: 'https://api.x.ai/v1' 459 } 460 ], 461 'grok-web': [ 462 { 463 id: 'GROK_COOKIE_TOKEN', 464 label: t('modal.provider.field.ssoToken'), 465 type: 'password', 466 placeholder: 'sso cookie token' 467 }, 468 { 469 id: 'GROK_BASE_URL', 470 label: `${t('modal.provider.field.grokBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`, 471 type: 'text', 472 placeholder: 'https://grok.com' 473 }, 474 { 475 id: 'GROK_CF_CLEARANCE', 476 label: `${t('modal.provider.field.cfClearance')} <span class="optional-tag">${t('config.optional')}</span>`, 477 type: 'text', 478 placeholder: 'cf_clearance cookie value' 479 }, 480 { 481 id: 'GROK_CF_BM', 482 label: `${t('modal.provider.field.cfBm')} <span class="optional-tag">${t('config.optional')}</span>`, 483 type: 'text', 484 placeholder: '__cf_bm cookie value' 485 }, 486 { 487 id: 'GROK_USER_AGENT', 488 label: `${t('modal.provider.field.userAgent')} <span class="optional-tag">${t('config.optional')}</span>`, 489 type: 'text', 490 placeholder: 'Mozilla/5.0 ...' 491 }, 492 { 493 id: 'GROK_STATSIG_ID', 494 label: `${t('modal.provider.field.statsigId')} <span class="optional-tag">${t('config.optional')}</span>`, 495 type: 'text', 496 placeholder: 'x-statsig-id header value' 497 } 498 ], 499 'forward-api': [ 500 { 501 id: 'FORWARD_API_KEY', 502 label: t('modal.provider.field.apiKey'), 503 type: 'password', 504 placeholder: t('modal.provider.field.apiKey.placeholder') 505 }, 506 { 507 id: 'FORWARD_BASE_URL', 508 label: t('modal.provider.field.baseUrl'), 509 type: 'text', 510 placeholder: 'https://api.example.com' 511 }, 512 { 513 id: 'FORWARD_HEADER_NAME', 514 label: `${t('modal.provider.field.headerName')} <span class="optional-tag">${t('config.optional')}</span>`, 515 type: 'text', 516 placeholder: 'Authorization' 517 }, 518 { 519 id: 'FORWARD_HEADER_VALUE_PREFIX', 520 label: `${t('modal.provider.field.headerPrefix')} <span class="optional-tag">${t('config.optional')}</span>`, 521 type: 'text', 522 placeholder: 'Bearer ' 523 } 524 ] 525 }; 526 527 // 1. å°è¯ç²¾ç¡®å¹é 528 if (fieldConfigs[providerType]) { 529 return fieldConfigs[providerType]; 530 } 531 532 // 2. å°è¯å¹é åç¼ (ä¾å¦ openai-custom-test -> openai-custom) 533 for (const baseType in fieldConfigs) { 534 if (providerType.startsWith(baseType + '-')) { 535 return fieldConfigs[baseType]; 536 } 537 } 538 539 return []; 540} 541 542/** 543 * è°è¯å½æ°ï¼è·åå½åæä¾åç»è®¡ä¿¡æ¯ 544 * @param {Object} providerStats - æä¾åç»è®¡å¯¹è±¡ 545 * @returns {Object} æ©å±çç»è®¡ä¿¡æ¯ 546 */ 547function getProviderStats(providerStats) { 548 return { 549 ...providerStats, 550 // æ·»å 计ç®å¾åºçç»è®¡ä¿¡æ¯ 551 successRate: providerStats.totalRequests > 0 ? 552 ((providerStats.totalRequests - providerStats.totalErrors) / providerStats.totalRequests * 100).toFixed(2) + '%' : '0%', 553 avgUsagePerProvider: providerStats.activeProviders > 0 ? 554 Math.round(providerStats.totalRequests / providerStats.activeProviders) : 0, 555 healthRatio: providerStats.totalAccounts > 0 ? 556 (providerStats.healthyProviders / providerStats.totalAccounts * 100).toFixed(2) + '%' : '0%' 557 }; 558} 559 560/** 561 * éç¨ API 请æ±å½æ° 562 * @param {string} url - API ç«¯ç¹ URL 563 * @param {Object} options - fetch é项 564 * @returns {Promise<any>} ååºæ°æ® 565 */ 566async function apiRequest(url, options = {}) { 567 // 妿 URL 以 /api å¼å¤´ï¼å»æå®ï¼å 为 apiClient.request ä¼èªå¨æ·»å ï¼ 568 const endpoint = url.startsWith('/api') ? url.slice(4) : url; 569 return apiClient.request(endpoint, options); 570} 571 572/**
573 * å¤å¶ææ¬å°åªè´´æ¿ï¼å¸¦å ¼å®¹æ§åéï¼ 574 * @param {string} text - è¦å¤å¶çææ¬ 575 * @returns {Promise<boolean>} æ¯å¦æå 576 */ 577async function copyToClipboard(text) { 578 if (navigator.clipboard && navigator.clipboard.writeText) { 579 try { 580 await navigator.clipboard.writeText(text); 581 return true; 582 } catch (err) { 583 console.warn('navigator.clipboard failed, trying fallback:', err); 584 } 585 } 586 587 // Fallback: ä½¿ç¨ textarea 模æå¤å¶ 588 try { 589 const textArea = document.createElement('textarea'); 590 textArea.value = text; 591 textArea.style.position = 'fixed'; 592 textArea.style.left = '-9999px'; 593 textArea.style.top = '0'; 594 document.body.appendChild(textArea); 595 textArea.focus(); 596 textArea.select(); 597 const successful = document.execCommand('copy'); 598 document.body.removeChild(textArea); 599 return successful; 600 } catch (err) { 601 console.error('Fallback copy failed:', err); 602 return false; 603 } 604} 605 606/** 607 * åªä¸ºå ç´ ç»å®ä¸æ¬¡äºä»¶ 608 * @param {HTMLElement|null} element - éè¦ç»å®äºä»¶çå ç´ 609 * @param {string} eventName - äºä»¶åç§° 610 * @param {Function} handler - äºä»¶å¤ç彿° 611 * @param {string} key - ç»å®æ è¯ï¼é¿å åä¸å ç´ éå¤ç»å®åä¸ç±»äºä»¶ 612 * @returns {boolean} æ¯å¦å®æäºæ¬æ¬¡ç»å® 613 */ 614function bindOnce(element, eventName, handler, key = eventName) { 615 if (!element) { 616 return false; 617 } 618 619 if (!markOnce(element, key)) { 620 return false; 621 } 622 623 element.addEventListener(eventName, handler); 624 return true; 625} 626 627/** 628 * 为å ç´ è®°å½ä¸æ¬¡æ§åå§åç¶æ 629 * @param {HTMLElement|null} element - éè¦æ è®°çå ç´ 630 * @param {string} key - æ è¯åç§° 631 * @returns {boolean} æ¯å¦æ¯é¦æ¬¡æ è®° 632 */ 633function markOnce(element, key) { 634 if (!element) { 635 return false; 636 } 637 638 const boundKey = `bound${key.charAt(0).toUpperCase()}${key.slice(1)}`; 639 if (element.dataset[boundKey]) { 640 return false; 641 } 642 643 element.dataset[boundKey] = 'true'; 644 return true; 645} 646 647// å¯¼åºææå·¥å ·å½æ° 648export { 649 formatUptime, 650 escapeHtml, 651 showToast, 652 getFieldLabel, 653 getProviderTypeFields, 654 getProviderConfigs, 655 getBaseProviderConfigs, 656 getProviderStats, 657 apiRequest, 658 copyToClipboard, 659 bindOnce, 660 markOnce 661};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.