diff --git a/app.js b/app.js
new file mode 100644
index 0000000..8f791ba
--- /dev/null
+++ b/app.js
@@ -0,0 +1,1463 @@
+// CLI Proxy API 管理界面 JavaScript
+class CLIProxyManager {
+ constructor() {
+ // 仅保存基础地址(不含 /v0/management),请求时自动补齐
+ this.apiBase = 'http://localhost:8317';
+ this.apiUrl = this.computeApiUrl(this.apiBase);
+ this.managementKey = '';
+ this.isConnected = false;
+
+ this.init();
+ }
+
+ // 简易防抖,减少频繁写 localStorage
+ debounce(fn, delay = 400) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => fn.apply(this, args), delay);
+ };
+ }
+
+ init() {
+ this.bindEvents();
+ this.loadSettings();
+ this.setupNavigation();
+ }
+
+ // 事件绑定
+ bindEvents() {
+ // 认证相关
+ document.getElementById('test-connection').addEventListener('click', () => this.testConnection());
+ document.getElementById('toggle-key-visibility').addEventListener('click', () => this.toggleKeyVisibility());
+
+ // 连接状态检查
+ document.getElementById('connection-status').addEventListener('click', () => this.checkConnectionStatus());
+ document.getElementById('refresh-all').addEventListener('click', () => this.refreshAllData());
+
+ // 基础设置
+ document.getElementById('debug-toggle').addEventListener('change', (e) => this.updateDebug(e.target.checked));
+ document.getElementById('update-proxy').addEventListener('click', () => this.updateProxyUrl());
+ document.getElementById('clear-proxy').addEventListener('click', () => this.clearProxyUrl());
+ document.getElementById('update-retry').addEventListener('click', () => this.updateRequestRetry());
+ document.getElementById('switch-project-toggle').addEventListener('change', (e) => this.updateSwitchProject(e.target.checked));
+ document.getElementById('switch-preview-model-toggle').addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
+ document.getElementById('allow-localhost-toggle').addEventListener('change', (e) => this.updateAllowLocalhost(e.target.checked));
+
+ // API 密钥管理
+ document.getElementById('add-api-key').addEventListener('click', () => this.showAddApiKeyModal());
+ document.getElementById('add-gemini-key').addEventListener('click', () => this.showAddGeminiKeyModal());
+ document.getElementById('add-codex-key').addEventListener('click', () => this.showAddCodexKeyModal());
+ document.getElementById('add-claude-key').addEventListener('click', () => this.showAddClaudeKeyModal());
+ document.getElementById('add-openai-provider').addEventListener('click', () => this.showAddOpenAIProviderModal());
+
+ // 认证文件管理
+ document.getElementById('upload-auth-file').addEventListener('click', () => this.uploadAuthFile());
+ document.getElementById('delete-all-auth-files').addEventListener('click', () => this.deleteAllAuthFiles());
+ document.getElementById('auth-file-input').addEventListener('change', (e) => this.handleFileUpload(e));
+
+ // 模态框
+ document.querySelector('.close').addEventListener('click', () => this.closeModal());
+ window.addEventListener('click', (e) => {
+ if (e.target === document.getElementById('modal')) {
+ this.closeModal();
+ }
+ });
+ }
+
+ // 设置导航
+ setupNavigation() {
+ const navItems = document.querySelectorAll('.nav-item');
+ navItems.forEach(item => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ // 移除所有活动状态
+ navItems.forEach(nav => nav.classList.remove('active'));
+ document.querySelectorAll('.content-section').forEach(section => section.classList.remove('active'));
+
+ // 添加活动状态
+ item.classList.add('active');
+ const sectionId = item.getAttribute('data-section');
+ document.getElementById(sectionId).classList.add('active');
+ });
+ });
+ }
+
+ // 规范化基础地址,移除尾部斜杠与 /v0/management
+ normalizeBase(input) {
+ let base = (input || '').trim();
+ if (!base) return '';
+ // 若用户粘贴了完整地址,剥离后缀
+ base = base.replace(/\/?v0\/management\/?$/i, '');
+ base = base.replace(/\/+$/i, '');
+ // 自动补 http://
+ if (!/^https?:\/\//i.test(base)) {
+ base = 'http://' + base;
+ }
+ return base;
+ }
+
+ // 由基础地址生成完整管理 API 地址
+ computeApiUrl(base) {
+ const b = this.normalizeBase(base);
+ if (!b) return '';
+ return b.replace(/\/$/, '') + '/v0/management';
+ }
+
+ setApiBase(newBase) {
+ this.apiBase = this.normalizeBase(newBase);
+ this.apiUrl = this.computeApiUrl(this.apiBase);
+ localStorage.setItem('apiBase', this.apiBase);
+ localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段
+ }
+
+ // 加载设置
+ loadSettings() {
+ const savedBase = localStorage.getItem('apiBase');
+ const savedUrl = localStorage.getItem('apiUrl');
+ const savedKey = localStorage.getItem('managementKey');
+
+ if (savedBase) {
+ this.setApiBase(savedBase);
+ document.getElementById('api-url').value = this.apiBase;
+ } else if (savedUrl) {
+ const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, '');
+ this.setApiBase(base);
+ document.getElementById('api-url').value = this.apiBase;
+ } else {
+ this.setApiBase(this.apiBase);
+ document.getElementById('api-url').value = this.apiBase;
+ }
+
+ if (savedKey) {
+ document.getElementById('management-key').value = savedKey;
+ this.managementKey = savedKey;
+ }
+
+ // 监听API URL和密钥变化
+ const apiInput = document.getElementById('api-url');
+ const keyInput = document.getElementById('management-key');
+
+ const saveBase = (val) => this.setApiBase(val);
+ const saveBaseDebounced = this.debounce(saveBase, 500);
+
+ apiInput.addEventListener('change', (e) => saveBase(e.target.value));
+ apiInput.addEventListener('input', (e) => saveBaseDebounced(e.target.value));
+
+ const saveKey = (val) => {
+ this.managementKey = val;
+ localStorage.setItem('managementKey', this.managementKey);
+ };
+ const saveKeyDebounced = this.debounce(saveKey, 500);
+
+ keyInput.addEventListener('change', (e) => saveKey(e.target.value));
+ keyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
+ }
+
+ // API 请求方法
+ async makeRequest(endpoint, options = {}) {
+ const url = `${this.apiUrl}${endpoint}`;
+ const headers = {
+ 'Authorization': `Bearer ${this.managementKey}`,
+ 'Content-Type': 'application/json',
+ ...options.headers
+ };
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || `HTTP ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('API请求失败:', error);
+ throw error;
+ }
+ }
+
+ // 显示通知
+ showNotification(message, type = 'info') {
+ const notification = document.getElementById('notification');
+ notification.textContent = message;
+ notification.className = `notification ${type}`;
+ notification.classList.add('show');
+
+ setTimeout(() => {
+ notification.classList.remove('show');
+ }, 3000);
+ }
+
+ // 密钥可见性切换
+ toggleKeyVisibility() {
+ const keyInput = document.getElementById('management-key');
+ const toggleButton = document.getElementById('toggle-key-visibility');
+
+ if (keyInput.type === 'password') {
+ keyInput.type = 'text';
+ toggleButton.innerHTML = '';
+ } else {
+ keyInput.type = 'password';
+ toggleButton.innerHTML = '';
+ }
+ }
+
+ // 测试连接
+ async testConnection() {
+ const button = document.getElementById('test-connection');
+ const originalText = button.innerHTML;
+
+ button.innerHTML = '
连接中...';
+ button.disabled = true;
+
+ try {
+ await this.makeRequest('/debug');
+ this.isConnected = true;
+ this.showNotification('连接成功!', 'success');
+ this.updateConnectionStatus();
+ await this.loadAllData();
+ } catch (error) {
+ this.isConnected = false;
+ this.showNotification(`连接失败: ${error.message}`, 'error');
+ this.updateConnectionStatus();
+ } finally {
+ button.innerHTML = originalText;
+ button.disabled = false;
+ }
+ }
+
+ // 更新连接状态
+ updateConnectionStatus() {
+ const statusButton = document.getElementById('connection-status');
+ const apiStatus = document.getElementById('api-status');
+ const lastUpdate = document.getElementById('last-update');
+
+ if (this.isConnected) {
+ statusButton.innerHTML = ' 已连接';
+ statusButton.className = 'btn btn-success';
+ apiStatus.textContent = '已连接';
+ } else {
+ statusButton.innerHTML = ' 未连接';
+ statusButton.className = 'btn btn-danger';
+ apiStatus.textContent = '未连接';
+ }
+
+ lastUpdate.textContent = new Date().toLocaleString('zh-CN');
+ }
+
+ // 检查连接状态
+ async checkConnectionStatus() {
+ await this.testConnection();
+ }
+
+ // 刷新所有数据
+ async refreshAllData() {
+ if (!this.isConnected) {
+ this.showNotification('请先建立连接', 'error');
+ return;
+ }
+
+ const button = document.getElementById('refresh-all');
+ const originalText = button.innerHTML;
+
+ button.innerHTML = ' 刷新中...';
+ button.disabled = true;
+
+ try {
+ await this.loadAllData();
+ this.showNotification('数据刷新成功', 'success');
+ } catch (error) {
+ this.showNotification(`刷新失败: ${error.message}`, 'error');
+ } finally {
+ button.innerHTML = originalText;
+ button.disabled = false;
+ }
+ }
+
+ // 加载所有数据
+ async loadAllData() {
+ await Promise.all([
+ this.loadDebugSettings(),
+ this.loadProxySettings(),
+ this.loadRetrySettings(),
+ this.loadQuotaSettings(),
+ this.loadLocalhostSettings(),
+ this.loadApiKeys(),
+ this.loadGeminiKeys(),
+ this.loadCodexKeys(),
+ this.loadClaudeKeys(),
+ this.loadOpenAIProviders(),
+ this.loadAuthFiles()
+ ]);
+ }
+
+ // 加载调试设置
+ async loadDebugSettings() {
+ try {
+ const data = await this.makeRequest('/debug');
+ document.getElementById('debug-toggle').checked = data.debug;
+ } catch (error) {
+ console.error('加载调试设置失败:', error);
+ }
+ }
+
+ // 更新调试设置
+ async updateDebug(enabled) {
+ try {
+ await this.makeRequest('/debug', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.showNotification('调试设置已更新', 'success');
+ } catch (error) {
+ this.showNotification(`更新调试设置失败: ${error.message}`, 'error');
+ // 恢复原状态
+ document.getElementById('debug-toggle').checked = !enabled;
+ }
+ }
+
+ // 加载代理设置
+ async loadProxySettings() {
+ try {
+ const data = await this.makeRequest('/proxy-url');
+ document.getElementById('proxy-url').value = data['proxy-url'] || '';
+ } catch (error) {
+ console.error('加载代理设置失败:', error);
+ }
+ }
+
+ // 更新代理URL
+ async updateProxyUrl() {
+ const proxyUrl = document.getElementById('proxy-url').value.trim();
+
+ try {
+ await this.makeRequest('/proxy-url', {
+ method: 'PUT',
+ body: JSON.stringify({ value: proxyUrl })
+ });
+ this.showNotification('代理设置已更新', 'success');
+ } catch (error) {
+ this.showNotification(`更新代理设置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 清空代理URL
+ async clearProxyUrl() {
+ try {
+ await this.makeRequest('/proxy-url', { method: 'DELETE' });
+ document.getElementById('proxy-url').value = '';
+ this.showNotification('代理设置已清空', 'success');
+ } catch (error) {
+ this.showNotification(`清空代理设置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载重试设置
+ async loadRetrySettings() {
+ try {
+ const data = await this.makeRequest('/request-retry');
+ document.getElementById('request-retry').value = data['request-retry'];
+ } catch (error) {
+ console.error('加载重试设置失败:', error);
+ }
+ }
+
+ // 更新请求重试
+ async updateRequestRetry() {
+ const retryCount = parseInt(document.getElementById('request-retry').value);
+
+ try {
+ await this.makeRequest('/request-retry', {
+ method: 'PUT',
+ body: JSON.stringify({ value: retryCount })
+ });
+ this.showNotification('重试设置已更新', 'success');
+ } catch (error) {
+ this.showNotification(`更新重试设置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载配额设置
+ async loadQuotaSettings() {
+ try {
+ const [switchProject, switchPreview] = await Promise.all([
+ this.makeRequest('/quota-exceeded/switch-project'),
+ this.makeRequest('/quota-exceeded/switch-preview-model')
+ ]);
+
+ document.getElementById('switch-project-toggle').checked = switchProject['switch-project'];
+ document.getElementById('switch-preview-model-toggle').checked = switchPreview['switch-preview-model'];
+ } catch (error) {
+ console.error('加载配额设置失败:', error);
+ }
+ }
+
+ // 更新项目切换设置
+ async updateSwitchProject(enabled) {
+ try {
+ await this.makeRequest('/quota-exceeded/switch-project', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.showNotification('项目切换设置已更新', 'success');
+ } catch (error) {
+ this.showNotification(`更新项目切换设置失败: ${error.message}`, 'error');
+ document.getElementById('switch-project-toggle').checked = !enabled;
+ }
+ }
+
+ // 更新预览模型切换设置
+ async updateSwitchPreviewModel(enabled) {
+ try {
+ await this.makeRequest('/quota-exceeded/switch-preview-model', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.showNotification('预览模型切换设置已更新', 'success');
+ } catch (error) {
+ this.showNotification(`更新预览模型切换设置失败: ${error.message}`, 'error');
+ document.getElementById('switch-preview-model-toggle').checked = !enabled;
+ }
+ }
+
+ // 加载本地访问设置
+ async loadLocalhostSettings() {
+ try {
+ const data = await this.makeRequest('/allow-localhost-unauthenticated');
+ document.getElementById('allow-localhost-toggle').checked = data['allow-localhost-unauthenticated'];
+ } catch (error) {
+ console.error('加载本地访问设置失败:', error);
+ }
+ }
+
+ // 更新本地访问设置
+ async updateAllowLocalhost(enabled) {
+ try {
+ await this.makeRequest('/allow-localhost-unauthenticated', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.showNotification('本地访问设置已更新', 'success');
+ } catch (error) {
+ this.showNotification(`更新本地访问设置失败: ${error.message}`, 'error');
+ document.getElementById('allow-localhost-toggle').checked = !enabled;
+ }
+ }
+
+ // 加载API密钥
+ async loadApiKeys() {
+ try {
+ const data = await this.makeRequest('/api-keys');
+ this.renderApiKeys(data['api-keys'] || []);
+ } catch (error) {
+ console.error('加载API密钥失败:', error);
+ }
+ }
+
+ // 渲染API密钥列表
+ renderApiKeys(keys) {
+ const container = document.getElementById('api-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
暂无API密钥
+
点击上方按钮添加第一个密钥
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((key, index) => `
+
+
+
API密钥 #${index + 1}
+
${this.maskApiKey(key)}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 遮蔽API密钥显示
+ maskApiKey(key) {
+ if (key.length <= 8) return key;
+ return key.substring(0, 4) + '...' + key.substring(key.length - 4);
+ }
+
+ // 显示添加API密钥模态框
+ showAddApiKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 添加API密钥
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加API密钥
+ async addApiKey() {
+ const newKey = document.getElementById('new-api-key').value.trim();
+
+ if (!newKey) {
+ this.showNotification('请输入API密钥', 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/api-keys');
+ const currentKeys = data['api-keys'] || [];
+ currentKeys.push(newKey);
+
+ await this.makeRequest('/api-keys', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.closeModal();
+ this.loadApiKeys();
+ this.showNotification('API密钥添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加API密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑API密钥
+ editApiKey(index, currentKey) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 编辑API密钥
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新API密钥
+ async updateApiKey(index) {
+ const newKey = document.getElementById('edit-api-key').value.trim();
+
+ if (!newKey) {
+ this.showNotification('请输入API密钥', 'error');
+ return;
+ }
+
+ try {
+ await this.makeRequest('/api-keys', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: newKey })
+ });
+
+ this.closeModal();
+ this.loadApiKeys();
+ this.showNotification('API密钥更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新API密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除API密钥
+ async deleteApiKey(index) {
+ if (!confirm('确定要删除这个API密钥吗?')) return;
+
+ try {
+ await this.makeRequest(`/api-keys?index=${index}`, { method: 'DELETE' });
+ this.loadApiKeys();
+ this.showNotification('API密钥删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除API密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载Gemini密钥
+ async loadGeminiKeys() {
+ try {
+ const data = await this.makeRequest('/generative-language-api-key');
+ this.renderGeminiKeys(data['generative-language-api-key'] || []);
+ } catch (error) {
+ console.error('加载Gemini密钥失败:', error);
+ }
+ }
+
+ // 渲染Gemini密钥列表
+ renderGeminiKeys(keys) {
+ const container = document.getElementById('gemini-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
暂无Gemini密钥
+
点击上方按钮添加第一个密钥
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((key, index) => `
+
+
+
Gemini密钥 #${index + 1}
+
${this.maskApiKey(key)}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加Gemini密钥模态框
+ showAddGeminiKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 添加Gemini API密钥
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加Gemini密钥
+ async addGeminiKey() {
+ const newKey = document.getElementById('new-gemini-key').value.trim();
+
+ if (!newKey) {
+ this.showNotification('请输入Gemini API密钥', 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/generative-language-api-key');
+ const currentKeys = data['generative-language-api-key'] || [];
+ currentKeys.push(newKey);
+
+ await this.makeRequest('/generative-language-api-key', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.closeModal();
+ this.loadGeminiKeys();
+ this.showNotification('Gemini密钥添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑Gemini密钥
+ editGeminiKey(index, currentKey) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 编辑Gemini API密钥
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新Gemini密钥
+ async updateGeminiKey(oldKey) {
+ const newKey = document.getElementById('edit-gemini-key').value.trim();
+
+ if (!newKey) {
+ this.showNotification('请输入Gemini API密钥', 'error');
+ return;
+ }
+
+ try {
+ await this.makeRequest('/generative-language-api-key', {
+ method: 'PATCH',
+ body: JSON.stringify({ old: oldKey, new: newKey })
+ });
+
+ this.closeModal();
+ this.loadGeminiKeys();
+ this.showNotification('Gemini密钥更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除Gemini密钥
+ async deleteGeminiKey(key) {
+ if (!confirm('确定要删除这个Gemini密钥吗?')) return;
+
+ try {
+ await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' });
+ this.loadGeminiKeys();
+ this.showNotification('Gemini密钥删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载Codex密钥
+ async loadCodexKeys() {
+ try {
+ const data = await this.makeRequest('/codex-api-key');
+ this.renderCodexKeys(data['codex-api-key'] || []);
+ } catch (error) {
+ console.error('加载Codex密钥失败:', error);
+ }
+ }
+
+ // 渲染Codex密钥列表
+ renderCodexKeys(keys) {
+ const container = document.getElementById('codex-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
暂无Codex配置
+
点击上方按钮添加第一个配置
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((config, index) => `
+
+
+
Codex配置 #${index + 1}
+
密钥: ${this.maskApiKey(config['api-key'])}
+ ${config['base-url'] ? `
地址: ${config['base-url']}
` : ''}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加Codex密钥模态框
+ showAddCodexKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 添加Codex API配置
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加Codex密钥
+ async addCodexKey() {
+ const apiKey = document.getElementById('new-codex-key').value.trim();
+ const baseUrl = document.getElementById('new-codex-url').value.trim();
+
+ if (!apiKey) {
+ this.showNotification('请输入API密钥', 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/codex-api-key');
+ const currentKeys = data['codex-api-key'] || [];
+
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+
+ currentKeys.push(newConfig);
+
+ await this.makeRequest('/codex-api-key', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.closeModal();
+ this.loadCodexKeys();
+ this.showNotification('Codex配置添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加Codex配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑Codex密钥
+ editCodexKey(index, config) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 编辑Codex API配置
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新Codex密钥
+ async updateCodexKey(index) {
+ const apiKey = document.getElementById('edit-codex-key').value.trim();
+ const baseUrl = document.getElementById('edit-codex-url').value.trim();
+
+ if (!apiKey) {
+ this.showNotification('请输入API密钥', 'error');
+ return;
+ }
+
+ try {
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+
+ await this.makeRequest('/codex-api-key', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: newConfig })
+ });
+
+ this.closeModal();
+ this.loadCodexKeys();
+ this.showNotification('Codex配置更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新Codex配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除Codex密钥
+ async deleteCodexKey(apiKey) {
+ if (!confirm('确定要删除这个Codex配置吗?')) return;
+
+ try {
+ await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
+ this.loadCodexKeys();
+ this.showNotification('Codex配置删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除Codex配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载Claude密钥
+ async loadClaudeKeys() {
+ try {
+ const data = await this.makeRequest('/claude-api-key');
+ this.renderClaudeKeys(data['claude-api-key'] || []);
+ } catch (error) {
+ console.error('加载Claude密钥失败:', error);
+ }
+ }
+
+ // 渲染Claude密钥列表
+ renderClaudeKeys(keys) {
+ const container = document.getElementById('claude-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
暂无Claude配置
+
点击上方按钮添加第一个配置
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((config, index) => `
+
+
+
Claude配置 #${index + 1}
+
密钥: ${this.maskApiKey(config['api-key'])}
+ ${config['base-url'] ? `
地址: ${config['base-url']}
` : ''}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加Claude密钥模态框
+ showAddClaudeKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 添加Claude API配置
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加Claude密钥
+ async addClaudeKey() {
+ const apiKey = document.getElementById('new-claude-key').value.trim();
+ const baseUrl = document.getElementById('new-claude-url').value.trim();
+
+ if (!apiKey) {
+ this.showNotification('请输入API密钥', 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/claude-api-key');
+ const currentKeys = data['claude-api-key'] || [];
+
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+
+ currentKeys.push(newConfig);
+
+ await this.makeRequest('/claude-api-key', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.closeModal();
+ this.loadClaudeKeys();
+ this.showNotification('Claude配置添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加Claude配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑Claude密钥
+ editClaudeKey(index, config) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 编辑Claude API配置
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新Claude密钥
+ async updateClaudeKey(index) {
+ const apiKey = document.getElementById('edit-claude-key').value.trim();
+ const baseUrl = document.getElementById('edit-claude-url').value.trim();
+
+ if (!apiKey) {
+ this.showNotification('请输入API密钥', 'error');
+ return;
+ }
+
+ try {
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+
+ await this.makeRequest('/claude-api-key', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: newConfig })
+ });
+
+ this.closeModal();
+ this.loadClaudeKeys();
+ this.showNotification('Claude配置更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新Claude配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除Claude密钥
+ async deleteClaudeKey(apiKey) {
+ if (!confirm('确定要删除这个Claude配置吗?')) return;
+
+ try {
+ await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
+ this.loadClaudeKeys();
+ this.showNotification('Claude配置删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除Claude配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载OpenAI提供商
+ async loadOpenAIProviders() {
+ try {
+ const data = await this.makeRequest('/openai-compatibility');
+ this.renderOpenAIProviders(data['openai-compatibility'] || []);
+ } catch (error) {
+ console.error('加载OpenAI提供商失败:', error);
+ }
+ }
+
+ // 渲染OpenAI提供商列表
+ renderOpenAIProviders(providers) {
+ const container = document.getElementById('openai-providers-list');
+
+ if (providers.length === 0) {
+ container.innerHTML = `
+
+
+
暂无OpenAI兼容提供商
+
点击上方按钮添加第一个提供商
+
+ `;
+ return;
+ }
+
+ container.innerHTML = providers.map((provider, index) => `
+
+
+
${provider.name}
+
地址: ${provider['base-url']}
+
密钥数量: ${(provider['api-keys'] || []).length}
+
模型数量: ${(provider.models || []).length}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加OpenAI提供商模态框
+ showAddOpenAIProviderModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 添加OpenAI兼容提供商
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加OpenAI提供商
+ async addOpenAIProvider() {
+ const name = document.getElementById('new-provider-name').value.trim();
+ const baseUrl = document.getElementById('new-provider-url').value.trim();
+ const keysText = document.getElementById('new-provider-keys').value.trim();
+
+ if (!name || !baseUrl) {
+ this.showNotification('请填写提供商名称和Base URL', 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/openai-compatibility');
+ const currentProviders = data['openai-compatibility'] || [];
+
+ const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
+
+ const newProvider = {
+ name,
+ 'base-url': baseUrl,
+ 'api-keys': apiKeys,
+ models: []
+ };
+
+ currentProviders.push(newProvider);
+
+ await this.makeRequest('/openai-compatibility', {
+ method: 'PUT',
+ body: JSON.stringify(currentProviders)
+ });
+
+ this.closeModal();
+ this.loadOpenAIProviders();
+ this.showNotification('OpenAI提供商添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑OpenAI提供商
+ editOpenAIProvider(index, provider) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ const apiKeysText = (provider['api-keys'] || []).join('\n');
+
+ modalBody.innerHTML = `
+ 编辑OpenAI兼容提供商
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新OpenAI提供商
+ async updateOpenAIProvider(index) {
+ const name = document.getElementById('edit-provider-name').value.trim();
+ const baseUrl = document.getElementById('edit-provider-url').value.trim();
+ const keysText = document.getElementById('edit-provider-keys').value.trim();
+
+ if (!name || !baseUrl) {
+ this.showNotification('请填写提供商名称和Base URL', 'error');
+ return;
+ }
+
+ try {
+ const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
+
+ const updatedProvider = {
+ name,
+ 'base-url': baseUrl,
+ 'api-keys': apiKeys,
+ models: []
+ };
+
+ await this.makeRequest('/openai-compatibility', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: updatedProvider })
+ });
+
+ this.closeModal();
+ this.loadOpenAIProviders();
+ this.showNotification('OpenAI提供商更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除OpenAI提供商
+ async deleteOpenAIProvider(name) {
+ if (!confirm('确定要删除这个OpenAI提供商吗?')) return;
+
+ try {
+ await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
+ this.loadOpenAIProviders();
+ this.showNotification('OpenAI提供商删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载认证文件
+ async loadAuthFiles() {
+ try {
+ const data = await this.makeRequest('/auth-files');
+ this.renderAuthFiles(data.files || []);
+ } catch (error) {
+ console.error('加载认证文件失败:', error);
+ }
+ }
+
+ // 渲染认证文件列表
+ renderAuthFiles(files) {
+ const container = document.getElementById('auth-files-list');
+
+ if (files.length === 0) {
+ container.innerHTML = `
+
+
+
暂无认证文件
+
点击上方按钮上传第一个文件
+
+ `;
+ return;
+ }
+
+ container.innerHTML = files.map(file => `
+
+
+
${file.name}
+
大小: ${this.formatFileSize(file.size)}
+
修改时间: ${new Date(file.modtime).toLocaleString('zh-CN')}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 格式化文件大小
+ formatFileSize(bytes) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+
+ // 上传认证文件
+ uploadAuthFile() {
+ document.getElementById('auth-file-input').click();
+ }
+
+ // 处理文件上传
+ async handleFileUpload(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ if (!file.name.endsWith('.json')) {
+ this.showNotification('只能上传JSON文件', 'error');
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await fetch(`${this.apiUrl}/auth-files`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.managementKey}`
+ },
+ body: formData
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || `HTTP ${response.status}`);
+ }
+
+ this.loadAuthFiles();
+ this.showNotification('文件上传成功', 'success');
+ } catch (error) {
+ this.showNotification(`文件上传失败: ${error.message}`, 'error');
+ }
+
+ // 清空文件输入
+ event.target.value = '';
+ }
+
+ // 下载认证文件
+ async downloadAuthFile(filename) {
+ try {
+ const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, {
+ headers: {
+ 'Authorization': `Bearer ${this.managementKey}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ window.URL.revokeObjectURL(url);
+
+ this.showNotification('文件下载成功', 'success');
+ } catch (error) {
+ this.showNotification(`文件下载失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除认证文件
+ async deleteAuthFile(filename) {
+ if (!confirm(`确定要删除文件 "${filename}" 吗?`)) return;
+
+ try {
+ await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' });
+ this.loadAuthFiles();
+ this.showNotification('文件删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`文件删除失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除所有认证文件
+ async deleteAllAuthFiles() {
+ if (!confirm('确定要删除所有认证文件吗?此操作不可恢复!')) return;
+
+ try {
+ const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' });
+ this.loadAuthFiles();
+ this.showNotification(`成功删除 ${response.deleted} 个文件`, 'success');
+ } catch (error) {
+ this.showNotification(`删除文件失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 关闭模态框
+ closeModal() {
+ document.getElementById('modal').style.display = 'none';
+ }
+}
+
+// 全局管理器实例
+let manager;
+
+// 尝试自动加载根目录 Logo(支持多种常见文件名/扩展名)
+function setupSiteLogo() {
+ const img = document.getElementById('site-logo');
+ if (!img) return;
+ const candidates = [
+ '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif',
+ 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif',
+ '/logo.svg', '/logo.png', '/logo.jpg', '/logo.jpeg', '/logo.webp', '/logo.gif'
+ ];
+ let idx = 0;
+ const tryNext = () => {
+ if (idx >= candidates.length) return;
+ const test = new Image();
+ test.onload = () => {
+ img.src = test.src;
+ img.style.display = 'inline-block';
+ };
+ test.onerror = () => {
+ idx++;
+ tryNext();
+ };
+ test.src = candidates[idx];
+ };
+ tryNext();
+}
+
+// 页面加载完成后初始化
+document.addEventListener('DOMContentLoaded', () => {
+ setupSiteLogo();
+ manager = new CLIProxyManager();
+});
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f89bd42
--- /dev/null
+++ b/index.html
@@ -0,0 +1,312 @@
+
+
+
+
+
+ CLI Proxy API 管理界面
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 基础设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 允许本地未认证访问
+
+
+
+
+
+
+
+
+
+
+ AI 提供商配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 系统信息
+
+
+
+
+
+
+ API 状态:
+ 未连接
+
+
+ 最后更新:
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/logo.jpg b/logo.jpg
new file mode 100644
index 0000000..f701a6c
Binary files /dev/null and b/logo.jpg differ
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0ab12cd
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "cli-proxy-api-webui",
+ "version": "1.0.0",
+ "description": "CLI Proxy API 管理界面",
+ "main": "index.html",
+ "scripts": {
+ "start": "npx serve .",
+ "dev": "npx serve . --port 3000",
+ "build": "echo '无需构建,直接使用静态文件'",
+ "lint": "echo '使用浏览器开发者工具检查代码'"
+ },
+ "keywords": [
+ "cli-proxy-api",
+ "webui",
+ "management",
+ "api"
+ ],
+ "author": "CLI Proxy API WebUI",
+ "license": "MIT",
+ "devDependencies": {
+ "serve": "^14.2.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "local"
+ },
+ "dependencies": {}
+}
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..ed874f1
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,672 @@
+/* 全局样式 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ /* 更沉稳的浅色中性背景 */
+ background: linear-gradient(135deg, #e5e7eb 0%, #f3f4f6 100%);
+ min-height: 100vh;
+ color: #333;
+}
+
+.container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+/* 头部样式 */
+.header {
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 15px;
+ padding: 48px 28px; /* 再次拉长,适配更大 Logo */
+ margin-bottom: 20px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header h1 {
+ color: #334155; /* slate-700 */
+ font-size: 1.6rem; /* 字体小一号 */
+ font-weight: 600;
+}
+
+.header h1.brand {
+ display: flex;
+ align-items: flex-end; /* 文字在 Logo 右下角(底对齐) */
+ gap: 18px;
+ line-height: 1; /* 减少标题内上下空白 */
+}
+
+.header h1 i {
+ color: #64748b; /* slate-500 */
+ margin-right: 10px;
+}
+
+/* 站点 Logo(嵌入头部) */
+#site-logo {
+ height: 88px; /* 继续增大 Logo */
+ width: auto;
+ display: inline-block;
+ object-fit: contain;
+ border-radius: 6px;
+ padding: 2px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.06);
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
+}
+
+.brand-title {
+ display: inline-block;
+ line-height: 1.1; /* 保持原字号不变,同时更紧凑 */
+ margin-bottom: 4px; /* 让文字更贴近更高的 Logo 底部 */
+}
+
+/* 小屏幕适配:避免 Logo 过大占位 */
+@media (max-width: 640px) {
+ #site-logo { height: 64px; }
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+}
+
+/* 认证区域 */
+.auth-section {
+ margin-bottom: 20px;
+}
+
+/* 主要内容区域 */
+.main-content {
+ display: flex;
+ gap: 20px;
+}
+
+/* 侧边栏 */
+.sidebar {
+ width: 250px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 15px;
+ padding: 20px;
+ height: fit-content;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+.nav-menu {
+ list-style: none;
+}
+
+.nav-menu li {
+ margin-bottom: 8px;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: #475569; /* slate-600 */
+ text-decoration: none;
+ border-radius: 10px;
+ transition: all 0.3s ease;
+ border: 2px solid transparent;
+}
+
+.nav-item:hover {
+ background: rgba(71, 85, 105, 0.12); /* slate-600 @ 12% */
+ color: #475569;
+}
+
+.nav-item.active {
+ background: linear-gradient(135deg, #475569, #334155);
+ color: white;
+ box-shadow: 0 4px 15px rgba(51, 65, 85, 0.35);
+}
+
+.nav-item i {
+ margin-right: 10px;
+ width: 20px;
+}
+
+/* 内容区域 */
+.content-area {
+ flex: 1;
+}
+
+.content-section {
+ display: none;
+}
+
+.content-section.active {
+ display: block;
+}
+
+.content-section h2 {
+ color: #334155; /* 更稳重的标题色 */
+ margin-bottom: 20px;
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+/* 卡片样式 */
+.card {
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 15px;
+ margin-bottom: 20px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.card-header {
+ background: linear-gradient(135deg, #f8fafc, #f1f5f9);
+ padding: 20px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.card-header h3 {
+ color: #334155;
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+.card-header h3 i {
+ color: #64748b;
+ margin-right: 10px;
+}
+
+.card-content {
+ padding: 20px;
+}
+
+/* 按钮样式 */
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, #475569, #334155);
+ color: white;
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(51, 65, 85, 0.45);
+}
+
+.btn-secondary {
+ background: #e2e8f0;
+ color: #4a5568;
+}
+
+.btn-secondary:hover {
+ background: #cbd5e0;
+ transform: translateY(-1px);
+}
+
+.btn-danger {
+ background: linear-gradient(135deg, #ef4444, #b91c1c);
+ color: white;
+}
+
+.btn-danger:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(185, 28, 28, 0.45);
+}
+
+.btn-success {
+ background: linear-gradient(135deg, #22c55e, #15803d);
+ color: white;
+}
+
+.btn-success:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(21, 128, 61, 0.45);
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+/* 表单元素 */
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: #4a5568;
+ font-weight: 500;
+}
+
+.form-hint {
+ margin-top: 6px;
+ color: #64748b; /* slate-500 */
+ font-size: 12px;
+}
+
+.input-group {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="number"],
+input[type="url"],
+textarea,
+select {
+ flex: 1;
+ padding: 12px 16px;
+ border: 2px solid #e2e8f0;
+ border-radius: 8px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+ background: white;
+}
+
+input:focus,
+textarea:focus,
+select:focus {
+ outline: none;
+ border-color: #475569;
+ box-shadow: 0 0 0 3px rgba(71, 85, 105, 0.15);
+}
+
+/* 切换开关 */
+.toggle-group {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.toggle-switch {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 25px;
+}
+
+.toggle-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ transition: .4s;
+ border-radius: 25px;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 19px;
+ width: 19px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ transition: .4s;
+ border-radius: 50%;
+}
+
+input:checked + .slider {
+ background: linear-gradient(135deg, #475569, #334155);
+}
+
+input:checked + .slider:before {
+ transform: translateX(25px);
+}
+
+.toggle-label {
+ color: #475569;
+ font-weight: 500;
+}
+
+/* 列表样式 */
+.key-list,
+.provider-list,
+.file-list {
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.key-item,
+.provider-item,
+.file-item {
+ background: #f7fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ padding: 15px;
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all 0.3s ease;
+}
+
+.key-item:hover,
+.provider-item:hover,
+.file-item:hover {
+ background: #edf2f7;
+ border-color: #cbd5e0;
+ transform: translateY(-1px);
+}
+
+.item-content {
+ flex: 1;
+}
+
+.item-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.item-title {
+ font-weight: 600;
+ color: #2d3748;
+ margin-bottom: 4px;
+}
+
+.item-subtitle {
+ color: #718096;
+ font-size: 0.9rem;
+}
+
+.item-value {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ background: #edf2f7;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ color: #4a5568;
+ word-break: break-all;
+}
+
+/* 状态信息 */
+.status-info {
+ background: #f7fafc;
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.status-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.status-item:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.status-label {
+ font-weight: 600;
+ color: #4a5568;
+}
+
+.status-value {
+ color: #718096;
+}
+
+/* 模态框 */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(5px);
+}
+
+.modal-content {
+ background-color: white;
+ margin: 10% auto;
+ padding: 0;
+ border-radius: 15px;
+ width: 90%;
+ max-width: 500px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ animation: modalSlideIn 0.3s ease;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.close {
+ color: #aaa;
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+ padding: 20px;
+ cursor: pointer;
+}
+
+.close:hover,
+.close:focus {
+ color: #475569;
+}
+
+#modal-body {
+ padding: 0 30px 30px 30px;
+}
+
+/* 通知 */
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 15px 20px;
+ border-radius: 8px;
+ color: white;
+ font-weight: 500;
+ z-index: 1001;
+ transform: translateX(400px);
+ transition: all 0.3s ease;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+}
+
+.notification.show {
+ transform: translateX(0);
+}
+
+.notification.success {
+ background: linear-gradient(135deg, #68d391, #38a169);
+}
+
+.notification.error {
+ background: linear-gradient(135deg, #fc8181, #e53e3e);
+}
+
+.notification.info {
+ background: linear-gradient(135deg, #63b3ed, #3182ce);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .main-content {
+ flex-direction: column;
+ }
+
+ .sidebar {
+ width: 100%;
+ order: 2;
+ }
+
+ .content-area {
+ order: 1;
+ }
+
+ .nav-menu {
+ display: flex;
+ overflow-x: auto;
+ gap: 10px;
+ }
+
+ .nav-menu li {
+ margin-bottom: 0;
+ flex-shrink: 0;
+ }
+
+ .header-content {
+ flex-direction: column;
+ gap: 15px;
+ text-align: center;
+ }
+
+ .input-group {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .card-header {
+ flex-direction: column;
+ gap: 15px;
+ align-items: flex-start;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+/* 加载动画 */
+.loading {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: #fff;
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 空状态 */
+.empty-state {
+ text-align: center;
+ padding: 40px 20px;
+ color: #718096;
+}
+
+.empty-state i {
+ font-size: 48px;
+ margin-bottom: 16px;
+ color: #cbd5e0;
+}
+
+.empty-state h3 {
+ margin-bottom: 8px;
+ color: #4a5568;
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: linear-gradient(135deg, #475569, #334155);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(135deg, #334155, #1f2937);
+}
+
+/* 连接状态指示器 */
+.connection-indicator {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ margin-right: 8px;
+}
+
+.connection-indicator.connected {
+ background-color: #68d391;
+ box-shadow: 0 0 8px rgba(104, 211, 145, 0.6);
+}
+
+.connection-indicator.disconnected {
+ background-color: #fc8181;
+ box-shadow: 0 0 8px rgba(252, 129, 129, 0.6);
+}
+
+.connection-indicator.connecting {
+ background-color: #fbb040;
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+}