From ac4f310fe878eb9b6ae4b2629a92d3643e41b311 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Fri, 5 Dec 2025 18:30:01 +0800 Subject: [PATCH] feat: add sensitive value masking functionality to usage module and update UI for system info localization --- i18n.js | 2 +- index.html | 2 +- src/modules/usage.js | 59 +++++++++++++++++++++++++++++++++++++++++--- styles.css | 7 +++++- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/i18n.js b/i18n.js index 9a96884..3a62dc1 100644 --- a/i18n.js +++ b/i18n.js @@ -94,7 +94,7 @@ const i18n = { 'nav.usage_stats': '使用统计', 'nav.config_management': '配置管理', 'nav.logs': '日志查看', - 'nav.system_info': '管理中心信息', + 'nav.system_info': '中心信息', // 基础设置 'basic_settings.title': '基础设置', diff --git a/index.html b/index.html index 388d39a..cb6d3c2 100644 --- a/index.html +++ b/index.html @@ -182,7 +182,7 @@ 日志查看
  • - 管理中心信息 + 中心信息
  • diff --git a/src/modules/usage.js b/src/modules/usage.js index 731c2bb..6e5d9db 100644 --- a/src/modules/usage.js +++ b/src/modules/usage.js @@ -5,6 +5,46 @@ const DEFAULT_CHART_LINE_COUNT = 3; const MIN_CHART_LINE_COUNT = 1; 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密钥的统计信息 export async function getKeyStats(usageData = null) { try { @@ -45,7 +85,9 @@ export async function getKeyStats(usageData = null) { const details = modelEntry.details || []; details.forEach(detail => { - const source = detail.source; + const source = this.maskUsageSensitiveValue + ? this.maskUsageSensitiveValue(detail.source) + : detail.source; const authIndexKey = normalizeAuthIndex(detail?.auth_index); const isFailed = detail.failed === true; @@ -1489,17 +1531,27 @@ export function updateApiStatsTable(data) { Object.entries(apis).forEach(([endpoint, apiData]) => { const totalRequests = apiData.total_requests || 0; const endpointCost = calculateEndpointCost(apiData); + const displayEndpoint = (this.maskUsageSensitiveValue + ? this.maskUsageSensitiveValue(endpoint) + : (endpoint ?? '')) || '-'; + const safeEndpoint = this.escapeHtml + ? this.escapeHtml(displayEndpoint) + : displayEndpoint; // 构建模型详情 let modelsHtml = ''; if (apiData.models && Object.keys(apiData.models).length > 0) { modelsHtml = '
    '; 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 modelTokens = this.formatTokensInMillions(modelData.total_tokens ?? 0); modelsHtml += `
    - ${modelName} + ${safeModel} ${modelRequests} 请求 / ${modelTokens} tokens
    `; @@ -1509,7 +1561,7 @@ export function updateApiStatsTable(data) { tableHtml += ` - ${endpoint} + ${safeEndpoint} ${totalRequests} ${this.formatTokensInMillions(apiData.total_tokens || 0)} ${hasPrices && endpointCost > 0 ? this.formatUsd(endpointCost) : '--'} @@ -1526,6 +1578,7 @@ export const usageModule = { getKeyStats, loadUsageStats, updateUsageOverview, + maskUsageSensitiveValue, getModelNamesFromUsage, getChartLineMaxCount, getVisibleChartLineCount, diff --git a/styles.css b/styles.css index c9cdf91..9fcf17a 100644 --- a/styles.css +++ b/styles.css @@ -804,7 +804,7 @@ body { display: flex; flex-direction: column; background: var(--bg-primary); - min-height: 100%; + min-height: calc(100vh - var(--navbar-height, 69px)); } /* 顶部导航栏 */ @@ -1036,6 +1036,8 @@ body { /* 主内容区域 */ .main-content { flex: 1; + display: flex; + flex-direction: column; padding: 24px 32px; max-width: 1400px; width: 100%; @@ -1046,6 +1048,8 @@ body { .content-area { flex: 1; width: 100%; + display: flex; + flex-direction: column; } .content-section { @@ -3528,6 +3532,7 @@ input:checked+.slider:before { margin-top: 40px; padding: 24px 0; border-top: 1px solid var(--border-primary); + flex-shrink: 0; } .version-info {