| <!DOCTYPE html> |
| <html lang="zh-CN" data-theme="light"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Business Gemini Pool 管理控制台</title> |
| <style> |
| |
| :root { |
| |
| --primary: #4285f4; |
| --primary-hover: #3367d6; |
| --primary-light: rgba(66, 133, 244, 0.1); |
| --success: #34a853; |
| --success-light: rgba(52, 168, 83, 0.1); |
| --danger: #ea4335; |
| --danger-light: rgba(234, 67, 53, 0.1); |
| --warning: #fbbc04; |
| --warning-light: rgba(251, 188, 4, 0.1); |
| |
| |
| --radius-sm: 6px; |
| --radius-md: 12px; |
| --radius-lg: 16px; |
| --transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); |
| |
| --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| } |
| |
| |
| [data-theme="light"] { |
| --bg-color: #f7f8fc; |
| --card-bg: #ffffff; |
| --text-main: #1f2328; |
| --text-muted: #656d76; |
| --border: #e4e7eb; |
| --hover-bg: #f2f3f5; |
| --input-bg: #ffffff; |
| --shadow-sm: 0 1px 2px 0 rgba(27, 31, 35, 0.04); |
| --shadow-md: 0 4px 8px 0 rgba(27, 31, 35, 0.06), 0 1px 2px 0 rgba(27, 31, 35, 0.05); |
| --shadow-lg: 0 10px 20px 0 rgba(27, 31, 35, 0.07), 0 3px 6px 0 rgba(27, 31, 35, 0.05); |
| } |
| |
| [data-theme="dark"] { |
| --bg-color: #1a1b1e; |
| --card-bg: #242528; |
| --text-main: #e8eaed; |
| --text-muted: #9aa0a6; |
| --border: #3a3c40; |
| --hover-bg: #303134; |
| --input-bg: #2f3033; |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.1); |
| --shadow-md: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 1px 2px 0 rgba(0, 0, 0, 0.1); |
| --shadow-lg: 0 10px 20px 0 rgba(0, 0, 0, 0.2), 0 3px 6px 0 rgba(0, 0, 0, 0.15); |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: var(--font-main); |
| background-color: var(--bg-color); |
| color: var(--text-main); |
| min-height: 100vh; |
| transition: background-color 0.3s, color 0.3s; |
| -webkit-font-smoothing: antialiased; |
| -moz-osx-font-smoothing: grayscale; |
| } |
| |
| .container { |
| max-width: 1400px; |
| margin: 0 auto; |
| padding: 32px; |
| } |
| |
| |
| .header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 32px; |
| |
| } |
| |
| .header-left { display: flex; align-items: center; gap: 16px; } |
| |
| .logo { |
| width: 44px; |
| height: 44px; |
| background: linear-gradient(135deg, #4285f4, #34a853, #fbbc04, #ea4335); |
| border-radius: var(--radius-md); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-weight: 600; |
| font-size: 22px; |
| transform: rotate(-10deg); |
| transition: var(--transition-ease); |
| } |
| .logo:hover { transform: rotate(0deg) scale(1.05); } |
| |
| .header h1 { |
| font-size: 26px; |
| font-weight: 600; |
| color: var(--text-main); |
| } |
| |
| .header h1 span { |
| color: var(--text-muted); |
| font-weight: 400; |
| font-size: 16px; |
| margin-left: 10px; |
| } |
| |
| .header-right { display: flex; align-items: center; gap: 16px; } |
| |
| .status-indicator { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| padding: 8px 16px; |
| background: var(--success-light); |
| border: 1px solid rgba(52, 168, 83, 0.2); |
| border-radius: 50px; |
| font-size: 14px; |
| color: var(--success); |
| font-weight: 500; |
| } |
| .status-indicator::before { |
| content: ''; width: 8px; height: 8px; |
| background: var(--success); border-radius: 50%; |
| animation: pulse 2s infinite; |
| } |
| @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.9); } } |
| |
| .theme-toggle { |
| width: 44px; height: 44px; border: 1px solid var(--border); |
| background: var(--card-bg); border-radius: var(--radius-md); |
| cursor: pointer; display: flex; align-items: center; justify-content: center; |
| font-size: 20px; transition: var(--transition-ease); |
| } |
| .theme-toggle:hover { background: var(--hover-bg); border-color: var(--primary); transform: translateY(-2px); } |
| |
| |
| .tabs { |
| display: flex; |
| gap: 16px; |
| border-bottom: 1px solid var(--border); |
| margin-bottom: 32px; |
| } |
| .tab { |
| padding: 14px 4px; |
| border: none; border-bottom: 2px solid transparent; |
| background: transparent; color: var(--text-muted); |
| font-size: 15px; font-weight: 500; |
| cursor: pointer; border-radius: 0; |
| transition: var(--transition-ease); |
| display: flex; align-items: center; justify-content: center; |
| gap: 8px; |
| } |
| .tab:hover { color: var(--primary); } |
| .tab.active { color: var(--primary); border-bottom-color: var(--primary); } |
| .tab-icon { font-size: 20px; } |
| |
| |
| |
| .badge { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| padding: 6px 12px; |
| border-radius: 20px; |
| font-size: 12px; |
| font-weight: 500; |
| } |
| |
| .badge::before { |
| content: ''; |
| width: 6px; |
| height: 6px; |
| border-radius: 50%; |
| } |
| |
| .badge-success { |
| background: var(--success-light); |
| color: var(--success); |
| } |
| |
| .badge-success::before { |
| background: var(--success); |
| } |
| |
| .badge-danger { |
| background: var(--danger-light); |
| color: var(--danger); |
| } |
| |
| .badge-danger::before { |
| background: var(--danger); |
| } |
| |
| .cooldown-hint { |
| display: block; |
| color: var(--text-muted); |
| font-size: 12px; |
| margin-top: 4px; |
| } |
| |
| .log-level-control { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| background: var(--card-bg); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-md); |
| padding: 6px 10px; |
| } |
| .log-level-control label { |
| font-size: 12px; |
| color: var(--text-muted); |
| } |
| .log-level-select { |
| border: 1px solid var(--border); |
| background: var(--input-bg); |
| color: var(--text-main); |
| border-radius: var(--radius-sm); |
| padding: 6px 8px; |
| } |
| |
| .token-actions { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| margin-bottom: 12px; |
| } |
| .token-input { |
| flex: 1; |
| min-width: 240px; |
| } |
| |
| .badge-warning { |
| background: var(--warning-light); |
| color: #b06000; |
| } |
| |
| .badge-warning::before { |
| background: var(--warning); |
| } |
| |
| |
| .tab-content { display: none; } |
| .tab-content.active { display: block; animation: contentFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; } |
| @keyframes contentFadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } |
| |
| |
| .card { |
| background: var(--card-bg); |
| border-radius: var(--radius-lg); |
| box-shadow: var(--shadow-md); |
| border: 1px solid var(--border); |
| margin-bottom: 32px; |
| overflow: hidden; |
| transition: var(--transition-ease); |
| } |
| .card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-lg); } |
| |
| .card-header { |
| display: flex; justify-content: space-between; align-items: center; |
| padding: 20px 24px; border-bottom: 1px solid var(--border); |
| } |
| .card-title { |
| font-size: 18px; font-weight: 600; color: var(--text-main); |
| display: flex; align-items: center; gap: 12px; |
| } |
| .card-title-icon { font-size: 22px; color: var(--text-muted); } |
| .card-body { padding: 24px; } |
| |
| |
| .btn { |
| padding: 10px 20px; border: none; border-radius: var(--radius-md); |
| cursor: pointer; font-size: 14px; font-weight: 500; |
| display: inline-flex; align-items: center; justify-content: center; gap: 8px; |
| transition: var(--transition-ease); text-decoration: none; |
| } |
| .btn:disabled { opacity: 0.5; cursor: not-allowed; } |
| .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-md); } |
| .btn-primary { background: var(--primary); color: white; } |
| .btn-primary:hover:not(:disabled) { background: var(--primary-hover); } |
| |
| .btn-outline { |
| background: transparent; color: var(--text-muted); |
| border: 1px solid var(--border); |
| } |
| .btn-outline:hover:not(:disabled) { border-color: var(--text-main); color: var(--text-main); } |
| |
| .btn-success { background: var(--success-light); color: var(--success); border: 1px solid rgba(52, 168, 83, 0.2); } |
| .btn-success:hover:not(:disabled) { background: var(--success); color: white; border-color: var(--success); } |
| .btn-danger { background: var(--danger-light); color: var(--danger); border: 1px solid rgba(234, 67, 53, 0.2); } |
| .btn-danger:hover:not(:disabled) { background: var(--danger); color: white; border-color: var(--danger); } |
| |
| .btn-sm { padding: 6px 14px; font-size: 13px; border-radius: var(--radius-sm); } |
| .btn-icon { width: 32px; height: 32px; padding: 0; border-radius: var(--radius-md); display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; } |
| .btn-warning { background: #fff3cd; color: #856404; border: 1px solid rgba(133, 100, 4, 0.2); } |
| .btn-warning:hover:not(:disabled) { background: #ffc107; color: #212529; border-color: #ffc107; } |
| |
| |
| .table-container { overflow-x: auto; } |
| table { width: 100%; border-collapse: collapse; } |
| th { |
| text-align: left; padding: 16px 24px; font-size: 13px; |
| font-weight: 500; color: var(--text-muted); text-transform: uppercase; |
| letter-spacing: 0.5px; background: transparent; |
| border-bottom: 2px solid var(--border); |
| } |
| td { |
| padding: 18px 24px; border-bottom: 1px solid var(--border); |
| font-size: 14px; color: var(--text-main); |
| transition: background-color 0.2s; |
| } |
| tr:last-child td { border-bottom: none; } |
| tr:hover td { background: var(--hover-bg); } |
| |
| |
| .form-group { |
| display: flex; |
| flex-direction: column; |
| margin-bottom: 20px; |
| } |
| .form-group label, |
| .form-label { |
| display: block; |
| margin-bottom: 12px; |
| font-size: 14px; |
| font-weight: 600; |
| color: var(--text-main); |
| letter-spacing: 0.2px; |
| } |
| .form-group input, .form-group textarea, .form-group select, |
| .form-input, |
| .form-textarea { |
| width: 100%; |
| padding: 14px 16px; |
| border-radius: var(--radius-md); |
| border: 1px solid var(--border); |
| background: var(--bg); |
| color: var(--text-main); |
| font-size: 14px; |
| transition: var(--transition-ease); |
| box-sizing: border-box; |
| line-height: 1.5; |
| } |
| .form-textarea { |
| min-height: 90px; |
| resize: vertical; |
| font-family: inherit; |
| } |
| .form-group input:focus, .form-group textarea:focus, .form-group select:focus { |
| outline: none; |
| border-color: var(--primary); |
| box-shadow: 0 0 0 3px var(--primary-light), 0 1px 2px rgba(0,0,0,0.05) inset; |
| } |
| .form-group input:disabled { |
| background: var(--hover-bg); |
| color: var(--text-muted); |
| cursor: not-allowed; |
| } |
| .form-group small { |
| display: block; |
| margin-top: 6px; |
| font-size: 13px; |
| color: var(--text-muted); |
| } |
| .form-row { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 24px; |
| } |
| |
| |
| .settings-section { |
| background: var(--bg); |
| border: 1px solid var(--border); |
| border-radius: var(--radius-lg); |
| padding: 28px; |
| margin-bottom: 28px; |
| } |
| .settings-section:last-child { |
| margin-bottom: 0; |
| } |
| .settings-section h3 { |
| font-size: 17px; |
| font-weight: 600; |
| color: var(--text-main); |
| margin-bottom: 24px; |
| padding-bottom: 16px; |
| border-bottom: 1px solid var(--border); |
| display: flex; |
| align-items: center; |
| } |
| .settings-section .form-group { |
| margin-bottom: 24px; |
| } |
| .settings-section .form-group:last-of-type { |
| margin-bottom: 20px; |
| } |
| |
| |
| .modal { |
| display: flex; |
| align-items: center; justify-content: center; |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; |
| background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px); |
| z-index: 1000; opacity: 0; visibility: hidden; |
| transition: opacity 0.3s, visibility 0.3s; |
| } |
| .modal.show { opacity: 1; visibility: visible; } |
| .modal-content { |
| background: var(--card-bg); border-radius: var(--radius-lg); |
| width: 600px; max-width: 90vw; max-height: 90vh; |
| overflow-y: auto; box-shadow: var(--shadow-lg); |
| transform: translateY(20px) scale(0.98); |
| transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| .modal.show .modal-content { transform: translateY(0) scale(1); } |
| .modal-header { padding: 24px; border-bottom: 1px solid var(--border); } |
| .modal-header h3 { font-size: 20px; font-weight: 600; display: inline-block; } |
| .modal-close { |
| width: 36px; height: 36px; border: none; background: transparent; |
| color: var(--text-muted); cursor: pointer; border-radius: 50%; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 22px; transition: var(--transition-ease); |
| float: right; |
| } |
| .modal-close:hover { background: var(--hover-bg); color: var(--text-main); transform: rotate(90deg); } |
| .modal-body { padding: 24px; } |
| .modal-footer { |
| display: flex; justify-content: flex-end; gap: 12px; |
| padding: 20px 24px; border-top: 1px solid var(--border); |
| background: var(--hover-bg); |
| border-bottom-left-radius: var(--radius-lg); |
| border-bottom-right-radius: var(--radius-lg); |
| } |
| |
| |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
| gap: 24px; |
| margin-bottom: 32px; |
| } |
| .stat-card { |
| background: var(--card-bg); border: 1px solid var(--border); |
| border-radius: var(--radius-lg); padding: 24px; |
| display: flex; flex-direction: column; |
| align-items: flex-start; gap: 16px; |
| transition: var(--transition-ease); |
| |
| opacity: 0; |
| transform: translateY(20px); |
| animation: fadeIn-up 0.5s ease-out forwards; |
| } |
| |
| .stat-card:nth-child(1) { animation-delay: 0.1s; } |
| .stat-card:nth-child(2) { animation-delay: 0.2s; } |
| .stat-card:nth-child(3) { animation-delay: 0.3s; } |
| .stat-card:nth-child(4) { animation-delay: 0.4s; } |
| |
| @keyframes fadeIn-up { |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| .stat-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-md); border-color: var(--primary); } |
| |
| .stat-info-top { display: flex; justify-content: space-between; align-items: center; width: 100%; } |
| .stat-info-top p { font-size: 14px; font-weight: 500; color: var(--text-muted); } |
| |
| .stat-icon { |
| width: 40px; height: 40px; border-radius: var(--radius-md); |
| display: flex; align-items: center; justify-content: center; font-size: 20px; |
| } |
| |
| .stat-info-bottom h3 { font-size: 32px; font-weight: 600; color: var(--text-main); } |
| .stat-icon.blue { background: var(--primary-light); color: var(--primary); } |
| .stat-icon.green { background: var(--success-light); color: var(--success); } |
| .stat-icon.red { background: var(--danger-light); color: var(--danger); } |
| .stat-icon.yellow { background: var(--warning-light); color: #b06000; } |
| |
| |
| .badge { |
| padding: 5px 12px; border-radius: 50px; |
| font-size: 12px; font-weight: 500; |
| } |
| .empty-state { text-align: center; padding: 80px 20px; color: var(--text-muted); } |
| .empty-state-icon { font-size: 56px; margin-bottom: 20px; opacity: 0.4; } |
| |
| .toast { |
| position: fixed; bottom: 32px; left: 50%; |
| transform: translateX(-50%) translateY(100px); |
| background: var(--card-bg); border: 1px solid var(--border); |
| border-radius: var(--radius-md); padding: 16px 24px; |
| box-shadow: var(--shadow-lg); min-width: 320px; |
| z-index: 2000; opacity: 0; visibility: hidden; |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); |
| display: flex; align-items: center; gap: 12px; |
| } |
| .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; visibility: visible; } |
| |
| |
| .icon { |
| width: 1em; |
| height: 1em; |
| stroke-width: 2; |
| fill: none; |
| stroke: currentColor; |
| stroke-linecap: round; |
| stroke-linejoin: round; |
| } |
| |
| |
| @media (max-width: 768px) { |
| .container { padding: 24px 16px; } |
| .header { flex-direction: column; gap: 24px; text-align: center; } |
| .tabs { |
| gap: 8px; |
| |
| overflow-x: auto; |
| white-space: nowrap; |
| -ms-overflow-style: none; |
| scrollbar-width: none; |
| } |
| .tabs::-webkit-scrollbar { display: none; } |
| .tab { flex-shrink: 0; } |
| .form-row { grid-template-columns: 1fr; } |
| .stats-grid { gap: 16px; } |
| } |
| </style> |
| </head> |
| <body> |
| |
| <svg width="0" height="0" style="display: none;"> |
| <symbol id="icon-users" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></symbol> |
| <symbol id="icon-robot" viewBox="0 0 24 24"><path d="M12 8V4H8"></path><rect x="4" y="12" width="16" height="8" rx="2"></rect><path d="M2 12h20"></path><path d="M12 12V8a4 4 0 0 0-4-4"></path></symbol> |
| <symbol id="icon-settings" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 0 2l-.15.08a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l-.22-.38a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1 0-2l.15-.08a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></symbol> |
| <symbol id="icon-server" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></symbol> |
| <symbol id="icon-list" viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></symbol> |
| <symbol id="icon-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></symbol> |
| <symbol id="icon-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></symbol> |
| <symbol id="icon-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol> |
| <symbol id="icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></symbol> |
| <symbol id="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></symbol> |
| <symbol id="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></symbol> |
| <symbol id="icon-message" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></symbol> |
| <symbol id="icon-play" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></symbol> |
| <symbol id="icon-pause" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></symbol> |
| <symbol id="icon-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></symbol> |
| <symbol id="icon-key" viewBox="0 0 24 24"><path d="M21 2l-2 2"></path><path d="M9 6l-2 2"></path><circle cx="7.5" cy="15.5" r="5.5"></circle><path d="M21 2l-9.6 9.6"></path><path d="M15.5 7.5l3 3"></path><path d="M16 13l-3-3"></path></symbol> |
| </svg> |
|
|
| <div class="container"> |
| |
| <header class="header"> |
| <div class="header-left"> |
| <div class="logo">G</div> |
| <h1>Business Gemini Pool <span>管理控制台</span></h1> |
| </div> |
| <div class="header-right"> |
| <div class="status-indicator" id="serviceStatus">服务运行中</div> |
| <div class="log-level-control"> |
| <label for="logLevelSelect">日志</label> |
| <select id="logLevelSelect" class="log-level-select" onchange="updateLogLevel(this.value)"> |
| <option value="DEBUG">DEBUG</option> |
| <option value="INFO" selected>INFO</option> |
| <option value="ERROR">ERROR</option> |
| </select> |
| </div> |
| <button class="btn btn-outline" id="loginButton" style="padding: 8px 12px;" onclick="showLoginModal()">登录</button> |
| <a href="chat_history.html" class="btn btn-primary" style="padding: 8px 16px; font-size: 14px; text-decoration: none; display: flex; align-items: center; gap: 6px;" title="进入在线对话"> |
| <svg class="icon" style="width: 16px; height: 16px;"><use xlink:href="#icon-message"></use></svg> |
| 在线对话 |
| </a> |
| <button class="theme-toggle" onclick="toggleTheme()" title="切换主题"> |
| <span id="themeIconContainer"> |
| <svg class="icon"><use xlink:href="#icon-sun"></use></svg> |
| </span> |
| </button> |
| </div> |
| </header> |
|
|
| |
| <div class="tabs"> |
| <button class="tab active" onclick="switchTab('accounts')"> |
| <svg class="icon tab-icon"><use xlink:href="#icon-users"></use></svg> |
| 账号管理 |
| </button> |
| <button class="tab" onclick="switchTab('models')"> |
| <svg class="icon tab-icon"><use xlink:href="#icon-robot"></use></svg> |
| 模型管理 |
| </button> |
| <button class="tab" onclick="switchTab('settings')"> |
| <svg class="icon tab-icon"><use xlink:href="#icon-settings"></use></svg> |
| 系统设置 |
| </button> |
| <button class="tab" onclick="switchTab('tokens')"> |
| <svg class="icon tab-icon"><use xlink:href="#icon-key"></use></svg> |
| Token 管理 |
| </button> |
| </div> |
|
|
| |
| <div id="accounts" class="tab-content active"> |
| |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-info-top"> |
| <p>总账号数</p> |
| <div class="stat-icon blue"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div> |
| </div> |
| <div class="stat-info-bottom"> |
| <h3 id="totalAccounts">0</h3> |
| </div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-info-top"> |
| <p>可用账号</p> |
| <div class="stat-icon green"><svg class="icon"><use xlink:href="#icon-check"></use></svg></div> |
| </div> |
| <div class="stat-info-bottom"> |
| <h3 id="availableAccounts">0</h3> |
| </div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-info-top"> |
| <p>不可用账号</p> |
| <div class="stat-icon red"><svg class="icon"><use xlink:href="#icon-x"></use></svg></div> |
| </div> |
| <div class="stat-info-bottom"> |
| <h3 id="unavailableAccounts">0</h3> |
| </div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-info-top"> |
| <p>当前轮训索引</p> |
| <div class="stat-icon yellow"><svg class="icon"><use xlink:href="#icon-refresh"></use></svg></div> |
| </div> |
| <div class="stat-info-bottom"> |
| <h3 id="currentIndex">0</h3> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <svg class="icon card-title-icon"><use xlink:href="#icon-list"></use></svg> |
| 账号列表 |
| </div> |
| <button class="btn btn-primary" onclick="showAddAccountModal()"> |
| <svg class="icon"><use xlink:href="#icon-plus"></use></svg> |
| 添加账号 |
| </button> |
| </div> |
| <div class="table-container"> |
| <table id="accountsTable"> |
| <thead> |
| <tr> |
| <th>序号</th> |
| <th>Team ID</th> |
| <th>csesidx</th> |
| <th>User Agent</th> |
| <th>状态</th> |
| <th>操作</th> |
| </tr> |
| </thead> |
| <tbody id="accountsTableBody"></tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="models" class="tab-content"> |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <svg class="icon card-title-icon"><use xlink:href="#icon-robot"></use></svg> |
| 模型列表 |
| </div> |
| <button class="btn btn-primary" onclick="showAddModelModal()"> |
| <svg class="icon"><use xlink:href="#icon-plus"></use></svg> |
| 添加模型 |
| </button> |
| </div> |
| <div class="table-container"> |
| <table id="modelsTable"> |
| <thead> |
| <tr> |
| <th>模型ID</th> |
| <th>名称</th> |
| <th>描述</th> |
| <th>上下文长度</th> |
| <th>最大Token</th> |
| <th>状态</th> |
| <th>操作</th> |
| </tr> |
| </thead> |
| <tbody id="modelsTableBody"></tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="settings" class="tab-content"> |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <svg class="icon card-title-icon"><use xlink:href="#icon-settings"></use></svg> |
| 系统配置 |
| </div> |
| </div> |
| <div class="card-body"> |
| <form id="settingsForm"> |
| <div class="settings-section"> |
| <h3>代理设置</h3> |
| <div class="form-group"> |
| <label class="form-label" for="proxyUrl">代理地址</label> |
| <input type="text" class="form-input" id="proxyUrl" placeholder="http://127.0.0.1:7890"> |
| <small>用于访问Google API的代理服务器地址</small> |
| <div class="proxy-status" id="proxyStatus"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="imageOutputMode">图片输出模式</label> |
| <select class="form-input" id="imageOutputMode"> |
| <option value="url">图片URL(默认)</option> |
| <option value="base64">Base64 Data URL</option> |
| </select> |
| <small>控制聊天接口返回的图片是以URL形式还是以 data:image/...;base64,... 形式输出</small> |
| </div> |
| <div style="display: flex; gap: 12px;"> |
| <button type="button" class="btn btn-outline" onclick="testProxy()"> |
| 测试代理 |
| </button> |
| <button type="button" class="btn btn-primary" onclick="saveSettings()"> |
| 保存设置 |
| </button> |
| </div> |
| </div> |
|
|
| <div class="settings-section"> |
| <h3><svg class="icon" style="width: 1em; height: 1em; vertical-align: -2px; margin-right: 8px;"><use xlink:href="#icon-server"></use></svg>服务信息</h3> |
| <div class="form-row"> |
| <div class="form-group"> |
| <label class="form-label">服务端口</label> |
| <input type="text" class="form-input" value="8000" disabled> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">API地址</label> |
| <input type="text" class="form-input" value="http://localhost:8000/v1" disabled> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="settings-section"> |
| <h3>配置文件</h3> |
| <div class="form-group"> |
| <label class="form-label" for="configJson">当前配置 (JSON)</label> |
| <textarea class="form-textarea" id="configJson" rows="15" readonly></textarea> |
| <small>配置文件路径: business_gemini_session.json</small> |
| </div> |
| <div style="display: flex; gap: 12px; flex-wrap: wrap;"> |
| <button type="button" class="btn btn-outline" onclick="refreshConfig()"> |
| 刷新配置 |
| </button> |
| <button type="button" class="btn btn-outline" onclick="downloadConfig()"> |
| 下载配置 |
| </button> |
| <button type="button" class="btn btn-primary" onclick="uploadConfig()"> |
| 导入配置 |
| </button> |
| <input type="file" id="configFileInput" accept=".json" style="display: none;" onchange="handleConfigUpload(event)"> |
| </div> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="tokens" class="tab-content"> |
| <div class="card"> |
| <div class="card-header" style="display:flex; justify-content:space-between; align-items:center;"> |
| <div class="card-title" style="display:flex; align-items:center; gap:8px;"> |
| <svg class="icon card-title-icon"><use xlink:href="#icon-key"></use></svg> |
| Token 管理 |
| </div> |
| <div class="token-actions"> |
| <input id="manualToken" class="form-input token-input" placeholder="手动输入 Token(留空自动生成)"> |
| <button class="btn btn-outline" type="button" onclick="generateToken()">生成 Token</button> |
| <button class="btn btn-primary" type="button" onclick="addToken()">添加 Token</button> |
| </div> |
| </div> |
| <table class="table"> |
| <thead> |
| <tr> |
| <th style="width:70%;">Token</th> |
| <th>操作</th> |
| </tr> |
| </thead> |
| <tbody id="tokensTableBody"> |
| <tr><td colspan="2" class="empty-state">加载中...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="addAccountModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>添加账号</h3> |
| <button class="modal-close" onclick="closeModal('addAccountModal')" title="关闭">×</button> |
| </div> |
| |
| <div class="modal-body"> |
| <div class="form-group"> |
| <label class="form-label" for="newAccountJson">粘贴账号JSON(可直接复制工具输出)</label> |
| <textarea class="form-textarea" id="newAccountJson" placeholder='{"team_id":"...","secure_c_ses":"...","host_c_oses":"...","csesidx":"...","user_agent":"..."}' rows="4"></textarea> |
| <div style="display:flex; gap:8px; margin-top:8px;"> |
| <button class="btn btn-outline btn-sm" type="button" onclick="parseAccountJson()">解析填充</button> |
| <button class="btn btn-outline btn-sm" type="button" onclick="pasteAccountJson()">从剪贴板读取并填充</button> |
| </div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newTeamId">Team ID</label> |
| <input type="text" class="form-input" id="newTeamId" placeholder="输入Team ID"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newSecureCses">Cookie中的__Secure-C_SES</label> |
| <textarea class="form-textarea" id="newSecureCses" placeholder="输入Cookie中的__Secure-C_SES" rows="3"></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newHostCoses">Cookie中的__Host-C_OSES</label> |
| <textarea class="form-textarea" id="newHostCoses" placeholder="输入Cookie中的__Host-C_OSES" rows="3"></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newCsesidx">CSESIDX</label> |
| <input type="text" class="form-input" id="newCsesidx" placeholder="输入CSESIDX"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newUserAgent">User Agent</label> |
| <input type="text" class="form-input" id="newUserAgent" placeholder="输入User Agent"> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn btn-outline" onclick="closeModal('addAccountModal')">取消</button> |
| <button class="btn btn-primary" onclick="saveNewAccount()">保存</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="modal" id="editAccountModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>编辑账号</h3> |
| <button class="modal-close" onclick="closeModal('editAccountModal')" title="关闭">×</button> |
| </div> |
| <div class="modal-body"> |
| <input type="hidden" id="editAccountId"> |
| <div class="form-group"> |
| <label class="form-label" for="editTeamId">Team ID</label> |
| <input type="text" class="form-input" id="editTeamId" placeholder="输入Team ID"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editSecureCses">Cookie中的__Secure-C_SES</label> |
| <textarea class="form-textarea" id="editSecureCses" placeholder="输入Secure C Ses" rows="3"></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editHostCoses">Cookie中的__Host-C_OSES</label> |
| <textarea class="form-textarea" id="editHostCoses" placeholder="输入Host C Oses" rows="3"></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editCsesidx">CSESIDX</label> |
| <input type="text" class="form-input" id="editCsesidx" placeholder="输入CSESIDX"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editUserAgent">User Agent</label> |
| <input type="text" class="form-input" id="editUserAgent" placeholder="输入User Agent"> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn btn-outline" onclick="closeModal('editAccountModal')">取消</button> |
| <button class="btn btn-primary" onclick="updateAccount()">保存</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="refreshCookieModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>刷新账号Cookie</h3> |
| <button class="modal-close" onclick="closeModal('refreshCookieModal')" title="关闭">×</button> |
| </div> |
| <div class="modal-body"> |
| <input type="hidden" id="refreshAccountId"> |
| <p class="text-muted" style="margin-bottom: 16px;">请输入新的Cookie值来刷新账号认证信息。刷新后将清除JWT缓存。</p> |
| <div class="form-group"> |
| <label class="form-label" for="refreshSecureCses">Cookie中的__Secure-C_SES <span style="color: var(--danger);">*</span></label> |
| <textarea class="form-textarea" id="refreshSecureCses" placeholder="输入新的__Secure-C_SES值" rows="3"></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="refreshHostCoses">Cookie中的__Host-C_OSES <span style="color: var(--danger);">*</span></label> |
| <textarea class="form-textarea" id="refreshHostCoses" placeholder="输入新的__Host-C_OSES值" rows="3"></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="refreshCsesidx">CSESIDX (可选)</label> |
| <input type="text" class="form-input" id="refreshCsesidx" placeholder="输入CSESIDX值"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">从JSON粘贴 (可选)</label> |
| <textarea class="form-textarea" id="refreshCookieJson" placeholder="粘贴Cookie JSON数据" rows="3"></textarea> |
| <div style="display:flex; gap:8px; margin-top:8px;"> |
| <button class="btn btn-outline btn-sm" type="button" onclick="parseRefreshCookieJson()">解析填充</button> |
| <button class="btn btn-outline btn-sm" type="button" onclick="pasteRefreshCookieJson()">📋 粘贴并解析</button> |
| </div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn btn-outline" onclick="closeModal('refreshCookieModal')">取消</button> |
| <button class="btn btn-primary" onclick="refreshAccountCookie()">刷新Cookie</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="addModelModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>添加模型</h3> |
| <button class="modal-close" onclick="closeModal('addModelModal')" title="关闭">×</button> |
| </div> |
| <div class="modal-body"> |
| <div class="form-row"> |
| <div class="form-group"> |
| <label class="form-label" for="newModelId">模型ID</label> |
| <input type="text" class="form-input" id="newModelId" placeholder="如: gemini-pro"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newModelName">模型名称</label> |
| <input type="text" class="form-input" id="newModelName" placeholder="如: Gemini Pro"> |
| </div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newModelDesc">描述</label> |
| <input type="text" class="form-input" id="newModelDesc" placeholder="模型描述"> |
| </div> |
| <div class="form-row"> |
| <div class="form-group"> |
| <label class="form-label" for="newContextLength">上下文长度</label> |
| <input type="number" class="form-input" id="newContextLength" value="32768"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="newMaxTokens">最大Token</label> |
| <input type="number" class="form-input" id="newMaxTokens" value="8192"> |
| </div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn btn-outline" onclick="closeModal('addModelModal')">取消</button> |
| <button class="btn btn-primary" onclick="saveNewModel()">保存</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="loginModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>管理员登录</h3> |
| <button class="modal-close" onclick="closeModal('loginModal')" title="关闭">×</button> |
| </div> |
| <div class="modal-body"> |
| <div class="form-group"> |
| <label class="form-label" for="loginPassword">后台密码</label> |
| <input type="password" class="form-input" id="loginPassword" placeholder="输入后台密码"> |
| </div> |
| <p class="text-muted" style="font-size: 12px;">首次登录将设置当前密码为后台密码。</p> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn btn-outline" onclick="closeModal('loginModal')">取消</button> |
| <button class="btn btn-primary" onclick="submitLogin()">登录</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="editModelModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>编辑模型</h3> |
| <button class="modal-close" onclick="closeModal('editModelModal')" title="关闭">×</button> |
| </div> |
| <div class="modal-body"> |
| <input type="hidden" id="editModelOriginalId"> |
| <div class="form-row"> |
| <div class="form-group"> |
| <label class="form-label" for="editModelId">模型ID</label> |
| <input type="text" class="form-input" id="editModelId" placeholder="如: gemini-pro" readonly style="background-color: var(--bg-tertiary); cursor: not-allowed;"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editModelName">模型名称</label> |
| <input type="text" class="form-input" id="editModelName" placeholder="如: Gemini Pro"> |
| </div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editModelDesc">描述</label> |
| <input type="text" class="form-input" id="editModelDesc" placeholder="模型描述"> |
| </div> |
| <div class="form-row"> |
| <div class="form-group"> |
| <label class="form-label" for="editContextLength">上下文长度</label> |
| <input type="number" class="form-input" id="editContextLength"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label" for="editMaxTokens">最大Token</label> |
| <input type="number" class="form-input" id="editMaxTokens"> |
| </div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn btn-outline" onclick="closeModal('editModelModal')">取消</button> |
| <button class="btn btn-primary" onclick="updateModel()">保存</button> |
| </div> |
| </div> |
| </div> |
|
|
|
|
| |
| <div id="toastContainer" class="toast-container"> |
| |
| </div> |
| <div class="toast" id="toast"></div> |
|
|
| <script> |
| |
| function updateThemeIcon(theme) { |
| const iconContainer = document.getElementById('themeIconContainer'); |
| if (iconContainer) { |
| const iconId = theme === 'dark' ? 'icon-sun' : 'icon-moon'; |
| iconContainer.innerHTML = `<svg class="icon"><use xlink:href="#${iconId}"></use></svg>`; |
| } |
| } |
| |
| |
| let toastTimeout; |
| function showToast(message, type = 'info') { |
| const toast = document.getElementById('toast'); |
| if (!toast) return; |
| |
| let icon = ''; |
| let borderType = type; |
| switch(type) { |
| case 'success': |
| icon = '<svg class="icon" style="color: var(--success);"><use xlink:href="#icon-check"></use></svg>'; |
| break; |
| case 'error': |
| icon = '<svg class="icon" style="color: var(--danger);"><use xlink:href="#icon-x"></use></svg>'; |
| break; |
| default: |
| icon = '<svg class="icon" style="color: var(--primary);"><use xlink:href="#icon-server"></use></svg>'; |
| borderType = 'primary'; |
| break; |
| } |
| |
| toast.innerHTML = `${icon} <span class="toast-message">${message}</span>`; |
| toast.className = `toast show`; |
| toast.style.borderLeft = `4px solid var(--${borderType})`; |
| |
| clearTimeout(toastTimeout); |
| toastTimeout = setTimeout(() => { |
| toast.classList.remove('show'); |
| }, 3500); |
| } |
| |
| |
| |
| |
| |
| |
| const API_BASE = '.'; |
| |
| |
| let accountsData = []; |
| let modelsData = []; |
| let configData = {}; |
| let currentEditAccountId = null; |
| let currentEditModelId = null; |
| const ADMIN_TOKEN_KEY = 'admin_token'; |
| let tokensData = []; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| initTheme(); |
| loadAllData(); |
| setInterval(checkServerStatus, 30000); |
| updateLoginButton(); |
| }); |
| |
| |
| async function loadAllData() { |
| await Promise.all([ |
| loadAccounts(), |
| loadModels(), |
| loadConfig(), |
| checkServerStatus(), |
| loadLogLevel(), |
| loadTokens() |
| ]); |
| } |
| |
| function getAuthHeaders() { |
| const token = localStorage.getItem(ADMIN_TOKEN_KEY); |
| return token ? { 'X-Admin-Token': token } : {}; |
| } |
| |
| function updateLoginButton() { |
| const token = localStorage.getItem(ADMIN_TOKEN_KEY); |
| const btn = document.getElementById('loginButton'); |
| if (!btn) return; |
| if (token) { |
| btn.textContent = '注销'; |
| btn.disabled = false; |
| btn.classList.remove('btn-disabled'); |
| btn.title = '注销登录'; |
| btn.onclick = logoutAdmin; |
| } else { |
| btn.textContent = '登录'; |
| btn.disabled = false; |
| btn.classList.remove('btn-disabled'); |
| btn.title = '管理员登录'; |
| btn.onclick = showLoginModal; |
| } |
| } |
| |
| async function apiFetch(url, options = {}) { |
| const headers = Object.assign({}, options.headers || {}, getAuthHeaders()); |
| const res = await fetch(url, { ...options, headers }); |
| if (res.status === 401 || res.status === 403) { |
| showLoginModal(); |
| updateLoginButton(); |
| throw new Error('需要登录'); |
| } |
| return res; |
| } |
| |
| |
| function initTheme() { |
| const savedTheme = localStorage.getItem('theme') || 'light'; |
| document.documentElement.setAttribute('data-theme', savedTheme); |
| updateThemeIcon(savedTheme); |
| } |
| |
| function toggleTheme() { |
| const current = document.documentElement.getAttribute('data-theme'); |
| const newTheme = current === 'dark' ? 'light' : 'dark'; |
| document.documentElement.setAttribute('data-theme', newTheme); |
| localStorage.setItem('theme', newTheme); |
| updateThemeIcon(newTheme); |
| } |
| |
| |
| function switchTab(tabName) { |
| document.querySelectorAll('.tab').forEach(btn => btn.classList.remove('active')); |
| document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); |
| |
| const tabBtn = document.querySelector(`[onclick="switchTab('${tabName}')"]`); |
| const tabContent = document.getElementById(tabName); |
| |
| if (tabBtn) tabBtn.classList.add('active'); |
| if (tabContent) tabContent.classList.add('active'); |
| } |
| |
| |
| async function checkServerStatus() { |
| const indicator = document.getElementById('serviceStatus'); |
| if (!indicator) return; |
| try { |
| const res = await apiFetch(`${API_BASE}/api/status`); |
| console.log('Server Status Response:', res); |
| if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); |
| const data = await res.json(); |
| indicator.textContent = '服务运行中'; |
| indicator.classList.remove('offline'); |
| indicator.title = '服务连接正常 - ' + new Date().toLocaleString(); |
| } catch (e) { |
| indicator.textContent = '服务离线'; |
| indicator.classList.add('offline'); |
| indicator.title = '无法连接到后端服务'; |
| } |
| } |
| |
| |
| async function loadAccounts() { |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts`); |
| const data = await res.json(); |
| accountsData = data.accounts || []; |
| document.getElementById('currentIndex').textContent = data.current_index || 0; |
| renderAccounts(); |
| updateAccountStats(); |
| } catch (e) { |
| showToast('加载账号列表失败: ' + e.message, 'error'); |
| } |
| } |
| |
| function renderAccounts() { |
| const tbody = document.getElementById('accountsTableBody'); |
| if (!tbody) return; |
| |
| if (accountsData.length === 0) { |
| tbody.innerHTML = `<tr><td colspan="6" class="empty-state"> |
| <div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div> |
| <h3>暂无账号</h3><p>点击 "添加账号" 按钮来创建一个。</p> |
| </td></tr>`; |
| return; |
| } |
| |
| tbody.innerHTML = accountsData.map((acc, index) => ` |
| <tr> |
| <td>${index + 1}</td> |
| <td><code>${acc.team_id || '-'}</code></td> |
| <td><code>${acc.csesidx || '-'}</code></td> |
| <td title="${acc.user_agent}">${acc.user_agent ? acc.user_agent.substring(0, 30) + '...' : '-'}</td> |
| <td> |
| <span class="badge ${acc.available ? 'badge-success' : 'badge-danger'}">${acc.available ? '可用' : '不可用'}</span> |
| ${renderNextRefresh(acc)} |
| </td> |
| <td style="white-space: nowrap;"> |
| <button class="btn btn-sm ${acc.enabled !== false ? 'btn-warning' : 'btn-success'} btn-icon" onclick="toggleAccount(${acc.id})" title="${acc.enabled !== false ? '停用' : '启用'}"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-${acc.enabled !== false ? 'pause' : 'play'}"></use></svg></button> |
| <button class="btn btn-sm btn-outline btn-icon" onclick="testAccount(${acc.id})" title="测试连接"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-zap"></use></svg></button> |
| <button class="btn btn-sm btn-outline btn-icon" onclick="showRefreshCookieModal(${acc.id})" title="刷新Cookie"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-refresh"></use></svg></button> |
| <button class="btn btn-sm btn-outline btn-icon" onclick="showEditAccountModal(${acc.id})" title="编辑"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-settings"></use></svg></button> |
| <button class="btn btn-sm btn-danger btn-icon" onclick="deleteAccount(${acc.id})" title="删除"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-x"></use></svg></button> |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| function updateAccountStats() { |
| document.getElementById('totalAccounts').textContent = accountsData.length; |
| document.getElementById('availableAccounts').textContent = accountsData.filter(a => a.available).length; |
| document.getElementById('unavailableAccounts').textContent = accountsData.length - accountsData.filter(a => a.available).length; |
| } |
| |
| function renderNextRefresh(acc) { |
| if (!acc || !acc.cooldown_until) return ''; |
| const now = Date.now(); |
| const ts = acc.cooldown_until * 1000; |
| if (ts <= now) return ''; |
| const next = new Date(ts); |
| const remaining = Math.max(0, ts - now); |
| const minutes = Math.floor(remaining / 60000); |
| const label = minutes >= 60 |
| ? `${Math.floor(minutes / 60)}小时${minutes % 60}分` |
| : `${minutes}分`; |
| return `<span class="cooldown-hint">下次恢复: ${next.toLocaleString()}(约${label})</span>`; |
| } |
| |
| function showAddAccountModal() { |
| |
| document.getElementById('newAccountJson').value = ''; |
| document.getElementById('newTeamId').value = ''; |
| document.getElementById('newSecureCses').value = ''; |
| document.getElementById('newHostCoses').value = ''; |
| document.getElementById('newCsesidx').value = ''; |
| document.getElementById('newUserAgent').value = ''; |
| openModal('addAccountModal'); |
| } |
| |
| function showEditAccountModal(id) { |
| const acc = accountsData.find(a => a.id === id); |
| if (!acc) return; |
| |
| document.getElementById('editAccountId').value = id; |
| document.getElementById('editTeamId').value = acc.team_id || ''; |
| document.getElementById('editSecureCses').value = acc.secure_c_ses || ''; |
| document.getElementById('editHostCoses').value = acc.host_c_oses || ''; |
| document.getElementById('editCsesidx').value = acc.csesidx || ''; |
| document.getElementById('editUserAgent').value = acc.user_agent ? acc.user_agent.replace('...', '') : ''; |
| |
| openModal('editAccountModal'); |
| } |
| |
| async function updateAccount() { |
| const id = document.getElementById('editAccountId').value; |
| const account = {}; |
| |
| const teamId = document.getElementById('editTeamId').value; |
| const secureCses = document.getElementById('editSecureCses').value; |
| const hostCoses = document.getElementById('editHostCoses').value; |
| const csesidx = document.getElementById('editCsesidx').value; |
| const userAgent = document.getElementById('editUserAgent').value; |
| |
| if (teamId) account.team_id = teamId; |
| if (secureCses) account.secure_c_ses = secureCses; |
| if (hostCoses) account.host_c_oses = hostCoses; |
| if (csesidx) account.csesidx = csesidx; |
| if (userAgent) account.user_agent = userAgent; |
| |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(account) |
| }); |
| const data = await res.json(); |
| |
| if (data.success) { |
| showToast('账号更新成功', 'success'); |
| closeModal('editAccountModal'); |
| loadAccounts(); |
| } else { |
| showToast('更新失败: ' + (data.error || '未知错误'), 'error'); |
| } |
| } catch (e) { |
| showToast('更新失败: ' + e.message, 'error'); |
| } |
| } |
| |
| async function saveNewAccount() { |
| const teamId = document.getElementById('newTeamId').value; |
| const secureCses = document.getElementById('newSecureCses').value; |
| const hostCoses = document.getElementById('newHostCoses').value; |
| const csesidx = document.getElementById('newCsesidx').value; |
| const userAgent = document.getElementById('newUserAgent').value; |
| |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| team_id: teamId, |
| "secure_c_ses": secureCses, |
| "host_c_oses": hostCoses, |
| "csesidx": csesidx, |
| "user_agent": userAgent }) |
| }); |
| const data = await res.json(); |
| if (!res.ok || data.error) throw new Error(data.error || data.detail || '添加失败'); |
| showToast('账号添加成功!', 'success'); |
| closeModal('addAccountModal'); |
| loadAccounts(); |
| } catch (e) { |
| showToast('添加失败: ' + e.message, 'error'); |
| } |
| } |
| |
| function parseAccountJson(text) { |
| const textarea = document.getElementById('newAccountJson'); |
| const raw = (typeof text === 'string' ? text : textarea.value || '').trim(); |
| if (!raw) { |
| showToast('请先粘贴账号JSON', 'warning'); |
| return; |
| } |
| let acc; |
| try { |
| const parsed = JSON.parse(raw); |
| acc = Array.isArray(parsed) ? parsed[0] : parsed; |
| if (!acc || typeof acc !== 'object') throw new Error('格式不正确'); |
| } catch (err) { |
| showToast('解析失败: ' + err.message, 'error'); |
| return; |
| } |
| |
| document.getElementById('newTeamId').value = acc.team_id || ''; |
| document.getElementById('newSecureCses').value = acc.secure_c_ses || ''; |
| document.getElementById('newHostCoses').value = acc.host_c_oses || ''; |
| document.getElementById('newCsesidx').value = acc.csesidx || ''; |
| document.getElementById('newUserAgent').value = acc.user_agent || ''; |
| showToast('已填充账号信息', 'success'); |
| } |
| |
| async function pasteAccountJson() { |
| try { |
| if (!navigator.clipboard || !navigator.clipboard.readText) { |
| showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning'); |
| return; |
| } |
| const text = await navigator.clipboard.readText(); |
| document.getElementById('newAccountJson').value = text; |
| parseAccountJson(text); |
| } catch (e) { |
| showToast('无法读取剪贴板: ' + e.message, 'error'); |
| } |
| } |
| |
| async function deleteAccount(id) { |
| if (!confirm('确定要删除这个账号吗?')) return; |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, { method: 'DELETE' }); |
| if (!res.ok) throw new Error((await res.json()).detail); |
| showToast('账号删除成功!', 'success'); |
| loadAccounts(); |
| } catch (e) { |
| showToast('删除失败: ' + e.message, 'error'); |
| } |
| } |
| |
| async function testAccount(id) { |
| showToast(`正在测试账号ID: ${id}...`, 'info'); |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts/${id}/test`); |
| const data = await res.json(); |
| if (res.ok && data.success) { |
| showToast(`账号 ${id} 测试成功!`, 'success'); |
| } else { |
| throw new Error(data.detail || '未知错误'); |
| } |
| loadAccounts(); |
| } catch (e) { |
| showToast(`账号 ${id} 测试失败: ${e.message}`, 'error'); |
| } |
| } |
| |
| async function toggleAccount(id) { |
| const acc = accountsData.find(a => a.id === id); |
| const action = acc && acc.enabled !== false ? '停用' : '启用'; |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts/${id}/toggle`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' } |
| }); |
| const data = await res.json(); |
| if (res.ok && data.success) { |
| showToast(`账号 ${id} ${action}成功!`, 'success'); |
| loadAccounts(); |
| } else { |
| throw new Error(data.error || data.detail || '未知错误'); |
| } |
| } catch (e) { |
| showToast(`账号 ${id} ${action}失败: ${e.message}`, 'error'); |
| } |
| } |
| |
| |
| |
| |
| |
| function showRefreshCookieModal(id) { |
| const acc = accountsData.find(a => a.id === id); |
| if (!acc) { |
| showToast('账号不存在', 'error'); |
| return; |
| } |
| |
| document.getElementById('refreshAccountId').value = id; |
| document.getElementById('refreshSecureCses').value = ''; |
| document.getElementById('refreshHostCoses').value = ''; |
| document.getElementById('refreshCsesidx').value = ''; |
| document.getElementById('refreshCookieJson').value = ''; |
| |
| openModal('refreshCookieModal'); |
| } |
| |
| |
| |
| |
| |
| function parseRefreshCookieJson(text) { |
| const textarea = document.getElementById('refreshCookieJson'); |
| const raw = (typeof text === 'string' ? text : textarea.value || '').trim(); |
| if (!raw) { |
| showToast('请先粘贴Cookie JSON', 'warning'); |
| return; |
| } |
| let acc; |
| try { |
| const parsed = JSON.parse(raw); |
| acc = Array.isArray(parsed) ? parsed[0] : parsed; |
| if (!acc || typeof acc !== 'object') throw new Error('格式不正确'); |
| } catch (err) { |
| showToast('解析失败: ' + err.message, 'error'); |
| return; |
| } |
| |
| document.getElementById('refreshSecureCses').value = acc.secure_c_ses || ''; |
| document.getElementById('refreshHostCoses').value = acc.host_c_oses || ''; |
| document.getElementById('refreshCsesidx').value = acc.csesidx || ''; |
| showToast('已填充Cookie信息', 'success'); |
| } |
| |
| |
| |
| |
| async function pasteRefreshCookieJson() { |
| try { |
| if (!navigator.clipboard || !navigator.clipboard.readText) { |
| showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning'); |
| return; |
| } |
| const text = await navigator.clipboard.readText(); |
| document.getElementById('refreshCookieJson').value = text; |
| parseRefreshCookieJson(text); |
| } catch (e) { |
| showToast('无法读取剪贴板: ' + e.message, 'error'); |
| } |
| } |
| |
| |
| |
| |
| |
| async function refreshAccountCookie() { |
| const id = document.getElementById('refreshAccountId').value; |
| const secureCses = document.getElementById('refreshSecureCses').value.trim(); |
| const hostCoses = document.getElementById('refreshHostCoses').value.trim(); |
| const csesidx = document.getElementById('refreshCsesidx').value.trim(); |
| |
| |
| if (!secureCses || !hostCoses) { |
| showToast('secure_c_ses 和 host_c_oses 为必填项', 'warning'); |
| return; |
| } |
| |
| try { |
| const res = await apiFetch(`${API_BASE}/api/accounts/${id}/refresh-cookie`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| secure_c_ses: secureCses, |
| host_c_oses: hostCoses, |
| csesidx: csesidx || undefined |
| }) |
| }); |
| const data = await res.json(); |
| |
| if (res.ok && data.success) { |
| showToast('Cookie刷新成功!', 'success'); |
| closeModal('refreshCookieModal'); |
| loadAccounts(); |
| } else { |
| throw new Error(data.error || data.detail || '未知错误'); |
| } |
| } catch (e) { |
| showToast('Cookie刷新失败: ' + e.message, 'error'); |
| } |
| } |
| |
| |
| async function loadModels() { |
| try { |
| const res = await apiFetch(`${API_BASE}/api/models`); |
| const data = await res.json(); |
| modelsData = data.models || []; |
| renderModels(); |
| } catch (e) { |
| showToast('加载模型列表失败: ' + e.message, 'error'); |
| } |
| } |
| |
| function escapeHtml(str) { |
| if (!str) return ''; |
| return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); |
| } |
| |
| function renderModels() { |
| const tbody = document.getElementById('modelsTableBody'); |
| if (!tbody) return; |
| if (modelsData.length === 0) { |
| tbody.innerHTML = `<tr><td colspan="7" class="empty-state"> |
| <div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-robot"></use></svg></div> |
| <h3>暂无模型</h3><p>点击 "添加模型" 按钮来创建一个。</p> |
| </td></tr>`; |
| return; |
| } |
| tbody.innerHTML = modelsData.map((model, index) => { |
| const safeId = escapeHtml(model.id); |
| const safeName = escapeHtml(model.name); |
| const safeDesc = escapeHtml(model.description); |
| return ` |
| <tr> |
| <td><code>${safeId}</code></td> |
| <td>${safeName}</td> |
| <td title="${safeDesc}">${model.description ? safeDesc.substring(0, 40) + '...' : ''}</td> |
| <td>${model.context_length}</td> |
| <td>${model.max_tokens}</td> |
| <td><span class="badge ${model.is_public ? 'badge-success' : 'badge-warning'}">${model.is_public ? '公共' : '私有'}</span></td> |
| <td> |
| <button class="btn btn-sm btn-outline btn-icon" onclick="showEditModelModalByIndex(${index})" title="编辑">✏️</button> |
| <button class="btn btn-sm btn-danger btn-icon" onclick="deleteModelByIndex(${index})" title="删除">🗑️</button> |
| </td> |
| </tr> |
| `; |
| }).join(''); |
| } |
| |
| function showAddModelModal() { |
| openModal('addModelModal'); |
| } |
| |
| function showEditModelModalByIndex(index) { |
| const model = modelsData[index]; |
| if (!model) return; |
| |
| document.getElementById('editModelOriginalId').value = model.id; |
| document.getElementById('editModelId').value = model.id; |
| document.getElementById('editModelName').value = model.name || ''; |
| document.getElementById('editModelDesc').value = model.description || ''; |
| document.getElementById('editContextLength').value = model.context_length || ''; |
| document.getElementById('editMaxTokens').value = model.max_tokens || ''; |
| |
| openModal('editModelModal'); |
| } |
| |
| async function updateModel() { |
| const originalId = document.getElementById('editModelOriginalId').value; |
| const model = { |
| name: document.getElementById('editModelName').value, |
| description: document.getElementById('editModelDesc').value, |
| context_length: parseInt(document.getElementById('editContextLength').value) || 32000, |
| max_tokens: parseInt(document.getElementById('editMaxTokens').value) || 8096 |
| }; |
| |
| try { |
| const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(originalId)}`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(model) |
| }); |
| const data = await res.json(); |
| |
| if (data.success) { |
| showToast('模型更新成功', 'success'); |
| closeModal('editModelModal'); |
| loadModels(); |
| } else { |
| showToast('更新失败: ' + (data.error || '未知错误'), 'error'); |
| } |
| } catch (e) { |
| showToast('更新失败: ' + e.message, 'error'); |
| } |
| } |
| |
| |
| |
| |
| |
| async function saveNewModel() { |
| const modelId = document.getElementById('newModelId').value.trim(); |
| const modelName = document.getElementById('newModelName').value.trim(); |
| const modelDesc = document.getElementById('newModelDesc').value.trim(); |
| const contextLength = parseInt(document.getElementById('newContextLength').value) || 32000; |
| const maxTokens = parseInt(document.getElementById('newMaxTokens').value) || 8096; |
| |
| |
| if (!modelId) { |
| showToast('请输入模型ID', 'warning'); |
| return; |
| } |
| if (!modelName) { |
| showToast('请输入模型名称', 'warning'); |
| return; |
| } |
| |
| const model = { |
| id: modelId, |
| name: modelName, |
| description: modelDesc, |
| context_length: contextLength, |
| max_tokens: maxTokens |
| }; |
| |
| try { |
| const res = await apiFetch(`${API_BASE}/api/models`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(model) |
| }); |
| const data = await res.json(); |
| |
| if (res.ok && (data.success || !data.error)) { |
| showToast('模型添加成功', 'success'); |
| closeModal('addModelModal'); |
| |
| document.getElementById('newModelId').value = ''; |
| document.getElementById('newModelName').value = ''; |
| document.getElementById('newModelDesc').value = ''; |
| document.getElementById('newContextLength').value = ''; |
| document.getElementById('newMaxTokens').value = ''; |
| loadModels(); |
| } else { |
| throw new Error(data.error || '添加失败'); |
| } |
| } catch (e) { |
| showToast('添加模型失败: ' + e.message, 'error'); |
| } |
| } |
| |
| |
| |
| |
| |
| async function deleteModelByIndex(index) { |
| const model = modelsData[index]; |
| if (!model) return; |
| const id = model.id; |
| if (!confirm(`确定要删除模型 "${id}" 吗?此操作不可恢复。`)) { |
| return; |
| } |
| |
| try { |
| const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(id)}`, { |
| method: 'DELETE' |
| }); |
| const data = await res.json(); |
| |
| if (res.ok && (data.success || !data.error)) { |
| showToast('模型删除成功', 'success'); |
| loadModels(); |
| } else { |
| throw new Error(data.error || '删除失败'); |
| } |
| } catch (e) { |
| showToast('删除模型失败: ' + e.message, 'error'); |
| } |
| } |
| |
| |
| async function loadConfig() { |
| try { |
| const res = await apiFetch(`${API_BASE}/api/config`); |
| configData = await res.json(); |
| document.getElementById('proxyUrl').value = configData.proxy || ''; |
| const imageModeSelect = document.getElementById('imageOutputMode'); |
| if (imageModeSelect) { |
| const mode = (configData.image_output_mode || 'url'); |
| imageModeSelect.value = mode === 'base64' ? 'base64' : 'url'; |
| } |
| document.getElementById('configJson').value = JSON.stringify(configData, null, 2); |
| } catch (e) { |
| showToast('加载配置失败: ' + e.message, 'error'); |
| } |
| } |
| |
| async function loadLogLevel() { |
| try { |
| const res = await apiFetch(`${API_BASE}/api/logging`); |
| const data = await res.json(); |
| const select = document.getElementById('logLevelSelect'); |
| if (select && data.level) { |
| select.value = data.level; |
| } |
| } catch (e) { |
| console.warn('日志级别加载失败', e); |
| } |
| } |
| |
| async function updateLogLevel(level) { |
| try { |
| const res = await apiFetch(`${API_BASE}/api/logging`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ level }) |
| }); |
| const data = await res.json(); |
| if (!res.ok || data.error) { |
| throw new Error(data.error || '设置失败'); |
| } |
| showToast(`日志级别已切换为 ${data.level}`, 'success'); |
| } catch (e) { |
| showToast('日志级别设置失败: ' + e.message, 'error'); |
| } |
| } |
| |
| |
| async function loadTokens() { |
| try { |
| const res = await apiFetch(`${API_BASE}/api/tokens`); |
| const data = await res.json(); |
| tokensData = data.tokens || []; |
| renderTokens(); |
| } catch (e) { |
| showToast('加载 Token 失败: ' + e.message, 'error'); |
| } |
| } |
| |
| function renderTokens() { |
| const tbody = document.getElementById('tokensTableBody'); |
| if (!tbody) return; |
| if (!tokensData.length) { |
| tbody.innerHTML = `<tr><td colspan="2" class="empty-state">暂无 Token</td></tr>`; |
| return; |
| } |
| tbody.innerHTML = tokensData.map(token => ` |
| <tr> |
| <td><code>${token}</code></td> |
| <td style="white-space: nowrap;"> |
| <button class="btn btn-outline btn-sm" data-token="${token}" onclick="copyToken(this.dataset.token)" title="复制Token">复制</button> |
| <button class="btn btn-danger btn-sm" data-token="${token}" onclick="deleteToken(this.dataset.token)" title="删除Token">删除</button> |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function addToken() { |
| const manual = document.getElementById('manualToken').value.trim(); |
| try { |
| const res = await apiFetch(`${API_BASE}/api/tokens`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(manual ? { token: manual } : {}) |
| }); |
| const data = await res.json(); |
| if (!res.ok || data.error) throw new Error(data.error || '创建失败'); |
| document.getElementById('manualToken').value = data.token; |
| showToast('Token 创建成功', 'success'); |
| loadTokens(); |
| } catch (e) { |
| showToast('创建 Token 失败: ' + e.message, 'error'); |
| } |
| } |
| |
| function generateToken() { |
| if (window.crypto && crypto.randomUUID) { |
| document.getElementById('manualToken').value = crypto.randomUUID().replace(/-/g, ''); |
| } else { |
| document.getElementById('manualToken').value = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); |
| } |
| } |
| |
| async function deleteToken(token) { |
| if (!confirm('确定删除该 Token 吗?')) return; |
| try { |
| const res = await apiFetch(`${API_BASE}/api/tokens/${token}`, { method: 'DELETE' }); |
| const data = await res.json(); |
| if (!res.ok || data.error) throw new Error(data.error || '删除失败'); |
| showToast('Token 删除成功', 'success'); |
| loadTokens(); |
| } catch (e) { |
| showToast('删除 Token 失败: ' + e.message, 'error'); |
| } |
| } |
| |
| function copyToken(token) { |
| if (!token) { |
| showToast('无效的 Token', 'warning'); |
| return; |
| } |
| if (navigator.clipboard && navigator.clipboard.writeText) { |
| navigator.clipboard.writeText(token).then(() => { |
| showToast('已复制', 'success'); |
| }).catch(() => { |
| fallbackCopy(token); |
| }); |
| } else { |
| fallbackCopy(token); |
| } |
| } |
| |
| function fallbackCopy(text) { |
| try { |
| const textarea = document.createElement('textarea'); |
| textarea.value = text; |
| document.body.appendChild(textarea); |
| textarea.select(); |
| document.execCommand('copy'); |
| document.body.removeChild(textarea); |
| showToast('已复制', 'success'); |
| } catch (err) { |
| showToast('复制失败', 'error'); |
| } |
| } |
| |
| function logoutAdmin() { |
| localStorage.removeItem(ADMIN_TOKEN_KEY); |
| document.cookie = 'admin_token=; Max-Age=0; path=/'; |
| showToast('已注销', 'success'); |
| updateLoginButton(); |
| } |
| |
| function showLoginModal() { |
| document.getElementById('loginPassword').value = ''; |
| openModal('loginModal'); |
| } |
| |
| async function submitLogin() { |
| const pwd = document.getElementById('loginPassword').value; |
| if (!pwd) { |
| showToast('请输入密码', 'warning'); |
| return; |
| } |
| try { |
| const res = await fetch(`${API_BASE}/api/auth/login`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ password: pwd }) |
| }); |
| const data = await res.json(); |
| if (!res.ok || data.error) { |
| throw new Error(data.error || '登录失败'); |
| } |
| localStorage.setItem(ADMIN_TOKEN_KEY, data.token); |
| showToast('登录成功', 'success'); |
| closeModal('loginModal'); |
| loadAllData(); |
| updateLoginButton(); |
| } catch (e) { |
| showToast('登录失败: ' + e.message, 'error'); |
| } |
| } |
| |
| async function saveSettings() { |
| const proxyUrl = document.getElementById('proxyUrl').value; |
| const imageModeSelect = document.getElementById('imageOutputMode'); |
| const imageOutputMode = imageModeSelect ? imageModeSelect.value : 'url'; |
| try { |
| const res = await apiFetch(`${API_BASE}/api/config`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ proxy: proxyUrl, image_output_mode: imageOutputMode }) |
| }); |
| if (!res.ok) throw new Error((await res.json()).detail); |
| showToast('设置保存成功!', 'success'); |
| loadConfig(); |
| } catch (e) { |
| showToast('保存失败: ' + e.message, 'error'); |
| } |
| } |
| |
| async function testProxy() { |
| const proxyUrl = document.getElementById('proxyUrl').value; |
| const proxyStatus = document.getElementById('proxyStatus'); |
| proxyStatus.textContent = '测试中...'; |
| proxyStatus.style.color = 'var(--text-muted)'; |
| try { |
| const res = await apiFetch(`${API_BASE}/api/proxy/test`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ proxy: proxyUrl }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.success) { |
| proxyStatus.textContent = `测试成功! (${data.delay_ms}ms)`; |
| proxyStatus.style.color = 'var(--success)'; |
| } else { |
| throw new Error(data.detail); |
| } |
| } catch (e) { |
| proxyStatus.textContent = `测试失败: ${e.message}`; |
| proxyStatus.style.color = 'var(--danger)'; |
| } |
| } |
| |
| function refreshConfig() { |
| loadConfig(); |
| showToast('配置已刷新', 'info'); |
| } |
| |
| function downloadConfig() { |
| const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(configData, null, 2)); |
| const downloadAnchorNode = document.createElement('a'); |
| downloadAnchorNode.setAttribute("href", dataStr); |
| downloadAnchorNode.setAttribute("download", "business_gemini_session.json"); |
| document.body.appendChild(downloadAnchorNode); |
| downloadAnchorNode.click(); |
| downloadAnchorNode.remove(); |
| showToast('配置文件已开始下载', 'success'); |
| } |
| |
| function uploadConfig() { |
| document.getElementById('configFileInput').click(); |
| } |
| |
| function handleConfigUpload(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| try { |
| const newConfig = JSON.parse(e.target.result); |
| const res = await apiFetch(`${API_BASE}/api/config/import`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(newConfig) |
| }); |
| if (!res.ok) throw new Error((await res.json()).detail); |
| showToast('配置导入成功!', 'success'); |
| loadAllData(); |
| } catch (err) { |
| showToast('导入失败: ' + err.message, 'error'); |
| } |
| }; |
| reader.readAsText(file); |
| } |
| |
| |
| function openModal(modalId) { |
| const modal = document.getElementById(modalId); |
| if (modal) modal.classList.add('show'); |
| } |
| |
| function closeModal(modalId) { |
| const modal = document.getElementById(modalId); |
| if (modal) modal.classList.remove('show'); |
| } |
| |
| document.querySelectorAll('.modal').forEach(modal => { |
| modal.addEventListener('click', (e) => { |
| if (e.target.classList.contains('modal')) { |
| closeModal(modal.id); |
| } |
| }); |
| }); |
| </script> |
|
|
| </body> |
| </html> |
|
|