mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat: add sensitive value masking functionality to usage module and update UI for system info localization
This commit is contained in:
2
i18n.js
2
i18n.js
@@ -94,7 +94,7 @@ const i18n = {
|
|||||||
'nav.usage_stats': '使用统计',
|
'nav.usage_stats': '使用统计',
|
||||||
'nav.config_management': '配置管理',
|
'nav.config_management': '配置管理',
|
||||||
'nav.logs': '日志查看',
|
'nav.logs': '日志查看',
|
||||||
'nav.system_info': '管理中心信息',
|
'nav.system_info': '中心信息',
|
||||||
|
|
||||||
// 基础设置
|
// 基础设置
|
||||||
'basic_settings.title': '基础设置',
|
'basic_settings.title': '基础设置',
|
||||||
|
|||||||
@@ -182,7 +182,7 @@
|
|||||||
<i class="fas fa-scroll"></i> <span data-i18n="nav.logs">日志查看</span>
|
<i class="fas fa-scroll"></i> <span data-i18n="nav.logs">日志查看</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
<li data-i18n-tooltip="nav.system_info"><a href="#system-info" class="nav-item" data-section="system-info">
|
<li data-i18n-tooltip="nav.system_info"><a href="#system-info" class="nav-item" data-section="system-info">
|
||||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">管理中心信息</span>
|
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">中心信息</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -5,6 +5,46 @@ const DEFAULT_CHART_LINE_COUNT = 3;
|
|||||||
const MIN_CHART_LINE_COUNT = 1;
|
const MIN_CHART_LINE_COUNT = 1;
|
||||||
const ALL_MODELS_VALUE = 'all';
|
const ALL_MODELS_VALUE = 'all';
|
||||||
|
|
||||||
|
export function maskUsageSensitiveValue(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const raw = typeof value === 'string' ? value : String(value);
|
||||||
|
if (!raw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const maskFn = (this && typeof this.maskApiKey === 'function') ? this.maskApiKey : (v) => v;
|
||||||
|
let masked = raw;
|
||||||
|
|
||||||
|
const queryRegex = /([?&])(api[-_]?key|key|token|access_token|authorization)=([^&#\s]+)/ig;
|
||||||
|
masked = masked.replace(queryRegex, (full, prefix, keyName, valuePart) => `${prefix}${keyName}=${maskFn(valuePart)}`);
|
||||||
|
|
||||||
|
const headerRegex = /(api[-_]?key|key|token|access[-_]?token|authorization)\s*([:=])\s*([A-Za-z0-9._-]+)/ig;
|
||||||
|
masked = masked.replace(headerRegex, (full, keyName, separator, valuePart) => `${keyName}${separator}${maskFn(valuePart)}`);
|
||||||
|
|
||||||
|
const keyLikeRegex = /(sk-[A-Za-z0-9]{6,}|AI[a-zA-Z0-9_-]{6,}|AIza[0-9A-Za-z-_]{8,}|hf_[A-Za-z0-9]{6,}|pk_[A-Za-z0-9]{6,}|rk_[A-Za-z0-9]{6,})/g;
|
||||||
|
masked = masked.replace(keyLikeRegex, match => maskFn(match));
|
||||||
|
|
||||||
|
if (masked === raw) {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (trimmed && !/\s/.test(trimmed)) {
|
||||||
|
const looksLikeKey = /^sk-/i.test(trimmed)
|
||||||
|
|| /^AI/i.test(trimmed)
|
||||||
|
|| /^AIza/i.test(trimmed)
|
||||||
|
|| /^hf_/i.test(trimmed)
|
||||||
|
|| /^pk_/i.test(trimmed)
|
||||||
|
|| /^rk_/i.test(trimmed)
|
||||||
|
|| (!/[\\/]/.test(trimmed) && (/\d/.test(trimmed) || trimmed.length >= 10))
|
||||||
|
|| trimmed.length >= 24;
|
||||||
|
if (looksLikeKey) {
|
||||||
|
return maskFn(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取API密钥的统计信息
|
// 获取API密钥的统计信息
|
||||||
export async function getKeyStats(usageData = null) {
|
export async function getKeyStats(usageData = null) {
|
||||||
try {
|
try {
|
||||||
@@ -45,7 +85,9 @@ export async function getKeyStats(usageData = null) {
|
|||||||
const details = modelEntry.details || [];
|
const details = modelEntry.details || [];
|
||||||
|
|
||||||
details.forEach(detail => {
|
details.forEach(detail => {
|
||||||
const source = detail.source;
|
const source = this.maskUsageSensitiveValue
|
||||||
|
? this.maskUsageSensitiveValue(detail.source)
|
||||||
|
: detail.source;
|
||||||
const authIndexKey = normalizeAuthIndex(detail?.auth_index);
|
const authIndexKey = normalizeAuthIndex(detail?.auth_index);
|
||||||
const isFailed = detail.failed === true;
|
const isFailed = detail.failed === true;
|
||||||
|
|
||||||
@@ -1489,17 +1531,27 @@ export function updateApiStatsTable(data) {
|
|||||||
Object.entries(apis).forEach(([endpoint, apiData]) => {
|
Object.entries(apis).forEach(([endpoint, apiData]) => {
|
||||||
const totalRequests = apiData.total_requests || 0;
|
const totalRequests = apiData.total_requests || 0;
|
||||||
const endpointCost = calculateEndpointCost(apiData);
|
const endpointCost = calculateEndpointCost(apiData);
|
||||||
|
const displayEndpoint = (this.maskUsageSensitiveValue
|
||||||
|
? this.maskUsageSensitiveValue(endpoint)
|
||||||
|
: (endpoint ?? '')) || '-';
|
||||||
|
const safeEndpoint = this.escapeHtml
|
||||||
|
? this.escapeHtml(displayEndpoint)
|
||||||
|
: displayEndpoint;
|
||||||
|
|
||||||
// 构建模型详情
|
// 构建模型详情
|
||||||
let modelsHtml = '';
|
let modelsHtml = '';
|
||||||
if (apiData.models && Object.keys(apiData.models).length > 0) {
|
if (apiData.models && Object.keys(apiData.models).length > 0) {
|
||||||
modelsHtml = '<div class="model-details">';
|
modelsHtml = '<div class="model-details">';
|
||||||
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
|
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
|
||||||
|
const maskedModel = (this.maskUsageSensitiveValue
|
||||||
|
? this.maskUsageSensitiveValue(modelName)
|
||||||
|
: modelName) || '';
|
||||||
|
const safeModel = this.escapeHtml ? this.escapeHtml(maskedModel) : maskedModel;
|
||||||
const modelRequests = modelData.total_requests ?? 0;
|
const modelRequests = modelData.total_requests ?? 0;
|
||||||
const modelTokens = this.formatTokensInMillions(modelData.total_tokens ?? 0);
|
const modelTokens = this.formatTokensInMillions(modelData.total_tokens ?? 0);
|
||||||
modelsHtml += `
|
modelsHtml += `
|
||||||
<div class="model-item">
|
<div class="model-item">
|
||||||
<span class="model-name">${modelName}</span>
|
<span class="model-name">${safeModel}</span>
|
||||||
<span>${modelRequests} 请求 / ${modelTokens} tokens</span>
|
<span>${modelRequests} 请求 / ${modelTokens} tokens</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1509,7 +1561,7 @@ export function updateApiStatsTable(data) {
|
|||||||
|
|
||||||
tableHtml += `
|
tableHtml += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${endpoint}</td>
|
<td>${safeEndpoint}</td>
|
||||||
<td>${totalRequests}</td>
|
<td>${totalRequests}</td>
|
||||||
<td>${this.formatTokensInMillions(apiData.total_tokens || 0)}</td>
|
<td>${this.formatTokensInMillions(apiData.total_tokens || 0)}</td>
|
||||||
<td>${hasPrices && endpointCost > 0 ? this.formatUsd(endpointCost) : '--'}</td>
|
<td>${hasPrices && endpointCost > 0 ? this.formatUsd(endpointCost) : '--'}</td>
|
||||||
@@ -1526,6 +1578,7 @@ export const usageModule = {
|
|||||||
getKeyStats,
|
getKeyStats,
|
||||||
loadUsageStats,
|
loadUsageStats,
|
||||||
updateUsageOverview,
|
updateUsageOverview,
|
||||||
|
maskUsageSensitiveValue,
|
||||||
getModelNamesFromUsage,
|
getModelNamesFromUsage,
|
||||||
getChartLineMaxCount,
|
getChartLineMaxCount,
|
||||||
getVisibleChartLineCount,
|
getVisibleChartLineCount,
|
||||||
|
|||||||
@@ -804,7 +804,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
min-height: 100%;
|
min-height: calc(100vh - var(--navbar-height, 69px));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部导航栏 */
|
/* 顶部导航栏 */
|
||||||
@@ -1036,6 +1036,8 @@ body {
|
|||||||
/* 主内容区域 */
|
/* 主内容区域 */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 24px 32px;
|
padding: 24px 32px;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1046,6 +1048,8 @@ body {
|
|||||||
.content-area {
|
.content-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
@@ -3528,6 +3532,7 @@ input:checked+.slider:before {
|
|||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
border-top: 1px solid var(--border-primary);
|
border-top: 1px solid var(--border-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-info {
|
.version-info {
|
||||||
|
|||||||
Reference in New Issue
Block a user