mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat(ui,keys): granular masking, stats match, legacy format
- Make maskApiKey progressive for short keys to reduce exposure - When aggregating usage, fall back to masked key to match stored stats - Support legacy provider config `api-keys` by mapping to `api-key-entries` - Add scrolling for provider lists >5 and reset styles when empty - Improves privacy, fixes mismatched stats display, and preserves compatibility
This commit is contained in:
63
app.js
63
app.js
@@ -2046,8 +2046,14 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
// 遮蔽API密钥显示
|
// 遮蔽API密钥显示
|
||||||
maskApiKey(key) {
|
maskApiKey(key) {
|
||||||
if (key.length <= 8) return key;
|
if (key.length > 8) {
|
||||||
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
||||||
|
} else if (key.length > 4) {
|
||||||
|
return key.substring(0, 2) + '...' + key.substring(key.length - 2);
|
||||||
|
} else if (key.length > 2) {
|
||||||
|
return key.substring(0, 1) + '...' + key.substring(key.length - 1);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML 转义,防止 XSS
|
// HTML 转义,防止 XSS
|
||||||
@@ -2198,7 +2204,8 @@ class CLIProxyManager {
|
|||||||
const stats = await this.getKeyStats();
|
const stats = await this.getKeyStats();
|
||||||
|
|
||||||
container.innerHTML = keys.map((key, index) => {
|
container.innerHTML = keys.map((key, index) => {
|
||||||
const keyStats = stats[key] || { success: 0, failure: 0 };
|
const masked = this.maskApiKey(key);
|
||||||
|
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 };
|
||||||
return `
|
return `
|
||||||
<div class="key-item">
|
<div class="key-item">
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
@@ -2362,7 +2369,9 @@ class CLIProxyManager {
|
|||||||
const stats = await this.getKeyStats();
|
const stats = await this.getKeyStats();
|
||||||
|
|
||||||
container.innerHTML = keys.map((config, index) => {
|
container.innerHTML = keys.map((config, index) => {
|
||||||
const keyStats = stats[config['api-key']] || { success: 0, failure: 0 };
|
const rawKey = config['api-key'];
|
||||||
|
const masked = rawKey ? this.maskApiKey(rawKey) : '';
|
||||||
|
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
|
||||||
return `
|
return `
|
||||||
<div class="provider-item">
|
<div class="provider-item">
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
@@ -2565,7 +2574,9 @@ class CLIProxyManager {
|
|||||||
const stats = await this.getKeyStats();
|
const stats = await this.getKeyStats();
|
||||||
|
|
||||||
container.innerHTML = keys.map((config, index) => {
|
container.innerHTML = keys.map((config, index) => {
|
||||||
const keyStats = stats[config['api-key']] || { success: 0, failure: 0 };
|
const rawKey = config['api-key'];
|
||||||
|
const masked = rawKey ? this.maskApiKey(rawKey) : '';
|
||||||
|
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
|
||||||
return `
|
return `
|
||||||
<div class="provider-item">
|
<div class="provider-item">
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
@@ -2761,15 +2772,37 @@ class CLIProxyManager {
|
|||||||
<p>${i18n.t('ai_providers.openai_empty_desc')}</p>
|
<p>${i18n.t('ai_providers.openai_empty_desc')}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// 重置样式
|
||||||
|
container.style.maxHeight = '';
|
||||||
|
container.style.overflowY = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据提供商数量设置滚动条
|
||||||
|
if (providers.length > 5) {
|
||||||
|
container.style.maxHeight = '400px';
|
||||||
|
container.style.overflowY = 'auto';
|
||||||
|
} else {
|
||||||
|
container.style.maxHeight = '';
|
||||||
|
container.style.overflowY = '';
|
||||||
|
}
|
||||||
|
|
||||||
// 获取使用统计,按 source 聚合
|
// 获取使用统计,按 source 聚合
|
||||||
const stats = await this.getKeyStats();
|
const stats = await this.getKeyStats();
|
||||||
|
|
||||||
container.innerHTML = providers.map((provider, index) => {
|
container.innerHTML = providers.map((provider, index) => {
|
||||||
const item = typeof provider === 'object' && provider !== null ? provider : {};
|
const item = typeof provider === 'object' && provider !== null ? provider : {};
|
||||||
const apiKeyEntries = Array.isArray(item['api-key-entries']) ? item['api-key-entries'] : [];
|
|
||||||
|
// 处理两种API密钥格式:新的 api-key-entries 和旧的 api-keys
|
||||||
|
let apiKeyEntries = [];
|
||||||
|
if (Array.isArray(item['api-key-entries'])) {
|
||||||
|
// 新格式:{api-key: "...", proxy-url: "..."}
|
||||||
|
apiKeyEntries = item['api-key-entries'];
|
||||||
|
} else if (Array.isArray(item['api-keys'])) {
|
||||||
|
// 旧格式:简单的字符串数组
|
||||||
|
apiKeyEntries = item['api-keys'].map(key => ({ 'api-key': key }));
|
||||||
|
}
|
||||||
|
|
||||||
const models = Array.isArray(item.models) ? item.models : [];
|
const models = Array.isArray(item.models) ? item.models : [];
|
||||||
const name = item.name || '';
|
const name = item.name || '';
|
||||||
const baseUrl = item['base-url'] || '';
|
const baseUrl = item['base-url'] || '';
|
||||||
@@ -2780,7 +2813,8 @@ class CLIProxyManager {
|
|||||||
apiKeyEntries.forEach(entry => {
|
apiKeyEntries.forEach(entry => {
|
||||||
const key = entry && entry['api-key'] ? entry['api-key'] : '';
|
const key = entry && entry['api-key'] ? entry['api-key'] : '';
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
const keyStats = stats[key] || { success: 0, failure: 0 };
|
const masked = this.maskApiKey(key);
|
||||||
|
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 };
|
||||||
totalSuccess += keyStats.success;
|
totalSuccess += keyStats.success;
|
||||||
totalFailure += keyStats.failure;
|
totalFailure += keyStats.failure;
|
||||||
});
|
});
|
||||||
@@ -2904,7 +2938,16 @@ class CLIProxyManager {
|
|||||||
const modal = document.getElementById('modal');
|
const modal = document.getElementById('modal');
|
||||||
const modalBody = document.getElementById('modal-body');
|
const modalBody = document.getElementById('modal-body');
|
||||||
|
|
||||||
const apiKeyEntries = Array.isArray(provider?.['api-key-entries']) ? provider['api-key-entries'] : [];
|
// 处理两种API密钥格式:新的 api-key-entries 和旧的 api-keys
|
||||||
|
let apiKeyEntries = [];
|
||||||
|
if (Array.isArray(provider?.['api-key-entries'])) {
|
||||||
|
// 新格式:{api-key: "...", proxy-url: "..."}
|
||||||
|
apiKeyEntries = provider['api-key-entries'];
|
||||||
|
} else if (Array.isArray(provider?.['api-keys'])) {
|
||||||
|
// 旧格式:简单的字符串数组
|
||||||
|
apiKeyEntries = provider['api-keys'].map(key => ({ 'api-key': key, 'proxy-url': '' }));
|
||||||
|
}
|
||||||
|
|
||||||
const apiKeysText = apiKeyEntries.map(entry => entry?.['api-key'] || '').join('\n');
|
const apiKeysText = apiKeyEntries.map(entry => entry?.['api-key'] || '').join('\n');
|
||||||
const proxiesText = apiKeyEntries.map(entry => entry?.['proxy-url'] || '').join('\n');
|
const proxiesText = apiKeyEntries.map(entry => entry?.['proxy-url'] || '').join('\n');
|
||||||
const models = Array.isArray(provider?.models) ? provider.models : [];
|
const models = Array.isArray(provider?.models) ? provider.models : [];
|
||||||
@@ -3935,8 +3978,8 @@ class CLIProxyManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = detail.success;
|
const isFailed = detail.failed === true;
|
||||||
if (success === false) {
|
if (isFailed) {
|
||||||
sourceStats[source].failure += 1;
|
sourceStats[source].failure += 1;
|
||||||
} else {
|
} else {
|
||||||
sourceStats[source].success += 1;
|
sourceStats[source].success += 1;
|
||||||
|
|||||||
@@ -1446,12 +1446,15 @@ input:checked+.slider:before {
|
|||||||
|
|
||||||
/* 列表样式 */
|
/* 列表样式 */
|
||||||
.key-list,
|
.key-list,
|
||||||
.provider-list,
|
|
||||||
.file-list {
|
.file-list {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-list {
|
||||||
|
/* 默认不限制高度,动态设置 */
|
||||||
|
}
|
||||||
|
|
||||||
.key-item,
|
.key-item,
|
||||||
.provider-item,
|
.provider-item,
|
||||||
.file-item {
|
.file-item {
|
||||||
|
|||||||
Reference in New Issue
Block a user