Download dashboard/src/components/shared/ConsoleDisplayer.vue from qa1145/astrbbbb: direct link, hf CLI and curl.
- Browser
- Download file 7.96 kB
-
https://huggingface.co/spaces/qa1145/astrbbbb/resolve/main/dashboard/src/components/shared/ConsoleDisplayer.vue
- Command line
-
hf download hf://spaces/qa1145/astrbbbb/dashboard/src/components/shared/ConsoleDisplayer.vue
-
curl -L -o ConsoleDisplayer.vue https://huggingface.co/spaces/qa1145/astrbbbb/resolve/main/dashboard/src/components/shared/ConsoleDisplayer.vue
7.96 kB
| <script setup> | |
| import { useCommonStore } from '@/stores/common'; | |
| import axios from 'axios'; | |
| import { EventSourcePolyfill } from 'event-source-polyfill'; | |
| </script> | |
| <template> | |
| <div> | |
| <div class="filter-controls mb-2" v-if="showLevelBtns"> | |
| <v-chip-group v-model="selectedLevels" column multiple> | |
| <v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small" | |
| :text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'" class="font-weight-medium"> | |
| {{ level }} | |
| </v-chip> | |
| </v-chip-group> | |
| </div> | |
| <div id="term" style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%"> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| export default { | |
| name: 'ConsoleDisplayer', | |
| data() { | |
| return { | |
| autoScroll: true, | |
| logColorAnsiMap: { | |
| '\u001b[1;34m': 'color: #39C5BB; font-weight: bold;', | |
| '\u001b[1;36m': 'color: #00FFFF; font-weight: bold;', | |
| '\u001b[1;33m': 'color: #FFFF00; font-weight: bold;', | |
| '\u001b[31m': 'color: #FF0000;', | |
| '\u001b[1;31m': 'color: #FF0000; font-weight: bold;', | |
| '\u001b[0m': 'color: inherit; font-weight: normal;', | |
| '\u001b[32m': 'color: #00FF00;', | |
| 'default': 'color: #FFFFFF;' | |
| }, | |
| logLevels: ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], | |
| selectedLevels: [0, 1, 2, 3, 4], | |
| levelColors: { | |
| 'DEBUG': 'grey', | |
| 'INFO': 'blue-lighten-3', | |
| 'WARNING': 'amber', | |
| 'ERROR': 'red', | |
| 'CRITICAL': 'purple' | |
| }, | |
| localLogCache: [], | |
| eventSource: null, | |
| retryTimer: null, | |
| retryAttempts: 0, | |
| maxRetryAttempts: 10, | |
| baseRetryDelay: 1000, | |
| lastEventId: null, | |
| } | |
| }, | |
| computed: { | |
| commonStore() { | |
| return useCommonStore(); | |
| }, | |
| }, | |
| props: { | |
| historyNum: { | |
| type: String, | |
| default: "-1" | |
| }, | |
| showLevelBtns: { | |
| type: Boolean, | |
| default: true | |
| } | |
| }, | |
| watch: { | |
| selectedLevels: { | |
| handler() { | |
| this.refreshDisplay(); | |
| }, | |
| deep: true | |
| } | |
| }, | |
| async mounted() { | |
| await this.fetchLogHistory(); | |
| 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.processNewLogs(res.data.data.logs); | |
| } | |
| } catch (err) { | |
| console.error('Failed to fetch log history:', err); | |
| } | |
| }, | |
| getLevelColor(level) { | |
| return this.levelColors[level] || 'grey'; | |
| }, | |
| isLevelSelected(level) { | |
| for (let i = 0; i < this.selectedLevels.length; ++i) { | |
| let level_ = this.logLevels[this.selectedLevels[i]] | |
| if (level_ === level) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }, | |
| refreshDisplay() { | |
| const termElement = document.getElementById('term'); | |
| if (termElement) { | |
| termElement.innerHTML = ''; | |
| if (this.localLogCache && this.localLogCache.length > 0) { | |
| this.localLogCache.forEach(logItem => { | |
| if (this.isLevelSelected(logItem.level)) { | |
| this.printLog(logItem.data); | |
| } | |
| }); | |
| } | |
| } | |
| }, | |
| toggleAutoScroll() { | |
| this.autoScroll = !this.autoScroll; | |
| }, | |
| printLog(log) { | |
| let ele = document.getElementById('term') | |
| if (!ele) { | |
| return; | |
| } | |
| let span = document.createElement('pre') | |
| let style = this.logColorAnsiMap['default'] | |
| for (let key in this.logColorAnsiMap) { | |
| if (log.startsWith(key)) { | |
| style = this.logColorAnsiMap[key] | |
| log = log.replace(key, '').replace('\u001b[0m', '') | |
| break | |
| } | |
| } | |
| span.style = style | |
| span.classList.add('console-log-line', 'fade-in') | |
| span.innerText = `${log}`; | |
| ele.appendChild(span) | |
| if (this.autoScroll) { | |
| ele.scrollTop = ele.scrollHeight | |
| } | |
| } | |
| }, | |
| } | |
| </script> | |
| <style scoped> | |
| .filter-controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| margin-left: 20px; | |
| } | |
| :deep(.console-log-line) { | |
| display: block; | |
| margin-bottom: 2px; | |
| font-family: SFMono-Regular, Menlo, Monaco, Consolas, var(--astrbot-font-cjk-mono), monospace; | |
| font-size: 12px; | |
| white-space: pre-wrap; | |
| } | |
| :deep(.fade-in) { | |
| animation: fadeIn 0.3s; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| } | |
| to { | |
| opacity: 1; | |
| } | |
| } | |
| </style> | |