Spaces:
Paused
Paused
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title data-i18n="title.login">CLI Proxy API Management Center</title> | |
| <style> | |
| /* 全局样式 */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| /* CSS变量主题系统 */ | |
| :root { | |
| /* 布局尺寸 */ | |
| --navbar-height: 69px; | |
| /* 亮色主题(默认) */ | |
| --bg-primary: #f5f7fa; | |
| --bg-secondary: #ffffff; | |
| --bg-tertiary: #f8f9fb; | |
| --bg-quaternary: #f7fafc; | |
| --bg-modal: rgba(0, 0, 0, 0.5); | |
| /* 侧边栏颜色 */ | |
| --sidebar-bg: #ffffff; | |
| --sidebar-hover: #f0f2f5; | |
| --sidebar-active: #e8f4ff; | |
| --sidebar-border: #e5e7eb; | |
| --text-primary: #1f2937; | |
| --text-secondary: #4b5563; | |
| --text-tertiary: #6b7280; | |
| --text-quaternary: #9ca3af; | |
| --text-inverse: white; | |
| --border-primary: #e5e7eb; | |
| --border-secondary: #d1d5db; | |
| --border-focus: #3b82f6; | |
| --accent-primary: #3b82f6; | |
| --accent-secondary: #e5e7eb; | |
| --accent-tertiary: #f3f4f6; | |
| --primary-color: #3b82f6; | |
| --primary-hover: #2563eb; | |
| --card-bg: #ffffff; | |
| --border-color: #e5e7eb; | |
| --success-bg: #d1fae5; | |
| --success-text: #065f46; | |
| --success-border: #6ee7b7; | |
| --error-bg: #fee2e2; | |
| --error-text: #991b1b; | |
| --error-border: #fca5a5; | |
| --warning-bg: #fef3c7; | |
| --warning-text: #92400e; | |
| --warning-border: #fbbf24; | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| --shadow-primary: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); | |
| --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| --shadow-secondary: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| --shadow-modal: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| --glass-bg: rgba(255, 255, 255, 0.95); | |
| --glass-border: rgba(229, 231, 235, 0.8); | |
| } | |
| /* 暗色主题 */ | |
| [data-theme="dark"] { | |
| --bg-primary: #111827; | |
| --bg-secondary: #1f2937; | |
| --bg-tertiary: #1a202c; | |
| --bg-quaternary: #2d3748; | |
| --bg-modal: rgba(0, 0, 0, 0.7); | |
| /* 侧边栏颜色 */ | |
| --sidebar-bg: #1f2937; | |
| --sidebar-hover: #374151; | |
| --sidebar-active: #1e3a5f; | |
| --sidebar-border: #374151; | |
| --text-primary: #f9fafb; | |
| --text-secondary: #e5e7eb; | |
| --text-tertiary: #d1d5db; | |
| --text-quaternary: #9ca3af; | |
| --text-inverse: #ffffff; | |
| --border-primary: #374151; | |
| --border-secondary: #4b5563; | |
| --border-focus: #60a5fa; | |
| --accent-primary: #3b82f6; | |
| --accent-secondary: #374151; | |
| --accent-tertiary: #1f2937; | |
| --primary-color: #60a5fa; | |
| --primary-hover: #3b82f6; | |
| --card-bg: #1f2937; | |
| --border-color: #374151; | |
| --success-bg: #064e3b; | |
| --success-text: #6ee7b7; | |
| --success-border: #059669; | |
| --error-bg: #7f1d1d; | |
| --error-text: #fca5a5; | |
| --error-border: #dc2626; | |
| --warning-bg: #78350f; | |
| --warning-text: #fbbf24; | |
| --warning-border: #f59e0b; | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); | |
| --shadow-primary: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); | |
| --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); | |
| --shadow-secondary: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); | |
| --shadow-modal: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.3); | |
| --glass-bg: rgba(31, 41, 59, 0.95); | |
| --glass-border: rgba(75, 85, 99, 0.8); | |
| } | |
| /* 登录页面样式 */ | |
| .login-container { | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-primary); | |
| padding: 20px; | |
| } | |
| /* 自动登录加载页面样式 */ | |
| .auto-login-content { | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .loading-spinner { | |
| margin: 0 auto 20px; | |
| width: 60px; | |
| height: 60px; | |
| } | |
| .spinner { | |
| width: 100%; | |
| height: 100%; | |
| border: 4px solid #e5e7eb; | |
| border-top: 4px solid #3b82f6; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { | |
| transform: rotate(0deg); | |
| } | |
| 100% { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .auto-login-content h2 { | |
| color: var(--text-secondary); | |
| margin-bottom: 10px; | |
| font-size: 24px; | |
| font-weight: 600; | |
| } | |
| .auto-login-content p { | |
| color: var(--text-tertiary); | |
| font-size: 16px; | |
| line-height: 1.5; | |
| } | |
| /* 登录页面头部布局 */ | |
| .login-header-top { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| position: relative; | |
| } | |
| .login-title { | |
| width: 100%; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| /* 头部控制按钮组 */ | |
| .header-controls { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| height: 100%; | |
| } | |
| /* 登录页面的控制按钮组样式 */ | |
| .login-header-top .header-controls { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 12px; | |
| padding: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); | |
| transition: all 0.3s ease; | |
| margin-top: 8px; | |
| /* 与标题拉开距离,避免遮挡 */ | |
| margin-bottom: 20px; | |
| } | |
| .login-header-top .header-controls:hover { | |
| background: rgba(255, 255, 255, 0.15); | |
| border-color: rgba(255, 255, 255, 0.3); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); | |
| transform: translateY(-1px); | |
| } | |
| [data-theme="dark"] .login-header-top .header-controls { | |
| background: rgba(30, 41, 59, 0.8); | |
| border: 1px solid rgba(100, 116, 139, 0.3); | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); | |
| } | |
| [data-theme="dark"] .login-header-top .header-controls:hover { | |
| background: rgba(30, 41, 59, 0.9); | |
| border-color: rgba(100, 116, 139, 0.5); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); | |
| } | |
| .language-switcher, | |
| .theme-switcher { | |
| position: relative; | |
| flex-shrink: 0; | |
| display: flex; | |
| align-items: center; | |
| height: 100%; | |
| } | |
| .language-btn, | |
| .theme-btn { | |
| padding: 8px 14px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| border-radius: 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-tertiary); | |
| transition: all 0.3s ease; | |
| white-space: nowrap; | |
| backdrop-filter: blur(5px); | |
| min-height: 36px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| } | |
| .top-navbar .language-btn, | |
| .top-navbar .theme-btn { | |
| padding: 6px 12px; | |
| min-height: 36px; | |
| } | |
| .top-navbar-actions .btn, | |
| .top-navbar .language-btn, | |
| .top-navbar .theme-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| height: 36px; | |
| } | |
| /* 登录页面的按钮样式优化 */ | |
| .login-header-top .language-btn, | |
| .login-header-top .theme-btn { | |
| padding: 8px 12px; | |
| font-size: 13px; | |
| height: 36px; | |
| min-width: 36px; | |
| border-radius: 8px; | |
| background: rgba(255, 255, 255, 0.9); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| color: #64748b; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| .login-header-top .language-btn:hover, | |
| .login-header-top .theme-btn:hover { | |
| background: rgba(255, 255, 255, 1); | |
| border-color: rgba(100, 116, 139, 0.3); | |
| color: #475569; | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| } | |
| [data-theme="dark"] .login-header-top .language-btn, | |
| [data-theme="dark"] .login-header-top .theme-btn { | |
| background: rgba(30, 41, 59, 0.9); | |
| border: 1px solid rgba(100, 116, 139, 0.3); | |
| color: #94a3b8; | |
| } | |
| [data-theme="dark"] .login-header-top .language-btn:hover, | |
| [data-theme="dark"] .login-header-top .theme-btn:hover { | |
| background: rgba(51, 65, 85, 0.95); | |
| border-color: rgba(100, 116, 139, 0.5); | |
| color: #cbd5e1; | |
| } | |
| .language-btn:hover, | |
| .theme-btn:hover { | |
| background: var(--accent-secondary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-secondary); | |
| } | |
| .language-btn i, | |
| .theme-btn i { | |
| margin-right: 6px; | |
| } | |
| .theme-btn.active { | |
| background: var(--accent-primary); | |
| color: var(--text-inverse); | |
| } | |
| .login-card { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 40px; | |
| width: 100%; | |
| max-width: 500px; | |
| box-shadow: var(--shadow-secondary); | |
| border: 1px solid var(--glass-border); | |
| } | |
| .login-header { | |
| text-align: center; | |
| margin-bottom: 25px; | |
| } | |
| .login-title { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 15px; | |
| color: var(--text-secondary); | |
| font-size: 1.8rem; | |
| font-weight: 600; | |
| margin-bottom: 0; | |
| line-height: 1.2; | |
| } | |
| #login-logo { | |
| height: 60px; | |
| width: auto; | |
| object-fit: contain; | |
| border-radius: 6px; | |
| padding: 2px; | |
| background: #fff; | |
| border: 1px solid rgba(15, 23, 42, 0.06); | |
| box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); | |
| } | |
| .login-subtitle { | |
| color: #64748b; | |
| font-size: 1rem; | |
| margin: 0; | |
| text-align: center; | |
| } | |
| .login-body { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| } | |
| .login-connection-info { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 16px; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| [data-theme="dark"] .login-connection-info { | |
| background: rgba(30, 41, 59, 0.7); | |
| border-color: rgba(100, 116, 139, 0.3); | |
| } | |
| .connection-summary { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| color: var(--text-secondary); | |
| } | |
| .connection-summary i { | |
| font-size: 24px; | |
| color: var(--primary-color); | |
| } | |
| .connection-url { | |
| font-size: 16px; | |
| color: var(--text-secondary); | |
| } | |
| .connection-url-separator { | |
| margin: 0 8px; | |
| color: var(--text-tertiary); | |
| } | |
| #login-connection-url { | |
| font-family: "Fira Code", "Consolas", "Courier New", monospace; | |
| color: var(--text-primary); | |
| word-break: break-all; | |
| } | |
| [data-theme="dark"] #login-connection-url { | |
| color: var(--text-secondary); | |
| } | |
| .login-form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .login-form .form-group { | |
| margin-bottom: 25px; | |
| } | |
| .login-form .form-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| } | |
| .login-form .form-hint { | |
| margin-top: 6px; | |
| color: #64748b; | |
| font-size: 12px; | |
| } | |
| .login-form .input-group { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .login-form input[type="text"], | |
| .login-form input[type="password"] { | |
| flex: 1; | |
| padding: 14px 16px; | |
| border: 2px solid var(--border-primary); | |
| border-radius: 10px; | |
| font-size: 15px; | |
| transition: all 0.3s ease; | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| .login-form input:focus { | |
| outline: none; | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px var(--border-primary); | |
| } | |
| .login-form .btn-secondary { | |
| padding: 14px 16px; | |
| border: 2px solid #e2e8f0; | |
| background: #f8fafc; | |
| color: #64748b; | |
| border-radius: 10px; | |
| transition: all 0.3s ease; | |
| } | |
| .login-form .btn-secondary:hover { | |
| background: #e2e8f0; | |
| border-color: #cbd5e0; | |
| } | |
| .form-actions { | |
| margin-top: 30px; | |
| text-align: center; | |
| } | |
| .login-btn { | |
| width: 100%; | |
| padding: 16px 24px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| background: linear-gradient(135deg, #475569, #334155); | |
| color: white; | |
| border: none; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| } | |
| .login-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(51, 65, 85, 0.4); | |
| } | |
| .login-btn:disabled { | |
| opacity: 0.7; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .login-error { | |
| background: var(--error-bg); | |
| border: 1px solid var(--error-border); | |
| color: var(--error-text); | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| margin-top: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .login-error i { | |
| color: var(--error-text); | |
| } | |
| /* 响应式设计 - 登录页面 */ | |
| @media (max-width: 640px) { | |
| .login-card { | |
| padding: 30px 20px; | |
| margin: 10px; | |
| max-width: 100%; | |
| } | |
| .login-header-top { | |
| flex-direction: column; | |
| gap: 20px; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| } | |
| .header-controls { | |
| flex-direction: row; | |
| justify-content: center; | |
| gap: 8px; | |
| margin-bottom: 10px; | |
| /* 在小屏幕上给控制按钮组添加下边距 */ | |
| } | |
| /* 登录页面小屏幕优化 */ | |
| .login-header-top .header-controls { | |
| margin: 8px auto 0 auto; | |
| /* 顶部留白,避免与标题拥挤 */ | |
| background: rgba(255, 255, 255, 0.08); | |
| padding: 6px; | |
| border-radius: 10px; | |
| } | |
| .login-header-top .language-btn, | |
| .login-header-top .theme-btn { | |
| padding: 6px 10px; | |
| font-size: 12px; | |
| height: 32px; | |
| min-width: 32px; | |
| } | |
| .language-btn, | |
| .theme-btn { | |
| padding: 8px 16px; | |
| font-size: 13px; | |
| height: 36px; | |
| /* 在小屏幕上稍微减小高度 */ | |
| } | |
| .login-title { | |
| font-size: 1.5rem; | |
| flex-direction: column; | |
| gap: 10px; | |
| text-align: center; | |
| justify-content: center; | |
| margin-bottom: 25px; | |
| } | |
| #login-logo { | |
| height: 50px; | |
| } | |
| .login-form .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .login-form .btn-secondary { | |
| width: 100%; | |
| margin-top: 10px; | |
| } | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; | |
| background: var(--bg-primary); | |
| min-height: 100vh; | |
| color: var(--text-primary); | |
| transition: background-color 0.3s ease, color 0.3s ease; | |
| overflow-x: hidden; | |
| } | |
| /* 侧边栏样式 */ | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 240px 1fr; | |
| min-height: calc(100vh - var(--navbar-height, 69px)); | |
| width: 100%; | |
| transition: grid-template-columns 0.3s ease; | |
| } | |
| .layout.sidebar-collapsed { | |
| grid-template-columns: 64px 1fr; | |
| } | |
| .sidebar { | |
| width: 240px; | |
| background: var(--sidebar-bg); | |
| border-right: 1px solid var(--sidebar-border); | |
| height: calc(100vh - var(--navbar-height, 69px)); | |
| position: sticky; | |
| top: var(--navbar-height, 69px); | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| z-index: 5; | |
| box-shadow: var(--shadow-sm); | |
| transition: all 0.3s ease; | |
| } | |
| .sidebar.collapsed { | |
| width: 64px; | |
| } | |
| .sidebar.collapsed .nav-item span { | |
| opacity: 0; | |
| width: 0; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| } | |
| .sidebar.collapsed .nav-item { | |
| justify-content: center; | |
| padding: 10px 0; | |
| } | |
| .sidebar.collapsed .nav-item i { | |
| margin-right: 0; | |
| } | |
| /* 侧边栏切换按钮(桌面端-在顶栏) */ | |
| .sidebar-toggle-btn-desktop { | |
| display: none; | |
| width: 40px; | |
| height: 40px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-primary); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| transition: all 0.2s ease; | |
| padding: 0; | |
| } | |
| .sidebar-toggle-btn-desktop:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--primary-color); | |
| } | |
| .sidebar-toggle-btn-desktop i { | |
| transition: transform 0.3s ease; | |
| } | |
| .layout.sidebar-collapsed .sidebar-toggle-btn-desktop i { | |
| transform: rotate(90deg); | |
| } | |
| /* 在大屏幕上显示桌面端切换按钮 */ | |
| @media (min-width: 1025px) { | |
| .sidebar-toggle-btn-desktop { | |
| display: flex; | |
| } | |
| } | |
| /* 侧边栏品牌区域 */ | |
| .sidebar-brand { | |
| padding: 24px 20px; | |
| border-bottom: 1px solid var(--sidebar-border); | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| background: var(--sidebar-bg); | |
| } | |
| .sidebar-brand-logo { | |
| width: 36px; | |
| height: 36px; | |
| object-fit: contain; | |
| border-radius: 8px; | |
| } | |
| .sidebar-brand-text { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| line-height: 1.2; | |
| } | |
| /* 侧边栏导航菜单 */ | |
| .sidebar .nav-menu { | |
| flex: 1; | |
| padding: 16px 12px; | |
| list-style: none; | |
| } | |
| .sidebar .nav-menu li { | |
| margin-bottom: 4px; | |
| position: relative; | |
| } | |
| .sidebar .nav-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 10px 14px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| border-radius: 8px; | |
| transition: all 0.2s ease; | |
| font-size: 14px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .sidebar .nav-item:hover { | |
| background: var(--sidebar-hover); | |
| color: var(--text-primary); | |
| } | |
| .sidebar .nav-item.active { | |
| background: var(--sidebar-active); | |
| color: var(--primary-color); | |
| } | |
| .sidebar .nav-item i { | |
| margin-right: 12px; | |
| width: 18px; | |
| font-size: 16px; | |
| text-align: center; | |
| transition: margin 0.3s ease; | |
| } | |
| /* 收起状态时的工具提示 */ | |
| .sidebar.collapsed .nav-menu li:hover::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| left: 68px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| white-space: nowrap; | |
| box-shadow: var(--shadow-md); | |
| border: 1px solid var(--border-primary); | |
| z-index: 1000; | |
| pointer-events: none; | |
| opacity: 0; | |
| animation: tooltipFadeIn 0.2s ease forwards; | |
| } | |
| @keyframes tooltipFadeIn { | |
| to { | |
| opacity: 1; | |
| } | |
| } | |
| /* 主内容区域包装器 */ | |
| .main-wrapper { | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-primary); | |
| min-height: 100%; | |
| } | |
| /* 顶部导航栏 */ | |
| .top-navbar { | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border-primary); | |
| padding: 16px 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| z-index: 200; | |
| box-shadow: var(--shadow-sm); | |
| height: var(--navbar-height, 69px); | |
| min-height: var(--navbar-height, 69px); | |
| box-sizing: border-box; | |
| } | |
| .top-navbar-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| flex: 1; | |
| } | |
| .top-navbar-brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| max-width: max-content; | |
| } | |
| .top-navbar-brand-logo { | |
| width: 32px; | |
| height: 32px; | |
| object-fit: contain; | |
| border-radius: 8px; | |
| } | |
| .top-navbar-brand-text { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .top-navbar-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-left: auto; | |
| } | |
| .top-navbar-actions>* { | |
| display: inline-flex; | |
| align-items: center; | |
| height: 36px; | |
| } | |
| .top-navbar .header-controls { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| height: 100%; | |
| } | |
| .top-navbar .language-btn, | |
| .top-navbar .theme-btn { | |
| width: 36px; | |
| height: 36px; | |
| min-height: 36px; | |
| padding: 0; | |
| justify-content: center; | |
| } | |
| .top-navbar-actions .btn { | |
| height: 36px; | |
| min-height: 36px; | |
| } | |
| @media (max-width: 768px) { | |
| .top-navbar-actions { | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| gap: 8px; | |
| } | |
| .top-navbar .header-controls { | |
| width: 100%; | |
| justify-content: flex-end; | |
| order: 99; | |
| gap: 8px; | |
| } | |
| .top-navbar-actions>* { | |
| height: 34px; | |
| min-height: 34px; | |
| } | |
| .top-navbar .language-btn, | |
| .top-navbar .theme-btn { | |
| width: 34px; | |
| height: 34px; | |
| } | |
| .top-navbar-actions .btn { | |
| height: 34px; | |
| min-height: 34px; | |
| padding: 0 10px; | |
| } | |
| .top-navbar-actions .btn span { | |
| display: none; | |
| } | |
| } | |
| .top-navbar .language-btn i, | |
| .top-navbar .theme-btn i { | |
| margin: 0; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .header-actions .btn { | |
| white-space: nowrap; | |
| } | |
| @media (max-width: 768px) { | |
| .header-actions .btn span { | |
| display: none; | |
| } | |
| .header-actions .btn { | |
| padding: 6px 10px; | |
| } | |
| } | |
| /* 连接信息样式 */ | |
| .connection-info { | |
| display: grid; | |
| gap: 16px; | |
| } | |
| .info-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 14px 0; | |
| border-bottom: 1px solid var(--border-primary); | |
| } | |
| .info-item:last-child { | |
| border-bottom: none; | |
| padding-bottom: 0; | |
| } | |
| .info-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| font-size: 14px; | |
| } | |
| .info-label i { | |
| color: var(--text-tertiary); | |
| width: 18px; | |
| font-size: 16px; | |
| } | |
| .info-value { | |
| color: var(--text-secondary); | |
| font-family: 'Monaco', 'Menlo', 'Consolas', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| border: 1px solid var(--border-primary); | |
| max-width: 350px; | |
| word-break: break-all; | |
| } | |
| .status-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 14px; | |
| border-radius: 16px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| } | |
| .status-indicator.connected { | |
| background: var(--success-bg); | |
| color: var(--success-text); | |
| } | |
| .status-indicator.disconnected { | |
| background: var(--error-bg); | |
| color: var(--error-text); | |
| } | |
| .status-indicator.connecting { | |
| background: var(--warning-bg); | |
| color: var(--warning-text); | |
| } | |
| /* 响应式设计 - 连接信息 */ | |
| @media (max-width: 768px) { | |
| .info-item { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 8px; | |
| } | |
| .info-value { | |
| max-width: 100%; | |
| width: 100%; | |
| } | |
| } | |
| /* 主内容区域 */ | |
| .main-content { | |
| flex: 1; | |
| padding: 24px 32px; | |
| max-width: 1400px; | |
| width: 100%; | |
| margin: 0 auto; | |
| } | |
| /* 内容区域 */ | |
| .content-area { | |
| flex: 1; | |
| width: 100%; | |
| } | |
| .content-section { | |
| display: none; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .content-section.active { | |
| display: block; | |
| } | |
| .content-section h2 { | |
| color: var(--text-primary); | |
| margin-bottom: 24px; | |
| font-size: 24px; | |
| font-weight: 600; | |
| } | |
| /* 卡片样式 */ | |
| .card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| box-shadow: var(--shadow-sm); | |
| overflow: hidden; | |
| transition: box-shadow 0.2s ease; | |
| } | |
| .card:hover { | |
| box-shadow: var(--shadow-md); | |
| } | |
| .card-header { | |
| background: var(--bg-secondary); | |
| padding: 18px 24px; | |
| border-bottom: 1px solid var(--border-primary); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .card-header h3 { | |
| color: var(--text-primary); | |
| font-size: 16px; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .card-header h3 i { | |
| color: var(--text-tertiary); | |
| margin-right: 10px; | |
| font-size: 18px; | |
| } | |
| .card-content { | |
| padding: 24px; | |
| } | |
| /* 按钮样式 */ | |
| .btn { | |
| padding: 8px 16px; | |
| border: 1px solid transparent; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 500; | |
| transition: all 0.2s ease; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| text-decoration: none; | |
| min-height: 36px; | |
| white-space: nowrap; | |
| outline: none; | |
| } | |
| .btn-primary { | |
| background: var(--primary-color); | |
| color: white; | |
| border-color: var(--primary-color); | |
| } | |
| .btn-primary:hover { | |
| background: var(--primary-hover); | |
| border-color: var(--primary-hover); | |
| box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3); | |
| } | |
| .btn-secondary { | |
| background: var(--bg-secondary); | |
| color: var(--text-secondary); | |
| border-color: var(--border-primary); | |
| } | |
| .btn-secondary:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-primary); | |
| } | |
| .btn-danger { | |
| background: #ef4444; | |
| color: white; | |
| border-color: #ef4444; | |
| } | |
| .btn-danger:hover { | |
| background: #dc2626; | |
| border-color: #dc2626; | |
| box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3); | |
| } | |
| .btn-success { | |
| background: #10b981; | |
| color: white; | |
| border-color: #10b981; | |
| } | |
| .btn-success:hover { | |
| background: #059669; | |
| border-color: #059669; | |
| box-shadow: 0 2px 6px rgba(16, 185, 129, 0.3); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| .btn i { | |
| font-size: 14px; | |
| } | |
| /* 表单元素 */ | |
| .form-group { | |
| margin-bottom: 18px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| font-size: 14px; | |
| } | |
| .form-hint { | |
| font-size: 13px; | |
| color: var(--text-tertiary); | |
| margin: 6px 0 0 0; | |
| line-height: 1.5; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| input[type="text"], | |
| input[type="password"], | |
| input[type="number"], | |
| input[type="url"], | |
| textarea, | |
| select { | |
| flex: 1; | |
| padding: 10px 14px; | |
| border: 1px solid var(--border-primary); | |
| border-radius: 6px; | |
| font-size: 14px; | |
| transition: all 0.2s ease; | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| } | |
| input:hover, | |
| textarea:hover, | |
| select:hover { | |
| border-color: var(--border-secondary); | |
| } | |
| input:focus, | |
| textarea:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| input::placeholder, | |
| textarea::placeholder { | |
| color: var(--text-quaternary); | |
| } | |
| /* 切换开关 */ | |
| .toggle-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 16px; | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 44px; | |
| height: 24px; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #d1d5db; | |
| transition: .3s; | |
| border-radius: 24px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 18px; | |
| width: 18px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: white; | |
| transition: .3s; | |
| border-radius: 50%; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); | |
| } | |
| input:checked+.slider { | |
| background: var(--primary-color); | |
| } | |
| input:checked+.slider:before { | |
| transform: translateX(20px); | |
| } | |
| .toggle-label { | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| font-size: 14px; | |
| } | |
| /* 列表样式 */ | |
| .key-list, | |
| .provider-list, | |
| .file-list { | |
| max-height: 400px; | |
| overflow-y: auto; | |
| } | |
| .key-item, | |
| .provider-item, | |
| .file-item { | |
| background: var(--bg-quaternary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-bottom: 10px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| transition: all 0.3s ease; | |
| } | |
| .key-item:hover, | |
| .provider-item:hover, | |
| .file-item:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| transform: translateY(-1px); | |
| } | |
| .item-content { | |
| flex: 1; | |
| } | |
| .item-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .item-title { | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| margin-bottom: 4px; | |
| } | |
| .item-subtitle { | |
| color: var(--text-tertiary); | |
| font-size: 0.9rem; | |
| } | |
| .provider-item .provider-models { | |
| margin-top: 8px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| } | |
| .provider-model-tag { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| background: var(--bg-quinary); | |
| color: var(--text-secondary); | |
| border: 1px solid var(--border-secondary); | |
| border-radius: 14px; | |
| padding: 4px 10px; | |
| margin-right: 6px; | |
| margin-top: 6px; | |
| font-size: 0.85rem; | |
| } | |
| .provider-model-tag .model-name { | |
| font-weight: 600; | |
| } | |
| .provider-model-tag .model-alias { | |
| color: var(--text-tertiary); | |
| font-style: italic; | |
| } | |
| .item-value { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| background: var(--bg-tertiary); | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| word-break: break-all; | |
| } | |
| /* 状态信息 */ | |
| .status-info { | |
| background: var(--bg-quaternary); | |
| border-radius: 8px; | |
| padding: 20px; | |
| } | |
| .status-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid var(--border-primary); | |
| } | |
| .status-item:last-child { | |
| margin-bottom: 0; | |
| padding-bottom: 0; | |
| border-bottom: none; | |
| } | |
| .status-label { | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| } | |
| .status-value { | |
| color: var(--text-tertiary); | |
| } | |
| /* 模态框 */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1000; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: var(--bg-modal); | |
| backdrop-filter: blur(5px); | |
| } | |
| .modal-content { | |
| background-color: var(--bg-secondary); | |
| margin: 4% auto; | |
| padding: 0; | |
| border-radius: 15px; | |
| width: 90%; | |
| max-width: 550px; | |
| box-shadow: var(--shadow-modal); | |
| animation: modalSlideIn 0.3s ease; | |
| position: relative; | |
| } | |
| @keyframes modalSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .close { | |
| color: var(--text-tertiary); | |
| position: absolute; | |
| top: 15px; | |
| right: 20px; | |
| font-size: 28px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| z-index: 1001; | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 50%; | |
| transition: all 0.3s ease; | |
| } | |
| .close:hover, | |
| .close:focus { | |
| color: var(--text-secondary); | |
| background-color: var(--bg-tertiary); | |
| } | |
| #modal-body { | |
| padding: 35px 30px 30px 30px; | |
| } | |
| /* 模态框标题样式 */ | |
| #modal-body h3 { | |
| color: var(--text-secondary); | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| margin: 0 0 20px 0; | |
| text-align: center; | |
| border-bottom: 2px solid var(--border-primary); | |
| padding-bottom: 12px; | |
| } | |
| /* 模态框表单组 */ | |
| #modal-body .form-group { | |
| margin-bottom: 16px; | |
| } | |
| #modal-body .form-group label { | |
| display: block; | |
| margin-bottom: 6px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| #modal-body .form-group input, | |
| #modal-body .form-group textarea { | |
| width: 100%; | |
| padding: 12px 16px; | |
| border: 2px solid var(--border-primary); | |
| border-radius: 8px; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| } | |
| #modal-body .form-group input:focus, | |
| #modal-body .form-group textarea:focus { | |
| outline: none; | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px var(--border-primary); | |
| } | |
| #modal-body .form-group textarea { | |
| resize: vertical; | |
| min-height: 80px; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| } | |
| /* 模态框按钮组 */ | |
| #modal-body .modal-actions { | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 12px; | |
| margin-top: 24px; | |
| padding-top: 18px; | |
| border-top: 1px solid var(--border-primary); | |
| } | |
| #modal-body .modal-actions .btn { | |
| min-width: 80px; | |
| padding: 10px 20px; | |
| } | |
| /* 通知 */ | |
| .notification { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 15px 20px; | |
| border-radius: 8px; | |
| color: white; | |
| font-weight: 500; | |
| z-index: 1001; | |
| transform: translateX(400px); | |
| transition: all 0.3s ease; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); | |
| } | |
| .notification.show { | |
| transform: translateX(0); | |
| } | |
| .notification.success { | |
| background: linear-gradient(135deg, #68d391, #38a169); | |
| } | |
| .notification.error { | |
| background: linear-gradient(135deg, #fc8181, #e53e3e); | |
| } | |
| .notification.info { | |
| background: linear-gradient(135deg, #63b3ed, #3182ce); | |
| } | |
| /* 响应式设计 */ | |
| @media (max-width: 1024px) { | |
| .layout { | |
| position: relative; | |
| grid-template-columns: 1fr; | |
| min-height: calc(100vh - var(--navbar-height, 69px)); | |
| } | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: var(--navbar-height, 69px); | |
| height: calc(100vh - var(--navbar-height, 69px)); | |
| transform: translateX(-100%); | |
| z-index: 150; | |
| box-shadow: var(--shadow-sm); | |
| background: var(--sidebar-bg); | |
| width: 240px ; | |
| } | |
| .sidebar.mobile-open { | |
| transform: translateX(0); | |
| box-shadow: var(--shadow-secondary); | |
| } | |
| /* 移动端强制恢复侧栏展开状态 */ | |
| .sidebar.collapsed { | |
| width: 240px ; | |
| } | |
| .sidebar.collapsed .nav-item span { | |
| opacity: 1 ; | |
| width: auto ; | |
| overflow: visible ; | |
| } | |
| .sidebar.collapsed .nav-item { | |
| justify-content: flex-start ; | |
| padding: 10px 14px ; | |
| } | |
| .sidebar.collapsed .nav-item i { | |
| margin-right: 12px ; | |
| } | |
| .main-wrapper { | |
| margin-left: 0; | |
| width: 100%; | |
| min-height: calc(100vh - var(--navbar-height, 69px)); | |
| overflow: hidden; | |
| } | |
| .main-content { | |
| padding: 20px 16px; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .top-navbar { | |
| padding: 12px 16px; | |
| } | |
| .top-navbar-title { | |
| font-size: 18px; | |
| } | |
| .top-navbar-actions { | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| } | |
| .top-navbar-actions>* { | |
| height: 34px; | |
| min-height: 34px; | |
| } | |
| .top-navbar .header-controls { | |
| height: 34px; | |
| gap: 6px; | |
| } | |
| .top-navbar-actions .btn, | |
| .top-navbar .language-btn, | |
| .top-navbar .theme-btn { | |
| height: 34px; | |
| min-height: 34px; | |
| padding: 0 10px; | |
| } | |
| .top-navbar-actions .btn span { | |
| display: none; | |
| } | |
| .btn { | |
| padding: 6px 12px; | |
| font-size: 13px; | |
| } | |
| .card { | |
| border-radius: 8px; | |
| } | |
| .card-header { | |
| padding: 16px 20px; | |
| } | |
| .card-content { | |
| padding: 20px; | |
| } | |
| .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .card-header { | |
| flex-direction: column; | |
| gap: 12px; | |
| align-items: flex-start; | |
| } | |
| .card-header .header-actions { | |
| width: 100%; | |
| justify-content: flex-start; | |
| } | |
| /* 模态框响应式 */ | |
| .modal-content { | |
| margin: 5% auto; | |
| width: 95%; | |
| max-width: none; | |
| } | |
| #modal-body { | |
| padding: 40px 20px 20px 20px; | |
| } | |
| #modal-body h3 { | |
| font-size: 18px; | |
| margin-bottom: 16px; | |
| } | |
| #modal-body .modal-actions { | |
| flex-direction: column-reverse; | |
| gap: 10px; | |
| } | |
| #modal-body .modal-actions .btn { | |
| width: 100%; | |
| margin: 0; | |
| } | |
| .content-section h2 { | |
| font-size: 20px; | |
| margin-bottom: 20px; | |
| } | |
| } | |
| /* 移动端侧边栏遮罩 */ | |
| .sidebar-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| z-index: 120; | |
| } | |
| .sidebar-overlay.active { | |
| display: block; | |
| } | |
| /* 移动端菜单按钮 */ | |
| .mobile-menu-btn { | |
| display: none; | |
| width: 40px; | |
| height: 40px; | |
| background: transparent; | |
| border: none; | |
| padding: 0; | |
| cursor: pointer; | |
| color: var(--text-primary); | |
| font-size: 20px; | |
| border-radius: 8px; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| } | |
| .mobile-menu-btn:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--primary-color); | |
| } | |
| @media (max-width: 1024px) { | |
| .mobile-menu-btn { | |
| display: flex; | |
| } | |
| } | |
| /* 加载动画 */ | |
| .loading { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: #fff; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| /* 空状态 */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: var(--text-tertiary); | |
| } | |
| .empty-state i { | |
| font-size: 48px; | |
| margin-bottom: 16px; | |
| color: var(--border-secondary); | |
| } | |
| .empty-state h3 { | |
| margin-bottom: 8px; | |
| color: var(--text-secondary); | |
| } | |
| /* 滚动条样式 */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-tertiary); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border-secondary); | |
| border-radius: 4px; | |
| transition: background 0.2s ease; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-quaternary); | |
| } | |
| /* 侧边栏滚动条 */ | |
| .sidebar::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .sidebar::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .sidebar::-webkit-scrollbar-thumb { | |
| background: rgba(0, 0, 0, 0.1); | |
| border-radius: 3px; | |
| } | |
| .sidebar::-webkit-scrollbar-thumb:hover { | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| [data-theme="dark"] .sidebar::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| [data-theme="dark"] .sidebar::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| /* 连接状态指示器 */ | |
| .connection-indicator { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| } | |
| .connection-indicator.connected { | |
| background-color: #68d391; | |
| box-shadow: 0 0 8px rgba(104, 211, 145, 0.6); | |
| } | |
| .connection-indicator.disconnected { | |
| background-color: #fc8181; | |
| box-shadow: 0 0 8px rgba(252, 129, 129, 0.6); | |
| } | |
| .connection-indicator.connecting { | |
| background-color: #fbb040; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| 100% { | |
| opacity: 1; | |
| } | |
| } | |
| /* Gemini Web Token 模态框样式 */ | |
| .gemini-web-form .form-group { | |
| margin-bottom: 20px; | |
| } | |
| .gemini-web-form .form-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| .gemini-web-form .form-group input { | |
| width: 100%; | |
| padding: 12px 16px; | |
| border: 2px solid var(--border-primary); | |
| border-radius: 8px; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| } | |
| .gemini-web-form .form-group input:focus { | |
| outline: none; | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px var(--border-primary); | |
| } | |
| .gemini-web-form .form-hint { | |
| margin-top: 6px; | |
| color: var(--text-tertiary); | |
| font-size: 12px; | |
| line-height: 1.4; | |
| } | |
| /* 使用统计样式 */ | |
| .stats-overview { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: var(--card-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 24px; | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .stat-card:hover { | |
| border-color: var(--border-primary); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| transform: translateY(-2px); | |
| } | |
| .stat-icon { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--primary-color); | |
| color: white; | |
| font-size: 20px; | |
| flex-shrink: 0; | |
| } | |
| .stat-icon.success { | |
| background: #10b981; | |
| } | |
| .stat-icon.error { | |
| background: #ef4444; | |
| } | |
| .stat-content { | |
| flex: 1; | |
| } | |
| .stat-number { | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| line-height: 1; | |
| margin-bottom: 4px; | |
| } | |
| .stat-label { | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .charts-container { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| @media (max-width: 1200px) { | |
| .charts-container { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .chart-card { | |
| min-height: 400px; | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 300px; | |
| width: 100%; | |
| } | |
| .chart-controls { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .btn.btn-small { | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| border-radius: 6px; | |
| border: 1px solid var(--border-color); | |
| background: var(--card-bg); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .btn.btn-small:hover { | |
| border-color: var(--border-primary); | |
| color: var(--text-primary); | |
| } | |
| .btn.btn-small.active { | |
| background: var(--primary-color); | |
| border-color: var(--primary-color); | |
| color: white; | |
| } | |
| .api-stats-table { | |
| overflow-x: auto; | |
| } | |
| .stats-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 16px; | |
| } | |
| .stats-table th, | |
| .stats-table td { | |
| padding: 12px 16px; | |
| text-align: left; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .stats-table th { | |
| background: var(--bg-secondary); | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| } | |
| .stats-table td { | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| } | |
| .stats-table tr:hover { | |
| background: var(--bg-secondary); | |
| } | |
| .model-details { | |
| margin-top: 8px; | |
| padding: 8px 12px; | |
| background: var(--bg-tertiary); | |
| border-radius: 6px; | |
| font-size: 12px; | |
| } | |
| .model-item { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 4px; | |
| color: var(--text-tertiary); | |
| } | |
| .model-name { | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| } | |
| .loading-placeholder { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100px; | |
| color: var(--text-tertiary); | |
| font-size: 14px; | |
| } | |
| .no-data-message { | |
| text-align: center; | |
| color: var(--text-tertiary); | |
| font-style: italic; | |
| padding: 40px; | |
| } | |
| /* 暗色主题适配 */ | |
| [data-theme="dark"] .stat-card { | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); | |
| } | |
| [data-theme="dark"] .stat-card:hover { | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); | |
| } | |
| [data-theme="dark"] .btn.btn-small { | |
| background: var(--bg-tertiary); | |
| } | |
| /* 版本信息样式 */ | |
| .version-footer { | |
| margin-top: 40px; | |
| padding: 24px 0; | |
| border-top: 1px solid var(--border-primary); | |
| } | |
| .version-info { | |
| text-align: center; | |
| font-size: 13px; | |
| color: var(--text-tertiary); | |
| } | |
| .version-info .separator { | |
| margin: 0 12px; | |
| color: var(--text-quaternary); | |
| } | |
| .connection-reset-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| white-space: nowrap; | |
| } | |
| .connection-reset-btn i { | |
| margin: 0; | |
| } | |
| .connection-reset-btn span { | |
| font-size: 13px; | |
| } | |
| [data-theme="dark"] .connection-reset-btn { | |
| background: rgba(30, 41, 59, 0.9); | |
| border-color: rgba(100, 116, 139, 0.4); | |
| color: #cbd5e1; | |
| } | |
| [data-theme="dark"] .connection-reset-btn:hover { | |
| background: rgba(51, 65, 85, 0.95); | |
| border-color: rgba(100, 116, 139, 0.6); | |
| } | |
| .model-input-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .model-input-row .input-group { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .model-input-row .model-name-input, | |
| .model-input-row .model-alias-input { | |
| flex: 1; | |
| } | |
| .model-input-row .model-alias-input { | |
| max-width: 220px; | |
| } | |
| .model-input-row .model-remove-btn { | |
| align-self: center; | |
| } | |
| /* Codex OAuth 样式 */ | |
| #codex-oauth-content { | |
| transition: all 0.3s ease; | |
| } | |
| #codex-oauth-url { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #codex-oauth-url:focus { | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| #codex-oauth-status { | |
| font-weight: 500; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| transition: all 0.3s ease; | |
| } | |
| #codex-oauth-status.success { | |
| background: var(--success-bg); | |
| border-color: var(--success-border); | |
| color: var(--success-text); | |
| } | |
| #codex-oauth-status.error { | |
| background: var(--error-bg); | |
| border-color: var(--error-border); | |
| color: var(--error-text); | |
| } | |
| #codex-oauth-status.warning { | |
| background: var(--warning-bg); | |
| border-color: var(--warning-border); | |
| color: var(--warning-text); | |
| } | |
| /* Codex OAuth 按钮样式 */ | |
| #codex-open-link, | |
| #codex-copy-link { | |
| min-width: 100px; | |
| white-space: nowrap; | |
| } | |
| #codex-open-link { | |
| background: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| #codex-open-link:hover { | |
| background: var(--primary-hover); | |
| border-color: var(--primary-hover); | |
| } | |
| #codex-copy-link { | |
| background: var(--bg-secondary); | |
| border-color: var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #codex-copy-link:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-primary); | |
| } | |
| /* 响应式设计 - Codex OAuth */ | |
| @media (max-width: 768px) { | |
| #codex-oauth-content .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| #codex-open-link, | |
| #codex-copy-link { | |
| width: 100%; | |
| margin-top: 8px; | |
| } | |
| #codex-oauth-url { | |
| font-size: 12px; | |
| } | |
| } | |
| /* Anthropic OAuth 样式 */ | |
| #anthropic-oauth-content { | |
| transition: all 0.3s ease; | |
| } | |
| #anthropic-oauth-url { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #anthropic-oauth-url:focus { | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| #anthropic-oauth-status { | |
| font-weight: 500; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| transition: all 0.3s ease; | |
| } | |
| #anthropic-oauth-status.success { | |
| background: var(--success-bg); | |
| border-color: var(--success-border); | |
| color: var(--success-text); | |
| } | |
| #anthropic-oauth-status.error { | |
| background: var(--error-bg); | |
| border-color: var(--error-border); | |
| color: var(--error-text); | |
| } | |
| #anthropic-oauth-status.warning { | |
| background: var(--warning-bg); | |
| border-color: var(--warning-border); | |
| color: var(--warning-text); | |
| } | |
| /* Anthropic OAuth 按钮样式 */ | |
| #anthropic-open-link, | |
| #anthropic-copy-link { | |
| min-width: 100px; | |
| white-space: nowrap; | |
| } | |
| #anthropic-open-link { | |
| background: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| #anthropic-open-link:hover { | |
| background: var(--primary-hover); | |
| border-color: var(--primary-hover); | |
| } | |
| #anthropic-copy-link { | |
| background: var(--bg-secondary); | |
| border-color: var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #anthropic-copy-link:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-primary); | |
| } | |
| /* 响应式设计 - Anthropic OAuth */ | |
| @media (max-width: 768px) { | |
| #anthropic-oauth-content .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| #anthropic-open-link, | |
| #anthropic-copy-link { | |
| width: 100%; | |
| margin-top: 8px; | |
| } | |
| #anthropic-oauth-url { | |
| font-size: 12px; | |
| } | |
| } | |
| /* Gemini CLI OAuth 样式 */ | |
| #gemini-cli-oauth-content { | |
| transition: all 0.3s ease; | |
| } | |
| #gemini-cli-oauth-url { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #gemini-cli-oauth-url:focus { | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| #gemini-cli-oauth-status { | |
| font-weight: 500; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| transition: all 0.3s ease; | |
| } | |
| #gemini-cli-oauth-status.success { | |
| background: var(--success-bg); | |
| border-color: var(--success-border); | |
| color: var(--success-text); | |
| } | |
| #gemini-cli-oauth-status.error { | |
| background: var(--error-bg); | |
| border-color: var(--error-border); | |
| color: var(--error-text); | |
| } | |
| #gemini-cli-oauth-status.warning { | |
| background: var(--warning-bg); | |
| border-color: var(--warning-border); | |
| color: var(--warning-text); | |
| } | |
| /* Gemini CLI OAuth 按钮样式 */ | |
| #gemini-cli-open-link, | |
| #gemini-cli-copy-link { | |
| min-width: 100px; | |
| white-space: nowrap; | |
| } | |
| #gemini-cli-open-link { | |
| background: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| #gemini-cli-open-link:hover { | |
| background: var(--primary-hover); | |
| border-color: var(--primary-hover); | |
| } | |
| #gemini-cli-copy-link { | |
| background: var(--bg-secondary); | |
| border-color: var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #gemini-cli-copy-link:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-primary); | |
| } | |
| /* Gemini CLI 项目 ID 输入框样式 */ | |
| #gemini-cli-project-id { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #gemini-cli-project-id:focus { | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| /* 响应式设计 - Gemini CLI OAuth */ | |
| @media (max-width: 768px) { | |
| #gemini-cli-oauth-content .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| #gemini-cli-open-link, | |
| #gemini-cli-copy-link { | |
| width: 100%; | |
| margin-top: 8px; | |
| } | |
| #gemini-cli-oauth-url { | |
| font-size: 12px; | |
| } | |
| #gemini-cli-project-id { | |
| font-size: 12px; | |
| } | |
| } | |
| /* Qwen OAuth 样式 */ | |
| #qwen-oauth-content { | |
| transition: all 0.3s ease; | |
| } | |
| #qwen-oauth-url { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #qwen-oauth-url:focus { | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| #qwen-oauth-status { | |
| font-weight: 500; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| transition: all 0.3s ease; | |
| } | |
| #qwen-oauth-status.success { | |
| background: var(--success-bg); | |
| border-color: var(--success-border); | |
| color: var(--success-text); | |
| } | |
| #qwen-oauth-status.error { | |
| background: var(--error-bg); | |
| border-color: var(--error-border); | |
| color: var(--error-text); | |
| } | |
| #qwen-oauth-status.warning { | |
| background: var(--warning-bg); | |
| border-color: var(--warning-border); | |
| color: var(--warning-text); | |
| } | |
| /* Qwen OAuth 按钮样式 */ | |
| #qwen-open-link, | |
| #qwen-copy-link { | |
| min-width: 100px; | |
| white-space: nowrap; | |
| } | |
| #qwen-open-link { | |
| background: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| #qwen-open-link:hover { | |
| background: var(--primary-hover); | |
| border-color: var(--primary-hover); | |
| } | |
| #qwen-copy-link { | |
| background: var(--bg-secondary); | |
| border-color: var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #qwen-copy-link:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-primary); | |
| } | |
| /* 响应式设计 - Qwen OAuth */ | |
| @media (max-width: 768px) { | |
| #qwen-oauth-content .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| #qwen-open-link, | |
| #qwen-copy-link { | |
| width: 100%; | |
| margin-top: 8px; | |
| } | |
| #qwen-oauth-url { | |
| font-size: 12px; | |
| } | |
| } | |
| /* iFlow OAuth 样式 */ | |
| #iflow-oauth-content { | |
| transition: all 0.3s ease; | |
| } | |
| #iflow-oauth-url { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #iflow-oauth-url:focus { | |
| border-color: var(--border-focus); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| #iflow-oauth-status { | |
| font-weight: 500; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| transition: all 0.3s ease; | |
| } | |
| #iflow-oauth-status.success { | |
| background: var(--success-bg); | |
| border-color: var(--success-border); | |
| color: var(--success-text); | |
| } | |
| #iflow-oauth-status.error { | |
| background: var(--error-bg); | |
| border-color: var(--error-border); | |
| color: var(--error-text); | |
| } | |
| #iflow-oauth-status.warning { | |
| background: var(--warning-bg); | |
| border-color: var(--warning-border); | |
| color: var(--warning-text); | |
| } | |
| /* iFlow OAuth 按钮样式 */ | |
| #iflow-open-link, | |
| #iflow-copy-link { | |
| min-width: 100px; | |
| white-space: nowrap; | |
| } | |
| #iflow-open-link { | |
| background: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| #iflow-open-link:hover { | |
| background: var(--primary-hover); | |
| border-color: var(--primary-hover); | |
| } | |
| #iflow-copy-link { | |
| background: var(--bg-secondary); | |
| border-color: var(--border-primary); | |
| color: var(--text-secondary); | |
| } | |
| #iflow-copy-link:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--border-secondary); | |
| color: var(--text-primary); | |
| } | |
| /* 响应式设计 - iFlow OAuth */ | |
| @media (max-width: 768px) { | |
| #iflow-oauth-content .input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| #iflow-open-link, | |
| #iflow-copy-link { | |
| width: 100%; | |
| margin-top: 8px; | |
| } | |
| #iflow-oauth-url { | |
| font-size: 12px; | |
| } | |
| } | |
| </style> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script> | |
| // 国际化语言包 | |
| const i18n = { | |
| // 语言配置 | |
| currentLanguage: 'zh-CN', | |
| fallbackLanguage: 'zh-CN', | |
| // 语言包 | |
| translations: { | |
| 'zh-CN': { | |
| // 通用 | |
| 'common.login': '登录', | |
| 'common.logout': '登出', | |
| 'common.cancel': '取消', | |
| 'common.confirm': '确认', | |
| 'common.save': '保存', | |
| 'common.delete': '删除', | |
| 'common.edit': '编辑', | |
| 'common.add': '添加', | |
| 'common.update': '更新', | |
| 'common.refresh': '刷新', | |
| 'common.close': '关闭', | |
| 'common.success': '成功', | |
| 'common.error': '错误', | |
| 'common.info': '信息', | |
| 'common.warning': '警告', | |
| 'common.loading': '加载中...', | |
| 'common.connecting': '连接中...', | |
| 'common.connected': '已连接', | |
| 'common.disconnected': '未连接', | |
| 'common.connecting_status': '连接中', | |
| 'common.connected_status': '已连接', | |
| 'common.disconnected_status': '未连接', | |
| 'common.yes': '是', | |
| 'common.no': '否', | |
| 'common.optional': '可选', | |
| 'common.required': '必填', | |
| 'common.api_key': '密钥', | |
| 'common.base_url': '地址', | |
| 'common.proxy_url': '代理', | |
| 'common.alias': '别名', | |
| // 页面标题 | |
| 'title.main': 'CLI Proxy API Management Center', | |
| 'title.login': 'CLI Proxy API Management Center', | |
| // 自动登录 | |
| 'auto_login.title': '正在自动登录...', | |
| 'auto_login.message': '正在使用本地保存的连接信息尝试连接服务器', | |
| // 登录页面 | |
| 'login.subtitle': '请输入连接信息以访问管理界面', | |
| 'login.connection_title': '连接地址', | |
| 'login.connection_current': '当前地址', | |
| 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接', | |
| 'login.custom_connection_label': '自定义连接地址:', | |
| 'login.custom_connection_placeholder': '例如: https://example.com:8317', | |
| 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。', | |
| 'login.use_current_address': '使用当前地址', | |
| 'login.management_key_label': '管理密钥:', | |
| 'login.management_key_placeholder': '请输入管理密钥', | |
| 'login.connect_button': '连接', | |
| 'login.submit_button': '登录', | |
| 'login.submitting': '连接中...', | |
| 'login.error_title': '登录失败', | |
| 'login.error_required': '请填写完整的连接信息', | |
| 'login.error_invalid': '连接失败,请检查地址和密钥', | |
| // 头部导航 | |
| 'header.check_connection': '检查连接', | |
| 'header.refresh_all': '刷新全部', | |
| 'header.logout': '登出', | |
| // 连接信息 | |
| 'connection.title': '连接信息', | |
| 'connection.server_address': '服务器地址:', | |
| 'connection.management_key': '管理密钥:', | |
| 'connection.status': '连接状态:', | |
| // 侧边栏导航 | |
| 'nav.basic_settings': '基础设置', | |
| 'nav.api_keys': 'API 密钥', | |
| 'nav.ai_providers': 'AI 提供商', | |
| 'nav.auth_files': '认证文件', | |
| 'nav.usage_stats': '使用统计', | |
| 'nav.system_info': '系统信息', | |
| // 基础设置 | |
| 'basic_settings.title': '基础设置', | |
| 'basic_settings.debug_title': '调试模式', | |
| 'basic_settings.debug_enable': '启用调试模式', | |
| 'basic_settings.proxy_title': '代理设置', | |
| 'basic_settings.proxy_url_label': '代理 URL:', | |
| 'basic_settings.proxy_url_placeholder': '例如: socks5://user:pass@127.0.0.1:1080/', | |
| 'basic_settings.proxy_update': '更新', | |
| 'basic_settings.proxy_clear': '清空', | |
| 'basic_settings.retry_title': '请求重试', | |
| 'basic_settings.retry_count_label': '重试次数:', | |
| 'basic_settings.retry_update': '更新', | |
| 'basic_settings.quota_title': '配额超出行为', | |
| 'basic_settings.quota_switch_project': '自动切换项目', | |
| 'basic_settings.quota_switch_preview': '切换到预览模型', | |
| // API 密钥管理 | |
| 'api_keys.title': 'API 密钥管理', | |
| 'api_keys.proxy_auth_title': '代理服务认证密钥', | |
| 'api_keys.add_button': '添加密钥', | |
| 'api_keys.empty_title': '暂无API密钥', | |
| 'api_keys.empty_desc': '点击上方按钮添加第一个密钥', | |
| 'api_keys.item_title': 'API密钥', | |
| 'api_keys.add_modal_title': '添加API密钥', | |
| 'api_keys.add_modal_key_label': 'API密钥:', | |
| 'api_keys.add_modal_key_placeholder': '请输入API密钥', | |
| 'api_keys.edit_modal_title': '编辑API密钥', | |
| 'api_keys.edit_modal_key_label': 'API密钥:', | |
| 'api_keys.delete_confirm': '确定要删除这个API密钥吗?', | |
| // AI 提供商 | |
| 'ai_providers.title': 'AI 提供商配置', | |
| 'ai_providers.gemini_title': 'Gemini API 密钥', | |
| 'ai_providers.gemini_add_button': '添加密钥', | |
| 'ai_providers.gemini_empty_title': '暂无Gemini密钥', | |
| 'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥', | |
| 'ai_providers.gemini_item_title': 'Gemini密钥', | |
| 'ai_providers.gemini_add_modal_title': '添加Gemini API密钥', | |
| 'ai_providers.gemini_add_modal_key_label': 'API密钥:', | |
| 'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥', | |
| 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥', | |
| 'ai_providers.gemini_edit_modal_key_label': 'API密钥:', | |
| 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?', | |
| 'ai_providers.codex_title': 'Codex API 配置', | |
| 'ai_providers.codex_add_button': '添加配置', | |
| 'ai_providers.codex_empty_title': '暂无Codex配置', | |
| 'ai_providers.codex_empty_desc': '点击上方按钮添加第一个配置', | |
| 'ai_providers.codex_item_title': 'Codex配置', | |
| 'ai_providers.codex_add_modal_title': '添加Codex API配置', | |
| 'ai_providers.codex_add_modal_key_label': 'API密钥:', | |
| 'ai_providers.codex_add_modal_key_placeholder': '请输入Codex API密钥', | |
| 'ai_providers.codex_add_modal_url_label': 'Base URL (可选):', | |
| 'ai_providers.codex_add_modal_url_placeholder': '例如: https://api.example.com', | |
| 'ai_providers.codex_add_modal_proxy_label': '代理 URL (可选):', | |
| 'ai_providers.codex_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080', | |
| 'ai_providers.codex_edit_modal_title': '编辑Codex API配置', | |
| 'ai_providers.codex_edit_modal_key_label': 'API密钥:', | |
| 'ai_providers.codex_edit_modal_url_label': 'Base URL (可选):', | |
| 'ai_providers.codex_edit_modal_proxy_label': '代理 URL (可选):', | |
| 'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗?', | |
| 'ai_providers.claude_title': 'Claude API 配置', | |
| 'ai_providers.claude_add_button': '添加配置', | |
| 'ai_providers.claude_empty_title': '暂无Claude配置', | |
| 'ai_providers.claude_empty_desc': '点击上方按钮添加第一个配置', | |
| 'ai_providers.claude_item_title': 'Claude配置', | |
| 'ai_providers.claude_add_modal_title': '添加Claude API配置', | |
| 'ai_providers.claude_add_modal_key_label': 'API密钥:', | |
| 'ai_providers.claude_add_modal_key_placeholder': '请输入Claude API密钥', | |
| 'ai_providers.claude_add_modal_url_label': 'Base URL (可选):', | |
| 'ai_providers.claude_add_modal_url_placeholder': '例如: https://api.anthropic.com', | |
| 'ai_providers.claude_add_modal_proxy_label': '代理 URL (可选):', | |
| 'ai_providers.claude_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080', | |
| 'ai_providers.claude_edit_modal_title': '编辑Claude API配置', | |
| 'ai_providers.claude_edit_modal_key_label': 'API密钥:', | |
| 'ai_providers.claude_edit_modal_url_label': 'Base URL (可选):', | |
| 'ai_providers.claude_edit_modal_proxy_label': '代理 URL (可选):', | |
| 'ai_providers.claude_delete_confirm': '确定要删除这个Claude配置吗?', | |
| 'ai_providers.openai_title': 'OpenAI 兼容提供商', | |
| 'ai_providers.openai_add_button': '添加提供商', | |
| 'ai_providers.openai_empty_title': '暂无OpenAI兼容提供商', | |
| 'ai_providers.openai_empty_desc': '点击上方按钮添加第一个提供商', | |
| 'ai_providers.openai_add_modal_title': '添加OpenAI兼容提供商', | |
| 'ai_providers.openai_add_modal_name_label': '提供商名称:', | |
| 'ai_providers.openai_add_modal_name_placeholder': '例如: openrouter', | |
| 'ai_providers.openai_add_modal_url_label': 'Base URL:', | |
| 'ai_providers.openai_add_modal_url_placeholder': '例如: https://openrouter.ai/api/v1', | |
| 'ai_providers.openai_add_modal_keys_label': 'API密钥 (每行一个):', | |
| 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', | |
| 'ai_providers.openai_add_modal_keys_proxy_label': '代理 URL (按行对应,可选):', | |
| 'ai_providers.openai_add_modal_keys_proxy_placeholder': 'socks5://proxy.example.com:1080\n', | |
| 'ai_providers.openai_add_modal_models_label': '模型列表 (name[, alias] 每行一个):', | |
| 'ai_providers.openai_models_hint': '示例:gpt-4o-mini 或 moonshotai/kimi-k2:free, kimi-k2', | |
| 'ai_providers.openai_model_name_placeholder': '模型名称,如 moonshotai/kimi-k2:free', | |
| 'ai_providers.openai_model_alias_placeholder': '模型别名 (可选)', | |
| 'ai_providers.openai_models_add_btn': '添加模型', | |
| 'ai_providers.openai_edit_modal_title': '编辑OpenAI兼容提供商', | |
| 'ai_providers.openai_edit_modal_name_label': '提供商名称:', | |
| 'ai_providers.openai_edit_modal_url_label': 'Base URL:', | |
| 'ai_providers.openai_edit_modal_keys_label': 'API密钥 (每行一个):', | |
| 'ai_providers.openai_edit_modal_keys_proxy_label': '代理 URL (按行对应,可选):', | |
| 'ai_providers.openai_edit_modal_models_label': '模型列表 (name[, alias] 每行一个):', | |
| 'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?', | |
| 'ai_providers.openai_keys_count': '密钥数量', | |
| 'ai_providers.openai_models_count': '模型数量', | |
| // 认证文件管理 | |
| 'auth_files.title': '认证文件管理', | |
| 'auth_files.title_section': '认证文件', | |
| 'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。', | |
| 'auth_files.upload_button': '上传文件', | |
| 'auth_files.delete_all_button': '删除全部', | |
| 'auth_files.empty_title': '暂无认证文件', | |
| 'auth_files.empty_desc': '点击上方按钮上传第一个文件', | |
| 'auth_files.file_size': '大小', | |
| 'auth_files.file_modified': '修改时间', | |
| 'auth_files.download_button': '下载', | |
| 'auth_files.delete_button': '删除', | |
| 'auth_files.delete_confirm': '确定要删除文件', | |
| 'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!', | |
| 'auth_files.upload_error_json': '只能上传JSON文件', | |
| 'auth_files.upload_success': '文件上传成功', | |
| 'auth_files.download_success': '文件下载成功', | |
| 'auth_files.delete_success': '文件删除成功', | |
| 'auth_files.delete_all_success': '成功删除', | |
| 'auth_files.files_count': '个文件', | |
| // Gemini Web Token | |
| 'auth_login.gemini_web_title': 'Gemini Web Token', | |
| 'auth_login.gemini_web_button': '保存 Gemini Web Token', | |
| 'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。', | |
| 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:', | |
| 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值', | |
| 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', | |
| 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值', | |
| 'auth_login.gemini_web_label_label': '标签 (可选):', | |
| 'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)', | |
| 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功', | |
| // Codex OAuth | |
| 'auth_login.codex_oauth_title': 'Codex OAuth', | |
| 'auth_login.codex_oauth_button': '开始 Codex 登录', | |
| 'auth_login.codex_oauth_hint': '通过 OAuth 流程登录 Codex 服务,自动获取并保存认证文件。', | |
| 'auth_login.codex_oauth_url_label': '授权链接:', | |
| 'auth_login.codex_open_link': '打开链接', | |
| 'auth_login.codex_copy_link': '复制链接', | |
| 'auth_login.codex_oauth_status_waiting': '等待认证中...', | |
| 'auth_login.codex_oauth_status_success': '认证成功!', | |
| 'auth_login.codex_oauth_status_error': '认证失败:', | |
| 'auth_login.codex_oauth_start_error': '启动 Codex OAuth 失败:', | |
| 'auth_login.codex_oauth_polling_error': '检查认证状态失败:', | |
| // Anthropic OAuth | |
| 'auth_login.anthropic_oauth_title': 'Anthropic OAuth', | |
| 'auth_login.anthropic_oauth_button': '开始 Anthropic 登录', | |
| 'auth_login.anthropic_oauth_hint': '通过 OAuth 流程登录 Anthropic (Claude) 服务,自动获取并保存认证文件。', | |
| 'auth_login.anthropic_oauth_url_label': '授权链接:', | |
| 'auth_login.anthropic_open_link': '打开链接', | |
| 'auth_login.anthropic_copy_link': '复制链接', | |
| 'auth_login.anthropic_oauth_status_waiting': '等待认证中...', | |
| 'auth_login.anthropic_oauth_status_success': '认证成功!', | |
| 'auth_login.anthropic_oauth_status_error': '认证失败:', | |
| 'auth_login.anthropic_oauth_start_error': '启动 Anthropic OAuth 失败:', | |
| 'auth_login.anthropic_oauth_polling_error': '检查认证状态失败:', | |
| // Gemini CLI OAuth | |
| 'auth_login.gemini_cli_oauth_title': 'Gemini CLI OAuth', | |
| 'auth_login.gemini_cli_oauth_button': '开始 Gemini CLI 登录', | |
| 'auth_login.gemini_cli_oauth_hint': '通过 OAuth 流程登录 Google Gemini CLI 服务,自动获取并保存认证文件。', | |
| 'auth_login.gemini_cli_project_id_label': 'Google Cloud 项目 ID (可选):', | |
| 'auth_login.gemini_cli_project_id_placeholder': '输入 Google Cloud 项目 ID (可选)', | |
| 'auth_login.gemini_cli_project_id_hint': '如果指定了项目 ID,将使用该项目的认证信息。', | |
| 'auth_login.gemini_cli_oauth_url_label': '授权链接:', | |
| 'auth_login.gemini_cli_open_link': '打开链接', | |
| 'auth_login.gemini_cli_copy_link': '复制链接', | |
| 'auth_login.gemini_cli_oauth_status_waiting': '等待认证中...', | |
| 'auth_login.gemini_cli_oauth_status_success': '认证成功!', | |
| 'auth_login.gemini_cli_oauth_status_error': '认证失败:', | |
| 'auth_login.gemini_cli_oauth_start_error': '启动 Gemini CLI OAuth 失败:', | |
| 'auth_login.gemini_cli_oauth_polling_error': '检查认证状态失败:', | |
| // Qwen OAuth | |
| 'auth_login.qwen_oauth_title': 'Qwen OAuth', | |
| 'auth_login.qwen_oauth_button': '开始 Qwen 登录', | |
| 'auth_login.qwen_oauth_hint': '通过设备授权流程登录 Qwen 服务,自动获取并保存认证文件。', | |
| 'auth_login.qwen_oauth_url_label': '授权链接:', | |
| 'auth_login.qwen_open_link': '打开链接', | |
| 'auth_login.qwen_copy_link': '复制链接', | |
| 'auth_login.qwen_oauth_status_waiting': '等待认证中...', | |
| 'auth_login.qwen_oauth_status_success': '认证成功!', | |
| 'auth_login.qwen_oauth_status_error': '认证失败:', | |
| 'auth_login.qwen_oauth_start_error': '启动 Qwen OAuth 失败:', | |
| 'auth_login.qwen_oauth_polling_error': '检查认证状态失败:', | |
| // iFlow OAuth | |
| 'auth_login.iflow_oauth_title': 'iFlow OAuth', | |
| 'auth_login.iflow_oauth_button': '开始 iFlow 登录', | |
| 'auth_login.iflow_oauth_hint': '通过 OAuth 流程登录 iFlow 服务,自动获取并保存认证文件。', | |
| 'auth_login.iflow_oauth_url_label': '授权链接:', | |
| 'auth_login.iflow_open_link': '打开链接', | |
| 'auth_login.iflow_copy_link': '复制链接', | |
| 'auth_login.iflow_oauth_status_waiting': '等待认证中...', | |
| 'auth_login.iflow_oauth_status_success': '认证成功!', | |
| 'auth_login.iflow_oauth_status_error': '认证失败:', | |
| 'auth_login.iflow_oauth_start_error': '启动 iFlow OAuth 失败:', | |
| 'auth_login.iflow_oauth_polling_error': '检查认证状态失败:', | |
| // 使用统计 | |
| 'usage_stats.title': '使用统计', | |
| 'usage_stats.total_requests': '总请求数', | |
| 'usage_stats.success_requests': '成功请求', | |
| 'usage_stats.failed_requests': '失败请求', | |
| 'usage_stats.total_tokens': '总Token数', | |
| 'usage_stats.requests_trend': '请求趋势', | |
| 'usage_stats.tokens_trend': 'Token 使用趋势', | |
| 'usage_stats.api_details': 'API 详细统计', | |
| 'usage_stats.by_hour': '按小时', | |
| 'usage_stats.by_day': '按天', | |
| 'usage_stats.refresh': '刷新', | |
| 'usage_stats.no_data': '暂无数据', | |
| 'usage_stats.loading_error': '加载失败', | |
| 'usage_stats.api_endpoint': 'API端点', | |
| 'usage_stats.requests_count': '请求次数', | |
| 'usage_stats.tokens_count': 'Token数量', | |
| 'usage_stats.models': '模型统计', | |
| 'usage_stats.success_rate': '成功率', | |
| // 系统信息 | |
| 'system_info.title': '系统信息', | |
| 'system_info.connection_status_title': '连接状态', | |
| 'system_info.api_status_label': 'API 状态:', | |
| 'system_info.config_status_label': '配置状态:', | |
| 'system_info.last_update_label': '最后更新:', | |
| 'system_info.cache_data': '缓存数据', | |
| 'system_info.real_time_data': '实时数据', | |
| 'system_info.not_loaded': '未加载', | |
| 'system_info.seconds_ago': '秒前', | |
| // 通知消息 | |
| 'notification.debug_updated': '调试设置已更新', | |
| 'notification.proxy_updated': '代理设置已更新', | |
| 'notification.proxy_cleared': '代理设置已清空', | |
| 'notification.retry_updated': '重试设置已更新', | |
| 'notification.quota_switch_project_updated': '项目切换设置已更新', | |
| 'notification.quota_switch_preview_updated': '预览模型切换设置已更新', | |
| 'notification.api_key_added': 'API密钥添加成功', | |
| 'notification.api_key_updated': 'API密钥更新成功', | |
| 'notification.api_key_deleted': 'API密钥删除成功', | |
| 'notification.gemini_key_added': 'Gemini密钥添加成功', | |
| 'notification.gemini_key_updated': 'Gemini密钥更新成功', | |
| 'notification.gemini_key_deleted': 'Gemini密钥删除成功', | |
| 'notification.codex_config_added': 'Codex配置添加成功', | |
| 'notification.codex_config_updated': 'Codex配置更新成功', | |
| 'notification.codex_config_deleted': 'Codex配置删除成功', | |
| 'notification.claude_config_added': 'Claude配置添加成功', | |
| 'notification.claude_config_updated': 'Claude配置更新成功', | |
| 'notification.claude_config_deleted': 'Claude配置删除成功', | |
| 'notification.field_required': '必填字段不能为空', | |
| 'notification.openai_provider_required': '请填写提供商名称和Base URL', | |
| 'notification.openai_provider_added': 'OpenAI提供商添加成功', | |
| 'notification.openai_provider_updated': 'OpenAI提供商更新成功', | |
| 'notification.openai_provider_deleted': 'OpenAI提供商删除成功', | |
| 'notification.openai_model_name_required': '请填写模型名称', | |
| 'notification.data_refreshed': '数据刷新成功', | |
| 'notification.connection_required': '请先建立连接', | |
| 'notification.refresh_failed': '刷新失败', | |
| 'notification.update_failed': '更新失败', | |
| 'notification.add_failed': '添加失败', | |
| 'notification.delete_failed': '删除失败', | |
| 'notification.upload_failed': '上传失败', | |
| 'notification.download_failed': '下载失败', | |
| 'notification.login_failed': '登录失败', | |
| 'notification.please_enter': '请输入', | |
| 'notification.please_fill': '请填写', | |
| 'notification.provider_name_url': '提供商名称和Base URL', | |
| 'notification.api_key': 'API密钥', | |
| 'notification.gemini_api_key': 'Gemini API密钥', | |
| 'notification.codex_api_key': 'Codex API密钥', | |
| 'notification.claude_api_key': 'Claude API密钥', | |
| // 语言切换 | |
| 'language.switch': '语言', | |
| 'language.chinese': '中文', | |
| 'language.english': 'English', | |
| // 主题切换 | |
| 'theme.switch': '主题', | |
| 'theme.light': '亮色', | |
| 'theme.dark': '暗色', | |
| 'theme.switch_to_light': '切换到亮色模式', | |
| 'theme.switch_to_dark': '切换到暗色模式', | |
| 'theme.auto': '跟随系统', | |
| // 页脚 | |
| 'footer.version': '版本', | |
| 'footer.author': '作者' | |
| }, | |
| 'en-US': { | |
| // Common | |
| 'common.login': 'Login', | |
| 'common.logout': 'Logout', | |
| 'common.cancel': 'Cancel', | |
| 'common.confirm': 'Confirm', | |
| 'common.save': 'Save', | |
| 'common.delete': 'Delete', | |
| 'common.edit': 'Edit', | |
| 'common.add': 'Add', | |
| 'common.update': 'Update', | |
| 'common.refresh': 'Refresh', | |
| 'common.close': 'Close', | |
| 'common.success': 'Success', | |
| 'common.error': 'Error', | |
| 'common.info': 'Info', | |
| 'common.warning': 'Warning', | |
| 'common.loading': 'Loading...', | |
| 'common.connecting': 'Connecting...', | |
| 'common.connected': 'Connected', | |
| 'common.disconnected': 'Disconnected', | |
| 'common.connecting_status': 'Connecting', | |
| 'common.connected_status': 'Connected', | |
| 'common.disconnected_status': 'Disconnected', | |
| 'common.yes': 'Yes', | |
| 'common.no': 'No', | |
| 'common.optional': 'Optional', | |
| 'common.required': 'Required', | |
| 'common.api_key': 'Key', | |
| 'common.base_url': 'Address', | |
| // Page titles | |
| 'title.main': 'CLI Proxy API Management Center', | |
| 'title.login': 'CLI Proxy API Management Center', | |
| // Auto login | |
| 'auto_login.title': 'Auto Login in Progress...', | |
| 'auto_login.message': 'Attempting to connect to server using locally saved connection information', | |
| // Login page | |
| 'login.subtitle': 'Please enter connection information to access the management interface', | |
| 'login.connection_title': 'Connection Address', | |
| 'login.connection_current': 'Current URL', | |
| 'login.connection_auto_hint': 'The system will automatically use the current URL for connection', | |
| 'login.custom_connection_label': 'Custom Connection URL:', | |
| 'login.custom_connection_placeholder': 'Eg: https://example.com:8317', | |
| 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.', | |
| 'login.use_current_address': 'Use Current URL', | |
| 'login.management_key_label': 'Management Key:', | |
| 'login.management_key_placeholder': 'Enter the management key', | |
| 'login.connect_button': 'Connect', | |
| 'login.submit_button': 'Login', | |
| 'login.submitting': 'Connecting...', | |
| 'login.error_title': 'Login Failed', | |
| 'login.error_required': 'Please fill in complete connection information', | |
| 'login.error_invalid': 'Connection failed, please check address and key', | |
| // Header navigation | |
| 'header.check_connection': 'Check Connection', | |
| 'header.refresh_all': 'Refresh All', | |
| 'header.logout': 'Logout', | |
| // Connection info | |
| 'connection.title': 'Connection Information', | |
| 'connection.server_address': 'Server Address:', | |
| 'connection.management_key': 'Management Key:', | |
| 'connection.status': 'Connection Status:', | |
| // Sidebar navigation | |
| 'nav.basic_settings': 'Basic Settings', | |
| 'nav.api_keys': 'API Keys', | |
| 'nav.ai_providers': 'AI Providers', | |
| 'nav.auth_files': 'Auth Files', | |
| 'nav.usage_stats': 'Usage Statistics', | |
| 'nav.system_info': 'System Info', | |
| // Basic settings | |
| 'basic_settings.title': 'Basic Settings', | |
| 'basic_settings.debug_title': 'Debug Mode', | |
| 'basic_settings.debug_enable': 'Enable Debug Mode', | |
| 'basic_settings.proxy_title': 'Proxy Settings', | |
| 'basic_settings.proxy_url_label': 'Proxy URL:', | |
| 'basic_settings.proxy_url_placeholder': 'e.g.: socks5://user:pass@127.0.0.1:1080/', | |
| 'basic_settings.proxy_update': 'Update', | |
| 'basic_settings.proxy_clear': 'Clear', | |
| 'basic_settings.retry_title': 'Request Retry', | |
| 'basic_settings.retry_count_label': 'Retry Count:', | |
| 'basic_settings.retry_update': 'Update', | |
| 'basic_settings.quota_title': 'Quota Exceeded Behavior', | |
| 'basic_settings.quota_switch_project': 'Auto Switch Project', | |
| 'basic_settings.quota_switch_preview': 'Switch to Preview Model', | |
| // API Keys management | |
| 'api_keys.title': 'API Keys Management', | |
| 'api_keys.proxy_auth_title': 'Proxy Service Authentication Keys', | |
| 'api_keys.add_button': 'Add Key', | |
| 'api_keys.empty_title': 'No API Keys', | |
| 'api_keys.empty_desc': 'Click the button above to add the first key', | |
| 'api_keys.item_title': 'API Key', | |
| 'api_keys.add_modal_title': 'Add API Key', | |
| 'api_keys.add_modal_key_label': 'API Key:', | |
| 'api_keys.add_modal_key_placeholder': 'Please enter API key', | |
| 'api_keys.edit_modal_title': 'Edit API Key', | |
| 'api_keys.edit_modal_key_label': 'API Key:', | |
| 'api_keys.delete_confirm': 'Are you sure you want to delete this API key?', | |
| // AI Providers | |
| 'ai_providers.title': 'AI Providers Configuration', | |
| 'ai_providers.gemini_title': 'Gemini API Keys', | |
| 'ai_providers.gemini_add_button': 'Add Key', | |
| 'ai_providers.gemini_empty_title': 'No Gemini Keys', | |
| 'ai_providers.gemini_empty_desc': 'Click the button above to add the first key', | |
| 'ai_providers.gemini_item_title': 'Gemini Key', | |
| 'ai_providers.gemini_add_modal_title': 'Add Gemini API Key', | |
| 'ai_providers.gemini_add_modal_key_label': 'API Key:', | |
| 'ai_providers.gemini_add_modal_key_placeholder': 'Please enter Gemini API key', | |
| 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key', | |
| 'ai_providers.gemini_edit_modal_key_label': 'API Key:', | |
| 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?', | |
| 'ai_providers.codex_title': 'Codex API Configuration', | |
| 'ai_providers.codex_add_button': 'Add Configuration', | |
| 'ai_providers.codex_empty_title': 'No Codex Configuration', | |
| 'ai_providers.codex_empty_desc': 'Click the button above to add the first configuration', | |
| 'ai_providers.codex_item_title': 'Codex Configuration', | |
| 'ai_providers.codex_add_modal_title': 'Add Codex API Configuration', | |
| 'ai_providers.codex_add_modal_key_label': 'API Key:', | |
| 'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex API key', | |
| 'ai_providers.codex_add_modal_url_label': 'Base URL (Optional):', | |
| 'ai_providers.codex_add_modal_url_placeholder': 'e.g.: https://api.example.com', | |
| 'ai_providers.codex_edit_modal_title': 'Edit Codex API Configuration', | |
| 'ai_providers.codex_edit_modal_key_label': 'API Key:', | |
| 'ai_providers.codex_edit_modal_url_label': 'Base URL (Optional):', | |
| 'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?', | |
| 'ai_providers.claude_title': 'Claude API Configuration', | |
| 'ai_providers.claude_add_button': 'Add Configuration', | |
| 'ai_providers.claude_empty_title': 'No Claude Configuration', | |
| 'ai_providers.claude_empty_desc': 'Click the button above to add the first configuration', | |
| 'ai_providers.claude_item_title': 'Claude Configuration', | |
| 'ai_providers.claude_add_modal_title': 'Add Claude API Configuration', | |
| 'ai_providers.claude_add_modal_key_label': 'API Key:', | |
| 'ai_providers.claude_add_modal_key_placeholder': 'Please enter Claude API key', | |
| 'ai_providers.claude_add_modal_url_label': 'Base URL (Optional):', | |
| 'ai_providers.claude_add_modal_url_placeholder': 'e.g.: https://api.anthropic.com', | |
| 'ai_providers.claude_edit_modal_title': 'Edit Claude API Configuration', | |
| 'ai_providers.claude_edit_modal_key_label': 'API Key:', | |
| 'ai_providers.claude_edit_modal_url_label': 'Base URL (Optional):', | |
| 'ai_providers.claude_delete_confirm': 'Are you sure you want to delete this Claude configuration?', | |
| 'ai_providers.openai_title': 'OpenAI Compatible Providers', | |
| 'ai_providers.openai_add_button': 'Add Provider', | |
| 'ai_providers.openai_empty_title': 'No OpenAI Compatible Providers', | |
| 'ai_providers.openai_empty_desc': 'Click the button above to add the first provider', | |
| 'ai_providers.openai_add_modal_title': 'Add OpenAI Compatible Provider', | |
| 'ai_providers.openai_add_modal_name_label': 'Provider Name:', | |
| 'ai_providers.openai_add_modal_name_placeholder': 'e.g.: openrouter', | |
| 'ai_providers.openai_add_modal_url_label': 'Base URL:', | |
| 'ai_providers.openai_add_modal_url_placeholder': 'e.g.: https://openrouter.ai/api/v1', | |
| 'ai_providers.openai_add_modal_keys_label': 'API Keys (one per line):', | |
| 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', | |
| 'ai_providers.openai_edit_modal_title': 'Edit OpenAI Compatible Provider', | |
| 'ai_providers.openai_edit_modal_name_label': 'Provider Name:', | |
| 'ai_providers.openai_edit_modal_url_label': 'Base URL:', | |
| 'ai_providers.openai_edit_modal_keys_label': 'API Keys (one per line):', | |
| 'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?', | |
| 'ai_providers.openai_keys_count': 'Keys Count', | |
| 'ai_providers.openai_models_count': 'Models Count', | |
| // Auth files management | |
| 'auth_files.title': 'Auth Files Management', | |
| 'auth_files.title_section': 'Auth Files', | |
| 'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.', | |
| 'auth_files.upload_button': 'Upload File', | |
| 'auth_files.delete_all_button': 'Delete All', | |
| 'auth_files.empty_title': 'No Auth Files', | |
| 'auth_files.empty_desc': 'Click the button above to upload the first file', | |
| 'auth_files.file_size': 'Size', | |
| 'auth_files.file_modified': 'Modified', | |
| 'auth_files.download_button': 'Download', | |
| 'auth_files.delete_button': 'Delete', | |
| 'auth_files.delete_confirm': 'Are you sure you want to delete file', | |
| 'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!', | |
| 'auth_files.upload_error_json': 'Only JSON files are allowed', | |
| 'auth_files.upload_success': 'File uploaded successfully', | |
| 'auth_files.download_success': 'File downloaded successfully', | |
| 'auth_files.delete_success': 'File deleted successfully', | |
| 'auth_files.delete_all_success': 'Successfully deleted', | |
| 'auth_files.files_count': 'files', | |
| // Gemini Web Token | |
| 'auth_login.gemini_web_title': 'Gemini Web Token', | |
| 'auth_login.gemini_web_button': 'Save Gemini Web Token', | |
| 'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.', | |
| 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:', | |
| 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value', | |
| 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', | |
| 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value', | |
| 'auth_login.gemini_web_label_label': 'Label (Optional):', | |
| 'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)', | |
| 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully', | |
| // Codex OAuth | |
| 'auth_login.codex_oauth_title': 'Codex OAuth', | |
| 'auth_login.codex_oauth_button': 'Start Codex Login', | |
| 'auth_login.codex_oauth_hint': 'Login to Codex service through OAuth flow, automatically obtain and save authentication files.', | |
| 'auth_login.codex_oauth_url_label': 'Authorization URL:', | |
| 'auth_login.codex_open_link': 'Open Link', | |
| 'auth_login.codex_copy_link': 'Copy Link', | |
| 'auth_login.codex_oauth_status_waiting': 'Waiting for authentication...', | |
| 'auth_login.codex_oauth_status_success': 'Authentication successful!', | |
| 'auth_login.codex_oauth_status_error': 'Authentication failed:', | |
| 'auth_login.codex_oauth_start_error': 'Failed to start Codex OAuth:', | |
| 'auth_login.codex_oauth_polling_error': 'Failed to check authentication status:', | |
| // Anthropic OAuth | |
| 'auth_login.anthropic_oauth_title': 'Anthropic OAuth', | |
| 'auth_login.anthropic_oauth_button': 'Start Anthropic Login', | |
| 'auth_login.anthropic_oauth_hint': 'Login to Anthropic (Claude) service through OAuth flow, automatically obtain and save authentication files.', | |
| 'auth_login.anthropic_oauth_url_label': 'Authorization URL:', | |
| 'auth_login.anthropic_open_link': 'Open Link', | |
| 'auth_login.anthropic_copy_link': 'Copy Link', | |
| 'auth_login.anthropic_oauth_status_waiting': 'Waiting for authentication...', | |
| 'auth_login.anthropic_oauth_status_success': 'Authentication successful!', | |
| 'auth_login.anthropic_oauth_status_error': 'Authentication failed:', | |
| 'auth_login.anthropic_oauth_start_error': 'Failed to start Anthropic OAuth:', | |
| 'auth_login.anthropic_oauth_polling_error': 'Failed to check authentication status:', | |
| // Gemini CLI OAuth | |
| 'auth_login.gemini_cli_oauth_title': 'Gemini CLI OAuth', | |
| 'auth_login.gemini_cli_oauth_button': 'Start Gemini CLI Login', | |
| 'auth_login.gemini_cli_oauth_hint': 'Login to Google Gemini CLI service through OAuth flow, automatically obtain and save authentication files.', | |
| 'auth_login.gemini_cli_project_id_label': 'Google Cloud Project ID (Optional):', | |
| 'auth_login.gemini_cli_project_id_placeholder': 'Enter Google Cloud Project ID (optional)', | |
| 'auth_login.gemini_cli_project_id_hint': 'If a project ID is specified, authentication information for that project will be used.', | |
| 'auth_login.gemini_cli_oauth_url_label': 'Authorization URL:', | |
| 'auth_login.gemini_cli_open_link': 'Open Link', | |
| 'auth_login.gemini_cli_copy_link': 'Copy Link', | |
| 'auth_login.gemini_cli_oauth_status_waiting': 'Waiting for authentication...', | |
| 'auth_login.gemini_cli_oauth_status_success': 'Authentication successful!', | |
| 'auth_login.gemini_cli_oauth_status_error': 'Authentication failed:', | |
| 'auth_login.gemini_cli_oauth_start_error': 'Failed to start Gemini CLI OAuth:', | |
| 'auth_login.gemini_cli_oauth_polling_error': 'Failed to check authentication status:', | |
| // Qwen OAuth | |
| 'auth_login.qwen_oauth_title': 'Qwen OAuth', | |
| 'auth_login.qwen_oauth_button': 'Start Qwen Login', | |
| 'auth_login.qwen_oauth_hint': 'Login to Qwen service through device authorization flow, automatically obtain and save authentication files.', | |
| 'auth_login.qwen_oauth_url_label': 'Authorization URL:', | |
| 'auth_login.qwen_open_link': 'Open Link', | |
| 'auth_login.qwen_copy_link': 'Copy Link', | |
| 'auth_login.qwen_oauth_status_waiting': 'Waiting for authentication...', | |
| 'auth_login.qwen_oauth_status_success': 'Authentication successful!', | |
| 'auth_login.qwen_oauth_status_error': 'Authentication failed:', | |
| 'auth_login.qwen_oauth_start_error': 'Failed to start Qwen OAuth:', | |
| 'auth_login.qwen_oauth_polling_error': 'Failed to check authentication status:', | |
| // iFlow OAuth | |
| 'auth_login.iflow_oauth_title': 'iFlow OAuth', | |
| 'auth_login.iflow_oauth_button': 'Start iFlow Login', | |
| 'auth_login.iflow_oauth_hint': 'Login to iFlow service through OAuth flow, automatically obtain and save authentication files.', | |
| 'auth_login.iflow_oauth_url_label': 'Authorization URL:', | |
| 'auth_login.iflow_open_link': 'Open Link', | |
| 'auth_login.iflow_copy_link': 'Copy Link', | |
| 'auth_login.iflow_oauth_status_waiting': 'Waiting for authentication...', | |
| 'auth_login.iflow_oauth_status_success': 'Authentication successful!', | |
| 'auth_login.iflow_oauth_status_error': 'Authentication failed:', | |
| 'auth_login.iflow_oauth_start_error': 'Failed to start iFlow OAuth:', | |
| 'auth_login.iflow_oauth_polling_error': 'Failed to check authentication status:', | |
| // Usage Statistics | |
| 'usage_stats.title': 'Usage Statistics', | |
| 'usage_stats.total_requests': 'Total Requests', | |
| 'usage_stats.success_requests': 'Success Requests', | |
| 'usage_stats.failed_requests': 'Failed Requests', | |
| 'usage_stats.total_tokens': 'Total Tokens', | |
| 'usage_stats.requests_trend': 'Request Trends', | |
| 'usage_stats.tokens_trend': 'Token Usage Trends', | |
| 'usage_stats.api_details': 'API Details', | |
| 'usage_stats.by_hour': 'By Hour', | |
| 'usage_stats.by_day': 'By Day', | |
| 'usage_stats.refresh': 'Refresh', | |
| 'usage_stats.no_data': 'No Data Available', | |
| 'usage_stats.loading_error': 'Loading Failed', | |
| 'usage_stats.api_endpoint': 'API Endpoint', | |
| 'usage_stats.requests_count': 'Request Count', | |
| 'usage_stats.tokens_count': 'Token Count', | |
| 'usage_stats.models': 'Model Statistics', | |
| 'usage_stats.success_rate': 'Success Rate', | |
| // System info | |
| 'system_info.title': 'System Information', | |
| 'system_info.connection_status_title': 'Connection Status', | |
| 'system_info.api_status_label': 'API Status:', | |
| 'system_info.config_status_label': 'Config Status:', | |
| 'system_info.last_update_label': 'Last Update:', | |
| 'system_info.cache_data': 'Cache Data', | |
| 'system_info.real_time_data': 'Real-time Data', | |
| 'system_info.not_loaded': 'Not Loaded', | |
| 'system_info.seconds_ago': 'seconds ago', | |
| // Notification messages | |
| 'notification.debug_updated': 'Debug settings updated', | |
| 'notification.proxy_updated': 'Proxy settings updated', | |
| 'notification.proxy_cleared': 'Proxy settings cleared', | |
| 'notification.retry_updated': 'Retry settings updated', | |
| 'notification.quota_switch_project_updated': 'Project switch settings updated', | |
| 'notification.quota_switch_preview_updated': 'Preview model switch settings updated', | |
| 'notification.api_key_added': 'API key added successfully', | |
| 'notification.api_key_updated': 'API key updated successfully', | |
| 'notification.api_key_deleted': 'API key deleted successfully', | |
| 'notification.gemini_key_added': 'Gemini key added successfully', | |
| 'notification.gemini_key_updated': 'Gemini key updated successfully', | |
| 'notification.gemini_key_deleted': 'Gemini key deleted successfully', | |
| 'notification.codex_config_added': 'Codex configuration added successfully', | |
| 'notification.codex_config_updated': 'Codex configuration updated successfully', | |
| 'notification.codex_config_deleted': 'Codex configuration deleted successfully', | |
| 'notification.claude_config_added': 'Claude configuration added successfully', | |
| 'notification.claude_config_updated': 'Claude configuration updated successfully', | |
| 'notification.claude_config_deleted': 'Claude configuration deleted successfully', | |
| 'notification.openai_provider_added': 'OpenAI provider added successfully', | |
| 'notification.openai_provider_updated': 'OpenAI provider updated successfully', | |
| 'notification.openai_provider_deleted': 'OpenAI provider deleted successfully', | |
| 'notification.openai_model_name_required': 'Model name is required', | |
| 'notification.data_refreshed': 'Data refreshed successfully', | |
| 'notification.connection_required': 'Please establish connection first', | |
| 'notification.refresh_failed': 'Refresh failed', | |
| 'notification.update_failed': 'Update failed', | |
| 'notification.add_failed': 'Add failed', | |
| 'notification.delete_failed': 'Delete failed', | |
| 'notification.upload_failed': 'Upload failed', | |
| 'notification.download_failed': 'Download failed', | |
| 'notification.login_failed': 'Login failed', | |
| 'notification.please_enter': 'Please enter', | |
| 'notification.please_fill': 'Please fill', | |
| 'notification.provider_name_url': 'provider name and Base URL', | |
| 'notification.api_key': 'API key', | |
| 'notification.gemini_api_key': 'Gemini API key', | |
| 'notification.codex_api_key': 'Codex API key', | |
| 'notification.claude_api_key': 'Claude API key', | |
| // Language switch | |
| 'language.switch': 'Language', | |
| 'language.chinese': '中文', | |
| 'language.english': 'English', | |
| // Theme switch | |
| 'theme.switch': 'Theme', | |
| 'theme.light': 'Light', | |
| 'theme.dark': 'Dark', | |
| 'theme.switch_to_light': 'Switch to light mode', | |
| 'theme.switch_to_dark': 'Switch to dark mode', | |
| 'theme.auto': 'Follow system', | |
| // Footer | |
| 'footer.version': 'Version', | |
| 'footer.author': 'Author' | |
| } | |
| }, | |
| // 获取翻译文本 | |
| t(key, params = {}) { | |
| const translation = this.translations[this.currentLanguage]?.[key] || | |
| this.translations[this.fallbackLanguage]?.[key] || | |
| key; | |
| // 简单的参数替换 | |
| return translation.replace(/\{(\w+)\}/g, (match, param) => { | |
| return params[param] || match; | |
| }); | |
| }, | |
| // 设置语言 | |
| setLanguage(lang) { | |
| if (this.translations[lang]) { | |
| this.currentLanguage = lang; | |
| localStorage.setItem('preferredLanguage', lang); | |
| this.updatePageLanguage(); | |
| this.updateAllTexts(); | |
| } | |
| }, | |
| // 更新页面语言属性 | |
| updatePageLanguage() { | |
| document.documentElement.lang = this.currentLanguage; | |
| }, | |
| // 更新所有文本 | |
| updateAllTexts() { | |
| // 更新所有带有 data-i18n 属性的元素 | |
| document.querySelectorAll('[data-i18n]').forEach(element => { | |
| const key = element.getAttribute('data-i18n'); | |
| const text = this.t(key); | |
| if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) { | |
| element.placeholder = text; | |
| } else if (element.tagName === 'TITLE') { | |
| element.textContent = text; | |
| } else { | |
| element.textContent = text; | |
| } | |
| }); | |
| // 更新所有带有 data-i18n-html 属性的元素(支持HTML) | |
| document.querySelectorAll('[data-i18n-html]').forEach(element => { | |
| const key = element.getAttribute('data-i18n-html'); | |
| const html = this.t(key); | |
| element.innerHTML = html; | |
| }); | |
| }, | |
| // 初始化 | |
| init() { | |
| // 从本地存储获取用户偏好语言 | |
| const savedLanguage = localStorage.getItem('preferredLanguage'); | |
| if (savedLanguage && this.translations[savedLanguage]) { | |
| this.currentLanguage = savedLanguage; | |
| } else { | |
| // 根据浏览器语言自动选择 | |
| const browserLang = navigator.language || navigator.userLanguage; | |
| if (browserLang.startsWith('zh')) { | |
| this.currentLanguage = 'zh-CN'; | |
| } else { | |
| this.currentLanguage = 'en-US'; | |
| } | |
| } | |
| this.updatePageLanguage(); | |
| this.updateAllTexts(); | |
| } | |
| }; | |
| // 全局函数,供HTML调用 | |
| window.t = (key, params) => i18n.t(key, params); | |
| window.setLanguage = (lang) => i18n.setLanguage(lang); | |
| </script> | |
| </head> | |
| <body> | |
| <!-- 自动登录加载页面 --> | |
| <div id="auto-login-loading" class="login-container" style="display: none;"> | |
| <div class="login-card"> | |
| <div class="auto-login-content"> | |
| <div class="loading-spinner"> | |
| <div class="spinner"></div> | |
| </div> | |
| <h2 data-i18n="auto_login.title">正在自动登录...</h2> | |
| <p data-i18n="auto_login.message">正在使用本地保存的连接信息尝试连接服务器</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 登录页面 --> | |
| <div id="login-page" class="login-container"> | |
| <div class="login-card"> | |
| <div class="login-header"> | |
| <div class="login-header-top"> | |
| <h1 class="login-title"> | |
| <img id="login-logo" alt="Logo" style="display:none" /> | |
| <span data-i18n="title.login">CLI Proxy API Management Center</span> | |
| </h1> | |
| <div class="header-controls"> | |
| <div class="language-switcher"> | |
| <button id="language-toggle" class="btn btn-secondary language-btn"> | |
| <i class="fas fa-globe"></i> | |
| <span data-i18n="language.switch">语言</span> | |
| </button> | |
| </div> | |
| <div class="theme-switcher"> | |
| <button id="theme-toggle" class="btn btn-secondary theme-btn"> | |
| <i class="fas fa-moon"></i> | |
| <span data-i18n="theme.switch">主题</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="login-body"> | |
| <div class="login-connection-info"> | |
| <div class="connection-summary"> | |
| <i class="fas fa-link"></i> | |
| <div> | |
| <h3 data-i18n="login.connection_title">连接地址</h3> | |
| <p class="connection-url"> | |
| <span data-i18n="login.connection_current">当前地址</span> | |
| <span class="connection-url-separator">:</span> | |
| <span id="login-connection-url">-</span> | |
| </p> | |
| </div> | |
| </div> | |
| <p class="form-hint" data-i18n="login.connection_auto_hint">系统将自动使用当前访问地址进行连接</p> | |
| </div> | |
| <form class="login-form"> | |
| <div class="form-group"> | |
| <label for="login-api-base" data-i18n="login.custom_connection_label">自定义连接地址:</label> | |
| <div class="input-group"> | |
| <input type="text" id="login-api-base" data-i18n="login.custom_connection_placeholder" | |
| placeholder="例如: https://example.com:8317"> | |
| <button type="button" id="login-reset-api-base" | |
| class="btn btn-secondary connection-reset-btn"> | |
| <i class="fas fa-location-arrow"></i> | |
| <span data-i18n="login.use_current_address">使用当前地址</span> | |
| </button> | |
| </div> | |
| <p class="form-hint" data-i18n="login.custom_connection_hint">默认使用当前访问地址,若需要可手动输入其他地址。</p> | |
| </div> | |
| <div class="form-group"> | |
| <label for="login-management-key" data-i18n="login.management_key_label">管理密钥:</label> | |
| <div class="input-group"> | |
| <input type="password" id="login-management-key" | |
| data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required> | |
| <button type="button" class="btn btn-secondary toggle-key-visibility"> | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- 连接按钮 --> | |
| <div class="form-actions"> | |
| <button type="button" id="login-submit" class="btn btn-primary login-btn"> | |
| <i class="fas fa-plug"></i> <span data-i18n="login.connect_button">Connect</span> | |
| </button> | |
| </div> | |
| <div id="login-error" class="login-error" style="display: none;"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| <span id="login-error-message"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 主页面 --> | |
| <div id="main-page" style="display: none;"> | |
| <!-- 顶部导航栏 --> | |
| <div class="top-navbar"> | |
| <div class="top-navbar-left"> | |
| <button class="mobile-menu-btn" id="mobile-menu-btn"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <button class="sidebar-toggle-btn-desktop" id="sidebar-toggle-btn-desktop" title="收起/展开侧边栏"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <div class="top-navbar-brand"> | |
| <img id="site-logo" class="top-navbar-brand-logo" alt="Logo" style="display:none" /> | |
| <span class="top-navbar-brand-text" data-i18n="title.main">CLI Proxy API Management Center</span> | |
| </div> | |
| </div> | |
| <div class="top-navbar-actions"> | |
| <div class="header-controls"> | |
| <div class="language-switcher"> | |
| <button id="language-toggle-main" class="btn btn-secondary language-btn"> | |
| <i class="fas fa-globe"></i> | |
| </button> | |
| </div> | |
| <div class="theme-switcher"> | |
| <button id="theme-toggle-main" class="btn btn-secondary theme-btn"> | |
| <i class="fas fa-moon"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <button id="connection-status" class="btn btn-secondary"> | |
| <i class="fas fa-circle"></i> <span data-i18n="header.check_connection">检查连接</span> | |
| </button> | |
| <button id="refresh-all" class="btn btn-primary"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| <button id="logout-btn" class="btn btn-danger"> | |
| <i class="fas fa-sign-out-alt"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="layout" id="layout-container"> | |
| <!-- 侧边栏 --> | |
| <nav class="sidebar" id="sidebar"> | |
| <!-- 导航菜单 --> | |
| <ul class="nav-menu"> | |
| <li data-tooltip="基础设置"><a href="#basic-settings" class="nav-item active" | |
| data-section="basic-settings"> | |
| <i class="fas fa-sliders-h"></i> <span data-i18n="nav.basic_settings">基础设置</span> | |
| </a></li> | |
| <li data-tooltip="API 密钥"><a href="#api-keys" class="nav-item" data-section="api-keys"> | |
| <i class="fas fa-key"></i> <span data-i18n="nav.api_keys">API 密钥</span> | |
| </a></li> | |
| <li data-tooltip="AI 提供商"><a href="#ai-providers" class="nav-item" data-section="ai-providers"> | |
| <i class="fas fa-robot"></i> <span data-i18n="nav.ai_providers">AI 提供商</span> | |
| </a></li> | |
| <li data-tooltip="认证文件"><a href="#auth-files" class="nav-item" data-section="auth-files"> | |
| <i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span> | |
| </a></li> | |
| <li data-tooltip="使用统计"><a href="#usage-stats" class="nav-item" data-section="usage-stats"> | |
| <i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span> | |
| </a></li> | |
| <li data-tooltip="系统信息"><a href="#system-info" class="nav-item" data-section="system-info"> | |
| <i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span> | |
| </a></li> | |
| </ul> | |
| </nav> | |
| <!-- 侧边栏遮罩(移动端) --> | |
| <div class="sidebar-overlay" id="sidebar-overlay"></div> | |
| <!-- 主内容包装器 --> | |
| <div class="main-wrapper" id="main-wrapper"> | |
| <!-- 主内容区域 --> | |
| <div class="main-content"> | |
| <!-- 内容区域 --> | |
| <div class="content-area"> | |
| <!-- 基础设置 --> | |
| <section id="basic-settings" class="content-section active"> | |
| <h2 data-i18n="basic_settings.title">基础设置</h2> | |
| <!-- Debug 设置 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-bug"></i> <span | |
| data-i18n="basic_settings.debug_title">调试模式</span></h3> | |
| </div> | |
| <div class="card-content"> | |
| <div class="toggle-group"> | |
| <label class="toggle-switch"> | |
| <input type="checkbox" id="debug-toggle"> | |
| <span class="slider"></span> | |
| </label> | |
| <span class="toggle-label" data-i18n="basic_settings.debug_enable">启用调试模式</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 代理设置 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-network-wired"></i> <span | |
| data-i18n="basic_settings.proxy_title">代理设置</span></h3> | |
| </div> | |
| <div class="card-content"> | |
| <div class="form-group"> | |
| <label for="proxy-url" data-i18n="basic_settings.proxy_url_label">代理 | |
| URL:</label> | |
| <div class="input-group"> | |
| <input type="text" id="proxy-url" | |
| data-i18n="basic_settings.proxy_url_placeholder" | |
| placeholder="例如: socks5://user:pass@127.0.0.1:1080/"> | |
| <button id="update-proxy" class="btn btn-primary" | |
| data-i18n="basic_settings.proxy_update">更新</button> | |
| <button id="clear-proxy" class="btn btn-danger" | |
| data-i18n="basic_settings.proxy_clear">清空</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 请求重试设置 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-redo"></i> <span | |
| data-i18n="basic_settings.retry_title">请求重试</span></h3> | |
| </div> | |
| <div class="card-content"> | |
| <div class="form-group"> | |
| <label for="request-retry" | |
| data-i18n="basic_settings.retry_count_label">重试次数:</label> | |
| <div class="input-group"> | |
| <input type="number" id="request-retry" min="0" max="10" value="3"> | |
| <button id="update-retry" class="btn btn-primary" | |
| data-i18n="basic_settings.retry_update">更新</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 配额超出行为 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-exclamation-triangle"></i> <span | |
| data-i18n="basic_settings.quota_title">配额超出行为</span></h3> | |
| </div> | |
| <div class="card-content"> | |
| <div class="toggle-group"> | |
| <label class="toggle-switch"> | |
| <input type="checkbox" id="switch-project-toggle"> | |
| <span class="slider"></span> | |
| </label> | |
| <span class="toggle-label" | |
| data-i18n="basic_settings.quota_switch_project">自动切换项目</span> | |
| </div> | |
| <div class="toggle-group"> | |
| <label class="toggle-switch"> | |
| <input type="checkbox" id="switch-preview-model-toggle"> | |
| <span class="slider"></span> | |
| </label> | |
| <span class="toggle-label" | |
| data-i18n="basic_settings.quota_switch_preview">切换到预览模型</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- API 密钥管理 --> | |
| <section id="api-keys" class="content-section"> | |
| <h2 data-i18n="api_keys.title">API 密钥管理</h2> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-key"></i> <span | |
| data-i18n="api_keys.proxy_auth_title">代理服务认证密钥</span></h3> | |
| <button id="add-api-key" class="btn btn-primary"> | |
| <i class="fas fa-plus"></i> <span data-i18n="api_keys.add_button">添加密钥</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <div id="api-keys-list" class="key-list"></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- AI 提供商 --> | |
| <section id="ai-providers" class="content-section"> | |
| <h2 data-i18n="ai_providers.title">AI 提供商配置</h2> | |
| <!-- Gemini API Keys --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fab fa-google"></i> <span data-i18n="ai_providers.gemini_title">Gemini | |
| API 密钥</span></h3> | |
| <button id="add-gemini-key" class="btn btn-primary"> | |
| <i class="fas fa-plus"></i> <span | |
| data-i18n="ai_providers.gemini_add_button">添加密钥</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <div id="gemini-keys-list" class="key-list"></div> | |
| </div> | |
| </div> | |
| <!-- Codex API Keys --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-code"></i> <span data-i18n="ai_providers.codex_title">Codex API | |
| 配置</span></h3> | |
| <button id="add-codex-key" class="btn btn-primary"> | |
| <i class="fas fa-plus"></i> <span | |
| data-i18n="ai_providers.codex_add_button">添加配置</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <div id="codex-keys-list" class="provider-list"></div> | |
| </div> | |
| </div> | |
| <!-- Claude API Keys --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-brain"></i> <span data-i18n="ai_providers.claude_title">Claude | |
| API 配置</span></h3> | |
| <button id="add-claude-key" class="btn btn-primary"> | |
| <i class="fas fa-plus"></i> <span | |
| data-i18n="ai_providers.claude_add_button">添加配置</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <div id="claude-keys-list" class="provider-list"></div> | |
| </div> | |
| </div> | |
| <!-- OpenAI 兼容提供商 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-plug"></i> <span data-i18n="ai_providers.openai_title">OpenAI | |
| 兼容提供商</span></h3> | |
| <button id="add-openai-provider" class="btn btn-primary"> | |
| <i class="fas fa-plus"></i> <span | |
| data-i18n="ai_providers.openai_add_button">添加提供商</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <div id="openai-providers-list" class="provider-list"></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- 认证文件管理 --> | |
| <section id="auth-files" class="content-section"> | |
| <h2 data-i18n="auth_files.title">认证文件管理</h2> | |
| <div class="card" style="margin-bottom: 20px;"> | |
| <div class="card-content"> | |
| <p class="form-hint" data-i18n="auth_files.description"> | |
| 这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。 | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Gemini Web Token --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fab fa-google"></i> <span | |
| data-i18n="auth_login.gemini_web_title">Gemini Web Token</span></h3> | |
| <button id="gemini-web-token-btn" class="btn btn-primary"> | |
| <i class="fas fa-save"></i> <span data-i18n="auth_login.gemini_web_button">保存 | |
| Gemini Web Token</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <p class="form-hint" style="margin-bottom: 20px;" | |
| data-i18n="auth_login.gemini_web_hint"> | |
| 从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。 | |
| </p> | |
| <div class="form-group"> | |
| <label for="secure-1psid-input" | |
| data-i18n="auth_login.secure_1psid_label">__Secure-1PSID Cookie:</label> | |
| <input type="text" id="secure-1psid-input" | |
| data-i18n="auth_login.secure_1psid_placeholder" | |
| placeholder="输入 __Secure-1PSID cookie 值"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="secure-1psidts-input" | |
| data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label> | |
| <input type="text" id="secure-1psidts-input" | |
| data-i18n="auth_login.secure_1psidts_placeholder" | |
| placeholder="输入 __Secure-1PSIDTS cookie 值"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="gemini-web-label-input" | |
| data-i18n="auth_login.gemini_web_label_label">Label (Optional):</label> | |
| <input type="text" id="gemini-web-label-input" | |
| data-i18n="auth_login.gemini_web_label_placeholder" | |
| placeholder="输入标签名称 (可选)"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 认证文件 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-file-alt"></i> <span | |
| data-i18n="auth_files.title_section">认证文件</span></h3> | |
| <div class="header-actions"> | |
| <button id="upload-auth-file" class="btn btn-primary"> | |
| <i class="fas fa-upload"></i> <span | |
| data-i18n="auth_files.upload_button">上传文件</span> | |
| </button> | |
| <button id="delete-all-auth-files" class="btn btn-danger"> | |
| <i class="fas fa-trash"></i> <span | |
| data-i18n="auth_files.delete_all_button">删除全部</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <div id="auth-files-list" class="file-list"></div> | |
| <input type="file" id="auth-file-input" accept=".json" style="display: none;"> | |
| </div> | |
| </div> | |
| <!-- Codex OAuth --> | |
| <div class="card" id="codex-oauth-card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-code"></i> <span data-i18n="auth_login.codex_oauth_title">Codex | |
| OAuth</span></h3> | |
| <button id="codex-oauth-btn" class="btn btn-primary"> | |
| <i class="fas fa-sign-in-alt"></i> <span | |
| data-i18n="auth_login.codex_oauth_button">开始 Codex 登录</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <p class="form-hint" style="margin-bottom: 20px;" | |
| data-i18n="auth_login.codex_oauth_hint"> | |
| 通过 OAuth 流程登录 Codex 服务,自动获取并保存认证文件。 | |
| </p> | |
| <div id="codex-oauth-content" style="display: none;"> | |
| <div class="form-group"> | |
| <label data-i18n="auth_login.codex_oauth_url_label">授权链接:</label> | |
| <div class="input-group"> | |
| <input type="text" id="codex-oauth-url" readonly> | |
| <button id="codex-open-link" class="btn btn-primary"> | |
| <i class="fas fa-external-link-alt"></i> <span | |
| data-i18n="auth_login.codex_open_link">打开链接</span> | |
| </button> | |
| <button id="codex-copy-link" class="btn btn-secondary"> | |
| <i class="fas fa-copy"></i> <span | |
| data-i18n="auth_login.codex_copy_link">复制链接</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="codex-oauth-status" class="form-hint" style="margin-top: 10px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Anthropic OAuth --> | |
| <div class="card" id="anthropic-oauth-card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-brain"></i> <span | |
| data-i18n="auth_login.anthropic_oauth_title">Anthropic OAuth</span></h3> | |
| <button id="anthropic-oauth-btn" class="btn btn-primary"> | |
| <i class="fas fa-sign-in-alt"></i> <span | |
| data-i18n="auth_login.anthropic_oauth_button">开始 Anthropic 登录</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <p class="form-hint" style="margin-bottom: 20px;" | |
| data-i18n="auth_login.anthropic_oauth_hint"> | |
| 通过 OAuth 流程登录 Anthropic (Claude) 服务,自动获取并保存认证文件。 | |
| </p> | |
| <div id="anthropic-oauth-content" style="display: none;"> | |
| <div class="form-group"> | |
| <label data-i18n="auth_login.anthropic_oauth_url_label">授权链接:</label> | |
| <div class="input-group"> | |
| <input type="text" id="anthropic-oauth-url" readonly> | |
| <button id="anthropic-open-link" class="btn btn-primary"> | |
| <i class="fas fa-external-link-alt"></i> <span | |
| data-i18n="auth_login.anthropic_open_link">打开链接</span> | |
| </button> | |
| <button id="anthropic-copy-link" class="btn btn-secondary"> | |
| <i class="fas fa-copy"></i> <span | |
| data-i18n="auth_login.anthropic_copy_link">复制链接</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="anthropic-oauth-status" class="form-hint" style="margin-top: 10px;"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Gemini CLI OAuth --> | |
| <div class="card" id="gemini-cli-oauth-card"> | |
| <div class="card-header"> | |
| <h3><i class="fab fa-google"></i> <span | |
| data-i18n="auth_login.gemini_cli_oauth_title">Gemini CLI OAuth</span></h3> | |
| <button id="gemini-cli-oauth-btn" class="btn btn-primary"> | |
| <i class="fas fa-sign-in-alt"></i> <span | |
| data-i18n="auth_login.gemini_cli_oauth_button">开始 Gemini CLI 登录</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <p class="form-hint" style="margin-bottom: 20px;" | |
| data-i18n="auth_login.gemini_cli_oauth_hint"> | |
| 通过 OAuth 流程登录 Google Gemini CLI 服务,自动获取并保存认证文件。 | |
| </p> | |
| <div class="form-group" style="margin-bottom: 20px;"> | |
| <label for="gemini-cli-project-id" | |
| data-i18n="auth_login.gemini_cli_project_id_label">Google Cloud 项目 ID | |
| (可选):</label> | |
| <input type="text" id="gemini-cli-project-id" | |
| data-i18n="auth_login.gemini_cli_project_id_placeholder" | |
| placeholder="输入 Google Cloud 项目 ID (可选)"> | |
| <div class="form-hint" data-i18n="auth_login.gemini_cli_project_id_hint"> | |
| 如果指定了项目 ID,将使用该项目的认证信息。 | |
| </div> | |
| </div> | |
| <div id="gemini-cli-oauth-content" style="display: none;"> | |
| <div class="form-group"> | |
| <label data-i18n="auth_login.gemini_cli_oauth_url_label">授权链接:</label> | |
| <div class="input-group"> | |
| <input type="text" id="gemini-cli-oauth-url" readonly> | |
| <button id="gemini-cli-open-link" class="btn btn-primary"> | |
| <i class="fas fa-external-link-alt"></i> <span | |
| data-i18n="auth_login.gemini_cli_open_link">打开链接</span> | |
| </button> | |
| <button id="gemini-cli-copy-link" class="btn btn-secondary"> | |
| <i class="fas fa-copy"></i> <span | |
| data-i18n="auth_login.gemini_cli_copy_link">复制链接</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="gemini-cli-oauth-status" class="form-hint" style="margin-top: 10px;"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Qwen OAuth --> | |
| <div class="card" id="qwen-oauth-card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-robot"></i> <span data-i18n="auth_login.qwen_oauth_title">Qwen | |
| OAuth</span></h3> | |
| <button id="qwen-oauth-btn" class="btn btn-primary"> | |
| <i class="fas fa-sign-in-alt"></i> <span | |
| data-i18n="auth_login.qwen_oauth_button">开始 Qwen 登录</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <p class="form-hint" style="margin-bottom: 20px;" | |
| data-i18n="auth_login.qwen_oauth_hint"> | |
| 通过设备授权流程登录 Qwen 服务,自动获取并保存认证文件。 | |
| </p> | |
| <div id="qwen-oauth-content" style="display: none;"> | |
| <div class="form-group"> | |
| <label data-i18n="auth_login.qwen_oauth_url_label">授权链接:</label> | |
| <div class="input-group"> | |
| <input type="text" id="qwen-oauth-url" readonly> | |
| <button id="qwen-open-link" class="btn btn-primary"> | |
| <i class="fas fa-external-link-alt"></i> <span | |
| data-i18n="auth_login.qwen_open_link">打开链接</span> | |
| </button> | |
| <button id="qwen-copy-link" class="btn btn-secondary"> | |
| <i class="fas fa-copy"></i> <span | |
| data-i18n="auth_login.qwen_copy_link">复制链接</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="qwen-oauth-status" class="form-hint" style="margin-top: 10px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- iFlow OAuth --> | |
| <div class="card" id="iflow-oauth-card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-stream"></i> <span | |
| data-i18n="auth_login.iflow_oauth_title">iFlow OAuth</span></h3> | |
| <button id="iflow-oauth-btn" class="btn btn-primary"> | |
| <i class="fas fa-sign-in-alt"></i> <span | |
| data-i18n="auth_login.iflow_oauth_button">开始 iFlow 登录</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <p class="form-hint" style="margin-bottom: 20px;" | |
| data-i18n="auth_login.iflow_oauth_hint"> | |
| 通过 OAuth 流程登录 iFlow 服务,自动获取并保存认证文件。 | |
| </p> | |
| <div id="iflow-oauth-content" style="display: none;"> | |
| <div class="form-group"> | |
| <label data-i18n="auth_login.iflow_oauth_url_label">授权链接:</label> | |
| <div class="input-group"> | |
| <input type="text" id="iflow-oauth-url" readonly> | |
| <button id="iflow-open-link" class="btn btn-primary"> | |
| <i class="fas fa-external-link-alt"></i> <span | |
| data-i18n="auth_login.iflow_open_link">打开链接</span> | |
| </button> | |
| <button id="iflow-copy-link" class="btn btn-secondary"> | |
| <i class="fas fa-copy"></i> <span | |
| data-i18n="auth_login.iflow_copy_link">复制链接</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="iflow-oauth-status" class="form-hint" style="margin-top: 10px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- 使用统计 --> | |
| <section id="usage-stats" class="content-section"> | |
| <h2 data-i18n="usage_stats.title">使用统计</h2> | |
| <!-- 概览统计卡片 --> | |
| <div class="stats-overview"> | |
| <div class="stat-card"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-paper-plane"></i> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-number" id="total-requests">0</div> | |
| <div class="stat-label" data-i18n="usage_stats.total_requests">总请求数</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon success"> | |
| <i class="fas fa-check-circle"></i> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-number" id="success-requests">0</div> | |
| <div class="stat-label" data-i18n="usage_stats.success_requests">成功请求</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon error"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-number" id="failed-requests">0</div> | |
| <div class="stat-label" data-i18n="usage_stats.failed_requests">失败请求</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-coins"></i> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-number" id="total-tokens">0</div> | |
| <div class="stat-label" data-i18n="usage_stats.total_tokens">总Token数</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 图表区域 --> | |
| <div class="charts-container"> | |
| <!-- 请求趋势图 --> | |
| <div class="card chart-card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-chart-line"></i> <span | |
| data-i18n="usage_stats.requests_trend">请求趋势</span></h3> | |
| <div class="chart-controls"> | |
| <button class="btn btn-small" data-period="hour" id="requests-hour-btn"> | |
| <span data-i18n="usage_stats.by_hour">按小时</span> | |
| </button> | |
| <button class="btn btn-small active" data-period="day" | |
| id="requests-day-btn"> | |
| <span data-i18n="usage_stats.by_day">按天</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <div class="chart-container"> | |
| <canvas id="requests-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Token使用趋势图 --> | |
| <div class="card chart-card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-chart-area"></i> <span | |
| data-i18n="usage_stats.tokens_trend">Token 使用趋势</span></h3> | |
| <div class="chart-controls"> | |
| <button class="btn btn-small" data-period="hour" id="tokens-hour-btn"> | |
| <span data-i18n="usage_stats.by_hour">按小时</span> | |
| </button> | |
| <button class="btn btn-small active" data-period="day" id="tokens-day-btn"> | |
| <span data-i18n="usage_stats.by_day">按天</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <div class="chart-container"> | |
| <canvas id="tokens-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- API详细统计 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-list"></i> <span data-i18n="usage_stats.api_details">API | |
| 详细统计</span></h3> | |
| <button id="refresh-usage-stats" class="btn btn-primary"> | |
| <i class="fas fa-sync-alt"></i> <span data-i18n="usage_stats.refresh">刷新</span> | |
| </button> | |
| </div> | |
| <div class="card-content"> | |
| <div id="api-stats-table" class="api-stats-table"> | |
| <div class="loading-placeholder" data-i18n="common.loading">正在加载...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- 系统信息 --> | |
| <section id="system-info" class="content-section"> | |
| <h2 data-i18n="system_info.title">系统信息</h2> | |
| <!-- 连接信息卡片 --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-server"></i> <span data-i18n="connection.title">连接信息</span> | |
| </h3> | |
| </div> | |
| <div class="card-content"> | |
| <div class="connection-info"> | |
| <div class="info-item"> | |
| <div class="info-label"> | |
| <i class="fas fa-globe"></i> | |
| <span data-i18n="connection.server_address">服务器地址:</span> | |
| </div> | |
| <div class="info-value" id="display-api-url">-</div> | |
| </div> | |
| <div class="info-item"> | |
| <div class="info-label"> | |
| <i class="fas fa-key"></i> | |
| <span data-i18n="connection.management_key">管理密钥:</span> | |
| </div> | |
| <div class="info-value" id="display-management-key">-</div> | |
| </div> | |
| <div class="info-item"> | |
| <div class="info-label"> | |
| <i class="fas fa-circle"></i> | |
| <span data-i18n="connection.status">连接状态:</span> | |
| </div> | |
| <div class="info-value" id="display-connection-status"> | |
| <span class="status-indicator disconnected" | |
| data-i18n="common.disconnected">未连接</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3><i class="fas fa-info-circle"></i> <span | |
| data-i18n="system_info.connection_status_title">连接状态</span></h3> | |
| </div> | |
| <div class="card-content"> | |
| <div id="system-status" class="status-info"> | |
| <div class="status-item"> | |
| <span class="status-label" data-i18n="system_info.api_status_label">API | |
| 状态:</span> | |
| <span id="api-status" class="status-value" | |
| data-i18n="common.disconnected">未连接</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label" | |
| data-i18n="system_info.config_status_label">配置状态:</span> | |
| <span id="config-status" class="status-value" | |
| data-i18n="system_info.not_loaded">未加载</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label" | |
| data-i18n="system_info.last_update_label">最后更新:</span> | |
| <span id="last-update" class="status-value">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- /内容区域 --> | |
| <!-- 版本信息 --> | |
| <footer class="version-footer"> | |
| <div class="version-info"> | |
| <span data-i18n="footer.version">版本</span>: v0.1.0 | |
| <span class="separator">•</span> | |
| <span data-i18n="footer.author">作者</span>: Supra4E8C | |
| </div> | |
| </footer> | |
| </div> | |
| <!-- /主内容区域 --> | |
| </div> | |
| <!-- /主内容包装器 --> | |
| </div> | |
| <!-- /主页面 --> | |
| <!-- 模态框 --> | |
| <div id="modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close">×</span> | |
| <div id="modal-body"></div> | |
| </div> | |
| </div> | |
| <!-- 通知 --> | |
| <div id="notification" class="notification"></div> | |
| </div> | |
| <script> | |
| // CLI Proxy API 管理界面 JavaScript | |
| class CLIProxyManager { | |
| constructor() { | |
| // 仅保存基础地址(不含 /v0/management),请求时自动补齐 | |
| const detectedBase = this.detectApiBaseFromLocation(); | |
| this.apiBase = detectedBase; | |
| this.apiUrl = this.computeApiUrl(this.apiBase); | |
| this.managementKey = ''; | |
| this.isConnected = false; | |
| this.isLoggedIn = false; | |
| // 配置缓存 | |
| this.configCache = null; | |
| this.cacheTimestamp = null; | |
| this.cacheExpiry = 30000; // 30秒缓存过期时间 | |
| // 状态更新定时器 | |
| this.statusUpdateTimer = null; | |
| // 主题管理 | |
| this.currentTheme = 'light'; | |
| this.init(); | |
| } | |
| // 简易防抖,减少频繁写 localStorage | |
| debounce(fn, delay = 400) { | |
| let timer; | |
| return (...args) => { | |
| clearTimeout(timer); | |
| timer = setTimeout(() => fn.apply(this, args), delay); | |
| }; | |
| } | |
| // 初始化主题 | |
| initializeTheme() { | |
| // 从本地存储获取用户偏好主题 | |
| const savedTheme = localStorage.getItem('preferredTheme'); | |
| if (savedTheme && ['light', 'dark'].includes(savedTheme)) { | |
| this.currentTheme = savedTheme; | |
| } else { | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| this.currentTheme = 'dark'; | |
| } else { | |
| this.currentTheme = 'light'; | |
| } | |
| } | |
| this.applyTheme(this.currentTheme); | |
| this.updateThemeButtons(); | |
| // 监听系统主题变化 | |
| if (window.matchMedia) { | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { | |
| if (!localStorage.getItem('preferredTheme')) { | |
| this.currentTheme = e.matches ? 'dark' : 'light'; | |
| this.applyTheme(this.currentTheme); | |
| this.updateThemeButtons(); | |
| } | |
| }); | |
| } | |
| } | |
| // 应用主题 | |
| applyTheme(theme) { | |
| if (theme === 'dark') { | |
| document.documentElement.setAttribute('data-theme', 'dark'); | |
| } else { | |
| document.documentElement.removeAttribute('data-theme'); | |
| } | |
| this.currentTheme = theme; | |
| } | |
| // 切换主题 | |
| toggleTheme() { | |
| const newTheme = this.currentTheme === 'light' ? 'dark' : 'light'; | |
| this.applyTheme(newTheme); | |
| this.updateThemeButtons(); | |
| localStorage.setItem('preferredTheme', newTheme); | |
| } | |
| // 更新主题按钮状态 | |
| updateThemeButtons() { | |
| const loginThemeBtn = document.getElementById('theme-toggle'); | |
| const mainThemeBtn = document.getElementById('theme-toggle-main'); | |
| const updateButton = (btn) => { | |
| if (!btn) return; | |
| const icon = btn.querySelector('i'); | |
| if (this.currentTheme === 'dark') { | |
| icon.className = 'fas fa-sun'; | |
| btn.title = i18n.t('theme.switch_to_light'); | |
| } else { | |
| icon.className = 'fas fa-moon'; | |
| btn.title = i18n.t('theme.switch_to_dark'); | |
| } | |
| }; | |
| updateButton(loginThemeBtn); | |
| updateButton(mainThemeBtn); | |
| } | |
| init() { | |
| this.initializeTheme(); | |
| this.checkLoginStatus(); | |
| this.bindEvents(); | |
| this.setupNavigation(); | |
| this.setupLanguageSwitcher(); | |
| this.setupThemeSwitcher(); | |
| // loadSettings 将在登录成功后调用 | |
| this.updateLoginConnectionInfo(); | |
| // 检查主机名,如果不是 localhost 或 127.0.0.1,则隐藏 OAuth 登录框 | |
| this.checkHostAndHideOAuth(); | |
| } | |
| // 检查主机名并隐藏 OAuth 登录框 | |
| checkHostAndHideOAuth() { | |
| const hostname = window.location.hostname; | |
| const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'; | |
| if (!isLocalhost) { | |
| // 隐藏所有 OAuth 登录卡片 | |
| const oauthCards = [ | |
| 'codex-oauth-card', | |
| 'anthropic-oauth-card', | |
| 'gemini-cli-oauth-card', | |
| 'qwen-oauth-card', | |
| 'iflow-oauth-card' | |
| ]; | |
| oauthCards.forEach(cardId => { | |
| const card = document.getElementById(cardId); | |
| if (card) { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| // 如果找不到具体的卡片 ID,尝试通过类名查找 | |
| const oauthCardElements = document.querySelectorAll('.card'); | |
| oauthCardElements.forEach(card => { | |
| const cardText = card.textContent || ''; | |
| if (cardText.includes('Codex OAuth') || | |
| cardText.includes('Anthropic OAuth') || | |
| cardText.includes('Gemini CLI OAuth') || | |
| cardText.includes('Qwen OAuth') || | |
| cardText.includes('iFlow OAuth')) { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| console.log(`当前主机名: ${hostname},已隐藏 OAuth 登录框`); | |
| } | |
| } | |
| // 检查登录状态 | |
| async checkLoginStatus() { | |
| // 检查是否有保存的连接信息 | |
| const savedBase = localStorage.getItem('apiBase'); | |
| const savedKey = localStorage.getItem('managementKey'); | |
| const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; | |
| // 如果有完整的连接信息且之前已登录,尝试自动登录 | |
| if (savedBase && savedKey && wasLoggedIn) { | |
| try { | |
| console.log('检测到本地连接数据,尝试自动登录...'); | |
| this.showAutoLoginLoading(); | |
| await this.attemptAutoLogin(savedBase, savedKey); | |
| return; // 自动登录成功,不显示登录页面 | |
| } catch (error) { | |
| console.log('自动登录失败:', error.message); | |
| // 清除无效的登录状态 | |
| localStorage.removeItem('isLoggedIn'); | |
| this.hideAutoLoginLoading(); | |
| } | |
| } | |
| // 如果没有连接信息或自动登录失败,显示登录页面 | |
| this.showLoginPage(); | |
| this.loadLoginSettings(); | |
| } | |
| // 显示自动登录加载页面 | |
| showAutoLoginLoading() { | |
| document.getElementById('auto-login-loading').style.display = 'flex'; | |
| document.getElementById('login-page').style.display = 'none'; | |
| document.getElementById('main-page').style.display = 'none'; | |
| } | |
| // 隐藏自动登录加载页面 | |
| hideAutoLoginLoading() { | |
| document.getElementById('auto-login-loading').style.display = 'none'; | |
| } | |
| // 尝试自动登录 | |
| async attemptAutoLogin(apiBase, managementKey) { | |
| try { | |
| // 设置API基础地址和密钥 | |
| this.setApiBase(apiBase); | |
| this.managementKey = managementKey; | |
| // 恢复代理设置(如果有) | |
| const savedProxy = localStorage.getItem('proxyUrl'); | |
| if (savedProxy) { | |
| // 代理设置会在后续的API请求中自动使用 | |
| } | |
| // 测试连接 | |
| await this.testConnection(); | |
| // 自动登录成功 | |
| this.isLoggedIn = true; | |
| this.hideAutoLoginLoading(); | |
| this.showMainPage(); | |
| console.log('自动登录成功'); | |
| return true; | |
| } catch (error) { | |
| console.error('自动登录失败:', error); | |
| // 重置状态 | |
| this.isLoggedIn = false; | |
| this.isConnected = false; | |
| throw error; | |
| } | |
| } | |
| // 显示登录页面 | |
| showLoginPage() { | |
| document.getElementById('login-page').style.display = 'flex'; | |
| document.getElementById('main-page').style.display = 'none'; | |
| this.isLoggedIn = false; | |
| this.updateLoginConnectionInfo(); | |
| } | |
| // 显示主页面 | |
| showMainPage() { | |
| document.getElementById('login-page').style.display = 'none'; | |
| document.getElementById('main-page').style.display = 'block'; | |
| this.isLoggedIn = true; | |
| this.updateConnectionInfo(); | |
| } | |
| // 登录验证 | |
| async login(apiBase, managementKey) { | |
| try { | |
| // 设置API基础地址和密钥 | |
| this.setApiBase(apiBase); | |
| this.managementKey = managementKey; | |
| localStorage.setItem('managementKey', this.managementKey); | |
| // 测试连接并加载所有数据 | |
| await this.testConnection(); | |
| // 登录成功 | |
| this.isLoggedIn = true; | |
| localStorage.setItem('isLoggedIn', 'true'); | |
| this.showMainPage(); | |
| // 不需要再调用loadSettings,因为内部状态已经在上面设置了 | |
| return true; | |
| } catch (error) { | |
| console.error('登录失败:', error); | |
| throw error; | |
| } | |
| } | |
| // 登出 | |
| logout() { | |
| this.isLoggedIn = false; | |
| this.isConnected = false; | |
| this.clearCache(); | |
| this.stopStatusUpdateTimer(); | |
| // 清除本地存储 | |
| localStorage.removeItem('isLoggedIn'); | |
| localStorage.removeItem('managementKey'); | |
| this.showLoginPage(); | |
| } | |
| // 处理登录表单提交 | |
| async handleLogin() { | |
| const apiBaseInput = document.getElementById('login-api-base'); | |
| const managementKeyInput = document.getElementById('login-management-key'); | |
| const managementKey = managementKeyInput ? managementKeyInput.value.trim() : ''; | |
| if (!managementKey) { | |
| this.showLoginError(i18n.t('login.error_required')); | |
| return; | |
| } | |
| if (apiBaseInput && apiBaseInput.value.trim()) { | |
| this.setApiBase(apiBaseInput.value.trim()); | |
| } | |
| const submitBtn = document.getElementById('login-submit'); | |
| const originalText = submitBtn ? submitBtn.innerHTML : ''; | |
| try { | |
| if (submitBtn) { | |
| submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`; | |
| submitBtn.disabled = true; | |
| } | |
| this.hideLoginError(); | |
| this.managementKey = managementKey; | |
| localStorage.setItem('managementKey', this.managementKey); | |
| await this.login(this.apiBase, this.managementKey); | |
| } catch (error) { | |
| this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`); | |
| } finally { | |
| if (submitBtn) { | |
| submitBtn.innerHTML = originalText; | |
| submitBtn.disabled = false; | |
| } | |
| } | |
| } | |
| // 切换登录页面密钥可见性 | |
| toggleLoginKeyVisibility(button) { | |
| const inputGroup = button.closest('.input-group'); | |
| const keyInput = inputGroup.querySelector('input[type="password"], input[type="text"]'); | |
| if (keyInput.type === 'password') { | |
| keyInput.type = 'text'; | |
| button.innerHTML = '<i class="fas fa-eye-slash"></i>'; | |
| } else { | |
| keyInput.type = 'password'; | |
| button.innerHTML = '<i class="fas fa-eye"></i>'; | |
| } | |
| } | |
| // 显示登录错误 | |
| showLoginError(message) { | |
| const errorDiv = document.getElementById('login-error'); | |
| const errorMessage = document.getElementById('login-error-message'); | |
| errorMessage.textContent = message; | |
| errorDiv.style.display = 'flex'; | |
| } | |
| // 隐藏登录错误 | |
| hideLoginError() { | |
| const errorDiv = document.getElementById('login-error'); | |
| errorDiv.style.display = 'none'; | |
| } | |
| // 更新连接信息显示 | |
| updateConnectionInfo() { | |
| const apiUrlElement = document.getElementById('display-api-url'); | |
| const keyElement = document.getElementById('display-management-key'); | |
| const statusElement = document.getElementById('display-connection-status'); | |
| // 显示API地址 | |
| if (apiUrlElement) { | |
| apiUrlElement.textContent = this.apiBase || '-'; | |
| } | |
| // 显示密钥(遮蔽显示) | |
| if (keyElement) { | |
| if (this.managementKey) { | |
| const maskedKey = this.maskApiKey(this.managementKey); | |
| keyElement.textContent = maskedKey; | |
| } else { | |
| keyElement.textContent = '-'; | |
| } | |
| } | |
| // 显示连接状态 | |
| if (statusElement) { | |
| let statusHtml = ''; | |
| if (this.isConnected) { | |
| statusHtml = `<span class="status-indicator connected"><i class="fas fa-circle"></i> ${i18n.t('common.connected')}</span>`; | |
| } else { | |
| statusHtml = `<span class="status-indicator disconnected"><i class="fas fa-circle"></i> ${i18n.t('common.disconnected')}</span>`; | |
| } | |
| statusElement.innerHTML = statusHtml; | |
| } | |
| } | |
| // 加载登录页面设置 | |
| loadLoginSettings() { | |
| const savedBase = localStorage.getItem('apiBase'); | |
| const savedKey = localStorage.getItem('managementKey'); | |
| const loginKeyInput = document.getElementById('login-management-key'); | |
| const apiBaseInput = document.getElementById('login-api-base'); | |
| if (savedBase) { | |
| this.setApiBase(savedBase); | |
| } else { | |
| this.setApiBase(this.detectApiBaseFromLocation()); | |
| } | |
| if (apiBaseInput) { | |
| apiBaseInput.value = this.apiBase || ''; | |
| } | |
| if (loginKeyInput && savedKey) { | |
| loginKeyInput.value = savedKey; | |
| } | |
| this.setupLoginAutoSave(); | |
| } | |
| setupLoginAutoSave() { | |
| const loginKeyInput = document.getElementById('login-management-key'); | |
| const apiBaseInput = document.getElementById('login-api-base'); | |
| const resetButton = document.getElementById('login-reset-api-base'); | |
| const saveKey = (val) => { | |
| if (val.trim()) { | |
| this.managementKey = val; | |
| localStorage.setItem('managementKey', this.managementKey); | |
| } | |
| }; | |
| const saveKeyDebounced = this.debounce(saveKey, 500); | |
| if (loginKeyInput) { | |
| loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value)); | |
| loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value)); | |
| } | |
| if (apiBaseInput) { | |
| const persistBase = (val) => { | |
| const normalized = this.normalizeBase(val); | |
| if (normalized) { | |
| this.setApiBase(normalized); | |
| } | |
| }; | |
| const persistBaseDebounced = this.debounce(persistBase, 500); | |
| apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value)); | |
| apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value)); | |
| } | |
| if (resetButton) { | |
| resetButton.addEventListener('click', () => { | |
| const detected = this.detectApiBaseFromLocation(); | |
| this.setApiBase(detected); | |
| if (apiBaseInput) { | |
| apiBaseInput.value = detected; | |
| } | |
| }); | |
| } | |
| } | |
| // 事件绑定 | |
| bindEvents() { | |
| // 登录相关(安全绑定) | |
| const loginSubmit = document.getElementById('login-submit'); | |
| const logoutBtn = document.getElementById('logout-btn'); | |
| if (loginSubmit) { | |
| loginSubmit.addEventListener('click', () => this.handleLogin()); | |
| } | |
| if (logoutBtn) { | |
| logoutBtn.addEventListener('click', () => this.logout()); | |
| } | |
| // 密钥可见性切换事件 | |
| this.setupKeyVisibilityToggle(); | |
| // 主页面元素(延迟绑定,在显示主页面时绑定) | |
| this.bindMainPageEvents(); | |
| } | |
| // 设置密钥可见性切换 | |
| setupKeyVisibilityToggle() { | |
| const toggleButtons = document.querySelectorAll('.toggle-key-visibility'); | |
| toggleButtons.forEach(button => { | |
| button.addEventListener('click', () => this.toggleLoginKeyVisibility(button)); | |
| }); | |
| } | |
| // 绑定主页面事件 | |
| bindMainPageEvents() { | |
| // 连接状态检查 | |
| const connectionStatus = document.getElementById('connection-status'); | |
| const refreshAll = document.getElementById('refresh-all'); | |
| if (connectionStatus) { | |
| connectionStatus.addEventListener('click', () => this.checkConnectionStatus()); | |
| } | |
| if (refreshAll) { | |
| refreshAll.addEventListener('click', () => this.refreshAllData()); | |
| } | |
| // 基础设置 | |
| const debugToggle = document.getElementById('debug-toggle'); | |
| const updateProxy = document.getElementById('update-proxy'); | |
| const clearProxy = document.getElementById('clear-proxy'); | |
| const updateRetry = document.getElementById('update-retry'); | |
| const switchProjectToggle = document.getElementById('switch-project-toggle'); | |
| const switchPreviewToggle = document.getElementById('switch-preview-model-toggle'); | |
| if (debugToggle) { | |
| debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked)); | |
| } | |
| if (updateProxy) { | |
| updateProxy.addEventListener('click', () => this.updateProxyUrl()); | |
| } | |
| if (clearProxy) { | |
| clearProxy.addEventListener('click', () => this.clearProxyUrl()); | |
| } | |
| if (updateRetry) { | |
| updateRetry.addEventListener('click', () => this.updateRequestRetry()); | |
| } | |
| if (switchProjectToggle) { | |
| switchProjectToggle.addEventListener('change', (e) => this.updateSwitchProject(e.target.checked)); | |
| } | |
| if (switchPreviewToggle) { | |
| switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked)); | |
| } | |
| // API 密钥管理 | |
| const addApiKey = document.getElementById('add-api-key'); | |
| const addGeminiKey = document.getElementById('add-gemini-key'); | |
| const addCodexKey = document.getElementById('add-codex-key'); | |
| const addClaudeKey = document.getElementById('add-claude-key'); | |
| const addOpenaiProvider = document.getElementById('add-openai-provider'); | |
| if (addApiKey) { | |
| addApiKey.addEventListener('click', () => this.showAddApiKeyModal()); | |
| } | |
| if (addGeminiKey) { | |
| addGeminiKey.addEventListener('click', () => this.showAddGeminiKeyModal()); | |
| } | |
| if (addCodexKey) { | |
| addCodexKey.addEventListener('click', () => this.showAddCodexKeyModal()); | |
| } | |
| if (addClaudeKey) { | |
| addClaudeKey.addEventListener('click', () => this.showAddClaudeKeyModal()); | |
| } | |
| if (addOpenaiProvider) { | |
| addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal()); | |
| } | |
| // Gemini Web Token | |
| const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn'); | |
| if (geminiWebTokenBtn) { | |
| geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal()); | |
| } | |
| // 认证文件管理 | |
| const uploadAuthFile = document.getElementById('upload-auth-file'); | |
| const deleteAllAuthFiles = document.getElementById('delete-all-auth-files'); | |
| const authFileInput = document.getElementById('auth-file-input'); | |
| if (uploadAuthFile) { | |
| uploadAuthFile.addEventListener('click', () => this.uploadAuthFile()); | |
| } | |
| if (deleteAllAuthFiles) { | |
| deleteAllAuthFiles.addEventListener('click', () => this.deleteAllAuthFiles()); | |
| } | |
| if (authFileInput) { | |
| authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); | |
| } | |
| // Codex OAuth | |
| const codexOauthBtn = document.getElementById('codex-oauth-btn'); | |
| const codexOpenLink = document.getElementById('codex-open-link'); | |
| const codexCopyLink = document.getElementById('codex-copy-link'); | |
| if (codexOauthBtn) { | |
| codexOauthBtn.addEventListener('click', () => this.startCodexOAuth()); | |
| } | |
| if (codexOpenLink) { | |
| codexOpenLink.addEventListener('click', () => this.openCodexLink()); | |
| } | |
| if (codexCopyLink) { | |
| codexCopyLink.addEventListener('click', () => this.copyCodexLink()); | |
| } | |
| // Anthropic OAuth | |
| const anthropicOauthBtn = document.getElementById('anthropic-oauth-btn'); | |
| const anthropicOpenLink = document.getElementById('anthropic-open-link'); | |
| const anthropicCopyLink = document.getElementById('anthropic-copy-link'); | |
| if (anthropicOauthBtn) { | |
| anthropicOauthBtn.addEventListener('click', () => this.startAnthropicOAuth()); | |
| } | |
| if (anthropicOpenLink) { | |
| anthropicOpenLink.addEventListener('click', () => this.openAnthropicLink()); | |
| } | |
| if (anthropicCopyLink) { | |
| anthropicCopyLink.addEventListener('click', () => this.copyAnthropicLink()); | |
| } | |
| // Gemini CLI OAuth | |
| const geminiCliOauthBtn = document.getElementById('gemini-cli-oauth-btn'); | |
| const geminiCliOpenLink = document.getElementById('gemini-cli-open-link'); | |
| const geminiCliCopyLink = document.getElementById('gemini-cli-copy-link'); | |
| if (geminiCliOauthBtn) { | |
| geminiCliOauthBtn.addEventListener('click', () => this.startGeminiCliOAuth()); | |
| } | |
| if (geminiCliOpenLink) { | |
| geminiCliOpenLink.addEventListener('click', () => this.openGeminiCliLink()); | |
| } | |
| if (geminiCliCopyLink) { | |
| geminiCliCopyLink.addEventListener('click', () => this.copyGeminiCliLink()); | |
| } | |
| // Qwen OAuth | |
| const qwenOauthBtn = document.getElementById('qwen-oauth-btn'); | |
| const qwenOpenLink = document.getElementById('qwen-open-link'); | |
| const qwenCopyLink = document.getElementById('qwen-copy-link'); | |
| if (qwenOauthBtn) { | |
| qwenOauthBtn.addEventListener('click', () => this.startQwenOAuth()); | |
| } | |
| if (qwenOpenLink) { | |
| qwenOpenLink.addEventListener('click', () => this.openQwenLink()); | |
| } | |
| if (qwenCopyLink) { | |
| qwenCopyLink.addEventListener('click', () => this.copyQwenLink()); | |
| } | |
| // iFlow OAuth | |
| const iflowOauthBtn = document.getElementById('iflow-oauth-btn'); | |
| const iflowOpenLink = document.getElementById('iflow-open-link'); | |
| const iflowCopyLink = document.getElementById('iflow-copy-link'); | |
| if (iflowOauthBtn) { | |
| iflowOauthBtn.addEventListener('click', () => this.startIflowOAuth()); | |
| } | |
| if (iflowOpenLink) { | |
| iflowOpenLink.addEventListener('click', () => this.openIflowLink()); | |
| } | |
| if (iflowCopyLink) { | |
| iflowCopyLink.addEventListener('click', () => this.copyIflowLink()); | |
| } | |
| // 使用统计 | |
| const refreshUsageStats = document.getElementById('refresh-usage-stats'); | |
| const requestsHourBtn = document.getElementById('requests-hour-btn'); | |
| const requestsDayBtn = document.getElementById('requests-day-btn'); | |
| const tokensHourBtn = document.getElementById('tokens-hour-btn'); | |
| const tokensDayBtn = document.getElementById('tokens-day-btn'); | |
| if (refreshUsageStats) { | |
| refreshUsageStats.addEventListener('click', () => this.loadUsageStats()); | |
| } | |
| if (requestsHourBtn) { | |
| requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour')); | |
| } | |
| if (requestsDayBtn) { | |
| requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day')); | |
| } | |
| if (tokensHourBtn) { | |
| tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour')); | |
| } | |
| if (tokensDayBtn) { | |
| tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day')); | |
| } | |
| // 模态框 | |
| const closeBtn = document.querySelector('.close'); | |
| if (closeBtn) { | |
| closeBtn.addEventListener('click', () => this.closeModal()); | |
| } | |
| window.addEventListener('click', (e) => { | |
| const modal = document.getElementById('modal'); | |
| if (modal && e.target === modal) { | |
| this.closeModal(); | |
| } | |
| }); | |
| // 移动端菜单按钮 | |
| const mobileMenuBtn = document.getElementById('mobile-menu-btn'); | |
| const sidebarOverlay = document.getElementById('sidebar-overlay'); | |
| const sidebar = document.getElementById('sidebar'); | |
| if (mobileMenuBtn) { | |
| mobileMenuBtn.addEventListener('click', () => this.toggleMobileSidebar()); | |
| } | |
| if (sidebarOverlay) { | |
| sidebarOverlay.addEventListener('click', () => this.closeMobileSidebar()); | |
| } | |
| // 侧边栏收起/展开按钮(桌面端) | |
| const sidebarToggleBtnDesktop = document.getElementById('sidebar-toggle-btn-desktop'); | |
| if (sidebarToggleBtnDesktop) { | |
| sidebarToggleBtnDesktop.addEventListener('click', () => this.toggleSidebar()); | |
| } | |
| // 从本地存储恢复侧边栏状态 | |
| this.restoreSidebarState(); | |
| // 监听窗口大小变化 | |
| window.addEventListener('resize', () => { | |
| const sidebar = document.getElementById('sidebar'); | |
| const layout = document.getElementById('layout-container'); | |
| if (window.innerWidth <= 1024) { | |
| // 移动端:移除收起状态 | |
| if (sidebar && layout) { | |
| sidebar.classList.remove('collapsed'); | |
| layout.classList.remove('sidebar-collapsed'); | |
| } | |
| } else { | |
| // 桌面端:恢复保存的状态 | |
| this.restoreSidebarState(); | |
| } | |
| }); | |
| // 点击侧边栏导航项时在移动端关闭侧边栏 | |
| const navItems = document.querySelectorAll('.nav-item'); | |
| navItems.forEach(item => { | |
| item.addEventListener('click', () => { | |
| if (window.innerWidth <= 1024) { | |
| this.closeMobileSidebar(); | |
| } | |
| }); | |
| }); | |
| } | |
| // 切换移动端侧边栏 | |
| toggleMobileSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('sidebar-overlay'); | |
| const layout = document.getElementById('layout-container'); | |
| const mainWrapper = document.getElementById('main-wrapper'); | |
| if (sidebar && overlay) { | |
| const isOpen = sidebar.classList.toggle('mobile-open'); | |
| overlay.classList.toggle('active'); | |
| if (layout) { | |
| layout.classList.toggle('sidebar-open', isOpen); | |
| } | |
| if (mainWrapper) { | |
| mainWrapper.classList.toggle('sidebar-open', isOpen); | |
| } | |
| } | |
| } | |
| // 关闭移动端侧边栏 | |
| closeMobileSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('sidebar-overlay'); | |
| const layout = document.getElementById('layout-container'); | |
| const mainWrapper = document.getElementById('main-wrapper'); | |
| if (sidebar && overlay) { | |
| sidebar.classList.remove('mobile-open'); | |
| overlay.classList.remove('active'); | |
| if (layout) { | |
| layout.classList.remove('sidebar-open'); | |
| } | |
| if (mainWrapper) { | |
| mainWrapper.classList.remove('sidebar-open'); | |
| } | |
| } | |
| } | |
| // 切换侧边栏收起/展开状态 | |
| toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const layout = document.getElementById('layout-container'); | |
| if (sidebar && layout) { | |
| const isCollapsed = sidebar.classList.toggle('collapsed'); | |
| layout.classList.toggle('sidebar-collapsed', isCollapsed); | |
| // 保存状态到本地存储 | |
| localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false'); | |
| // 更新按钮提示文本 | |
| const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop'); | |
| if (toggleBtn) { | |
| toggleBtn.setAttribute('title', isCollapsed ? '展开侧边栏' : '收起侧边栏'); | |
| } | |
| } | |
| } | |
| // 恢复侧边栏状态 | |
| restoreSidebarState() { | |
| // 只在桌面端恢复侧栏状态 | |
| if (window.innerWidth > 1024) { | |
| const savedState = localStorage.getItem('sidebarCollapsed'); | |
| if (savedState === 'true') { | |
| const sidebar = document.getElementById('sidebar'); | |
| const layout = document.getElementById('layout-container'); | |
| if (sidebar && layout) { | |
| sidebar.classList.add('collapsed'); | |
| layout.classList.add('sidebar-collapsed'); | |
| // 更新按钮提示文本 | |
| const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop'); | |
| if (toggleBtn) { | |
| toggleBtn.setAttribute('title', '展开侧边栏'); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // 设置导航 | |
| setupNavigation() { | |
| const navItems = document.querySelectorAll('.nav-item'); | |
| navItems.forEach(item => { | |
| item.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| // 移除所有活动状态 | |
| navItems.forEach(nav => nav.classList.remove('active')); | |
| document.querySelectorAll('.content-section').forEach(section => section.classList.remove('active')); | |
| // 添加活动状态 | |
| item.classList.add('active'); | |
| const sectionId = item.getAttribute('data-section'); | |
| document.getElementById(sectionId).classList.add('active'); | |
| }); | |
| }); | |
| } | |
| // 设置语言切换 | |
| setupLanguageSwitcher() { | |
| const loginToggle = document.getElementById('language-toggle'); | |
| const mainToggle = document.getElementById('language-toggle-main'); | |
| if (loginToggle) { | |
| loginToggle.addEventListener('click', () => this.toggleLanguage()); | |
| } | |
| if (mainToggle) { | |
| mainToggle.addEventListener('click', () => this.toggleLanguage()); | |
| } | |
| } | |
| // 设置主题切换 | |
| setupThemeSwitcher() { | |
| const loginToggle = document.getElementById('theme-toggle'); | |
| const mainToggle = document.getElementById('theme-toggle-main'); | |
| if (loginToggle) { | |
| loginToggle.addEventListener('click', () => this.toggleTheme()); | |
| } | |
| if (mainToggle) { | |
| mainToggle.addEventListener('click', () => this.toggleTheme()); | |
| } | |
| } | |
| // 切换语言 | |
| toggleLanguage() { | |
| const currentLang = i18n.currentLanguage; | |
| const newLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN'; | |
| i18n.setLanguage(newLang); | |
| // 更新主题按钮文本 | |
| this.updateThemeButtons(); | |
| // 更新连接状态显示 | |
| this.updateConnectionStatus(); | |
| // 重新加载所有数据以更新动态内容 | |
| if (this.isLoggedIn && this.isConnected) { | |
| this.loadAllData(true); | |
| } | |
| } | |
| // 规范化基础地址,移除尾部斜杠与 /v0/management | |
| normalizeBase(input) { | |
| let base = (input || '').trim(); | |
| if (!base) return ''; | |
| // 若用户粘贴了完整地址,剥离后缀 | |
| base = base.replace(/\/?v0\/management\/?$/i, ''); | |
| base = base.replace(/\/+$/i, ''); | |
| // 自动补 http:// | |
| if (!/^https?:\/\//i.test(base)) { | |
| base = 'http://' + base; | |
| } | |
| return base; | |
| } | |
| // 由基础地址生成完整管理 API 地址 | |
| computeApiUrl(base) { | |
| const b = this.normalizeBase(base); | |
| if (!b) return ''; | |
| return b.replace(/\/$/, '') + '/v0/management'; | |
| } | |
| setApiBase(newBase) { | |
| this.apiBase = this.normalizeBase(newBase); | |
| this.apiUrl = this.computeApiUrl(this.apiBase); | |
| localStorage.setItem('apiBase', this.apiBase); | |
| localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 | |
| this.updateLoginConnectionInfo(); | |
| } | |
| // 加载设置(简化版,仅加载内部状态) | |
| loadSettings() { | |
| const savedBase = localStorage.getItem('apiBase'); | |
| const savedUrl = localStorage.getItem('apiUrl'); | |
| const savedKey = localStorage.getItem('managementKey'); | |
| if (savedBase) { | |
| this.setApiBase(savedBase); | |
| } else if (savedUrl) { | |
| const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, ''); | |
| this.setApiBase(base); | |
| } else { | |
| this.setApiBase(this.detectApiBaseFromLocation()); | |
| } | |
| if (savedKey) { | |
| this.managementKey = savedKey; | |
| } | |
| this.updateLoginConnectionInfo(); | |
| } | |
| // API 请求方法 | |
| async makeRequest(endpoint, options = {}) { | |
| const url = `${this.apiUrl}${endpoint}`; | |
| const headers = { | |
| 'Authorization': `Bearer ${this.managementKey}`, | |
| 'Content-Type': 'application/json', | |
| ...options.headers | |
| }; | |
| try { | |
| const response = await fetch(url, { | |
| ...options, | |
| headers | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.error || `HTTP ${response.status}`); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('API请求失败:', error); | |
| throw error; | |
| } | |
| } | |
| // 显示通知 | |
| showNotification(message, type = 'info') { | |
| const notification = document.getElementById('notification'); | |
| notification.textContent = message; | |
| notification.className = `notification ${type}`; | |
| notification.classList.add('show'); | |
| setTimeout(() => { | |
| notification.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // 密钥可见性切换 | |
| toggleKeyVisibility() { | |
| const keyInput = document.getElementById('management-key'); | |
| const toggleButton = document.getElementById('toggle-key-visibility'); | |
| if (keyInput.type === 'password') { | |
| keyInput.type = 'text'; | |
| toggleButton.innerHTML = '<i class="fas fa-eye-slash"></i>'; | |
| } else { | |
| keyInput.type = 'password'; | |
| toggleButton.innerHTML = '<i class="fas fa-eye"></i>'; | |
| } | |
| } | |
| // 测试连接(简化版,用于内部调用) | |
| async testConnection() { | |
| try { | |
| await this.makeRequest('/debug'); | |
| this.isConnected = true; | |
| this.updateConnectionStatus(); | |
| this.startStatusUpdateTimer(); | |
| await this.loadAllData(); | |
| return true; | |
| } catch (error) { | |
| this.isConnected = false; | |
| this.updateConnectionStatus(); | |
| this.stopStatusUpdateTimer(); | |
| throw error; | |
| } | |
| } | |
| // 更新连接状态 | |
| updateConnectionStatus() { | |
| const statusButton = document.getElementById('connection-status'); | |
| const apiStatus = document.getElementById('api-status'); | |
| const configStatus = document.getElementById('config-status'); | |
| const lastUpdate = document.getElementById('last-update'); | |
| if (this.isConnected) { | |
| statusButton.innerHTML = `<i class="fas fa-circle connection-indicator connected"></i> ${i18n.t('common.connected')}`; | |
| statusButton.className = 'btn btn-success'; | |
| apiStatus.textContent = i18n.t('common.connected'); | |
| // 更新配置状态 | |
| if (this.isCacheValid()) { | |
| const cacheAge = Math.floor((Date.now() - this.cacheTimestamp) / 1000); | |
| configStatus.textContent = `${i18n.t('system_info.cache_data')} (${cacheAge}${i18n.t('system_info.seconds_ago')})`; | |
| configStatus.style.color = '#f59e0b'; // 橙色表示缓存 | |
| } else if (this.configCache) { | |
| configStatus.textContent = i18n.t('system_info.real_time_data'); | |
| configStatus.style.color = '#10b981'; // 绿色表示实时 | |
| } else { | |
| configStatus.textContent = i18n.t('system_info.not_loaded'); | |
| configStatus.style.color = '#6b7280'; // 灰色表示未加载 | |
| } | |
| } else { | |
| statusButton.innerHTML = `<i class="fas fa-circle connection-indicator disconnected"></i> ${i18n.t('common.disconnected')}`; | |
| statusButton.className = 'btn btn-danger'; | |
| apiStatus.textContent = i18n.t('common.disconnected'); | |
| configStatus.textContent = i18n.t('system_info.not_loaded'); | |
| configStatus.style.color = '#6b7280'; | |
| } | |
| lastUpdate.textContent = new Date().toLocaleString('zh-CN'); | |
| // 更新连接信息显示 | |
| this.updateConnectionInfo(); | |
| } | |
| // 检查连接状态 | |
| async checkConnectionStatus() { | |
| await this.testConnection(); | |
| } | |
| // 刷新所有数据 | |
| async refreshAllData() { | |
| if (!this.isConnected) { | |
| this.showNotification(i18n.t('notification.connection_required'), 'error'); | |
| return; | |
| } | |
| const button = document.getElementById('refresh-all'); | |
| const originalText = button.innerHTML; | |
| button.innerHTML = `<div class="loading"></div> ${i18n.t('common.loading')}`; | |
| button.disabled = true; | |
| try { | |
| // 强制刷新,清除缓存 | |
| await this.loadAllData(true); | |
| this.showNotification(i18n.t('notification.data_refreshed'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.refresh_failed')}: ${error.message}`, 'error'); | |
| } finally { | |
| button.innerHTML = originalText; | |
| button.disabled = false; | |
| } | |
| } | |
| // 检查缓存是否有效 | |
| isCacheValid() { | |
| if (!this.configCache || !this.cacheTimestamp) { | |
| return false; | |
| } | |
| return (Date.now() - this.cacheTimestamp) < this.cacheExpiry; | |
| } | |
| // 获取配置(优先使用缓存) | |
| async getConfig(forceRefresh = false) { | |
| if (!forceRefresh && this.isCacheValid()) { | |
| this.updateConnectionStatus(); // 更新状态显示 | |
| return this.configCache; | |
| } | |
| try { | |
| const config = await this.makeRequest('/config'); | |
| this.configCache = config; | |
| this.cacheTimestamp = Date.now(); | |
| this.updateConnectionStatus(); // 更新状态显示 | |
| return config; | |
| } catch (error) { | |
| console.error('获取配置失败:', error); | |
| throw error; | |
| } | |
| } | |
| // 清除缓存 | |
| clearCache() { | |
| this.configCache = null; | |
| this.cacheTimestamp = null; | |
| } | |
| // 启动状态更新定时器 | |
| startStatusUpdateTimer() { | |
| if (this.statusUpdateTimer) { | |
| clearInterval(this.statusUpdateTimer); | |
| } | |
| this.statusUpdateTimer = setInterval(() => { | |
| if (this.isConnected) { | |
| this.updateConnectionStatus(); | |
| } | |
| }, 1000); // 每秒更新一次 | |
| } | |
| // 停止状态更新定时器 | |
| stopStatusUpdateTimer() { | |
| if (this.statusUpdateTimer) { | |
| clearInterval(this.statusUpdateTimer); | |
| this.statusUpdateTimer = null; | |
| } | |
| } | |
| // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置 | |
| async loadAllData(forceRefresh = false) { | |
| try { | |
| console.log('使用新的 /config 端点加载所有配置...'); | |
| // 使用新的 /config 端点一次性获取所有配置 | |
| const config = await this.getConfig(forceRefresh); | |
| // 从配置中提取并设置各个设置项 | |
| this.updateSettingsFromConfig(config); | |
| // 认证文件需要单独加载,因为不在配置中 | |
| await this.loadAuthFiles(); | |
| // 使用统计需要单独加载 | |
| await this.loadUsageStats(); | |
| console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); | |
| } catch (error) { | |
| console.error('加载配置失败:', error); | |
| console.log('回退到逐个加载方式...'); | |
| // 如果新方法失败,回退到原来的逐个加载方式 | |
| await this.loadAllDataLegacy(); | |
| } | |
| } | |
| // 从配置对象更新所有设置 | |
| updateSettingsFromConfig(config) { | |
| // 调试设置 | |
| if (config.debug !== undefined) { | |
| document.getElementById('debug-toggle').checked = config.debug; | |
| } | |
| // 代理设置 | |
| if (config['proxy-url'] !== undefined) { | |
| document.getElementById('proxy-url').value = config['proxy-url'] || ''; | |
| } | |
| // 请求重试设置 | |
| if (config['request-retry'] !== undefined) { | |
| document.getElementById('request-retry').value = config['request-retry']; | |
| } | |
| // 配额超出行为 | |
| if (config['quota-exceeded']) { | |
| if (config['quota-exceeded']['switch-project'] !== undefined) { | |
| document.getElementById('switch-project-toggle').checked = config['quota-exceeded']['switch-project']; | |
| } | |
| if (config['quota-exceeded']['switch-preview-model'] !== undefined) { | |
| document.getElementById('switch-preview-model-toggle').checked = config['quota-exceeded']['switch-preview-model']; | |
| } | |
| } | |
| // API 密钥 | |
| if (config['api-keys']) { | |
| this.renderApiKeys(config['api-keys']); | |
| } | |
| // Gemini 密钥 | |
| if (config['generative-language-api-key']) { | |
| this.renderGeminiKeys(config['generative-language-api-key']); | |
| } | |
| // Codex 密钥 | |
| if (config['codex-api-key']) { | |
| this.renderCodexKeys(config['codex-api-key']); | |
| } | |
| // Claude 密钥 | |
| if (config['claude-api-key']) { | |
| this.renderClaudeKeys(config['claude-api-key']); | |
| } | |
| // OpenAI 兼容提供商 | |
| if (config['openai-compatibility']) { | |
| this.renderOpenAIProviders(config['openai-compatibility']); | |
| } | |
| } | |
| // 回退方法:原来的逐个加载方式 | |
| async loadAllDataLegacy() { | |
| await Promise.all([ | |
| this.loadDebugSettings(), | |
| this.loadProxySettings(), | |
| this.loadRetrySettings(), | |
| this.loadQuotaSettings(), | |
| this.loadApiKeys(), | |
| this.loadGeminiKeys(), | |
| this.loadCodexKeys(), | |
| this.loadClaudeKeys(), | |
| this.loadOpenAIProviders(), | |
| this.loadAuthFiles() | |
| ]); | |
| } | |
| // 加载调试设置 | |
| async loadDebugSettings() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config.debug !== undefined) { | |
| document.getElementById('debug-toggle').checked = config.debug; | |
| } | |
| } catch (error) { | |
| console.error('加载调试设置失败:', error); | |
| } | |
| } | |
| // 更新调试设置 | |
| async updateDebug(enabled) { | |
| try { | |
| await this.makeRequest('/debug', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value: enabled }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.showNotification(i18n.t('notification.debug_updated'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| // 恢复原状态 | |
| document.getElementById('debug-toggle').checked = !enabled; | |
| } | |
| } | |
| // 加载代理设置 | |
| async loadProxySettings() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['proxy-url'] !== undefined) { | |
| document.getElementById('proxy-url').value = config['proxy-url'] || ''; | |
| } | |
| } catch (error) { | |
| console.error('加载代理设置失败:', error); | |
| } | |
| } | |
| // 更新代理URL | |
| async updateProxyUrl() { | |
| const proxyUrl = document.getElementById('proxy-url').value.trim(); | |
| try { | |
| await this.makeRequest('/proxy-url', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value: proxyUrl }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.showNotification(i18n.t('notification.proxy_updated'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 清空代理URL | |
| async clearProxyUrl() { | |
| try { | |
| await this.makeRequest('/proxy-url', { method: 'DELETE' }); | |
| document.getElementById('proxy-url').value = ''; | |
| this.clearCache(); // 清除缓存 | |
| this.showNotification(i18n.t('notification.proxy_cleared'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载重试设置 | |
| async loadRetrySettings() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['request-retry'] !== undefined) { | |
| document.getElementById('request-retry').value = config['request-retry']; | |
| } | |
| } catch (error) { | |
| console.error('加载重试设置失败:', error); | |
| } | |
| } | |
| // 更新请求重试 | |
| async updateRequestRetry() { | |
| const retryCount = parseInt(document.getElementById('request-retry').value); | |
| try { | |
| await this.makeRequest('/request-retry', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value: retryCount }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.showNotification(i18n.t('notification.retry_updated'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载配额设置 | |
| async loadQuotaSettings() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['quota-exceeded']) { | |
| if (config['quota-exceeded']['switch-project'] !== undefined) { | |
| document.getElementById('switch-project-toggle').checked = config['quota-exceeded']['switch-project']; | |
| } | |
| if (config['quota-exceeded']['switch-preview-model'] !== undefined) { | |
| document.getElementById('switch-preview-model-toggle').checked = config['quota-exceeded']['switch-preview-model']; | |
| } | |
| } | |
| } catch (error) { | |
| console.error('加载配额设置失败:', error); | |
| } | |
| } | |
| // 更新项目切换设置 | |
| async updateSwitchProject(enabled) { | |
| try { | |
| await this.makeRequest('/quota-exceeded/switch-project', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value: enabled }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.showNotification(i18n.t('notification.quota_switch_project_updated'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| document.getElementById('switch-project-toggle').checked = !enabled; | |
| } | |
| } | |
| // 更新预览模型切换设置 | |
| async updateSwitchPreviewModel(enabled) { | |
| try { | |
| await this.makeRequest('/quota-exceeded/switch-preview-model', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value: enabled }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.showNotification(i18n.t('notification.quota_switch_preview_updated'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| document.getElementById('switch-preview-model-toggle').checked = !enabled; | |
| } | |
| } | |
| // 加载API密钥 | |
| async loadApiKeys() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['api-keys']) { | |
| this.renderApiKeys(config['api-keys']); | |
| } | |
| } catch (error) { | |
| console.error('加载API密钥失败:', error); | |
| } | |
| } | |
| // 渲染API密钥列表 | |
| renderApiKeys(keys) { | |
| const container = document.getElementById('api-keys-list'); | |
| if (keys.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-key"></i> | |
| <h3>${i18n.t('api_keys.empty_title')}</h3> | |
| <p>${i18n.t('api_keys.empty_desc')}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = keys.map((key, index) => ` | |
| <div class="key-item"> | |
| <div class="item-content"> | |
| <div class="item-title">${i18n.t('api_keys.item_title')} #${index + 1}</div> | |
| <div class="item-value">${this.maskApiKey(key)}</div> | |
| </div> | |
| <div class="item-actions"> | |
| <button class="btn btn-secondary" onclick="manager.editApiKey(${index}, '${key}')"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="btn btn-danger" onclick="manager.deleteApiKey(${index})"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // 遮蔽API密钥显示 | |
| maskApiKey(key) { | |
| if (key.length <= 8) return key; | |
| return key.substring(0, 4) + '...' + key.substring(key.length - 4); | |
| } | |
| // HTML 转义,防止 XSS | |
| escapeHtml(value) { | |
| if (value === null || value === undefined) return ''; | |
| return String(value) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } | |
| // 显示添加API密钥模态框 | |
| showAddApiKeyModal() { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('api_keys.add_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="new-api-key">${i18n.t('api_keys.add_modal_key_label')}</label> | |
| <input type="text" id="new-api-key" placeholder="${i18n.t('api_keys.add_modal_key_placeholder')}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.addApiKey()">${i18n.t('common.add')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 添加API密钥 | |
| async addApiKey() { | |
| const newKey = document.getElementById('new-api-key').value.trim(); | |
| if (!newKey) { | |
| this.showNotification(`${i18n.t('notification.please_enter')} ${i18n.t('notification.api_key')}`, 'error'); | |
| return; | |
| } | |
| try { | |
| const data = await this.makeRequest('/api-keys'); | |
| const currentKeys = data['api-keys'] || []; | |
| currentKeys.push(newKey); | |
| await this.makeRequest('/api-keys', { | |
| method: 'PUT', | |
| body: JSON.stringify(currentKeys) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadApiKeys(); | |
| this.showNotification(i18n.t('notification.api_key_added'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 编辑API密钥 | |
| editApiKey(index, currentKey) { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('api_keys.edit_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="edit-api-key">${i18n.t('api_keys.edit_modal_key_label')}</label> | |
| <input type="text" id="edit-api-key" value="${currentKey}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.updateApiKey(${index})">${i18n.t('common.update')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 更新API密钥 | |
| async updateApiKey(index) { | |
| const newKey = document.getElementById('edit-api-key').value.trim(); | |
| if (!newKey) { | |
| this.showNotification(`${i18n.t('notification.please_enter')} ${i18n.t('notification.api_key')}`, 'error'); | |
| return; | |
| } | |
| try { | |
| await this.makeRequest('/api-keys', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ index, value: newKey }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadApiKeys(); | |
| this.showNotification(i18n.t('notification.api_key_updated'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除API密钥 | |
| async deleteApiKey(index) { | |
| if (!confirm(i18n.t('api_keys.delete_confirm'))) return; | |
| try { | |
| await this.makeRequest(`/api-keys?index=${index}`, { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadApiKeys(); | |
| this.showNotification(i18n.t('notification.api_key_deleted'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载Gemini密钥 | |
| async loadGeminiKeys() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['generative-language-api-key']) { | |
| this.renderGeminiKeys(config['generative-language-api-key']); | |
| } | |
| } catch (error) { | |
| console.error('加载Gemini密钥失败:', error); | |
| } | |
| } | |
| // 渲染Gemini密钥列表 | |
| renderGeminiKeys(keys) { | |
| const container = document.getElementById('gemini-keys-list'); | |
| if (keys.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fab fa-google"></i> | |
| <h3>${i18n.t('ai_providers.gemini_empty_title')}</h3> | |
| <p>${i18n.t('ai_providers.gemini_empty_desc')}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = keys.map((key, index) => ` | |
| <div class="key-item"> | |
| <div class="item-content"> | |
| <div class="item-title">${i18n.t('ai_providers.gemini_item_title')} #${index + 1}</div> | |
| <div class="item-value">${this.maskApiKey(key)}</div> | |
| </div> | |
| <div class="item-actions"> | |
| <button class="btn btn-secondary" onclick="manager.editGeminiKey(${index}, '${key}')"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="btn btn-danger" onclick="manager.deleteGeminiKey('${key}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // 显示添加Gemini密钥模态框 | |
| showAddGeminiKeyModal() { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>添加Gemini API密钥</h3> | |
| <div class="form-group"> | |
| <label for="new-gemini-key">API密钥:</label> | |
| <input type="text" id="new-gemini-key" placeholder="请输入Gemini API密钥"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> | |
| <button class="btn btn-primary" onclick="manager.addGeminiKey()">添加</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 添加Gemini密钥 | |
| async addGeminiKey() { | |
| const newKey = document.getElementById('new-gemini-key').value.trim(); | |
| if (!newKey) { | |
| this.showNotification('请输入Gemini API密钥', 'error'); | |
| return; | |
| } | |
| try { | |
| const data = await this.makeRequest('/generative-language-api-key'); | |
| const currentKeys = data['generative-language-api-key'] || []; | |
| currentKeys.push(newKey); | |
| await this.makeRequest('/generative-language-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(currentKeys) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadGeminiKeys(); | |
| this.showNotification('Gemini密钥添加成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 编辑Gemini密钥 | |
| editGeminiKey(index, currentKey) { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>编辑Gemini API密钥</h3> | |
| <div class="form-group"> | |
| <label for="edit-gemini-key">API密钥:</label> | |
| <input type="text" id="edit-gemini-key" value="${currentKey}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> | |
| <button class="btn btn-primary" onclick="manager.updateGeminiKey('${currentKey}')">更新</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 更新Gemini密钥 | |
| async updateGeminiKey(oldKey) { | |
| const newKey = document.getElementById('edit-gemini-key').value.trim(); | |
| if (!newKey) { | |
| this.showNotification('请输入Gemini API密钥', 'error'); | |
| return; | |
| } | |
| try { | |
| await this.makeRequest('/generative-language-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ old: oldKey, new: newKey }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadGeminiKeys(); | |
| this.showNotification('Gemini密钥更新成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除Gemini密钥 | |
| async deleteGeminiKey(key) { | |
| if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return; | |
| try { | |
| await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadGeminiKeys(); | |
| this.showNotification('Gemini密钥删除成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载Codex密钥 | |
| async loadCodexKeys() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['codex-api-key']) { | |
| this.renderCodexKeys(config['codex-api-key']); | |
| } | |
| } catch (error) { | |
| console.error('加载Codex密钥失败:', error); | |
| } | |
| } | |
| // 渲染Codex密钥列表 | |
| renderCodexKeys(keys) { | |
| const container = document.getElementById('codex-keys-list'); | |
| if (keys.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-code"></i> | |
| <h3>${i18n.t('ai_providers.codex_empty_title')}</h3> | |
| <p>${i18n.t('ai_providers.codex_empty_desc')}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = keys.map((config, index) => ` | |
| <div class="provider-item"> | |
| <div class="item-content"> | |
| <div class="item-title">${i18n.t('ai_providers.codex_item_title')} #${index + 1}</div> | |
| <div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div> | |
| ${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''} | |
| ${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''} | |
| </div> | |
| <div class="item-actions"> | |
| <button class="btn btn-secondary" onclick="manager.editCodexKey(${index}, ${JSON.stringify(config).replace(/"/g, '"')})"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="btn btn-danger" onclick="manager.deleteCodexKey('${config['api-key']}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // 显示添加Codex密钥模态框 | |
| showAddCodexKeyModal() { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('ai_providers.codex_add_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="new-codex-key">${i18n.t('ai_providers.codex_add_modal_key_label')}</label> | |
| <input type="text" id="new-codex-key" placeholder="${i18n.t('ai_providers.codex_add_modal_key_placeholder')}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-codex-url">${i18n.t('ai_providers.codex_add_modal_url_label')}</label> | |
| <input type="text" id="new-codex-url" placeholder="${i18n.t('ai_providers.codex_add_modal_url_placeholder')}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-codex-proxy">${i18n.t('ai_providers.codex_add_modal_proxy_label')}</label> | |
| <input type="text" id="new-codex-proxy" placeholder="${i18n.t('ai_providers.codex_add_modal_proxy_placeholder')}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.addCodexKey()">${i18n.t('common.add')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 添加Codex密钥 | |
| async addCodexKey() { | |
| const apiKey = document.getElementById('new-codex-key').value.trim(); | |
| const baseUrl = document.getElementById('new-codex-url').value.trim(); | |
| const proxyUrl = document.getElementById('new-codex-proxy').value.trim(); | |
| if (!apiKey) { | |
| this.showNotification(i18n.t('notification.field_required'), 'error'); | |
| return; | |
| } | |
| try { | |
| const data = await this.makeRequest('/codex-api-key'); | |
| const currentKeys = data['codex-api-key'] || []; | |
| const newConfig = { 'api-key': apiKey }; | |
| if (baseUrl) { | |
| newConfig['base-url'] = baseUrl; | |
| } | |
| if (proxyUrl) { | |
| newConfig['proxy-url'] = proxyUrl; | |
| } | |
| currentKeys.push(newConfig); | |
| await this.makeRequest('/codex-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(currentKeys) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadCodexKeys(); | |
| this.showNotification('Codex配置添加成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`添加Codex配置失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 编辑Codex密钥 | |
| editCodexKey(index, config) { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('ai_providers.codex_edit_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="edit-codex-key">${i18n.t('ai_providers.codex_edit_modal_key_label')}</label> | |
| <input type="text" id="edit-codex-key" value="${config['api-key']}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-codex-url">${i18n.t('ai_providers.codex_edit_modal_url_label')}</label> | |
| <input type="text" id="edit-codex-url" value="${config['base-url'] || ''}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-codex-proxy">${i18n.t('ai_providers.codex_edit_modal_proxy_label')}</label> | |
| <input type="text" id="edit-codex-proxy" value="${config['proxy-url'] || ''}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">${i18n.t('common.update')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 更新Codex密钥 | |
| async updateCodexKey(index) { | |
| const apiKey = document.getElementById('edit-codex-key').value.trim(); | |
| const baseUrl = document.getElementById('edit-codex-url').value.trim(); | |
| const proxyUrl = document.getElementById('edit-codex-proxy').value.trim(); | |
| if (!apiKey) { | |
| this.showNotification(i18n.t('notification.field_required'), 'error'); | |
| return; | |
| } | |
| try { | |
| const newConfig = { 'api-key': apiKey }; | |
| if (baseUrl) { | |
| newConfig['base-url'] = baseUrl; | |
| } | |
| if (proxyUrl) { | |
| newConfig['proxy-url'] = proxyUrl; | |
| } | |
| await this.makeRequest('/codex-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ index, value: newConfig }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadCodexKeys(); | |
| this.showNotification('Codex配置更新成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`更新Codex配置失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除Codex密钥 | |
| async deleteCodexKey(apiKey) { | |
| if (!confirm(i18n.t('ai_providers.codex_delete_confirm'))) return; | |
| try { | |
| await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadCodexKeys(); | |
| this.showNotification('Codex配置删除成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`删除Codex配置失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载Claude密钥 | |
| async loadClaudeKeys() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['claude-api-key']) { | |
| this.renderClaudeKeys(config['claude-api-key']); | |
| } | |
| } catch (error) { | |
| console.error('加载Claude密钥失败:', error); | |
| } | |
| } | |
| // 渲染Claude密钥列表 | |
| renderClaudeKeys(keys) { | |
| const container = document.getElementById('claude-keys-list'); | |
| if (keys.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-brain"></i> | |
| <h3>${i18n.t('ai_providers.claude_empty_title')}</h3> | |
| <p>${i18n.t('ai_providers.claude_empty_desc')}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = keys.map((config, index) => ` | |
| <div class="provider-item"> | |
| <div class="item-content"> | |
| <div class="item-title">${i18n.t('ai_providers.claude_item_title')} #${index + 1}</div> | |
| <div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div> | |
| ${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''} | |
| ${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''} | |
| </div> | |
| <div class="item-actions"> | |
| <button class="btn btn-secondary" onclick="manager.editClaudeKey(${index}, ${JSON.stringify(config).replace(/"/g, '"')})"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="btn btn-danger" onclick="manager.deleteClaudeKey('${config['api-key']}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // 显示添加Claude密钥模态框 | |
| showAddClaudeKeyModal() { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('ai_providers.claude_add_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="new-claude-key">${i18n.t('ai_providers.claude_add_modal_key_label')}</label> | |
| <input type="text" id="new-claude-key" placeholder="${i18n.t('ai_providers.claude_add_modal_key_placeholder')}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-claude-url">${i18n.t('ai_providers.claude_add_modal_url_label')}</label> | |
| <input type="text" id="new-claude-url" placeholder="${i18n.t('ai_providers.claude_add_modal_url_placeholder')}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-claude-proxy">${i18n.t('ai_providers.claude_add_modal_proxy_label')}</label> | |
| <input type="text" id="new-claude-proxy" placeholder="${i18n.t('ai_providers.claude_add_modal_proxy_placeholder')}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.addClaudeKey()">${i18n.t('common.add')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 添加Claude密钥 | |
| async addClaudeKey() { | |
| const apiKey = document.getElementById('new-claude-key').value.trim(); | |
| const baseUrl = document.getElementById('new-claude-url').value.trim(); | |
| const proxyUrl = document.getElementById('new-claude-proxy').value.trim(); | |
| if (!apiKey) { | |
| this.showNotification(i18n.t('notification.field_required'), 'error'); | |
| return; | |
| } | |
| try { | |
| const data = await this.makeRequest('/claude-api-key'); | |
| const currentKeys = data['claude-api-key'] || []; | |
| const newConfig = { 'api-key': apiKey }; | |
| if (baseUrl) { | |
| newConfig['base-url'] = baseUrl; | |
| } | |
| if (proxyUrl) { | |
| newConfig['proxy-url'] = proxyUrl; | |
| } | |
| currentKeys.push(newConfig); | |
| await this.makeRequest('/claude-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(currentKeys) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadClaudeKeys(); | |
| this.showNotification('Claude配置添加成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`添加Claude配置失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 编辑Claude密钥 | |
| editClaudeKey(index, config) { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('ai_providers.claude_edit_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="edit-claude-key">${i18n.t('ai_providers.claude_edit_modal_key_label')}</label> | |
| <input type="text" id="edit-claude-key" value="${config['api-key']}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-claude-url">${i18n.t('ai_providers.claude_edit_modal_url_label')}</label> | |
| <input type="text" id="edit-claude-url" value="${config['base-url'] || ''}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-claude-proxy">${i18n.t('ai_providers.claude_edit_modal_proxy_label')}</label> | |
| <input type="text" id="edit-claude-proxy" value="${config['proxy-url'] || ''}"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">${i18n.t('common.update')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| } | |
| // 更新Claude密钥 | |
| async updateClaudeKey(index) { | |
| const apiKey = document.getElementById('edit-claude-key').value.trim(); | |
| const baseUrl = document.getElementById('edit-claude-url').value.trim(); | |
| const proxyUrl = document.getElementById('edit-claude-proxy').value.trim(); | |
| if (!apiKey) { | |
| this.showNotification(i18n.t('notification.field_required'), 'error'); | |
| return; | |
| } | |
| try { | |
| const newConfig = { 'api-key': apiKey }; | |
| if (baseUrl) { | |
| newConfig['base-url'] = baseUrl; | |
| } | |
| if (proxyUrl) { | |
| newConfig['proxy-url'] = proxyUrl; | |
| } | |
| await this.makeRequest('/claude-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ index, value: newConfig }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadClaudeKeys(); | |
| this.showNotification('Claude配置更新成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`更新Claude配置失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除Claude密钥 | |
| async deleteClaudeKey(apiKey) { | |
| if (!confirm(i18n.t('ai_providers.claude_delete_confirm'))) return; | |
| try { | |
| await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadClaudeKeys(); | |
| this.showNotification('Claude配置删除成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`删除Claude配置失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载OpenAI提供商 | |
| async loadOpenAIProviders() { | |
| try { | |
| const config = await this.getConfig(); | |
| if (config['openai-compatibility']) { | |
| this.renderOpenAIProviders(config['openai-compatibility']); | |
| } | |
| } catch (error) { | |
| console.error('加载OpenAI提供商失败:', error); | |
| } | |
| } | |
| // 渲染OpenAI提供商列表 | |
| renderOpenAIProviders(providers) { | |
| const container = document.getElementById('openai-providers-list'); | |
| if (providers.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-plug"></i> | |
| <h3>${i18n.t('ai_providers.openai_empty_title')}</h3> | |
| <p>${i18n.t('ai_providers.openai_empty_desc')}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = providers.map((provider, index) => ` | |
| <div class="provider-item"> | |
| <div class="item-content"> | |
| <div class="item-title">${this.escapeHtml(provider.name)}</div> | |
| <div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(provider['base-url'])}</div> | |
| <div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-key-entries'] || []).length}</div> | |
| <div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}</div> | |
| ${this.renderOpenAIModelBadges(provider.models || [])} | |
| </div> | |
| <div class="item-actions"> | |
| <button class="btn btn-secondary" onclick="manager.editOpenAIProvider(${index}, ${JSON.stringify(provider).replace(/"/g, '"')})"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="btn btn-danger" onclick="manager.deleteOpenAIProvider('${provider.name}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // 显示添加OpenAI提供商模态框 | |
| showAddOpenAIProviderModal() { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('ai_providers.openai_add_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="new-provider-name">${i18n.t('ai_providers.openai_add_modal_name_label')}</label> | |
| <input type="text" id="new-provider-name" placeholder="${i18n.t('ai_providers.openai_add_modal_name_placeholder')}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-provider-url">${i18n.t('ai_providers.openai_add_modal_url_label')}</label> | |
| <input type="text" id="new-provider-url" placeholder="${i18n.t('ai_providers.openai_add_modal_url_placeholder')}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-provider-keys">${i18n.t('ai_providers.openai_add_modal_keys_label')}</label> | |
| <textarea id="new-provider-keys" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_placeholder')}"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="new-provider-proxies">${i18n.t('ai_providers.openai_add_modal_keys_proxy_label')}</label> | |
| <textarea id="new-provider-proxies" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_proxy_placeholder')}"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label>${i18n.t('ai_providers.openai_add_modal_models_label')}</label> | |
| <p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p> | |
| <div id="new-provider-models-wrapper" class="model-input-list"></div> | |
| <button type="button" class="btn btn-secondary" onclick="manager.addModelField('new-provider-models-wrapper')">${i18n.t('ai_providers.openai_models_add_btn')}</button> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.addOpenAIProvider()">${i18n.t('common.add')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| this.populateModelFields('new-provider-models-wrapper', []); | |
| } | |
| // 添加OpenAI提供商 | |
| async addOpenAIProvider() { | |
| const name = document.getElementById('new-provider-name').value.trim(); | |
| const baseUrl = document.getElementById('new-provider-url').value.trim(); | |
| const keysText = document.getElementById('new-provider-keys').value.trim(); | |
| const proxiesText = document.getElementById('new-provider-proxies').value.trim(); | |
| const models = this.collectModelInputs('new-provider-models-wrapper'); | |
| if (!this.validateOpenAIProviderInput(name, baseUrl, models)) { | |
| return; | |
| } | |
| try { | |
| const data = await this.makeRequest('/openai-compatibility'); | |
| const currentProviders = data['openai-compatibility'] || []; | |
| const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; | |
| const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : []; | |
| const apiKeyEntries = apiKeys.map((key, idx) => ({ | |
| 'api-key': key, | |
| 'proxy-url': proxies[idx] || '' | |
| })); | |
| const newProvider = { | |
| name, | |
| 'base-url': baseUrl, | |
| 'api-key-entries': apiKeyEntries, | |
| models | |
| }; | |
| currentProviders.push(newProvider); | |
| await this.makeRequest('/openai-compatibility', { | |
| method: 'PUT', | |
| body: JSON.stringify(currentProviders) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadOpenAIProviders(); | |
| this.showNotification('OpenAI提供商添加成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 编辑OpenAI提供商 | |
| editOpenAIProvider(index, provider) { | |
| const modal = document.getElementById('modal'); | |
| const modalBody = document.getElementById('modal-body'); | |
| const apiKeyEntries = provider['api-key-entries'] || []; | |
| const apiKeysText = apiKeyEntries.map(entry => entry['api-key'] || '').join('\n'); | |
| const proxiesText = apiKeyEntries.map(entry => entry['proxy-url'] || '').join('\n'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('ai_providers.openai_edit_modal_title')}</h3> | |
| <div class="form-group"> | |
| <label for="edit-provider-name">${i18n.t('ai_providers.openai_edit_modal_name_label')}</label> | |
| <input type="text" id="edit-provider-name" value="${provider.name}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-provider-url">${i18n.t('ai_providers.openai_edit_modal_url_label')}</label> | |
| <input type="text" id="edit-provider-url" value="${provider['base-url']}"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-provider-keys">${i18n.t('ai_providers.openai_edit_modal_keys_label')}</label> | |
| <textarea id="edit-provider-keys" rows="3">${apiKeysText}</textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label for="edit-provider-proxies">${i18n.t('ai_providers.openai_edit_modal_keys_proxy_label')}</label> | |
| <textarea id="edit-provider-proxies" rows="3">${proxiesText}</textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label>${i18n.t('ai_providers.openai_edit_modal_models_label')}</label> | |
| <p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p> | |
| <div id="edit-provider-models-wrapper" class="model-input-list"></div> | |
| <button type="button" class="btn btn-secondary" onclick="manager.addModelField('edit-provider-models-wrapper')">${i18n.t('ai_providers.openai_models_add_btn')}</button> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">${i18n.t('common.update')}</button> | |
| </div> | |
| `; | |
| modal.style.display = 'block'; | |
| this.populateModelFields('edit-provider-models-wrapper', provider.models || []); | |
| } | |
| // 更新OpenAI提供商 | |
| async updateOpenAIProvider(index) { | |
| const name = document.getElementById('edit-provider-name').value.trim(); | |
| const baseUrl = document.getElementById('edit-provider-url').value.trim(); | |
| const keysText = document.getElementById('edit-provider-keys').value.trim(); | |
| const proxiesText = document.getElementById('edit-provider-proxies').value.trim(); | |
| const models = this.collectModelInputs('edit-provider-models-wrapper'); | |
| if (!this.validateOpenAIProviderInput(name, baseUrl, models)) { | |
| return; | |
| } | |
| try { | |
| const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; | |
| const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : []; | |
| const apiKeyEntries = apiKeys.map((key, idx) => ({ | |
| 'api-key': key, | |
| 'proxy-url': proxies[idx] || '' | |
| })); | |
| const updatedProvider = { | |
| name, | |
| 'base-url': baseUrl, | |
| 'api-key-entries': apiKeyEntries, | |
| models | |
| }; | |
| await this.makeRequest('/openai-compatibility', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ index, value: updatedProvider }) | |
| }); | |
| this.clearCache(); // 清除缓存 | |
| this.closeModal(); | |
| this.loadOpenAIProviders(); | |
| this.showNotification('OpenAI提供商更新成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除OpenAI提供商 | |
| async deleteOpenAIProvider(name) { | |
| if (!confirm(i18n.t('ai_providers.openai_delete_confirm'))) return; | |
| try { | |
| await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadOpenAIProviders(); | |
| this.showNotification('OpenAI提供商删除成功', 'success'); | |
| } catch (error) { | |
| this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 加载认证文件 | |
| async loadAuthFiles() { | |
| try { | |
| const data = await this.makeRequest('/auth-files'); | |
| this.renderAuthFiles(data.files || []); | |
| } catch (error) { | |
| console.error('加载认证文件失败:', error); | |
| } | |
| } | |
| // 渲染认证文件列表 | |
| renderAuthFiles(files) { | |
| const container = document.getElementById('auth-files-list'); | |
| if (files.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-file-alt"></i> | |
| <h3>${i18n.t('auth_files.empty_title')}</h3> | |
| <p>${i18n.t('auth_files.empty_desc')}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = files.map(file => ` | |
| <div class="file-item"> | |
| <div class="item-content"> | |
| <div class="item-title">${file.name}</div> | |
| <div class="item-subtitle">${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}</div> | |
| <div class="item-subtitle">${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}</div> | |
| </div> | |
| <div class="item-actions"> | |
| <button class="btn btn-primary" onclick="manager.downloadAuthFile('${file.name}')"> | |
| <i class="fas fa-download"></i> | |
| </button> | |
| <button class="btn btn-danger" onclick="manager.deleteAuthFile('${file.name}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // 格式化文件大小 | |
| formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| // 上传认证文件 | |
| uploadAuthFile() { | |
| document.getElementById('auth-file-input').click(); | |
| } | |
| // 处理文件上传 | |
| async handleFileUpload(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| if (!file.name.endsWith('.json')) { | |
| this.showNotification(i18n.t('auth_files.upload_error_json'), 'error'); | |
| return; | |
| } | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| const response = await fetch(`${this.apiUrl}/auth-files`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${this.managementKey}` | |
| }, | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.error || `HTTP ${response.status}`); | |
| } | |
| this.clearCache(); // 清除缓存 | |
| this.loadAuthFiles(); | |
| this.showNotification(i18n.t('auth_files.upload_success'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`文件上传失败: ${error.message}`, 'error'); | |
| } | |
| // 清空文件输入 | |
| event.target.value = ''; | |
| } | |
| // 下载认证文件 | |
| async downloadAuthFile(filename) { | |
| try { | |
| const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, { | |
| headers: { | |
| 'Authorization': `Bearer ${this.managementKey}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| this.showNotification(i18n.t('auth_files.download_success'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`文件下载失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除认证文件 | |
| async deleteAuthFile(filename) { | |
| if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return; | |
| try { | |
| await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadAuthFiles(); | |
| this.showNotification(i18n.t('auth_files.delete_success'), 'success'); | |
| } catch (error) { | |
| this.showNotification(`文件删除失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 删除所有认证文件 | |
| async deleteAllAuthFiles() { | |
| if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return; | |
| try { | |
| const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' }); | |
| this.clearCache(); // 清除缓存 | |
| this.loadAuthFiles(); | |
| this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success'); | |
| } catch (error) { | |
| this.showNotification(`删除文件失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // 显示 Gemini Web Token 模态框 | |
| showGeminiWebTokenModal() { | |
| const inlineSecure1psid = document.getElementById('secure-1psid-input'); | |
| const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); | |
| const inlineLabel = document.getElementById('gemini-web-label-input'); | |
| const modalBody = document.getElementById('modal-body'); | |
| modalBody.innerHTML = ` | |
| <h3>${i18n.t('auth_login.gemini_web_button')}</h3> | |
| <div class="gemini-web-form"> | |
| <div class="form-group"> | |
| <label for="modal-secure-1psid">${i18n.t('auth_login.secure_1psid_label')}</label> | |
| <input type="text" id="modal-secure-1psid" placeholder="${i18n.t('auth_login.secure_1psid_placeholder')}" required> | |
| <div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="modal-secure-1psidts">${i18n.t('auth_login.secure_1psidts_label')}</label> | |
| <input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required> | |
| <div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="modal-gemini-web-label">${i18n.t('auth_login.gemini_web_label_label')}</label> | |
| <input type="text" id="modal-gemini-web-label" placeholder="${i18n.t('auth_login.gemini_web_label_placeholder')}"> | |
| <div class="form-hint">为此认证文件设置一个标签名称(可选)</div> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> | |
| <button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button> | |
| </div> | |
| </div> | |
| `; | |
| this.showModal(); | |
| const modalSecure1psid = document.getElementById('modal-secure-1psid'); | |
| const modalSecure1psidts = document.getElementById('modal-secure-1psidts'); | |
| const modalLabel = document.getElementById('modal-gemini-web-label'); | |
| if (modalSecure1psid && inlineSecure1psid) { | |
| modalSecure1psid.value = inlineSecure1psid.value.trim(); | |
| } | |
| if (modalSecure1psidts && inlineSecure1psidts) { | |
| modalSecure1psidts.value = inlineSecure1psidts.value.trim(); | |
| } | |
| if (modalLabel && inlineLabel) { | |
| modalLabel.value = inlineLabel.value.trim(); | |
| } | |
| if (modalSecure1psid) { | |
| modalSecure1psid.focus(); | |
| } | |
| } | |
| // 保存 Gemini Web Token | |
| async saveGeminiWebToken() { | |
| const secure1psid = document.getElementById('modal-secure-1psid').value.trim(); | |
| const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim(); | |
| const label = document.getElementById('modal-gemini-web-label').value.trim(); | |
| if (!secure1psid || !secure1psidts) { | |
| this.showNotification('请填写完整的 Cookie 信息', 'error'); | |
| return; | |
| } | |
| try { | |
| const requestBody = { | |
| secure_1psid: secure1psid, | |
| secure_1psidts: secure1psidts | |
| }; | |
| // 如果提供了 label,则添加到请求体中 | |
| if (label) { | |
| requestBody.label = label; | |
| } | |
| const response = await this.makeRequest('/gemini-web-token', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(requestBody) | |
| }); | |
| this.closeModal(); | |
| this.loadAuthFiles(); // 刷新认证文件列表 | |
| const inlineSecure1psid = document.getElementById('secure-1psid-input'); | |
| const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); | |
| const inlineLabel = document.getElementById('gemini-web-label-input'); | |
| if (inlineSecure1psid) { | |
| inlineSecure1psid.value = secure1psid; | |
| } | |
| if (inlineSecure1psidts) { | |
| inlineSecure1psidts.value = secure1psidts; | |
| } | |
| if (inlineLabel) { | |
| inlineLabel.value = label; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success'); | |
| } catch (error) { | |
| this.showNotification(`保存失败: ${error.message}`, 'error'); | |
| } | |
| } | |
| // ===== Codex OAuth 相关方法 ===== | |
| // 开始 Codex OAuth 流程 | |
| async startCodexOAuth() { | |
| try { | |
| const response = await this.makeRequest('/codex-auth-url?is_webui=1'); | |
| const authUrl = response.url; | |
| const state = this.extractStateFromUrl(authUrl); | |
| // 显示授权链接 | |
| const urlInput = document.getElementById('codex-oauth-url'); | |
| const content = document.getElementById('codex-oauth-content'); | |
| const status = document.getElementById('codex-oauth-status'); | |
| if (urlInput) { | |
| urlInput.value = authUrl; | |
| } | |
| if (content) { | |
| content.style.display = 'block'; | |
| } | |
| if (status) { | |
| status.textContent = i18n.t('auth_login.codex_oauth_status_waiting'); | |
| status.style.color = 'var(--warning-text)'; | |
| } | |
| // 开始轮询认证状态 | |
| this.startCodexOAuthPolling(state); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('auth_login.codex_oauth_start_error')} ${error.message}`, 'error'); | |
| } | |
| } | |
| // 从 URL 中提取 state 参数 | |
| extractStateFromUrl(url) { | |
| try { | |
| const urlObj = new URL(url); | |
| return urlObj.searchParams.get('state'); | |
| } catch (error) { | |
| console.error('Failed to extract state from URL:', error); | |
| return null; | |
| } | |
| } | |
| // 打开 Codex 授权链接 | |
| openCodexLink() { | |
| const urlInput = document.getElementById('codex-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| window.open(urlInput.value, '_blank'); | |
| } | |
| } | |
| // 复制 Codex 授权链接 | |
| async copyCodexLink() { | |
| const urlInput = document.getElementById('codex-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| try { | |
| await navigator.clipboard.writeText(urlInput.value); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } catch (error) { | |
| // 降级方案:使用传统的复制方法 | |
| urlInput.select(); | |
| document.execCommand('copy'); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } | |
| } | |
| } | |
| // 开始轮询 OAuth 状态 | |
| startCodexOAuthPolling(state) { | |
| if (!state) { | |
| this.showNotification('无法获取认证状态参数', 'error'); | |
| return; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const response = await this.makeRequest(`/get-auth-status?state=${encodeURIComponent(state)}`); | |
| const status = response.status; | |
| const statusElement = document.getElementById('codex-oauth-status'); | |
| if (status === 'ok') { | |
| // 认证成功 | |
| clearInterval(pollInterval); | |
| // 隐藏授权链接相关内容,恢复到初始状态 | |
| this.resetCodexOAuthUI(); | |
| // 显示成功通知 | |
| this.showNotification(i18n.t('auth_login.codex_oauth_status_success'), 'success'); | |
| // 刷新认证文件列表 | |
| this.loadAuthFiles(); | |
| } else if (status === 'error') { | |
| // 认证失败 | |
| clearInterval(pollInterval); | |
| const errorMessage = response.error || 'Unknown error'; | |
| // 显示错误信息 | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.codex_oauth_status_error')} ${errorMessage}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.codex_oauth_status_error')} ${errorMessage}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetCodexOAuthUI(); | |
| }, 3000); | |
| } else if (status === 'wait') { | |
| // 继续等待 | |
| if (statusElement) { | |
| statusElement.textContent = i18n.t('auth_login.codex_oauth_status_waiting'); | |
| statusElement.style.color = 'var(--warning-text)'; | |
| } | |
| } | |
| } catch (error) { | |
| clearInterval(pollInterval); | |
| const statusElement = document.getElementById('codex-oauth-status'); | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.codex_oauth_polling_error')} ${error.message}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.codex_oauth_polling_error')} ${error.message}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetCodexOAuthUI(); | |
| }, 3000); | |
| } | |
| }, 2000); // 每2秒轮询一次 | |
| // 设置超时,5分钟后停止轮询 | |
| setTimeout(() => { | |
| clearInterval(pollInterval); | |
| }, 5 * 60 * 1000); | |
| } | |
| // 重置 Codex OAuth UI 到初始状态 | |
| resetCodexOAuthUI() { | |
| const urlInput = document.getElementById('codex-oauth-url'); | |
| const content = document.getElementById('codex-oauth-content'); | |
| const status = document.getElementById('codex-oauth-status'); | |
| // 清空并隐藏授权链接输入框 | |
| if (urlInput) { | |
| urlInput.value = ''; | |
| } | |
| // 隐藏整个授权链接内容区域 | |
| if (content) { | |
| content.style.display = 'none'; | |
| } | |
| // 清空状态显示 | |
| if (status) { | |
| status.textContent = ''; | |
| status.style.color = ''; | |
| status.className = ''; | |
| } | |
| } | |
| // ===== Anthropic OAuth 相关方法 ===== | |
| // 开始 Anthropic OAuth 流程 | |
| async startAnthropicOAuth() { | |
| try { | |
| const response = await this.makeRequest('/anthropic-auth-url?is_webui=1'); | |
| const authUrl = response.url; | |
| const state = this.extractStateFromUrl(authUrl); | |
| // 显示授权链接 | |
| const urlInput = document.getElementById('anthropic-oauth-url'); | |
| const content = document.getElementById('anthropic-oauth-content'); | |
| const status = document.getElementById('anthropic-oauth-status'); | |
| if (urlInput) { | |
| urlInput.value = authUrl; | |
| } | |
| if (content) { | |
| content.style.display = 'block'; | |
| } | |
| if (status) { | |
| status.textContent = i18n.t('auth_login.anthropic_oauth_status_waiting'); | |
| status.style.color = 'var(--warning-text)'; | |
| } | |
| // 开始轮询认证状态 | |
| this.startAnthropicOAuthPolling(state); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('auth_login.anthropic_oauth_start_error')} ${error.message}`, 'error'); | |
| } | |
| } | |
| // 打开 Anthropic 授权链接 | |
| openAnthropicLink() { | |
| const urlInput = document.getElementById('anthropic-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| window.open(urlInput.value, '_blank'); | |
| } | |
| } | |
| // 复制 Anthropic 授权链接 | |
| async copyAnthropicLink() { | |
| const urlInput = document.getElementById('anthropic-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| try { | |
| await navigator.clipboard.writeText(urlInput.value); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } catch (error) { | |
| // 降级方案:使用传统的复制方法 | |
| urlInput.select(); | |
| document.execCommand('copy'); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } | |
| } | |
| } | |
| // 开始轮询 Anthropic OAuth 状态 | |
| startAnthropicOAuthPolling(state) { | |
| if (!state) { | |
| this.showNotification('无法获取认证状态参数', 'error'); | |
| return; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const response = await this.makeRequest(`/get-auth-status?state=${encodeURIComponent(state)}`); | |
| const status = response.status; | |
| const statusElement = document.getElementById('anthropic-oauth-status'); | |
| if (status === 'ok') { | |
| // 认证成功 | |
| clearInterval(pollInterval); | |
| // 隐藏授权链接相关内容,恢复到初始状态 | |
| this.resetAnthropicOAuthUI(); | |
| // 显示成功通知 | |
| this.showNotification(i18n.t('auth_login.anthropic_oauth_status_success'), 'success'); | |
| // 刷新认证文件列表 | |
| this.loadAuthFiles(); | |
| } else if (status === 'error') { | |
| // 认证失败 | |
| clearInterval(pollInterval); | |
| const errorMessage = response.error || 'Unknown error'; | |
| // 显示错误信息 | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.anthropic_oauth_status_error')} ${errorMessage}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.anthropic_oauth_status_error')} ${errorMessage}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetAnthropicOAuthUI(); | |
| }, 3000); | |
| } else if (status === 'wait') { | |
| // 继续等待 | |
| if (statusElement) { | |
| statusElement.textContent = i18n.t('auth_login.anthropic_oauth_status_waiting'); | |
| statusElement.style.color = 'var(--warning-text)'; | |
| } | |
| } | |
| } catch (error) { | |
| clearInterval(pollInterval); | |
| const statusElement = document.getElementById('anthropic-oauth-status'); | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.anthropic_oauth_polling_error')} ${error.message}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.anthropic_oauth_polling_error')} ${error.message}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetAnthropicOAuthUI(); | |
| }, 3000); | |
| } | |
| }, 2000); // 每2秒轮询一次 | |
| // 设置超时,5分钟后停止轮询 | |
| setTimeout(() => { | |
| clearInterval(pollInterval); | |
| }, 5 * 60 * 1000); | |
| } | |
| // 重置 Anthropic OAuth UI 到初始状态 | |
| resetAnthropicOAuthUI() { | |
| const urlInput = document.getElementById('anthropic-oauth-url'); | |
| const content = document.getElementById('anthropic-oauth-content'); | |
| const status = document.getElementById('anthropic-oauth-status'); | |
| // 清空并隐藏授权链接输入框 | |
| if (urlInput) { | |
| urlInput.value = ''; | |
| } | |
| // 隐藏整个授权链接内容区域 | |
| if (content) { | |
| content.style.display = 'none'; | |
| } | |
| // 清空状态显示 | |
| if (status) { | |
| status.textContent = ''; | |
| status.style.color = ''; | |
| status.className = ''; | |
| } | |
| } | |
| // ===== Gemini CLI OAuth 相关方法 ===== | |
| // 开始 Gemini CLI OAuth 流程 | |
| async startGeminiCliOAuth() { | |
| try { | |
| // 获取项目 ID(可选) | |
| const projectId = document.getElementById('gemini-cli-project-id').value.trim(); | |
| // 构建请求 URL | |
| let requestUrl = '/gemini-cli-auth-url?is_webui=1'; | |
| if (projectId) { | |
| requestUrl += `&project_id=${encodeURIComponent(projectId)}`; | |
| } | |
| const response = await this.makeRequest(requestUrl); | |
| const authUrl = response.url; | |
| const state = this.extractStateFromUrl(authUrl); | |
| // 显示授权链接 | |
| const urlInput = document.getElementById('gemini-cli-oauth-url'); | |
| const content = document.getElementById('gemini-cli-oauth-content'); | |
| const status = document.getElementById('gemini-cli-oauth-status'); | |
| if (urlInput) { | |
| urlInput.value = authUrl; | |
| } | |
| if (content) { | |
| content.style.display = 'block'; | |
| } | |
| if (status) { | |
| status.textContent = i18n.t('auth_login.gemini_cli_oauth_status_waiting'); | |
| status.style.color = 'var(--warning-text)'; | |
| } | |
| // 开始轮询认证状态 | |
| this.startGeminiCliOAuthPolling(state); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('auth_login.gemini_cli_oauth_start_error')} ${error.message}`, 'error'); | |
| } | |
| } | |
| // 打开 Gemini CLI 授权链接 | |
| openGeminiCliLink() { | |
| const urlInput = document.getElementById('gemini-cli-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| window.open(urlInput.value, '_blank'); | |
| } | |
| } | |
| // 复制 Gemini CLI 授权链接 | |
| async copyGeminiCliLink() { | |
| const urlInput = document.getElementById('gemini-cli-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| try { | |
| await navigator.clipboard.writeText(urlInput.value); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } catch (error) { | |
| // 降级方案:使用传统的复制方法 | |
| urlInput.select(); | |
| document.execCommand('copy'); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } | |
| } | |
| } | |
| // 开始轮询 Gemini CLI OAuth 状态 | |
| startGeminiCliOAuthPolling(state) { | |
| if (!state) { | |
| this.showNotification('无法获取认证状态参数', 'error'); | |
| return; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const response = await this.makeRequest(`/get-auth-status?state=${encodeURIComponent(state)}`); | |
| const status = response.status; | |
| const statusElement = document.getElementById('gemini-cli-oauth-status'); | |
| if (status === 'ok') { | |
| // 认证成功 | |
| clearInterval(pollInterval); | |
| // 隐藏授权链接相关内容,恢复到初始状态 | |
| this.resetGeminiCliOAuthUI(); | |
| // 显示成功通知 | |
| this.showNotification(i18n.t('auth_login.gemini_cli_oauth_status_success'), 'success'); | |
| // 刷新认证文件列表 | |
| this.loadAuthFiles(); | |
| } else if (status === 'error') { | |
| // 认证失败 | |
| clearInterval(pollInterval); | |
| const errorMessage = response.error || 'Unknown error'; | |
| // 显示错误信息 | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.gemini_cli_oauth_status_error')} ${errorMessage}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.gemini_cli_oauth_status_error')} ${errorMessage}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetGeminiCliOAuthUI(); | |
| }, 3000); | |
| } else if (status === 'wait') { | |
| // 继续等待 | |
| if (statusElement) { | |
| statusElement.textContent = i18n.t('auth_login.gemini_cli_oauth_status_waiting'); | |
| statusElement.style.color = 'var(--warning-text)'; | |
| } | |
| } | |
| } catch (error) { | |
| clearInterval(pollInterval); | |
| const statusElement = document.getElementById('gemini-cli-oauth-status'); | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.gemini_cli_oauth_polling_error')} ${error.message}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.gemini_cli_oauth_polling_error')} ${error.message}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetGeminiCliOAuthUI(); | |
| }, 3000); | |
| } | |
| }, 2000); // 每2秒轮询一次 | |
| // 设置超时,5分钟后停止轮询 | |
| setTimeout(() => { | |
| clearInterval(pollInterval); | |
| }, 5 * 60 * 1000); | |
| } | |
| // 重置 Gemini CLI OAuth UI 到初始状态 | |
| resetGeminiCliOAuthUI() { | |
| const urlInput = document.getElementById('gemini-cli-oauth-url'); | |
| const content = document.getElementById('gemini-cli-oauth-content'); | |
| const status = document.getElementById('gemini-cli-oauth-status'); | |
| // 清空并隐藏授权链接输入框 | |
| if (urlInput) { | |
| urlInput.value = ''; | |
| } | |
| // 隐藏整个授权链接内容区域 | |
| if (content) { | |
| content.style.display = 'none'; | |
| } | |
| // 清空状态显示 | |
| if (status) { | |
| status.textContent = ''; | |
| status.style.color = ''; | |
| status.className = ''; | |
| } | |
| } | |
| // ===== Qwen OAuth 相关方法 ===== | |
| // 开始 Qwen OAuth 流程 | |
| async startQwenOAuth() { | |
| try { | |
| const response = await this.makeRequest('/qwen-auth-url?is_webui=1'); | |
| const authUrl = response.url; | |
| const state = this.extractStateFromUrl(authUrl); | |
| // 显示授权链接 | |
| const urlInput = document.getElementById('qwen-oauth-url'); | |
| const content = document.getElementById('qwen-oauth-content'); | |
| const status = document.getElementById('qwen-oauth-status'); | |
| if (urlInput) { | |
| urlInput.value = authUrl; | |
| } | |
| if (content) { | |
| content.style.display = 'block'; | |
| } | |
| if (status) { | |
| status.textContent = i18n.t('auth_login.qwen_oauth_status_waiting'); | |
| status.style.color = 'var(--warning-text)'; | |
| } | |
| // 开始轮询认证状态 | |
| this.startQwenOAuthPolling(response.state); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('auth_login.qwen_oauth_start_error')} ${error.message}`, 'error'); | |
| } | |
| } | |
| // 打开 Qwen 授权链接 | |
| openQwenLink() { | |
| const urlInput = document.getElementById('qwen-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| window.open(urlInput.value, '_blank'); | |
| } | |
| } | |
| // 复制 Qwen 授权链接 | |
| async copyQwenLink() { | |
| const urlInput = document.getElementById('qwen-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| try { | |
| await navigator.clipboard.writeText(urlInput.value); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } catch (error) { | |
| // 降级方案:使用传统的复制方法 | |
| urlInput.select(); | |
| document.execCommand('copy'); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } | |
| } | |
| } | |
| // 开始轮询 Qwen OAuth 状态 | |
| startQwenOAuthPolling(state) { | |
| if (!state) { | |
| this.showNotification('无法获取认证状态参数', 'error'); | |
| return; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const response = await this.makeRequest(`/get-auth-status?state=${encodeURIComponent(state)}`); | |
| const status = response.status; | |
| const statusElement = document.getElementById('qwen-oauth-status'); | |
| if (status === 'ok') { | |
| // 认证成功 | |
| clearInterval(pollInterval); | |
| // 隐藏授权链接相关内容,恢复到初始状态 | |
| this.resetQwenOAuthUI(); | |
| // 显示成功通知 | |
| this.showNotification(i18n.t('auth_login.qwen_oauth_status_success'), 'success'); | |
| // 刷新认证文件列表 | |
| this.loadAuthFiles(); | |
| } else if (status === 'error') { | |
| // 认证失败 | |
| clearInterval(pollInterval); | |
| const errorMessage = response.error || 'Unknown error'; | |
| // 显示错误信息 | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.qwen_oauth_status_error')} ${errorMessage}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.qwen_oauth_status_error')} ${errorMessage}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetQwenOAuthUI(); | |
| }, 3000); | |
| } else if (status === 'wait') { | |
| // 继续等待 | |
| if (statusElement) { | |
| statusElement.textContent = i18n.t('auth_login.qwen_oauth_status_waiting'); | |
| statusElement.style.color = 'var(--warning-text)'; | |
| } | |
| } | |
| } catch (error) { | |
| clearInterval(pollInterval); | |
| const statusElement = document.getElementById('qwen-oauth-status'); | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.qwen_oauth_polling_error')} ${error.message}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.qwen_oauth_polling_error')} ${error.message}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetQwenOAuthUI(); | |
| }, 3000); | |
| } | |
| }, 2000); // 每2秒轮询一次 | |
| // 设置超时,5分钟后停止轮询 | |
| setTimeout(() => { | |
| clearInterval(pollInterval); | |
| }, 5 * 60 * 1000); | |
| } | |
| // 重置 Qwen OAuth UI 到初始状态 | |
| resetQwenOAuthUI() { | |
| const urlInput = document.getElementById('qwen-oauth-url'); | |
| const content = document.getElementById('qwen-oauth-content'); | |
| const status = document.getElementById('qwen-oauth-status'); | |
| // 清空并隐藏授权链接输入框 | |
| if (urlInput) { | |
| urlInput.value = ''; | |
| } | |
| // 隐藏整个授权链接内容区域 | |
| if (content) { | |
| content.style.display = 'none'; | |
| } | |
| // 清空状态显示 | |
| if (status) { | |
| status.textContent = ''; | |
| status.style.color = ''; | |
| status.className = ''; | |
| } | |
| } | |
| // ===== iFlow OAuth 相关方法 ===== | |
| // 开始 iFlow OAuth 流程 | |
| async startIflowOAuth() { | |
| try { | |
| const response = await this.makeRequest('/iflow-auth-url?is_webui=1'); | |
| const authUrl = response.url; | |
| const state = this.extractStateFromUrl(authUrl); | |
| // 显示授权链接 | |
| const urlInput = document.getElementById('iflow-oauth-url'); | |
| const content = document.getElementById('iflow-oauth-content'); | |
| const status = document.getElementById('iflow-oauth-status'); | |
| if (urlInput) { | |
| urlInput.value = authUrl; | |
| } | |
| if (content) { | |
| content.style.display = 'block'; | |
| } | |
| if (status) { | |
| status.textContent = i18n.t('auth_login.iflow_oauth_status_waiting'); | |
| status.style.color = 'var(--warning-text)'; | |
| } | |
| // 开始轮询认证状态 | |
| this.startIflowOAuthPolling(state); | |
| } catch (error) { | |
| this.showNotification(`${i18n.t('auth_login.iflow_oauth_start_error')} ${error.message}`, 'error'); | |
| } | |
| } | |
| // 打开 iFlow 授权链接 | |
| openIflowLink() { | |
| const urlInput = document.getElementById('iflow-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| window.open(urlInput.value, '_blank'); | |
| } | |
| } | |
| // 复制 iFlow 授权链接 | |
| async copyIflowLink() { | |
| const urlInput = document.getElementById('iflow-oauth-url'); | |
| if (urlInput && urlInput.value) { | |
| try { | |
| await navigator.clipboard.writeText(urlInput.value); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } catch (error) { | |
| // 降级方案:使用传统的复制方法 | |
| urlInput.select(); | |
| document.execCommand('copy'); | |
| this.showNotification('链接已复制到剪贴板', 'success'); | |
| } | |
| } | |
| } | |
| // 开始轮询 iFlow OAuth 状态 | |
| startIflowOAuthPolling(state) { | |
| if (!state) { | |
| this.showNotification('无法获取认证状态参数', 'error'); | |
| return; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const response = await this.makeRequest(`/get-auth-status?state=${encodeURIComponent(state)}`); | |
| const status = response.status; | |
| const statusElement = document.getElementById('iflow-oauth-status'); | |
| if (status === 'ok') { | |
| // 认证成功 | |
| clearInterval(pollInterval); | |
| // 隐藏授权链接相关内容,恢复到初始状态 | |
| this.resetIflowOAuthUI(); | |
| // 显示成功通知 | |
| this.showNotification(i18n.t('auth_login.iflow_oauth_status_success'), 'success'); | |
| // 刷新认证文件列表 | |
| this.loadAuthFiles(); | |
| } else if (status === 'error') { | |
| // 认证失败 | |
| clearInterval(pollInterval); | |
| const errorMessage = response.error || 'Unknown error'; | |
| // 显示错误信息 | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.iflow_oauth_status_error')} ${errorMessage}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.iflow_oauth_status_error')} ${errorMessage}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetIflowOAuthUI(); | |
| }, 3000); | |
| } else if (status === 'wait') { | |
| // 继续等待 | |
| if (statusElement) { | |
| statusElement.textContent = i18n.t('auth_login.iflow_oauth_status_waiting'); | |
| statusElement.style.color = 'var(--warning-text)'; | |
| } | |
| } | |
| } catch (error) { | |
| clearInterval(pollInterval); | |
| const statusElement = document.getElementById('iflow-oauth-status'); | |
| if (statusElement) { | |
| statusElement.textContent = `${i18n.t('auth_login.iflow_oauth_polling_error')} ${error.message}`; | |
| statusElement.style.color = 'var(--error-text)'; | |
| } | |
| this.showNotification(`${i18n.t('auth_login.iflow_oauth_polling_error')} ${error.message}`, 'error'); | |
| // 3秒后重置UI,让用户能够重新开始 | |
| setTimeout(() => { | |
| this.resetIflowOAuthUI(); | |
| }, 3000); | |
| } | |
| }, 2000); // 每2秒轮询一次 | |
| // 设置超时,5分钟后停止轮询 | |
| setTimeout(() => { | |
| clearInterval(pollInterval); | |
| }, 5 * 60 * 1000); | |
| } | |
| // 重置 iFlow OAuth UI 到初始状态 | |
| resetIflowOAuthUI() { | |
| const urlInput = document.getElementById('iflow-oauth-url'); | |
| const content = document.getElementById('iflow-oauth-content'); | |
| const status = document.getElementById('iflow-oauth-status'); | |
| // 清空并隐藏授权链接输入框 | |
| if (urlInput) { | |
| urlInput.value = ''; | |
| } | |
| // 隐藏整个授权链接内容区域 | |
| if (content) { | |
| content.style.display = 'none'; | |
| } | |
| // 清空状态显示 | |
| if (status) { | |
| status.textContent = ''; | |
| status.style.color = ''; | |
| status.className = ''; | |
| } | |
| } | |
| // ===== 使用统计相关方法 ===== | |
| // 初始化图表变量 | |
| requestsChart = null; | |
| tokensChart = null; | |
| currentUsageData = null; | |
| // 加载使用统计 | |
| async loadUsageStats() { | |
| try { | |
| const response = await this.makeRequest('/usage'); | |
| const usage = response?.usage || null; | |
| this.currentUsageData = usage; | |
| if (!usage) { | |
| throw new Error('usage payload missing'); | |
| } | |
| // 更新概览卡片 | |
| this.updateUsageOverview(usage); | |
| // 读取当前图表周期 | |
| const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); | |
| const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); | |
| const requestsPeriod = requestsHourActive ? 'hour' : 'day'; | |
| const tokensPeriod = tokensHourActive ? 'hour' : 'day'; | |
| // 初始化图表(使用当前周期) | |
| this.initializeRequestsChart(requestsPeriod); | |
| this.initializeTokensChart(tokensPeriod); | |
| // 更新API详细统计表格 | |
| this.updateApiStatsTable(usage); | |
| } catch (error) { | |
| console.error('加载使用统计失败:', error); | |
| this.currentUsageData = null; | |
| // 清空概览数据 | |
| ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) el.textContent = '-'; | |
| }); | |
| // 清空图表 | |
| if (this.requestsChart) { | |
| this.requestsChart.destroy(); | |
| this.requestsChart = null; | |
| } | |
| if (this.tokensChart) { | |
| this.tokensChart.destroy(); | |
| this.tokensChart = null; | |
| } | |
| const tableElement = document.getElementById('api-stats-table'); | |
| if (tableElement) { | |
| tableElement.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.loading_error')}: ${error.message}</div>`; | |
| } | |
| } | |
| } | |
| // 更新使用统计概览 | |
| updateUsageOverview(data) { | |
| const safeData = data || {}; | |
| document.getElementById('total-requests').textContent = safeData.total_requests ?? 0; | |
| document.getElementById('success-requests').textContent = safeData.success_count ?? 0; | |
| document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0; | |
| document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0; | |
| } | |
| // 初始化图表 | |
| initializeCharts() { | |
| const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); | |
| const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); | |
| this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day'); | |
| this.initializeTokensChart(tokensHourActive ? 'hour' : 'day'); | |
| } | |
| // 初始化请求趋势图表 | |
| initializeRequestsChart(period = 'day') { | |
| const ctx = document.getElementById('requests-chart'); | |
| if (!ctx) return; | |
| // 销毁现有图表 | |
| if (this.requestsChart) { | |
| this.requestsChart.destroy(); | |
| } | |
| const data = this.getRequestsChartData(period); | |
| this.requestsChart = new Chart(ctx, { | |
| type: 'line', | |
| data: data, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| title: { | |
| display: true, | |
| text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') | |
| } | |
| }, | |
| y: { | |
| beginAtZero: true, | |
| title: { | |
| display: true, | |
| text: i18n.t('usage_stats.requests_count') | |
| } | |
| } | |
| }, | |
| elements: { | |
| line: { | |
| borderColor: '#3b82f6', | |
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | |
| fill: true, | |
| tension: 0.4 | |
| }, | |
| point: { | |
| backgroundColor: '#3b82f6', | |
| borderColor: '#ffffff', | |
| borderWidth: 2, | |
| radius: 4 | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // 初始化Token使用趋势图表 | |
| initializeTokensChart(period = 'day') { | |
| const ctx = document.getElementById('tokens-chart'); | |
| if (!ctx) return; | |
| // 销毁现有图表 | |
| if (this.tokensChart) { | |
| this.tokensChart.destroy(); | |
| } | |
| const data = this.getTokensChartData(period); | |
| this.tokensChart = new Chart(ctx, { | |
| type: 'line', | |
| data: data, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| title: { | |
| display: true, | |
| text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') | |
| } | |
| }, | |
| y: { | |
| beginAtZero: true, | |
| title: { | |
| display: true, | |
| text: i18n.t('usage_stats.tokens_count') | |
| } | |
| } | |
| }, | |
| elements: { | |
| line: { | |
| borderColor: '#10b981', | |
| backgroundColor: 'rgba(16, 185, 129, 0.1)', | |
| fill: true, | |
| tension: 0.4 | |
| }, | |
| point: { | |
| backgroundColor: '#10b981', | |
| borderColor: '#ffffff', | |
| borderWidth: 2, | |
| radius: 4 | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // 获取请求图表数据 | |
| getRequestsChartData(period) { | |
| if (!this.currentUsageData) { | |
| return { labels: [], datasets: [{ data: [] }] }; | |
| } | |
| let dataSource, labels, values; | |
| if (period === 'hour') { | |
| dataSource = this.currentUsageData.requests_by_hour || {}; | |
| labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); | |
| values = labels.map(hour => dataSource[hour] || 0); | |
| } else { | |
| dataSource = this.currentUsageData.requests_by_day || {}; | |
| labels = Object.keys(dataSource).sort(); | |
| values = labels.map(day => dataSource[day] || 0); | |
| } | |
| return { | |
| labels: labels, | |
| datasets: [{ | |
| data: values | |
| }] | |
| }; | |
| } | |
| // 获取Token图表数据 | |
| getTokensChartData(period) { | |
| if (!this.currentUsageData) { | |
| return { labels: [], datasets: [{ data: [] }] }; | |
| } | |
| let dataSource, labels, values; | |
| if (period === 'hour') { | |
| dataSource = this.currentUsageData.tokens_by_hour || {}; | |
| labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); | |
| values = labels.map(hour => dataSource[hour] || 0); | |
| } else { | |
| dataSource = this.currentUsageData.tokens_by_day || {}; | |
| labels = Object.keys(dataSource).sort(); | |
| values = labels.map(day => dataSource[day] || 0); | |
| } | |
| return { | |
| labels: labels, | |
| datasets: [{ | |
| data: values | |
| }] | |
| }; | |
| } | |
| // 切换请求图表时间周期 | |
| switchRequestsPeriod(period) { | |
| // 更新按钮状态 | |
| document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour'); | |
| document.getElementById('requests-day-btn').classList.toggle('active', period === 'day'); | |
| // 更新图表数据 | |
| if (this.requestsChart) { | |
| const newData = this.getRequestsChartData(period); | |
| this.requestsChart.data = newData; | |
| this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); | |
| this.requestsChart.update(); | |
| } | |
| } | |
| // 切换Token图表时间周期 | |
| switchTokensPeriod(period) { | |
| // 更新按钮状态 | |
| document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour'); | |
| document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day'); | |
| // 更新图表数据 | |
| if (this.tokensChart) { | |
| const newData = this.getTokensChartData(period); | |
| this.tokensChart.data = newData; | |
| this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); | |
| this.tokensChart.update(); | |
| } | |
| } | |
| // 更新API详细统计表格 | |
| updateApiStatsTable(data) { | |
| const container = document.getElementById('api-stats-table'); | |
| if (!container) return; | |
| const apis = data.apis || {}; | |
| if (Object.keys(apis).length === 0) { | |
| container.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.no_data')}</div>`; | |
| return; | |
| } | |
| let tableHtml = ` | |
| <table class="stats-table"> | |
| <thead> | |
| <tr> | |
| <th>${i18n.t('usage_stats.api_endpoint')}</th> | |
| <th>${i18n.t('usage_stats.requests_count')}</th> | |
| <th>${i18n.t('usage_stats.tokens_count')}</th> | |
| <th>${i18n.t('usage_stats.success_rate')}</th> | |
| <th>${i18n.t('usage_stats.models')}</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| Object.entries(apis).forEach(([endpoint, apiData]) => { | |
| const totalRequests = apiData.total_requests || 0; | |
| const successCount = apiData.success_count ?? null; | |
| const successRate = successCount !== null && totalRequests > 0 | |
| ? Math.round((successCount / totalRequests) * 100) | |
| : null; | |
| // 构建模型详情 | |
| let modelsHtml = ''; | |
| if (apiData.models && Object.keys(apiData.models).length > 0) { | |
| modelsHtml = '<div class="model-details">'; | |
| Object.entries(apiData.models).forEach(([modelName, modelData]) => { | |
| const modelRequests = modelData.total_requests ?? 0; | |
| const modelTokens = modelData.total_tokens ?? 0; | |
| modelsHtml += ` | |
| <div class="model-item"> | |
| <span class="model-name">${modelName}</span> | |
| <span>${modelRequests} 请求 / ${modelTokens} tokens</span> | |
| </div> | |
| `; | |
| }); | |
| modelsHtml += '</div>'; | |
| } | |
| tableHtml += ` | |
| <tr> | |
| <td>${endpoint}</td> | |
| <td>${totalRequests}</td> | |
| <td>${apiData.total_tokens || 0}</td> | |
| <td>${successRate !== null ? successRate + '%' : '-'}</td> | |
| <td>${modelsHtml || '-'}</td> | |
| </tr> | |
| `; | |
| }); | |
| tableHtml += '</tbody></table>'; | |
| container.innerHTML = tableHtml; | |
| } | |
| showModal() { | |
| const modal = document.getElementById('modal'); | |
| if (modal) { | |
| modal.style.display = 'block'; | |
| } | |
| } | |
| // 关闭模态框 | |
| closeModal() { | |
| document.getElementById('modal').style.display = 'none'; | |
| } | |
| detectApiBaseFromLocation() { | |
| try { | |
| const { protocol, hostname, port } = window.location; | |
| const normalizedPort = port ? `:${port}` : ''; | |
| return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`); | |
| } catch (error) { | |
| console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error); | |
| return this.normalizeBase(this.apiBase || 'http://localhost:8317'); | |
| } | |
| } | |
| updateLoginConnectionInfo() { | |
| const connectionUrlElement = document.getElementById('login-connection-url'); | |
| const customInput = document.getElementById('login-api-base'); | |
| if (connectionUrlElement) { | |
| connectionUrlElement.textContent = this.apiBase || '-'; | |
| } | |
| if (customInput && customInput !== document.activeElement) { | |
| customInput.value = this.apiBase || ''; | |
| } | |
| } | |
| addModelField(wrapperId, model = {}) { | |
| const wrapper = document.getElementById(wrapperId); | |
| if (!wrapper) return; | |
| const row = document.createElement('div'); | |
| row.className = 'model-input-row'; | |
| row.innerHTML = ` | |
| <div class="input-group"> | |
| <input type="text" class="model-name-input" placeholder="${i18n.t('ai_providers.openai_model_name_placeholder')}" value="${model.name ? this.escapeHtml(model.name) : ''}"> | |
| <input type="text" class="model-alias-input" placeholder="${i18n.t('ai_providers.openai_model_alias_placeholder')}" value="${model.alias ? this.escapeHtml(model.alias) : ''}"> | |
| <button type="button" class="btn btn-small btn-danger model-remove-btn"><i class="fas fa-trash"></i></button> | |
| </div> | |
| `; | |
| const removeBtn = row.querySelector('.model-remove-btn'); | |
| if (removeBtn) { | |
| removeBtn.addEventListener('click', () => { | |
| wrapper.removeChild(row); | |
| }); | |
| } | |
| wrapper.appendChild(row); | |
| } | |
| populateModelFields(wrapperId, models = []) { | |
| const wrapper = document.getElementById(wrapperId); | |
| if (!wrapper) return; | |
| wrapper.innerHTML = ''; | |
| if (!models.length) { | |
| this.addModelField(wrapperId); | |
| return; | |
| } | |
| models.forEach(model => this.addModelField(wrapperId, model)); | |
| } | |
| collectModelInputs(wrapperId) { | |
| const wrapper = document.getElementById(wrapperId); | |
| if (!wrapper) return []; | |
| const rows = Array.from(wrapper.querySelectorAll('.model-input-row')); | |
| const models = []; | |
| rows.forEach(row => { | |
| const nameInput = row.querySelector('.model-name-input'); | |
| const aliasInput = row.querySelector('.model-alias-input'); | |
| const name = nameInput ? nameInput.value.trim() : ''; | |
| const alias = aliasInput ? aliasInput.value.trim() : ''; | |
| if (name) { | |
| const model = { name }; | |
| if (alias) { | |
| model.alias = alias; | |
| } | |
| models.push(model); | |
| } | |
| }); | |
| return models; | |
| } | |
| renderOpenAIModelBadges(models) { | |
| if (!models || models.length === 0) { | |
| return ''; | |
| } | |
| return ` | |
| <div class="provider-models"> | |
| ${models.map(model => ` | |
| <span class="provider-model-tag"> | |
| <span class="model-name">${this.escapeHtml(model.name || '')}</span> | |
| ${model.alias ? `<span class="model-alias">${this.escapeHtml(model.alias)}</span>` : ''} | |
| </span> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| validateOpenAIProviderInput(name, baseUrl, models) { | |
| if (!name || !baseUrl) { | |
| this.showNotification(i18n.t('notification.openai_provider_required'), 'error'); | |
| return false; | |
| } | |
| const invalidModel = models.find(model => !model.name); | |
| if (invalidModel) { | |
| this.showNotification(i18n.t('notification.openai_model_name_required'), 'error'); | |
| return false; | |
| } | |
| return true; | |
| } | |
| } | |
| // 全局管理器实例 | |
| let manager; | |
| // 尝试自动加载根目录 Logo(支持多种常见文件名/扩展名) | |
| function setupSiteLogo() { | |
| const img = document.getElementById('site-logo'); | |
| const loginImg = document.getElementById('login-logo'); | |
| if (!img && !loginImg) return; | |
| const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null; | |
| if (inlineLogo) { | |
| if (img) { | |
| img.src = inlineLogo; | |
| img.style.display = 'inline-block'; | |
| } | |
| if (loginImg) { | |
| loginImg.src = inlineLogo; | |
| loginImg.style.display = 'inline-block'; | |
| } | |
| return; | |
| } | |
| const candidates = [ | |
| '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif', | |
| 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif', | |
| '/logo.svg', '/logo.png', '/logo.jpg', '/logo.jpeg', '/logo.webp', '/logo.gif' | |
| ]; | |
| let idx = 0; | |
| const tryNext = () => { | |
| if (idx >= candidates.length) return; | |
| const test = new Image(); | |
| test.onload = () => { | |
| if (img) { | |
| img.src = test.src; | |
| img.style.display = 'inline-block'; | |
| } | |
| if (loginImg) { | |
| loginImg.src = test.src; | |
| loginImg.style.display = 'inline-block'; | |
| } | |
| }; | |
| test.onerror = () => { | |
| idx++; | |
| tryNext(); | |
| }; | |
| test.src = candidates[idx]; | |
| }; | |
| tryNext(); | |
| } | |
| // 页面加载完成后初始化 | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // 初始化国际化 | |
| i18n.init(); | |
| setupSiteLogo(); | |
| manager = new CLIProxyManager(); | |
| }); | |
| </script> | |
| <script>window.__INLINE_LOGO__ = "data:image/jpeg;base64,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";</script> | |
| </body> | |
| </html> |