diff --git a/app.js b/app.js
index c0d4cd4..6e614ac 100644
--- a/app.js
+++ b/app.js
@@ -2322,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 = `
+
+ `;
+
+ 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]) => `
+
+ `).join('');
+
+ return `
+
+ `;
+ }
+
// 构造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密钥模态框
@@ -2496,6 +2604,7 @@ class CLIProxyManager {
@@ -2654,6 +2778,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
+ this.populateHeaderFields('edit-gemini-headers-wrapper', config.headers || null);
}
// 更新Gemini密钥
@@ -2661,6 +2786,7 @@ class CLIProxyManager {
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');
@@ -2675,6 +2801,7 @@ class CLIProxyManager {
} else {
delete newConfig['base-url'];
}
+ this.applyHeadersToConfig(newConfig, headers);
await this.makeRequest('/gemini-api-key', {
method: 'PATCH',
@@ -2749,6 +2876,7 @@ class CLIProxyManager {
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''}
${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''}
+ ${this.renderHeaderBadges(config.headers)}
${i18n.t('stats.success')}: ${keyStats.success}
@@ -2789,6 +2917,12 @@ class CLIProxyManager {
+
@@ -2796,6 +2930,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
+ this.populateHeaderFields('new-codex-headers-wrapper');
}
// 添加Codex密钥
@@ -2803,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');
@@ -2813,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);
@@ -2850,6 +2986,12 @@ class CLIProxyManager {
+
@@ -2857,6 +2999,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
+ this.populateHeaderFields('edit-codex-headers-wrapper', config.headers || null);
}
// 更新Codex密钥
@@ -2864,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');
@@ -2879,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',
@@ -2953,6 +3097,7 @@ class CLIProxyManager {
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''}
${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''}
+ ${this.renderHeaderBadges(config.headers)}
${i18n.t('stats.success')}: ${keyStats.success}
@@ -2993,6 +3138,12 @@ class CLIProxyManager {
+
@@ -3000,6 +3151,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
+ this.populateHeaderFields('new-claude-headers-wrapper');
}
// 添加Claude密钥
@@ -3007,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');
@@ -3024,6 +3177,7 @@ class CLIProxyManager {
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
+ this.applyHeadersToConfig(newConfig, headers);
currentKeys.push(newConfig);
@@ -3060,6 +3214,12 @@ class CLIProxyManager {
+
@@ -3067,6 +3227,7 @@ class CLIProxyManager {
`;
modal.style.display = 'block';
+ this.populateHeaderFields('edit-claude-headers-wrapper', config.headers || null);
}
// 更新Claude密钥
@@ -3074,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');
@@ -3088,6 +3250,7 @@ class CLIProxyManager {
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
+ this.applyHeadersToConfig(newConfig, headers);
await this.makeRequest('/claude-api-key', {
method: 'PATCH',
@@ -3196,6 +3359,7 @@ class CLIProxyManager {
${this.escapeHtml(name)}
${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}
+ ${this.renderHeaderBadges(item.headers)}
${i18n.t('ai_providers.openai_keys_count')}: ${apiKeyEntries.length}
${i18n.t('ai_providers.openai_models_count')}: ${models.length}
${this.renderOpenAIModelBadges(models)}
@@ -3243,6 +3407,12 @@ class CLIProxyManager {
+
+
${i18n.t('ai_providers.openai_models_hint')}
@@ -3356,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提供商
@@ -3365,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;
@@ -3384,6 +3565,7 @@ class CLIProxyManager {
'api-key-entries': apiKeyEntries,
models
};
+ this.applyHeadersToConfig(updatedProvider, headers);
await this.makeRequest('/openai-compatibility', {
method: 'PATCH',
diff --git a/i18n.js b/i18n.js
index be29314..9ad1c0c 100644
--- a/i18n.js
+++ b/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',
@@ -494,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',
diff --git a/styles.css b/styles.css
index 5d448da..b7a9a04 100644
--- a/styles.css
+++ b/styles.css
@@ -2814,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;