mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6837100dec | ||
|
|
8542041981 | ||
|
|
35ceab0dae | ||
|
|
d3fe186df7 | ||
|
|
5aff22a20b |
356
app.js
356
app.js
@@ -701,13 +701,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');
|
||||
@@ -2329,14 +2322,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 +2550,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);
|
||||
@@ -2462,9 +2567,18 @@ class CLIProxyManager {
|
||||
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>
|
||||
@@ -2478,14 +2592,19 @@ class CLIProxyManager {
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
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, '"');
|
||||
const apiKeyJson = JSON.stringify(rawKey || '').replace(/"/g, '"');
|
||||
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 +2615,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 +2635,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 +2655,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 +2794,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 +2819,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');
|
||||
@@ -2657,6 +2876,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}
|
||||
@@ -2697,6 +2917,12 @@ class CLIProxyManager {
|
||||
<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 +2930,7 @@ class CLIProxyManager {
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateHeaderFields('new-codex-headers-wrapper');
|
||||
}
|
||||
|
||||
// 添加Codex密钥
|
||||
@@ -2711,6 +2938,7 @@ 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');
|
||||
@@ -2721,7 +2949,7 @@ class CLIProxyManager {
|
||||
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);
|
||||
|
||||
@@ -2758,6 +2986,12 @@ class CLIProxyManager {
|
||||
<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 +2999,7 @@ class CLIProxyManager {
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateHeaderFields('edit-codex-headers-wrapper', config.headers || null);
|
||||
}
|
||||
|
||||
// 更新Codex密钥
|
||||
@@ -2772,6 +3007,7 @@ 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');
|
||||
@@ -2787,7 +3023,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',
|
||||
@@ -2861,6 +3097,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 +3138,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 +3151,7 @@ class CLIProxyManager {
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateHeaderFields('new-claude-headers-wrapper');
|
||||
}
|
||||
|
||||
// 添加Claude密钥
|
||||
@@ -2915,6 +3159,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 +3177,7 @@ class CLIProxyManager {
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
this.applyHeadersToConfig(newConfig, headers);
|
||||
|
||||
currentKeys.push(newConfig);
|
||||
|
||||
@@ -2968,6 +3214,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 +3227,7 @@ class CLIProxyManager {
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateHeaderFields('edit-claude-headers-wrapper', config.headers || null);
|
||||
}
|
||||
|
||||
// 更新Claude密钥
|
||||
@@ -2982,6 +3235,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 +3250,7 @@ class CLIProxyManager {
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
this.applyHeadersToConfig(newConfig, headers);
|
||||
|
||||
await this.makeRequest('/claude-api-key', {
|
||||
method: 'PATCH',
|
||||
@@ -3104,6 +3359,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 +3407,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 +3427,7 @@ class CLIProxyManager {
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateModelFields('new-provider-models-wrapper', []);
|
||||
this.populateHeaderFields('new-openai-headers-wrapper');
|
||||
}
|
||||
|
||||
// 添加OpenAI提供商
|
||||
@@ -3174,6 +3437,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 +3460,7 @@ class CLIProxyManager {
|
||||
'api-key-entries': apiKeyEntries,
|
||||
models
|
||||
};
|
||||
this.applyHeadersToConfig(newProvider, headers);
|
||||
|
||||
currentProviders.push(newProvider);
|
||||
|
||||
@@ -3250,6 +3515,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 +3535,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 +3545,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 +3565,7 @@ class CLIProxyManager {
|
||||
'api-key-entries': apiKeyEntries,
|
||||
models
|
||||
};
|
||||
this.applyHeadersToConfig(updatedProvider, headers);
|
||||
|
||||
await this.makeRequest('/openai-compatibility', {
|
||||
method: 'PATCH',
|
||||
|
||||
32
i18n.js
32
i18n.js
@@ -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 配置',
|
||||
@@ -400,6 +409,9 @@ 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配置删除成功',
|
||||
@@ -487,6 +499,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 +594,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',
|
||||
@@ -845,6 +866,9 @@ 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',
|
||||
|
||||
@@ -26,6 +26,5 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "local"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
69
styles.css
69
styles.css
@@ -2076,9 +2076,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 +2123,9 @@ input:checked+.slider:before {
|
||||
|
||||
#modal-body {
|
||||
padding: 35px 30px 30px 30px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 模态框标题样式 */
|
||||
@@ -2807,6 +2814,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;
|
||||
|
||||
Reference in New Issue
Block a user