feat: add sensitive value masking functionality to usage module and update UI for system info localization

This commit is contained in:
Supra4E8C
2025-12-05 18:30:01 +08:00
parent ba6a461a40
commit ac4f310fe8
4 changed files with 64 additions and 6 deletions

View File

@@ -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': '基础设置',

View File

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

View File

@@ -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,

View File

@@ -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 {