mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa1dedc932 | ||
|
|
61e75eee97 | ||
|
|
3a2d96725f | ||
|
|
8283e99909 | ||
|
|
181cba6886 |
412
app.js
412
app.js
@@ -20,6 +20,10 @@ class CLIProxyManager {
|
||||
// 日志自动刷新定时器
|
||||
this.logsRefreshTimer = null;
|
||||
|
||||
// 当前展示的日志行
|
||||
this.displayedLogLines = [];
|
||||
this.maxDisplayLogLines = 10000;
|
||||
|
||||
// 日志时间戳(用于增量加载)
|
||||
this.latestLogTimestamp = null;
|
||||
|
||||
@@ -1786,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>';
|
||||
@@ -1795,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) {
|
||||
@@ -1830,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>`;
|
||||
}
|
||||
|
||||
@@ -1870,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 {
|
||||
@@ -2061,6 +2315,30 @@ class CLIProxyManager {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 兼容服务端返回的数组结构
|
||||
normalizeArrayResponse(data, key) {
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
if (data && Array.isArray(data[key])) {
|
||||
return data[key];
|
||||
}
|
||||
if (data && Array.isArray(data.items)) {
|
||||
return data.items;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 构造Codex配置,保持未展示的字段
|
||||
buildCodexConfig(apiKey, baseUrl, proxyUrl, original = {}) {
|
||||
return {
|
||||
...original,
|
||||
'api-key': apiKey,
|
||||
'base-url': baseUrl || '',
|
||||
'proxy-url': proxyUrl || ''
|
||||
};
|
||||
}
|
||||
|
||||
// 显示添加API密钥模态框
|
||||
showAddApiKeyModal() {
|
||||
const modal = document.getElementById('modal');
|
||||
@@ -2441,15 +2719,9 @@ class CLIProxyManager {
|
||||
|
||||
try {
|
||||
const data = await this.makeRequest('/codex-api-key');
|
||||
const currentKeys = data['codex-api-key'] || [];
|
||||
const currentKeys = this.normalizeArrayResponse(data, 'codex-api-key').map(item => ({ ...item }));
|
||||
|
||||
const newConfig = { 'api-key': apiKey };
|
||||
if (baseUrl) {
|
||||
newConfig['base-url'] = baseUrl;
|
||||
}
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl);
|
||||
|
||||
currentKeys.push(newConfig);
|
||||
|
||||
@@ -2507,14 +2779,16 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const newConfig = { 'api-key': apiKey };
|
||||
if (baseUrl) {
|
||||
newConfig['base-url'] = baseUrl;
|
||||
}
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
const listResponse = await this.makeRequest('/codex-api-key');
|
||||
const currentList = this.normalizeArrayResponse(listResponse, 'codex-api-key');
|
||||
|
||||
if (!Array.isArray(currentList) || index < 0 || index >= currentList.length) {
|
||||
throw new Error('Invalid codex configuration index');
|
||||
}
|
||||
|
||||
const original = currentList[index] ? { ...currentList[index] } : {};
|
||||
const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, original);
|
||||
|
||||
await this.makeRequest('/codex-api-key', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ index, value: newConfig })
|
||||
@@ -3096,17 +3370,13 @@ class CLIProxyManager {
|
||||
if (fileStats.success === 0 && fileStats.failure === 0) {
|
||||
const nameWithoutExt = file.name.replace(/\.[^/.]+$/, ""); // 去掉扩展名
|
||||
|
||||
// 后端有两种脱敏规则,都要尝试:
|
||||
// 规则1:完整描述脱敏 - mikiunameina@gmail.com (ethereal-advice-465201-t0) -> 脱敏 -> miki...-t0)
|
||||
// 规则2:直接整体脱敏 - mikiunameina@gmail.com-ethereal-advice-465201-t0 -> 脱敏 -> ???
|
||||
|
||||
const possibleSources = [];
|
||||
|
||||
// 规则1:尝试完整描述脱敏
|
||||
const match = nameWithoutExt.match(/^([^@]+@[^-]+)-(.+)$/);
|
||||
if (match) {
|
||||
const email = match[1]; // mikiunameina@gmail.com
|
||||
const projectName = match[2]; // ethereal-advice-465201-t0
|
||||
const email = match[1];
|
||||
const projectName = match[2];
|
||||
|
||||
// 组合成完整的描述格式
|
||||
const fullDescription = `${email} (${projectName})`;
|
||||
@@ -3124,6 +3394,12 @@ class CLIProxyManager {
|
||||
possibleSources.push(maskedPersonalId);
|
||||
}
|
||||
|
||||
// 规则3:AI Studio 特殊处理 - 对完整文件名脱敏
|
||||
if (nameWithoutExt.startsWith('aistudio-')) {
|
||||
const maskedFullName = this.maskApiKey(nameWithoutExt);
|
||||
possibleSources.push(maskedFullName);
|
||||
}
|
||||
|
||||
// 查找第一个有统计数据的匹配
|
||||
for (const source of possibleSources) {
|
||||
if (stats[source] && (stats[source].success > 0 || stats[source].failure > 0)) {
|
||||
@@ -3144,6 +3420,12 @@ class CLIProxyManager {
|
||||
case 'gemini':
|
||||
typeDisplayKey = 'auth_files.type_gemini';
|
||||
break;
|
||||
case 'gemini-cli':
|
||||
typeDisplayKey = 'auth_files.type_gemini-cli';
|
||||
break;
|
||||
case 'aistudio':
|
||||
typeDisplayKey = 'auth_files.type_aistudio';
|
||||
break;
|
||||
case 'claude':
|
||||
typeDisplayKey = 'auth_files.type_claude';
|
||||
break;
|
||||
@@ -3162,8 +3444,28 @@ class CLIProxyManager {
|
||||
}
|
||||
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
|
||||
|
||||
// 检查是否为 runtime-only 文件
|
||||
const isRuntimeOnly = file.runtime_only === true;
|
||||
|
||||
// 生成操作按钮 HTML,runtime-only 文件显示虚拟标记
|
||||
const actionsHtml = isRuntimeOnly ? `
|
||||
<div class="item-actions">
|
||||
<span class="virtual-auth-badge">虚拟认证文件</span>
|
||||
</div>` : `
|
||||
<div class="item-actions" data-filename="${file.name}">
|
||||
<button class="btn-small btn-info" data-action="showDetails" title="详细信息">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
<button class="btn-small btn-primary" data-action="download" title="下载">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button class="btn-small btn-danger" data-action="delete" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<div class="file-item" data-file-type="${fileType}">
|
||||
<div class="file-item" data-file-type="${fileType}" ${isRuntimeOnly ? 'data-runtime-only="true"' : ''}>
|
||||
<div class="item-content">
|
||||
<div class="item-title">${typeBadge}${file.name}</div>
|
||||
<div class="item-meta">
|
||||
@@ -3179,17 +3481,7 @@ class CLIProxyManager {
|
||||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${fileStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-actions" data-filename="${file.name}">
|
||||
<button class="btn-small btn-info" data-action="showDetails" title="详细信息">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
<button class="btn-small btn-primary" data-action="download" title="下载">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button class="btn-small btn-danger" data-action="delete" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
${actionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3213,6 +3505,8 @@ class CLIProxyManager {
|
||||
{ type: 'all', labelKey: 'auth_files.filter_all' },
|
||||
{ type: 'qwen', labelKey: 'auth_files.filter_qwen' },
|
||||
{ type: 'gemini', labelKey: 'auth_files.filter_gemini' },
|
||||
{ type: 'gemini-cli', labelKey: 'auth_files.filter_gemini-cli' },
|
||||
{ type: 'aistudio', labelKey: 'auth_files.filter_aistudio' },
|
||||
{ type: 'claude', labelKey: 'auth_files.filter_claude' },
|
||||
{ type: 'codex', labelKey: 'auth_files.filter_codex' },
|
||||
{ type: 'iflow', labelKey: 'auth_files.filter_iflow' },
|
||||
|
||||
8
i18n.js
8
i18n.js
@@ -225,6 +225,8 @@ const i18n = {
|
||||
'auth_files.filter_all': '全部',
|
||||
'auth_files.filter_qwen': 'Qwen',
|
||||
'auth_files.filter_gemini': 'Gemini',
|
||||
'auth_files.filter_gemini-cli': 'GeminiCLI',
|
||||
'auth_files.filter_aistudio': 'AIStudio',
|
||||
'auth_files.filter_claude': 'Claude',
|
||||
'auth_files.filter_codex': 'Codex',
|
||||
'auth_files.filter_iflow': 'iFlow',
|
||||
@@ -232,6 +234,8 @@ const i18n = {
|
||||
'auth_files.filter_unknown': '其他',
|
||||
'auth_files.type_qwen': 'Qwen',
|
||||
'auth_files.type_gemini': 'Gemini',
|
||||
'auth_files.type_gemini-cli': 'GeminiCLI',
|
||||
'auth_files.type_aistudio': 'AIStudio',
|
||||
'auth_files.type_claude': 'Claude',
|
||||
'auth_files.type_codex': 'Codex',
|
||||
'auth_files.type_iflow': 'iFlow',
|
||||
@@ -667,6 +671,8 @@ const i18n = {
|
||||
'auth_files.filter_all': 'All',
|
||||
'auth_files.filter_qwen': 'Qwen',
|
||||
'auth_files.filter_gemini': 'Gemini',
|
||||
'auth_files.filter_gemini-cli': 'GeminiCLI',
|
||||
'auth_files.filter_aistudio': 'AIStudio',
|
||||
'auth_files.filter_claude': 'Claude',
|
||||
'auth_files.filter_codex': 'Codex',
|
||||
'auth_files.filter_iflow': 'iFlow',
|
||||
@@ -674,6 +680,8 @@ const i18n = {
|
||||
'auth_files.filter_unknown': 'Other',
|
||||
'auth_files.type_qwen': 'Qwen',
|
||||
'auth_files.type_gemini': 'Gemini',
|
||||
'auth_files.type_gemini-cli': 'GeminiCLI',
|
||||
'auth_files.type_aistudio': 'AIStudio',
|
||||
'auth_files.type_claude': 'Claude',
|
||||
'auth_files.type_codex': 'Codex',
|
||||
'auth_files.type_iflow': 'iFlow',
|
||||
|
||||
@@ -427,6 +427,8 @@
|
||||
<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="gemini-cli" data-i18n-text="auth_files.filter_gemini-cli">GeminiCLI</button>
|
||||
<button class="filter-btn" data-type="aistudio" data-i18n-text="auth_files.filter_aistudio">AIStudio</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>
|
||||
|
||||
247
styles.css
247
styles.css
@@ -1683,6 +1683,27 @@ input:checked+.slider:before {
|
||||
border: 1px dashed #666666;
|
||||
}
|
||||
|
||||
/* 虚拟认证文件标记样式 */
|
||||
.virtual-auth-badge {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.25);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .virtual-auth-badge {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||||
box-shadow: 0 2px 4px rgba(96, 165, 250, 0.25);
|
||||
}
|
||||
|
||||
|
||||
.provider-list {
|
||||
/* 默认不限制高度,动态设置 */
|
||||
min-height: 0;
|
||||
@@ -1751,6 +1772,22 @@ input:checked+.slider:before {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 认证文件项的底部布局优化 - 确保状态标签和操作按钮垂直对齐 */
|
||||
.file-item .item-footer {
|
||||
align-items: center; /* 垂直居中对齐 */
|
||||
gap: 12px; /* 增加状态标签和按钮之间的间距 */
|
||||
}
|
||||
|
||||
.file-item .item-stats {
|
||||
align-items: center; /* 状态徽章垂直居中 */
|
||||
margin: 0; /* 移除默认外边距 */
|
||||
}
|
||||
|
||||
.file-item .item-actions {
|
||||
align-items: center; /* 操作按钮垂直居中 */
|
||||
gap: 6px; /* 按钮之间适当间距 */
|
||||
}
|
||||
|
||||
/* API Keys 和 AI Providers 的按钮组 - 绝对定位到右侧垂直居中 */
|
||||
.key-item .item-actions,
|
||||
.provider-item .item-actions {
|
||||
@@ -3280,9 +3317,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);
|
||||
@@ -3291,6 +3329,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;
|
||||
@@ -3424,10 +3589,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提供商统计徽章样式 ===== */
|
||||
|
||||
/* 统计信息容器 */
|
||||
@@ -3435,6 +3679,7 @@ input:checked+.slider:before {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 统计徽章基础样式 */
|
||||
|
||||
Reference in New Issue
Block a user