Compare commits

..

5 Commits

7 changed files with 492 additions and 16 deletions

20
app.js
View File

@@ -221,6 +221,7 @@ class CLIProxyManager {
const cardText = card.textContent || '';
if (cardText.includes('Codex OAuth') ||
cardText.includes('Anthropic OAuth') ||
cardText.includes('Antigravity OAuth') ||
cardText.includes('Gemini CLI OAuth') ||
cardText.includes('Qwen OAuth') ||
cardText.includes('iFlow OAuth')) {
@@ -430,6 +431,21 @@ class CLIProxyManager {
anthropicCopyLink.addEventListener('click', () => this.copyAnthropicLink());
}
// Antigravity OAuth
const antigravityOauthBtn = document.getElementById('antigravity-oauth-btn');
const antigravityOpenLink = document.getElementById('antigravity-open-link');
const antigravityCopyLink = document.getElementById('antigravity-copy-link');
if (antigravityOauthBtn) {
antigravityOauthBtn.addEventListener('click', () => this.startAntigravityOAuth());
}
if (antigravityOpenLink) {
antigravityOpenLink.addEventListener('click', () => this.openAntigravityLink());
}
if (antigravityCopyLink) {
antigravityCopyLink.addEventListener('click', () => this.copyAntigravityLink());
}
// Gemini CLI OAuth
const geminiCliOauthBtn = document.getElementById('gemini-cli-oauth-btn');
const geminiCliOpenLink = document.getElementById('gemini-cli-open-link');
@@ -464,6 +480,7 @@ class CLIProxyManager {
const iflowOauthBtn = document.getElementById('iflow-oauth-btn');
const iflowOpenLink = document.getElementById('iflow-open-link');
const iflowCopyLink = document.getElementById('iflow-copy-link');
const iflowCookieSubmit = document.getElementById('iflow-cookie-submit');
if (iflowOauthBtn) {
iflowOauthBtn.addEventListener('click', () => this.startIflowOAuth());
@@ -474,6 +491,9 @@ class CLIProxyManager {
if (iflowCopyLink) {
iflowCopyLink.addEventListener('click', () => this.copyIflowLink());
}
if (iflowCookieSubmit) {
iflowCookieSubmit.addEventListener('click', () => this.submitIflowCookieLogin());
}
// 使用统计
const refreshUsageStats = document.getElementById('refresh-usage-stats');

54
i18n.js
View File

@@ -333,6 +333,19 @@ const i18n = {
'auth_login.anthropic_oauth_start_error': '启动 Anthropic OAuth 失败:',
'auth_login.anthropic_oauth_polling_error': '检查认证状态失败:',
// Antigravity OAuth
'auth_login.antigravity_oauth_title': 'Antigravity OAuth',
'auth_login.antigravity_oauth_button': '开始 Antigravity 登录',
'auth_login.antigravity_oauth_hint': '通过 OAuth 流程登录 AntigravityGoogle 账号)服务,自动获取并保存认证文件。',
'auth_login.antigravity_oauth_url_label': '授权链接:',
'auth_login.antigravity_open_link': '打开链接',
'auth_login.antigravity_copy_link': '复制链接',
'auth_login.antigravity_oauth_status_waiting': '等待认证中...',
'auth_login.antigravity_oauth_status_success': '认证成功!',
'auth_login.antigravity_oauth_status_error': '认证失败:',
'auth_login.antigravity_oauth_start_error': '启动 Antigravity OAuth 失败:',
'auth_login.antigravity_oauth_polling_error': '检查认证状态失败:',
// Gemini CLI OAuth
'auth_login.gemini_cli_oauth_title': 'Gemini CLI OAuth',
'auth_login.gemini_cli_oauth_button': '开始 Gemini CLI 登录',
@@ -375,6 +388,20 @@ const i18n = {
'auth_login.iflow_oauth_status_error': '认证失败:',
'auth_login.iflow_oauth_start_error': '启动 iFlow OAuth 失败:',
'auth_login.iflow_oauth_polling_error': '检查认证状态失败:',
'auth_login.iflow_cookie_title': 'iFlow Cookie 登录',
'auth_login.iflow_cookie_label': 'Cookie 内容:',
'auth_login.iflow_cookie_placeholder': '粘贴浏览器中的 Cookie例如 sessionid=...;',
'auth_login.iflow_cookie_hint': '直接提交 Cookie 以完成登录(无需打开授权链接),服务端将自动保存凭据。',
'auth_login.iflow_cookie_button': '提交 Cookie 登录',
'auth_login.iflow_cookie_status_success': 'Cookie 登录成功,凭据已保存。',
'auth_login.iflow_cookie_status_error': 'Cookie 登录失败:',
'auth_login.iflow_cookie_start_error': '提交 Cookie 登录失败:',
'auth_login.iflow_cookie_required': '请先填写 Cookie 内容',
'auth_login.iflow_cookie_result_title': 'Cookie 登录结果',
'auth_login.iflow_cookie_result_email': '账号',
'auth_login.iflow_cookie_result_expired': '过期时间',
'auth_login.iflow_cookie_result_path': '保存路径',
'auth_login.iflow_cookie_result_type': '类型',
// 使用统计
'usage_stats.title': '使用统计',
@@ -854,6 +881,19 @@ const i18n = {
'auth_login.anthropic_oauth_start_error': 'Failed to start Anthropic OAuth:',
'auth_login.anthropic_oauth_polling_error': 'Failed to check authentication status:',
// Antigravity OAuth
'auth_login.antigravity_oauth_title': 'Antigravity OAuth',
'auth_login.antigravity_oauth_button': 'Start Antigravity Login',
'auth_login.antigravity_oauth_hint': 'Login to Antigravity service (Google account) through OAuth flow, automatically obtain and save authentication files.',
'auth_login.antigravity_oauth_url_label': 'Authorization URL:',
'auth_login.antigravity_open_link': 'Open Link',
'auth_login.antigravity_copy_link': 'Copy Link',
'auth_login.antigravity_oauth_status_waiting': 'Waiting for authentication...',
'auth_login.antigravity_oauth_status_success': 'Authentication successful!',
'auth_login.antigravity_oauth_status_error': 'Authentication failed:',
'auth_login.antigravity_oauth_start_error': 'Failed to start Antigravity OAuth:',
'auth_login.antigravity_oauth_polling_error': 'Failed to check authentication status:',
// Gemini CLI OAuth
'auth_login.gemini_cli_oauth_title': 'Gemini CLI OAuth',
'auth_login.gemini_cli_oauth_button': 'Start Gemini CLI Login',
@@ -896,6 +936,20 @@ const i18n = {
'auth_login.iflow_oauth_status_error': 'Authentication failed:',
'auth_login.iflow_oauth_start_error': 'Failed to start iFlow OAuth:',
'auth_login.iflow_oauth_polling_error': 'Failed to check authentication status:',
'auth_login.iflow_cookie_title': 'iFlow Cookie Login',
'auth_login.iflow_cookie_label': 'Cookie Value:',
'auth_login.iflow_cookie_placeholder': 'Paste browser cookie, e.g. sessionid=...;',
'auth_login.iflow_cookie_hint': 'Submit an existing cookie to finish login without opening the authorization link; the credential file will be saved automatically.',
'auth_login.iflow_cookie_button': 'Submit Cookie Login',
'auth_login.iflow_cookie_status_success': 'Cookie login succeeded and credentials are saved.',
'auth_login.iflow_cookie_status_error': 'Cookie login failed:',
'auth_login.iflow_cookie_start_error': 'Failed to submit cookie login:',
'auth_login.iflow_cookie_required': 'Please provide the Cookie value first.',
'auth_login.iflow_cookie_result_title': 'Cookie Login Result',
'auth_login.iflow_cookie_result_email': 'Account',
'auth_login.iflow_cookie_result_expired': 'Expires At',
'auth_login.iflow_cookie_result_path': 'Saved Path',
'auth_login.iflow_cookie_result_type': 'Type',
// Usage Statistics
'usage_stats.title': 'Usage Statistics',

View File

@@ -628,6 +628,42 @@
</div>
</div>
<!-- Antigravity OAuth -->
<div class="card" id="antigravity-oauth-card">
<div class="card-header">
<h3><i class="fas fa-rocket"></i> <span
data-i18n="auth_login.antigravity_oauth_title">Antigravity OAuth</span></h3>
<button id="antigravity-oauth-btn" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> <span
data-i18n="auth_login.antigravity_oauth_button">开始 Antigravity 登录</span>
</button>
</div>
<div class="card-content">
<p class="form-hint" style="margin-bottom: 20px;"
data-i18n="auth_login.antigravity_oauth_hint">
通过 OAuth 流程登录 AntigravityGoogle 账号)服务,自动获取并保存认证文件。
</p>
<div id="antigravity-oauth-content" style="display: none;">
<div class="form-group">
<label data-i18n="auth_login.antigravity_oauth_url_label">授权链接:</label>
<div class="input-group">
<input type="text" id="antigravity-oauth-url" readonly>
<button id="antigravity-open-link" class="btn btn-primary">
<i class="fas fa-external-link-alt"></i> <span
data-i18n="auth_login.antigravity_open_link">打开链接</span>
</button>
<button id="antigravity-copy-link" class="btn btn-secondary">
<i class="fas fa-copy"></i> <span
data-i18n="auth_login.antigravity_copy_link">复制链接</span>
</button>
</div>
</div>
<div id="antigravity-oauth-status" class="form-hint" style="margin-top: 10px;">
</div>
</div>
</div>
</div>
<!-- Gemini CLI OAuth -->
<div class="card" id="gemini-cli-oauth-card">
<div class="card-header">
@@ -742,6 +778,34 @@
<div id="iflow-oauth-status" class="form-hint" style="margin-top: 10px;"></div>
</div>
</div>
<div class="card-content" style="border-top: 1px solid var(--border-color); margin-top: 16px; padding-top: 16px;">
<h4 style="margin-bottom: 10px;" data-i18n="auth_login.iflow_cookie_title">iFlow Cookie 登录</h4>
<p class="form-hint" data-i18n="auth_login.iflow_cookie_hint">
直接提交 Cookie 完成登录并保存凭据,无需打开授权链接。
</p>
<div class="form-group">
<label for="iflow-cookie-input" data-i18n="auth_login.iflow_cookie_label">Cookie 内容:</label>
<textarea id="iflow-cookie-input" rows="3" data-i18n-placeholder="auth_login.iflow_cookie_placeholder" placeholder="粘贴浏览器中的 Cookie"></textarea>
</div>
<div class="form-actions">
<button id="iflow-cookie-submit" class="btn btn-primary">
<i class="fas fa-cookie-bite"></i> <span data-i18n="auth_login.iflow_cookie_button">提交 Cookie 登录</span>
</button>
</div>
<div id="iflow-cookie-status" class="form-hint" style="margin-top: 10px;"></div>
<div id="iflow-cookie-result" class="vertex-import-result" style="display: none;">
<div class="vertex-import-result-header">
<i class="fas fa-check-circle"></i>
<span data-i18n="auth_login.iflow_cookie_result_title">Cookie 登录结果</span>
</div>
<ul>
<li><span data-i18n="auth_login.iflow_cookie_result_email">账号</span>: <code id="iflow-cookie-result-email">-</code></li>
<li><span data-i18n="auth_login.iflow_cookie_result_expired">过期时间</span>: <code id="iflow-cookie-result-expired">-</code></li>
<li><span data-i18n="auth_login.iflow_cookie_result_path">保存路径</span>: <code id="iflow-cookie-result-path">-</code></li>
<li><span data-i18n="auth_login.iflow_cookie_result_type">类型</span>: <code id="iflow-cookie-result-type">-</code></li>
</ul>
</div>
</div>
</div>
</section>

View File

@@ -26,17 +26,18 @@ export const apiKeysModule = {
return;
}
container.innerHTML = keys.map((key, index) => {
const rows = keys.map((key, index) => {
const normalizedKey = typeof key === 'string' ? key : String(key ?? '');
const maskedDisplay = this.escapeHtml(this.maskApiKey(normalizedKey));
const keyArgument = encodeURIComponent(normalizedKey);
return `
<div class="key-item">
<div class="item-content">
<div class="item-title">${i18n.t('api_keys.item_title')} #${index + 1}</div>
<div class="item-value">${maskedDisplay}</div>
<div class="key-table-row">
<div class="key-badge">#${index + 1}</div>
<div class="key-table-value">
<div class="item-title">${i18n.t('api_keys.item_title')}</div>
<div class="key-value">${maskedDisplay}</div>
</div>
<div class="item-actions">
<div class="item-actions compact">
<button class="btn btn-secondary" data-action="edit-api-key" data-index="${index}" data-key="${keyArgument}">
<i class="fas fa-edit"></i>
</button>
@@ -48,6 +49,12 @@ export const apiKeysModule = {
`;
}).join('');
container.innerHTML = `
<div class="key-table">
${rows}
</div>
`;
this.bindApiKeyListEvents(container);
},

View File

@@ -300,6 +300,135 @@ export const oauthModule = {
}
},
// ===== Antigravity OAuth 相关方法 =====
// 开始 Antigravity OAuth 流程
async startAntigravityOAuth() {
try {
const response = await this.makeRequest('/antigravity-auth-url?is_webui=1');
const authUrl = response.url;
const state = response.state || this.extractStateFromUrl(authUrl);
// 显示授权链接
const urlInput = document.getElementById('antigravity-oauth-url');
const content = document.getElementById('antigravity-oauth-content');
const status = document.getElementById('antigravity-oauth-status');
if (urlInput) {
urlInput.value = authUrl;
}
if (content) {
content.style.display = 'block';
}
if (status) {
status.textContent = i18n.t('auth_login.antigravity_oauth_status_waiting');
status.style.color = 'var(--warning-text)';
}
// 开始轮询认证状态
this.startAntigravityOAuthPolling(state);
} catch (error) {
this.showNotification(`${i18n.t('auth_login.antigravity_oauth_start_error')} ${error.message}`, 'error');
}
},
// 打开 Antigravity 授权链接
openAntigravityLink() {
const urlInput = document.getElementById('antigravity-oauth-url');
if (urlInput && urlInput.value) {
window.open(urlInput.value, '_blank');
}
},
// 复制 Antigravity 授权链接
async copyAntigravityLink() {
const urlInput = document.getElementById('antigravity-oauth-url');
if (urlInput && urlInput.value) {
try {
await navigator.clipboard.writeText(urlInput.value);
this.showNotification(i18n.t('notification.link_copied'), 'success');
} catch (error) {
urlInput.select();
document.execCommand('copy');
this.showNotification(i18n.t('notification.link_copied'), 'success');
}
}
},
// 开始轮询 Antigravity OAuth 状态
startAntigravityOAuthPolling(state) {
if (!state) {
this.showNotification(i18n.t('auth_login.missing_state'), 'error');
return;
}
const pollInterval = setInterval(async () => {
try {
const response = await this.makeRequest(`/get-auth-status?state=${encodeURIComponent(state)}`);
const status = response.status;
const statusElement = document.getElementById('antigravity-oauth-status');
if (status === 'ok') {
clearInterval(pollInterval);
this.resetAntigravityOAuthUI();
this.showNotification(i18n.t('auth_login.antigravity_oauth_status_success'), 'success');
this.loadAuthFiles();
} else if (status === 'error') {
clearInterval(pollInterval);
const errorMessage = response.error || 'Unknown error';
if (statusElement) {
statusElement.textContent = `${i18n.t('auth_login.antigravity_oauth_status_error')} ${errorMessage}`;
statusElement.style.color = 'var(--error-text)';
}
this.showNotification(`${i18n.t('auth_login.antigravity_oauth_status_error')} ${errorMessage}`, 'error');
setTimeout(() => {
this.resetAntigravityOAuthUI();
}, 3000);
} else if (status === 'wait') {
if (statusElement) {
statusElement.textContent = i18n.t('auth_login.antigravity_oauth_status_waiting');
statusElement.style.color = 'var(--warning-text)';
}
}
} catch (error) {
clearInterval(pollInterval);
const statusElement = document.getElementById('antigravity-oauth-status');
if (statusElement) {
statusElement.textContent = `${i18n.t('auth_login.antigravity_oauth_polling_error')} ${error.message}`;
statusElement.style.color = 'var(--error-text)';
}
this.showNotification(`${i18n.t('auth_login.antigravity_oauth_polling_error')} ${error.message}`, 'error');
setTimeout(() => {
this.resetAntigravityOAuthUI();
}, 3000);
}
}, 2000);
setTimeout(() => {
clearInterval(pollInterval);
}, 5 * 60 * 1000);
},
// 重置 Antigravity OAuth UI 到初始状态
resetAntigravityOAuthUI() {
const urlInput = document.getElementById('antigravity-oauth-url');
const content = document.getElementById('antigravity-oauth-content');
const status = document.getElementById('antigravity-oauth-status');
if (urlInput) {
urlInput.value = '';
}
if (content) {
content.style.display = 'none';
}
if (status) {
status.textContent = '';
status.style.color = '';
status.className = '';
}
},
// ===== Gemini CLI OAuth 相关方法 =====
// 开始 Gemini CLI OAuth 流程
@@ -733,6 +862,88 @@ export const oauthModule = {
status.style.color = '';
status.className = '';
}
},
// 提交 iFlow Cookie 登录
async submitIflowCookieLogin() {
const cookieInput = document.getElementById('iflow-cookie-input');
const statusEl = document.getElementById('iflow-cookie-status');
const submitBtn = document.getElementById('iflow-cookie-submit');
const cookieValue = cookieInput ? cookieInput.value.trim() : '';
this.renderIflowCookieResult(null);
if (!cookieValue) {
this.showNotification(i18n.t('auth_login.iflow_cookie_required'), 'error');
if (statusEl) {
statusEl.textContent = `${i18n.t('auth_login.iflow_cookie_status_error')} ${i18n.t('auth_login.iflow_cookie_required')}`;
statusEl.style.color = 'var(--error-text)';
}
return;
}
try {
if (submitBtn) {
submitBtn.disabled = true;
}
if (statusEl) {
statusEl.textContent = i18n.t('auth_login.iflow_oauth_status_waiting');
statusEl.style.color = 'var(--warning-text)';
}
const response = await this.makeRequest('/iflow-auth-url', {
method: 'POST',
body: JSON.stringify({ cookie: cookieValue })
});
this.renderIflowCookieResult(response);
if (statusEl) {
statusEl.textContent = i18n.t('auth_login.iflow_cookie_status_success');
statusEl.style.color = 'var(--success-text)';
}
if (cookieInput) {
cookieInput.value = '';
}
this.showNotification(i18n.t('auth_login.iflow_cookie_status_success'), 'success');
this.loadAuthFiles();
} catch (error) {
if (statusEl) {
statusEl.textContent = `${i18n.t('auth_login.iflow_cookie_status_error')} ${error.message}`;
statusEl.style.color = 'var(--error-text)';
}
this.showNotification(`${i18n.t('auth_login.iflow_cookie_start_error')} ${error.message}`, 'error');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
}
}
},
renderIflowCookieResult(result = null) {
const container = document.getElementById('iflow-cookie-result');
const emailEl = document.getElementById('iflow-cookie-result-email');
const expiredEl = document.getElementById('iflow-cookie-result-expired');
const pathEl = document.getElementById('iflow-cookie-result-path');
const typeEl = document.getElementById('iflow-cookie-result-type');
if (!container || !emailEl || !expiredEl || !pathEl || !typeEl) {
return;
}
if (!result) {
container.style.display = 'none';
emailEl.textContent = '-';
expiredEl.textContent = '-';
pathEl.textContent = '-';
typeEl.textContent = '-';
return;
}
emailEl.textContent = result.email || '-';
expiredEl.textContent = result.expired || '-';
pathEl.textContent = result.saved_path || result.savedPath || result.path || '-';
typeEl.textContent = result.type || '-';
container.style.display = 'block';
}
};

View File

@@ -109,6 +109,7 @@ export const REQUEST_TIMEOUT_MS = 30 * 1000;
export const OAUTH_CARD_IDS = [
'codex-oauth-card',
'anthropic-oauth-card',
'antigravity-oauth-card',
'gemini-cli-oauth-card',
'qwen-oauth-card',
'iflow-oauth-card'
@@ -120,6 +121,7 @@ export const OAUTH_CARD_IDS = [
export const OAUTH_PROVIDERS = {
CODEX: 'codex',
ANTHROPIC: 'anthropic',
ANTIGRAVITY: 'antigravity',
GEMINI_CLI: 'gemini-cli',
QWEN: 'qwen',
IFLOW: 'iflow'

View File

@@ -1548,10 +1548,128 @@ input:checked+.slider:before {
/* 列表样式 */
.key-list {
max-height: 400px;
/* 便于一次展示 6 条密钥记录(约 6*80px 高度) */
max-height: 520px;
overflow-y: auto;
}
/* API 密钥列表压缩布局 */
.key-table {
display: flex;
flex-direction: column;
gap: 8px;
}
.key-table-row {
display: grid;
grid-template-columns: 72px 1fr auto;
column-gap: 14px;
align-items: center;
background: var(--bg-quaternary);
border: 1px solid var(--border-primary);
border-radius: 10px;
padding: 10px 12px;
transition: all 0.2s ease;
}
.key-table-row:hover {
background: var(--bg-tertiary);
border-color: var(--border-secondary);
transform: translateY(-1px);
}
.key-badge {
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
min-width: 52px;
padding: 0 12px;
border-radius: 10px;
background: var(--bg-secondary);
color: var(--primary-color);
font-weight: 700;
font-size: 14px;
border: 1px solid var(--border-primary);
letter-spacing: 0.2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.key-table-value {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
padding: 2px 0;
}
.key-table .item-title {
margin: 0;
font-size: 13px;
color: var(--text-secondary);
}
.key-value {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 8px 12px;
font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
font-size: 13px;
color: var(--text-primary);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.item-actions.compact {
position: static;
transform: none;
gap: 6px;
justify-content: flex-end;
justify-self: end;
}
.item-actions.compact .btn {
min-width: 34px;
height: 34px;
padding: 0;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.item-actions.compact .btn:hover {
transform: translateY(-1px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12);
}
.item-actions.compact .btn i {
font-size: 14px;
}
@media (max-width: 768px) {
.key-table-row {
grid-template-columns: 1fr;
row-gap: 8px;
}
.item-actions.compact {
justify-content: flex-start;
}
.key-badge {
width: fit-content;
}
.key-value {
white-space: normal;
word-break: break-all;
}
}
.file-list {
/* 认证文件列表填满页面,保留版本信息空间 */
max-height: calc(100vh - 300px); /* 减去导航栏、padding和版本信息的高度 */