gcli2api / front /control_panel.html
lightspeed's picture
Upload 8 files
aea8fce verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GCLI2API 控制面板</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
}
input[type="text"]:focus {
border-color: #4285f4;
outline: none;
}
.btn {
background-color: #4285f4;
color: white;
padding: 12px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin-bottom: 10px;
}
.btn:hover {
background-color: #3367d6;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.auth-url {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 5px;
padding: 15px;
margin: 20px 0;
word-break: break-all;
}
.auth-url a {
color: #4285f4;
text-decoration: none;
}
.auth-url a:hover {
text-decoration: underline;
}
.credentials {
background-color: #f0f8ff;
border: 1px solid #b0d4ff;
border-radius: 5px;
padding: 15px;
margin: 20px 0;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
}
.status {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.status.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.status.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.status.info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.hidden {
display: none;
}
.loading {
text-align: center;
color: #666;
}
.login-form {
text-align: center;
padding: 50px 0;
}
.login-form input[type="password"] {
width: 300px;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
margin-bottom: 20px;
box-sizing: border-box;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: #f5f5f5;
border-bottom: 2px solid transparent;
}
.tab.active {
background: white;
border-bottom-color: #4285f4;
color: #4285f4;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 5px;
padding: 40px;
text-align: center;
background-color: #fafafa;
margin: 20px 0;
cursor: pointer;
transition: border-color 0.3s;
}
.upload-area:hover {
border-color: #4285f4;
}
.upload-area.dragover {
border-color: #4285f4;
background-color: #f0f8ff;
}
.file-list {
margin: 20px 0;
}
.file-item {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 3px;
padding: 10px;
margin: 5px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-item .file-name {
font-family: monospace;
color: #333;
}
.file-item .file-size {
color: #666;
font-size: 12px;
}
.file-item .remove-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 3px;
padding: 2px 8px;
cursor: pointer;
font-size: 12px;
}
.upload-progress {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 5px;
padding: 15px;
margin: 20px 0;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background-color: #28a745;
transition: width 0.3s ease;
}
/* 文件管理样式 */
.cred-card {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
position: relative;
min-width: 600px;
}
.cred-card.disabled {
background-color: #f5f5f5;
border-color: #ccc;
opacity: 0.7;
}
.cred-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
gap: 20px;
}
.cred-filename {
font-family: monospace;
font-weight: bold;
color: #333;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 300px;
}
.cred-status {
display: flex;
gap: 5px;
}
.status-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
color: white;
}
.status-badge.enabled {
background-color: #28a745;
}
.status-badge.disabled {
background-color: #6c757d;
}
.error-codes {
background-color: #f8d7da;
color: #721c24;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
.cred-actions {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.cred-btn {
padding: 4px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.cred-btn.enable {
background-color: #28a745;
color: white;
}
.cred-btn.disable {
background-color: #6c757d;
color: white;
}
.cred-btn.delete {
background-color: #dc3545;
color: white;
}
.cred-btn.download {
background-color: #007bff;
color: white;
}
.cred-btn.view {
background-color: #17a2b8;
color: white;
}
.cred-details {
margin-top: 10px;
display: none;
}
.cred-details.show {
display: block;
}
.cred-content {
background-color: #f0f8ff;
border: 1px solid #b0d4ff;
border-radius: 4px;
padding: 10px;
font-family: monospace;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
.manage-actions {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
/* 文件管理新增样式 */
.stats-container {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-item {
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px 20px;
text-align: center;
min-width: 120px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #333;
display: block;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-top: 4px;
}
.stat-item.total {
border-left: 4px solid #007bff;
}
.stat-item.normal {
border-left: 4px solid #28a745;
}
.stat-item.disabled {
border-left: 4px solid #6c757d;
}
.filter-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.filter-select {
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
background-color: white;
}
.filter-select:focus {
border-color: #4285f4;
outline: none;
}
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}
.pagination-info {
color: #666;
font-size: 14px;
}
.pagination-btn {
padding: 8px 12px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.pagination-btn:hover:not(:disabled) {
background: #f8f9fa;
border-color: #4285f4;
}
.pagination-btn:disabled {
background: #f5f5f5;
color: #ccc;
cursor: not-allowed;
}
.pagination-btn.active {
background: #4285f4;
color: white;
border-color: #4285f4;
}
.page-size-select {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.refresh-btn {
background-color: #17a2b8;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.download-all-btn {
background-color: #28a745;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* 批量操作样式 */
.batch-controls {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.checkbox-container {
display: flex;
align-items: center;
margin-right: 15px;
}
.batch-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 10px;
}
.batch-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.batch-btn.batch-enable {
background-color: #28a745;
color: white;
}
.batch-btn.batch-disable {
background-color: #6c757d;
color: white;
}
.batch-btn.batch-delete {
background-color: #dc3545;
color: white;
}
.batch-btn.batch-email {
background-color: #17a2b8;
color: white;
}
.batch-btn:disabled {
background-color: #e9ecef;
color: #6c757d;
cursor: not-allowed;
}
.cred-btn.email {
background-color: #17a2b8;
color: white;
}
.cred-btn.email:hover {
background-color: #138496;
}
.selected-count {
font-weight: bold;
color: #007bff;
margin-right: 10px;
}
.select-all-checkbox {
margin-right: 8px;
transform: scale(1.2);
}
/* 错误码筛选增强 */
.error-filter-container {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.error-code-badge {
display: inline-block;
background-color: #dc3545;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
margin: 1px;
cursor: pointer;
transition: background-color 0.2s;
}
.error-code-badge:hover {
background-color: #c82333;
}
.error-code-badge.selected {
background-color: #007bff;
}
/* 配置管理样式 */
.config-group {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 8px;
padding: 20px;
margin: 15px 0;
}
.config-group h4 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
border-bottom: 1px solid #e1e4e8;
padding-bottom: 8px;
}
.config-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
margin-bottom: 5px;
}
.config-input:focus {
border-color: #4285f4;
outline: none;
}
.config-input:disabled {
background-color: #f5f5f5;
color: #666;
cursor: not-allowed;
}
.config-checkbox {
margin-right: 8px;
transform: scale(1.2);
}
.config-note {
display: block;
color: #666;
font-size: 12px;
margin-bottom: 10px;
font-style: italic;
}
.config-info {
background-color: #e3f2fd;
border: 1px solid #1976d2;
border-radius: 4px;
padding: 12px;
margin-top: 8px;
font-size: 13px;
color: #1565c0;
}
.config-info ul {
margin: 8px 0 4px 0;
color: #424242;
}
.config-info li {
margin: 3px 0;
}
.env-locked {
position: relative;
}
.env-locked::after {
content: "🔒 环境变量锚定";
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background-color: #ffc107;
color: #212529;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
pointer-events: none;
}
/* 使用统计样式 */
.usage-card {
background-color: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
position: relative;
}
.usage-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.usage-filename {
font-family: monospace;
font-weight: bold;
color: #333;
font-size: 14px;
}
.usage-progress {
margin: 10px 0;
}
.usage-progress-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
font-size: 12px;
}
.usage-progress-bar {
width: 100%;
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.usage-progress-fill {
height: 100%;
transition: width 0.3s ease;
}
.usage-progress-fill.gemini {
background-color: #ff6b35;
}
.usage-progress-fill.total {
background-color: #007bff;
}
.usage-progress-fill.warning {
background-color: #ffc107;
}
.usage-progress-fill.danger {
background-color: #dc3545;
}
.usage-actions {
display: flex;
gap: 5px;
margin-top: 10px;
}
.usage-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: background-color 0.2s;
}
.usage-btn.reset {
background-color: #6c757d;
color: white;
}
.usage-btn.limits {
background-color: #17a2b8;
color: white;
}
.usage-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
font-size: 12px;
}
.usage-info-item {
background-color: #ffffff;
padding: 8px;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.usage-info-label {
font-weight: bold;
color: #666;
display: block;
margin-bottom: 2px;
}
.usage-info-value {
color: #333;
}
.reset-time {
font-size: 11px;
color: #666;
font-style: italic;
margin-top: 5px;
}
/* 限制设置弹窗样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 400px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.modal-title {
margin: 0;
font-size: 16px;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-close:hover {
color: #333;
}
.modal-body {
margin-bottom: 15px;
}
.modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
</style>
</head>
<body>
<div class="container">
<!-- 登录界面 -->
<div id="loginSection" class="login-form">
<h1>GCLI2API 管理面板</h1>
<p>请输入访问密码:</p>
<input type="password" id="loginPassword" placeholder="输入密码" onkeypress="handlePasswordEnter(event)" />
<br>
<button class="btn" onclick="login()">登录</button>
</div>
<!-- 主界面 -->
<div id="mainSection" class="hidden">
<h1>GCLI2API 管理面板</h1>
<!-- 标签页 -->
<div class="tabs">
<button class="tab active" onclick="switchTab('oauth')">OAuth认证</button>
<button class="tab" onclick="switchTab('upload')">批量上传</button>
<button class="tab" onclick="switchTab('envload')">环境变量</button>
<button class="tab" onclick="switchTab('manage')">文件管理</button>
<button class="tab" onclick="switchTab('usage')">使用统计</button>
<button class="tab" onclick="switchTab('config')">配置管理</button>
<button class="tab" onclick="switchTab('logs')">实时日志</button>
<button class="tab" onclick="switchTab('about')">项目信息</button>
</div>
<!-- OAuth认证标签页 -->
<div id="oauthTab" class="tab-content active">
<!-- API 自动启用说明 -->
<div class="status success" style="margin-bottom: 20px;">
<strong>✨ 自动化优化:</strong> 系统现在会在认证成功后自动为您的项目启用必需的API服务
<ul style="margin: 10px 0; padding-left: 20px;">
<li><strong>Gemini Cloud Assist API</strong></li>
<li><strong>Gemini for Google Cloud API</strong></li>
</ul>
<p style="margin: 10px 0; color: #155724;"><strong>说明:</strong>无需手动启用API,系统会自动处理这些配置步骤,让认证流程更加顺畅。
</p>
</div>
<!-- 折叠式 Project ID 输入框 -->
<div class="form-group">
<div style="cursor: pointer; user-select: none; padding: 12px; border: 2px solid #ddd; border-radius: 5px; background: #f8f9fa; display: flex; justify-content: space-between; align-items: center;"
onclick="toggleProjectIdSection()">
<span style="font-weight: bold; color: #555;">📁 高级选项:Google Cloud Project ID
(不用管,直接点击获取链接即可)</span>
<span id="projectIdToggleIcon"
style="font-size: 14px; color: #666; transition: transform 0.3s ease;"></span>
</div>
<div id="projectIdSection"
style="display: none; margin-top: 15px; padding: 15px; border: 2px solid #ddd; border-top: none; border-radius: 0 0 5px 5px; background: #ffffff;">
<label for="projectId"
style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">Google Cloud
Project ID (可选):</label>
<input type="text" id="projectId"
style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px; box-sizing: border-box;"
placeholder="留空将尝试自动检测,或手动输入项目ID" />
<small style="color: #666; font-size: 12px; margin-top: 5px; display: block;">
💡 提示:如果你不懂这是什么,可以留空此字段让系统自动检测项目ID
</small>
</div>
</div>
<!-- 为所有项目获取凭证选项 -->
<div class="form-group">
<div style="padding: 15px; border: 2px solid #28a745; border-radius: 8px; background: #f8fff9;">
<div style="display: flex; align-items: center; gap: 12px;">
<input type="checkbox" id="getAllProjectsCreds" style="transform: scale(1.3);">
<label for="getAllProjectsCreds"
style="font-weight: bold; color: #28a745; margin: 0; cursor: pointer;">
🌐 为当前账号所有项目获取凭证
</label>
</div>
<div style="margin-top: 10px; color: #155724; font-size: 14px; line-height: 1.5;">
<strong>说明:</strong>勾选此选项后,系统会自动检测当前Google账号下的所有项目,<strong>并发处理</strong>为每个项目生成独立的认证文件。
</div>
<div id="allProjectsNote"
style="margin-top: 8px; padding: 8px; background: #d4edda; border-radius: 5px; font-size: 13px; color: #155724; display: none;">
<strong>批量并发认证模式已启用</strong> - 认证完成后将并发为所有可访问的项目生成凭证文件
</div>
</div>
</div>
<button class="btn" id="getAuthBtn" onclick="startAuth()">获取认证链接</button>
<div id="authUrlSection" class="hidden">
<h3>认证链接:</h3>
<div class="auth-url">
<a id="authUrl" href="#" target="_blank">点击此链接进行认证</a>
</div>
<div class="status info">
<strong>重要说明:</strong>
<ol style="margin: 10px 0; padding-left: 20px;">
<li>点击上方认证链接,会在新窗口中打开Google OAuth页面</li>
<li>完成Google账号登录和授权</li>
<li>授权成功后会跳转到localhost:8080显示成功页面</li>
<li>关闭OAuth窗口,返回本页面</li>
<li>点击下方"获取认证文件"按钮完成流程</li>
</ol>
</div>
<!-- 快捷回调URL输入选项 -->
<div class="form-group"
style="margin: 20px 0; padding: 15px; border: 2px solid #e8f4fd; border-radius: 8px; background: #f8fcff;">
<div style="cursor: pointer; user-select: none; display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"
onclick="toggleCallbackUrlSection()">
<span style="font-weight: bold; color: #0066cc;">🚀 无法回源?试试快捷方式</span>
<span id="callbackUrlToggleIcon"
style="font-size: 14px; color: #666; transition: transform 0.3s ease;"></span>
</div>
<div id="callbackUrlSection" style="display: none;">
<div
style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 12px; margin-bottom: 12px;">
<div style="color: #856404; font-size: 14px; font-weight: bold; margin-bottom: 6px;">📚
适用场景:</div>
<ul
style="color: #856404; font-size: 13px; margin: 0; padding-left: 18px; line-height: 1.5;">
<li>云服务器、VPS等非本地环境</li>
<li>防火墙阻止了8080端口访问</li>
<li>网络环境无法正常回源到localhost</li>
<li>Docker容器内运行,端口映射问题</li>
</ul>
</div>
<div style="color: #666; font-size: 13px; margin-bottom: 12px; line-height: 1.6;">
<strong style="color: #0066cc;">🔍 什么是回调URL?</strong><br>
完成Google OAuth授权后,浏览器地址栏显示的完整URL,通常看起来像这样:<br>
<code
style="background: #f1f3f4; padding: 2px 6px; border-radius: 3px; font-size: 12px; word-break: break-all;">
http://localhost:8080/?state=abc123...&code=4/0AVMBsJ...&scope=email%20profile...
</code>
</div>
<div
style="background: #e7f3ff; border: 1px solid #b3d9ff; border-radius: 6px; padding: 10px; margin-bottom: 12px;">
<div style="color: #0066cc; font-size: 13px; font-weight: bold; margin-bottom: 4px;">📋
使用步骤:</div>
<ol
style="color: #0066cc; font-size: 12px; margin: 0; padding-left: 18px; line-height: 1.4;">
<li>点击上方认证链接,完成Google授权</li>
<li>授权成功后,复制浏览器地址栏的<strong>完整URL</strong></li>
<li>粘贴到下方输入框,点击获取凭证即可</li>
</ol>
</div>
<div class="input-group">
<input type="url" id="callbackUrlInput"
placeholder="粘贴完整的回调URL,例如:http://localhost:8080/?state=xxx&code=xxx&scope=xxx..."
style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 4px; font-size: 13px;">
</div>
<button class="btn" style="margin-top: 10px; background: #28a745; border-color: #28a745;"
onclick="processCallbackUrl()">
从回调URL获取凭证
</button>
</div>
</div>
<button class="btn" id="getCredsBtn" onclick="getCredentials()">获取认证文件</button>
</div>
<div id="credentialsSection" class="hidden">
<h3>认证文件内容:</h3>
<div class="credentials" id="credentialsContent"></div>
</div>
</div>
<!-- 批量上传标签页 -->
<div id="uploadTab" class="tab-content">
<h3>批量上传认证文件</h3>
<p>支持上传多个JSON格式的认证文件到服务器</p>
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
<p>点击选择文件或拖拽文件到此区域</p>
<p style="color: #666; font-size: 14px;">支持 .json 和 .zip 格式文件</p>
<p style="color: #888; font-size: 12px;">ZIP文件会自动解压提取其中的JSON凭证</p>
</div>
<input type="file" id="fileInput" multiple accept=".json,.zip" style="display: none;"
onchange="handleFileSelect(event)" />
<div id="fileListSection" class="hidden">
<h4>选择的文件:</h4>
<div class="file-list" id="fileList"></div>
<button class="btn" onclick="uploadFiles()">上传文件</button>
<button class="btn" style="background-color: #6c757d;" onclick="clearFiles()">清空列表</button>
</div>
<div id="uploadProgressSection" class="hidden">
<div class="upload-progress">
<h4>上传进度:</h4>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
</div>
<p id="progressText">0%</p>
</div>
</div>
</div>
<!-- 环境变量标签页 -->
<div id="envloadTab" class="tab-content">
<h3>环境变量凭证导入</h3>
<p>从环境变量批量导入认证文件,支持部署自动化场景</p>
<!-- 环境变量状态信息 -->
<div id="envStatusSection">
<div class="loading" id="envStatusLoading">正在检查环境变量状态...</div>
<div id="envStatusContent" class="hidden">
<div class="config-group">
<h4>环境变量状态</h4>
<div class="form-group">
<label>可用的环境变量:</label>
<div id="envVarsList" class="cred-content"></div>
</div>
<div class="form-group">
<label>自动加载设置:</label>
<span id="autoLoadStatus" style="font-weight: bold;"></span>
<small class="config-note">
要启用自动加载,请设置环境变量 AUTO_LOAD_ENV_CREDS=true
</small>
</div>
<div class="form-group">
<label>已导入的环境变量文件:</label>
<span id="envFilesCount" style="font-weight: bold;"></span>
<div id="envFilesList" class="config-note"></div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="manage-actions">
<button class="btn" onclick="checkEnvCredsStatus()">刷新状态</button>
<button class="btn" onclick="loadEnvCredentials()">从环境变量导入</button>
<button class="btn" style="background-color: #dc3545;"
onclick="clearEnvCredentials()">清除环境变量文件</button>
</div>
<!-- 使用说明 -->
<div class="config-group">
<h4>使用说明</h4>
<div class="config-info">
<strong>环境变量格式:</strong>
<ul>
<li><code>GCLI_CREDS_1</code>, <code>GCLI_CREDS_2</code>, ... (编号格式)</li>
<li><code>GCLI_CREDS_项目名1</code>, <code>GCLI_CREDS_项目名2</code>, ... (项目名格式)</li>
</ul>
<strong>环境变量值:</strong> 完整的认证文件JSON内容
<br><br>
<strong>示例:</strong>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 11px;">
export GCLI_CREDS_1='{"client_id":"your-client-id","client_secret":"your-secret","refresh_token":"your-token","token_uri":"https://oauth2.googleapis.com/token","project_id":"your-project"}'
export GCLI_CREDS_myproject='{"client_id":"...","project_id":"myproject",...}'
export AUTO_LOAD_ENV_CREDS=true # 启用程序启动时自动导入</pre>
<strong>Docker部署示例:</strong>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 11px;">
docker run -e GCLI_CREDS_1='{"client_id":"..."}' \
-e AUTO_LOAD_ENV_CREDS=true \
your-image</pre>
</div>
</div>
</div>
<!-- 文件管理标签页 -->
<div id="manageTab" class="tab-content">
<h3>凭证文件管理</h3>
<p>管理所有认证文件,查看状态和执行操作</p>
<!-- 状态统计 -->
<div class="stats-container" id="statsContainer">
<div class="stat-item total">
<span class="stat-number" id="statTotal">0</span>
<span class="stat-label">总计</span>
</div>
<div class="stat-item normal">
<span class="stat-number" id="statNormal">0</span>
<span class="stat-label">正常</span>
</div>
<div class="stat-item disabled">
<span class="stat-number" id="statDisabled">0</span>
<span class="stat-label">禁用</span>
</div>
</div>
<div class="manage-actions">
<button class="refresh-btn" onclick="refreshCredsStatus()">刷新状态</button>
<button class="download-all-btn" onclick="downloadAllCreds()">打包下载所有文件</button>
</div>
<!-- 批量操作控件 -->
<div class="batch-controls">
<h4 style="margin-top: 0; margin-bottom: 10px;">批量操作</h4>
<div class="batch-actions">
<div class="checkbox-container">
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox"
onchange="toggleSelectAll()">
<label for="selectAllCheckbox">全选</label>
</div>
<span class="selected-count" id="selectedCount">已选择 0 项</span>
<button class="batch-btn batch-enable" id="batchEnableBtn" onclick="batchAction('enable')"
disabled>批量启用</button>
<button class="batch-btn batch-disable" id="batchDisableBtn" onclick="batchAction('disable')"
disabled>批量禁用</button>
<button class="batch-btn batch-delete" id="batchDeleteBtn" onclick="batchAction('delete')"
disabled>批量删除</button>
<button class="batch-btn batch-email" onclick="refreshAllEmails()">刷新所有邮箱</button>
</div>
</div>
<!-- 筛选和分页控件 -->
<div class="filter-container">
<label for="statusFilter">状态筛选:</label>
<select id="statusFilter" class="filter-select" onchange="applyFilters()">
<option value="all">全部</option>
<option value="normal">正常</option>
<option value="disabled">禁用</option>
</select>
<label for="errorCodeFilter">错误码筛选:</label>
<select id="errorCodeFilter" class="filter-select" onchange="applyFilters()">
<option value="all">全部错误码</option>
<option value="no-errors">无错误</option>
<option value="has-errors">有错误</option>
<option value="400">错误码 400</option>
<option value="401">错误码 401</option>
<option value="403">错误码 403</option>
<option value="404">错误码 404</option>
<option value="429">错误码 429</option>
<option value="500">错误码 500</option>
</select>
<label for="pageSizeSelect">每页显示:</label>
<select id="pageSizeSelect" class="page-size-select" onchange="changePageSize()">
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<div class="error-filter-container" id="errorFilterContainer">
<span>快速筛选错误码:</span>
<div id="errorCodeBadges"></div>
</div>
</div>
<div id="credsListSection">
<div class="loading" id="credsLoading">正在加载凭证文件...</div>
<div id="credsList"></div>
<!-- 分页控件 -->
<div class="pagination-container" id="paginationContainer" style="display: none;">
<button class="pagination-btn" id="prevPageBtn" onclick="changePage(-1)">上一页</button>
<div class="pagination-info" id="paginationInfo">第 1 页,共 1 页</div>
<button class="pagination-btn" id="nextPageBtn" onclick="changePage(1)">下一页</button>
</div>
</div>
</div>
<!-- 使用统计标签页 -->
<div id="usageTab" class="tab-content">
<h3>使用统计</h3>
<p>查看每个凭证文件的API调用统计和配额使用情况</p>
<!-- 统计概览 -->
<div class="stats-container" id="usageStatsContainer">
<div class="stat-item total">
<span class="stat-number" id="totalApiCalls">0</span>
<span class="stat-label">总调用数</span>
</div>
<div class="stat-item normal" style="border-left: 4px solid #ff6b35;">
<span class="stat-number" id="geminiProCalls">0</span>
<span class="stat-label">Gemini 2.5 Pro</span>
</div>
<div class="stat-item disabled">
<span class="stat-number" id="totalFiles">0</span>
<span class="stat-label">活跃文件数</span>
</div>
</div>
<div class="manage-actions">
<button class="refresh-btn" onclick="refreshUsageStats()">刷新统计</button>
<button class="btn" style="background-color: #dc3545;"
onclick="resetAllUsageStats()">重置所有统计</button>
</div>
<!-- 文件使用统计列表 -->
<div id="usageListSection">
<div class="loading" id="usageLoading">正在加载使用统计...</div>
<div id="usageList"></div>
</div>
<!-- 使用说明 -->
<div class="config-group">
<h4>使用说明</h4>
<div class="config-info">
<strong>统计范围:</strong>
<ul>
<li><strong>Gemini 2.5 Pro 调用次数:</strong>仅统计 gemini-2.5-pro 及其变体模型的成功调用</li>
<li><strong>所有模型调用次数:</strong>统计所有模型的成功调用总数</li>
<li><strong>每日配额:</strong>默认每日配额 Gemini 2.5 Pro: 100次,所有模型: 1000次</li>
<li><strong>配额重置:</strong>每天 UTC 07:00 自动重置调用计数</li>
</ul>
<strong>注意:</strong>
<ul>
<li>只统计返回正常响应的API调用,报错的调用不计入统计</li>
<li>统计数据持久化保存在 creds_state.toml 文件中</li>
<li>支持每个凭证文件独立统计和配额管理</li>
</ul>
</div>
</div>
</div>
<!-- 配置管理标签页 -->
<div id="configTab" class="tab-content">
<h3>配置管理</h3>
<p>管理系统配置参数,修改后立即生效</p>
<div class="manage-actions">
<button class="refresh-btn" onclick="loadConfig()">刷新配置</button>
<button class="btn" onclick="saveConfig()">保存配置</button>
</div>
<div id="configSection">
<div class="loading" id="configLoading">正在加载配置...</div>
<div id="configForm" class="hidden">
<div class="config-group">
<h4>服务器配置</h4>
<div class="form-group">
<label for="host">服务器主机地址:</label>
<input type="text" id="host" class="config-input"
placeholder="例如: 0.0.0.0, 127.0.0.1" />
<small class="config-note">服务器监听的主机地址,0.0.0.0表示监听所有接口</small>
</div>
<div class="form-group">
<label for="port">服务器端口:</label>
<input type="number" id="port" class="config-input" min="1" max="65535"
placeholder="7861" />
<small class="config-note">服务器监听的端口号,修改后需要重启服务器</small>
</div>
<div class="form-group">
<label for="configApiPassword">API访问密码:</label>
<input type="text" id="configApiPassword" class="config-input" placeholder="pwd" />
<small class="config-note">聊天API访问密码,用于OpenAI和Gemini API端点的认证</small>
</div>
<div class="form-group">
<label for="configPanelPassword">控制面板密码:</label>
<input type="text" id="configPanelPassword" class="config-input" placeholder="pwd" />
<small class="config-note">控制面板访问密码,用于web界面登录认证</small>
</div>
<div class="form-group">
<label for="configPassword">通用密码:</label>
<input type="text" id="configPassword" class="config-input" placeholder="pwd" />
<small class="config-note">(兼容性保留)设置后将覆盖上述两个密码,留空则使用分开的密码设置</small>
</div>
</div>
<div class="config-group">
<h4>基础配置</h4>
<div class="form-group">
<label for="credentialsDir">凭证目录路径:</label>
<input type="text" id="credentialsDir" class="config-input" />
<small class="config-note">存储认证文件的目录路径</small>
</div>
<div class="form-group">
<label for="proxy">代理设置:</label>
<input type="text" id="proxy" class="config-input"
placeholder="例如: http://proxy:8080 或 socks5://proxy:1080" />
<small class="config-note">HTTP/HTTPS/SOCKS5Endpoint,留空表示不使用代理</small>
</div>
</div>
<div class="config-group">
<h4>端点配置</h4>
<!-- 快速配置按钮 -->
<div class="form-group">
<div style="display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap;">
<button type="button" class="btn" onclick="useMirrorUrls()"
style="background-color: #28a745; font-size: 14px;">
🚀 一键使用镜像网址
</button>
<button type="button" class="btn" onclick="restoreOfficialUrls()"
style="background-color: #17a2b8; font-size: 14px;">
🔄 还原官方端点
</button>
</div>
<small class="config-note">镜像网址主要解决墙内无法访问官方端点的问题,部分地区可能无法使用</small>
</div>
<div class="form-group">
<label for="codeAssistEndpoint">Code Assist Endpoint:</label>
<input type="text" id="codeAssistEndpoint" class="config-input" />
<small class="config-note">Google Cloud Code Assist API端点地址</small>
</div>
<div class="form-group">
<label for="oauthProxyUrl">OAuth Endpoint:</label>
<input type="text" id="oauthProxyUrl" class="config-input"
placeholder="https://oauth2.googleapis.com" />
<small class="config-note">Google OAuth2 API端点地址,用于token获取和刷新</small>
</div>
<div class="form-group">
<label for="googleapisProxyUrl">Google APIs Endpoint:</label>
<input type="text" id="googleapisProxyUrl" class="config-input"
placeholder="https://www.googleapis.com" />
<small class="config-note">Google APIs API端点地址,用于API服务调用</small>
</div>
<div class="form-group">
<label for="resourceManagerApiUrl">Resource Manager API Endpoint:</label>
<input type="text" id="resourceManagerApiUrl" class="config-input"
placeholder="https://cloudresourcemanager.googleapis.com" />
<small class="config-note">Google Cloud Resource Manager API端点地址,用于项目管理</small>
</div>
<div class="form-group">
<label for="serviceUsageApiUrl">Service Usage API Endpoint:</label>
<input type="text" id="serviceUsageApiUrl" class="config-input"
placeholder="https://serviceusage.googleapis.com" />
<small class="config-note">Google Cloud Service Usage API端点地址,用于服务启用管理</small>
</div>
</div>
<div class="config-group">
<h4>自动封禁配置</h4>
<div class="form-group">
<label>
<input type="checkbox" id="autoBanEnabled" class="config-checkbox" />
启用自动封禁
</label>
<small class="config-note">遇到指定错误码时自动禁用凭证</small>
</div>
<div class="form-group">
<label for="autoBanErrorCodes">自动封禁错误码:</label>
<input type="text" id="autoBanErrorCodes" class="config-input"
placeholder="例如: 400,403" />
<small class="config-note">用逗号分隔的错误码列表</small>
</div>
</div>
<div class="config-group">
<h4>性能配置</h4>
<div class="form-group">
<label for="callsPerRotation">凭证轮换调用次数:</label>
<input type="number" id="callsPerRotation" class="config-input" min="1" max="100" />
<small class="config-note">每个凭证使用多少次后轮换到下一个</small>
</div>
</div>
<div class="config-group">
<h4>429重试配置</h4>
<div class="form-group">
<label>
<input type="checkbox" id="retry429Enabled" class="config-checkbox" />
启用429重试
</label>
<small class="config-note">遇到429错误时自动重试</small>
</div>
<div class="form-group">
<label for="retry429MaxRetries">429重试次数:</label>
<input type="number" id="retry429MaxRetries" class="config-input" min="1" max="50" />
<small class="config-note">遇到429错误时的最大重试次数</small>
</div>
<div class="form-group">
<label for="retry429Interval">429重试间隔(秒):</label>
<input type="number" id="retry429Interval" class="config-input" min="0.01" max="10"
step="0.01" />
<small class="config-note">遇到429错误时每两次重试间的等待时间</small>
</div>
</div>
<div class="config-group">
<h4>兼容性配置</h4>
<div class="form-group">
<label>
<input type="checkbox" id="compatibilityModeEnabled" class="config-checkbox" />
启用兼容性模式
</label>
<small class="config-note">启用后所有system消息全部转换成user,停用system_instructions <span
style="color: #28a745;">✓ 支持热更新</span></small>
<div class="config-info"
style="background-color: #fff3cd; border: 1px solid #ffc107; color: #856404;">
<strong>⚠️ 注意:</strong>该选项可能会降低模型理解能力,但是能避免流式空回的情况。
<br><strong>适用场景:</strong>当遇到流式传输时模型不返回内容或返回空响应时启用此选项。
</div>
</div>
</div>
<div class="config-group">
<h4>抗截断配置</h4>
<div class="form-group">
<label for="antiTruncationMaxAttempts">抗截断最大重试次数:</label>
<input type="number" id="antiTruncationMaxAttempts" class="config-input" min="1"
max="10" />
<small class="config-note">当检测到输出截断时的最大续传尝试次数</small>
</div>
<div class="form-group">
<div class="config-info">
<strong>注意:</strong>抗截断功能现在通过模型名控制:
<ul style="margin: 5px 0; padding-left: 20px;">
<li>选择带有 "-流式抗截断" 后缀的模型即可启用</li>
<li>该功能仅在流式传输时生效</li>
<li>例如: "gemini-2.5-pro-流式抗截断"</li>
</ul>
</div>
</div>
</div>
<div class="config-group">
<h4>配置热更新说明</h4>
<div class="form-group">
<div class="config-info"
style="background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
<strong>🔥 热更新配置(立即生效):</strong>
<ul style="margin: 8px 0; padding-left: 20px; color: #212529;">
<li><strong>网络配置:</strong>代理设置、端点配置、HTTP超时时间、最大连接数</li>
<li><strong>API配置:</strong>凭证轮换次数、429重试设置、自动封禁配置</li>
<li><strong>密码配置:</strong>API密码、控制面板密码、通用密码</li>
<li><strong>功能配置:</strong>抗截断最大重试次数</li>
</ul>
</div>
</div>
<div class="form-group">
<div class="config-info"
style="background-color: #fff3cd; border: 1px solid #ffc107; color: #856404;">
<strong>🔄 需要重启的配置:</strong>
<ul style="margin: 8px 0; padding-left: 20px; color: #212529;">
<li><strong>服务器配置:</strong>主机地址、端口号</li>
<li><strong>目录配置:</strong>凭证目录路径、Code Assist端点</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 实时日志标签页 -->
<div id="logsTab" class="tab-content">
<h3>实时日志</h3>
<p>查看系统实时日志输出,支持日志筛选和自动滚动</p>
<div class="manage-actions">
<button class="refresh-btn" onclick="connectWebSocket()">连接日志流</button>
<button class="btn" style="background-color: #dc3545;" onclick="disconnectWebSocket()">断开连接</button>
<button class="btn" style="background-color: #28a745;" onclick="downloadLogs()">下载日志</button>
<button class="btn" style="background-color: #6c757d;" onclick="clearLogs()">清空日志</button>
</div>
<div class="filter-container">
<label for="logLevelFilter">日志级别筛选:</label>
<select id="logLevelFilter" class="filter-select" onchange="filterLogs()">
<option value="all">全部</option>
<option value="ERROR">错误</option>
<option value="WARNING">警告</option>
<option value="INFO">信息</option>
<option value="DEBUG">调试</option>
</select>
<label>
<input type="checkbox" id="autoScroll" checked> 自动滚动到底部
</label>
</div>
<div id="logConnectionStatus" class="status info">
<strong>连接状态:</strong> <span id="connectionStatusText">未连接</span>
</div>
<div id="logContainer"
style="background-color: #1e1e1e; color: #ffffff; font-family: 'Courier New', monospace; font-size: 12px; height: 600px; overflow-y: auto; border: 1px solid #333; border-radius: 5px; padding: 15px; white-space: pre-wrap; word-break: break-all;">
<div id="logContent">等待连接日志流...</div>
</div>
</div>
<!-- 项目信息标签页 -->
<div id="aboutTab" class="tab-content">
<h3>项目信息</h3>
<p>关于GCLI2API项目的详细信息和支持方式</p>
<!-- 项目介绍 -->
<div
style="background-color: #f8f9fa; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 4px solid #007bff;">
<h4 style="margin-top: 0; color: #007bff;">📋 项目简介</h4>
<p style="margin: 10px 0; line-height: 1.6; color: #495057;">
GCLI2API是一个将Google Gemini API转换为OpenAI 和GEMINI API格式的代理工具,支持多账户管理、自动轮换、实时日志监控等功能。
</p>
<div style="margin: 15px 0;">
<p style="margin: 5px 0;"><strong>🔗 项目地址:</strong> <a
href="https://github.com/su-kaka/gcli2api" target="_blank"
style="color: #007bff; text-decoration: none;">GitHub - su-kaka/gcli2api</a></p>
<p style="margin: 5px 0;"><strong>⚠️ 使用声明:</strong> <span
style="color: #dc3545; font-weight: 500;">禁止商业用途和倒卖 - 仅供学习使用</span></p>
</div>
</div>
<!-- 功能特性 -->
<div
style="background-color: #e7f3ff; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 4px solid #17a2b8;">
<h4 style="margin-top: 0; color: #17a2b8;">✨ 主要功能</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
<div>
<p><strong>🔄 多账户管理:</strong> 支持批量上传和管理多个Google账户</p>
<p><strong>⚡ 自动轮换:</strong> 智能轮换账户,避免单账户限额</p>
<p><strong>📊 实时监控:</strong> 使用统计、错误监控、实时日志</p>
</div>
<div>
<p><strong>🛡️ 安全可靠:</strong> OAuth2认证、自动封禁异常账户</p>
<p><strong>🎛️ 配置灵活:</strong> 支持热更新配置、代理设置</p>
<p><strong>📱 界面友好:</strong> 响应式设计、移动端适配</p>
</div>
</div>
</div>
<!-- 支持项目 -->
<div
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin: 30px 0; text-align: center; box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);">
<h4 style="margin: 0 0 15px 0; font-size: 24px; font-weight: 600;">💝 支持项目发展</h4>
<p style="margin: 0 0 25px 0; font-size: 16px; opacity: 0.9; line-height: 1.6;">
如果这个项目对您有帮助,欢迎通过币安扫码捐赠支持项目的持续发展!<br>
您的每一份支持都是我们前进的动力 ❤️
</p>
<div
style="display: inline-block; background: white; padding: 20px; border-radius: 15px; box-shadow: 0 4px 25px rgba(0,0,0,0.1);">
<img src="docs/币安.jpg" alt="币安捐赠二维码"
style="width: 200px; height: 200px; border-radius: 10px; display: block;">
<p style="color: #666; margin: 12px 0 0 0; font-size: 14px; font-weight: 600;">扫码币安捐赠</p>
</div>
</div>
<!-- 联系和反馈 -->
<div
style="background-color: #d1ecf1; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 4px solid #bee5eb;">
<h4 style="margin-top: 0; color: #0c5460;">📞 联系我们</h4>
<div style="color: #0c5460; line-height: 1.6;">
<p><strong>问题反馈:</strong> 通过GitHub Issues提交问题和建议</p>
<p><strong>功能请求:</strong> 在GitHub Discussions中讨论新功能</p>
<p><strong>代码贡献:</strong> 欢迎提交Pull Request改进项目</p>
<p><strong>文档完善:</strong> 帮助改进项目文档和使用指南</p>
</div>
</div>
</div>
<div id="statusSection"></div>
<!-- 项目信息 -->
<div
style="background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 12px; margin-top: 30px; text-align: center; border-left: 4px solid #007bff;">
<p style="margin: 5px 0; font-size: 14px; color: #495057;">GitHub: <a
href="https://github.com/su-kaka/gcli2api" target="_blank"
style="color: #007bff; text-decoration: none;">https://github.com/su-kaka/gcli2api</a></p>
<p style="margin: 5px 0; font-size: 14px; color: #dc3545; font-weight: 500;">⚠️ 禁止商业用途和倒卖 - 仅供学习使用 ⚠️
</p>
</div>
</div>
</div>
<!-- 限制设置弹窗 -->
<div id="limitsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">设置使用限制</h3>
<button class="modal-close" onclick="closeLimitsModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="modalFilename">文件名:</label>
<input type="text" id="modalFilename" class="config-input" readonly />
</div>
<div class="form-group">
<label for="modalGeminiLimit">Gemini 2.5 Pro 每日限制:</label>
<input type="number" id="modalGeminiLimit" class="config-input" min="1" max="10000" />
<small class="config-note">每日Gemini 2.5 Pro模型调用次数限制</small>
</div>
<div class="form-group">
<label for="modalTotalLimit">所有模型每日限制:</label>
<input type="number" id="modalTotalLimit" class="config-input" min="1" max="50000" />
<small class="config-note">每日所有模型调用次数总限制</small>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeLimitsModal()" style="background-color: #6c757d;">取消</button>
<button class="btn" onclick="saveLimits()">保存</button>
</div>
</div>
</div>
<script>
let currentProjectId = '';
let authInProgress = false;
let uploadSelectedFiles = []; // 上传页面用的文件列表
let authToken = '';
let credsData = {};
// 分页和筛选相关变量
let filteredCredsData = {};
let currentPage = 1;
let pageSize = 20;
let currentFilter = 'all';
let currentErrorCodeFilter = 'all';
let selectedCredFiles = new Set(); // 选中的凭证文件名集合
let availableErrorCodes = new Set(); // 所有可用的错误码
let statsData = {
total: 0,
normal: 0,
disabled: 0
};
function showStatus(message, type = 'info') {
console.log('showStatus called:', message, type);
const statusSection = document.getElementById('statusSection');
if (statusSection) {
statusSection.innerHTML = `<div class="status ${type}">${message}</div>`;
} else {
console.error('statusSection not found');
alert(message); // 临时回退方案
}
}
// 登录相关函数
async function login() {
console.log('Login function called');
const password = document.getElementById('loginPassword').value;
console.log('Password length:', password ? password.length : 0);
if (!password) {
showStatus('请输入密码', 'error');
return;
}
try {
console.log('Sending login request...');
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password: password })
});
console.log('Login response status:', response.status);
const data = await response.json();
console.log('Login response data:', data);
if (response.ok) {
authToken = data.token;
console.log('Login successful, token received');
document.getElementById('loginSection').classList.add('hidden');
document.getElementById('mainSection').classList.remove('hidden');
showStatus('登录成功', 'success');
} else {
console.log('Login failed:', data);
showStatus(`登录失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('Login error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
function handlePasswordEnter(event) {
if (event.key === 'Enter') {
login();
}
}
// 标签页切换
function switchTab(tabName) {
// 移除所有活动标签
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// 激活选中标签
event.target.classList.add('active');
document.getElementById(tabName + 'Tab').classList.add('active');
// 如果切换到文件管理页面,自动加载数据
if (tabName === 'manage') {
refreshCredsStatus();
}
// 如果切换到使用统计页面,自动加载统计
if (tabName === 'usage') {
refreshUsageStats();
}
// 如果切换到配置管理页面,自动加载配置
if (tabName === 'config') {
loadConfig();
}
// 如果切换到环境变量页面,自动检查状态
if (tabName === 'envload') {
checkEnvCredsStatus();
}
// 如果切换到日志页面,自动连接WebSocket
if (tabName === 'logs') {
connectWebSocket();
}
}
// 获取认证头
function getAuthHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
};
}
async function startAuth() {
const projectId = document.getElementById('projectId').value.trim();
const getAllProjects = document.getElementById('getAllProjectsCreds').checked;
// 项目ID现在是可选的
currentProjectId = projectId || null;
const btn = document.getElementById('getAuthBtn');
btn.disabled = true;
btn.textContent = '正在获取认证链接...';
try {
const requestBody = {};
if (projectId) {
requestBody.project_id = projectId;
}
if (getAllProjects) {
requestBody.get_all_projects = true;
showStatus('批量并发认证模式:将为当前账号所有项目生成认证链接...', 'info');
} else if (projectId) {
showStatus('使用指定的项目ID生成认证链接...', 'info');
} else {
showStatus('将尝试自动检测项目ID,正在生成认证链接...', 'info');
}
const response = await fetch('/auth/start', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (response.ok) {
document.getElementById('authUrl').href = data.auth_url;
document.getElementById('authUrl').textContent = data.auth_url;
document.getElementById('authUrlSection').classList.remove('hidden');
if (getAllProjects) {
showStatus('批量并发认证链接已生成,完成授权后将并发为所有可访问项目生成凭证文件', 'info');
} else if (data.auto_project_detection) {
showStatus('认证链接已生成(将在认证完成后自动检测项目ID),请点击链接完成授权', 'info');
} else {
showStatus(`认证链接已生成(项目ID: ${data.detected_project_id}),请点击链接完成授权`, 'info');
}
authInProgress = true;
} else {
showStatus(`错误: ${data.error || '获取认证链接失败'}`, 'error');
}
} catch (error) {
showStatus(`网络错误: ${error.message}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = '获取认证链接';
}
}
async function getCredentials() {
if (!authInProgress) {
showStatus('请先获取认证链接并完成授权', 'error');
return;
}
const btn = document.getElementById('getCredsBtn');
const getAllProjects = document.getElementById('getAllProjectsCreds').checked;
btn.disabled = true;
btn.textContent = getAllProjects ? '并发批量获取所有项目凭证中...' : '等待OAuth回调中...';
try {
if (getAllProjects) {
showStatus('正在并发为所有项目获取认证凭证,采用并发处理提升速度...', 'info');
} else {
showStatus('正在等待OAuth回调,这可能需要一些时间...', 'info');
}
const requestBody = {};
if (currentProjectId) {
requestBody.project_id = currentProjectId;
}
if (getAllProjects) {
requestBody.get_all_projects = true;
}
const response = await fetch('/auth/callback', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (response.ok) {
const credentialsSection = document.getElementById('credentialsSection');
const credentialsContent = document.getElementById('credentialsContent');
if (getAllProjects && data.multiple_credentials) {
// 处理多项目认证结果
const results = data.multiple_credentials;
let resultText = `批量并发认证完成!成功为 ${results.success.length} 个项目生成凭证:\n\n`;
// 显示成功的项目
results.success.forEach((item, index) => {
resultText += `${index + 1}. 项目: ${item.project_name} (${item.project_id})\n`;
resultText += ` 文件: ${item.file_path}\n\n`;
});
// 显示失败的项目(如果有)
if (results.failed.length > 0) {
resultText += `\n失败的项目 (${results.failed.length} 个):\n`;
results.failed.forEach((item, index) => {
resultText += `${index + 1}. 项目: ${item.project_name} (${item.project_id})\n`;
resultText += ` 错误: ${item.error}\n\n`;
});
}
credentialsContent.textContent = resultText;
showStatus(`✅ 批量并发认证完成!成功生成 ${results.success.length} 个项目的凭证文件${results.failed.length > 0 ? `,${results.failed.length} 个项目失败` : ''}`, 'success');
} else {
// 处理单项目认证结果
credentialsContent.textContent = JSON.stringify(data.credentials, null, 2);
if (data.auto_detected_project) {
showStatus(`✅ 认证成功!项目ID已自动检测为: ${data.credentials.project_id},文件已保存到: ${data.file_path}`, 'success');
} else {
showStatus(`✅ 认证成功!文件已保存到: ${data.file_path}`, 'success');
}
}
credentialsSection.classList.remove('hidden');
authInProgress = false;
} else {
// 检查是否需要项目选择
if (data.requires_project_selection && data.available_projects) {
let projectOptions = "请选择一个项目:\n\n";
data.available_projects.forEach((project, index) => {
projectOptions += `${index + 1}. ${project.name} (${project.projectId})\n`;
});
projectOptions += `\n请输入序号 (1-${data.available_projects.length}):`;
const selection = prompt(projectOptions);
const projectIndex = parseInt(selection) - 1;
if (projectIndex >= 0 && projectIndex < data.available_projects.length) {
const selectedProject = data.available_projects[projectIndex];
currentProjectId = selectedProject.projectId;
btn.textContent = '重新尝试获取认证文件';
showStatus(`使用选择的项目 ${selectedProject.name} (${selectedProject.projectId}) 重新尝试...`, 'info');
setTimeout(() => getCredentials(), 1000);
return;
} else {
showStatus('无效的选择,请重新开始认证', 'error');
}
}
// 检查是否需要手动输入项目ID
else if (data.requires_manual_project_id) {
const userProjectId = prompt('无法自动检测项目ID,请手动输入您的Google Cloud项目ID:');
if (userProjectId && userProjectId.trim()) {
// 重新尝试,使用用户输入的项目ID
currentProjectId = userProjectId.trim();
btn.textContent = '重新尝试获取认证文件';
showStatus('使用手动输入的项目ID重新尝试...', 'info');
setTimeout(() => getCredentials(), 1000);
return;
} else {
showStatus('需要项目ID才能完成认证,请重新开始并输入正确的项目ID', 'error');
}
} else {
showStatus(`❌ 错误: ${data.error || '获取认证文件失败'}`, 'error');
if (data.error && data.error.includes('未接收到授权回调')) {
showStatus('提示:请确保已完成浏览器中的OAuth认证,并看到了"OAuth authentication successful"页面', 'info');
}
}
}
} catch (error) {
showStatus(`网络错误: ${error.message}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = '获取认证文件';
}
}
// 文件上传相关函数
function handleFileSelect(event) {
const files = Array.from(event.target.files);
addFiles(files);
}
function addFiles(files) {
files.forEach(file => {
if (file.type === 'application/json' || file.name.endsWith('.json') ||
file.type === 'application/zip' || file.name.endsWith('.zip')) {
if (!uploadSelectedFiles.find(f => f.name === file.name && f.size === file.size)) {
uploadSelectedFiles.push(file);
}
} else {
showStatus(`文件 ${file.name} 格式不支持,只支持JSON和ZIP文件`, 'error');
}
});
updateFileList();
}
function updateFileList() {
const fileList = document.getElementById('fileList');
const fileListSection = document.getElementById('fileListSection');
if (uploadSelectedFiles.length === 0) {
fileListSection.classList.add('hidden');
return;
}
fileListSection.classList.remove('hidden');
fileList.innerHTML = '';
uploadSelectedFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
const isZip = file.name.endsWith('.zip');
const fileIcon = isZip ? '📦' : '📄';
const fileType = isZip ? ' (ZIP压缩包)' : ' (JSON文件)';
fileItem.innerHTML = `
<div>
<span class="file-name">${fileIcon} ${file.name}</span>
<span class="file-size">(${formatFileSize(file.size)}${fileType})</span>
</div>
<button class="remove-btn" onclick="removeFile(${index})">删除</button>
`;
fileList.appendChild(fileItem);
});
}
function removeFile(index) {
uploadSelectedFiles.splice(index, 1);
updateFileList();
}
function clearFiles() {
uploadSelectedFiles = [];
updateFileList();
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
return Math.round(bytes / (1024 * 1024)) + ' MB';
}
async function uploadFiles() {
if (uploadSelectedFiles.length === 0) {
showStatus('请选择要上传的文件', 'error');
return;
}
// 检查文件大小
const totalSize = uploadSelectedFiles.reduce((sum, file) => sum + file.size, 0);
const maxSize = 200 * 1024 * 1024; // 200MB limit
if (totalSize > maxSize) {
showStatus(`文件总大小 ${(totalSize / 1024 / 1024).toFixed(1)}MB 超过限制 ${maxSize / 1024 / 1024}MB。请分批上传或删除部分文件。`, 'error');
return;
}
// 检查单个文件大小
for (const file of uploadSelectedFiles) {
if (file.size > 5 * 1024 * 1024) {
showStatus(`文件 "${file.name}" 大小 ${(file.size / 1024 / 1024).toFixed(1)}MB 超过单文件5MB限制`, 'error');
return;
}
}
const progressSection = document.getElementById('uploadProgressSection');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
progressSection.classList.remove('hidden');
const formData = new FormData();
uploadSelectedFiles.forEach(file => {
formData.append('files', file);
});
// 检查是否有ZIP文件,给用户提示
const hasZipFiles = uploadSelectedFiles.some(file => file.name.endsWith('.zip'));
if (hasZipFiles) {
showStatus('正在上传并解压ZIP文件...', 'info');
}
try {
const xhr = new XMLHttpRequest();
// 设置超时时间 (5分钟)
xhr.timeout = 300000;
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
progressFill.style.width = percentComplete + '%';
progressText.textContent = Math.round(percentComplete) + '%';
}
};
xhr.onload = function () {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
showStatus(`成功上传 ${data.uploaded_count} 个文件`, 'success');
clearFiles();
progressSection.classList.add('hidden');
} catch (e) {
showStatus('上传失败: 服务器响应格式错误', 'error');
}
} else {
try {
const error = JSON.parse(xhr.responseText);
showStatus(`上传失败: ${error.detail || error.error || '未知错误'}`, 'error');
} catch (e) {
showStatus(`上传失败: HTTP ${xhr.status} - ${xhr.statusText || '未知错误'}`, 'error');
}
}
};
xhr.onerror = function () {
console.error('Upload XHR error:', {
readyState: xhr.readyState,
status: xhr.status,
statusText: xhr.statusText,
responseText: xhr.responseText,
fileCount: uploadSelectedFiles.length,
totalSize: (totalSize / 1024 / 1024).toFixed(1) + 'MB'
});
showStatus(`上传失败:连接中断 - 可能原因:文件过多(${uploadSelectedFiles.length}个)或网络不稳定。建议分批上传。`, 'error');
progressSection.classList.add('hidden');
};
xhr.ontimeout = function () {
showStatus('上传失败:请求超时 - 文件处理时间过长,请减少文件数量或检查网络连接', 'error');
progressSection.classList.add('hidden');
};
xhr.open('POST', '/auth/upload');
xhr.setRequestHeader('Authorization', `Bearer ${authToken}`);
xhr.send(formData);
} catch (error) {
showStatus(`上传失败: ${error.message}`, 'error');
}
}
// 拖拽功能
const uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', function (event) {
event.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function (event) {
event.preventDefault();
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', function (event) {
event.preventDefault();
uploadArea.classList.remove('dragover');
const files = Array.from(event.dataTransfer.files);
addFiles(files);
});
// WebSocket日志相关变量和函数
let logWebSocket = null;
let allLogs = [];
let filteredLogs = [];
let currentLogFilter = 'all';
function connectWebSocket() {
if (logWebSocket && logWebSocket.readyState === WebSocket.OPEN) {
showStatus('WebSocket已经连接', 'info');
return;
}
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/auth/logs/stream`;
document.getElementById('connectionStatusText').textContent = '连接中...';
document.getElementById('logConnectionStatus').className = 'status info';
logWebSocket = new WebSocket(wsUrl);
logWebSocket.onopen = function (event) {
document.getElementById('connectionStatusText').textContent = '已连接';
document.getElementById('logConnectionStatus').className = 'status success';
showStatus('日志流连接成功', 'success');
clearLogsDisplay(); // 只清空前端显示的旧日志,不清空服务器文件
};
logWebSocket.onmessage = function (event) {
const logLine = event.data;
if (logLine.trim()) {
allLogs.push(logLine);
// 限制日志数量,保留最后1000条
if (allLogs.length > 1000) {
allLogs = allLogs.slice(-1000);
}
filterLogs();
// 自动滚动到底部
if (document.getElementById('autoScroll').checked) {
const logContainer = document.getElementById('logContainer');
logContainer.scrollTop = logContainer.scrollHeight;
}
}
};
logWebSocket.onclose = function (event) {
document.getElementById('connectionStatusText').textContent = '连接断开';
document.getElementById('logConnectionStatus').className = 'status error';
showStatus('日志流连接断开', 'info');
};
logWebSocket.onerror = function (error) {
document.getElementById('connectionStatusText').textContent = '连接错误';
document.getElementById('logConnectionStatus').className = 'status error';
showStatus('日志流连接错误: ' + error, 'error');
};
} catch (error) {
showStatus('创建WebSocket连接失败: ' + error.message, 'error');
document.getElementById('connectionStatusText').textContent = '连接失败';
document.getElementById('logConnectionStatus').className = 'status error';
}
}
function disconnectWebSocket() {
if (logWebSocket) {
logWebSocket.close();
logWebSocket = null;
document.getElementById('connectionStatusText').textContent = '未连接';
document.getElementById('logConnectionStatus').className = 'status info';
showStatus('日志流连接已断开', 'info');
}
}
function clearLogsDisplay() {
// 只清空前端显示的日志,不清空服务器文件
allLogs = [];
filteredLogs = [];
document.getElementById('logContent').textContent = '日志已清空,等待新日志...';
}
async function downloadLogs() {
try {
// 调用后端API下载日志文件
const response = await fetch('/auth/logs/download', {
method: 'GET',
headers: getAuthHeaders()
});
if (response.ok) {
// 获取文件名
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'gcli2api_logs.txt';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename=(.+)/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showStatus(`日志文件下载成功: ${filename}`, 'success');
} else {
const errorText = await response.text();
let errorMsg = '下载失败';
try {
const errorData = JSON.parse(errorText);
errorMsg = errorData.detail || errorData.error || '未知错误';
} catch (e) {
errorMsg = errorText || '未知错误';
}
showStatus(`下载日志失败: ${errorMsg}`, 'error');
}
} catch (error) {
console.error('downloadLogs error:', error);
showStatus(`下载日志时网络错误: ${error.message}`, 'error');
}
}
async function clearLogs() {
try {
// 调用后端API清空日志文件
const response = await fetch('/auth/logs/clear', {
method: 'POST',
headers: getAuthHeaders()
});
const data = await response.json();
if (response.ok) {
// 清空前端显示的日志
clearLogsDisplay();
showStatus(data.message, 'success');
} else {
showStatus(`清空日志失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('clearLogs error:', error);
// 即使后端清空失败,也清空前端显示
clearLogsDisplay();
showStatus(`清空日志时网络错误: ${error.message}`, 'error');
}
}
function filterLogs() {
const filter = document.getElementById('logLevelFilter').value;
currentLogFilter = filter;
if (filter === 'all') {
filteredLogs = [...allLogs];
} else {
filteredLogs = allLogs.filter(log => log.toUpperCase().includes(filter));
}
displayLogs();
}
function displayLogs() {
const logContent = document.getElementById('logContent');
if (filteredLogs.length === 0) {
logContent.textContent = currentLogFilter === 'all' ?
'暂无日志...' : `暂无${currentLogFilter}级别的日志...`;
} else {
logContent.textContent = filteredLogs.join('\n');
}
}
// 凭证文件管理相关函数
async function refreshCredsStatus() {
const credsLoading = document.getElementById('credsLoading');
const credsList = document.getElementById('credsList');
try {
credsLoading.style.display = 'block';
credsList.innerHTML = '';
console.log('Fetching creds status...');
const response = await fetch('/creds/status', {
method: 'GET',
headers: getAuthHeaders()
});
console.log('Creds status response:', response.status);
const data = await response.json();
console.log('Creds status data:', data);
if (response.ok) {
credsData = data.creds;
// 计算统计数据
calculateStats();
// 更新统计显示
updateStatsDisplay();
// 应用筛选并显示第一页
currentPage = 1;
applyFilters();
showStatus(`已加载 ${Object.keys(credsData).length} 个凭证文件`, 'success');
} else {
showStatus(`加载失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('refreshCredsStatus error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
} finally {
credsLoading.style.display = 'none';
}
}
// 计算统计数据
function calculateStats() {
statsData = {
total: 0,
normal: 0,
disabled: 0
};
// 清空并重新收集错误码
availableErrorCodes.clear();
for (const [fullPath, credInfo] of Object.entries(credsData)) {
statsData.total++;
if (credInfo.status.disabled) {
statsData.disabled++;
} else {
statsData.normal++;
}
// 收集错误码信息
if (credInfo.status.error_codes && credInfo.status.error_codes.length > 0) {
credInfo.status.error_codes.forEach(code => {
availableErrorCodes.add(code);
});
}
}
// 更新错误码快速筛选按钮
updateErrorCodeBadges();
}
// 更新错误码筛选快速按钮
function updateErrorCodeBadges() {
const errorCodeBadges = document.getElementById('errorCodeBadges');
errorCodeBadges.innerHTML = '';
if (availableErrorCodes.size === 0) {
errorCodeBadges.innerHTML = '<span style="color: #28a745;">所有文件都无错误</span>';
return;
}
const sortedCodes = Array.from(availableErrorCodes).sort((a, b) => a - b);
sortedCodes.forEach(code => {
const badge = document.createElement('span');
badge.className = 'error-code-badge';
badge.textContent = code;
badge.onclick = () => filterByErrorCode(code);
errorCodeBadges.appendChild(badge);
});
}
// 按错误码快速筛选
function filterByErrorCode(code) {
document.getElementById('errorCodeFilter').value = code.toString();
applyFilters();
}
// 更新统计显示
function updateStatsDisplay() {
document.getElementById('statTotal').textContent = statsData.total;
document.getElementById('statNormal').textContent = statsData.normal;
document.getElementById('statDisabled').textContent = statsData.disabled;
}
// 应用筛选
function applyFilters() {
const statusFilter = document.getElementById('statusFilter').value;
const errorCodeFilter = document.getElementById('errorCodeFilter').value;
currentFilter = statusFilter;
currentErrorCodeFilter = errorCodeFilter;
filteredCredsData = {};
for (const [fullPath, credInfo] of Object.entries(credsData)) {
let shouldInclude = false;
// 状态筛选
switch (statusFilter) {
case 'all':
shouldInclude = true;
break;
case 'normal':
shouldInclude = !credInfo.status.disabled;
break;
case 'disabled':
shouldInclude = credInfo.status.disabled;
break;
}
// 如果状态筛选已经排除,跳过错误码筛选
if (!shouldInclude) {
continue;
}
// 错误码筛选
const errorCodes = credInfo.status.error_codes || [];
switch (errorCodeFilter) {
case 'all':
// 保持当前状态
break;
case 'no-errors':
shouldInclude = errorCodes.length === 0;
break;
case 'has-errors':
shouldInclude = errorCodes.length > 0;
break;
default:
// 具体错误码筛选
const targetCode = parseInt(errorCodeFilter);
if (!isNaN(targetCode)) {
shouldInclude = errorCodes.includes(targetCode);
}
break;
}
if (shouldInclude) {
filteredCredsData[fullPath] = credInfo;
}
}
// 清空选择状态
selectedCredFiles.clear();
updateBatchControls();
currentPage = 1;
renderCredsList();
updatePagination();
}
// 获取当前页数据
function getCurrentPageData() {
const filteredEntries = Object.entries(filteredCredsData);
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filteredEntries.slice(startIndex, endIndex);
}
// 获取总页数
function getTotalPages() {
return Math.ceil(Object.keys(filteredCredsData).length / pageSize);
}
// 渲染凭证列表
function renderCredsList() {
const credsList = document.getElementById('credsList');
credsList.innerHTML = '';
const currentPageData = getCurrentPageData();
if (currentPageData.length === 0) {
const message = Object.keys(credsData).length === 0 ?
'暂无凭证文件' : '当前筛选条件下暂无数据';
credsList.innerHTML = `<p style="text-align: center; color: #666;">${message}</p>`;
document.getElementById('paginationContainer').style.display = 'none';
return;
}
for (const [fullPath, credInfo] of currentPageData) {
const card = createCredCard(fullPath, credInfo);
credsList.appendChild(card);
}
document.getElementById('paginationContainer').style.display = getTotalPages() > 1 ? 'flex' : 'none';
// 更新批量控件状态
updateBatchControls();
}
// 更新分页信息
function updatePagination() {
const totalPages = getTotalPages();
const totalItems = Object.keys(filteredCredsData).length;
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, totalItems);
document.getElementById('paginationInfo').textContent =
`第 ${currentPage} 页,共 ${totalPages} 页 (显示 ${startItem}-${endItem},共 ${totalItems} 项)`;
document.getElementById('prevPageBtn').disabled = currentPage <= 1;
document.getElementById('nextPageBtn').disabled = currentPage >= totalPages;
}
// 切换页面
function changePage(direction) {
const totalPages = getTotalPages();
const newPage = currentPage + direction;
if (newPage >= 1 && newPage <= totalPages) {
currentPage = newPage;
renderCredsList();
updatePagination();
}
}
// 改变每页显示数量
function changePageSize() {
pageSize = parseInt(document.getElementById('pageSizeSelect').value);
currentPage = 1;
renderCredsList();
updatePagination();
}
function createCredCard(fullPath, credInfo) {
const div = document.createElement('div');
const status = credInfo.status;
const filename = credInfo.filename;
// 调试:记录状态
if (filename.includes('atomic-affinity')) {
console.log(`Creating card for ${filename}:`, status);
}
// 设置卡片状态样式
let cardClass = 'cred-card';
if (status.disabled) cardClass += ' disabled';
div.className = cardClass;
// 创建状态标签
let statusBadges = '';
if (status.disabled) {
statusBadges += '<span class="status-badge disabled">已禁用</span>';
} else {
statusBadges += '<span class="status-badge enabled">已启用</span>';
}
// 调试:记录 error_codes
console.log(`Error codes for ${filename}:`, status.error_codes);
if (status.error_codes && status.error_codes.length > 0) {
statusBadges += `<span class="error-codes">错误码: ${status.error_codes.join(', ')}</span>`;
// 检查是否包含自动封禁的错误码
const autoBanErrors = status.error_codes.filter(code => code === 400 || code === 403);
if (autoBanErrors.length > 0 && status.disabled) {
statusBadges += `<span class="status-badge" style="background-color: #e74c3c; color: white;">AUTO_BAN</span>`;
}
} else {
// 显示无错误码状态
statusBadges += `<span class="status-badge" style="background-color: #28a745; color: white;">无错误</span>`;
}
// 为HTML ID生成安全的标识符
const pathId = btoa(encodeURIComponent(fullPath)).replace(/[+/=]/g, '_');
// 创建操作按钮 - 使用文件名而不是完整路径
let actionButtons = '';
if (status.disabled) {
actionButtons += `<button class="cred-btn enable" data-filename="${filename}" data-action="enable">启用</button>`;
} else {
actionButtons += `<button class="cred-btn disable" data-filename="${filename}" data-action="disable">禁用</button>`;
}
actionButtons += `
<button class="cred-btn view" onclick="toggleCredDetails('${pathId}')">查看内容</button>
<button class="cred-btn download" onclick="downloadCred('${filename}')">下载</button>
<button class="cred-btn email" onclick="fetchUserEmail('${filename}')">查看账号邮箱</button>
<button class="cred-btn delete" data-filename="${filename}" data-action="delete">删除</button>
`;
// 构建邮箱显示
let emailInfo = '';
if (credInfo.user_email) {
emailInfo = `<div class="cred-email" style="font-size: 12px; color: #666; margin-top: 2px;">${credInfo.user_email}</div>`;
} else {
emailInfo = `<div class="cred-email" style="font-size: 12px; color: #999; margin-top: 2px; font-style: italic;">未获取邮箱</div>`;
}
div.innerHTML = `
<div class="cred-header">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" class="file-checkbox" data-filename="${filename}" onchange="toggleFileSelection('${filename}')">
<div>
<div class="cred-filename">${filename}</div>
${emailInfo}
</div>
</div>
<div class="cred-status">${statusBadges}</div>
</div>
<div class="cred-actions">${actionButtons}</div>
<div class="cred-details" id="details-${pathId}">
<div class="cred-content"></div>
</div>
`;
// 设置文件内容(避免HTML注入)
const contentDiv = div.querySelector('.cred-content');
if (credInfo.content) {
contentDiv.textContent = JSON.stringify(credInfo.content, null, 2);
} else {
contentDiv.textContent = credInfo.error || '无法读取文件内容';
}
// 添加事件监听器到按钮
const actionButtonElements = div.querySelectorAll('[data-filename][data-action]');
actionButtonElements.forEach(button => {
button.addEventListener('click', function () {
const filename = this.getAttribute('data-filename');
const action = this.getAttribute('data-action');
if (action === 'delete') {
deleteCred(filename);
} else {
credAction(filename, action);
}
});
});
return div;
}
async function credAction(filename, action) {
try {
console.log('Performing action:', action, 'on file:', filename);
console.log('Filename type:', typeof filename);
console.log('Filename length:', filename.length);
console.log('Ends with .json:', filename.endsWith('.json'));
const requestBody = {
filename: filename,
action: action
};
console.log('Request body:', requestBody);
const response = await fetch('/creds/action', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(requestBody)
});
console.log('Response status:', response.status);
const data = await response.json();
console.log('Response data:', data);
if (response.ok) {
showStatus(data.message, 'success');
await refreshCredsStatus(); // 刷新状态
} else {
showStatus(`操作失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('credAction error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
function toggleCredDetails(pathId) {
const detailsId = 'details-' + pathId;
const details = document.getElementById(detailsId);
if (details) {
details.classList.toggle('show');
}
}
async function downloadCred(filename) {
try {
const response = await fetch(`/creds/download/${filename}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showStatus(`已下载文件: ${filename}`, 'success');
} else {
const data = await response.json();
showStatus(`下载失败: ${data.error}`, 'error');
}
} catch (error) {
showStatus(`下载失败: ${error.message}`, 'error');
}
}
async function downloadAllCreds() {
try {
const response = await fetch('/creds/download-all', {
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'credentials.zip';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showStatus('已下载所有凭证文件', 'success');
} else {
const data = await response.json();
showStatus(`打包下载失败: ${data.error}`, 'error');
}
} catch (error) {
showStatus(`打包下载失败: ${error.message}`, 'error');
}
}
async function deleteCred(filename) {
if (!confirm(`确定要删除凭证文件吗?\n${filename}`)) {
return;
}
await credAction(filename, 'delete');
}
// 配置管理相关函数
let currentConfig = {};
let envLockedFields = new Set();
async function loadConfig() {
const configLoading = document.getElementById('configLoading');
const configForm = document.getElementById('configForm');
try {
configLoading.style.display = 'block';
configForm.classList.add('hidden');
const response = await fetch('/config/get', {
method: 'GET',
headers: getAuthHeaders()
});
const data = await response.json();
if (response.ok) {
currentConfig = data.config;
envLockedFields = new Set(data.env_locked || []);
populateConfigForm();
configForm.classList.remove('hidden');
showStatus('配置加载成功', 'success');
} else {
showStatus(`加载配置失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('loadConfig error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
} finally {
configLoading.style.display = 'none';
}
}
function populateConfigForm() {
// 服务器配置
setConfigField('host', currentConfig.host || '0.0.0.0');
setConfigField('port', currentConfig.port || 7861);
setConfigField('configApiPassword', currentConfig.api_password || '');
setConfigField('configPanelPassword', currentConfig.panel_password || '');
setConfigField('configPassword', currentConfig.password || 'pwd');
// 基础配置
setConfigField('credentialsDir', currentConfig.credentials_dir || '');
setConfigField('proxy', currentConfig.proxy || '');
// 端点配置
setConfigField('codeAssistEndpoint', currentConfig.code_assist_endpoint || '');
setConfigField('oauthProxyUrl', currentConfig.oauth_proxy_url || '');
setConfigField('googleapisProxyUrl', currentConfig.googleapis_proxy_url || '');
setConfigField('resourceManagerApiUrl', currentConfig.resource_manager_api_url || '');
setConfigField('serviceUsageApiUrl', currentConfig.service_usage_api_url || '');
// 自动封禁配置
document.getElementById('autoBanEnabled').checked = Boolean(currentConfig.auto_ban_enabled);
setConfigField('autoBanErrorCodes', (currentConfig.auto_ban_error_codes || []).join(','));
// 性能配置
setConfigField('callsPerRotation', currentConfig.calls_per_rotation || 10);
// 429重试配置
document.getElementById('retry429Enabled').checked = Boolean(currentConfig.retry_429_enabled);
setConfigField('retry429MaxRetries', currentConfig.retry_429_max_retries || 20);
setConfigField('retry429Interval', currentConfig.retry_429_interval || 0.1);
// 兼容性配置
document.getElementById('compatibilityModeEnabled').checked = Boolean(currentConfig.compatibility_mode_enabled);
// 抗截断配置
setConfigField('antiTruncationMaxAttempts', currentConfig.anti_truncation_max_attempts || 3);
}
function setConfigField(fieldId, value) {
const field = document.getElementById(fieldId);
if (field) {
field.value = value;
// 检查是否被环境变量锚定
const configKey = fieldId.replace(/([A-Z])/g, '_$1').toLowerCase();
if (envLockedFields.has(configKey)) {
field.disabled = true;
field.classList.add('env-locked');
} else {
field.disabled = false;
field.classList.remove('env-locked');
}
}
}
async function saveConfig() {
try {
// 调试:检查password字段的实际值
const passwordElement = document.getElementById('configPassword');
console.log('DEBUG: configPassword元素:', passwordElement);
console.log('DEBUG: configPassword值:', passwordElement ? passwordElement.value : 'ELEMENT_NOT_FOUND');
const config = {
host: document.getElementById('host').value.trim(),
port: parseInt(document.getElementById('port').value) || 7861,
api_password: document.getElementById('configApiPassword').value.trim(),
panel_password: document.getElementById('configPanelPassword').value.trim(),
password: document.getElementById('configPassword').value.trim(),
code_assist_endpoint: document.getElementById('codeAssistEndpoint').value.trim(),
credentials_dir: document.getElementById('credentialsDir').value.trim(),
proxy: document.getElementById('proxy').value.trim(),
// 端点配置
oauth_proxy_url: document.getElementById('oauthProxyUrl').value.trim(),
googleapis_proxy_url: document.getElementById('googleapisProxyUrl').value.trim(),
resource_manager_api_url: document.getElementById('resourceManagerApiUrl').value.trim(),
service_usage_api_url: document.getElementById('serviceUsageApiUrl').value.trim(),
auto_ban_enabled: document.getElementById('autoBanEnabled').checked,
auto_ban_error_codes: document.getElementById('autoBanErrorCodes').value
.split(',')
.map(code => parseInt(code.trim()))
.filter(code => !isNaN(code)),
calls_per_rotation: parseInt(document.getElementById('callsPerRotation').value) || 10,
retry_429_enabled: document.getElementById('retry429Enabled').checked,
retry_429_max_retries: parseInt(document.getElementById('retry429MaxRetries').value) || 20,
retry_429_interval: parseFloat(document.getElementById('retry429Interval').value) || 0.1,
// 兼容性配置
compatibility_mode_enabled: document.getElementById('compatibilityModeEnabled').checked,
// 抗截断配置
anti_truncation_max_attempts: parseInt(document.getElementById('antiTruncationMaxAttempts').value) || 3
};
const response = await fetch('/config/save', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ config: config })
});
const data = await response.json();
if (response.ok) {
let message = '配置保存成功';
// 处理热更新状态信息
if (data.hot_updated && data.hot_updated.length > 0) {
message += `,以下配置已立即生效: ${data.hot_updated.join(', ')}`;
}
// 处理重启提醒
if (data.restart_required && data.restart_required.length > 0) {
message += `\n⚠️ 重启提醒: ${data.restart_notice}`;
showStatus(message, 'info');
} else {
showStatus(message, 'success');
}
// 重新加载配置以获取最新状态
setTimeout(() => loadConfig(), 1000);
} else {
showStatus(`保存配置失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('saveConfig error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
// 环境变量凭证管理相关函数
async function checkEnvCredsStatus() {
const envStatusLoading = document.getElementById('envStatusLoading');
const envStatusContent = document.getElementById('envStatusContent');
try {
envStatusLoading.style.display = 'block';
envStatusContent.classList.add('hidden');
const response = await fetch('/auth/env-creds-status', {
method: 'GET',
headers: getAuthHeaders()
});
const data = await response.json();
if (response.ok) {
// 更新环境变量列表
const envVarsList = document.getElementById('envVarsList');
if (Object.keys(data.available_env_vars).length > 0) {
envVarsList.textContent = Object.keys(data.available_env_vars).join(', ');
} else {
envVarsList.textContent = '未找到GCLI_CREDS_*环境变量';
}
// 更新自动加载状态
const autoLoadStatus = document.getElementById('autoLoadStatus');
autoLoadStatus.textContent = data.auto_load_enabled ? '✅ 已启用' : '❌ 未启用';
autoLoadStatus.style.color = data.auto_load_enabled ? '#28a745' : '#dc3545';
// 更新已导入文件统计
const envFilesCount = document.getElementById('envFilesCount');
envFilesCount.textContent = `${data.existing_env_files_count} 个文件`;
const envFilesList = document.getElementById('envFilesList');
if (data.existing_env_files.length > 0) {
envFilesList.textContent = data.existing_env_files.join(', ');
} else {
envFilesList.textContent = '无';
}
envStatusContent.classList.remove('hidden');
showStatus('环境变量状态检查完成', 'success');
} else {
showStatus(`获取环境变量状态失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('checkEnvCredsStatus error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
} finally {
envStatusLoading.style.display = 'none';
}
}
async function loadEnvCredentials() {
try {
showStatus('正在从环境变量导入凭证...', 'info');
const response = await fetch('/auth/load-env-creds', {
method: 'POST',
headers: getAuthHeaders()
});
const data = await response.json();
if (response.ok) {
if (data.loaded_count > 0) {
showStatus(`✅ 成功导入 ${data.loaded_count}/${data.total_count} 个凭证文件`, 'success');
// 刷新状态
setTimeout(() => checkEnvCredsStatus(), 1000);
} else {
showStatus(`⚠️ ${data.message}`, 'info');
}
} else {
showStatus(`导入失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('loadEnvCredentials error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
async function clearEnvCredentials() {
if (!confirm('确定要清除所有从环境变量导入的凭证文件吗?\n这将删除所有文件名以 "env-" 开头的认证文件。')) {
return;
}
try {
showStatus('正在清除环境变量凭证文件...', 'info');
const response = await fetch('/auth/env-creds', {
method: 'DELETE',
headers: getAuthHeaders()
});
const data = await response.json();
if (response.ok) {
showStatus(`✅ 成功删除 ${data.deleted_count} 个环境变量凭证文件`, 'success');
// 刷新状态
setTimeout(() => checkEnvCredsStatus(), 1000);
} else {
showStatus(`清除失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('clearEnvCredentials error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
// 批量操作相关函数
function toggleFileSelection(filename) {
if (selectedCredFiles.has(filename)) {
selectedCredFiles.delete(filename);
} else {
selectedCredFiles.add(filename);
}
updateBatchControls();
}
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const fileCheckboxes = document.querySelectorAll('.file-checkbox');
if (selectAllCheckbox.checked) {
// 全选当前页面的文件
fileCheckboxes.forEach(checkbox => {
const filename = checkbox.getAttribute('data-filename');
selectedCredFiles.add(filename);
checkbox.checked = true;
});
} else {
// 取消全选
selectedCredFiles.clear();
fileCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
}
updateBatchControls();
}
function updateBatchControls() {
const selectedCount = selectedCredFiles.size;
const selectedCountElement = document.getElementById('selectedCount');
const batchEnableBtn = document.getElementById('batchEnableBtn');
const batchDisableBtn = document.getElementById('batchDisableBtn');
const batchDeleteBtn = document.getElementById('batchDeleteBtn');
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
selectedCountElement.textContent = `已选择 ${selectedCount} 项`;
// 启用/禁用批量操作按钮
const hasSelection = selectedCount > 0;
batchEnableBtn.disabled = !hasSelection;
batchDisableBtn.disabled = !hasSelection;
batchDeleteBtn.disabled = !hasSelection;
// 更新全选复选框状态
const currentPageFileCount = document.querySelectorAll('.file-checkbox').length;
const currentPageSelectedCount = Array.from(document.querySelectorAll('.file-checkbox'))
.filter(checkbox => selectedCredFiles.has(checkbox.getAttribute('data-filename'))).length;
if (currentPageSelectedCount === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (currentPageSelectedCount === currentPageFileCount) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
selectAllCheckbox.checked = false;
}
// 更新页面上的复选框状态
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
const filename = checkbox.getAttribute('data-filename');
checkbox.checked = selectedCredFiles.has(filename);
});
}
async function batchAction(action) {
const selectedFiles = Array.from(selectedCredFiles);
if (selectedFiles.length === 0) {
showStatus('请先选择要操作的文件', 'error');
return;
}
let confirmMessage = '';
switch (action) {
case 'enable':
confirmMessage = `确定要启用选中的 ${selectedFiles.length} 个文件吗?`;
break;
case 'disable':
confirmMessage = `确定要禁用选中的 ${selectedFiles.length} 个文件吗?`;
break;
case 'delete':
confirmMessage = `确定要删除选中的 ${selectedFiles.length} 个文件吗?\n注意:此操作不可恢复!`;
break;
}
if (!confirm(confirmMessage)) {
return;
}
try {
showStatus(`正在执行批量${action === 'enable' ? '启用' : action === 'disable' ? '禁用' : '删除'}操作...`, 'info');
const requestBody = {
action: action,
filenames: selectedFiles
};
const response = await fetch('/creds/batch-action', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (response.ok) {
showStatus(`批量操作完成:成功处理 ${data.success_count}/${selectedFiles.length} 个文件`, 'success');
// 清空选择
selectedCredFiles.clear();
updateBatchControls();
// 刷新列表
await refreshCredsStatus();
} else {
showStatus(`批量操作失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('batchAction error:', error);
showStatus(`批量操作网络错误: ${error.message}`, 'error');
}
}
// 邮箱相关函数
async function fetchUserEmail(filename) {
try {
showStatus('正在获取用户邮箱...', 'info');
const response = await fetch(`/creds/fetch-email/${encodeURIComponent(filename)}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.user_email) {
showStatus(`成功获取邮箱: ${data.user_email}`, 'success');
// 刷新凭证状态以更新显示
await refreshCredsStatus();
} else {
showStatus(data.message || '无法获取用户邮箱', 'error');
}
} catch (error) {
console.error('fetchUserEmail error:', error);
showStatus(`获取邮箱失败: ${error.message}`, 'error');
}
}
async function refreshAllEmails() {
try {
if (!confirm('确定要刷新所有凭证的用户邮箱吗?这可能需要一些时间。')) {
return;
}
showStatus('正在刷新所有用户邮箱...', 'info');
const response = await fetch('/creds/refresh-all-emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showStatus(`邮箱刷新完成:成功获取 ${data.success_count}/${data.total_count} 个邮箱地址`, 'success');
// 刷新凭证状态以更新显示
await refreshCredsStatus();
} else {
showStatus(data.message || '邮箱刷新失败', 'error');
}
} catch (error) {
console.error('refreshAllEmails error:', error);
showStatus(`邮箱刷新网络错误: ${error.message}`, 'error');
}
}
// Project ID 折叠切换函数
function toggleProjectIdSection() {
const section = document.getElementById('projectIdSection');
const icon = document.getElementById('projectIdToggleIcon');
if (section.style.display === 'none') {
section.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
icon.textContent = '▼';
} else {
section.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
icon.textContent = '▶';
}
}
// 回调URL输入区域折叠切换函数
function toggleCallbackUrlSection() {
const section = document.getElementById('callbackUrlSection');
const icon = document.getElementById('callbackUrlToggleIcon');
if (section.style.display === 'none') {
section.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
icon.textContent = '▲';
} else {
section.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
icon.textContent = '▼';
}
}
// 处理回调URL的函数
async function processCallbackUrl() {
const callbackUrlInput = document.getElementById('callbackUrlInput');
const callbackUrl = callbackUrlInput.value.trim();
const getAllProjects = document.getElementById('getAllProjectsCreds').checked;
if (!callbackUrl) {
showStatus('请输入回调URL', 'error');
return;
}
// 简单验证URL格式
if (!callbackUrl.startsWith('http://') && !callbackUrl.startsWith('https://')) {
showStatus('请输入有效的URL(以http://或https://开头)', 'error');
return;
}
// 检查是否包含必要参数
if (!callbackUrl.includes('code=') || !callbackUrl.includes('state=')) {
showStatus('❌ 这不是有效的回调URL!请确保:\n1. 已完成Google OAuth授权\n2. 复制的是浏览器地址栏的完整URL\n3. URL包含code和state参数', 'error');
return;
}
if (getAllProjects) {
showStatus('正在从回调URL并发批量获取所有项目凭证...', 'info');
} else {
showStatus('正在从回调URL获取凭证...', 'info');
}
try {
// 获取当前项目ID设置(如果有的话)
const projectIdInput = document.getElementById('projectId');
const projectId = projectIdInput ? projectIdInput.value.trim() : null;
const response = await fetch('/auth/callback-url', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
callback_url: callbackUrl,
project_id: projectId || null,
get_all_projects: getAllProjects
})
});
const result = await response.json();
if (getAllProjects && result.multiple_credentials) {
// 处理多项目认证结果
const results = result.multiple_credentials;
let resultText = `批量并发认证完成!成功为 ${results.success.length} 个项目生成凭证:\n\n`;
// 显示成功的项目
results.success.forEach((item, index) => {
resultText += `${index + 1}. 项目: ${item.project_name} (${item.project_id})\n`;
resultText += ` 文件: ${item.file_path}\n\n`;
});
// 显示失败的项目(如果有)
if (results.failed.length > 0) {
resultText += `\n失败的项目 (${results.failed.length} 个):\n`;
results.failed.forEach((item, index) => {
resultText += `${index + 1}. 项目: ${item.project_name} (${item.project_id})\n`;
resultText += ` 错误: ${item.error}\n\n`;
});
}
// 显示结果
document.getElementById('credentialsContent').textContent = resultText;
document.getElementById('credentialsSection').classList.remove('hidden');
showStatus(`✅ 批量并发认证完成!成功生成 ${results.success.length} 个项目的凭证文件${results.failed.length > 0 ? `,${results.failed.length} 个项目失败` : ''}`, 'success');
} else if (result.credentials) {
// 处理单项目认证结果
showStatus(result.message || '从回调URL获取凭证成功!', 'success');
// 显示凭证内容
document.getElementById('credentialsContent').innerHTML =
'<pre>' + JSON.stringify(result.credentials, null, 2) + '</pre>';
document.getElementById('credentialsSection').classList.remove('hidden');
} else if (result.requires_manual_project_id) {
showStatus('需要手动指定项目ID,请在高级选项中填入Google Cloud项目ID后重试', 'error');
} else if (result.requires_project_selection) {
let projectOptions = '<br><strong>可用项目:</strong><br>';
result.available_projects.forEach(project => {
projectOptions += `• ${project.name} (ID: ${project.projectId})<br>`;
});
showStatus('检测到多个项目,请在高级选项中指定项目ID:' + projectOptions, 'error');
} else {
showStatus(result.error || '从回调URL获取凭证失败', 'error');
}
// 清空输入框
callbackUrlInput.value = '';
// 刷新凭证列表(如果有)
setTimeout(() => {
if (typeof refreshCredsStatus === 'function') {
refreshCredsStatus();
}
}, 1000);
} catch (error) {
console.error('从回调URL获取凭证时出错:', error);
showStatus(`从回调URL获取凭证失败: ${error.message}`, 'error');
}
}
// 处理勾选框状态变化
function handleGetAllProjectsChange() {
const checkbox = document.getElementById('getAllProjectsCreds');
const note = document.getElementById('allProjectsNote');
const projectIdSection = document.getElementById('projectIdSection');
const projectIdToggle = document.querySelector('[onclick="toggleProjectIdSection()"]');
if (checkbox.checked) {
// 显示批量认证提示
note.style.display = 'block';
// 禁用项目ID输入(批量模式下不需要指定单个项目)
if (projectIdSection.style.display !== 'none') {
toggleProjectIdSection();
}
projectIdToggle.style.opacity = '0.5';
projectIdToggle.style.pointerEvents = 'none';
projectIdToggle.title = '批量认证模式下无需指定单个项目ID';
} else {
// 隐藏批量认证提示
note.style.display = 'none';
// 重新启用项目ID输入
projectIdToggle.style.opacity = '1';
projectIdToggle.style.pointerEvents = 'auto';
projectIdToggle.title = '';
}
}
// 页面加载时检查状态
window.onload = function () {
console.log('Page loaded');
console.log('Login section exists:', !!document.getElementById('loginSection'));
console.log('Main section exists:', !!document.getElementById('mainSection'));
console.log('Status section exists:', !!document.getElementById('statusSection'));
// 添加勾选框事件监听器
const checkbox = document.getElementById('getAllProjectsCreds');
if (checkbox) {
checkbox.addEventListener('change', handleGetAllProjectsChange);
}
showStatus('请输入密码登录', 'info');
};
// =====================================================================
// 使用统计相关函数
// =====================================================================
let usageStatsData = {};
let currentEditingFile = '';
// 刷新使用统计
async function refreshUsageStats() {
const usageLoading = document.getElementById('usageLoading');
const usageList = document.getElementById('usageList');
try {
usageLoading.style.display = 'block';
usageList.innerHTML = '';
// 获取所有文件的使用统计
const [statsResponse, aggregatedResponse] = await Promise.all([
fetch('/usage/stats', {
method: 'GET',
headers: getAuthHeaders()
}),
fetch('/usage/aggregated', {
method: 'GET',
headers: getAuthHeaders()
})
]);
const statsData = await statsResponse.json();
const aggregatedData = await aggregatedResponse.json();
if (statsResponse.ok && aggregatedResponse.ok) {
usageStatsData = statsData.data;
// 更新概览统计
document.getElementById('totalApiCalls').textContent = aggregatedData.data.total_all_model_calls || 0;
document.getElementById('geminiProCalls').textContent = aggregatedData.data.total_gemini_2_5_pro_calls || 0;
document.getElementById('totalFiles').textContent = aggregatedData.data.total_files || 0;
// 渲染使用统计列表
renderUsageList();
showStatus(`已加载 ${aggregatedData.data.total_files} 个文件的使用统计`, 'success');
} else {
showStatus('加载使用统计失败', 'error');
}
} catch (error) {
console.error('refreshUsageStats error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
} finally {
usageLoading.style.display = 'none';
}
}
// 渲染使用统计列表
function renderUsageList() {
const usageList = document.getElementById('usageList');
usageList.innerHTML = '';
if (Object.keys(usageStatsData).length === 0) {
usageList.innerHTML = '<p style="text-align: center; color: #666;">暂无使用统计数据</p>';
return;
}
for (const [filename, stats] of Object.entries(usageStatsData)) {
const card = createUsageCard(filename, stats);
usageList.appendChild(card);
}
}
// 创建使用统计卡片
function createUsageCard(filename, stats) {
const div = document.createElement('div');
div.className = 'usage-card';
// 计算使用百分比
const geminiPercent = Math.min((stats.gemini_2_5_pro_calls / stats.daily_limit_gemini_2_5_pro) * 100, 100);
const totalPercent = Math.min((stats.total_calls / stats.daily_limit_total) * 100, 100);
// 确定进度条颜色
function getProgressClass(percent) {
if (percent >= 90) return 'danger';
if (percent >= 70) return 'warning';
return 'gemini';
}
function getTotalProgressClass(percent) {
if (percent >= 90) return 'danger';
if (percent >= 70) return 'warning';
return 'total';
}
// 格式化时间
function formatTime(isoString) {
if (!isoString) return '未知';
try {
const date = new Date(isoString);
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
} catch (e) {
return '格式错误';
}
}
div.innerHTML = `
<div class="usage-header">
<div class="usage-filename">${filename}</div>
</div>
<div class="usage-progress">
<div class="usage-progress-label">
<span>Gemini 2.5 Pro</span>
<span>${stats.gemini_2_5_pro_calls}/${stats.daily_limit_gemini_2_5_pro} (${geminiPercent.toFixed(1)}%)</span>
</div>
<div class="usage-progress-bar">
<div class="usage-progress-fill ${getProgressClass(geminiPercent)}" style="width: ${geminiPercent}%"></div>
</div>
</div>
<div class="usage-progress">
<div class="usage-progress-label">
<span>所有模型</span>
<span>${stats.total_calls}/${stats.daily_limit_total} (${totalPercent.toFixed(1)}%)</span>
</div>
<div class="usage-progress-bar">
<div class="usage-progress-fill ${getTotalProgressClass(totalPercent)}" style="width: ${totalPercent}%"></div>
</div>
</div>
<div class="usage-info">
<div class="usage-info-item" style="grid-column: 1 / -1;">
<span class="usage-info-label">下次重置时间</span>
<span class="usage-info-value">${formatTime(stats.next_reset_time)}</span>
</div>
</div>
<div class="usage-actions">
<button class="usage-btn limits" onclick="openLimitsModal('${filename}')">设置限制</button>
<button class="usage-btn reset" onclick="resetSingleUsageStats('${filename}')">重置统计</button>
</div>
`;
return div;
}
// 打开限制设置弹窗
function openLimitsModal(filename) {
const stats = usageStatsData[filename];
if (!stats) {
showStatus('找不到文件统计数据', 'error');
return;
}
currentEditingFile = filename;
document.getElementById('modalFilename').value = filename;
document.getElementById('modalGeminiLimit').value = stats.daily_limit_gemini_2_5_pro;
document.getElementById('modalTotalLimit').value = stats.daily_limit_total;
document.getElementById('limitsModal').style.display = 'block';
}
// 关闭限制设置弹窗
function closeLimitsModal() {
document.getElementById('limitsModal').style.display = 'none';
currentEditingFile = '';
}
// 保存限制设置
async function saveLimits() {
const geminiLimit = parseInt(document.getElementById('modalGeminiLimit').value);
const totalLimit = parseInt(document.getElementById('modalTotalLimit').value);
if (isNaN(geminiLimit) || geminiLimit < 1) {
showStatus('Gemini 2.5 Pro 限制必须是大于0的数字', 'error');
return;
}
if (isNaN(totalLimit) || totalLimit < 1) {
showStatus('总调用限制必须是大于0的数字', 'error');
return;
}
try {
const response = await fetch('/usage/update-limits', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
filename: currentEditingFile,
gemini_2_5_pro_limit: geminiLimit,
total_limit: totalLimit
})
});
const data = await response.json();
if (response.ok) {
showStatus(data.message, 'success');
closeLimitsModal();
// 刷新统计数据
await refreshUsageStats();
} else {
showStatus(`设置失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('saveLimits error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
// 重置单个文件的使用统计
async function resetSingleUsageStats(filename) {
if (!confirm(`确定要重置 ${filename} 的使用统计吗?`)) {
return;
}
try {
const response = await fetch('/usage/reset', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ filename: filename })
});
const data = await response.json();
if (response.ok) {
showStatus(data.message, 'success');
await refreshUsageStats();
} else {
showStatus(`重置失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('resetSingleUsageStats error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
// 重置所有使用统计
async function resetAllUsageStats() {
if (!confirm('确定要重置所有文件的使用统计吗?此操作不可恢复!')) {
return;
}
try {
const response = await fetch('/usage/reset', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({}) // 不提供filename表示重置所有
});
const data = await response.json();
if (response.ok) {
showStatus(data.message, 'success');
await refreshUsageStats();
} else {
showStatus(`重置失败: ${data.detail || data.error || '未知错误'}`, 'error');
}
} catch (error) {
console.error('resetAllUsageStats error:', error);
showStatus(`网络错误: ${error.message}`, 'error');
}
}
// 关闭弹窗当点击弹窗外部时
window.onclick = function (event) {
const modal = document.getElementById('limitsModal');
if (event.target == modal) {
closeLimitsModal();
}
}
// =====================================================================
// 端点配置快速切换函数
// =====================================================================
// 镜像网址配置
const mirrorUrls = {
codeAssistEndpoint: 'https://gcli-api.sukaka.top/cloudcode-pa',
oauthProxyUrl: 'https://gcli-api.sukaka.top/oauth2',
googleapisProxyUrl: 'https://gcli-api.sukaka.top/googleapis',
resourceManagerApiUrl: 'https://gcli-api.sukaka.top/cloudresourcemanager',
serviceUsageApiUrl: 'https://gcli-api.sukaka.top/serviceusage'
};
// 官方端点配置
const officialUrls = {
codeAssistEndpoint: 'https://cloudcode-pa.googleapis.com',
oauthProxyUrl: 'https://oauth2.googleapis.com',
googleapisProxyUrl: 'https://www.googleapis.com',
resourceManagerApiUrl: 'https://cloudresourcemanager.googleapis.com',
serviceUsageApiUrl: 'https://serviceusage.googleapis.com'
};
// 使用镜像网址
function useMirrorUrls() {
if (confirm('确定要将所有端点配置为镜像网址吗?\n\n镜像网址:\n• Code Assist: https://gcli-api.sukaka.top/cloudcode-pa\n• OAuth: https://gcli-api.sukaka.top/oauth2\n• Google APIs: https://gcli-api.sukaka.top/googleapis\n• Resource Manager: https://gcli-api.sukaka.top/cloudresourcemanager\n• Service Usage: https://gcli-api.sukaka.top/serviceusage')) {
// 设置所有端点为镜像网址
for (const [fieldId, url] of Object.entries(mirrorUrls)) {
const field = document.getElementById(fieldId);
if (field && !field.disabled) {
field.value = url;
}
}
showStatus('✅ 已切换到镜像网址配置,记得点击"保存配置"按钮保存设置', 'success');
}
}
// 还原官方端点
function restoreOfficialUrls() {
if (confirm('确定要将所有端点配置为官方地址吗?\n\n官方端点:\n• Code Assist: https://cloudcode-pa.googleapis.com\n• OAuth: https://oauth2.googleapis.com\n• Google APIs: https://www.googleapis.com\n• Resource Manager: https://cloudresourcemanager.googleapis.com\n• Service Usage: https://serviceusage.googleapis.com')) {
// 设置所有端点为官方地址
for (const [fieldId, url] of Object.entries(officialUrls)) {
const field = document.getElementById(fieldId);
if (field && !field.disabled) {
field.value = url;
}
}
showStatus('✅ 已切换到官方端点配置,记得点击"保存配置"按钮保存设置', 'success');
}
}
</script>
</body>
</html>