Compare commits

...

2 Commits

Author SHA1 Message Date
Supra4E8C
5be40092f7 feat(app.js, i18n): add custom model management for Claude API
- Introduced UI elements for adding and managing custom models in the CLIProxyManager, including model name and alias inputs.
- Enhanced the display of model counts and badges in the provider item view.
- Updated internationalization strings to support new model management features in both English and Chinese.
2025-11-13 17:52:13 +08:00
Luis Pater
d422606f99 feat(app.js, styles.css): add main flag for Gemini CLI auth files, update styles and filtering logic 2025-11-13 09:22:14 +08:00
3 changed files with 84 additions and 8 deletions

65
app.js
View File

@@ -3319,6 +3319,11 @@ class CLIProxyManager {
const maskedDisplay = this.escapeHtml(masked);
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"');
const models = Array.isArray(config.models) ? config.models : [];
const modelsCountHtml = models.length
? `<div class="item-subtitle">${i18n.t('ai_providers.claude_models_count')}: ${models.length}</div>`
: '';
const modelsBadgesHtml = this.renderModelBadges(models);
return `
<div class="provider-item">
<div class="item-content">
@@ -3327,6 +3332,8 @@ class CLIProxyManager {
${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)}
${modelsCountHtml}
${modelsBadgesHtml}
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
@@ -3373,6 +3380,12 @@ class CLIProxyManager {
<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="form-group">
<label>${i18n.t('ai_providers.claude_models_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.claude_models_hint')}</p>
<div id="new-claude-models-wrapper" class="model-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('new-claude-models-wrapper')">${i18n.t('ai_providers.claude_models_add_btn')}</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>
@@ -3381,6 +3394,7 @@ class CLIProxyManager {
modal.style.display = 'block';
this.populateHeaderFields('new-claude-headers-wrapper');
this.populateModelFields('new-claude-models-wrapper');
}
// 添加Claude密钥
@@ -3389,6 +3403,7 @@ class CLIProxyManager {
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');
const models = this.collectModelInputs('new-claude-models-wrapper');
if (!apiKey) {
this.showNotification(i18n.t('notification.field_required'), 'error');
@@ -3407,6 +3422,9 @@ class CLIProxyManager {
newConfig['proxy-url'] = proxyUrl;
}
this.applyHeadersToConfig(newConfig, headers);
if (models.length) {
newConfig.models = models;
}
currentKeys.push(newConfig);
@@ -3449,6 +3467,12 @@ class CLIProxyManager {
<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="form-group">
<label>${i18n.t('ai_providers.claude_models_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.claude_models_hint')}</p>
<div id="edit-claude-models-wrapper" class="model-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('edit-claude-models-wrapper')">${i18n.t('ai_providers.claude_models_add_btn')}</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>
@@ -3457,6 +3481,7 @@ class CLIProxyManager {
modal.style.display = 'block';
this.populateHeaderFields('edit-claude-headers-wrapper', config.headers || null);
this.populateModelFields('edit-claude-models-wrapper', Array.isArray(config.models) ? config.models : []);
}
// 更新Claude密钥
@@ -3465,6 +3490,7 @@ class CLIProxyManager {
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');
const models = this.collectModelInputs('edit-claude-models-wrapper');
if (!apiKey) {
this.showNotification(i18n.t('notification.field_required'), 'error');
@@ -3480,6 +3506,9 @@ class CLIProxyManager {
newConfig['proxy-url'] = proxyUrl;
}
this.applyHeadersToConfig(newConfig, headers);
if (models.length) {
newConfig.models = models;
}
await this.makeRequest('/claude-api-key', {
method: 'PATCH',
@@ -3595,7 +3624,7 @@ class CLIProxyManager {
${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)}
${this.renderModelBadges(models)}
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${totalSuccess}
@@ -3845,7 +3874,22 @@ class CLIProxyManager {
// 渲染认证文件列表
async renderAuthFiles(files, keyStats = null) {
const container = document.getElementById('auth-files-list');
const visibleFiles = Array.isArray(files) ? files.filter(file => file.disabled !== true) : [];
const isRuntimeOnlyFile = (file) => {
if (!file) return false;
const runtimeValue = file.runtime_only;
return runtimeValue === true || runtimeValue === 'true';
};
const shouldDisplayDisabledGeminiCli = (file) => {
if (!file) return false;
const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : '';
const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli';
return isGeminiCli && !isRuntimeOnlyFile(file);
};
const visibleFiles = Array.isArray(files) ? files.filter(file => {
if (!file) return false;
return shouldDisplayDisabledGeminiCli(file) || file.disabled !== true;
}) : [];
this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
if (visibleFiles.length === 0) {
@@ -3968,15 +4012,20 @@ class CLIProxyManager {
}
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
// 检查是否为 runtime-only 文件
const isRuntimeOnly = file.runtime_only === true;
// Determine whether the entry is runtime-only
const isRuntimeOnly = isRuntimeOnlyFile(file);
// 生成操作按钮 HTMLruntime-only 文件显示虚拟标记
const shouldShowMainFlag = shouldDisplayDisabledGeminiCli(file);
const mainFlagButton = shouldShowMainFlag ? `
<button class="btn-small btn-warning main-flag-btn" title="主" disabled>主</button>` : '';
// Build action buttons; runtime-only entries display placeholder badge
const actionsHtml = isRuntimeOnly ? `
<div class="item-actions">
<span class="virtual-auth-badge">虚拟认证文件</span>
</div>` : `
<div class="item-actions" data-filename="${safeFileName}">
${mainFlagButton}
<button class="btn-small btn-info" data-action="showDetails" title="详细信息">
<i class="fas fa-info-circle"></i>
</button>
@@ -5868,8 +5917,8 @@ class CLIProxyManager {
row.className = 'model-input-row';
row.innerHTML = `
<div class="input-group">
<input type="text" class="model-name-input" placeholder="${i18n.t('ai_providers.openai_model_name_placeholder')}" value="${model.name ? this.escapeHtml(model.name) : ''}">
<input type="text" class="model-alias-input" placeholder="${i18n.t('ai_providers.openai_model_alias_placeholder')}" value="${model.alias ? this.escapeHtml(model.alias) : ''}">
<input type="text" class="model-name-input" placeholder="${i18n.t('common.model_name_placeholder')}" value="${model.name ? this.escapeHtml(model.name) : ''}">
<input type="text" class="model-alias-input" placeholder="${i18n.t('common.model_alias_placeholder')}" value="${model.alias ? this.escapeHtml(model.alias) : ''}">
<button type="button" class="btn btn-small btn-danger model-remove-btn"><i class="fas fa-trash"></i></button>
</div>
`;
@@ -5922,7 +5971,7 @@ class CLIProxyManager {
return models;
}
renderOpenAIModelBadges(models) {
renderModelBadges(models) {
if (!models || models.length === 0) {
return '';
}

12
i18n.js
View File

@@ -46,6 +46,8 @@ const i18n = {
'common.custom_headers_add': '添加请求头',
'common.custom_headers_key_placeholder': 'Header 名称,例如 X-Custom-Header',
'common.custom_headers_value_placeholder': 'Header 值',
'common.model_name_placeholder': '模型名称,例如 claude-3-5-sonnet-20241022',
'common.model_alias_placeholder': '模型别名 (可选)',
// 页面标题
'title.main': 'CLI Proxy API Management Center',
@@ -184,6 +186,10 @@ const i18n = {
'ai_providers.claude_edit_modal_url_label': 'Base URL (可选):',
'ai_providers.claude_edit_modal_proxy_label': '代理 URL (可选):',
'ai_providers.claude_delete_confirm': '确定要删除这个Claude配置吗',
'ai_providers.claude_models_label': '自定义模型 (可选):',
'ai_providers.claude_models_hint': '为空表示使用全部模型;可填写 name[, alias] 以限制或重命名模型。',
'ai_providers.claude_models_add_btn': '添加模型',
'ai_providers.claude_models_count': '模型数量',
'ai_providers.openai_title': 'OpenAI 兼容提供商',
'ai_providers.openai_add_button': '添加提供商',
@@ -533,6 +539,8 @@ const i18n = {
'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',
'common.model_name_placeholder': 'Model name, e.g. claude-3-5-sonnet-20241022',
'common.model_alias_placeholder': 'Model alias (optional)',
// Page titles
'title.main': 'CLI Proxy API Management Center',
@@ -671,6 +679,10 @@ const i18n = {
'ai_providers.claude_edit_modal_url_label': 'Base URL (Optional):',
'ai_providers.claude_edit_modal_proxy_label': 'Proxy URL (Optional):',
'ai_providers.claude_delete_confirm': 'Are you sure you want to delete this Claude configuration?',
'ai_providers.claude_models_label': 'Custom Models (Optional):',
'ai_providers.claude_models_hint': 'Leave empty to allow all models, or add name[, alias] entries to limit/alias them.',
'ai_providers.claude_models_add_btn': 'Add Model',
'ai_providers.claude_models_count': 'Models Count',
'ai_providers.openai_title': 'OpenAI Compatible Providers',
'ai_providers.openai_add_button': 'Add Provider',

View File

@@ -1248,6 +1248,21 @@ body {
transform: translateY(-1px);
}
.btn-small.btn-warning {
background: #ff9800;
color: #fff;
border-color: #ff9800;
cursor: default;
}
.btn-small.btn-warning:disabled,
.btn-small.btn-warning[disabled] {
background: #fb8c00;
border-color: #fb8c00;
color: #fff;
opacity: 1;
}
.btn-small i {
font-size: 13px;
}