astrbbbb / dashboard /src /stores /common.js
qa1145's picture
Upload 1245 files
8ede856 verified
import { defineStore } from 'pinia';
import axios from 'axios';
export const useCommonStore = defineStore({
id: 'common',
state: () => ({
// @ts-ignore
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(() => {});
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 : "",
"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);
});
},
}
});