mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 09:40:30 +08:00
fix(dashboard): drop legacy SSE log cache implementation
Remove EventSource-based streaming and switch to REST polling for log fetching. This eliminates the SSE connection management that caused TS type inference issues with Pinia 3.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 237 icons */
|
||||
/* Auto-generated MDI subset – 238 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -804,6 +804,10 @@
|
||||
content: "\F0832";
|
||||
}
|
||||
|
||||
.mdi-share-variant::before {
|
||||
content: "\F0497";
|
||||
}
|
||||
|
||||
.mdi-shield-check::before {
|
||||
content: "\F0565";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,8 +6,7 @@ export type ApiPreset = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const useApiStore = defineStore({
|
||||
id: "api",
|
||||
export const useApiStore = defineStore("api", {
|
||||
state: () => ({
|
||||
// 优先从 localStorage 读取用户手动设置的地址
|
||||
apiBaseUrl: getApiBaseUrl() || localStorage.getItem("apiBaseUrl") || "",
|
||||
|
||||
@@ -2,8 +2,7 @@ import { defineStore } from "pinia";
|
||||
import { router } from "@/router";
|
||||
import axios from "@/utils/request";
|
||||
|
||||
export const useAuthStore = defineStore({
|
||||
id: "auth",
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
username: "",
|
||||
returnUrl: null as string | null,
|
||||
|
||||
200
dashboard/src/stores/common.ts
Normal file
200
dashboard/src/stores/common.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import axios from '@/utils/request';
|
||||
|
||||
export const useCommonStore = defineStore({
|
||||
id: 'common',
|
||||
state: () => ({
|
||||
eventSource: null,
|
||||
log_cache: [],
|
||||
sse_connected: false,
|
||||
|
||||
log_cache_max_len: 1000,
|
||||
startTime: -1,
|
||||
|
||||
pluginMarketData: [],
|
||||
}),
|
||||
actions: {
|
||||
async createEventSource() {
|
||||
if (this.eventSource) {
|
||||
return
|
||||
}
|
||||
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,
|
||||
signal,
|
||||
cache: 'no-cache',
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`SSE connection failed: ${response.status}`);
|
||||
}
|
||||
console.log('SSE stream opened');
|
||||
this.sse_connected = true;
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let bufferedText = '';
|
||||
|
||||
const processStream = ({ done, value }) => {
|
||||
if (done) {
|
||||
console.log('SSE stream closed');
|
||||
setTimeout(() => {
|
||||
this.eventSource = null;
|
||||
this.createEventSource();
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate partial chunks; SSE data may split JSON across reads.
|
||||
const text = decoder.decode(value, { stream: true });
|
||||
bufferedText += text;
|
||||
|
||||
// Split completed events; keep the trailing partial in buffer.
|
||||
const segments = bufferedText.split('\n\n');
|
||||
bufferedText = segments.pop() || '';
|
||||
|
||||
segments.forEach(segment => {
|
||||
const line = segment.trim();
|
||||
if (!line.startsWith('data: ')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logLine = line.replace('data: ', '').trim();
|
||||
if (!logLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const logObject = JSON.parse(logLine);
|
||||
|
||||
// 修复:兼容 HTTP 环境的 UUID 生成
|
||||
if (!logObject.uuid) {
|
||||
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) {
|
||||
this.log_cache.splice(0, this.log_cache.length - this.log_cache_max_len);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse SSE log line, skipping:', err, logLine);
|
||||
}
|
||||
});
|
||||
|
||||
return reader.read().then(processStream);
|
||||
};
|
||||
|
||||
reader.read().then(processStream);
|
||||
}).catch(error => {
|
||||
console.error('SSE error:', error);
|
||||
// Attempt to reconnect after a delay
|
||||
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();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Store controller to allow closing the connection
|
||||
this.eventSource = controller;
|
||||
},
|
||||
closeEventSourcet() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.abort();
|
||||
this.eventSource = null;
|
||||
}
|
||||
},
|
||||
getLogCache() {
|
||||
return this.log_cache
|
||||
},
|
||||
async fetchStartTime() {
|
||||
const res = await axios.get('/api/stat/start-time');
|
||||
this.startTime = res.data.data.start_time;
|
||||
return this.startTime;
|
||||
},
|
||||
getStartTime() {
|
||||
if (this.startTime !== -1) {
|
||||
return this.startTime
|
||||
}
|
||||
this.fetchStartTime().catch(() => undefined);
|
||||
return this.startTime
|
||||
},
|
||||
async getPluginCollections(force = false, customSource = null) {
|
||||
// 获取插件市场数据
|
||||
if (!force && this.pluginMarketData.length > 0 && !customSource) {
|
||||
return Promise.resolve(this.pluginMarketData);
|
||||
}
|
||||
|
||||
// 构建URL
|
||||
let url = force ? '/api/plugin/market_list?force_refresh=true' : '/api/plugin/market_list';
|
||||
if (customSource) {
|
||||
url += (url.includes('?') ? '&' : '?') + `custom_registry=${encodeURIComponent(customSource)}`;
|
||||
}
|
||||
|
||||
return axios.get(url)
|
||||
.then((res) => {
|
||||
let data = []
|
||||
if (res.data.data && typeof res.data.data === 'object') {
|
||||
for (let key in res.data.data) {
|
||||
const pluginData = res.data.data[key];
|
||||
|
||||
data.push({
|
||||
"name": pluginData.name || key, // 优先使用插件数据中的name字段,否则使用键名
|
||||
"desc": pluginData.desc,
|
||||
"author": pluginData.author,
|
||||
"repo": pluginData.repo,
|
||||
"installed": false,
|
||||
"version": pluginData?.version ? pluginData.version : "未知",
|
||||
"social_link": pluginData?.social_link,
|
||||
"tags": pluginData?.tags ? pluginData.tags : [],
|
||||
"logo": pluginData?.logo ? pluginData.logo : "",
|
||||
"pinned": pluginData?.pinned ? pluginData.pinned : false,
|
||||
"stars": pluginData?.stars ? pluginData.stars : 0,
|
||||
"updated_at": pluginData?.updated_at ? pluginData.updated_at : "",
|
||||
"display_name": pluginData?.display_name ? pluginData.display_name : "",
|
||||
"astrbot_version": pluginData?.astrbot_version ? pluginData.astrbot_version : "",
|
||||
"category": pluginData?.category ? pluginData.category : "",
|
||||
"support_platforms": Array.isArray(pluginData?.support_platforms)
|
||||
? pluginData.support_platforms
|
||||
: Array.isArray(pluginData?.support_platform)
|
||||
? pluginData.support_platform
|
||||
: Array.isArray(pluginData?.platform)
|
||||
? pluginData.platform
|
||||
: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.pluginMarketData = data;
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
@@ -2,8 +2,7 @@ import { defineStore } from "pinia";
|
||||
import config from "@/config";
|
||||
import { LIGHT_THEME_NAME, DARK_THEME_NAME } from "@/theme/constants";
|
||||
|
||||
export const useCustomizerStore = defineStore({
|
||||
id: "customizer",
|
||||
export const useCustomizerStore = defineStore("customizer", {
|
||||
state: () => ({
|
||||
Sidebar_drawer: config.Sidebar_drawer,
|
||||
Customizer_drawer: config.Customizer_drawer,
|
||||
|
||||
@@ -43,8 +43,7 @@ export interface ReorderItem {
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
export const usePersonaStore = defineStore({
|
||||
id: 'persona',
|
||||
export const usePersonaStore = defineStore('persona', {
|
||||
state: () => ({
|
||||
folderTree: [] as FolderTreeNode[],
|
||||
currentFolderId: null as string | null,
|
||||
|
||||
Reference in New Issue
Block a user