Compare commits

...

5 Commits

Author SHA1 Message Date
Luis Pater
1d7408cb25 feat(app.js, i18n): enhance auth file management with filtering and update related UI messages 2025-11-11 11:50:12 +08:00
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
3 changed files with 340 additions and 66 deletions

367
app.js
View File

@@ -27,6 +27,10 @@ class CLIProxyManager {
// 日志时间戳(用于增量加载)
this.latestLogTimestamp = null;
// Auth file filter state cache
this.currentAuthFileFilter = 'all';
this.cachedAuthFiles = [];
// 主题管理
this.currentTheme = 'light';
@@ -41,6 +45,7 @@ class CLIProxyManager {
statusEl: null
};
this.lastConfigFetchUrl = null;
this.lastEditorConnectionState = null;
this.init();
}
@@ -967,6 +972,7 @@ class CLIProxyManager {
}
this.refreshConfigEditor();
this.lastEditorConnectionState = this.isConnected;
}
refreshConfigEditor() {
@@ -1326,7 +1332,9 @@ class CLIProxyManager {
lastUpdate.textContent = new Date().toLocaleString('zh-CN');
this.updateConfigEditorAvailability();
if (this.lastEditorConnectionState !== this.isConnected) {
this.updateConfigEditorAvailability();
}
// 更新连接信息显示
this.updateConnectionInfo();
@@ -1423,14 +1431,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);
@@ -1446,7 +1498,7 @@ class CLIProxyManager {
}
// 从配置对象更新所有设置
async updateSettingsFromConfig(config) {
async updateSettingsFromConfig(config, keyStats = null) {
// 调试设置
if (config.debug !== undefined) {
document.getElementById('debug-toggle').checked = config.debug;
@@ -1494,17 +1546,17 @@ class CLIProxyManager {
this.renderApiKeys(config['api-keys']);
}
// Gemini 密钥
await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []);
// Gemini keys
await this.renderGeminiKeys(this.getGeminiKeysFromConfig(config), 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);
}
// 回退方法:原来的逐个加载方式
@@ -1790,8 +1842,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;
@@ -1831,8 +1883,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; // 如果过滤后没有新日志,直接返回
}
@@ -2546,35 +2598,67 @@ class CLIProxyManager {
}
}
// 加载Gemini密钥
// Load Gemini keys
async loadGeminiKeys() {
try {
const config = await this.getConfig();
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 }));
}
const keys = this.getGeminiKeysFromConfig(config);
await this.renderGeminiKeys(keys);
} catch (error) {
console.error('加载Gemini密钥失败:', error);
}
}
// 渲染Gemini密钥列表
async renderGeminiKeys(keys) {
// Extract Gemini keys from config with backward compatibility
getGeminiKeysFromConfig(config) {
if (!config) {
return [];
}
const geminiKeys = Array.isArray(config['gemini-api-key']) ? config['gemini-api-key'] : [];
if (geminiKeys.length > 0) {
return geminiKeys;
}
const legacyKeys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [];
return legacyKeys
.map(item => {
if (item && typeof item === 'object') {
return { ...item };
}
if (typeof item === 'string') {
const trimmed = item.trim();
if (trimmed) {
return { 'api-key': trimmed };
}
}
return null;
})
.filter(Boolean);
}
// Render Gemini key list
async renderGeminiKeys(keys, keyStats = null) {
const container = document.getElementById('gemini-keys-list');
if (!container) {
return;
}
const normalizedList = (Array.isArray(keys) ? keys : []).map(item => {
let normalized = null;
if (item && typeof item === 'object') {
return { ...item };
normalized = { ...item };
} else if (typeof item === 'string') {
const trimmed = item.trim();
if (trimmed) {
normalized = { 'api-key': trimmed };
}
}
if (typeof item === 'string') {
return { 'api-key': item };
if (normalized && !normalized['base-url'] && normalized['base_url']) {
normalized['base-url'] = normalized['base_url'];
}
return null;
return normalized;
}).filter(config => config && config['api-key']);
this.cachedGeminiKeys = normalizedList;
@@ -2589,8 +2673,11 @@ class CLIProxyManager {
return;
}
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
// 使用传入的keyStats如果没有则获取一次
if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = normalizedList.map((config, index) => {
const rawKey = config['api-key'] || '';
@@ -2598,12 +2685,13 @@ class CLIProxyManager {
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, '"');
const baseUrl = config['base-url'] || config['base_url'] || '';
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(rawKey || '')}</div>
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
${baseUrl ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}</div>` : ''}
${this.renderHeaderBadges(config.headers)}
<div class="item-stats">
<span class="stat-badge stat-success">
@@ -2844,7 +2932,7 @@ class CLIProxyManager {
}
// 渲染Codex密钥列表
async renderCodexKeys(keys) {
async renderCodexKeys(keys, keyStats = null) {
const container = document.getElementById('codex-keys-list');
if (!container) {
return;
@@ -2862,8 +2950,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'];
@@ -2911,7 +3002,7 @@ 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>
@@ -2944,6 +3035,10 @@ class CLIProxyManager {
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');
@@ -2980,7 +3075,7 @@ 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>
@@ -3013,6 +3108,10 @@ class CLIProxyManager {
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');
@@ -3065,7 +3164,7 @@ class CLIProxyManager {
}
// 渲染Claude密钥列表
async renderClaudeKeys(keys) {
async renderClaudeKeys(keys, keyStats = null) {
const container = document.getElementById('claude-keys-list');
if (!container) {
return;
@@ -3083,8 +3182,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'];
@@ -3292,7 +3394,7 @@ class CLIProxyManager {
}
// 渲染OpenAI提供商列表
async renderOpenAIProviders(providers) {
async renderOpenAIProviders(providers, keyStats = null) {
const container = document.getElementById('openai-providers-list');
if (!container) {
return;
@@ -3322,8 +3424,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 : {};
@@ -3596,20 +3701,26 @@ 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');
const visibleFiles = Array.isArray(files) ? files.filter(file => file.disabled !== true) : [];
this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
if (files.length === 0) {
if (visibleFiles.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-alt"></i>
@@ -3617,15 +3728,21 @@ class CLIProxyManager {
<p>${i18n.t('auth_files.empty_desc')}</p>
</div>
`;
this.updateFilterButtons(new Set(['all']));
this.bindAuthFileFilterEvents();
this.applyAuthFileFilterState();
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' 总是存在
files.forEach(file => {
visibleFiles.forEach(file => {
if (file.type) {
existingTypes.add(file.type);
}
@@ -3634,7 +3751,7 @@ class CLIProxyManager {
// 更新筛选按钮显示
this.updateFilterButtons(existingTypes);
container.innerHTML = files.map(file => {
container.innerHTML = visibleFiles.map(file => {
// 认证文件的统计匹配逻辑:
// 1. 首先尝试完整文件名匹配
// 2. 如果没有匹配,尝试脱敏文件名匹配(去掉扩展名后的脱敏版本)
@@ -3739,7 +3856,7 @@ class CLIProxyManager {
</div>`;
return `
<div class="file-item" data-file-type="${fileType}" ${isRuntimeOnly ? 'data-runtime-only="true"' : ''}>
<div class="file-item" data-file-type="${fileType}" data-file-name="${this.escapeHtml(file.name)}" ${isRuntimeOnly ? 'data-runtime-only="true"' : ''}>
<div class="item-content">
<div class="item-title">${typeBadge}${file.name}</div>
<div class="item-meta">
@@ -3767,6 +3884,9 @@ class CLIProxyManager {
// 绑定认证文件操作按钮事件(使用事件委托)
this.bindAuthFileActionEvents();
// Reapply current filter state
this.applyAuthFileFilterState();
}
// 更新筛选按钮显示
@@ -3849,6 +3969,7 @@ class CLIProxyManager {
// 获取筛选类型
const filterType = clickedBtn.dataset.type;
this.currentAuthFileFilter = filterType || 'all';
// 筛选文件
const fileItems = document.querySelectorAll('.file-item');
@@ -3900,6 +4021,65 @@ class CLIProxyManager {
this.refreshFilterButtonTexts();
}
// Apply current filter selection to the list
applyAuthFileFilterState() {
const filterContainer = document.querySelector('.auth-file-filter');
if (!filterContainer) return;
const currentType = this.currentAuthFileFilter || 'all';
const buttons = filterContainer.querySelectorAll('.filter-btn');
if (buttons.length === 0) return;
let targetButton = null;
buttons.forEach(btn => {
if (btn.dataset.type === currentType) {
targetButton = btn;
}
});
if (!targetButton) {
targetButton = filterContainer.querySelector('.filter-btn[data-type="all"]') || buttons[0];
if (targetButton) {
this.currentAuthFileFilter = targetButton.dataset.type || 'all';
}
}
if (targetButton) {
this.handleFilterClick(targetButton);
}
}
// Remove deleted auth files from cache and DOM instantly
removeAuthFileElements(filenames = []) {
if (!Array.isArray(filenames) || filenames.length === 0) {
return;
}
const removalSet = new Set(filenames);
this.cachedAuthFiles = (this.cachedAuthFiles || []).filter(file => file && !removalSet.has(file.name));
const container = document.getElementById('auth-files-list');
if (!container) return;
const fileItems = container.querySelectorAll('.file-item');
fileItems.forEach(item => {
const fileNameAttr = item.getAttribute('data-file-name');
if (fileNameAttr && removalSet.has(fileNameAttr)) {
item.remove();
}
});
if (!container.querySelector('.file-item')) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h3>${i18n.t('auth_files.empty_title')}</h3>
<p>${i18n.t('auth_files.empty_desc')}</p>
</div>
`;
}
}
// 刷新筛选按钮文本(根据 data-i18n-text
refreshFilterButtonTexts() {
document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => {
@@ -4159,23 +4339,86 @@ class CLIProxyManager {
try {
await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' });
this.removeAuthFileElements([filename]);
this.clearCache(); // 清除缓存
this.loadAuthFiles();
await this.loadAuthFiles();
this.showNotification(i18n.t('auth_files.delete_success'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
}
}
// 删除所有认证文件
// Delete auth files (respect current filter)
async deleteAllAuthFiles() {
if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return;
const filterType = (this.currentAuthFileFilter || 'all').toLowerCase();
const isFiltered = filterType !== 'all';
const typeLabel = this.generateDynamicTypeLabel(filterType);
const confirmMessage = isFiltered
? i18n.t('auth_files.delete_filtered_confirm').replace('{type}', typeLabel)
: i18n.t('auth_files.delete_all_confirm');
if (!confirm(confirmMessage)) return;
try {
const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' });
if (!isFiltered) {
const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' });
const currentNames = (this.cachedAuthFiles || []).map(file => file.name).filter(Boolean);
if (currentNames.length > 0) {
this.removeAuthFileElements(currentNames);
}
this.clearCache(); // 清除缓存
this.currentAuthFileFilter = 'all';
await this.loadAuthFiles();
this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success');
return;
}
const deletableFiles = (this.cachedAuthFiles || []).filter(file => {
if (!file || file.runtime_only) return false;
const fileType = (file.type || 'unknown').toLowerCase();
return fileType === filterType;
});
if (deletableFiles.length === 0) {
this.showNotification(i18n.t('auth_files.delete_filtered_none').replace('{type}', typeLabel), 'info');
return;
}
let success = 0;
let failed = 0;
const deletedNames = [];
for (const file of deletableFiles) {
try {
await this.makeRequest(`/auth-files?name=${encodeURIComponent(file.name)}`, { method: 'DELETE' });
success++;
deletedNames.push(file.name);
} catch (error) {
console.error('删除认证文件失败:', file?.name, error);
failed++;
}
}
if (deletedNames.length > 0) {
this.removeAuthFileElements(deletedNames);
}
this.clearCache(); // 清除缓存
this.loadAuthFiles();
this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success');
this.currentAuthFileFilter = 'all';
await this.loadAuthFiles();
if (failed === 0) {
const successMsg = i18n.t('auth_files.delete_filtered_success')
.replace('{count}', success)
.replace('{type}', typeLabel);
this.showNotification(successMsg, 'success');
} else {
const warningMsg = i18n.t('auth_files.delete_filtered_partial')
.replace('{success}', success)
.replace('{failed}', failed)
.replace('{type}', typeLabel);
this.showNotification(warningMsg, 'warning');
}
} catch (error) {
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
}
@@ -4985,10 +5228,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) {

18
i18n.js
View File

@@ -154,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配置吗',
@@ -225,11 +225,15 @@ const i18n = {
'auth_files.delete_button': '删除',
'auth_files.delete_confirm': '确定要删除文件',
'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!',
'auth_files.delete_filtered_confirm': '确定要删除筛选出的 {type} 认证文件吗?此操作不可恢复!',
'auth_files.upload_error_json': '只能上传JSON文件',
'auth_files.upload_success': '文件上传成功',
'auth_files.download_success': '文件下载成功',
'auth_files.delete_success': '文件删除成功',
'auth_files.delete_all_success': '成功删除',
'auth_files.delete_filtered_success': '成功删除 {count} 个 {type} 认证文件',
'auth_files.delete_filtered_partial': '{type} 认证文件删除完成,成功 {success} 个,失败 {failed} 个',
'auth_files.delete_filtered_none': '当前筛选类型 ({type}) 下没有可删除的认证文件',
'auth_files.files_count': '个文件',
'auth_files.filter_all': '全部',
'auth_files.filter_qwen': 'Qwen',
@@ -415,6 +419,7 @@ const i18n = {
'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配置删除成功',
@@ -612,13 +617,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?',
@@ -683,11 +688,15 @@ const i18n = {
'auth_files.delete_button': 'Delete',
'auth_files.delete_confirm': 'Are you sure you want to delete file',
'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!',
'auth_files.delete_filtered_confirm': 'Are you sure you want to delete all {type} auth files? This operation cannot be undone!',
'auth_files.upload_error_json': 'Only JSON files are allowed',
'auth_files.upload_success': 'File uploaded successfully',
'auth_files.download_success': 'File downloaded successfully',
'auth_files.delete_success': 'File deleted successfully',
'auth_files.delete_all_success': 'Successfully deleted',
'auth_files.delete_filtered_success': 'Deleted {count} {type} auth files successfully',
'auth_files.delete_filtered_partial': '{type} auth files deletion finished: {success} succeeded, {failed} failed',
'auth_files.delete_filtered_none': 'No deletable auth files under the current filter ({type})',
'auth_files.files_count': 'files',
'auth_files.filter_all': 'All',
'auth_files.filter_qwen': 'Qwen',
@@ -872,6 +881,7 @@ const i18n = {
'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

@@ -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);