diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 787e510a5..971679a0e 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -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"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 0bec68760..e3c3af713 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 index e062f2682..b81e4462d 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/stores/api.ts b/dashboard/src/stores/api.ts index 4a054d13d..27230a30c 100644 --- a/dashboard/src/stores/api.ts +++ b/dashboard/src/stores/api.ts @@ -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") || "", diff --git a/dashboard/src/stores/auth.ts b/dashboard/src/stores/auth.ts index 3553e5270..6dfc2b2e5 100644 --- a/dashboard/src/stores/auth.ts +++ b/dashboard/src/stores/auth.ts @@ -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, diff --git a/dashboard/src/stores/common.ts b/dashboard/src/stores/common.ts new file mode 100644 index 000000000..9d1322533 --- /dev/null +++ b/dashboard/src/stores/common.ts @@ -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); + }); + }, + } +}); diff --git a/dashboard/src/stores/customizer.ts b/dashboard/src/stores/customizer.ts index b0674e76f..18f14056d 100644 --- a/dashboard/src/stores/customizer.ts +++ b/dashboard/src/stores/customizer.ts @@ -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, diff --git a/dashboard/src/stores/personaStore.ts b/dashboard/src/stores/personaStore.ts index 4f162886f..ffd600aa8 100644 --- a/dashboard/src/stores/personaStore.ts +++ b/dashboard/src/stores/personaStore.ts @@ -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,