Compare commits

...

5 Commits

Author SHA1 Message Date
hkfires
181cba6886 feat(logs): limit lines, incremental updates, and highlighting 2025-11-03 18:44:50 +08:00
hkfires
aa729914c5 fix(ui): position API Keys/Providers action buttons; prevent overlap 2025-11-03 16:31:29 +08:00
hkfires
f98f31f2ed fix(i18n, ui): correct syntax error in generateDynamicTypeLabel method
- Move generateDynamicTypeLabel method from inside updateAuthFileFilterButtons to proper class method location
- Fix JavaScript syntax error that prevented app from loading
- Preserve all internationalization functionality from 257260b

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 16:05:51 +08:00
hkfires
1e79f918e2 fix(ui): harden key list rendering and config handling 2025-10-31 16:24:43 +08:00
Supra4E8C
257260b1d2 feat(i18n, ui): enhance file type filtering with internationalization support
- Updated the file type display logic to utilize i18n for dynamic translations.
- Refactored filter button definitions to include data attributes for improved localization.
- Added new translation keys for various file types and filter options in both English and Chinese.
- Implemented a method to refresh button texts upon language change, ensuring consistent UI updates.
2025-10-26 17:46:57 +08:00
4 changed files with 724 additions and 86 deletions

469
app.js
View File

@@ -20,6 +20,10 @@ class CLIProxyManager {
// 日志自动刷新定时器
this.logsRefreshTimer = null;
// 当前展示的日志行
this.displayedLogLines = [];
this.maxDisplayLogLines = 10000;
// 日志时间戳(用于增量加载)
this.latestLogTimestamp = null;
@@ -1498,24 +1502,16 @@ class CLIProxyManager {
}
// Gemini 密钥
if (config['generative-language-api-key']) {
await this.renderGeminiKeys(config['generative-language-api-key']);
}
await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []);
// Codex 密钥
if (config['codex-api-key']) {
await this.renderCodexKeys(config['codex-api-key']);
}
await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : []);
// Claude 密钥
if (config['claude-api-key']) {
await this.renderClaudeKeys(config['claude-api-key']);
}
await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : []);
// OpenAI 兼容提供商
if (config['openai-compatibility']) {
await this.renderOpenAIProviders(config['openai-compatibility']);
}
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : []);
}
// 回退方法:原来的逐个加载方式
@@ -1794,6 +1790,7 @@ class CLIProxyManager {
if (!logsContent) return;
if (!lines || lines.length === 0) {
this.displayedLogLines = [];
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p data-i18n="logs.empty_title">' +
i18n.t('logs.empty_title') + '</p><p data-i18n="logs.empty_desc">' +
i18n.t('logs.empty_desc') + '</p></div>';
@@ -1803,26 +1800,22 @@ class CLIProxyManager {
// 过滤掉 /v0/management/logs 相关的日志
const filteredLines = lines.filter(line => !line.includes('/v0/management/logs'));
// 限制前端显示的最大行数为 10000 行
const MAX_DISPLAY_LINES = 10000;
// 限制前端显示的最大行数
let displayedLines = filteredLines;
let displayedLineCount = filteredLines.length;
if (filteredLines.length > MAX_DISPLAY_LINES) {
const linesToRemove = filteredLines.length - MAX_DISPLAY_LINES;
if (filteredLines.length > this.maxDisplayLogLines) {
const linesToRemove = filteredLines.length - this.maxDisplayLogLines;
displayedLines = filteredLines.slice(linesToRemove);
displayedLineCount = MAX_DISPLAY_LINES;
}
// 将数组转换为文本
const logsText = displayedLines.join('\n');
const logHtml = `
this.displayedLogLines = displayedLines.slice();
const displayedLineCount = this.displayedLogLines.length;
logsContent.innerHTML = `
<div class="logs-info">
<span><i class="fas fa-list-ol"></i> ${displayedLineCount} ${i18n.t('logs.lines')}</span>
</div>
<pre class="logs-text">${this.escapeHtml(logsText)}</pre>
<pre class="logs-text">${this.buildLogsHtml(this.displayedLogLines)}</pre>
`;
logsContent.innerHTML = logHtml;
// 自动滚动到底部
if (scrollToBottom) {
@@ -1838,37 +1831,37 @@ class CLIProxyManager {
const logsContent = document.getElementById('logs-content');
if (!logsContent) return;
const logsTextElement = logsContent.querySelector('.logs-text');
const logsInfoElement = logsContent.querySelector('.logs-info');
if (!logsTextElement || !newLines || newLines.length === 0) {
if (!newLines || newLines.length === 0) {
return;
}
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'));
if (filteredNewLines.length === 0) {
return; // 如果过滤后没有新日志,直接返回
}
if (!logsTextElement) {
this.renderLogs(filteredNewLines, totalLineCount || filteredNewLines.length, true);
return;
}
// 检查用户是否正在查看底部(判断是否需要自动滚动)
const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50;
// 追加新日志文本
const newLogsText = '\n' + filteredNewLines.join('\n');
logsTextElement.textContent += newLogsText;
// 限制前端显示的最大行数为 10000 行
const MAX_DISPLAY_LINES = 10000;
const allLines = logsTextElement.textContent.split('\n').filter(line => line.trim());
if (allLines.length > MAX_DISPLAY_LINES) {
const linesToRemove = allLines.length - MAX_DISPLAY_LINES;
logsTextElement.textContent = allLines.slice(linesToRemove).join('\n');
this.displayedLogLines = this.displayedLogLines.concat(filteredNewLines);
if (this.displayedLogLines.length > this.maxDisplayLogLines) {
this.displayedLogLines = this.displayedLogLines.slice(this.displayedLogLines.length - this.maxDisplayLogLines);
}
logsTextElement.innerHTML = this.buildLogsHtml(this.displayedLogLines);
// 更新行数统计(只显示实际显示的行数,最多 10000 行)
if (logsInfoElement) {
const displayedLines = logsTextElement.textContent.split('\n').filter(line => line.trim()).length;
const displayedLines = this.displayedLogLines.length;
logsInfoElement.innerHTML = `<span><i class="fas fa-list-ol"></i> ${displayedLines} ${i18n.t('logs.lines')}</span>`;
}
@@ -1878,6 +1871,259 @@ class CLIProxyManager {
}
}
// 根据日志内容构造高亮 HTML
buildLogsHtml(lines) {
if (!lines || lines.length === 0) {
return '';
}
return lines.map(line => {
// 先过滤掉 [GIN] 2025/11/03 - 18:32:59 部分
// 匹配模式:[GIN] 后面跟着日期 - 时间
let processedLine = line.replace(/\[GIN\]\s+\d{4}\/\d{2}\/\d{2}\s+-\s+\d{2}:\d{2}:\d{2}\s+/g, '');
// 创建标记数组来跟踪需要高亮的位置
const highlights = [];
// 1. 检测 HTTP 状态码
const statusInfo = this.detectHttpStatus(line);
if (statusInfo) {
const statusPattern = new RegExp(`\\b${statusInfo.code}\\b`);
const match = statusPattern.exec(processedLine);
if (match) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: `log-status-tag log-status-${statusInfo.bucket}`,
priority: 10
});
}
}
// 2. 时间戳(只匹配标准格式,排除日期和时间被分隔符分开的情况)
// 匹配格式:
// - 2024-01-01 12:00:00 或 2024/01/01 12:00:00中间只有一个空格
// - 2024-01-01T12:00:00
// - [12:00:00]
// 排除2024/01/01 - 12:00:00中间有 - 分隔符)
const timestampPattern = /\d{4}[-/]\d{2}[-/]\d{2}[T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?|\[\d{2}:\d{2}:\d{2}\]/g;
let match;
while ((match = timestampPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-timestamp',
priority: 5
});
}
// 2b. 只匹配在方括号内的完整日期时间(如 [2025-11-03 18:23:14]
const bracketTimestampPattern = /\[\d{4}[-/]\d{2}[-/]\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?\]/g;
while ((match = bracketTimestampPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-timestamp',
priority: 5
});
}
// 3. 日志级别(只匹配在方括号内的,避免匹配路径中的关键字)
const levelPattern = /\[(ERROR|ERRO|ERR|FATAL|CRITICAL|CRIT|WARN|WARNING|INFO|DEBUG|TRACE|PANIC)\]/gi;
while ((match = levelPattern.exec(processedLine)) !== null) {
const level = match[1].toUpperCase();
let className = 'log-level';
if (['ERROR', 'ERRO', 'ERR', 'FATAL', 'CRITICAL', 'CRIT', 'PANIC'].includes(level)) {
className += ' log-level-error';
} else if (['WARN', 'WARNING'].includes(level)) {
className += ' log-level-warn';
} else if (level === 'INFO') {
className += ' log-level-info';
} else if (['DEBUG', 'TRACE'].includes(level)) {
className += ' log-level-debug';
}
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: className,
priority: 8
});
}
// 4. HTTP 方法
const methodPattern = /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\b/g;
while ((match = methodPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-http-method',
priority: 6
});
}
// 5. URL仅匹配 HTTP(S) 完整URL不匹配路径
const urlPattern = /(https?:\/\/[^\s<>"]+)/g;
while ((match = urlPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-path',
priority: 4
});
}
// 6. IP 地址
const ipPattern = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
while ((match = ipPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-ip',
priority: 7
});
}
// 7. 关键字
const successPattern = /\b(success|successful|succeeded|completed|ok|done|passed)\b/gi;
while ((match = successPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-keyword-success',
priority: 3
});
}
const errorPattern = /\b(failed|failure|error|exception|panic|fatal|critical|aborted|denied|refused|timeout|invalid)\b/gi;
while ((match = errorPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-keyword-error',
priority: 3
});
}
const warnPattern = /\b(warning|warn|deprecated|slow|retry|retrying)\b/gi;
while ((match = warnPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-keyword-warn',
priority: 3
});
}
// 8. 数字和单位
const numberPattern = /\b(\d+(?:\.\d+)?)(ms|μs|ns|s|KB|MB|GB|TB|B|%)\b/g;
while ((match = numberPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-number-unit',
priority: 2
});
}
// 移除重叠的高亮(保留优先级高的)
highlights.sort((a, b) => {
if (a.start !== b.start) return a.start - b.start;
return b.priority - a.priority;
});
const filteredHighlights = [];
for (const h of highlights) {
const overlaps = filteredHighlights.some(existing =>
(h.start >= existing.start && h.start < existing.end) ||
(h.end > existing.start && h.end <= existing.end) ||
(h.start <= existing.start && h.end >= existing.end)
);
if (!overlaps) {
filteredHighlights.push(h);
}
}
// 构建最终的 HTML
filteredHighlights.sort((a, b) => a.start - b.start);
let result = '';
let lastIndex = 0;
for (const h of filteredHighlights) {
// 添加高亮之前的文本
if (h.start > lastIndex) {
result += this.escapeHtml(processedLine.substring(lastIndex, h.start));
}
// 添加高亮的文本
result += `<span class="${h.className}">${this.escapeHtml(processedLine.substring(h.start, h.end))}</span>`;
lastIndex = h.end;
}
// 添加剩余的文本
if (lastIndex < processedLine.length) {
result += this.escapeHtml(processedLine.substring(lastIndex));
}
return `<span class="log-line">${result}</span>`;
}).join('');
}
// 检测 HTTP 状态码
detectHttpStatus(line) {
if (!line) return null;
// 更精确的 HTTP 状态码匹配模式
// 匹配:
// 1. "| 状态码 |"(如:| 200 | 或 |200|
// 2. "状态码 -" 或 "状态码-"200 - 45ms 或 200-45ms
// 3. "HTTP方法 路径 状态码"GET /api 200
// 4. "状态码 OK/Error/等状态文本"200 OK
// 5. "status: 状态码" 或 "code: 状态码"
// 排除:
// - 文件路径中的数字(如 .go:396
// - 端口号(如 :8080
// - 普通数字序列
const patterns = [
// 匹配 "| 状态码 |" 或 "|状态码|"(日志中常见格式)
/\|\s*([1-5]\d{2})\s*\|/,
// 匹配 "状态码 -" 或 "状态码-"
/\b([1-5]\d{2})\s*-/,
// 匹配 HTTP 方法后的状态码GET/POST/等 路径 状态码)
/\b(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+\S+\s+([1-5]\d{2})\b/,
// 匹配 "status:" 或 "code:" 后的状态码
/\b(?:status|code|http)[:\s]+([1-5]\d{2})\b/i,
// 匹配状态码后跟状态文本200 OK, 404 Not Found 等)
/\b([1-5]\d{2})\s+(?:OK|Created|Accepted|No Content|Moved|Found|Bad Request|Unauthorized|Forbidden|Not Found|Method Not Allowed|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)\b/i
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
const code = parseInt(match[1], 10);
if (Number.isNaN(code)) {
continue;
}
if (code >= 500) {
return { code, bucket: '5xx', match: match[1] };
}
if (code >= 400) {
return { code, bucket: '4xx', match: match[1] };
}
if (code >= 300) {
return { code, bucket: '3xx', match: match[1] };
}
if (code >= 200) {
return { code, bucket: '2xx', match: match[1] };
}
if (code >= 100) {
return { code, bucket: '1xx', match: match[1] };
}
}
}
return null;
}
// 下载日志
async downloadLogs() {
try {
@@ -2179,9 +2425,8 @@ class CLIProxyManager {
async loadGeminiKeys() {
try {
const config = await this.getConfig();
if (config['generative-language-api-key']) {
await this.renderGeminiKeys(config['generative-language-api-key']);
}
const keys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [];
await this.renderGeminiKeys(keys);
} catch (error) {
console.error('加载Gemini密钥失败:', error);
}
@@ -2190,8 +2435,12 @@ class CLIProxyManager {
// 渲染Gemini密钥列表
async renderGeminiKeys(keys) {
const container = document.getElementById('gemini-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
if (keys.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fab fa-google"></i>
@@ -2205,7 +2454,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = keys.map((key, index) => {
container.innerHTML = list.map((key, index) => {
const masked = this.maskApiKey(key);
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 };
return `
@@ -2344,9 +2593,8 @@ class CLIProxyManager {
async loadCodexKeys() {
try {
const config = await this.getConfig();
if (config['codex-api-key']) {
await this.renderCodexKeys(config['codex-api-key']);
}
const keys = Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [];
await this.renderCodexKeys(keys);
} catch (error) {
console.error('加载Codex密钥失败:', error);
}
@@ -2355,8 +2603,12 @@ class CLIProxyManager {
// 渲染Codex密钥列表
async renderCodexKeys(keys) {
const container = document.getElementById('codex-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
if (keys.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-code"></i>
@@ -2370,7 +2622,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = keys.map((config, index) => {
container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'];
const masked = rawKey ? this.maskApiKey(rawKey) : '';
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
@@ -2549,9 +2801,8 @@ class CLIProxyManager {
async loadClaudeKeys() {
try {
const config = await this.getConfig();
if (config['claude-api-key']) {
await this.renderClaudeKeys(config['claude-api-key']);
}
const keys = Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : [];
await this.renderClaudeKeys(keys);
} catch (error) {
console.error('加载Claude密钥失败:', error);
}
@@ -2560,8 +2811,12 @@ class CLIProxyManager {
// 渲染Claude密钥列表
async renderClaudeKeys(keys) {
const container = document.getElementById('claude-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
if (keys.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-brain"></i>
@@ -2575,7 +2830,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = keys.map((config, index) => {
container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'];
const masked = rawKey ? this.maskApiKey(rawKey) : '';
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
@@ -2754,9 +3009,8 @@ class CLIProxyManager {
async loadOpenAIProviders() {
try {
const config = await this.getConfig();
if (config['openai-compatibility']) {
await this.renderOpenAIProviders(config['openai-compatibility']);
}
const providers = Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [];
await this.renderOpenAIProviders(providers);
} catch (error) {
console.error('加载OpenAI提供商失败:', error);
}
@@ -2765,8 +3019,12 @@ class CLIProxyManager {
// 渲染OpenAI提供商列表
async renderOpenAIProviders(providers) {
const container = document.getElementById('openai-providers-list');
if (!container) {
return;
}
const list = Array.isArray(providers) ? providers : [];
if (!Array.isArray(providers) || providers.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-plug"></i>
@@ -2781,7 +3039,7 @@ class CLIProxyManager {
}
// 根据提供商数量设置滚动条
if (providers.length > 5) {
if (list.length > 5) {
container.style.maxHeight = '400px';
container.style.overflowY = 'auto';
} else {
@@ -2792,7 +3050,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = providers.map((provider, index) => {
container.innerHTML = list.map((provider, index) => {
const item = typeof provider === 'object' && provider !== null ? provider : {};
// 处理两种API密钥格式新的 api-key-entries 和旧的 api-keys
@@ -3132,13 +3390,31 @@ class CLIProxyManager {
// 使用API返回的文件类型
const fileType = file.type || 'unknown';
// 首字母大写显示类型,特殊处理 iFlow
let typeDisplay;
if (fileType === 'iflow') {
typeDisplay = 'iFlow';
} else {
typeDisplay = fileType.charAt(0).toUpperCase() + fileType.slice(1);
let typeDisplayKey;
switch (fileType) {
case 'qwen':
typeDisplayKey = 'auth_files.type_qwen';
break;
case 'gemini':
typeDisplayKey = 'auth_files.type_gemini';
break;
case 'claude':
typeDisplayKey = 'auth_files.type_claude';
break;
case 'codex':
typeDisplayKey = 'auth_files.type_codex';
break;
case 'iflow':
typeDisplayKey = 'auth_files.type_iflow';
break;
case 'empty':
typeDisplayKey = 'auth_files.type_empty';
break;
default:
typeDisplayKey = 'auth_files.type_unknown';
break;
}
const typeBadge = `<span class="file-type-badge ${fileType}">${typeDisplay}</span>`;
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
return `
<div class="file-item" data-file-type="${fileType}">
@@ -3188,13 +3464,13 @@ class CLIProxyManager {
// 预定义的按钮顺序和显示文本
const predefinedTypes = [
{ type: 'all', label: 'All' },
{ type: 'qwen', label: 'Qwen' },
{ type: 'gemini', label: 'Gemini' },
{ type: 'claude', label: 'Claude' },
{ type: 'codex', label: 'Codex' },
{ type: 'iflow', label: 'iFlow' },
{ type: 'empty', label: 'Empty' }
{ type: 'all', labelKey: 'auth_files.filter_all' },
{ type: 'qwen', labelKey: 'auth_files.filter_qwen' },
{ type: 'gemini', labelKey: 'auth_files.filter_gemini' },
{ type: 'claude', labelKey: 'auth_files.filter_claude' },
{ type: 'codex', labelKey: 'auth_files.filter_codex' },
{ type: 'iflow', labelKey: 'auth_files.filter_iflow' },
{ type: 'empty', labelKey: 'auth_files.filter_empty' }
];
// 获取现有按钮
@@ -3209,6 +3485,11 @@ class CLIProxyManager {
const btnType = btn.dataset.type;
if (existingTypes.has(btnType)) {
btn.style.display = 'inline-block';
const match = predefinedTypes.find(item => item.type === btnType);
if (match) {
btn.textContent = i18n.t(match.labelKey);
btn.setAttribute('data-i18n-text', match.labelKey);
}
} else {
btn.style.display = 'none';
}
@@ -3222,9 +3503,17 @@ class CLIProxyManager {
const btn = document.createElement('button');
btn.className = 'filter-btn';
btn.dataset.type = type;
// 首字母大写
btn.textContent = type.charAt(0).toUpperCase() + type.slice(1);
const match = predefinedTypes.find(item => item.type === type);
if (match) {
btn.setAttribute('data-i18n-text', match.labelKey);
btn.textContent = i18n.t(match.labelKey);
} else {
const dynamicKey = `auth_files.filter_${type}`;
btn.setAttribute('data-i18n-text', dynamicKey);
btn.textContent = this.generateDynamicTypeLabel(type);
}
// 插入到 Empty 按钮之前(如果存在)
const emptyBtn = filterContainer.querySelector('[data-type="empty"]');
if (emptyBtn) {
@@ -3256,6 +3545,21 @@ class CLIProxyManager {
item.classList.add('hidden');
}
});
// 更新筛选按钮文本(以防语言切换后新按钮未刷新)
this.refreshFilterButtonTexts();
}
// 生成动态类型标签
generateDynamicTypeLabel(type) {
if (!type) return '';
const key = `auth_files.type_${type}`;
const translated = i18n.t(key);
if (translated && translated !== key) {
return translated;
}
if (type.toLowerCase() === 'iflow') return 'iFlow';
return type.charAt(0).toUpperCase() + type.slice(1);
}
// 绑定认证文件筛选事件
@@ -3277,6 +3581,19 @@ class CLIProxyManager {
filterContainer._filterListener = listener;
filterContainer.addEventListener('click', listener);
// 首次渲染时刷新按钮文本
this.refreshFilterButtonTexts();
}
// 刷新筛选按钮文本(根据 data-i18n-text
refreshFilterButtonTexts() {
document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => {
const key = btn.getAttribute('data-i18n-text');
if (key) {
btn.textContent = i18n.t(key);
}
});
}
// 绑定认证文件操作按钮事件(使用事件委托)

30
i18n.js
View File

@@ -222,6 +222,21 @@ const i18n = {
'auth_files.delete_success': '文件删除成功',
'auth_files.delete_all_success': '成功删除',
'auth_files.files_count': '个文件',
'auth_files.filter_all': '全部',
'auth_files.filter_qwen': 'Qwen',
'auth_files.filter_gemini': 'Gemini',
'auth_files.filter_claude': 'Claude',
'auth_files.filter_codex': 'Codex',
'auth_files.filter_iflow': 'iFlow',
'auth_files.filter_empty': '空文件',
'auth_files.filter_unknown': '其他',
'auth_files.type_qwen': 'Qwen',
'auth_files.type_gemini': 'Gemini',
'auth_files.type_claude': 'Claude',
'auth_files.type_codex': 'Codex',
'auth_files.type_iflow': 'iFlow',
'auth_files.type_empty': '空文件',
'auth_files.type_unknown': '其他',
// Codex OAuth
@@ -649,6 +664,21 @@ const i18n = {
'auth_files.delete_success': 'File deleted successfully',
'auth_files.delete_all_success': 'Successfully deleted',
'auth_files.files_count': 'files',
'auth_files.filter_all': 'All',
'auth_files.filter_qwen': 'Qwen',
'auth_files.filter_gemini': 'Gemini',
'auth_files.filter_claude': 'Claude',
'auth_files.filter_codex': 'Codex',
'auth_files.filter_iflow': 'iFlow',
'auth_files.filter_empty': 'Empty',
'auth_files.filter_unknown': 'Other',
'auth_files.type_qwen': 'Qwen',
'auth_files.type_gemini': 'Gemini',
'auth_files.type_claude': 'Claude',
'auth_files.type_codex': 'Codex',
'auth_files.type_iflow': 'iFlow',
'auth_files.type_empty': 'Empty',
'auth_files.type_unknown': 'Other',
// Codex OAuth
'auth_login.codex_oauth_title': 'Codex OAuth',

View File

@@ -424,13 +424,13 @@
data-i18n="auth_files.title_section">认证文件</span></h3>
<!-- 类型筛选 -->
<div class="auth-file-filter">
<button class="filter-btn active" data-type="all">All</button>
<button class="filter-btn" data-type="qwen">Qwen</button>
<button class="filter-btn" data-type="gemini">Gemini</button>
<button class="filter-btn" data-type="claude">Claude</button>
<button class="filter-btn" data-type="codex">Codex</button>
<button class="filter-btn" data-type="iflow">iFlow</button>
<button class="filter-btn" data-type="empty">Empty</button>
<button class="filter-btn active" data-type="all" data-i18n-text="auth_files.filter_all">All</button>
<button class="filter-btn" data-type="qwen" data-i18n-text="auth_files.filter_qwen">Qwen</button>
<button class="filter-btn" data-type="gemini" data-i18n-text="auth_files.filter_gemini">Gemini</button>
<button class="filter-btn" data-type="claude" data-i18n-text="auth_files.filter_claude">Claude</button>
<button class="filter-btn" data-type="codex" data-i18n-text="auth_files.filter_codex">Codex</button>
<button class="filter-btn" data-type="iflow" data-i18n-text="auth_files.filter_iflow">iFlow</button>
<button class="filter-btn" data-type="empty" data-i18n-text="auth_files.filter_empty">Empty</button>
</div>
</div>
<div class="header-actions">

View File

@@ -1699,6 +1699,7 @@ input:checked+.slider:before {
display: flex;
flex-direction: column;
transition: all 0.3s ease;
position: relative;
}
.file-item.hidden {
@@ -1718,6 +1719,13 @@ input:checked+.slider:before {
width: 100%;
}
/* 为 API Keys 和 AI Providers 的项优化按钮位置 */
.key-item .item-content,
.provider-item .item-content {
padding-right: 120px; /* 为按钮预留空间,防止内容重叠 */
min-width: 0; /* 确保内容可以正常换行 */
}
.item-footer {
display: flex;
justify-content: space-between;
@@ -1725,6 +1733,7 @@ input:checked+.slider:before {
margin-top: 12px;
flex-wrap: wrap;
gap: 10px;
align-items: flex-start;
}
.item-stats {
@@ -1736,9 +1745,46 @@ input:checked+.slider:before {
.item-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
gap: 8px;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
}
/* API Keys 和 AI Providers 的按钮组 - 绝对定位到右侧垂直居中 */
.key-item .item-actions,
.provider-item .item-actions {
position: absolute;
top: 50%;
right: 15px;
transform: translateY(-50%);
gap: 8px;
}
/* 按钮样式优化 */
.key-item .item-actions .btn,
.provider-item .item-actions .btn {
min-width: 40px;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.key-item .item-actions .btn:hover,
.provider-item .item-actions .btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.key-item .item-actions .btn i,
.provider-item .item-actions .btn i {
font-size: 16px;
margin: 0;
}
.item-title {
@@ -3234,9 +3280,10 @@ input:checked+.slider:before {
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 16px;
font-size: 12px;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
font-weight: 600;
white-space: pre-wrap;
word-wrap: break-word;
max-height: calc(100vh - 480px);
@@ -3245,6 +3292,133 @@ input:checked+.slider:before {
margin: 0;
}
.logs-text .log-line {
display: block;
padding: 2px 0;
margin: 0;
color: inherit;
}
.logs-text .log-line + .log-line {
margin-top: 2px;
}
.logs-text .log-status-tag {
display: inline-block;
padding: 0 8px;
margin-right: 6px;
border-radius: 6px;
font-weight: 700;
letter-spacing: 0.3px;
}
.logs-text .log-status-tag.log-status-2xx {
background: rgba(16, 185, 129, 0.18);
color: #047857;
}
.logs-text .log-status-tag.log-status-3xx {
background: rgba(6, 182, 212, 0.2);
color: #0f766e;
}
.logs-text .log-status-tag.log-status-4xx {
background: rgba(251, 191, 36, 0.26);
color: #92400e;
}
.logs-text .log-status-tag.log-status-5xx {
background: rgba(239, 68, 68, 0.22);
color: #b91c1c;
}
.logs-text .log-status-tag.log-status-1xx {
background: rgba(165, 180, 252, 0.22);
color: #4338ca;
}
/* 日志高亮样式 */
/* 时间戳高亮 */
.logs-text .log-timestamp {
color: #8b5cf6;
font-weight: 600;
}
/* 日志级别高亮 */
.logs-text .log-level {
display: inline-block;
padding: 1px 6px;
border-radius: 4px;
font-weight: 700;
font-size: 0.95em;
letter-spacing: 0.5px;
}
.logs-text .log-level-error {
background: rgba(239, 68, 68, 0.20);
color: #dc2626;
}
.logs-text .log-level-warn {
background: rgba(251, 191, 36, 0.25);
color: #d97706;
}
.logs-text .log-level-info {
background: rgba(59, 130, 246, 0.20);
color: #2563eb;
}
.logs-text .log-level-debug {
background: rgba(107, 114, 128, 0.18);
color: #6b7280;
}
/* HTTP 方法高亮 */
.logs-text .log-http-method {
color: #0891b2;
font-weight: 700;
}
/* 路径/URL 高亮 */
.logs-text .log-path {
color: #059669;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: rgba(5, 150, 105, 0.3);
}
/* IP 地址高亮 */
.logs-text .log-ip {
color: #7c3aed;
font-weight: 600;
}
/* 关键字高亮 - 成功 */
.logs-text .log-keyword-success {
color: #059669;
font-weight: 600;
}
/* 关键字高亮 - 错误 */
.logs-text .log-keyword-error {
color: #dc2626;
font-weight: 600;
}
/* 关键字高亮 - 警告 */
.logs-text .log-keyword-warn {
color: #ea580c;
font-weight: 600;
}
/* 数字和单位高亮 */
.logs-text .log-number-unit {
color: #db2777;
font-weight: 600;
}
.logs-text::-webkit-scrollbar {
width: 8px;
height: 8px;
@@ -3378,10 +3552,89 @@ input:checked+.slider:before {
color: var(--text-tertiary);
}
[data-theme="dark"] .logs-text .log-status-tag.log-status-2xx {
background: rgba(16, 185, 129, 0.32);
color: #6ee7b7;
}
[data-theme="dark"] .logs-text .log-status-tag.log-status-3xx {
background: rgba(6, 182, 212, 0.34);
color: #5eead4;
}
[data-theme="dark"] .logs-text .log-status-tag.log-status-4xx {
background: rgba(251, 191, 36, 0.38);
color: #fcd34d;
}
[data-theme="dark"] .logs-text .log-status-tag.log-status-5xx {
background: rgba(239, 68, 68, 0.38);
color: #fca5a5;
}
[data-theme="dark"] .logs-text .log-status-tag.log-status-1xx {
background: rgba(165, 180, 252, 0.38);
color: #c7d2fe;
}
[data-theme="dark"] .logs-container {
background: rgba(15, 23, 42, 0.3);
}
/* 暗色主题 - 日志高亮样式 */
[data-theme="dark"] .logs-text .log-timestamp {
color: #c4b5fd;
}
[data-theme="dark"] .logs-text .log-level-error {
background: rgba(239, 68, 68, 0.32);
color: #fca5a5;
}
[data-theme="dark"] .logs-text .log-level-warn {
background: rgba(251, 191, 36, 0.35);
color: #fbbf24;
}
[data-theme="dark"] .logs-text .log-level-info {
background: rgba(59, 130, 246, 0.32);
color: #93c5fd;
}
[data-theme="dark"] .logs-text .log-level-debug {
background: rgba(107, 114, 128, 0.28);
color: #9ca3af;
}
[data-theme="dark"] .logs-text .log-http-method {
color: #22d3ee;
}
[data-theme="dark"] .logs-text .log-path {
color: #34d399;
text-decoration-color: rgba(52, 211, 153, 0.3);
}
[data-theme="dark"] .logs-text .log-ip {
color: #a78bfa;
}
[data-theme="dark"] .logs-text .log-keyword-success {
color: #34d399;
}
[data-theme="dark"] .logs-text .log-keyword-error {
color: #f87171;
}
[data-theme="dark"] .logs-text .log-keyword-warn {
color: #fb923c;
}
[data-theme="dark"] .logs-text .log-number-unit {
color: #f472b6;
}
/* ===== AI提供商统计徽章样式 ===== */
/* 统计信息容器 */
@@ -3460,6 +3713,44 @@ input:checked+.slider:before {
font-size: 11px !important;
}
}
/* 响应式设计 - API Keys 和 AI Providers 按钮优化 */
@media (max-width: 768px) {
/* 移动端按钮位置调整 */
.key-item .item-actions,
.provider-item .item-actions {
position: relative;
top: auto;
right: auto;
transform: none;
margin-top: 12px;
justify-content: flex-start;
}
/* 移动端移除内容右侧内边距 */
.key-item .item-content,
.provider-item .item-content {
padding-right: 0;
}
/* 移动端按钮尺寸调整 */
.key-item .item-actions .btn,
.provider-item .item-actions .btn {
min-width: 36px;
height: 36px;
}
.key-item .item-actions .btn:hover,
.provider-item .item-actions .btn:hover {
transform: scale(1.05);
}
.key-item .item-actions .btn i,
.provider-item .item-actions .btn i {
font-size: 14px;
}
}
#config-management .CodeMirror .CodeMirror-lines {
padding: 12px;
}