Compare commits

...

9 Commits

Author SHA1 Message Date
Supra4E8C
3468fd8373 feat(app.js, styles): enhance connection state handling and update editor dimensions
- Added a new property to track the last connection state in CLIProxyManager.
- Updated the config editor availability logic to reflect changes in connection state.
- Increased minimum height for various editor components in styles to improve usability.
- Introduced responsive styles for smaller screens to ensure proper display of editor elements.
2025-11-10 18:07:31 +08:00
Supra4E8C
4f15c3f5c5 feat(app.js, i18n): enforce required base URL for Codex API configuration
- Updated the Codex API configuration modals to mark the Base URL field as required.
- Added validation to ensure the Base URL is provided before submission, with appropriate error notifications.
- Updated internationalization strings to reflect the change in the Base URL label from "Optional" to "Required" in both English and Chinese.
2025-11-10 12:16:46 +08:00
hkfires
72cd117aab fix(logs): exclude all /v0/management/ API entries from viewer 2025-11-10 10:40:28 +08:00
hkfires
5d62cd91f2 perf(cli): prefetch /usage, derive keyStats, reuse across renders 2025-11-10 10:32:57 +08:00
Supra4E8C
6837100dec feat(app.js, i18n, styles): implement custom headers functionality in API key management
- Added the ability to input custom HTTP headers for API keys in the CLIProxyManager.
- Introduced new methods for adding, populating, collecting, and applying custom headers.
- Updated the UI to include input fields for custom headers in the modals for adding and editing API keys.
- Enhanced internationalization strings to support custom headers in both English and Chinese.
- Added corresponding styles for the new header input fields and badges in the UI.
2025-11-09 18:27:29 +08:00
Supra4E8C
8542041981 feat(app.js, i18n): support batch addition of Gemini API keys with enhanced UI
- Updated the Gemini API key modal to allow input of multiple keys, each on a new line.
- Added a hint for users on how to input multiple keys and updated the corresponding internationalization strings.
- Enhanced the key addition logic to handle success, skipped, and failed counts, providing detailed feedback to users.
- Improved error handling and notifications for batch operations.
2025-11-09 18:06:56 +08:00
Supra4E8C
35ceab0dae feat(app.js, i18n): enhance Gemini API key management with base URL support
- Updated the CLIProxyManager to handle both API keys and optional base URLs for Gemini.
- Added new input fields in the modal for base URL entry during key addition and editing.
- Updated internationalization strings to include labels and placeholders for the base URL in both English and Chinese.
- Improved key normalization and rendering logic to accommodate the new structure.
2025-11-09 17:51:38 +08:00
Supra4E8C
d3fe186df7 refactor(app.js): remove modal click event listener to streamline modal behavior 2025-11-09 17:20:02 +08:00
Supra4E8C
5aff22a20b fix(ui): update modal styles for better responsiveness and overflow handling 2025-11-09 17:17:49 +08:00
4 changed files with 554 additions and 90 deletions

509
app.js
View File

@@ -41,6 +41,7 @@ class CLIProxyManager {
statusEl: null
};
this.lastConfigFetchUrl = null;
this.lastEditorConnectionState = null;
this.init();
}
@@ -701,13 +702,6 @@ class CLIProxyManager {
closeBtn.addEventListener('click', () => this.closeModal());
}
window.addEventListener('click', (e) => {
const modal = document.getElementById('modal');
if (modal && e.target === modal) {
this.closeModal();
}
});
// 移动端菜单按钮
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const sidebarOverlay = document.getElementById('sidebar-overlay');
@@ -974,6 +968,7 @@ class CLIProxyManager {
}
this.refreshConfigEditor();
this.lastEditorConnectionState = this.isConnected;
}
refreshConfigEditor() {
@@ -1333,7 +1328,9 @@ class CLIProxyManager {
lastUpdate.textContent = new Date().toLocaleString('zh-CN');
this.updateConfigEditorAvailability();
if (this.lastEditorConnectionState !== this.isConnected) {
this.updateConfigEditorAvailability();
}
// 更新连接信息显示
this.updateConnectionInfo();
@@ -1430,14 +1427,58 @@ class CLIProxyManager {
// 使用新的 /config 端点一次性获取所有配置
const config = await this.getConfig(forceRefresh);
// 从配置中提取并设置各个设置项
await this.updateSettingsFromConfig(config);
// 获取一次usage统计数据供渲染函数和loadUsageStats复用
let usageData = null;
let keyStats = null;
try {
const response = await this.makeRequest('/usage');
usageData = response?.usage || null;
if (usageData) {
// 从usage数据中提取keyStats
const sourceStats = {};
const apis = usageData.apis || {};
Object.values(apis).forEach(apiEntry => {
const models = apiEntry.models || {};
Object.values(models).forEach(modelEntry => {
const details = modelEntry.details || [];
details.forEach(detail => {
const source = detail.source;
if (!source) return;
if (!sourceStats[source]) {
sourceStats[source] = {
success: 0,
failure: 0
};
}
const isFailed = detail.failed === true;
if (isFailed) {
sourceStats[source].failure += 1;
} else {
sourceStats[source].success += 1;
}
});
});
});
keyStats = sourceStats;
}
} catch (error) {
console.warn('获取usage统计失败:', error);
}
// 从配置中提取并设置各个设置项现在传递keyStats
await this.updateSettingsFromConfig(config, keyStats);
// 认证文件需要单独加载,因为不在配置中
await this.loadAuthFiles();
await this.loadAuthFiles(keyStats);
// 使用统计需要单独加载
await this.loadUsageStats();
// 使用统计需要单独加载复用已获取的usage数据
await this.loadUsageStats(usageData);
// 加载配置文件编辑器内容
await this.loadConfigFileEditor(forceRefresh);
@@ -1453,7 +1494,7 @@ class CLIProxyManager {
}
// 从配置对象更新所有设置
async updateSettingsFromConfig(config) {
async updateSettingsFromConfig(config, keyStats = null) {
// 调试设置
if (config.debug !== undefined) {
document.getElementById('debug-toggle').checked = config.debug;
@@ -1502,16 +1543,16 @@ class CLIProxyManager {
}
// Gemini 密钥
await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []);
await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [], keyStats);
// Codex 密钥
await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : []);
await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [], keyStats);
// Claude 密钥
await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : []);
await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : [], keyStats);
// OpenAI 兼容提供商
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : []);
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [], keyStats);
}
// 回退方法:原来的逐个加载方式
@@ -1797,8 +1838,8 @@ class CLIProxyManager {
return;
}
// 过滤掉 /v0/management/logs 相关的日志
const filteredLines = lines.filter(line => !line.includes('/v0/management/logs'));
// 过滤掉 /v0/management/ 相关的日志
const filteredLines = lines.filter(line => !line.includes('/v0/management/'));
// 限制前端显示的最大行数
let displayedLines = filteredLines;
@@ -1838,8 +1879,8 @@ class CLIProxyManager {
const logsTextElement = logsContent.querySelector('.logs-text');
const logsInfoElement = logsContent.querySelector('.logs-info');
// 过滤掉 /v0/management/logs 相关的日志
const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/logs'));
// 过滤掉 /v0/management/ 相关的日志
const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/'));
if (filteredNewLines.length === 0) {
return; // 如果过滤后没有新日志,直接返回
}
@@ -2329,14 +2370,122 @@ class CLIProxyManager {
return [];
}
// 添加一行自定义请求头输入
addHeaderField(wrapperId, header = {}) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return;
const row = document.createElement('div');
row.className = 'header-input-row';
const keyValue = typeof header.key === 'string' ? header.key : '';
const valueValue = typeof header.value === 'string' ? header.value : '';
row.innerHTML = `
<div class="input-group header-input-group">
<input type="text" class="header-key-input" placeholder="${i18n.t('common.custom_headers_key_placeholder')}" value="${this.escapeHtml(keyValue)}">
<span class="header-separator">:</span>
<input type="text" class="header-value-input" placeholder="${i18n.t('common.custom_headers_value_placeholder')}" value="${this.escapeHtml(valueValue)}">
<button type="button" class="btn btn-small btn-danger header-remove-btn"><i class="fas fa-trash"></i></button>
</div>
`;
const removeBtn = row.querySelector('.header-remove-btn');
if (removeBtn) {
removeBtn.addEventListener('click', () => {
wrapper.removeChild(row);
if (wrapper.childElementCount === 0) {
this.addHeaderField(wrapperId);
}
});
}
wrapper.appendChild(row);
}
// 填充自定义请求头输入
populateHeaderFields(wrapperId, headers = null) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return;
wrapper.innerHTML = '';
const entries = (headers && typeof headers === 'object')
? Object.entries(headers).filter(([key, value]) => key && value !== undefined && value !== null)
: [];
if (!entries.length) {
this.addHeaderField(wrapperId);
return;
}
entries.forEach(([key, value]) => this.addHeaderField(wrapperId, { key, value: String(value ?? '') }));
}
// 收集自定义请求头输入
collectHeaderInputs(wrapperId) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return null;
const rows = Array.from(wrapper.querySelectorAll('.header-input-row'));
const headers = {};
rows.forEach(row => {
const keyInput = row.querySelector('.header-key-input');
const valueInput = row.querySelector('.header-value-input');
const key = keyInput ? keyInput.value.trim() : '';
const value = valueInput ? valueInput.value.trim() : '';
if (key && value) {
headers[key] = value;
}
});
return Object.keys(headers).length ? headers : null;
}
// 规范化并写入请求头
applyHeadersToConfig(target, headers) {
if (!target) {
return;
}
if (headers && typeof headers === 'object' && Object.keys(headers).length) {
target.headers = { ...headers };
} else {
delete target.headers;
}
}
// 渲染请求头徽章
renderHeaderBadges(headers) {
if (!headers || typeof headers !== 'object') {
return '';
}
const entries = Object.entries(headers).filter(([key, value]) => key && value !== undefined && value !== null && value !== '');
if (!entries.length) {
return '';
}
const badges = entries.map(([key, value]) => `
<span class="header-badge"><strong>${this.escapeHtml(key)}:</strong> ${this.escapeHtml(String(value))}</span>
`).join('');
return `
<div class="item-subtitle header-badges-wrapper">
<span class="header-badges-label">${i18n.t('common.custom_headers_label')}:</span>
<div class="header-badge-list">
${badges}
</div>
</div>
`;
}
// 构造Codex配置保持未展示的字段
buildCodexConfig(apiKey, baseUrl, proxyUrl, original = {}) {
return {
buildCodexConfig(apiKey, baseUrl, proxyUrl, original = {}, headers = null) {
const result = {
...original,
'api-key': apiKey,
'base-url': baseUrl || '',
'proxy-url': proxyUrl || ''
};
this.applyHeadersToConfig(result, headers);
return result;
}
// 显示添加API密钥模态框
@@ -2449,7 +2598,11 @@ class CLIProxyManager {
async loadGeminiKeys() {
try {
const config = await this.getConfig();
const keys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [];
let keys = Array.isArray(config['gemini-api-key']) ? config['gemini-api-key'] : [];
if (keys.length === 0) {
const legacyKeys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [];
keys = legacyKeys.map(key => ({ 'api-key': key }));
}
await this.renderGeminiKeys(keys);
} catch (error) {
console.error('加载Gemini密钥失败:', error);
@@ -2457,14 +2610,23 @@ class CLIProxyManager {
}
// 渲染Gemini密钥列表
async renderGeminiKeys(keys) {
async renderGeminiKeys(keys, keyStats = null) {
const container = document.getElementById('gemini-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
const normalizedList = (Array.isArray(keys) ? keys : []).map(item => {
if (item && typeof item === 'object') {
return { ...item };
}
if (typeof item === 'string') {
return { 'api-key': item };
}
return null;
}).filter(config => config && config['api-key']);
this.cachedGeminiKeys = normalizedList;
if (list.length === 0) {
if (normalizedList.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fab fa-google"></i>
@@ -2475,17 +2637,25 @@ class CLIProxyManager {
return;
}
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
// 使用传入的keyStats如果没有则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((key, index) => {
const masked = this.maskApiKey(key);
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 };
container.innerHTML = normalizedList.map((config, index) => {
const rawKey = config['api-key'] || '';
const masked = rawKey ? this.maskApiKey(rawKey) : '';
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
const configJson = JSON.stringify(config).replace(/"/g, '&quot;');
const apiKeyJson = JSON.stringify(rawKey || '').replace(/"/g, '&quot;');
return `
<div class="key-item">
<div class="item-content">
<div class="item-title">${i18n.t('ai_providers.gemini_item_title')} #${index + 1}</div>
<div class="item-value">${this.maskApiKey(key)}</div>
<div class="item-value">${this.maskApiKey(rawKey || '')}</div>
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
${this.renderHeaderBadges(config.headers)}
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
@@ -2496,10 +2666,10 @@ class CLIProxyManager {
</div>
</div>
<div class="item-actions">
<button class="btn btn-secondary" onclick="manager.editGeminiKey(${index}, '${key}')">
<button class="btn btn-secondary" onclick="manager.editGeminiKey(${index}, ${configJson})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-danger" onclick="manager.deleteGeminiKey('${key}')">
<button class="btn btn-danger" onclick="manager.deleteGeminiKey(${apiKeyJson})">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -2516,7 +2686,18 @@ class CLIProxyManager {
<h3>${i18n.t('ai_providers.gemini_add_modal_title')}</h3>
<div class="form-group">
<label for="new-gemini-key">${i18n.t('ai_providers.gemini_add_modal_key_label')}</label>
<input type="text" id="new-gemini-key" placeholder="${i18n.t('ai_providers.gemini_add_modal_key_placeholder')}">
<textarea id="new-gemini-key" rows="6" placeholder="${i18n.t('ai_providers.gemini_add_modal_key_placeholder')}"></textarea>
<p class="form-hint">${i18n.t('ai_providers.gemini_add_modal_key_hint')}</p>
</div>
<div class="form-group">
<label for="new-gemini-url">${i18n.t('ai_providers.gemini_add_modal_url_label')}</label>
<input type="text" id="new-gemini-url" placeholder="${i18n.t('ai_providers.gemini_add_modal_url_placeholder')}">
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="new-gemini-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-gemini-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
@@ -2525,59 +2706,138 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
this.populateHeaderFields('new-gemini-headers-wrapper');
}
// 添加Gemini密钥
async addGeminiKey() {
const newKey = document.getElementById('new-gemini-key').value.trim();
const keyInput = document.getElementById('new-gemini-key');
const baseUrlInput = document.getElementById('new-gemini-url');
if (!keyInput) {
return;
}
if (!newKey) {
this.showNotification(i18n.t('notification.please_enter') + ' ' + i18n.t('notification.gemini_api_key'), 'error');
const keys = keyInput.value
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : '';
const headers = this.collectHeaderInputs('new-gemini-headers-wrapper');
if (keys.length === 0) {
this.showNotification(i18n.t('notification.gemini_multi_input_required'), 'error');
return;
}
try {
const data = await this.makeRequest('/generative-language-api-key');
const currentKeys = data['generative-language-api-key'] || [];
currentKeys.push(newKey);
const data = await this.makeRequest('/gemini-api-key');
let currentKeys = Array.isArray(data['gemini-api-key']) ? data['gemini-api-key'] : [];
const existingKeys = new Set(currentKeys.map(item => item && item['api-key']).filter(Boolean));
const batchSeen = new Set();
await this.makeRequest('/generative-language-api-key', {
method: 'PUT',
body: JSON.stringify(currentKeys)
});
let successCount = 0;
let skippedCount = 0;
let failedCount = 0;
for (const apiKey of keys) {
if (!apiKey) {
continue;
}
if (batchSeen.has(apiKey)) {
skippedCount++;
continue;
}
batchSeen.add(apiKey);
if (existingKeys.has(apiKey)) {
skippedCount++;
continue;
}
const newConfig = { 'api-key': apiKey };
if (baseUrl) {
newConfig['base-url'] = baseUrl;
} else {
delete newConfig['base-url'];
}
this.applyHeadersToConfig(newConfig, headers);
const nextKeys = [...currentKeys, newConfig];
try {
await this.makeRequest('/gemini-api-key', {
method: 'PUT',
body: JSON.stringify(nextKeys)
});
currentKeys = nextKeys;
existingKeys.add(apiKey);
successCount++;
} catch (error) {
failedCount++;
console.error('Gemini key add failed:', error);
}
}
this.clearCache(); // 清除缓存
this.closeModal();
this.loadGeminiKeys();
this.showNotification(i18n.t('notification.gemini_key_added'), 'success');
if (successCount === 1 && skippedCount === 0 && failedCount === 0) {
this.showNotification(i18n.t('notification.gemini_key_added'), 'success');
return;
}
const summaryTemplate = i18n.t('notification.gemini_multi_summary');
const summary = summaryTemplate
.replace('{success}', successCount)
.replace('{skipped}', skippedCount)
.replace('{failed}', failedCount);
const status = failedCount > 0 ? 'warning' : (successCount > 0 ? 'success' : 'info');
this.showNotification(summary, status);
} catch (error) {
this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error');
this.showNotification(`${i18n.t('notification.gemini_multi_failed')}: ${error.message}`, 'error');
}
}
// 编辑Gemini密钥
editGeminiKey(index, currentKey) {
editGeminiKey(index, config) {
const modal = document.getElementById('modal');
const modalBody = document.getElementById('modal-body');
this.currentGeminiEditConfig = config || {};
modalBody.innerHTML = `
<h3>${i18n.t('ai_providers.gemini_edit_modal_title')}</h3>
<div class="form-group">
<label for="edit-gemini-key">${i18n.t('ai_providers.gemini_edit_modal_key_label')}</label>
<input type="text" id="edit-gemini-key" value="${currentKey}">
<input type="text" id="edit-gemini-key" value="${config['api-key'] ? this.escapeHtml(config['api-key']) : ''}">
</div>
<div class="form-group">
<label for="edit-gemini-url">${i18n.t('ai_providers.gemini_edit_modal_url_label')}</label>
<input type="text" id="edit-gemini-url" value="${config['base-url'] ? this.escapeHtml(config['base-url']) : ''}" placeholder="${i18n.t('ai_providers.gemini_add_modal_url_placeholder')}">
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="edit-gemini-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-gemini-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateGeminiKey('${currentKey}')">${i18n.t('common.update')}</button>
<button class="btn btn-primary" onclick="manager.updateGeminiKey(${index})">${i18n.t('common.update')}</button>
</div>
`;
modal.style.display = 'block';
this.populateHeaderFields('edit-gemini-headers-wrapper', config.headers || null);
}
// 更新Gemini密钥
async updateGeminiKey(oldKey) {
async updateGeminiKey(index) {
const newKey = document.getElementById('edit-gemini-key').value.trim();
const baseUrlInput = document.getElementById('edit-gemini-url');
const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : '';
const headers = this.collectHeaderInputs('edit-gemini-headers-wrapper');
if (!newKey) {
this.showNotification(i18n.t('notification.please_enter') + ' ' + i18n.t('notification.gemini_api_key'), 'error');
@@ -2585,14 +2845,24 @@ class CLIProxyManager {
}
try {
await this.makeRequest('/generative-language-api-key', {
const existingConfig = (this.cachedGeminiKeys && this.cachedGeminiKeys[index]) || this.currentGeminiEditConfig || {};
const newConfig = { ...existingConfig, 'api-key': newKey };
if (baseUrl) {
newConfig['base-url'] = baseUrl;
} else {
delete newConfig['base-url'];
}
this.applyHeadersToConfig(newConfig, headers);
await this.makeRequest('/gemini-api-key', {
method: 'PATCH',
body: JSON.stringify({ old: oldKey, new: newKey })
body: JSON.stringify({ index, value: newConfig })
});
this.clearCache(); // 清除缓存
this.closeModal();
this.loadGeminiKeys();
this.currentGeminiEditConfig = null;
this.showNotification(i18n.t('notification.gemini_key_updated'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
@@ -2600,11 +2870,11 @@ class CLIProxyManager {
}
// 删除Gemini密钥
async deleteGeminiKey(key) {
async deleteGeminiKey(apiKey) {
if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return;
try {
await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' });
await this.makeRequest(`/gemini-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
this.clearCache(); // 清除缓存
this.loadGeminiKeys();
this.showNotification(i18n.t('notification.gemini_key_deleted'), 'success');
@@ -2625,7 +2895,7 @@ class CLIProxyManager {
}
// 渲染Codex密钥列表
async renderCodexKeys(keys) {
async renderCodexKeys(keys, keyStats = null) {
const container = document.getElementById('codex-keys-list');
if (!container) {
return;
@@ -2643,8 +2913,11 @@ class CLIProxyManager {
return;
}
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
// 使用传入的keyStats如果没有则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'];
@@ -2657,6 +2930,7 @@ class CLIProxyManager {
<div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div>
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
${this.renderHeaderBadges(config.headers)}
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
@@ -2691,12 +2965,18 @@ class CLIProxyManager {
</div>
<div class="form-group">
<label for="new-codex-url">${i18n.t('ai_providers.codex_add_modal_url_label')}</label>
<input type="text" id="new-codex-url" placeholder="${i18n.t('ai_providers.codex_add_modal_url_placeholder')}">
<input type="text" id="new-codex-url" placeholder="${i18n.t('ai_providers.codex_add_modal_url_placeholder')}" required>
</div>
<div class="form-group">
<label for="new-codex-proxy">${i18n.t('ai_providers.codex_add_modal_proxy_label')}</label>
<input type="text" id="new-codex-proxy" placeholder="${i18n.t('ai_providers.codex_add_modal_proxy_placeholder')}">
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="new-codex-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-codex-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.addCodexKey()">${i18n.t('common.add')}</button>
@@ -2704,6 +2984,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
this.populateHeaderFields('new-codex-headers-wrapper');
}
// 添加Codex密钥
@@ -2711,17 +2992,22 @@ class CLIProxyManager {
const apiKey = document.getElementById('new-codex-key').value.trim();
const baseUrl = document.getElementById('new-codex-url').value.trim();
const proxyUrl = document.getElementById('new-codex-proxy').value.trim();
const headers = this.collectHeaderInputs('new-codex-headers-wrapper');
if (!apiKey) {
this.showNotification(i18n.t('notification.field_required'), 'error');
return;
}
if (!baseUrl) {
this.showNotification(i18n.t('notification.codex_base_url_required'), 'error');
return;
}
try {
const data = await this.makeRequest('/codex-api-key');
const currentKeys = this.normalizeArrayResponse(data, 'codex-api-key').map(item => ({ ...item }));
const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl);
const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, {}, headers);
currentKeys.push(newConfig);
@@ -2752,12 +3038,18 @@ class CLIProxyManager {
</div>
<div class="form-group">
<label for="edit-codex-url">${i18n.t('ai_providers.codex_edit_modal_url_label')}</label>
<input type="text" id="edit-codex-url" value="${config['base-url'] || ''}">
<input type="text" id="edit-codex-url" value="${config['base-url'] || ''}" required>
</div>
<div class="form-group">
<label for="edit-codex-proxy">${i18n.t('ai_providers.codex_edit_modal_proxy_label')}</label>
<input type="text" id="edit-codex-proxy" value="${config['proxy-url'] || ''}">
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="edit-codex-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-codex-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">${i18n.t('common.update')}</button>
@@ -2765,6 +3057,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
this.populateHeaderFields('edit-codex-headers-wrapper', config.headers || null);
}
// 更新Codex密钥
@@ -2772,11 +3065,16 @@ class CLIProxyManager {
const apiKey = document.getElementById('edit-codex-key').value.trim();
const baseUrl = document.getElementById('edit-codex-url').value.trim();
const proxyUrl = document.getElementById('edit-codex-proxy').value.trim();
const headers = this.collectHeaderInputs('edit-codex-headers-wrapper');
if (!apiKey) {
this.showNotification(i18n.t('notification.field_required'), 'error');
return;
}
if (!baseUrl) {
this.showNotification(i18n.t('notification.codex_base_url_required'), 'error');
return;
}
try {
const listResponse = await this.makeRequest('/codex-api-key');
@@ -2787,7 +3085,7 @@ class CLIProxyManager {
}
const original = currentList[index] ? { ...currentList[index] } : {};
const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, original);
const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, original, headers);
await this.makeRequest('/codex-api-key', {
method: 'PATCH',
@@ -2829,7 +3127,7 @@ class CLIProxyManager {
}
// 渲染Claude密钥列表
async renderClaudeKeys(keys) {
async renderClaudeKeys(keys, keyStats = null) {
const container = document.getElementById('claude-keys-list');
if (!container) {
return;
@@ -2847,8 +3145,11 @@ class CLIProxyManager {
return;
}
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
// 使用传入的keyStats如果没有则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'];
@@ -2861,6 +3162,7 @@ class CLIProxyManager {
<div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div>
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
${this.renderHeaderBadges(config.headers)}
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
@@ -2901,6 +3203,12 @@ class CLIProxyManager {
<label for="new-claude-proxy">${i18n.t('ai_providers.claude_add_modal_proxy_label')}</label>
<input type="text" id="new-claude-proxy" placeholder="${i18n.t('ai_providers.claude_add_modal_proxy_placeholder')}">
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="new-claude-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-claude-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.addClaudeKey()">${i18n.t('common.add')}</button>
@@ -2908,6 +3216,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
this.populateHeaderFields('new-claude-headers-wrapper');
}
// 添加Claude密钥
@@ -2915,6 +3224,7 @@ class CLIProxyManager {
const apiKey = document.getElementById('new-claude-key').value.trim();
const baseUrl = document.getElementById('new-claude-url').value.trim();
const proxyUrl = document.getElementById('new-claude-proxy').value.trim();
const headers = this.collectHeaderInputs('new-claude-headers-wrapper');
if (!apiKey) {
this.showNotification(i18n.t('notification.field_required'), 'error');
@@ -2932,6 +3242,7 @@ class CLIProxyManager {
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
this.applyHeadersToConfig(newConfig, headers);
currentKeys.push(newConfig);
@@ -2968,6 +3279,12 @@ class CLIProxyManager {
<label for="edit-claude-proxy">${i18n.t('ai_providers.claude_edit_modal_proxy_label')}</label>
<input type="text" id="edit-claude-proxy" value="${config['proxy-url'] || ''}">
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="edit-claude-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-claude-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">${i18n.t('common.update')}</button>
@@ -2975,6 +3292,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
this.populateHeaderFields('edit-claude-headers-wrapper', config.headers || null);
}
// 更新Claude密钥
@@ -2982,6 +3300,7 @@ class CLIProxyManager {
const apiKey = document.getElementById('edit-claude-key').value.trim();
const baseUrl = document.getElementById('edit-claude-url').value.trim();
const proxyUrl = document.getElementById('edit-claude-proxy').value.trim();
const headers = this.collectHeaderInputs('edit-claude-headers-wrapper');
if (!apiKey) {
this.showNotification(i18n.t('notification.field_required'), 'error');
@@ -2996,6 +3315,7 @@ class CLIProxyManager {
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
this.applyHeadersToConfig(newConfig, headers);
await this.makeRequest('/claude-api-key', {
method: 'PATCH',
@@ -3037,7 +3357,7 @@ class CLIProxyManager {
}
// 渲染OpenAI提供商列表
async renderOpenAIProviders(providers) {
async renderOpenAIProviders(providers, keyStats = null) {
const container = document.getElementById('openai-providers-list');
if (!container) {
return;
@@ -3067,8 +3387,11 @@ class CLIProxyManager {
container.style.overflowY = '';
}
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
// 使用传入的keyStats如果没有则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((provider, index) => {
const item = typeof provider === 'object' && provider !== null ? provider : {};
@@ -3104,6 +3427,7 @@ class CLIProxyManager {
<div class="item-content">
<div class="item-title">${this.escapeHtml(name)}</div>
<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}</div>
${this.renderHeaderBadges(item.headers)}
<div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${apiKeyEntries.length}</div>
<div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${models.length}</div>
${this.renderOpenAIModelBadges(models)}
@@ -3151,6 +3475,12 @@ class CLIProxyManager {
<label for="new-provider-proxies">${i18n.t('ai_providers.openai_add_modal_keys_proxy_label')}</label>
<textarea id="new-provider-proxies" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_proxy_placeholder')}"></textarea>
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="new-openai-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-openai-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="form-group">
<label>${i18n.t('ai_providers.openai_add_modal_models_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p>
@@ -3165,6 +3495,7 @@ class CLIProxyManager {
modal.style.display = 'block';
this.populateModelFields('new-provider-models-wrapper', []);
this.populateHeaderFields('new-openai-headers-wrapper');
}
// 添加OpenAI提供商
@@ -3174,6 +3505,7 @@ class CLIProxyManager {
const keysText = document.getElementById('new-provider-keys').value.trim();
const proxiesText = document.getElementById('new-provider-proxies').value.trim();
const models = this.collectModelInputs('new-provider-models-wrapper');
const headers = this.collectHeaderInputs('new-openai-headers-wrapper');
if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
return;
@@ -3196,6 +3528,7 @@ class CLIProxyManager {
'api-key-entries': apiKeyEntries,
models
};
this.applyHeadersToConfig(newProvider, headers);
currentProviders.push(newProvider);
@@ -3250,6 +3583,12 @@ class CLIProxyManager {
<label for="edit-provider-proxies">${i18n.t('ai_providers.openai_edit_modal_keys_proxy_label')}</label>
<textarea id="edit-provider-proxies" rows="3">${this.escapeHtml(proxiesText)}</textarea>
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
<div id="edit-openai-headers-wrapper" class="header-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-openai-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
</div>
<div class="form-group">
<label>${i18n.t('ai_providers.openai_edit_modal_models_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p>
@@ -3264,6 +3603,7 @@ class CLIProxyManager {
modal.style.display = 'block';
this.populateModelFields('edit-provider-models-wrapper', models);
this.populateHeaderFields('edit-openai-headers-wrapper', provider?.headers || null);
}
// 更新OpenAI提供商
@@ -3273,6 +3613,7 @@ class CLIProxyManager {
const keysText = document.getElementById('edit-provider-keys').value.trim();
const proxiesText = document.getElementById('edit-provider-proxies').value.trim();
const models = this.collectModelInputs('edit-provider-models-wrapper');
const headers = this.collectHeaderInputs('edit-openai-headers-wrapper');
if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
return;
@@ -3292,6 +3633,7 @@ class CLIProxyManager {
'api-key-entries': apiKeyEntries,
models
};
this.applyHeadersToConfig(updatedProvider, headers);
await this.makeRequest('/openai-compatibility', {
method: 'PATCH',
@@ -3322,17 +3664,21 @@ class CLIProxyManager {
}
// 加载认证文件
async loadAuthFiles() {
async loadAuthFiles(keyStats = null) {
try {
const data = await this.makeRequest('/auth-files');
await this.renderAuthFiles(data.files || []);
// 如果没有传入keyStats则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
await this.renderAuthFiles(data.files || [], keyStats);
} catch (error) {
console.error('加载认证文件失败:', error);
}
}
// 渲染认证文件列表
async renderAuthFiles(files) {
async renderAuthFiles(files, keyStats = null) {
const container = document.getElementById('auth-files-list');
if (files.length === 0) {
@@ -3346,8 +3692,11 @@ class CLIProxyManager {
return;
}
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
// 使用传入的keyStats如果没有则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
// 收集所有文件类型使用API返回的type字段
const existingTypes = new Set(['all']); // 'all' 总是存在
@@ -4711,10 +5060,14 @@ class CLIProxyManager {
}
// 加载使用统计
async loadUsageStats() {
async loadUsageStats(usageData = null) {
try {
const response = await this.makeRequest('/usage');
const usage = response?.usage || null;
let usage = usageData;
// 如果没有传入usage数据则调用API获取
if (!usage) {
const response = await this.makeRequest('/usage');
usage = response?.usage || null;
}
this.currentUsageData = usage;
if (!usage) {

42
i18n.js
View File

@@ -41,6 +41,11 @@ const i18n = {
'common.failure': '失败',
'common.unknown_error': '未知错误',
'common.copy': '复制',
'common.custom_headers_label': '自定义请求头',
'common.custom_headers_hint': '可选,设置需要附带到请求中的 HTTP 头,名称和值均不能为空。',
'common.custom_headers_add': '添加请求头',
'common.custom_headers_key_placeholder': 'Header 名称,例如 X-Custom-Header',
'common.custom_headers_value_placeholder': 'Header 值',
// 页面标题
'title.main': 'CLI Proxy API Management Center',
@@ -131,10 +136,14 @@ const i18n = {
'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥',
'ai_providers.gemini_item_title': 'Gemini密钥',
'ai_providers.gemini_add_modal_title': '添加Gemini API密钥',
'ai_providers.gemini_add_modal_key_label': 'API密钥:',
'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥',
'ai_providers.gemini_add_modal_key_label': 'API密钥列表:',
'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥(每行一个)',
'ai_providers.gemini_add_modal_key_hint': '可一次粘贴多个密钥,每行一个。',
'ai_providers.gemini_add_modal_url_label': 'Base URL (可选):',
'ai_providers.gemini_add_modal_url_placeholder': '例如: https://generativelanguage.googleapis.com',
'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥',
'ai_providers.gemini_edit_modal_key_label': 'API密钥:',
'ai_providers.gemini_edit_modal_url_label': 'Base URL (可选):',
'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗',
'ai_providers.codex_title': 'Codex API 配置',
@@ -145,13 +154,13 @@ const i18n = {
'ai_providers.codex_add_modal_title': '添加Codex API配置',
'ai_providers.codex_add_modal_key_label': 'API密钥:',
'ai_providers.codex_add_modal_key_placeholder': '请输入Codex API密钥',
'ai_providers.codex_add_modal_url_label': 'Base URL (可选):',
'ai_providers.codex_add_modal_url_label': 'Base URL (必填):',
'ai_providers.codex_add_modal_url_placeholder': '例如: https://api.example.com',
'ai_providers.codex_add_modal_proxy_label': '代理 URL (可选):',
'ai_providers.codex_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080',
'ai_providers.codex_edit_modal_title': '编辑Codex API配置',
'ai_providers.codex_edit_modal_key_label': 'API密钥:',
'ai_providers.codex_edit_modal_url_label': 'Base URL (可选):',
'ai_providers.codex_edit_modal_url_label': 'Base URL (必填):',
'ai_providers.codex_edit_modal_proxy_label': '代理 URL (可选):',
'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗',
@@ -400,9 +409,13 @@ const i18n = {
'notification.gemini_key_added': 'Gemini密钥添加成功',
'notification.gemini_key_updated': 'Gemini密钥更新成功',
'notification.gemini_key_deleted': 'Gemini密钥删除成功',
'notification.gemini_multi_input_required': '请先输入至少一个Gemini密钥',
'notification.gemini_multi_failed': 'Gemini密钥批量添加失败',
'notification.gemini_multi_summary': 'Gemini批量添加完成成功 {success},跳过 {skipped},失败 {failed}',
'notification.codex_config_added': 'Codex配置添加成功',
'notification.codex_config_updated': 'Codex配置更新成功',
'notification.codex_config_deleted': 'Codex配置删除成功',
'notification.codex_base_url_required': '请填写Codex Base URL',
'notification.claude_config_added': 'Claude配置添加成功',
'notification.claude_config_updated': 'Claude配置更新成功',
'notification.claude_config_deleted': 'Claude配置删除成功',
@@ -487,6 +500,11 @@ const i18n = {
'common.failure': 'Failure',
'common.unknown_error': 'Unknown error',
'common.copy': 'Copy',
'common.custom_headers_label': 'Custom Headers',
'common.custom_headers_hint': 'Optional HTTP headers to send with the request. Leave blank to remove.',
'common.custom_headers_add': 'Add Header',
'common.custom_headers_key_placeholder': 'Header name, e.g. X-Custom-Header',
'common.custom_headers_value_placeholder': 'Header value',
// Page titles
'title.main': 'CLI Proxy API Management Center',
@@ -577,10 +595,14 @@ const i18n = {
'ai_providers.gemini_empty_desc': 'Click the button above to add the first key',
'ai_providers.gemini_item_title': 'Gemini Key',
'ai_providers.gemini_add_modal_title': 'Add Gemini API Key',
'ai_providers.gemini_add_modal_key_label': 'API Key:',
'ai_providers.gemini_add_modal_key_placeholder': 'Please enter Gemini API key',
'ai_providers.gemini_add_modal_key_label': 'API Keys:',
'ai_providers.gemini_add_modal_key_placeholder': 'Enter Gemini API keys (one per line)',
'ai_providers.gemini_add_modal_key_hint': 'You can paste multiple keys, one per line.',
'ai_providers.gemini_add_modal_url_label': 'Base URL (optional):',
'ai_providers.gemini_add_modal_url_placeholder': 'e.g. https://generativelanguage.googleapis.com',
'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key',
'ai_providers.gemini_edit_modal_key_label': 'API Key:',
'ai_providers.gemini_edit_modal_url_label': 'Base URL (optional):',
'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?',
'ai_providers.codex_title': 'Codex API Configuration',
@@ -591,13 +613,13 @@ const i18n = {
'ai_providers.codex_add_modal_title': 'Add Codex API Configuration',
'ai_providers.codex_add_modal_key_label': 'API Key:',
'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex API key',
'ai_providers.codex_add_modal_url_label': 'Base URL (Optional):',
'ai_providers.codex_add_modal_url_label': 'Base URL (Required):',
'ai_providers.codex_add_modal_url_placeholder': 'e.g.: https://api.example.com',
'ai_providers.codex_add_modal_proxy_label': 'Proxy URL (Optional):',
'ai_providers.codex_add_modal_proxy_placeholder': 'e.g.: socks5://proxy.example.com:1080',
'ai_providers.codex_edit_modal_title': 'Edit Codex API Configuration',
'ai_providers.codex_edit_modal_key_label': 'API Key:',
'ai_providers.codex_edit_modal_url_label': 'Base URL (Optional):',
'ai_providers.codex_edit_modal_url_label': 'Base URL (Required):',
'ai_providers.codex_edit_modal_proxy_label': 'Proxy URL (Optional):',
'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?',
@@ -845,9 +867,13 @@ const i18n = {
'notification.gemini_key_added': 'Gemini key added successfully',
'notification.gemini_key_updated': 'Gemini key updated successfully',
'notification.gemini_key_deleted': 'Gemini key deleted successfully',
'notification.gemini_multi_input_required': 'Please enter at least one Gemini key',
'notification.gemini_multi_failed': 'Gemini bulk add failed',
'notification.gemini_multi_summary': 'Gemini bulk add finished: {success} added, {skipped} skipped, {failed} failed',
'notification.codex_config_added': 'Codex configuration added successfully',
'notification.codex_config_updated': 'Codex configuration updated successfully',
'notification.codex_config_deleted': 'Codex configuration deleted successfully',
'notification.codex_base_url_required': 'Please enter the Codex Base URL',
'notification.claude_config_added': 'Claude configuration added successfully',
'notification.claude_config_updated': 'Claude configuration updated successfully',
'notification.claude_config_deleted': 'Claude configuration deleted successfully',

View File

@@ -26,6 +26,5 @@
"repository": {
"type": "git",
"url": "local"
},
"dependencies": {}
}
}

View File

@@ -1364,6 +1364,7 @@ textarea::placeholder {
display: flex;
flex-direction: column;
gap: 12px;
min-height: 520px;
}
#config-management .card {
@@ -1383,12 +1384,13 @@ textarea::placeholder {
flex: 1;
display: flex;
flex-direction: column;
min-height: 520px;
}
.yaml-editor {
width: 100%;
flex: 1;
min-height: 360px;
min-height: 520px;
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 12px 14px;
@@ -1402,6 +1404,7 @@ textarea::placeholder {
#config-management .CodeMirror {
flex: 1;
min-height: 520px;
height: 100%;
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
font-size: 14px;
@@ -1414,8 +1417,10 @@ textarea::placeholder {
#config-management .CodeMirror-scroll {
min-height: 0;
max-height: calc(100vh - 440px);
height: 100%;
max-height: none;
overflow-y: auto;
overscroll-behavior: contain;
}
#config-management .CodeMirror.cm-s-default {
@@ -1458,6 +1463,18 @@ textarea::placeholder {
opacity: 0.6;
}
@media (max-width: 768px) {
.yaml-editor-container,
#config-management .yaml-editor-container {
min-height: 360px;
}
.yaml-editor,
#config-management .CodeMirror {
min-height: 360px;
}
}
.editor-status {
font-size: 13px;
color: var(--text-quaternary);
@@ -2076,9 +2093,13 @@ input:checked+.slider:before {
border-radius: 15px;
width: 90%;
max-width: 550px;
max-height: 90vh;
box-shadow: var(--shadow-modal);
animation: modalSlideIn 0.3s ease;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
@keyframes modalSlideIn {
@@ -2119,6 +2140,9 @@ input:checked+.slider:before {
#modal-body {
padding: 35px 30px 30px 30px;
flex: 1;
min-height: 0;
overflow-y: auto;
}
/* 模态框标题样式 */
@@ -2807,6 +2831,68 @@ input:checked+.slider:before {
align-self: center;
}
.header-input-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.header-input-row .header-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.header-key-input {
flex: 1;
}
.header-value-input {
flex: 2;
}
.header-separator {
color: var(--text-tertiary);
}
.header-badges-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-badge-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.header-badge {
background: var(--accent-tertiary);
border: 1px solid var(--border-primary);
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
color: var(--text-secondary);
display: inline-flex;
gap: 4px;
}
.header-badge strong {
font-weight: 600;
color: var(--text-primary);
}
[data-theme="dark"] .header-badge {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
color: var(--text-secondary);
}
[data-theme="dark"] .header-badge strong {
color: var(--text-secondary);
}
/* Codex OAuth 样式 */
#codex-oauth-content {
transition: all 0.3s ease;