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
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3468fd8373 | ||
|
|
4f15c3f5c5 | ||
|
|
72cd117aab | ||
|
|
5d62cd91f2 |
153
app.js
153
app.js
@@ -41,6 +41,7 @@ class CLIProxyManager {
|
||||
statusEl: null
|
||||
};
|
||||
this.lastConfigFetchUrl = null;
|
||||
this.lastEditorConnectionState = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
@@ -967,6 +968,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
this.refreshConfigEditor();
|
||||
this.lastEditorConnectionState = this.isConnected;
|
||||
}
|
||||
|
||||
refreshConfigEditor() {
|
||||
@@ -1326,7 +1328,9 @@ class CLIProxyManager {
|
||||
|
||||
lastUpdate.textContent = new Date().toLocaleString('zh-CN');
|
||||
|
||||
this.updateConfigEditorAvailability();
|
||||
if (this.lastEditorConnectionState !== this.isConnected) {
|
||||
this.updateConfigEditorAvailability();
|
||||
}
|
||||
|
||||
// 更新连接信息显示
|
||||
this.updateConnectionInfo();
|
||||
@@ -1423,14 +1427,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 +1494,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 从配置对象更新所有设置
|
||||
async updateSettingsFromConfig(config) {
|
||||
async updateSettingsFromConfig(config, keyStats = null) {
|
||||
// 调试设置
|
||||
if (config.debug !== undefined) {
|
||||
document.getElementById('debug-toggle').checked = config.debug;
|
||||
@@ -1495,16 +1543,16 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 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 密钥
|
||||
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 +1838,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 +1879,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; // 如果过滤后没有新日志,直接返回
|
||||
}
|
||||
@@ -2562,7 +2610,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染Gemini密钥列表
|
||||
async renderGeminiKeys(keys) {
|
||||
async renderGeminiKeys(keys, keyStats = null) {
|
||||
const container = document.getElementById('gemini-keys-list');
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -2589,8 +2637,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'] || '';
|
||||
@@ -2844,7 +2895,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染Codex密钥列表
|
||||
async renderCodexKeys(keys) {
|
||||
async renderCodexKeys(keys, keyStats = null) {
|
||||
const container = document.getElementById('codex-keys-list');
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -2862,8 +2913,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 +2965,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 +2998,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 +3038,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 +3071,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 +3127,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染Claude密钥列表
|
||||
async renderClaudeKeys(keys) {
|
||||
async renderClaudeKeys(keys, keyStats = null) {
|
||||
const container = document.getElementById('claude-keys-list');
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -3083,8 +3145,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 +3357,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染OpenAI提供商列表
|
||||
async renderOpenAIProviders(providers) {
|
||||
async renderOpenAIProviders(providers, keyStats = null) {
|
||||
const container = document.getElementById('openai-providers-list');
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -3322,8 +3387,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,17 +3664,21 @@ 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');
|
||||
|
||||
if (files.length === 0) {
|
||||
@@ -3620,8 +3692,11 @@ class CLIProxyManager {
|
||||
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' 总是存在
|
||||
@@ -4985,10 +5060,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) {
|
||||
|
||||
10
i18n.js
10
i18n.js
@@ -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配置吗?',
|
||||
|
||||
@@ -415,6 +415,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 +613,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?',
|
||||
|
||||
@@ -872,6 +873,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',
|
||||
|
||||
21
styles.css
21
styles.css
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user