diff --git a/app.js b/app.js index 23fe115..5c4a126 100644 --- a/app.js +++ b/app.js @@ -31,6 +31,13 @@ class CLIProxyManager { this.currentAuthFileFilter = 'all'; this.cachedAuthFiles = []; + // Vertex AI credential import state + this.vertexImportState = { + file: null, + loading: false, + result: null + }; + // 主题管理 this.currentTheme = 'light'; @@ -610,6 +617,24 @@ class CLIProxyManager { authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); } + // Vertex AI credential import + const vertexSelectFile = document.getElementById('vertex-select-file'); + const vertexFileInput = document.getElementById('vertex-file-input'); + const vertexImportBtn = document.getElementById('vertex-import-btn'); + + if (vertexSelectFile) { + vertexSelectFile.addEventListener('click', () => this.openVertexFilePicker()); + } + if (vertexFileInput) { + vertexFileInput.addEventListener('change', (e) => this.handleVertexFileSelection(e)); + } + if (vertexImportBtn) { + vertexImportBtn.addEventListener('click', () => this.importVertexCredential()); + } + this.updateVertexFileDisplay(); + this.updateVertexImportButtonState(); + this.renderVertexImportResult(this.vertexImportState.result); + // Codex OAuth const codexOauthBtn = document.getElementById('codex-oauth-btn'); const codexOpenLink = document.getElementById('codex-open-link'); @@ -3914,6 +3939,9 @@ class CLIProxyManager { case 'iflow': typeDisplayKey = 'auth_files.type_iflow'; break; + case 'vertex': + typeDisplayKey = 'auth_files.type_vertex'; + break; case 'empty': typeDisplayKey = 'auth_files.type_empty'; break; @@ -3992,6 +4020,7 @@ class CLIProxyManager { { type: 'claude', labelKey: 'auth_files.filter_claude' }, { type: 'codex', labelKey: 'auth_files.filter_codex' }, { type: 'iflow', labelKey: 'auth_files.filter_iflow' }, + { type: 'vertex', labelKey: 'auth_files.filter_vertex' }, { type: 'empty', labelKey: 'auth_files.filter_empty' } ]; @@ -4233,6 +4262,136 @@ class CLIProxyManager { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } + // ===== Vertex AI Credential Import ===== + openVertexFilePicker() { + const fileInput = document.getElementById('vertex-file-input'); + if (fileInput) { + fileInput.click(); + } + } + + handleVertexFileSelection(event) { + const fileInput = event?.target; + const file = fileInput?.files?.[0] || null; + + if (fileInput) { + fileInput.value = ''; + } + + if (file && !file.name.toLowerCase().endsWith('.json')) { + this.showNotification(i18n.t('vertex_import.file_required'), 'error'); + this.vertexImportState.file = null; + this.updateVertexFileDisplay(); + this.updateVertexImportButtonState(); + return; + } + + this.vertexImportState.file = file; + this.updateVertexFileDisplay(file ? file.name : ''); + this.updateVertexImportButtonState(); + } + + updateVertexFileDisplay(filename = '') { + const displayInput = document.getElementById('vertex-file-display'); + if (!displayInput) return; + displayInput.value = filename || ''; + } + + updateVertexImportButtonState() { + const importBtn = document.getElementById('vertex-import-btn'); + if (!importBtn) return; + const disabled = !this.vertexImportState.file || this.vertexImportState.loading; + importBtn.disabled = disabled; + } + + async importVertexCredential() { + if (!this.vertexImportState.file) { + this.showNotification(i18n.t('vertex_import.file_required'), 'error'); + return; + } + + const locationInput = document.getElementById('vertex-location'); + const location = locationInput ? locationInput.value.trim() : ''; + const formData = new FormData(); + formData.append('file', this.vertexImportState.file, this.vertexImportState.file.name); + if (location) { + formData.append('location', location); + } + + try { + this.vertexImportState.loading = true; + this.updateVertexImportButtonState(); + + const response = await fetch(`${this.apiUrl}/vertex/import`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.managementKey}` + }, + body: formData + }); + + if (!response.ok) { + let errorMessage = `HTTP ${response.status}`; + try { + const errorData = await response.json(); + errorMessage = errorData?.message || errorData?.error || errorMessage; + } catch (parseError) { + const text = await response.text(); + if (text) { + errorMessage = text; + } + } + throw new Error(errorMessage); + } + + const result = await response.json(); + this.showNotification(i18n.t('vertex_import.success'), 'success'); + this.renderVertexImportResult(result); + this.vertexImportState.file = null; + this.updateVertexFileDisplay(); + this.clearCache(); + await this.loadAuthFiles(); + } catch (error) { + this.showNotification(`${i18n.t('notification.upload_failed')}: ${error.message}`, 'error'); + } finally { + this.vertexImportState.loading = false; + this.updateVertexImportButtonState(); + const fileInput = document.getElementById('vertex-file-input'); + if (fileInput) { + fileInput.value = ''; + } + } + } + + renderVertexImportResult(result = null) { + const container = document.getElementById('vertex-import-result'); + const projectEl = document.getElementById('vertex-result-project'); + const emailEl = document.getElementById('vertex-result-email'); + const locationEl = document.getElementById('vertex-result-location'); + const fileEl = document.getElementById('vertex-result-file'); + + if (!container || !projectEl || !emailEl || !locationEl || !fileEl) { + return; + } + + if (!result) { + this.vertexImportState.result = null; + container.style.display = 'none'; + projectEl.textContent = '-'; + emailEl.textContent = '-'; + locationEl.textContent = '-'; + fileEl.textContent = '-'; + return; + } + + this.vertexImportState.result = result; + projectEl.textContent = result.project_id || '-'; + emailEl.textContent = result.email || '-'; + locationEl.textContent = result.location || 'us-central1'; + fileEl.textContent = result['auth-file'] || result.auth_file || '-'; + container.style.display = 'block'; + } + // 上传认证文件 uploadAuthFile() { document.getElementById('auth-file-input').click(); diff --git a/i18n.js b/i18n.js index 5560a15..2e7096a 100644 --- a/i18n.js +++ b/i18n.js @@ -217,7 +217,7 @@ const i18n = { // 认证文件管理 'auth_files.title': '认证文件管理', 'auth_files.title_section': '认证文件', - 'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。', + 'auth_files.description': '这里集中管理 CLI Proxy 支持的所有 JSON 认证文件(如 Qwen、Gemini、Vertex 等),上传后即可在运行时启用相应的 AI 服务。', 'auth_files.upload_button': '上传文件', 'auth_files.delete_all_button': '删除全部', 'auth_files.empty_title': '暂无认证文件', @@ -246,6 +246,7 @@ const i18n = { 'auth_files.filter_claude': 'Claude', 'auth_files.filter_codex': 'Codex', 'auth_files.filter_iflow': 'iFlow', + 'auth_files.filter_vertex': 'Vertex', 'auth_files.filter_empty': '空文件', 'auth_files.filter_unknown': '其他', 'auth_files.type_qwen': 'Qwen', @@ -255,8 +256,26 @@ const i18n = { 'auth_files.type_claude': 'Claude', 'auth_files.type_codex': 'Codex', 'auth_files.type_iflow': 'iFlow', + 'auth_files.type_vertex': 'Vertex', 'auth_files.type_empty': '空文件', 'auth_files.type_unknown': '其他', + 'vertex_import.title': 'Vertex AI 凭证导入', + 'vertex_import.description': '上传 Google 服务账号 JSON,使用 CLI vertex-import 同步规则写入 auth-dir/vertex-.json。', + 'vertex_import.location_label': '目标区域 (可选)', + 'vertex_import.location_placeholder': 'us-central1', + 'vertex_import.location_hint': '留空表示使用默认区域 us-central1。', + 'vertex_import.file_label': '服务账号密钥 JSON', + 'vertex_import.file_hint': '仅支持 Google Cloud service account key JSON 文件,私钥会自动规范化。', + 'vertex_import.file_placeholder': '尚未选择文件', + 'vertex_import.choose_file': '选择文件', + 'vertex_import.import_button': '导入 Vertex 凭证', + 'vertex_import.file_required': '请先选择 .json 凭证文件', + 'vertex_import.success': 'Vertex 凭证导入成功', + 'vertex_import.result_title': '凭证已保存', + 'vertex_import.result_project': '项目 ID', + 'vertex_import.result_email': '服务账号', + 'vertex_import.result_location': '区域', + 'vertex_import.result_file': '存储文件', // Codex OAuth @@ -685,7 +704,7 @@ const i18n = { // Auth files management 'auth_files.title': 'Auth Files Management', 'auth_files.title_section': 'Auth Files', - 'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.', + 'auth_files.description': 'Manage all CLI Proxy JSON auth files here (e.g. Qwen, Gemini, Vertex). Uploading a credential immediately enables the corresponding AI integration.', 'auth_files.upload_button': 'Upload File', 'auth_files.delete_all_button': 'Delete All', 'auth_files.empty_title': 'No Auth Files', @@ -714,6 +733,7 @@ const i18n = { 'auth_files.filter_claude': 'Claude', 'auth_files.filter_codex': 'Codex', 'auth_files.filter_iflow': 'iFlow', + 'auth_files.filter_vertex': 'Vertex', 'auth_files.filter_empty': 'Empty', 'auth_files.filter_unknown': 'Other', 'auth_files.type_qwen': 'Qwen', @@ -723,8 +743,26 @@ const i18n = { 'auth_files.type_claude': 'Claude', 'auth_files.type_codex': 'Codex', 'auth_files.type_iflow': 'iFlow', + 'auth_files.type_vertex': 'Vertex', 'auth_files.type_empty': 'Empty', 'auth_files.type_unknown': 'Other', + 'vertex_import.title': 'Vertex AI Credential Import', + 'vertex_import.description': 'Upload a Google service account JSON to store it as auth-dir/vertex-.json using the same rules as the CLI vertex-import helper.', + 'vertex_import.location_label': 'Region (optional)', + 'vertex_import.location_placeholder': 'us-central1', + 'vertex_import.location_hint': 'Leave empty to use the default region us-central1.', + 'vertex_import.file_label': 'Service account key JSON', + 'vertex_import.file_hint': 'Only Google Cloud service account key JSON files are accepted.', + 'vertex_import.file_placeholder': 'No file selected', + 'vertex_import.choose_file': 'Choose File', + 'vertex_import.import_button': 'Import Vertex Credential', + 'vertex_import.file_required': 'Select a .json credential file first', + 'vertex_import.success': 'Vertex credential imported successfully', + 'vertex_import.result_title': 'Credential saved', + 'vertex_import.result_project': 'Project ID', + 'vertex_import.result_email': 'Service account', + 'vertex_import.result_location': 'Region', + 'vertex_import.result_file': 'Persisted file', // Codex OAuth 'auth_login.codex_oauth_title': 'Codex OAuth', diff --git a/index.html b/index.html index 56efae9..3be665e 100644 --- a/index.html +++ b/index.html @@ -458,6 +458,7 @@ + @@ -478,6 +479,51 @@ + +
+
+

Vertex AI 凭证导入

+
+
+

+ 上传 Google 服务账号 JSON 并保存为 vertex-.json。 +

+
+ + +

留空则使用默认 us-central1。

+
+
+ +
+ + + +
+

仅支持 Google Cloud service account JSON。

+
+
+ +
+ +
+
+
diff --git a/styles.css b/styles.css index 1bc9602..b4fb72c 100644 --- a/styles.css +++ b/styles.css @@ -3758,6 +3758,60 @@ input:checked+.slider:before { color: #f472b6; } +/* Vertex AI Credential Import */ +.vertex-import-actions { + text-align: left; + margin-top: 10px; +} + +.vertex-import-result { + margin-top: 20px; + border: 1px dashed var(--border-primary); + border-radius: 12px; + padding: 16px; + background: var(--bg-quaternary); + color: var(--text-primary); +} + +.vertex-import-result-header { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + margin-bottom: 10px; + color: var(--success-text); +} + +.vertex-import-result ul { + list-style: none; + margin: 0; + padding: 0; +} + +.vertex-import-result li { + margin-bottom: 6px; + font-size: 14px; + color: var(--text-secondary); +} + +.vertex-import-result code { + background: var(--bg-secondary); + border-radius: 6px; + padding: 2px 6px; + font-size: 13px; + color: var(--text-primary); +} + +[data-theme="dark"] .vertex-import-result { + border-color: rgba(96, 165, 250, 0.4); + background: rgba(15, 23, 42, 0.4); +} + +[data-theme="dark"] .vertex-import-result code { + background: rgba(7, 11, 22, 0.8); + color: #f3f4f6; +} + /* ===== AI提供商统计徽章样式 ===== */ /* 统计信息容器 */