-
0) {
- // 确保 DOM 已经准备好
- this.$nextTick(() => {
- // 合并到本地缓存并按时间排序
- const newLogs = newVal.filter(log => log.time > this.lastProcessedTime);
-
- if (newLogs.length > 0) {
- this.localLogCache.push(...newLogs);
- // 按时间戳排序
- this.localLogCache.sort((a, b) => a.time - b.time);
-
- // 只保留最新的 log_cache_max_len 条
- if (this.localLogCache.length > this.commonStore.log_cache_max_len) {
- this.localLogCache.splice(0, this.localLogCache.length - this.commonStore.log_cache_max_len);
- }
-
- // 显示新日志
- newLogs.forEach(logItem => {
- if (this.isLevelSelected(logItem.level)) {
- this.printLog(logItem.data);
- }
- });
-
- // 更新最后处理时间
- this.lastProcessedTime = Math.max(...newLogs.map(log => log.time));
- }
- });
- }
- },
- deep: true,
- immediate: false
- },
selectedLevels: {
handler() {
this.refreshDisplay();
@@ -113,30 +78,142 @@ export default {
}
},
async mounted() {
- // 请求历史日志
await this.fetchLogHistory();
-
- // 等待 DOM 准备好后,显示历史日志
- this.$nextTick(() => {
- if (this.localLogCache.length > 0) {
- this.localLogCache.forEach(logItem => {
- if (this.isLevelSelected(logItem.level)) {
- this.printLog(logItem.data);
- }
- });
- // 更新最后处理时间
- this.lastProcessedTime = Math.max(...this.localLogCache.map(log => log.time));
- }
- });
+ this.connectSSE();
+ },
+ beforeUnmount() {
+ if (this.eventSource) {
+ this.eventSource.close();
+ this.eventSource = null;
+ }
+ if (this.retryTimer) {
+ clearTimeout(this.retryTimer);
+ this.retryTimer = null;
+ }
+ this.retryAttempts = 0;
},
methods: {
+ connectSSE() {
+ if (this.eventSource) {
+ this.eventSource.close();
+ this.eventSource = null;
+ }
+
+ console.log(`正在连接日志流... (尝试次数: ${this.retryAttempts})`);
+
+ const token = localStorage.getItem('token');
+
+ this.eventSource = new EventSourcePolyfill('/api/live-log', {
+ headers: {
+ 'Authorization': token ? `Bearer ${token}` : ''
+ },
+ heartbeatTimeout: 300000,
+ withCredentials: true
+ });
+
+ this.eventSource.onopen = () => {
+ console.log('日志流连接成功!');
+ this.retryAttempts = 0;
+
+ if (!this.lastEventId) {
+ this.fetchLogHistory();
+ }
+ };
+
+ this.eventSource.onmessage = (event) => {
+ try {
+ if (event.lastEventId) {
+ this.lastEventId = event.lastEventId;
+ }
+
+ const payload = JSON.parse(event.data);
+ this.processNewLogs([payload]);
+ } catch (e) {
+ console.error('解析日志失败:', e);
+ }
+ };
+
+ this.eventSource.onerror = (err) => {
+
+ if (err.status === 401) {
+ console.error('鉴权失败 (401),可能是 Token 过期了。');
+
+ } else {
+ console.warn('日志流连接错误:', err);
+ }
+
+ if (this.eventSource) {
+ this.eventSource.close();
+ this.eventSource = null;
+ }
+
+ if (this.retryAttempts >= this.maxRetryAttempts) {
+ console.error('❌ 已达到最大重试次数,停止重连。请刷新页面重试。');
+ return;
+ }
+
+ const delay = Math.min(
+ this.baseRetryDelay * Math.pow(2, this.retryAttempts),
+ 30000
+ );
+
+ console.log(`⏳ ${delay}ms 后尝试第 ${this.retryAttempts + 1} 次重连...`);
+
+ if (this.retryTimer) {
+ clearTimeout(this.retryTimer);
+ this.retryTimer = null;
+ }
+
+ this.retryTimer = setTimeout(async () => {
+ this.retryAttempts++;
+
+ if (!this.lastEventId) {
+ await this.fetchLogHistory();
+ }
+
+ this.connectSSE();
+ }, delay);
+ };
+ },
+
+ processNewLogs(newLogs) {
+ if (!newLogs || newLogs.length === 0) return;
+
+ let hasUpdate = false;
+
+ newLogs.forEach(log => {
+
+ const exists = this.localLogCache.some(existing =>
+ existing.time === log.time &&
+ existing.data === log.data &&
+ existing.level === log.level
+ );
+
+ if (!exists) {
+ this.localLogCache.push(log);
+ hasUpdate = true;
+
+ if (this.isLevelSelected(log.level)) {
+ this.printLog(log.data);
+ }
+ }
+ });
+
+ if (hasUpdate) {
+ this.localLogCache.sort((a, b) => a.time - b.time);
+
+ const maxSize = this.commonStore.log_cache_max_len || 200;
+ if (this.localLogCache.length > maxSize) {
+ this.localLogCache.splice(0, this.localLogCache.length - maxSize);
+ }
+ }
+ },
+
async fetchLogHistory() {
try {
const res = await axios.get('/api/log-history');
if (res.data.data.logs && res.data.data.logs.length > 0) {
- this.localLogCache = [...res.data.data.logs];
- // 按时间戳排序
- this.localLogCache.sort((a, b) => a.time - b.time);
+ this.processNewLogs(res.data.data.logs);
}
} catch (err) {
console.error('Failed to fetch log history:', err);
@@ -162,7 +239,6 @@ export default {
if (termElement) {
termElement.innerHTML = '';
- // 重新显示所有符合筛选条件的日志
if (this.localLogCache && this.localLogCache.length > 0) {
this.localLogCache.forEach(logItem => {
if (this.isLevelSelected(logItem.level)) {
@@ -173,16 +249,13 @@ export default {
}
},
-
toggleAutoScroll() {
this.autoScroll = !this.autoScroll;
},
printLog(log) {
- // append 一个 span 标签到 term,block 的方式
let ele = document.getElementById('term')
if (!ele) {
- console.warn('term element not found, skipping log print');
return;
}
@@ -196,11 +269,11 @@ export default {
}
}
- span.style = style + 'display: block; font-size: 12px; font-family: Consolas, monospace; white-space: pre-wrap;'
+ span.style = style + 'display: block; font-size: 12px; font-family: Consolas, monospace; white-space: pre-wrap; margin-bottom: 2px;'
span.classList.add('fade-in')
span.innerText = `${log}`;
ele.appendChild(span)
- if (this.autoScroll ) {
+ if (this.autoScroll) {
ele.scrollTop = ele.scrollHeight
}
}
@@ -230,4 +303,4 @@ export default {
opacity: 1;
}
}
-
\ No newline at end of file
+
diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js
index b8e905216..337d1ba79 100644
--- a/dashboard/src/stores/common.js
+++ b/dashboard/src/stores/common.js
@@ -21,10 +21,14 @@ export const useCommonStore = defineStore({
}
const controller = new AbortController();
const { signal } = controller;
+
+ // 注意:这里如果之前改过 Polyfill 的话,可能需要保持原样
+ // 如果是用 fetch 的话,这里是支持 Authorization Header 的
const headers = {
'Content-Type': 'multipart/form-data',
'Authorization': 'Bearer ' + localStorage.getItem('token')
};
+
fetch('/api/live-log', {
method: 'GET',
headers,
@@ -72,10 +76,20 @@ export const useCommonStore = defineStore({
try {
const logObject = JSON.parse(logLine);
- // give a uuid if not exists
+
+ // 修复:兼容 HTTP 环境的 UUID 生成
if (!logObject.uuid) {
- logObject.uuid = crypto.randomUUID();
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ logObject.uuid = crypto.randomUUID();
+ } else {
+ // 手动生成 UUID v4
+ logObject.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
}
+
this.log_cache.push(logObject);
// Limit log cache size
if (this.log_cache.length > this.log_cache_max_len) {
@@ -93,7 +107,13 @@ export const useCommonStore = defineStore({
}).catch(error => {
console.error('SSE error:', error);
// Attempt to reconnect after a delay
- this.log_cache.push('SSE Connection failed, retrying in 5 seconds...');
+ this.log_cache.push({
+ type: 'log',
+ level: 'ERROR',
+ time: Date.now() / 1000,
+ data: 'SSE Connection failed, retrying in 5 seconds...',
+ uuid: 'error-' + Date.now()
+ });
setTimeout(() => {
this.eventSource = null;
this.createEventSource();