"""Web UI - 组件化单文件结构""" # ==================== CSS 样式 ==================== CSS_BASE = ''' * { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg: #0a0a0a; --card: #1a1a1a; --border: #333; --text: #fafafa; --muted: #a3a3a3; --accent: #3b82f6; --success: #22c55e; --error: #ef4444; --warn: #f59e0b; --info: #3b82f6; --primary: #6366f1; --secondary: #8b5cf6; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; } .container { max-width: 1200px; margin: 0 auto; padding: 1rem; min-height: 100vh; display: flex; flex-direction: column; } ''' CSS_LAYOUT = ''' /* Header */ header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding: 1.5rem; background: var(--card); border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } h1 { font-size: 1.75rem; font-weight: 700; display: flex; align-items: center; gap: 0.75rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } h1 img { width: 32px; height: 32px; border-radius: 8px; } .status { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; color: var(--muted); } .status-dot { width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 0 8px currentColor; } .status-dot.ok { background: var(--success); color: var(--success); } .status-dot.err { background: var(--error); color: var(--error); } /* Navigation Tabs */ .tabs { display: flex; justify-content: center; gap: 0.5rem; margin-bottom: 2rem; padding: 0.5rem; background: var(--card); border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } .tab { padding: 0.75rem 1.5rem; border: none; background: transparent; color: var(--muted); cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: all 0.3s ease; border-radius: 12px; position: relative; } .tab:hover { color: var(--text); background: rgba(255,255,255,0.05); } .tab.active { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; box-shadow: 0 4px 12px rgba(59,130,246,0.3); } /* Panels */ .panel { display: none; flex: 1; } .panel.active { display: block; } /* Footer */ .footer { text-align: center; color: var(--muted); font-size: 0.75rem; margin-top: 2rem; padding: 1rem; border-top: 1px solid var(--border); } ''' CSS_COMPONENTS = ''' /* Cards */ .card { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 2rem; margin-bottom: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: all 0.3s ease; } .card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.4); transform: translateY(-2px); } .card h3 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; color: var(--text); } /* Stats Grid - OXO Style */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; } .stat-item { text-align: center; padding: 1.5rem; background: linear-gradient(135deg, rgba(59,130,246,0.1), rgba(139,92,246,0.1)); border-radius: 16px; border: 1px solid rgba(59,130,246,0.2); transition: all 0.3s ease; } .stat-item:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(59,130,246,0.2); } .stat-value { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 0.5rem; } .stat-label { font-size: 0.875rem; color: var(--muted); font-weight: 500; } /* Compact Stats Grid for Monitor Panel */ .stats-grid-compact { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.5rem; } .stats-grid-compact .stat-item { text-align: center; padding: 0.5rem; background: linear-gradient(135deg, rgba(59,130,246,0.08), rgba(139,92,246,0.08)); border-radius: 8px; border: 1px solid rgba(59,130,246,0.15); transition: all 0.2s ease; } .stats-grid-compact .stat-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(59,130,246,0.15); } .stats-grid-compact .stat-value { font-size: 1.2rem; font-weight: 600; background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 0.25rem; line-height: 1.2; } .stats-grid-compact .stat-label { font-size: 0.7rem; color: var(--muted); font-weight: 500; line-height: 1.2; } /* Badges */ .badge { display: inline-flex; align-items: center; padding: 0.375rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; } .badge.success { background: linear-gradient(135deg, #22c55e, #16a34a); color: white; box-shadow: 0 2px 8px rgba(34,197,94,0.3); } .badge.error { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; box-shadow: 0 2px 8px rgba(239,68,68,0.3); } .badge.warn { background: linear-gradient(135deg, #f59e0b, #d97706); color: white; box-shadow: 0 2px 8px rgba(245,158,11,0.3); } .badge.info { background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; box-shadow: 0 2px 8px rgba(59,130,246,0.3); } /* Circular Progress */ .progress-circle { width: 80px; height: 80px; border-radius: 50%; background: conic-gradient(var(--primary) 0deg, var(--secondary) 180deg, var(--border) 180deg); display: flex; align-items: center; justify-content: center; position: relative; } .progress-circle::before { content: ''; width: 60px; height: 60px; border-radius: 50%; background: var(--card); position: absolute; } .progress-text { position: relative; z-index: 1; font-weight: 700; font-size: 0.875rem; } ''' CSS_FORMS = ''' /* Buttons - OXO Style */ button { padding: 0.75rem 1.5rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 0.875rem; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(59,130,246,0.3); text-transform: uppercase; letter-spacing: 0.025em; } button:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(59,130,246,0.4); } button:active { transform: translateY(0); } button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } button.secondary { background: var(--card); color: var(--text); border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } button.secondary:hover { background: rgba(255,255,255,0.05); border-color: var(--primary); } button.small { padding: 0.5rem 1rem; font-size: 0.75rem; border-radius: 8px; } button.circle { width: 48px; height: 48px; border-radius: 50%; padding: 0; display: flex; align-items: center; justify-content: center; } button.large { padding: 1rem 2rem; font-size: 1rem; border-radius: 16px; } /* Inputs */ input[type="text"], input[type="number"], input[type="search"], input[type="password"], textarea { padding: 0.75rem 1rem; border: 1px solid var(--border); border-radius: 12px; background: var(--card); color: var(--text); font-size: 0.875rem; transition: all 0.3s ease; width: 100%; } input:hover, textarea:hover { border-color: var(--primary); } input:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,0.1); } input::placeholder, textarea::placeholder { color: var(--muted); } /* Select */ select { padding: 0.75rem 1rem; border: 1px solid var(--border); border-radius: 12px; background: var(--card); color: var(--text); font-size: 0.875rem; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a3a3a3' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 1rem center; padding-right: 3rem; transition: all 0.3s ease; } select:hover { border-color: var(--primary); } select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,0.1); } /* Tables */ table { width: 100%; border-collapse: collapse; font-size: 0.875rem; background: var(--card); border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } th, td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--border); } /* Compact table styles for monitor panel */ #monitor table th, #monitor table td { padding: 0.25rem 0.5rem; font-size: 0.75rem; line-height: 1.3; } #monitor table tbody tr { height: 32px; } #monitor table tbody tr:hover { background: rgba(59,130,246,0.05); } th { font-weight: 600; color: var(--muted); background: rgba(59,130,246,0.05); } tr:hover { background: rgba(255,255,255,0.02); } /* Code blocks */ pre { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; overflow-x: auto; font-size: 0.8rem; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; } code { background: rgba(59,130,246,0.1); padding: 0.25rem 0.5rem; border-radius: 6px; font-size: 0.875em; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; } ''' CSS_ACCOUNTS = ''' .account-card { border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin-bottom: 0.75rem; background: var(--card); } .account-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } .account-name { font-weight: 500; display: flex; align-items: center; gap: 0.5rem; } .account-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; font-size: 0.8rem; color: var(--muted); } .account-meta-item { display: flex; justify-content: space-between; padding: 0.25rem 0; } .account-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border); } ''' CSS_API = ''' .endpoint { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; } .method { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; } .method.get { background: #dcfce7; color: #166534; } .method.post { background: #fef3c7; color: #92400e; } @media (prefers-color-scheme: dark) { .method.get { background: #14532d; color: #86efac; } .method.post { background: #78350f; color: #fde68a; } } .copy-btn { padding: 0.25rem 0.5rem; font-size: 0.75rem; background: var(--card); border: 1px solid var(--border); color: var(--text); } ''' CSS_DOCS = ''' .docs-container { display: flex; gap: 1.5rem; min-height: 500px; } .docs-nav { width: 200px; flex-shrink: 0; } .docs-nav-item { display: block; padding: 0.5rem 0.75rem; margin-bottom: 0.25rem; border-radius: 6px; cursor: pointer; font-size: 0.875rem; color: var(--text); text-decoration: none; transition: background 0.2s; } .docs-nav-item:hover { background: var(--bg); } .docs-nav-item.active { background: var(--accent); color: var(--bg); } .docs-content { flex: 1; min-width: 0; } .docs-content h1 { font-size: 1.5rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); } .docs-content h2 { font-size: 1.25rem; margin: 1.5rem 0 0.75rem; color: var(--text); } .docs-content h3 { font-size: 1rem; margin: 1rem 0 0.5rem; color: var(--text); } .docs-content h4 { font-size: 0.9rem; margin: 0.75rem 0 0.5rem; color: var(--muted); } .docs-content p { margin: 0.5rem 0; } .docs-content ul, .docs-content ol { margin: 0.5rem 0; padding-left: 1.5rem; } .docs-content li { margin: 0.25rem 0; } .docs-content code { background: var(--bg); padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; } .docs-content pre { margin: 0.75rem 0; } .docs-content pre code { background: none; padding: 0; } .docs-content table { margin: 0.75rem 0; } .docs-content blockquote { margin: 0.75rem 0; padding: 0.5rem 1rem; border-left: 3px solid var(--border); color: var(--muted); background: var(--bg); border-radius: 0 6px 6px 0; } .docs-content hr { margin: 1.5rem 0; border: none; border-top: 1px solid var(--border); } .docs-content a { color: var(--info); text-decoration: none; } .docs-content a:hover { text-decoration: underline; } @media (max-width: 768px) { .docs-container { flex-direction: column; } .docs-nav { width: 100%; display: flex; flex-wrap: wrap; gap: 0.5rem; } .docs-nav-item { margin-bottom: 0; } } ''' # ==================== UI 组件库样式 ==================== CSS_UI_COMPONENTS = ''' /* Modal 模态框 */ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.2s; } .modal-overlay.active { opacity: 1; visibility: visible; } .modal { background: var(--card); border-radius: 12px; max-width: 500px; width: 90%; max-height: 90vh; overflow: hidden; transform: scale(0.9); transition: transform 0.2s; } .modal-overlay.active .modal { transform: scale(1); } .modal-header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; } .modal-header h3 { font-size: 1.1rem; font-weight: 600; } .modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--muted); padding: 0; line-height: 1; } .modal-body { padding: 1.5rem; overflow-y: auto; max-height: 60vh; } .modal-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 0.5rem; } .modal.danger .modal-header { background: #fee2e2; } .modal.warning .modal-header { background: #fef3c7; } @media (prefers-color-scheme: dark) { .modal.danger .modal-header { background: #7f1d1d; } .modal.warning .modal-header { background: #78350f; } } /* Toast 通知 */ .toast-container { position: fixed; top: 1rem; right: 1rem; z-index: 1100; display: flex; flex-direction: column; gap: 0.5rem; } .toast { padding: 0.75rem 1rem; border-radius: 8px; background: var(--card); border: 1px solid var(--border); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 0.5rem; animation: slideIn 0.3s ease; min-width: 250px; } .toast.success { border-left: 4px solid var(--success); } .toast.error { border-left: 4px solid var(--error); } .toast.warning { border-left: 4px solid var(--warn); } .toast.info { border-left: 4px solid var(--info); } .toast-close { margin-left: auto; background: none; border: none; cursor: pointer; color: var(--muted); font-size: 1.2rem; padding: 0; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } /* Select 下拉选择 */ .custom-select { position: relative; } .custom-select-trigger { padding: 0.75rem 1rem; border: 1px solid var(--border); border-radius: 6px; background: var(--card); cursor: pointer; display: flex; justify-content: space-between; align-items: center; } .custom-select-trigger::after { content: "▼"; font-size: 0.7rem; color: var(--muted); } .custom-select-options { position: absolute; top: 100%; left: 0; right: 0; background: var(--card); border: 1px solid var(--border); border-radius: 6px; margin-top: 4px; max-height: 200px; overflow-y: auto; z-index: 100; display: none; } .custom-select.open .custom-select-options { display: block; } .custom-select-option { padding: 0.5rem 1rem; cursor: pointer; } .custom-select-option:hover { background: var(--bg); } .custom-select-option.selected { background: var(--accent); color: var(--bg); } /* ProgressBar 进度条 */ .progress-bar { height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden; } .progress-bar.large { height: 12px; } .progress-bar.small { height: 4px; } .progress-fill { height: 100%; background: var(--info); transition: width 0.3s; } .progress-fill.success { background: var(--success); } .progress-fill.warning { background: var(--warn); } .progress-fill.error { background: var(--error); } .progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--muted); margin-top: 0.25rem; } /* Dropdown 下拉菜单 */ .dropdown { position: relative; display: inline-block; } .dropdown-menu { position: absolute; top: 100%; right: 0; background: var(--card); border: 1px solid var(--border); border-radius: 8px; min-width: 120px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 100; display: none; margin-top: 4px; overflow: hidden; } .dropdown.open .dropdown-menu { display: block; } .dropdown-item { padding: 0.5rem 0.75rem; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; white-space: nowrap; } .dropdown-item:hover { background: var(--bg); } .dropdown-item.danger { color: var(--error); } .dropdown-divider { height: 1px; background: var(--border); margin: 0.25rem 0; } /* 账号卡片增强 */ .account-card-enhanced { border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; background: var(--card); } .account-card-enhanced.priority { border-color: var(--info); border-width: 2px; } .account-card-enhanced.active { box-shadow: 0 0 0 2px var(--success); } .account-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; } .account-card-title { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .account-card-badges { display: flex; gap: 0.25rem; flex-wrap: wrap; } .account-quota-section { margin: 1rem 0; } .quota-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.875rem; } .quota-detail { display: flex; gap: 1rem; font-size: 0.75rem; color: var(--muted); margin-top: 0.5rem; flex-wrap: wrap; } .quota-reset-info { display: flex; gap: 1rem; flex-wrap: wrap; } .quota-reset-info span { display: inline-flex; align-items: center; gap: 0.25rem; } .account-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; margin: 1rem 0; } .account-stat { text-align: center; padding: 0.5rem; background: var(--bg); border-radius: 6px; } .account-stat-value { font-weight: 600; font-size: 0.9rem; } .account-stat-label { font-size: 0.7rem; color: var(--muted); } /* 账号网格布局 - 动态自适应 */ .accounts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; margin-top: 1rem; } .account-card-compact { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 0.875rem; transition: all 0.2s; } .account-card-compact:hover { border-color: var(--accent); } .account-card-compact.priority { border-color: var(--info); border-width: 2px; } .account-card-compact.low-balance { border-color: var(--warn); } .account-card-compact.exhausted { border-color: var(--error); border-width: 2px; } .account-card-compact.suspended { border-color: var(--error); border-width: 2px; background: rgba(239, 68, 68, 0.1); } .account-card-compact.unavailable { opacity: 0.6; } .account-card-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem; } .account-card-info { flex: 1; min-width: 0; } .account-card-name { font-weight: 600; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 0.25rem; } .account-card-email { font-size: 0.75rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .account-card-status { display: flex; gap: 0.25rem; flex-wrap: wrap; } .account-card-quota { margin: 0.75rem 0; } .account-card-quota-bar { height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; } .account-card-quota-fill { height: 100%; transition: width 0.3s; } .account-card-quota-text { display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--muted); margin-top: 0.25rem; } .account-card-stats { display: flex; gap: 1rem; font-size: 0.75rem; color: var(--muted); margin-bottom: 0.75rem; } .account-card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; padding-top: 0.75rem; border-top: 1px solid var(--border); } .account-card-actions button { flex: 1; min-width: 60px; } /* 紧凑汇总面板 */ .summary-compact { display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; padding: 0.75rem; background: var(--bg); border-radius: 8px; } .summary-compact-item { display: flex; align-items: center; gap: 0.5rem; } .summary-compact-value { font-weight: 600; font-size: 1.1rem; } .summary-compact-label { font-size: 0.75rem; color: var(--muted); } .summary-compact-divider { width: 1px; height: 24px; background: var(--border); } .summary-quota-bar { flex: 1; min-width: 200px; } /* 全局进度条 - 批量刷新操作进度显示 */ .global-progress-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 1200; background: var(--card); border-bottom: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.1); transform: translateY(-100%); transition: transform 0.3s ease; } .global-progress-bar.active { transform: translateY(0); } .global-progress-bar-inner { max-width: 1400px; margin: 0 auto; padding: 0.75rem 1rem; } .global-progress-bar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } .global-progress-bar-title { font-weight: 600; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; } .global-progress-bar-title .spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; } .global-progress-bar-stats { display: flex; gap: 1rem; font-size: 0.8rem; color: var(--muted); } .global-progress-bar-stats span { display: flex; align-items: center; gap: 0.25rem; } .global-progress-bar-stats .success { color: var(--success); } .global-progress-bar-stats .error { color: var(--error); } .global-progress-bar-track { height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; margin-bottom: 0.5rem; } .global-progress-bar-fill { height: 100%; background: var(--info); transition: width 0.3s ease; border-radius: 3px; } .global-progress-bar-fill.complete { background: var(--success); } .global-progress-bar-current { font-size: 0.75rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .global-progress-bar-close { background: none; border: none; font-size: 1.2rem; cursor: pointer; color: var(--muted); padding: 0; margin-left: 0.5rem; } .global-progress-bar-close:hover { color: var(--text); } /* 汇总面板 */ .summary-panel { background: linear-gradient(135deg, var(--card) 0%, var(--bg) 100%); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 1rem; margin-bottom: 1rem; } .summary-item { text-align: center; } .summary-value { font-size: 1.75rem; font-weight: 700; } .summary-label { font-size: 0.75rem; color: var(--muted); } .summary-item.success .summary-value { color: var(--success); } .summary-item.warning .summary-value { color: var(--warn); } .summary-item.error .summary-value { color: var(--error); } .summary-quota { margin: 1rem 0; } .summary-info { display: flex; gap: 2rem; flex-wrap: wrap; font-size: 0.875rem; color: var(--muted); } .summary-actions { margin-top: 1rem; display: flex; gap: 0.5rem; } ''' CSS_AUTH = ''' /* 登录模态框 */ .auth-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--bg); z-index: 2000; display: flex; align-items: center; justify-content: center; } .auth-modal { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 2rem; max-width: 400px; width: 90%; text-align: center; } .auth-modal h2 { margin-bottom: 1rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .auth-modal input { margin: 1rem 0; } .auth-modal .auth-error { color: var(--error); font-size: 0.875rem; margin-top: 0.5rem; } ''' CSS_STYLES = CSS_BASE + CSS_LAYOUT + CSS_COMPONENTS + CSS_FORMS + CSS_ACCOUNTS + CSS_API + CSS_DOCS + CSS_UI_COMPONENTS + CSS_AUTH # ==================== HTML 模板 ==================== # 登录模态框 HTML_AUTH_MODAL = ''' ''' # 全局进度条容器 - 显示在页面顶部 HTML_GLOBAL_PROGRESS = '''
正在刷新额度...
完成: 0/0 成功: 0 失败: 0
准备中...
''' HTML_HEADER = '''

KiroKiro API Proxy

检查中...
📚 帮助
📊 监控
👥 账号
🔌 API
⚙️ 设置
''' HTML_HELP = '''

加载中...

''' HTML_FLOWS = '''

Flow 统计

流量监控

''' HTML_MONITOR = '''

🚀 服务状态

📈 流量统计

⚡ 配额状态

🎯 速度测试 点击开始测试

📋 请求监控

📝 请求日志

时间 路径 模型 账号 状态 耗时
''' HTML_ACCOUNTS = '''

账号管理

''' HTML_LOGS = '''

请求日志

时间路径模型账号状态耗时
''' HTML_API = '''

API 端点

支持 OpenAI、Anthropic、Gemini 三种协议

OpenAI 协议

POST/v1/chat/completions
GET/v1/models

Anthropic 协议

POST/v1/messages
POST/v1/messages/count_tokens

Gemini 协议

POST/v1/models/{model}:generateContent

Base URL

配置示例

Claude Code

Base URL: 
API Key: any
模型: claude-sonnet-4

Codex CLI

Endpoint: /v1
API Key: any
模型: gpt-4o

Claude Code 终端配置

Claude Code 终端版需要配置 ~/.claude/settings.json 才能跳过登录使用代理

临时生效(当前终端)

export ANTHROPIC_BASE_URL=""
export ANTHROPIC_AUTH_TOKEN="sk-any"
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

永久生效(推荐,写入配置文件)

# 写入 Claude Code 配置文件
mkdir -p ~/.claude
cat > ~/.claude/settings.json << 'EOF'
{
  "env": {
    "ANTHROPIC_BASE_URL": "",
    "ANTHROPIC_AUTH_TOKEN": "sk-any",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
  }
}
EOF

清除配置

# 删除 Claude Code 配置
rm -f ~/.claude/settings.json
unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC

💡 使用 ANTHROPIC_AUTH_TOKEN + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 可跳过登录

模型映射

支持多种模型名称,自动映射到 Kiro 模型

Kiro 模型能力可用名称
claude-sonnet-4⭐⭐⭐ 推荐gpt-4o, gpt-4, claude-3-5-sonnet-*, sonnet
claude-sonnet-4.5⭐⭐⭐⭐ 更强gemini-1.5-pro, o1, o1-preview, claude-3-opus-*, opus
claude-haiku-4.5⚡ 快速gpt-4o-mini, gpt-3.5-turbo, haiku
auto🤖 自动auto

💡 直接使用 Kiro 模型名(如 claude-sonnet-4)或任意映射名称均可

''' HTML_SETTINGS = '''

🤖 自动化管理

以下功能已启用自动化管理,无需手动配置:

🔄 Token 与额度刷新 自动

Token 过期前自动刷新,额度信息定期更新

请求限速与 429 冷却 自动

遇到 429 错误自动冷却 5 分钟,自动切换到其他可用账号

📝 历史消息压缩 自动

上下文超限时默认自动压缩并重试,也可在下方设置为超限直接报错

🎲 账号负载均衡 自动

支持随机、轮询、最少请求等多种账号选择策略,分散请求压力

历史消息管理

控制「上下文超限」时的行为:自动压缩重试,或直接报错

🤖 错误触发压缩模式 自动

不再依赖阈值预检测,仅在收到上下文超限错误后自动压缩到 20K-50K 字符范围

超限错误后的重试次数
相同上下文复用摘要

工作原理:
1. 正常发送请求,不进行预检测
2. 收到 CONTENT_LENGTH_EXCEEDS_THRESHOLD 错误时触发压缩
3. 用 AI 生成早期对话摘要,保留最近 6-20 条消息
4. 压缩目标: 20K-50K 字符,自动重试
5. 关闭“超限自动压缩并重试”后:超限直接报错,不进行压缩/重试

✓ 最大化利用上下文   ✓ 错误触发无需预估   ✓ 智能缓存避免重复调用

''' HTML_BODY = HTML_AUTH_MODAL + HTML_GLOBAL_PROGRESS + HTML_HEADER + HTML_HELP + HTML_MONITOR + HTML_ACCOUNTS + HTML_API + HTML_SETTINGS # ==================== JavaScript ==================== JS_AUTH = ''' // ==================== 认证检查 ==================== let authToken = localStorage.getItem('admin_session'); let authRequired = false; async function checkAuth() { try { const r = await fetch('/api/auth/check', { method: 'POST', headers: authToken ? {'Authorization': 'Bearer ' + authToken} : {} }); const d = await r.json(); authRequired = d.auth_required; if (d.auth_required && !d.authenticated) { showAuthModal(); return false; } else { hideAuthModal(); return true; } } catch(e) { console.error('认证检查失败:', e); return true; } } function showAuthModal() { const overlay = $('#authOverlay'); if (overlay) { overlay.style.display = 'flex'; setTimeout(() => $('#authPassword')?.focus(), 100); } } function hideAuthModal() { const overlay = $('#authOverlay'); if (overlay) { overlay.style.display = 'none'; } } async function adminLogin() { const password = $('#authPassword').value; const errorEl = $('#authError'); if (!password) { errorEl.textContent = '请输入密码'; return; } try { const r = await fetch('/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({password}) }); const d = await r.json(); if (d.success && d.session_id) { authToken = d.session_id; localStorage.setItem('admin_session', authToken); hideAuthModal(); errorEl.textContent = ''; $('#authPassword').value = ''; Toast.success('登录成功'); initializeDefaultTab(); } else { errorEl.textContent = d.detail || '密码错误'; } } catch(e) { errorEl.textContent = '登录失败: ' + e.message; } } function adminLogout() { localStorage.removeItem('admin_session'); authToken = null; if (authRequired) { showAuthModal(); } Toast.info('已登出'); } // 页面加载时检查认证 checkAuth(); ''' JS_UTILS = ''' const $=s=>document.querySelector(s); const $$=s=>document.querySelectorAll(s); function copy(text){ navigator.clipboard.writeText(text).then(()=>{ const toast=document.createElement('div'); toast.textContent='已复制'; toast.style.cssText='position:fixed;bottom:2rem;left:50%;transform:translateX(-50%);background:var(--accent);color:var(--bg);padding:0.5rem 1rem;border-radius:6px;font-size:0.875rem;z-index:1000'; document.body.appendChild(toast); setTimeout(()=>toast.remove(),1500); }); } function copyEnvTemp(){ const url=location.origin; copy(`export ANTHROPIC_BASE_URL="${url}" export ANTHROPIC_AUTH_TOKEN="sk-any" export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`); } function copyEnvPerm(){ const url=location.origin; copy(`# 写入 Claude Code 配置文件(推荐) mkdir -p ~/.claude cat > ~/.claude/settings.json << 'EOF' { "env": { "ANTHROPIC_BASE_URL": "${url}", "ANTHROPIC_AUTH_TOKEN": "sk-any", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" } } EOF echo "配置完成,请重新打开终端运行 claude"`); } function copyEnvClear(){ copy(`# 删除 Claude Code 配置 rm -f ~/.claude/settings.json unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC echo "配置已清除"`); } function formatUptime(s){ if(s<60)return s+'秒'; if(s<3600)return Math.floor(s/60)+'分钟'; return Math.floor(s/3600)+'小时'+Math.floor((s%3600)/60)+'分钟'; } function escapeHtml(text){ const div=document.createElement('div'); div.textContent=text; return div.innerHTML; } ''' JS_TABS = ''' // Tabs $$('.tab').forEach(t=>t.onclick=()=>{ $$('.tab').forEach(x=>x.classList.remove('active')); $$('.panel').forEach(x=>x.classList.remove('active')); t.classList.add('active'); $('#'+t.dataset.tab).classList.add('active'); // 监控面板加载所有数据 if(t.dataset.tab==='monitor'){ loadStats(); loadQuota(); loadFlowStats(); loadFlows(); loadLogs(); } if(t.dataset.tab==='accounts'){ loadAccounts(); loadAccountsEnhanced(); } }); ''' JS_STATUS = ''' // Status async function checkStatus(){ try{ const r=await fetch('/api/status'); const d=await r.json(); $('#statusDot').className='status-dot '+(d.ok?'ok':'err'); $('#statusText').textContent=d.ok?'已连接':'未连接'; if(d.stats)$('#uptime').textContent='运行 '+formatUptime(d.stats.uptime_seconds); }catch(e){ $('#statusDot').className='status-dot err'; $('#statusText').textContent='连接失败'; } } checkStatus(); setInterval(checkStatus,30000); // URLs $('#baseUrl').textContent=location.origin; $$('.pyUrl').forEach(e=>e.textContent=location.origin); ''' JS_DOCS = ''' // 文档浏览 let docsData = []; let currentDoc = null; // 简单的 Markdown 渲染 function renderMarkdown(text) { return text .replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '
$2
') .replace(/`([^`]+)`/g, '$1') .replace(/^#### (.+)$/gm, '

$1

') .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') .replace(/\\*\\*(.+?)\\*\\*/g, '$1') .replace(/\\*(.+?)\\*/g, '$1') .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '$1') .replace(/^- (.+)$/gm, '
  • $1
  • ') .replace(/(
  • .*<\\/li>\\n?)+/g, '') .replace(/^\\d+\\. (.+)$/gm, '
  • $1
  • ') .replace(/^> (.+)$/gm, '
    $1
    ') .replace(/^---$/gm, '
    ') .replace(/\\|(.+)\\|/g, function(match) { const cells = match.split('|').filter(c => c.trim()); if (cells.every(c => /^[\\s-:]+$/.test(c))) return ''; const tag = match.includes('---') ? 'th' : 'td'; return '' + cells.map(c => '<' + tag + '>' + c.trim() + '').join('') + ''; }) .replace(/(.*<\\/tr>\\n?)+/g, '$&
    ') .replace(/\\n\\n/g, '

    ') .replace(/\\n/g, '
    '); } async function loadDocs() { try { const r = await fetch('/api/docs'); const d = await r.json(); docsData = d.docs || []; // 渲染导航 $('#docsNav').innerHTML = docsData.map((doc, i) => '' + doc.title + '' ).join(''); // 显示第一个文档 if (docsData.length > 0) { showDoc(docsData[0].id); } } catch (e) { $('#docsContent').innerHTML = '

    加载文档失败

    '; } } async function showDoc(id) { // 更新导航状态 $$('.docs-nav-item').forEach(item => { item.classList.toggle('active', item.dataset.id === id); }); // 获取文档内容 try { const r = await fetch('/api/docs/' + id); const d = await r.json(); currentDoc = d; $('#docsContent').innerHTML = renderMarkdown(d.content); } catch (e) { $('#docsContent').innerHTML = '

    加载文档失败

    '; } } // 页面加载时加载文档 loadDocs(); ''' JS_STATS = ''' // Stats async function loadStats(){ try{ const r=await fetch('/api/stats'); const d=await r.json(); $('#statsGrid').innerHTML=`
    ${d.total_requests}
    总请求
    ${d.total_errors}
    错误数
    ${d.error_rate}
    错误率
    ${d.accounts_available}/${d.accounts_total}
    可用账号
    ${d.accounts_cooldown||0}
    冷却中
    `; }catch(e){console.error(e)} } // Quota async function loadQuota(){ try{ const r=await fetch('/api/quota'); const d=await r.json(); if(d.exceeded_credentials&&d.exceeded_credentials.length>0){ $('#quotaStatus').innerHTML=d.exceeded_credentials.map(c=>`
    冷却中 ${c.credential_id} 剩余 ${c.remaining_seconds}秒
    `).join(''); }else{ $('#quotaStatus').innerHTML='

    无冷却中的账号

    '; } }catch(e){console.error(e)} } // Speedtest async function runSpeedtest(){ $('#speedtestBtn').disabled=true; $('#speedtestResult').textContent='测试中...'; try{ const r=await fetch('/api/speedtest',{method:'POST'}); const d=await r.json(); $('#speedtestResult').textContent=d.ok?`延迟: ${d.latency_ms.toFixed(0)}ms (${d.account_id})`:'测试失败: '+d.error; }catch(e){$('#speedtestResult').textContent='测试失败'} $('#speedtestBtn').disabled=false; } ''' JS_LOGS = ''' // Logs async function loadLogs(){ try{ const r=await fetch('/api/logs?limit=50'); const d=await r.json(); $('#logTable').innerHTML=(d.logs||[]).map(l=>` ${new Date(l.timestamp*1000).toLocaleTimeString()} ${l.path} ${l.model||'-'} ${l.account_id||'-'} ${l.status} ${l.duration_ms.toFixed(0)}ms `).join(''); }catch(e){console.error(e)} } ''' JS_ACCOUNTS = ''' // Accounts async function loadAccounts(){ try{ const r=await fetch('/api/accounts'); const d=await r.json(); if(!d.accounts||d.accounts.length===0){ $('#accountList').innerHTML='

    暂无账号,请点击"扫描 Token"

    '; return; } $('#accountList').innerHTML=d.accounts.map(a=>{ const statusBadge=a.status==='active'?'success':a.status==='cooldown'?'warn':a.status==='suspended'?'error':'error'; const statusText={active:'可用',cooldown:'冷却中',unhealthy:'不健康',disabled:'已禁用',suspended:'已封禁'}[a.status]||a.status; const authBadge=a.auth_method==='idc'?'info':'success'; const authText=a.auth_method==='idc'?'IdC':'Social'; return `
    ${a.id}
    ${a.cooldown_remaining?``:''}
    ${a.status==='cooldown'?``:''}
    `; }).join(''); }catch(e){console.error(e)} } async function queryUsage(id){ const usageDiv=$('#usage-'+id); usageDiv.style.display='block'; usageDiv.innerHTML='查询中...'; try{ const r=await fetch('/api/accounts/'+id+'/usage'); const d=await r.json(); if(d.ok){ const u=d.usage; const pct=u.usage_limit>0?((u.current_usage/u.usage_limit)*100).toFixed(1):0; const barColor=u.is_low_balance?'var(--error)':'var(--success)'; usageDiv.innerHTML=`
    ${u.subscription_title} ${u.is_low_balance?'余额不足':'正常'}
    已用/总额: ${u.current_usage.toFixed(2)} / ${u.usage_limit.toFixed(2)}
    使用率: ${pct}%
    ${u.reset_date_text ? `
    重置时间: ${u.reset_date_text}
    ` : ''} ${u.trial_expiry_text ? `
    试用过期: ${u.trial_expiry_text}
    ` : ''}
    `; }else{ usageDiv.innerHTML=`查询失败: ${d.error}`; } }catch(e){ usageDiv.innerHTML=`查询失败: ${e.message}`; } } async function refreshToken(id){ try{ Toast.info('正在刷新 Token...'); const r=await fetch('/api/accounts/'+id+'/refresh',{method:'POST'}); const d=await r.json(); if(d.ok) { Toast.success('Token 刷新成功'); } else { Toast.error('刷新失败: '+(d.message||d.error)); } loadAccounts(); loadAccountsEnhanced(); }catch(e){Toast.error('刷新失败: '+e.message)} } async function refreshAllTokens(){ try{ Toast.info('正在刷新所有 Token...'); const r=await fetch('/api/accounts/refresh-all',{method:'POST'}); const d=await r.json(); Toast.success(`刷新完成: ${d.refreshed} 个账号`); loadAccounts(); loadAccountsEnhanced(); }catch(e){Toast.error('刷新失败: '+e.message)} } async function restoreAccount(id){ try{ Toast.info('正在恢复账号...'); const r = await fetch('/api/accounts/'+id+'/restore',{method:'POST'}); const d = await r.json(); if(d.ok) { Toast.success('账号已恢复'); } else { Toast.error(d.error || '恢复失败'); } loadAccounts(); loadAccountsEnhanced(); loadQuota(); }catch(e){Toast.error('恢复失败: '+e.message)} } async function viewAccountDetail(id){ try{ const r=await fetch('/api/accounts/'+id); const d=await r.json(); Modal.info('账号详情', `

    账号名: ${d.name}

    ID: ${d.id}

    状态: ${d.status}

    请求数: ${d.request_count}

    错误数: ${d.error_count}

    `); }catch(e){Toast.error('获取详情失败: '+e.message)} } async function toggleAccount(id){ try { const r = await fetch('/api/accounts/'+id+'/toggle',{method:'POST'}); const d = await r.json(); if(d.ok) { Toast.success(d.enabled ? '账号已启用' : '账号已禁用'); } else { Toast.error(d.error || '操作失败'); } } catch(e) { Toast.error('操作失败: ' + e.message); } loadAccounts(); loadAccountsEnhanced(); } async function deleteAccount(id){ if(confirm('确定删除此账号?')){ try { const r = await fetch('/api/accounts/'+id,{method:'DELETE'}); const d = await r.json(); if(d.ok) { Toast.success('账号已删除'); } else { Toast.error(d.error || '删除失败'); } } catch(e) { Toast.error('删除失败: ' + e.message); } loadAccounts(); loadAccountsEnhanced(); } } function showAddAccount(){ const path=prompt('输入 Token 文件路径:'); if(path){ const name=prompt('账号名称:','账号'); fetch('/api/accounts',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name,token_path:path}) }).then(r=>r.json()).then(d=>{ if(d.ok){ Toast.success('账号添加成功'); loadAccounts(); loadAccountsEnhanced(); } else alert(d.detail||'添加失败'); }); } } async function scanTokens(){ try{ const r=await fetch('/api/token/scan'); const d=await r.json(); const panel=$('#scanResults'); const list=$('#scanList'); if(d.tokens&&d.tokens.length>0){ panel.style.display='block'; list.innerHTML=d.tokens.map(t=>{ const path=encodeURIComponent(t.path||''); const name=encodeURIComponent(t.name||''); return `
    ${t.name}
    ${t.path}
    ${t.already_added?'已添加':``}
    `; }).join(''); }else{ alert('未找到 Token 文件'); } }catch(e){alert('扫描失败: '+e.message)} } async function addFromScan(path,name){ try{ const r=await fetch('/api/token/add-from-scan',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path,name}) }); const d=await r.json(); if(d.ok){ loadAccounts(); scanTokens(); }else{ alert(d.detail||'添加失败'); } }catch(e){alert('添加失败: '+e.message)} } async function checkTokens(){ try{ const r=await fetch('/api/token/refresh-check',{method:'POST'}); const d=await r.json(); let msg='Token 状态:\\n\\n'; (d.accounts||[]).forEach(a=>{ const status=a.valid?'✅ 有效':'❌ 无效'; msg+=`${a.name}: ${status}\\n`; }); alert(msg); }catch(e){alert('检查失败: '+e.message)} } // 手动添加 Token function showManualAdd(){ $('#manualAddPanel').style.display='block'; $('#manualName').value=''; $('#manualAccessToken').value=''; $('#manualRefreshToken').value=''; } async function submitManualToken(){ const name=$('#manualName').value.trim(); const accessToken=$('#manualAccessToken').value.trim(); const refreshToken=$('#manualRefreshToken').value.trim(); const authMethod=$('#manualAuthMethod').value; const provider=$('#manualProvider')?.value || ''; const clientId=$('#manualClientId')?.value?.trim() || ''; const clientSecret=$('#manualClientSecret')?.value?.trim() || ''; const region=$('#manualRegion')?.value?.trim() || 'us-east-1'; // Refresh Token 必填 if (!refreshToken) { Toast.error('Refresh Token 是必填项'); return; } // 验证 Refresh Token 格式 if (refreshToken.length < 100) { Toast.error('Refresh Token 格式不正确(太短)'); return; } // IDC 认证需要 clientId 和 clientSecret if (authMethod === 'idc' && (!clientId || !clientSecret)) { Toast.error('IDC 认证需要填写 Client ID 和 Client Secret'); return; } Toast.info('正在添加账号...'); try{ const r=await fetchWithRetry('/api/accounts/manual',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ name: name || '', access_token: accessToken, refresh_token: refreshToken, auth_method: authMethod, provider: provider, client_id: clientId, client_secret: clientSecret, region: region }) }); const d=await r.json(); if(d.ok){ let msg = '添加成功'; if (d.auto_name) { msg += '(已自动获取邮箱作为名称)'; } Toast.success(msg); $('#manualAddPanel').style.display='none'; // 清空表单 $('#manualName').value = ''; $('#manualAccessToken').value = ''; $('#manualRefreshToken').value = ''; if ($('#manualClientId')) $('#manualClientId').value = ''; if ($('#manualClientSecret')) $('#manualClientSecret').value = ''; loadAccounts(); loadAccountsEnhanced(); }else{ Toast.error(d.detail||'添加失败'); } }catch(e){Toast.error('添加失败: '+e.message)} } // 切换手动添加表单字段显示 function toggleManualFields() { const authMethod = $('#manualAuthMethod').value; const idcFields = $('#manualIdcFields'); const providerField = $('#manualProviderField'); if (authMethod === 'idc') { if (idcFields) idcFields.style.display = 'block'; if (providerField) providerField.style.display = 'none'; } else { if (idcFields) idcFields.style.display = 'none'; if (providerField) providerField.style.display = 'block'; } } // 导出账号 async function exportAccounts(){ try{ const r=await fetch('/api/accounts/export'); const d=await r.json(); if(!d.ok){alert('导出失败');return;} const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='kiro-accounts-'+new Date().toISOString().slice(0,10)+'.json'; a.click(); }catch(e){alert('导出失败: '+e.message)} } // 导入账号 function importAccounts(){ const input=document.createElement('input'); input.type='file'; input.accept='.json'; input.onchange=async(e)=>{ const file=e.target.files[0]; if(!file)return; try{ const text=await file.text(); const data=JSON.parse(text); const r=await fetch('/api/accounts/import',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) }); const d=await r.json(); if(d.ok){ alert(`导入成功: ${d.imported} 个账号`+(d.errors?.length?`\\n错误: ${d.errors.join(', ')}`:'')); loadAccounts(); }else{ alert('导入失败'); } }catch(e){alert('导入失败: '+e.message)} }; input.click(); } ''' JS_LOGIN = ''' // Kiro 在线登录 let loginPollTimer=null; function showLoginOptions(){ $('#loginOptions').style.display='block'; } async function startSocialLogin(provider){ $('#loginOptions').style.display='none'; try{ const r=await fetch('/api/kiro/social/start',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({provider}) }); const d=await r.json(); if(!d.ok){alert('启动登录失败: '+d.error);return;} showSocialLoginPanel(d.provider, d.login_url); }catch(e){alert('启动登录失败: '+e.message)} } // 协议注册状态 let protocolRegistered = false; let callbackPollTimer = null; function showSocialLoginPanel(provider, loginUrl){ $('#loginPanel').style.display='block'; $('#loginContent').innerHTML=`

    ${provider} 登录

    步骤 1:打开登录链接

    步骤 2:完成授权后粘贴回调 URL

    授权完成后,浏览器会尝试打开 kiro:// 链接。
    如果提示"无法打开",请复制地址栏中的完整 URL 粘贴到下方。

    可选:自动回调模式

    `; } async function registerProtocolAndWait(provider) { $('#loginStatus').textContent = '正在注册协议处理器...'; $('#loginStatus').style.color = 'var(--muted)'; try { const regResp = await fetch('/api/protocol/register', { method: 'POST' }); const regData = await regResp.json(); if (!regData.ok) { $('#loginStatus').textContent = '协议注册失败: ' + regData.error; $('#loginStatus').style.color = 'var(--error)'; return; } protocolRegistered = true; $('#loginStatus').textContent = '✅ 协议已注册,授权完成后将自动接收回调'; $('#loginStatus').style.color = 'var(--success)'; // 开始轮询回调结果 startCallbackPolling(provider); } catch(e) { $('#loginStatus').textContent = '操作失败: ' + e.message; $('#loginStatus').style.color = 'var(--error)'; } } function startCallbackPolling(provider) { if (callbackPollTimer) clearInterval(callbackPollTimer); let pollCount = 0; const maxPolls = 300; // 5分钟超时 (300 * 1秒) callbackPollTimer = setInterval(async () => { pollCount++; if (pollCount > maxPolls) { clearInterval(callbackPollTimer); callbackPollTimer = null; $('#loginStatus').textContent = '等待超时,请重试'; $('#loginStatus').style.color = 'var(--error)'; return; } try { const resp = await fetch('/api/protocol/callback'); const data = await resp.json(); if (data.ok && data.result) { clearInterval(callbackPollTimer); callbackPollTimer = null; if (data.result.error) { $('#loginStatus').textContent = '授权失败: ' + data.result.error; $('#loginStatus').style.color = 'var(--error)'; } else if (data.result.code && data.result.state) { // 自动交换 Token $('#loginStatus').textContent = '正在交换 Token...'; await exchangeTokenWithCode(data.result.code, data.result.state); } } } catch(e) { console.error('轮询回调失败:', e); } }, 1000); } async function exchangeTokenWithCode(code, state) { try { const r = await fetch('/api/kiro/social/exchange', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ code, state }) }); const d = await r.json(); if (d.ok && d.completed) { $('#loginStatus').textContent = '✅ ' + d.message; $('#loginStatus').style.color = 'var(--success)'; setTimeout(() => { $('#loginPanel').style.display = 'none'; loadAccounts(); loadAccountsEnhanced(); }, 1500); } else { $('#loginStatus').textContent = '❌ ' + (d.error || '登录失败'); $('#loginStatus').style.color = 'var(--error)'; } } catch(e) { $('#loginStatus').textContent = '交换 Token 失败: ' + e.message; $('#loginStatus').style.color = 'var(--error)'; } } function cancelSocialLogin(){ if (callbackPollTimer) { clearInterval(callbackPollTimer); callbackPollTimer = null; } fetch('/api/kiro/social/cancel',{method:'POST'}); $('#loginPanel').style.display='none'; } async function handleSocialCallback(){ const url=$('#callbackUrl').value.trim(); if(!url){alert('请粘贴回调 URL');return;} try{ // 支持 kiro:// 协议的 URL 解析 let code, state; if(url.startsWith('kiro://')){ // kiro://kiro.kiroAgent/authenticate-success?code=xxx&state=xxx const queryStart = url.indexOf('?'); if(queryStart > -1){ const params = new URLSearchParams(url.substring(queryStart + 1)); code = params.get('code'); state = params.get('state'); } } else { // 标准 http/https URL const urlObj=new URL(url); code=urlObj.searchParams.get('code'); state=urlObj.searchParams.get('state'); } if(!code||!state){alert('无效的回调 URL,缺少 code 或 state 参数');return;} $('#loginStatus').textContent='正在交换 Token...'; const r=await fetch('/api/kiro/social/exchange',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({code,state}) }); const d=await r.json(); if(d.ok&&d.completed){ $('#loginStatus').textContent='✅ '+d.message; $('#loginStatus').style.color='var(--success)'; setTimeout(()=>{$('#loginPanel').style.display='none';loadAccounts();},1500); }else{ $('#loginStatus').textContent='❌ '+(d.error||'登录失败'); $('#loginStatus').style.color='var(--error)'; } }catch(e){alert('处理回调失败: '+e.message)} } async function startAwsLogin(){ $('#loginOptions').style.display='none'; try{ const r=await fetch('/api/kiro/login/start',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({}) }); const d=await r.json(); if(!d.ok){alert('启动登录失败: '+d.error);return;} showAwsLoginPanel(d); startLoginPoll(); }catch(e){alert('启动登录失败: '+e.message)} } function showAwsLoginPanel(data){ $('#loginPanel').style.display='block'; $('#loginContent').innerHTML=`

    AWS Builder ID 登录

    ${data.user_code}

    复制上方授权码,然后打开以下链接完成授权:

    授权码有效期: ${Math.floor(data.expires_in/60)} 分钟

    等待授权...

    `; } function startLoginPoll(){ if(loginPollTimer)clearInterval(loginPollTimer); loginPollTimer=setInterval(pollLogin,3000); } async function pollLogin(){ try{ const r=await fetch('/api/kiro/login/poll'); const d=await r.json(); if(!d.ok){$('#loginStatus').textContent='错误: '+d.error;stopLoginPoll();return;} if(d.completed){ $('#loginStatus').textContent='✅ 登录成功!'; $('#loginStatus').style.color='var(--success)'; stopLoginPoll(); setTimeout(()=>{$('#loginPanel').style.display='none';loadAccounts();},1500); } }catch(e){$('#loginStatus').textContent='轮询失败: '+e.message} } function stopLoginPoll(){ if(loginPollTimer){clearInterval(loginPollTimer);loginPollTimer=null;} } async function cancelKiroLogin(){ stopLoginPoll(); await fetch('/api/kiro/login/cancel',{method:'POST'}); $('#loginPanel').style.display='none'; } ''' JS_FLOWS = ''' // Flow Monitor async function loadFlowStats(){ try{ const r=await fetch('/api/flows/stats'); const d=await r.json(); $('#flowStatsGrid').innerHTML=`
    ${d.total_flows}
    总请求
    ${d.completed}
    完成
    ${d.errors}
    错误
    ${d.error_rate}
    错误率
    ${d.avg_duration_ms.toFixed(0)}ms
    平均延迟
    ${d.total_tokens_in}
    输入Token
    ${d.total_tokens_out}
    输出Token
    `; }catch(e){console.error(e)} } async function loadFlows(){ try{ const protocol=$('#flowProtocol').value; const state=$('#flowState').value; const search=$('#flowSearch').value; let url='/api/flows?limit=50'; if(protocol)url+=`&protocol=${protocol}`; if(state)url+=`&state=${state}`; if(search)url+=`&search=${encodeURIComponent(search)}`; const r=await fetch(url); const d=await r.json(); if(!d.flows||d.flows.length===0){ $('#flowList').innerHTML='

    暂无请求记录

    '; return; } $('#flowList').innerHTML=d.flows.map(f=>{ const stateBadge={completed:'success',error:'error',streaming:'info',pending:'warn'}[f.state]||'info'; const stateText={completed:'完成',error:'错误',streaming:'流式中',pending:'等待中'}[f.state]||f.state; const time=new Date(f.timing.created_at*1000).toLocaleTimeString(); const duration=f.timing.duration_ms?f.timing.duration_ms.toFixed(0)+'ms':'-'; const model=f.request?.model||'-'; const tokens=f.response?.usage?(f.response.usage.input_tokens+'/'+f.response.usage.output_tokens):'-'; return `
    ${stateText} ${model} ${f.bookmarked?'':''}
    ${time} · ${duration} · ${tokens} tokens · ${f.protocol}
    `; }).join(''); }catch(e){console.error(e)} } async function viewFlow(id){ try{ const r=await fetch('/api/flows/'+id); const f=await r.json(); let html=`
    ID: ${f.id}
    协议: ${f.protocol}
    状态: ${f.state}
    时间: ${new Date(f.timing.created_at*1000).toLocaleString()}
    延迟: ${f.timing.duration_ms?f.timing.duration_ms.toFixed(0)+'ms':'N/A'}
    `; if(f.request){ html+=`

    请求

    模型: ${f.request.model}
    流式: ${f.request.stream?'是':'否'}
    `; } if(f.response){ html+=`

    响应

    状态码: ${f.response.status_code}
    Token: ${f.response.usage?.input_tokens||0} in / ${f.response.usage?.output_tokens||0} out
    `; } if(f.error){ html+=`

    错误

    类型: ${f.error.type}
    消息: ${f.error.message}
    `; } $('#flowDetailContent').innerHTML=html; $('#flowDetail').style.display='block'; }catch(e){alert('获取详情失败: '+e.message)} } async function toggleBookmark(id,bookmarked){ await fetch('/api/flows/'+id+'/bookmark',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({bookmarked})}); loadFlows(); } async function exportFlows(){ try{ const r=await fetch('/api/flows/export',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({format:'json'})}); const d=await r.json(); const blob=new Blob([d.content],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='flows_'+new Date().toISOString().slice(0,10)+'.json'; a.click(); }catch(e){alert('导出失败: '+e.message)} } ''' JS_SETTINGS = ''' // 设置页面 // 历史消息管理 async function loadHistoryConfig(){ try{ const r=await fetch('/api/settings/history'); const d=await r.json(); const strategies=Array.isArray(d.strategies)?d.strategies:[]; $('#historyErrorRetryEnabled').checked=strategies.includes('error_retry'); $('#maxRetries').value=d.max_retries||3; $('#summaryCacheMaxAge').value=d.summary_cache_max_age_seconds||300; $('#addWarningHeader').checked=d.add_warning_header!==false; }catch(e){console.error('加载配置失败:',e)} } async function updateHistoryConfig(){ const strategies=[]; if($('#historyErrorRetryEnabled').checked) strategies.push('error_retry'); const config={ strategies, max_retries:parseInt($('#maxRetries').value)||3, summary_cache_enabled:true, summary_cache_max_age_seconds:parseInt($('#summaryCacheMaxAge').value)||300, add_warning_header:$('#addWarningHeader').checked }; try{ await fetch('/api/settings/history',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(config)}); }catch(e){console.error('保存配置失败:',e)} } // 刷新配置 async function loadRefreshConfig(){ try{ const r=await fetch('/api/refresh/config'); const d=await r.json(); if(d.ok && d.config){ const c=d.config; $('#refreshMaxRetries').value=c.max_retries||3; $('#refreshConcurrency').value=c.concurrency||3; $('#refreshAutoInterval').value=c.auto_refresh_interval||60; $('#refreshRetryDelay').value=c.retry_base_delay||1.0; $('#refreshBeforeExpiry').value=c.token_refresh_before_expiry||300; // 更新状态显示 $('#refreshConfigStatus').innerHTML=`
    最大重试: ${c.max_retries||3} 并发数: ${c.concurrency||3} 自动刷新间隔: ${c.auto_refresh_interval||60} 提前刷新: ${c.token_refresh_before_expiry||300}
    `; } }catch(e){console.error('加载刷新配置失败:',e)} } async function saveRefreshConfig(){ const config={ max_retries:parseInt($('#refreshMaxRetries').value)||3, concurrency:parseInt($('#refreshConcurrency').value)||3, auto_refresh_interval:parseInt($('#refreshAutoInterval').value)||60, retry_base_delay:parseFloat($('#refreshRetryDelay').value)||1.0, token_refresh_before_expiry:parseInt($('#refreshBeforeExpiry').value)||300 }; try{ const r=await fetch('/api/refresh/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(config)}); const d=await r.json(); if(d.ok){ Toast.success('刷新配置保存成功'); loadRefreshConfig(); }else{ Toast.error(d.error||'保存失败'); } }catch(e){ console.error('保存刷新配置失败:',e); Toast.error('保存刷新配置失败'); } } // 限速配置 async function loadRateLimitConfig(){ try{ const r=await fetch('/api/settings/rate-limit'); const d=await r.json(); $('#rateLimitEnabled').checked=d.enabled; $('#minRequestInterval').value=d.min_request_interval||0.5; $('#maxRequestsPerMinute').value=d.max_requests_per_minute||60; $('#globalMaxRequestsPerMinute').value=d.global_max_requests_per_minute||120; // 更新统计 const stats=d.stats||{}; $('#rateLimitStats').innerHTML=`
    状态: ${d.enabled?'已启用':'已禁用'} 全局 RPM: ${stats.global_rpm||0} 429 冷却: 自动 5 分钟
    `; }catch(e){console.error('加载限速配置失败:',e)} } async function updateRateLimitConfig(){ const config={ enabled:$('#rateLimitEnabled').checked, min_request_interval:parseFloat($('#minRequestInterval').value)||0.5, max_requests_per_minute:parseInt($('#maxRequestsPerMinute').value)||60, global_max_requests_per_minute:parseInt($('#globalMaxRequestsPerMinute').value)||120 }; try{ await fetch('/api/settings/rate-limit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(config)}); loadRateLimitConfig(); }catch(e){console.error('保存限速配置失败:',e)} } // 还原默认配置函数 async function resetRefreshConfig(){ if(!confirm('确定要还原刷新配置为默认值吗?')) return; const defaultConfig={ max_retries:3, concurrency:3, auto_refresh_interval:60, retry_base_delay:1.0, token_refresh_before_expiry:300 }; try{ const r=await fetch('/api/refresh/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(defaultConfig)}); const d=await r.json(); if(d.ok){ Toast.success('已还原为默认配置'); loadRefreshConfig(); }else{ Toast.error(d.error||'还原失败'); } }catch(e){ Toast.error('还原配置失败'); } } async function resetRateLimitConfig(){ if(!confirm('确定要还原限速配置为默认值吗?')) return; const defaultConfig={ enabled:false, min_request_interval:0.5, max_requests_per_minute:60, global_max_requests_per_minute:120 }; try{ await fetch('/api/settings/rate-limit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(defaultConfig)}); Toast.success('已还原为默认配置'); loadRateLimitConfig(); }catch(e){ Toast.error('还原配置失败'); } } async function resetHistoryConfig(){ if(!confirm('确定要还原历史消息配置为默认值吗?')) return; const defaultConfig={ strategies:['error_retry'], max_retries:3, summary_cache_enabled:true, summary_cache_max_age_seconds:300, add_warning_header:true }; try{ await fetch('/api/settings/history',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(defaultConfig)}); Toast.success('已还原为默认配置'); loadHistoryConfig(); }catch(e){ Toast.error('还原配置失败'); } } // 页面加载时加载设置 loadHistoryConfig(); loadRateLimitConfig(); loadRefreshConfig(); ''' # ==================== UI 组件库 JavaScript ==================== JS_UI_COMPONENTS = ''' // ==================== Modal 模态框组件 ==================== class Modal { constructor(options = {}) { this.title = options.title || ''; this.content = options.content || ''; this.type = options.type || 'default'; this.confirmText = options.confirmText || '确认'; this.cancelText = options.cancelText || '取消'; this.onConfirm = options.onConfirm; this.onCancel = options.onCancel; this.showCancel = options.showCancel !== false; this.element = null; } show() { const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` `; overlay.modal = this; this.element = overlay; document.body.appendChild(overlay); // 键盘事件 this.keyHandler = (e) => { if (e.key === 'Escape') this.hide(); if (e.key === 'Enter' && !e.target.matches('textarea')) this.confirm(); }; document.addEventListener('keydown', this.keyHandler); // 点击遮罩关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay) this.hide(); }); requestAnimationFrame(() => overlay.classList.add('active')); return this; } hide() { if (this.element) { this.element.classList.remove('active'); document.removeEventListener('keydown', this.keyHandler); setTimeout(() => this.element.remove(), 200); } } confirm() { if (this.onConfirm) this.onConfirm(); this.hide(); } cancel() { if (this.onCancel) this.onCancel(); this.hide(); } setLoading(loading) { const btn = this.element?.querySelector('.modal-footer button:last-child'); if (btn) { btn.disabled = loading; btn.textContent = loading ? '处理中...' : this.confirmText; } } static confirm(title, message, onConfirm) { return new Modal({ title, content: `

    ${message}

    `, onConfirm }).show(); } static alert(title, message) { return new Modal({ title, content: `

    ${message}

    `, showCancel: false }).show(); } static danger(title, message, onConfirm) { return new Modal({ title, content: `

    ${message}

    `, type: 'danger', onConfirm, confirmText: '删除' }).show(); } } // ==================== Toast 通知组件 ==================== class Toast { static container = null; static getContainer() { if (!this.container) { this.container = document.createElement('div'); this.container.className = 'toast-container'; document.body.appendChild(this.container); } return this.container; } static show(message, type = 'info', duration = 3000) { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${message} `; this.getContainer().appendChild(toast); if (duration > 0) { setTimeout(() => toast.remove(), duration); } return toast; } static success(message, duration) { return this.show(message, 'success', duration); } static error(message, duration) { return this.show(message, 'error', duration); } static warning(message, duration) { return this.show(message, 'warning', duration); } static info(message, duration) { return this.show(message, 'info', duration); } } // ==================== Dropdown 下拉菜单组件 ==================== class Dropdown { constructor(trigger, items) { this.trigger = trigger; this.items = items; this.element = null; this.init(); } init() { const wrapper = document.createElement('div'); wrapper.className = 'dropdown'; this.trigger.parentNode.insertBefore(wrapper, this.trigger); wrapper.appendChild(this.trigger); const menu = document.createElement('div'); menu.className = 'dropdown-menu'; menu.innerHTML = this.items.map(item => { if (item.divider) return ''; return ``; }).join(''); wrapper.appendChild(menu); this.element = wrapper; this.trigger.addEventListener('click', (e) => { e.stopPropagation(); this.toggle(); }); menu.addEventListener('click', (e) => { const item = e.target.closest('.dropdown-item'); if (item) { const action = item.dataset.action; const itemConfig = this.items.find(i => i.action === action); if (itemConfig?.onClick) itemConfig.onClick(); this.close(); } }); document.addEventListener('click', () => this.close()); } toggle() { this.element.classList.toggle('open'); } close() { this.element.classList.remove('open'); } } // ==================== 进度条渲染函数 ==================== function renderProgressBar(value, max, options = {}) { const percent = max > 0 ? (value / max * 100) : 0; const color = options.color || (percent > 80 ? 'error' : percent > 60 ? 'warning' : 'success'); const size = options.size || ''; const showLabel = options.showLabel !== false; return `
    ${showLabel ? `
    ${options.leftLabel || ''}${options.rightLabel || Math.round(percent) + '%'}
    ` : ''} `; } // ==================== 账号卡片渲染函数 ==================== function renderAccountCard(account) { const quota = account.quota; const isPriority = account.is_priority; const isActive = account.is_active; let statusBadge = ''; if (!account.enabled) statusBadge = '禁用'; else if (account.cooldown_remaining > 0) statusBadge = `冷却 ${account.cooldown_remaining}s`; else if (account.available) statusBadge = '正常'; else statusBadge = '不可用'; let quotaSection = ''; if (quota && !quota.error) { const usedPercent = quota.usage_limit > 0 ? (quota.current_usage / quota.usage_limit * 100) : 0; quotaSection = `
    已用/总额 ${quota.current_usage.toFixed(1)} / ${quota.usage_limit.toFixed(1)}
    ${renderProgressBar(quota.current_usage, quota.usage_limit, { color: quota.is_low_balance ? 'error' : usedPercent > 60 ? 'warning' : 'success', rightLabel: usedPercent.toFixed(1) + '%' })}
    ${quota.free_trial_limit > 0 ? `试用: ${quota.free_trial_usage.toFixed(0)}/${quota.free_trial_limit.toFixed(0)}` : ''} ${quota.bonus_limit > 0 ? `奖励: ${quota.bonus_usage.toFixed(0)}/${quota.bonus_limit.toFixed(0)} (${quota.active_bonuses || 0}个)` : ''} 更新: ${quota.updated_at || '未知'}
    ${quota.reset_date_text || quota.free_trial_expiry ? `
    ${quota.reset_date_text ? `🔄 重置: ${quota.reset_date_text}` : ''} ${quota.free_trial_expiry ? `🎁 试用过期: ${quota.trial_expiry_text}` : ''}
    ` : ''}
    `; } else if (quota?.error) { quotaSection = `
    额度获取失败: ${quota.error}
    `; } return `
    ${quotaSection}
    `; } // ==================== 汇总面板渲染函数 ==================== function renderSummaryPanel(summary) { const strategyLabel = { lowest_balance: '剩余额度最少优先', round_robin: '轮询', least_requests: '请求最少优先', random: '随机' }[summary.strategy] || summary.strategy; return `
    ${summary.total_accounts}
    总账号
    ${summary.available_accounts}
    可用
    ${summary.cooldown_accounts}
    冷却中
    ${summary.unhealthy_accounts + summary.disabled_accounts}
    不可用
    总剩余额度 ${summary.total_balance.toFixed(1)}
    ${renderProgressBar(summary.total_usage, summary.total_limit, { size: 'large', leftLabel: `已用 ${summary.total_usage.toFixed(0)}`, rightLabel: `总计 ${summary.total_limit.toFixed(0)}` })}
    选择策略: ${strategyLabel} 优先账号: ${summary.priority_accounts.length > 0 ? summary.priority_accounts.join(', ') : '无'} 最后刷新: ${summary.last_refresh || '未刷新'}
    `; } // ==================== 账号操作菜单 ==================== let currentAccountMenu = null; function showAccountMenu(accountId, btn) { if (currentAccountMenu) { currentAccountMenu.remove(); currentAccountMenu = null; } const menu = document.createElement('div'); menu.className = 'dropdown-menu'; menu.style.cssText = 'display:block;position:absolute;z-index:100;'; menu.innerHTML = ` `; const rect = btn.getBoundingClientRect(); menu.style.top = (rect.bottom + window.scrollY) + 'px'; menu.style.left = (rect.left + window.scrollX - 100) + 'px'; document.body.appendChild(menu); currentAccountMenu = menu; setTimeout(() => { document.addEventListener('click', function closeMenu() { if (currentAccountMenu) { currentAccountMenu.remove(); currentAccountMenu = null; } document.removeEventListener('click', closeMenu); }, { once: true }); }, 0); } // ==================== 额度管理 API 调用 ==================== async function loadAccountsEnhanced() { showLoading('#accountsGrid', '加载账号列表...'); try { const r = await fetchWithRetry('/api/accounts/status'); const d = await r.json(); if (d.ok) { $('#accountsSummaryCompact').innerHTML = renderSummaryCompact(d.summary); $('#accountsGrid').innerHTML = d.accounts.map(renderAccountCardCompact).join(''); } else { $('#accountsGrid').innerHTML = `

    加载失败: ${d.error || '未知错误'}

    `; } } catch(e) { $('#accountsGrid').innerHTML = `

    网络错误,点击重试

    `; Toast.error('加载账号列表失败'); } } // ==================== 紧凑汇总面板 ==================== function renderSummaryCompact(summary) { const usedPercent = summary.total_limit > 0 ? (summary.total_usage / summary.total_limit * 100) : 0; const barColor = usedPercent > 80 ? 'var(--error)' : usedPercent > 60 ? 'var(--warn)' : 'var(--success)'; return `
    ${summary.total_accounts} 总账号
    ${summary.available_accounts} 可用
    ${summary.cooldown_accounts} 冷却
    总额度 ${summary.total_balance.toFixed(0)} / ${summary.total_limit.toFixed(0)}
    ${summary.last_refresh || '未刷新'}
    `; } // ==================== 紧凑账号卡片 ==================== function renderAccountCardCompact(account) { const quota = account.quota; const isPriority = account.is_priority; const isLowBalance = quota?.is_low_balance; const isExhausted = quota?.is_exhausted || (quota && quota.balance <= 0); // 额度耗尽 const isSuspended = quota?.is_suspended; // 账号被封禁 const isUnavailable = !account.available; let cardClass = 'account-card-compact'; if (isPriority) cardClass += ' priority'; if (isSuspended) cardClass += ' suspended'; // 封禁状态 else if (isExhausted) cardClass += ' exhausted'; // 无额度状态 else if (isLowBalance) cardClass += ' low-balance'; if (isUnavailable) cardClass += ' unavailable'; // 状态徽章 let statusBadges = ''; if (!account.enabled) statusBadges += '禁用'; else if (account.cooldown_remaining > 0) statusBadges += `冷却`; else if (account.available) statusBadges += '正常'; else statusBadges += '异常'; if (isPriority) statusBadges += `#${account.priority_order}`; // Provider 徽章 (Google/Github) if (account.provider) { const providerIcon = account.provider === 'Google' ? '🔵' : account.provider === 'Github' ? '⚫' : ''; statusBadges += `${providerIcon}${account.provider}`; } // 状态徽章:封禁(红色)> 无额度(红色)> 低额度(黄色) if (isSuspended) statusBadges += '已封禁'; else if (isExhausted) statusBadges += '无额度'; else if (isLowBalance) statusBadges += '低额度'; // Token 过期状态徽章 if (account.token_expired) statusBadges += 'Token过期'; else if (account.token_expiring_soon) statusBadges += 'Token即将过期'; // 额度条 - 根据状态显示不同颜色 let quotaBar = ''; if (quota && !quota.error) { const usedPercent = quota.usage_limit > 0 ? (quota.current_usage / quota.usage_limit * 100) : 0; // 颜色逻辑:无额度(红色) > 低额度(黄色) > 正常(绿色) let barColor = 'var(--success)'; if (isExhausted) barColor = 'var(--error)'; else if (isLowBalance) barColor = 'var(--warn)'; else if (usedPercent > 60) barColor = 'var(--warn)'; quotaBar = `
    ${quota.current_usage.toFixed(1)} / ${quota.usage_limit.toFixed(1)} ${usedPercent.toFixed(0)}%
    ${quota.reset_date_text || quota.trial_expiry_text ? `
    ${quota.reset_date_text ? `🔄 ${quota.reset_date_text}` : ''} ${quota.trial_expiry_text ? `🎁 ${quota.trial_expiry_text}` : ''}
    ` : ''}
    `; } else if (quota?.error) { // 额度获取失败时显示重试按钮 // 如果是封禁错误,显示封禁状态 const errorMsg = quota.error; const isSuspendedError = errorMsg && ( errorMsg.toLowerCase().includes('temporarily_suspended') || errorMsg.toLowerCase().includes('suspended') || errorMsg.toLowerCase().includes('accountsuspendedexception') ); if (isSuspendedError) { quotaBar = `
    账号已封禁
    `; } else { quotaBar = `
    额度获取失败: ${quota.error}
    `; } } else { // 未查询额度时显示查询按钮 quotaBar = `
    额度未查询
    `; } // Token 过期时间显示 let tokenExpireInfo = ''; if (account.token_expires_at) { // expires_at 可能是 ISO 字符串或时间戳 let expireDate; if (typeof account.token_expires_at === 'string') { // ISO 格式字符串 expireDate = new Date(account.token_expires_at); } else if (account.token_expires_at > 1000000000000) { // 毫秒时间戳 expireDate = new Date(account.token_expires_at); } else { // 秒时间戳 expireDate = new Date(account.token_expires_at * 1000); } const now = new Date(); const diffMs = expireDate - now; // 检查是否为有效日期 if (!isNaN(expireDate.getTime()) && !isNaN(diffMs)) { const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffHours / 24); let expireText = ''; if (diffMs < 0) { expireText = '已过期'; } else if (diffDays > 0) { expireText = `${diffDays}天`; } else if (diffHours > 0) { expireText = `${diffHours}时`; } else { const diffMins = Math.floor(diffMs / (1000 * 60)); expireText = diffMins > 0 ? `${diffMins}分` : '即将过期'; } tokenExpireInfo = `Token ${expireText}`; } } return `
    ${quotaBar}
    请求: ${account.request_count} 错误: ${account.error_count} ${tokenExpireInfo}
    `; } // ==================== 导入导出菜单 ==================== let importExportMenu = null; function showImportExportMenu(btn) { if (importExportMenu) { importExportMenu.remove(); importExportMenu = null; return; } const menu = document.createElement('div'); menu.className = 'dropdown-menu'; menu.style.cssText = 'display:block;position:absolute;z-index:100;min-width:140px;'; menu.innerHTML = ` `; const rect = btn.getBoundingClientRect(); menu.style.top = (rect.bottom + window.scrollY + 4) + 'px'; menu.style.left = (rect.left + window.scrollX) + 'px'; document.body.appendChild(menu); importExportMenu = menu; setTimeout(() => { document.addEventListener('click', function closeMenu(e) { if (importExportMenu && !importExportMenu.contains(e.target)) { importExportMenu.remove(); importExportMenu = null; } document.removeEventListener('click', closeMenu); }, { once: true }); }, 10); } async function refreshAllQuotas() { // 检查是否正在刷新中 if (GlobalProgressBar.isRefreshing) { Toast.warning('正在刷新中,请稍候...'); return; } try { // 先获取账号数量用于显示 const statusR = await fetch('/api/accounts/status'); const statusD = await statusR.json(); const total = statusD.ok ? statusD.accounts?.length || 0 : 0; // 显示进度条 GlobalProgressBar.show(total); // 调用新的批量刷新 API const r = await fetch('/api/refresh/all', { method: 'POST' }); const d = await r.json(); if (d.ok) { // 开始轮询进度 GlobalProgressBar.startPolling(); } else { GlobalProgressBar.hide(); Toast.error('启动刷新失败: ' + (d.error || '未知错误')); } } catch(e) { GlobalProgressBar.hide(); Toast.error('刷新失败: ' + e.message); } } async function refreshAccountQuota(accountId) { Toast.info('正在刷新额度...'); try { const r = await fetch(`/api/accounts/${accountId}/refresh-quota`, { method: 'POST' }); const d = await r.json(); if (d.ok) { Toast.success('额度刷新成功'); loadAccounts(); loadAccountsEnhanced(); } else { Toast.error(d.error || '刷新失败'); } } catch(e) { Toast.error('刷新失败: ' + e.message); } } // ==================== 测试账号 Token ==================== async function testAccountToken(accountId) { // 显示测试中的模态框 const modal = document.createElement('div'); modal.className = 'modal'; modal.id = 'testTokenModal'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.style.display = 'flex'; try { const r = await fetch('/api/accounts/' + accountId + '/test'); const d = await r.json(); const resultDiv = document.getElementById('testTokenResult'); if (!resultDiv) return; if (d.ok) { // 测试通过 let testsHtml = ''; for (const [key, test] of Object.entries(d.tests || {})) { const icon = test.passed ? '✅' : '❌'; const color = test.passed ? 'var(--success)' : 'var(--error)'; testsHtml += `
    ${icon}
    ${test.message}
    ${test.suggestion ? `
    ${test.suggestion}
    ` : ''} ${test.latency_ms ? `
    延迟: ${test.latency_ms.toFixed(0)}ms
    ` : ''} ${test.email ? `
    邮箱: ${test.email}
    ` : ''}
    `; } resultDiv.innerHTML = `

    Token 有效

    ${d.summary}

    ${testsHtml}
    `; } else { // 测试失败 let testsHtml = ''; for (const [key, test] of Object.entries(d.tests || {})) { const icon = test.passed ? '✅' : '❌'; testsHtml += `
    ${icon}
    ${test.message}
    ${test.suggestion ? `
    💡 ${test.suggestion}
    ` : ''}
    `; } resultDiv.innerHTML = `

    Token 无效

    ${d.summary || d.error || '测试失败'}

    ${Object.keys(d.tests || {}).length > 0 ? `
    ${testsHtml}
    ` : ''}
    `; } } catch(e) { const resultDiv = document.getElementById('testTokenResult'); if (resultDiv) { resultDiv.innerHTML = `
    ⚠️

    测试失败

    ${e.message}

    `; } } } function closeTestTokenModal() { const modal = document.getElementById('testTokenModal'); if (modal) modal.remove(); } // ==================== 单账号额度查询 (任务 19.2) ==================== async function refreshSingleAccountQuota(accountId) { // 获取按钮元素,显示加载状态 const safeId = accountId.replace(/[^a-zA-Z0-9]/g, '_'); const btn = document.getElementById('quota-btn-' + safeId); const card = document.getElementById('account-card-' + safeId); if (btn) { btn.disabled = true; btn.dataset.originalText = btn.textContent; btn.textContent = '查询中...'; } try { const r = await fetch(`/api/accounts/${accountId}/refresh-quota`, { method: 'POST' }); const d = await r.json(); if (d.ok) { Toast.success('额度查询成功'); // 刷新整个账号列表以更新显示 loadAccounts(); loadAccountsEnhanced(); } else { // 失败时显示错误信息和重试按钮 Toast.error(d.error || '额度查询失败'); if (btn) { btn.textContent = '重试'; btn.disabled = false; btn.classList.add('error-state'); } // 在卡片上显示错误状态 if (card) { const quotaDiv = card.querySelector('.account-card-quota'); if (quotaDiv) { quotaDiv.innerHTML = ` 查询失败: ${d.error || '未知错误'} `; } } } } catch(e) { Toast.error('网络错误: ' + e.message); if (btn) { btn.textContent = '重试'; btn.disabled = false; } } finally { // 恢复按钮状态(如果没有错误) if (btn && !btn.classList.contains('error-state')) { btn.disabled = false; if (btn.dataset.originalText) { btn.textContent = btn.dataset.originalText; } } if (btn) { btn.classList.remove('error-state'); } } } // ==================== 单账号 Token 刷新 (任务 19.2) ==================== async function refreshSingleAccountToken(accountId) { // 获取按钮元素,显示加载状态 const safeId = accountId.replace(/[^a-zA-Z0-9]/g, '_'); const btn = document.getElementById('token-btn-' + safeId); if (btn) { btn.disabled = true; btn.dataset.originalText = btn.textContent; btn.textContent = '刷新中...'; } try { const r = await fetch(`/api/accounts/${accountId}/refresh`, { method: 'POST' }); const d = await r.json(); if (d.ok) { Toast.success('Token 刷新成功'); // 刷新整个账号列表以更新显示 loadAccounts(); loadAccountsEnhanced(); } else { // 失败时显示错误信息 Toast.error(d.message || d.error || 'Token 刷新失败'); if (btn) { btn.textContent = '重试'; btn.disabled = false; } } } catch(e) { Toast.error('网络错误: ' + e.message); if (btn) { btn.textContent = '重试'; btn.disabled = false; } } finally { // 恢复按钮状态 if (btn && btn.textContent !== '重试') { btn.disabled = false; if (btn.dataset.originalText) { btn.textContent = btn.dataset.originalText; } } } } async function togglePriority(accountId) { try { // 先检查是否已是优先账号 const r1 = await fetch('/api/priority'); const d1 = await r1.json(); const isPriority = d1.priority_accounts?.some(a => a.id === accountId); if (isPriority) { const r = await fetch(`/api/priority/${accountId}`, { method: 'DELETE' }); const d = await r.json(); Toast.show(d.message, d.ok ? 'success' : 'error'); } else { const r = await fetch(`/api/priority/${accountId}`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}' }); const d = await r.json(); Toast.show(d.message, d.ok ? 'success' : 'error'); } loadAccounts(); loadAccountsEnhanced(); } catch(e) { Toast.error('操作失败: ' + e.message); } } function confirmDeleteAccount(accountId) { Modal.danger('删除账号', `确定要删除账号 ${accountId} 吗?此操作不可恢复。`, async () => { try { const r = await fetch(`/api/accounts/${accountId}`, { method: 'DELETE' }); const d = await r.json(); if (d.ok) { Toast.success('账号已删除'); loadAccounts(); loadAccountsEnhanced(); } else { Toast.error('删除失败'); } } catch(e) { Toast.error('删除失败: ' + e.message); } }); } // ==================== 账号编辑功能 ==================== function showEditAccountModal(accountId, currentName) { const modal = new Modal({ title: '编辑账号', content: `
    `, confirmText: '保存', onConfirm: async () => { const name = document.getElementById('editAccountName').value.trim(); const provider = document.getElementById('editAccountProvider').value; const region = document.getElementById('editAccountRegion').value.trim(); const updateData = {}; if (name) updateData.name = name; if (provider) updateData.provider = provider; if (region) updateData.region = region; try { const r = await fetch(`/api/accounts/${accountId}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(updateData) }); const d = await r.json(); if (d.ok) { Toast.success(d.message || '账号已更新'); loadAccounts(); loadAccountsEnhanced(); } else { Toast.error(d.error || '更新失败'); } } catch(e) { Toast.error('更新失败: ' + e.message); } } }); modal.show(); // 加载当前账号信息填充表单 loadAccountForEdit(accountId); } async function refreshTokenInModal(accountId) { const btn = document.getElementById('refreshTokenBtn'); if (btn) { btn.disabled = true; btn.textContent = '刷新中...'; } try { const r = await fetch(`/api/accounts/${accountId}/refresh`, { method: 'POST' }); const d = await r.json(); if (d.ok) { Toast.success('Token 刷新成功'); // 重新加载账号信息 await loadAccountForEdit(accountId); loadAccounts(); loadAccountsEnhanced(); } else { Toast.error(d.message || d.error || 'Token 刷新失败'); } } catch(e) { Toast.error('刷新失败: ' + e.message); } finally { if (btn) { btn.disabled = false; btn.textContent = '🔄 刷新 Token'; } } } function copyToClipboard(text, label) { navigator.clipboard.writeText(text).then(() => { Toast.success(label + ' 已复制'); }).catch(() => { // 降级方案 const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); Toast.success(label + ' 已复制'); }); } function renderTokenField(label, value, fieldId) { if (!value) return ''; const shortValue = value.length > 50 ? value.substring(0, 50) + '...' : value; return `
    ${label}:
    ${shortValue}
    `; } async function loadAccountForEdit(accountId) { try { const r = await fetch(`/api/accounts/${accountId}`); const d = await r.json(); const providerSelect = document.getElementById('editAccountProvider'); const regionInput = document.getElementById('editAccountRegion'); const tokenSection = document.getElementById('tokenInfoSection'); const tokenDetails = document.getElementById('tokenDetails'); if (d.credentials) { if (providerSelect && d.credentials.provider) { providerSelect.value = d.credentials.provider; } if (regionInput && d.credentials.region) { regionInput.value = d.credentials.region; } // 显示 Token 信息 if (tokenSection && tokenDetails) { tokenSection.style.display = 'block'; let html = ''; // Access Token if (d.credentials.access_token) { html += renderTokenField('Access Token', d.credentials.access_token, 'field_access_token'); } // Refresh Token if (d.credentials.refresh_token) { html += renderTokenField('Refresh Token', d.credentials.refresh_token, 'field_refresh_token'); } // Profile ARN if (d.credentials.profile_arn) { html += renderTokenField('Profile ARN', d.credentials.profile_arn, 'field_profile_arn'); } // Client ID if (d.credentials.client_id) { html += renderTokenField('Client ID', d.credentials.client_id, 'field_client_id'); } // 过期时间 if (d.credentials.expires_at) { const expiresAt = new Date(d.credentials.expires_at); const now = new Date(); const diffMs = expiresAt - now; const diffMins = Math.floor(diffMs / 60000); let expiryText = expiresAt.toLocaleString(); if (diffMs < 0) { expiryText += ' (已过期)'; } else if (diffMins < 60) { expiryText += ' (' + diffMins + '分钟后过期)'; } else { expiryText += ' (' + Math.floor(diffMins/60) + '小时后过期)'; } html += '
    过期时间: ' + expiryText + '
    '; } // Auth Method if (d.credentials.auth_method) { html += '
    认证方式: ' + d.credentials.auth_method + '
    '; } tokenDetails.innerHTML = html || '无 Token 信息'; } } } catch(e) { console.error('加载账号信息失败:', e); } } // ==================== 自动刷新功能 (任务 10.2) ==================== let autoRefreshTimer = null; const AUTO_REFRESH_INTERVAL = 60000; // 60秒 function startAutoRefresh() { if (autoRefreshTimer) clearInterval(autoRefreshTimer); autoRefreshTimer = setInterval(() => { const accountsTab = document.querySelector('.tab[data-tab="accounts"]'); if (accountsTab && accountsTab.classList.contains('active')) { loadAccounts(); loadAccountsEnhanced(); } }, AUTO_REFRESH_INTERVAL); } function stopAutoRefresh() { if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; } } // 页面加载时启动自动刷新 startAutoRefresh(); // 页面初始化:如果默认显示账号页面,则加载账号数据 function initializeDefaultTab() { const accountsTab = document.querySelector('.tab[data-tab="accounts"]'); const accountsPanel = document.querySelector('#accounts'); // 检查账号标签页和面板是否都处于激活状态(默认页面) if (accountsTab && accountsTab.classList.contains('active') && accountsPanel && accountsPanel.classList.contains('active')) { // 延迟一点时间确保DOM完全加载 setTimeout(() => { loadAccounts(); loadAccountsEnhanced(); }, 100); } } // 页面加载完成后初始化默认标签页 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeDefaultTab); } else { initializeDefaultTab(); } // ==================== 加载状态指示器 (任务 10.1) ==================== function showLoading(container, message = '加载中...') { const el = typeof container === 'string' ? document.querySelector(container) : container; if (el) { el.innerHTML = `

    ${message}

    `; } } // 添加旋转动画 if (!document.querySelector('#spinKeyframes')) { const style = document.createElement('style'); style.id = 'spinKeyframes'; style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }'; document.head.appendChild(style); } // ==================== 表单验证 (任务 10.3) ==================== function validateToken(token) { if (!token || token.trim().length === 0) { return { valid: false, error: 'Token 不能为空' }; } if (token.trim().length < 20) { return { valid: false, error: 'Token 格式不正确,长度过短' }; } return { valid: true }; } function validateAccountName(name) { if (!name || name.trim().length === 0) { return { valid: true, default: '手动添加账号' }; // 名称可选 } if (name.length > 50) { return { valid: false, error: '账号名称不能超过50个字符' }; } return { valid: true }; } // ==================== 网络错误处理 (任务 10.1) ==================== async function fetchWithRetry(url, options = {}, retries = 2) { for (let i = 0; i <= retries; i++) { try { const r = await fetch(url, options); if (!r.ok && r.status >= 500 && i < retries) { await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); continue; } return r; } catch (e) { if (i === retries) throw e; await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } } } // ==================== 全局进度条组件 (任务 18.1) ==================== const GlobalProgressBar = { pollTimer: null, isRefreshing: false, // 显示进度条 show(total) { this.isRefreshing = true; const bar = $('#globalProgressBar'); if (bar) { bar.classList.add('active'); // 重置显示 $('#globalProgressTitle').textContent = '正在刷新额度...'; $('#globalProgressCompleted').textContent = '0'; $('#globalProgressTotal').textContent = total || '0'; $('#globalProgressSuccess').textContent = '0'; $('#globalProgressFailed').textContent = '0'; $('#globalProgressFill').style.width = '0%'; $('#globalProgressFill').classList.remove('complete'); $('#globalProgressCurrent').textContent = '准备中...'; $('#globalProgressClose').style.display = 'none'; // 显示 spinner const spinner = bar.querySelector('.spinner'); if (spinner) spinner.style.display = 'inline-block'; } // 禁用刷新按钮 this.updateRefreshButton(true); }, // 更新进度 update(progress) { if (!progress) return; const completed = progress.completed || 0; const total = progress.total || 0; const success = progress.success || 0; const failed = progress.failed || 0; const current = progress.current_account || ''; const isComplete = progress.status === 'completed' || progress.status === 'idle'; // 更新数字 $('#globalProgressCompleted').textContent = completed; $('#globalProgressTotal').textContent = total; $('#globalProgressSuccess').textContent = success; $('#globalProgressFailed').textContent = failed; // 更新进度条 const percent = total > 0 ? (completed / total * 100) : 0; const fill = $('#globalProgressFill'); if (fill) { fill.style.width = percent + '%'; if (isComplete) { fill.classList.add('complete'); } } // 更新当前处理的账号 if (current) { $('#globalProgressCurrent').textContent = '正在处理: ' + current; } else if (isComplete) { $('#globalProgressCurrent').textContent = `刷新完成: 成功 ${success} 个, 失败 ${failed} 个`; } // 完成后的处理 if (isComplete) { this.isRefreshing = false; $('#globalProgressTitle').textContent = '刷新完成'; $('#globalProgressClose').style.display = 'inline-block'; // 隐藏 spinner const spinner = $('#globalProgressBar')?.querySelector('.spinner'); if (spinner) spinner.style.display = 'none'; // 恢复刷新按钮 this.updateRefreshButton(false); // 刷新账号列表 loadAccounts(); loadAccountsEnhanced(); // 显示完成通知 if (failed > 0) { Toast.warning(`刷新完成: 成功 ${success} 个, 失败 ${failed} 个`); } else { Toast.success(`刷新完成: 成功 ${success} 个`); } // 5秒后自动关闭进度条 setTimeout(() => this.hide(), 5000); } }, // 隐藏进度条 hide() { const bar = $('#globalProgressBar'); if (bar) { bar.classList.remove('active'); } this.isRefreshing = false; this.stopPolling(); this.updateRefreshButton(false); }, // 开始轮询进度 startPolling() { this.stopPolling(); this.pollTimer = setInterval(() => this.pollProgress(), 500); }, // 停止轮询 stopPolling() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } }, // 轮询进度 API async pollProgress() { try { const r = await fetch('/api/refresh/progress'); const d = await r.json(); if (d.ok) { // 传入 progress 对象,如果没有则传入整个响应(兼容) const progress = d.progress || d; // 添加 status 字段用于判断完成状态 if (!d.is_refreshing && !progress.status) { progress.status = 'completed'; } this.update(progress); // 如果完成则停止轮询 if (!d.is_refreshing || progress.status === 'completed' || progress.status === 'idle') { this.stopPolling(); } } } catch (e) { console.error('轮询进度失败:', e); } }, // 更新刷新按钮状态 updateRefreshButton(disabled) { // 查找所有刷新额度按钮 const buttons = document.querySelectorAll('button'); buttons.forEach(btn => { const text = btn.textContent; const originalText = btn.dataset.originalText; // 匹配"刷新额度"、"刷新全部额度"或已经变成"刷新中..."的按钮 if (text.includes('刷新额度') || text.includes('刷新全部额度') || text === '刷新中...' || (originalText && (originalText.includes('刷新额度') || originalText.includes('刷新全部额度')))) { btn.disabled = disabled; if (disabled) { if (!btn.dataset.originalText) { btn.dataset.originalText = text; } btn.textContent = '刷新中...'; } else if (btn.dataset.originalText) { btn.textContent = btn.dataset.originalText; delete btn.dataset.originalText; } } }); } }; // ==================== 进度轮询函数 (任务 18.2) ==================== async function pollRefreshProgress() { return GlobalProgressBar.pollProgress(); } ''' JS_SCRIPTS = JS_UTILS + JS_TABS + JS_STATUS + JS_DOCS + JS_STATS + JS_LOGS + JS_ACCOUNTS + JS_LOGIN + JS_FLOWS + JS_SETTINGS + JS_UI_COMPONENTS + JS_AUTH # ==================== 组装最终 HTML ==================== HTML_PAGE = f''' Kiro API
    {HTML_BODY}
    '''