Compare commits

...

4 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
3 changed files with 141 additions and 43 deletions

153
app.js
View File

@@ -41,6 +41,7 @@ class CLIProxyManager {
statusEl: null statusEl: null
}; };
this.lastConfigFetchUrl = null; this.lastConfigFetchUrl = null;
this.lastEditorConnectionState = null;
this.init(); this.init();
} }
@@ -967,6 +968,7 @@ class CLIProxyManager {
} }
this.refreshConfigEditor(); this.refreshConfigEditor();
this.lastEditorConnectionState = this.isConnected;
} }
refreshConfigEditor() { refreshConfigEditor() {
@@ -1326,7 +1328,9 @@ class CLIProxyManager {
lastUpdate.textContent = new Date().toLocaleString('zh-CN'); lastUpdate.textContent = new Date().toLocaleString('zh-CN');
this.updateConfigEditorAvailability(); if (this.lastEditorConnectionState !== this.isConnected) {
this.updateConfigEditorAvailability();
}
// 更新连接信息显示 // 更新连接信息显示
this.updateConnectionInfo(); this.updateConnectionInfo();
@@ -1423,14 +1427,58 @@ class CLIProxyManager {
// 使用新的 /config 端点一次性获取所有配置 // 使用新的 /config 端点一次性获取所有配置
const config = await this.getConfig(forceRefresh); const config = await this.getConfig(forceRefresh);
// 从配置中提取并设置各个设置项 // 获取一次usage统计数据供渲染函数和loadUsageStats复用
await this.updateSettingsFromConfig(config); 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);
// 使用统计需要单独加载 // 使用统计需要单独加载复用已获取的usage数据
await this.loadUsageStats(); await this.loadUsageStats(usageData);
// 加载配置文件编辑器内容 // 加载配置文件编辑器内容
await this.loadConfigFileEditor(forceRefresh); await this.loadConfigFileEditor(forceRefresh);
@@ -1446,7 +1494,7 @@ class CLIProxyManager {
} }
// 从配置对象更新所有设置 // 从配置对象更新所有设置
async updateSettingsFromConfig(config) { async updateSettingsFromConfig(config, keyStats = null) {
// 调试设置 // 调试设置
if (config.debug !== undefined) { if (config.debug !== undefined) {
document.getElementById('debug-toggle').checked = config.debug; document.getElementById('debug-toggle').checked = config.debug;
@@ -1495,16 +1543,16 @@ class CLIProxyManager {
} }
// Gemini 密钥 // 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 密钥 // 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 密钥 // 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 兼容提供商 // 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 +1838,8 @@ class CLIProxyManager {
return; return;
} }
// 过滤掉 /v0/management/logs 相关的日志 // 过滤掉 /v0/management/ 相关的日志
const filteredLines = lines.filter(line => !line.includes('/v0/management/logs')); const filteredLines = lines.filter(line => !line.includes('/v0/management/'));
// 限制前端显示的最大行数 // 限制前端显示的最大行数
let displayedLines = filteredLines; let displayedLines = filteredLines;
@@ -1831,8 +1879,8 @@ class CLIProxyManager {
const logsTextElement = logsContent.querySelector('.logs-text'); const logsTextElement = logsContent.querySelector('.logs-text');
const logsInfoElement = logsContent.querySelector('.logs-info'); const logsInfoElement = logsContent.querySelector('.logs-info');
// 过滤掉 /v0/management/logs 相关的日志 // 过滤掉 /v0/management/ 相关的日志
const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/logs')); const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/'));
if (filteredNewLines.length === 0) { if (filteredNewLines.length === 0) {
return; // 如果过滤后没有新日志,直接返回 return; // 如果过滤后没有新日志,直接返回
} }
@@ -2562,7 +2610,7 @@ class CLIProxyManager {
} }
// 渲染Gemini密钥列表 // 渲染Gemini密钥列表
async renderGeminiKeys(keys) { async renderGeminiKeys(keys, keyStats = null) {
const container = document.getElementById('gemini-keys-list'); const container = document.getElementById('gemini-keys-list');
if (!container) { if (!container) {
return; return;
@@ -2589,8 +2637,11 @@ class CLIProxyManager {
return; return;
} }
// 获取使用统计,按 source 聚合 // 使用传入的keyStats如果没有则获取一次
const stats = await this.getKeyStats(); if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = normalizedList.map((config, index) => { container.innerHTML = normalizedList.map((config, index) => {
const rawKey = config['api-key'] || ''; const rawKey = config['api-key'] || '';
@@ -2844,7 +2895,7 @@ class CLIProxyManager {
} }
// 渲染Codex密钥列表 // 渲染Codex密钥列表
async renderCodexKeys(keys) { async renderCodexKeys(keys, keyStats = null) {
const container = document.getElementById('codex-keys-list'); const container = document.getElementById('codex-keys-list');
if (!container) { if (!container) {
return; return;
@@ -2862,8 +2913,11 @@ class CLIProxyManager {
return; return;
} }
// 获取使用统计,按 source 聚合 // 使用传入的keyStats如果没有则获取一次
const stats = await this.getKeyStats(); if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((config, index) => { container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key']; const rawKey = config['api-key'];
@@ -2911,7 +2965,7 @@ class CLIProxyManager {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="new-codex-url">${i18n.t('ai_providers.codex_add_modal_url_label')}</label> <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>
<div class="form-group"> <div class="form-group">
<label for="new-codex-proxy">${i18n.t('ai_providers.codex_add_modal_proxy_label')}</label> <label for="new-codex-proxy">${i18n.t('ai_providers.codex_add_modal_proxy_label')}</label>
@@ -2944,6 +2998,10 @@ class CLIProxyManager {
this.showNotification(i18n.t('notification.field_required'), 'error'); this.showNotification(i18n.t('notification.field_required'), 'error');
return; return;
} }
if (!baseUrl) {
this.showNotification(i18n.t('notification.codex_base_url_required'), 'error');
return;
}
try { try {
const data = await this.makeRequest('/codex-api-key'); const data = await this.makeRequest('/codex-api-key');
@@ -2980,7 +3038,7 @@ class CLIProxyManager {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-codex-url">${i18n.t('ai_providers.codex_edit_modal_url_label')}</label> <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>
<div class="form-group"> <div class="form-group">
<label for="edit-codex-proxy">${i18n.t('ai_providers.codex_edit_modal_proxy_label')}</label> <label for="edit-codex-proxy">${i18n.t('ai_providers.codex_edit_modal_proxy_label')}</label>
@@ -3013,6 +3071,10 @@ class CLIProxyManager {
this.showNotification(i18n.t('notification.field_required'), 'error'); this.showNotification(i18n.t('notification.field_required'), 'error');
return; return;
} }
if (!baseUrl) {
this.showNotification(i18n.t('notification.codex_base_url_required'), 'error');
return;
}
try { try {
const listResponse = await this.makeRequest('/codex-api-key'); const listResponse = await this.makeRequest('/codex-api-key');
@@ -3065,7 +3127,7 @@ class CLIProxyManager {
} }
// 渲染Claude密钥列表 // 渲染Claude密钥列表
async renderClaudeKeys(keys) { async renderClaudeKeys(keys, keyStats = null) {
const container = document.getElementById('claude-keys-list'); const container = document.getElementById('claude-keys-list');
if (!container) { if (!container) {
return; return;
@@ -3083,8 +3145,11 @@ class CLIProxyManager {
return; return;
} }
// 获取使用统计,按 source 聚合 // 使用传入的keyStats如果没有则获取一次
const stats = await this.getKeyStats(); if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((config, index) => { container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key']; const rawKey = config['api-key'];
@@ -3292,7 +3357,7 @@ class CLIProxyManager {
} }
// 渲染OpenAI提供商列表 // 渲染OpenAI提供商列表
async renderOpenAIProviders(providers) { async renderOpenAIProviders(providers, keyStats = null) {
const container = document.getElementById('openai-providers-list'); const container = document.getElementById('openai-providers-list');
if (!container) { if (!container) {
return; return;
@@ -3322,8 +3387,11 @@ class CLIProxyManager {
container.style.overflowY = ''; container.style.overflowY = '';
} }
// 获取使用统计,按 source 聚合 // 使用传入的keyStats如果没有则获取一次
const stats = await this.getKeyStats(); if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
container.innerHTML = list.map((provider, index) => { container.innerHTML = list.map((provider, index) => {
const item = typeof provider === 'object' && provider !== null ? provider : {}; const item = typeof provider === 'object' && provider !== null ? provider : {};
@@ -3596,17 +3664,21 @@ class CLIProxyManager {
} }
// 加载认证文件 // 加载认证文件
async loadAuthFiles() { async loadAuthFiles(keyStats = null) {
try { try {
const data = await this.makeRequest('/auth-files'); 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) { } catch (error) {
console.error('加载认证文件失败:', error); console.error('加载认证文件失败:', error);
} }
} }
// 渲染认证文件列表 // 渲染认证文件列表
async renderAuthFiles(files) { async renderAuthFiles(files, keyStats = null) {
const container = document.getElementById('auth-files-list'); const container = document.getElementById('auth-files-list');
if (files.length === 0) { if (files.length === 0) {
@@ -3620,8 +3692,11 @@ class CLIProxyManager {
return; return;
} }
// 获取使用统计,按 source 聚合 // 使用传入的keyStats如果没有则获取一次
const stats = await this.getKeyStats(); if (!keyStats) {
keyStats = await this.getKeyStats();
}
const stats = keyStats;
// 收集所有文件类型使用API返回的type字段 // 收集所有文件类型使用API返回的type字段
const existingTypes = new Set(['all']); // 'all' 总是存在 const existingTypes = new Set(['all']); // 'all' 总是存在
@@ -4985,10 +5060,14 @@ class CLIProxyManager {
} }
// 加载使用统计 // 加载使用统计
async loadUsageStats() { async loadUsageStats(usageData = null) {
try { try {
const response = await this.makeRequest('/usage'); let usage = usageData;
const usage = response?.usage || null; // 如果没有传入usage数据则调用API获取
if (!usage) {
const response = await this.makeRequest('/usage');
usage = response?.usage || null;
}
this.currentUsageData = usage; this.currentUsageData = usage;
if (!usage) { if (!usage) {

10
i18n.js
View File

@@ -154,13 +154,13 @@ const i18n = {
'ai_providers.codex_add_modal_title': '添加Codex API配置', 'ai_providers.codex_add_modal_title': '添加Codex API配置',
'ai_providers.codex_add_modal_key_label': 'API密钥:', 'ai_providers.codex_add_modal_key_label': 'API密钥:',
'ai_providers.codex_add_modal_key_placeholder': '请输入Codex 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_url_placeholder': '例如: https://api.example.com',
'ai_providers.codex_add_modal_proxy_label': '代理 URL (可选):', 'ai_providers.codex_add_modal_proxy_label': '代理 URL (可选):',
'ai_providers.codex_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080', '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_title': '编辑Codex API配置',
'ai_providers.codex_edit_modal_key_label': '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_edit_modal_proxy_label': '代理 URL (可选):',
'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗', 'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗',
@@ -415,6 +415,7 @@ const i18n = {
'notification.codex_config_added': 'Codex配置添加成功', 'notification.codex_config_added': 'Codex配置添加成功',
'notification.codex_config_updated': 'Codex配置更新成功', 'notification.codex_config_updated': 'Codex配置更新成功',
'notification.codex_config_deleted': 'Codex配置删除成功', 'notification.codex_config_deleted': 'Codex配置删除成功',
'notification.codex_base_url_required': '请填写Codex Base URL',
'notification.claude_config_added': 'Claude配置添加成功', 'notification.claude_config_added': 'Claude配置添加成功',
'notification.claude_config_updated': 'Claude配置更新成功', 'notification.claude_config_updated': 'Claude配置更新成功',
'notification.claude_config_deleted': 'Claude配置删除成功', 'notification.claude_config_deleted': 'Claude配置删除成功',
@@ -612,13 +613,13 @@ const i18n = {
'ai_providers.codex_add_modal_title': 'Add Codex API Configuration', '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_label': 'API Key:',
'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex 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_url_placeholder': 'e.g.: https://api.example.com',
'ai_providers.codex_add_modal_proxy_label': 'Proxy URL (Optional):', '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_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_title': 'Edit Codex API Configuration',
'ai_providers.codex_edit_modal_key_label': 'API Key:', '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_edit_modal_proxy_label': 'Proxy URL (Optional):',
'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?', 'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?',
@@ -872,6 +873,7 @@ const i18n = {
'notification.codex_config_added': 'Codex configuration added successfully', 'notification.codex_config_added': 'Codex configuration added successfully',
'notification.codex_config_updated': 'Codex configuration updated successfully', 'notification.codex_config_updated': 'Codex configuration updated successfully',
'notification.codex_config_deleted': 'Codex configuration deleted 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_added': 'Claude configuration added successfully',
'notification.claude_config_updated': 'Claude configuration updated successfully', 'notification.claude_config_updated': 'Claude configuration updated successfully',
'notification.claude_config_deleted': 'Claude configuration deleted successfully', 'notification.claude_config_deleted': 'Claude configuration deleted successfully',

View File

@@ -1364,6 +1364,7 @@ textarea::placeholder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
min-height: 520px;
} }
#config-management .card { #config-management .card {
@@ -1383,12 +1384,13 @@ textarea::placeholder {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 520px;
} }
.yaml-editor { .yaml-editor {
width: 100%; width: 100%;
flex: 1; flex: 1;
min-height: 360px; min-height: 520px;
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: 6px; border-radius: 6px;
padding: 12px 14px; padding: 12px 14px;
@@ -1402,6 +1404,7 @@ textarea::placeholder {
#config-management .CodeMirror { #config-management .CodeMirror {
flex: 1; flex: 1;
min-height: 520px;
height: 100%; height: 100%;
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Courier New', monospace; font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
font-size: 14px; font-size: 14px;
@@ -1414,8 +1417,10 @@ textarea::placeholder {
#config-management .CodeMirror-scroll { #config-management .CodeMirror-scroll {
min-height: 0; min-height: 0;
max-height: calc(100vh - 440px); height: 100%;
max-height: none;
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain;
} }
#config-management .CodeMirror.cm-s-default { #config-management .CodeMirror.cm-s-default {
@@ -1458,6 +1463,18 @@ textarea::placeholder {
opacity: 0.6; 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 { .editor-status {
font-size: 13px; font-size: 13px;
color: var(--text-quaternary); color: var(--text-quaternary);