From d5c9e663daa647aa9981b019f4133d6b263a6b52 Mon Sep 17 00:00:00 2001 From: Megghy Date: Sun, 6 Apr 2025 13:50:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=A2=E6=88=B7=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 593015 -> 597100 bytes default.d.ts | 3 +- package.json | 10 + src/App.vue | 340 ++--- src/api/account.ts | 26 +- src/client/ClientFetcher.vue | 1113 +++++++++++++++++ src/client/ClientIndex.vue | 143 +++ src/client/ClientLayout.vue | 434 +++++++ src/client/ClientSettings.vue | 320 +++++ src/client/ClientTest.vue | 31 + src/client/WindowBar.vue | 154 +++ src/client/data/biliLogin.ts | 92 ++ src/client/data/info.ts | 236 ++++ src/client/data/initialize.ts | 243 ++++ src/client/data/models.ts | 282 +++++ src/client/data/notification.ts | 27 + src/client/data/utils.ts | 77 ++ src/client/store/useBiliCookie.ts | 581 +++++++++ src/client/store/useSettings.ts | 45 + src/client/store/useTauriStore.ts | 53 + src/components.d.ts | 19 + src/components/TempComponent.vue | 1 + src/data/DanmakuClients/BaseDanmakuClient.ts | 2 + src/data/DanmakuClients/DirectClient.ts | 1 + src/data/DanmakuClients/OpenLiveClient.ts | 1 + src/main.ts | 7 +- src/router/client.ts | 38 + src/router/index.ts | 2 + src/store/useWebFetcher.ts | 576 +++++---- src/views/client/ClientLayout.vue | 30 - src/views/obs/WebFetcherOBS.vue | 11 +- .../TraditionalSongListTemplate.vue | 7 +- 32 files changed, 4462 insertions(+), 443 deletions(-) create mode 100644 src/client/ClientFetcher.vue create mode 100644 src/client/ClientIndex.vue create mode 100644 src/client/ClientLayout.vue create mode 100644 src/client/ClientSettings.vue create mode 100644 src/client/ClientTest.vue create mode 100644 src/client/WindowBar.vue create mode 100644 src/client/data/biliLogin.ts create mode 100644 src/client/data/info.ts create mode 100644 src/client/data/initialize.ts create mode 100644 src/client/data/models.ts create mode 100644 src/client/data/notification.ts create mode 100644 src/client/data/utils.ts create mode 100644 src/client/store/useBiliCookie.ts create mode 100644 src/client/store/useSettings.ts create mode 100644 src/client/store/useTauriStore.ts create mode 100644 src/router/client.ts delete mode 100644 src/views/client/ClientLayout.vue diff --git a/bun.lockb b/bun.lockb index 7e5b2546a1a620db34ae699c469ad9232587fc3d..57090dac7f7b0fde7352645e5a60d93770b0615c 100644 GIT binary patch delta 115750 zcmeFa33OD&y8gYpp&^?YM1erSFbQe^Cps8t91&3*5it;;NdgIEA_J3Rf;fPP(^fY` zgWv=xAPy+bI2}dB={ONZ1qVRISrHZee$TGznB(Q1|GEF~u5YbxEqd8`>UnCedh4y) z)ths6FY5l)`tH~E>3!u*5A5nUbMK~yn?82fBayqE(GT=E%&F|~v48cT7u&rys`CdO zT}PjLt~^N<~T(AXLURIDlfu!r<2f#mSWb_3|hVOza z{UAzI`e_xp71^W*RfCh=#KiXmn-c#l*cv>-Vl`qL$EmDp(abi$Vs24+etCXw*^eYp z!+Pp8?h3l?<5LQ)BTHMo0Ok=T0bz zXHRh+JkcrB?_VFsy% z@yXMI5#9_>hCdG~-$S4VqM|fEZE~68lvEVW%r4H!?Tt=fs=83V8nTPgQ$zCNWoZ)& z;*++*n>m&Alz)xy|94(mmbGi0uNE?p@MXYA8qd zG{!vyq|B=CK~>xjRL^D{VhU&uCvVl)JsgKlR;{)bKitc7pjU6lAz1YkII91oK?b|(;6t550^?pv2DNY@s8vV#0Y4P3mbOpxsvBafb_z54n3;GHRJL|~ zO?$7!HpMRon}CT$nOGD5&0?%dEY`%DNi5>Ot+j4-i|qp{)S|nPiewU0l$MiQm|xP+ zn-J-l8AsLxl}(#eEStUm7}Ks5pxQO4znRbp#l-~^;$^ux#f25+`2}T1p)39mP^S8= zMb>88^t^cabt&GqNT13xDPHqEEx$Z)Iupx`<&+i|6jYQr&c=aeI#z=kW)IXfPqTU! z$l|CP3}V|=9l`G4ZyAnrF!(Mg{s^e}n?Ox_IfzMCR-H#c^WWcMdr%2~Kh{*Z1yuag zmfr<-geS%~;fr$1CRF4X?@!~ zWId>iw}A(NS6e$Dj_1`5z~l{9l4f_YSCfo&nox$*!^q7D<3# z4yu5q1B~St&nU<*DtDZB&M@)Mfr?)RYGvF6s)3cD8a5eJ1?PjRa0n;^>)+5!)gujz z1yxk!=TN`nq!OVFADv@NCHGuoQYG>7yjRk=>^- zKfAQJ$9aYq=2Vmxkgp?LCKxZBR1(Jz-bea2@FgQn{_96NgM%e`8xgYc%fU|I6X%

s!N_|Zn6n3G?seijxK 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 Ed3piZ-iizg;6tteiY`^-snL@p^YREp4P@djH^ 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 delta 115103 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 JZ!ys8e*j|itfv3~ diff --git a/default.d.ts b/default.d.ts index 6f315f7..07b6bbc 100644 --- a/default.d.ts +++ b/default.d.ts @@ -1,5 +1,5 @@ -import type { LoadingBarProviderInst, MessageProviderInst, ModalProviderInst } from 'naive-ui' +import type { LoadingBarProviderInst, MessageProviderInst, ModalProviderInst, NotificationProviderInst } from 'naive-ui' import type { useRoute } from 'vue-router' declare module 'vue3-aplayer' { @@ -22,5 +22,6 @@ declare global { $route: ReturnType $modal: ModalProviderInst $mitt: Emitter + $notification: NotificationProviderInst } } diff --git a/package.json b/package.json index ebd26a1..b00c26f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,16 @@ "@microsoft/signalr-protocol-msgpack": "^8.0.7", "@mixer/postmessage-rpc": "^1.1.4", "@tauri-apps/api": "^2.4.0", + "@tauri-apps/plugin-autostart": "^2.3.0", "@tauri-apps/plugin-http": "^2.4.2", + "@tauri-apps/plugin-log": "^2.3.1", + "@tauri-apps/plugin-notification": "^2.2.2", + "@tauri-apps/plugin-opener": "^2.2.6", + "@tauri-apps/plugin-os": "^2.2.1", + "@tauri-apps/plugin-process": "^2.2.1", + "@tauri-apps/plugin-store": "^2.2.0", + "@tauri-apps/plugin-updater": "^2.7.0", + "@types/crypto-js": "^4.2.2", "@typescript-eslint/eslint-plugin": "^8.27.0", "@vicons/fluent": "^0.13.0", "@vitejs/plugin-basic-ssl": "^2.0.0", @@ -28,6 +37,7 @@ "@wangeditor/editor-for-vue": "^5.1.12", "bilibili-live-ws": "^6.3.1", "brotli-compress": "^1.3.3", + "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "easy-speech": "^2.4.0", "echarts": "^5.6.0", diff --git a/src/App.vue b/src/App.vue index d5c25f5..f9c0111 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,75 +1,80 @@ diff --git a/src/api/account.ts b/src/api/account.ts index 2a6f74f..9da94d0 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -5,19 +5,20 @@ import { isSameDay } from 'date-fns' import { createDiscreteApi } from 'naive-ui' import { ref } from 'vue' import { APIRoot, AccountInfo, FunctionTypes } from './api-models' -import { useRoute } from 'vue-router' export const ACCOUNT = ref({} as AccountInfo) export const isLoadingAccount = ref(true) -const route = useRoute() +export const isLoggedIn = computed(() => { + return ACCOUNT.value.id > 0 +}) const { message } = createDiscreteApi(['message']) const cookie = useLocalStorage('JWT_Token', '') -const cookieRefreshDate = useLocalStorage('JWT_Token_Last_Refresh', Date.now()) +const cookieRefreshDate = useLocalStorage('JWT_Token_Last_Refresh', 0) -export async function GetSelfAccount() { - if (cookie.value) { - const result = await Self() +export async function GetSelfAccount(token?: string) { + if (cookie.value || token) { + const result = await Self(token) if (result.code == 200) { if (!ACCOUNT.value.id) { ACCOUNT.value = result.data @@ -28,7 +29,7 @@ export async function GetSelfAccount() { isLoadingAccount.value = false //console.log('[vtsuru] 已获取账户信息') if (!isSameDay(new Date(), cookieRefreshDate.value)) { - refreshCookie() + refreshCookie(token) } return result.data } else if (result.code == 401) { @@ -45,16 +46,17 @@ export async function GetSelfAccount() { } isLoadingAccount.value = false } + export function UpdateAccountLoop() { setInterval(() => { - if (ACCOUNT.value && route?.name != 'question-display') { + if (ACCOUNT.value && window.$route?.name != 'question-display') { // 防止在问题详情页刷新 GetSelfAccount() } }, 60 * 1000) } -function refreshCookie() { - QueryPostAPI(`${ACCOUNT_API_URL}refresh-token`).then((data) => { +function refreshCookie(token?: string) { + QueryPostAPIWithParams(`${ACCOUNT_API_URL}refresh-token`, { token }).then((data) => { if (data.code == 200) { cookie.value = data.data cookieRefreshDate.value = Date.now() @@ -155,8 +157,8 @@ export async function Login( password }) } -export async function Self(): Promise> { - return QueryPostAPI(`${ACCOUNT_API_URL}self`) +export async function Self(token?: string): Promise> { + return QueryPostAPIWithParams(`${ACCOUNT_API_URL}self`, token ? { token } : undefined) } export async function AddBiliBlackList( id: number, diff --git a/src/client/ClientFetcher.vue b/src/client/ClientFetcher.vue new file mode 100644 index 0000000..5465b82 --- /dev/null +++ b/src/client/ClientFetcher.vue @@ -0,0 +1,1113 @@ + + + \ No newline at end of file diff --git a/src/client/ClientIndex.vue b/src/client/ClientIndex.vue new file mode 100644 index 0000000..449a21f --- /dev/null +++ b/src/client/ClientIndex.vue @@ -0,0 +1,143 @@ + + + \ No newline at end of file diff --git a/src/client/ClientLayout.vue b/src/client/ClientLayout.vue new file mode 100644 index 0000000..ed24728 --- /dev/null +++ b/src/client/ClientLayout.vue @@ -0,0 +1,434 @@ + + + + + \ No newline at end of file diff --git a/src/client/ClientSettings.vue b/src/client/ClientSettings.vue new file mode 100644 index 0000000..c5dc5df --- /dev/null +++ b/src/client/ClientSettings.vue @@ -0,0 +1,320 @@ + + + + + \ No newline at end of file diff --git a/src/client/ClientTest.vue b/src/client/ClientTest.vue new file mode 100644 index 0000000..7da7315 --- /dev/null +++ b/src/client/ClientTest.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/src/client/WindowBar.vue b/src/client/WindowBar.vue new file mode 100644 index 0000000..ee25b7b --- /dev/null +++ b/src/client/WindowBar.vue @@ -0,0 +1,154 @@ + + + + + \ No newline at end of file diff --git a/src/client/data/biliLogin.ts b/src/client/data/biliLogin.ts new file mode 100644 index 0000000..f88dad1 --- /dev/null +++ b/src/client/data/biliLogin.ts @@ -0,0 +1,92 @@ +import { fetch } from '@tauri-apps/plugin-http'; +import { error } from '@tauri-apps/plugin-log' +import { QueryBiliAPI } from './utils'; + +export async function checkLoginStatusAsync(): Promise { + const url = 'https://api.bilibili.com/x/web-interface/nav/stat'; + const response = await fetch(url); + const json = await response.json(); + + return json.code === 0; +} + +export async function getUidAsync(): Promise { + const url = 'https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info'; + const response = await fetch(url); + const json = await response.json(); + + if (json.data && json.data.uid) { + return json.data.uid; + } + + return 0; +} +// 二维码地址及扫码密钥 +export async function getLoginUrlAsync(): Promise { + const url = 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate'; + const response = await QueryBiliAPI(url, 'GET') + if (!response.ok) { + const result = await response.text(); + error('无法获取B站登陆二维码: ' + result); + throw new Error('获取二维码地址失败'); + } + return await response.json(); +} + +export async function getLoginUrlDataAsync(): Promise<{ + url: string; + qrcode_key: string; +}> { + const message = await getLoginUrlAsync(); + if (message.code !== 0) { + throw new Error('获取二维码地址失败'); + } + return message.data as { + url: string; + qrcode_key: string; + }; +} +type QRCodeLoginInfo = + | { status: 'expired' } + | { status: 'unknown' } + | { status: 'scanned' } + | { status: 'waiting' } + | { status: 'confirmed'; cookie: string; refresh_token: string }; +export async function getLoginInfoAsync(qrcodeKey: string): Promise { + const url = `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${qrcodeKey}&source=main-fe-header`; + const response = await QueryBiliAPI(url); + const message = await response.json(); + + if (!message.data) { + throw new Error('获取登录信息失败'); + } + + if (message.data.code !== 0) { + switch (message.data.code) { + case 86038: + return { status: 'expired' }; + case 86090: + return { status: 'scanned' }; + case 86101: + return { status: 'waiting' }; + default: + return { status: 'unknown' }; + } + } + + const cookies = response.headers.get('set-cookie'); + if (!cookies) { + throw new Error('无法获取 Cookie'); + } + + return { status: 'confirmed', cookie: extractCookie(cookies), refresh_token: message.data.refresh_token }; +} + +function extractCookie(cookies: string): string { + const cookieArray = cookies + .split(',') + .map((cookie) => cookie.split(';')[0].trim()) + .filter(Boolean); + const cookieSet = new Set(cookieArray); + return Array.from(cookieSet).join('; '); +} diff --git a/src/client/data/info.ts b/src/client/data/info.ts new file mode 100644 index 0000000..3e1bd89 --- /dev/null +++ b/src/client/data/info.ts @@ -0,0 +1,236 @@ +import { ref } from 'vue'; +import { format } from 'date-fns'; +import { info, error } from '@tauri-apps/plugin-log'; +import { QueryBiliAPI } from './utils'; // 假设 Bili API 工具路径 +import { BiliRoomInfo, BiliStreamingInfo, FetcherStatisticData } from './models'; // 假设模型路径 +import { useTauriStore } from '../store/useTauriStore'; +// import { useAccount } from '@/api/account'; // 如果需要账户信息 + +// const accountInfo = useAccount(); // 如果需要 + +export const STATISTIC_STORE_KEY = 'webfetcher.statistics'; + +/** + * 当前日期 (YYYY-MM-DD) 的统计数据 (会被持久化) + */ +export const currentStatistic = ref(); +/** + * 标记当前统计数据是否已更新且需要保存 + */ +export const shouldUpdateStatistic = ref(false); + +/** + * 直播流信息 (从B站API获取) + */ +export const streamingInfo = ref({ + status: 'prepare', // 初始状态 +} as BiliStreamingInfo); + +/** + * 房间基本信息 (从B站API获取) + */ +export const roomInfo = ref(); // 可以添加房间信息 + +// --- Bili API 更新相关 --- +const updateCount = ref(0); // 用于控制API调用频率的计数器 + +/** + * 初始化统计和信息获取逻辑 + */ +export function initInfo() { + // 立即执行一次以加载或初始化当天数据 + updateCallback(); + // 设置定时器,定期检查和保存统计数据,并更新直播间信息 + setInterval(() => { + updateCallback(); + }, 5000); // 每 5 秒检查一次统计数据保存和更新直播信息 +} + +/** + * 定时回调函数: 处理统计数据持久化和B站信息更新 + */ +async function updateCallback() { + const store = useTauriStore(); + const currentDate = format(new Date(), 'yyyy-MM-dd'); + const key = `${STATISTIC_STORE_KEY}.${currentDate}`; + + // --- 统计数据管理 --- + // 检查是否需要加载或初始化当天的统计数据 + if (!currentStatistic.value || currentStatistic.value.date !== currentDate) { + const loadedData = await store.get(key); + if (loadedData && loadedData.date === currentDate) { + currentStatistic.value = loadedData; + // 确保 eventTypeCounts 存在 + if (!currentStatistic.value.eventTypeCounts) { + currentStatistic.value.eventTypeCounts = {}; + } + // info(`Loaded statistics for ${currentDate}`); // 日志保持不变 + } else { + info(`Initializing statistics for new day: ${currentDate}`); + currentStatistic.value = { + date: currentDate, + count: 0, + eventTypeCounts: {}, // 初始化类型计数 + }; + await store.set(key, currentStatistic.value); // 立即保存新一天的初始结构 + shouldUpdateStatistic.value = false; // 重置保存标记 + + // 清理旧数据逻辑 (保持不变) + const allKeys = (await store.store.keys()).filter((k) => k.startsWith(STATISTIC_STORE_KEY)); + if (allKeys.length > 30) { // 例如,只保留最近30天的数据 + allKeys.sort(); // 按日期字符串升序排序 + const oldestKey = allKeys[0]; + await store.store.delete(oldestKey); + info('清理过期统计数据: ' + oldestKey); + } + } + } + + // 如果数据有更新,则保存 + if (shouldUpdateStatistic.value && currentStatistic.value) { + try { + await store.set(key, currentStatistic.value); + shouldUpdateStatistic.value = false; // 保存后重置标记 + } catch (err) { + error("Failed to save statistics: " + err); + } + } + + // --- B站信息更新 --- + let updateDelay = 30; // 默认30秒更新一次房间信息 + if (streamingInfo.value.status === 'streaming' && !import.meta.env.DEV) { + updateDelay = 15; // 直播中15秒更新一次 (可以适当调整) + } + // 使用取模运算控制调用频率 + if (updateCount.value % (updateDelay / 5) === 0) { // 因为主循环是5秒一次 + updateRoomAndStreamingInfo(); + } + updateCount.value++; +} + +/** + * 记录一个接收到的事件 (由 useWebFetcher 调用) + * @param eventType 事件类型字符串 (例如 "DANMU_MSG") + */ +export function recordEvent(eventType: string) { + const currentDate = format(new Date(), 'yyyy-MM-dd'); + + // 确保 currentStatistic 已为当天初始化 + if (!currentStatistic.value || currentStatistic.value.date !== currentDate) { + // 理论上 updateCallback 会先执行初始化,这里加个警告以防万一 + console.warn("recordEvent called before currentStatistic was initialized for today."); + // 可以选择在这里强制调用一次 updateCallback 来初始化,但这可能是异步的 + // await updateCallback(); // 可能会引入复杂性 + return; // 或者直接返回,丢失这个事件计数 + } + + // 增加总数 + currentStatistic.value.count++; + + // 增加对应类型的计数 + if (!currentStatistic.value.eventTypeCounts) { + currentStatistic.value.eventTypeCounts = {}; // 防御性初始化 + } + currentStatistic.value.eventTypeCounts[eventType] = (currentStatistic.value.eventTypeCounts[eventType] || 0) + 1; + + // 标记需要保存 + shouldUpdateStatistic.value = true; +} + +/** + * 从 command 数据中解析事件类型 + * (需要根据实际接收到的数据结构调整) + */ +export function getEventType(command: any): string { + if (typeof command === 'string') { + try { + command = JSON.parse(command); + } catch (e) { + return 'UNKNOWN_FORMAT'; + } + } + + if (command && typeof command === 'object') { + // 优先使用 'cmd' 字段 (常见于 Web 或 OpenLive) + if (command.cmd) return command.cmd; + // 备选 'command' 字段 + if (command.command) return command.command; + // 备选 'type' 字段 + if (command.type) return command.type; + } + return 'UNKNOWN'; // 未知类型 +} + +/** + * 获取指定天数的历史统计数据 + * @param days 要获取的天数,默认为 7 + */ +export async function getHistoricalStatistics(days: number = 7): Promise { + const store = useTauriStore(); + const keys = (await store.store.keys()) + .filter(key => key.startsWith(STATISTIC_STORE_KEY)) + .sort((a, b) => b.localeCompare(a)); // 按日期降序排序 + + const historicalData: FetcherStatisticData[] = []; + for (let i = 0; i < Math.min(days, keys.length); i++) { + const data = await store.get(keys[i]); + if (data) { + historicalData.push(data); + } + } + return historicalData.reverse(); // 返回按日期升序排列的结果 +} + +/** + * 更新房间和直播流信息 + */ +async function updateRoomAndStreamingInfo() { + // 需要一个房间ID来查询,这个ID可能来自设置、登录信息或固定配置 + // const roomId = accountInfo.value?.roomid ?? settings.value.roomId; // 示例:获取房间ID + const roomId = 21484828; // !!! 示例:这里需要替换成实际获取房间ID的逻辑 !!! + if (!roomId) { + // error("无法获取房间ID以更新直播信息"); + return; + } + + try { + // 查询房间基本信息 + const roomRes = await QueryBiliAPI( + `https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}` + ); + const json = await roomRes.json(); + if (json.code === 0) { + roomInfo.value = json.data; + } else { + error(`Failed to fetch Bili room info: ${json.message}`); + } + // 查询直播流信息 (开放平台或Web接口) + // 注意:这里可能需要根据所选模式(openlive/direct)调用不同的API + // 以下是Web接口示例 + const streamRes = await QueryBiliAPI( + `https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids?uids[]=${roomInfo.value?.uid}` // 通过 UID 查询 + // 或者使用 `https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo?room_ids=${roomId}&req_biz=web_room_componet` + ); + const streamJson = await streamRes.json(); + if (streamJson.code === 0 && streamJson.data && roomInfo.value?.uid) { + // Web API 返回的是一个以 UID 为 key 的对象 + const uidData = streamJson.data[roomInfo.value.uid.toString()]; + if (uidData) { + streamingInfo.value = { + ...uidData, // 合并获取到的数据 + status: uidData.live_status === 1 ? 'streaming' : uidData.live_status === 2 ? 'rotating' : 'prepare', + }; + } else { + // 如果没有对应UID的数据,可能表示未开播或接口变更 + //streamingInfo.value = { status: 'prepare', ...streamingInfo.value }; // 保留旧数据,状态设为prepare + } + } else if (streamJson.code !== 0) { + error(`Failed to fetch Bili streaming info: ${streamJson.message}`); + // 可选:如果获取失败,将状态设为未知或准备 + // streamingInfo.value = { status: 'prepare', ...streamingInfo.value }; + } + + } catch (err) { + error("Error updating room/streaming info: " + err); + } +} \ No newline at end of file diff --git a/src/client/data/initialize.ts b/src/client/data/initialize.ts new file mode 100644 index 0000000..2a38970 --- /dev/null +++ b/src/client/data/initialize.ts @@ -0,0 +1,243 @@ +import { isLoggedIn, useAccount } from "@/api/account"; +import { attachConsole, info, warn } from "@tauri-apps/plugin-log"; +import { useSettings } from "../store/useSettings"; +import { useWebFetcher } from "@/store/useWebFetcher"; +import { useBiliCookie } from "../store/useBiliCookie"; +import { getBuvid, getRoomKey } from "./utils"; +import { initInfo } from "./info"; +import { TrayIcon, TrayIconOptions } from '@tauri-apps/api/tray'; +import { Menu } from "@tauri-apps/api/menu"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { + isPermissionGranted, + onAction, + requestPermission, + sendNotification, +} from '@tauri-apps/plugin-notification'; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { CN_HOST } from "@/data/constants"; +import { invoke } from "@tauri-apps/api/core"; +import { check } from '@tauri-apps/plugin-updater'; +import { relaunch } from '@tauri-apps/plugin-process'; + +const accountInfo = useAccount(); + +export const clientInited = ref(false); +let tray: TrayIcon; +export async function initAll() { + if (clientInited.value) { + return; + } + let permissionGranted = await isPermissionGranted(); + + // If not we need to request it + if (!permissionGranted) { + const permission = await requestPermission(); + permissionGranted = permission === 'granted'; + if (permissionGranted) { + info('Notification permission granted'); + } + + } + initNotificationHandler(); + const detach = await attachConsole(); + const settings = useSettings(); + const biliCookie = useBiliCookie(); + await settings.init(); + info('[init] 已加载账户信息'); + biliCookie.init(); + info('[init] 已加载bilibili cookie'); + initInfo(); + info('[init] 开始更新数据'); + + if (isLoggedIn && accountInfo.value.isBiliVerified) { + const danmakuInitNoticeRef = window.$notification.info({ + title: '正在初始化弹幕客户端...', + closable: false + }); + const result = await initDanmakuClient(); + danmakuInitNoticeRef.destroy(); + if (result.success) { + window.$notification.success({ + title: '弹幕客户端初始化完成', + duration: 3000 + }); + } else { + window.$notification.error({ + title: '弹幕客户端初始化失败: ' + result.message, + }); + } + } + info('[init] 已加载弹幕客户端'); + // 初始化系统托盘图标和菜单 + const menu = await Menu.new({ + items: [ + { + id: 'quit', + text: '退出', + action: () => { + invoke('quit_app'); + }, + }, + ], + }); + const iconData = await (await fetch('https://oss.suki.club/vtsuru/icon.ico')).arrayBuffer(); + const appWindow = getCurrentWindow(); + const options: TrayIconOptions = { + // here you can add a tray menu, title, tooltip, event handler, etc + menu: menu, + title: 'VTsuru.Client', + tooltip: 'VTsuru 事件收集器', + icon: iconData, + action: (event) => { + + switch (event.type) { + case 'DoubleClick': + appWindow.show(); + break; + case 'Click': + appWindow.show(); + break; + } + } + }; + + + tray = await TrayIcon.new(options); + + clientInited.value = true; +} +export function OnClientUnmounted() { + if (clientInited.value) { + clientInited.value = false; + } + + tray.close(); +} + +async function checkUpdate() { + const update = await check(); + if (update) { + console.log( + `found update ${update.version} from ${update.date} with notes ${update.body}` + ); + let downloaded = 0; + let contentLength = 0; + // alternatively we could also call update.download() and update.install() separately + await update.downloadAndInstall((event) => { + switch (event.event) { + case 'Started': + contentLength = event.data.contentLength || 0; + console.log(`started downloading ${event.data.contentLength} bytes`); + break; + case 'Progress': + downloaded += event.data.chunkLength; + console.log(`downloaded ${downloaded} from ${contentLength}`); + break; + case 'Finished': + console.log('download finished'); + break; + } + }); + + console.log('update installed'); + await relaunch(); + } +} + +export const isInitedDanmakuClient = ref(false); +export const isInitingDanmakuClient = ref(false); +export async function initDanmakuClient() { + const biliCookie = useBiliCookie(); + const settings = useSettings(); + if (isInitedDanmakuClient.value || isInitingDanmakuClient.value) { + return { success: true, message: '' }; + } + isInitingDanmakuClient.value = true; + let result = { success: false, message: '' }; + try { + if (isLoggedIn) { + if (settings.settings.useDanmakuClientType === 'openlive') { + result = await initOpenLive(); + } else { + const cookie = await biliCookie.getBiliCookie(); + if (!cookie) { + if (settings.settings.fallbackToOpenLive) { + settings.settings.useDanmakuClientType = 'openlive'; + settings.save(); + info('未设置bilibili cookie, 根据设置切换为openlive'); + result = await initOpenLive(); + } else { + info('未设置bilibili cookie, 跳过弹幕客户端初始化'); + window.$notification.warning({ + title: '未设置bilibili cookie, 跳过弹幕客户端初始化', + duration: 5, + }); + result = { success: false, message: '未设置bilibili cookie' }; + } + } else { + const resp = await callStartDanmakuClient(); + if (!resp?.success) { + warn('加载弹幕客户端失败: ' + resp?.message); + result = { success: false, message: resp?.message }; + } else { + info('已加载弹幕客户端'); + result = { success: true, message: '' }; + } + } + } + } + return result; + } catch (err) { + warn('加载弹幕客户端失败: ' + err); + return { success: false, message: '加载弹幕客户端失败' }; + } finally { + if (result) { + isInitedDanmakuClient.value = true; + } + isInitingDanmakuClient.value = false; + } +} +export async function initOpenLive() { + const reuslt = await callStartDanmakuClient(); + if (reuslt?.success == true) { + info('已加载弹幕客户端 [openlive]'); + } else { + warn('加载弹幕客户端失败 [openlive]: ' + reuslt?.message); + } + return reuslt; +} +function initNotificationHandler(){ + onAction((event) => { + if (event.extra?.type === 'question-box') { + openUrl(CN_HOST + '/manage/question-box'); + } + }); +} + +export async function callStartDanmakuClient() { + const biliCookie = useBiliCookie(); + const settings = useSettings(); + const webFetcher = useWebFetcher(); + if (settings.settings.useDanmakuClientType === 'direct') { + const key = await getRoomKey( + accountInfo.value.biliRoomId!, await biliCookie.getBiliCookie() || ''); + if (!key) { + warn('获取房间密钥失败, 无法连接弹幕客户端'); + return { success: false, message: '无法获取房间密钥' }; + } + const buvid = await getBuvid(); + if (!buvid) { + warn('获取buvid失败, 无法连接弹幕客户端'); + return { success: false, message: '无法获取buvid' }; + } + return await webFetcher.Start('direct', { + roomId: accountInfo.value.biliRoomId!, + buvid: buvid.data, + token: key, + tokenUserId: biliCookie.uId!, + }, true); + } else { + return await webFetcher.Start('openlive', undefined, true); + } +} \ No newline at end of file diff --git a/src/client/data/models.ts b/src/client/data/models.ts new file mode 100644 index 0000000..a6a29cb --- /dev/null +++ b/src/client/data/models.ts @@ -0,0 +1,282 @@ +export interface EventFetcherStateModel { + online: boolean; + status: { [errorCode: string]: string }; + version?: string; + todayReceive: number; + useCookie: boolean; + type: EventFetcherType; +} + +export enum EventFetcherType { + Application, + OBS, + Server, + Tauri, +} + +export type BiliRoomInfo = { + uid: number; + room_id: number; + short_id: number; + attention: number; + online: number; + is_portrait: boolean; + description: string; + live_status: number; + area_id: number; + parent_area_id: number; + parent_area_name: string; + old_area_id: number; + background: string; + title: string; + user_cover: string; + keyframe: string; + is_strict_room: boolean; + live_time: string; + tags: string; + is_anchor: number; + room_silent_type: string; + room_silent_level: number; + room_silent_second: number; + area_name: string; + pendants: string; + area_pendants: string; + hot_words: string[]; + hot_words_status: number; + verify: string; + new_pendants: { + frame: { + name: string; + value: string; + position: number; + desc: string; + area: number; + area_old: number; + bg_color: string; + bg_pic: string; + use_old_area: boolean; + }; + badge: unknown; // null in the example, adjust to proper type if known + mobile_frame: { + name: string; + value: string; + position: number; + desc: string; + area: number; + area_old: number; + bg_color: string; + bg_pic: string; + use_old_area: boolean; + }; + mobile_badge: unknown; // null in the example, adjust to proper type if known + }; + up_session: string; + pk_status: number; + pk_id: number; + battle_id: number; + allow_change_area_time: number; + allow_upload_cover_time: number; + studio_info: { + status: number; + master_list: any[]; // empty array in the example, adjust to proper type if known + }; +} + +export type FetcherStatisticData = { + date: string; + count: number; + eventTypeCounts: { [eventType: string]: number }; +}; +export type BiliStreamingInfo = { + status: 'prepare' | 'streaming' | 'cycle'; + streamAt: Date; + roomId: number; + title: string; + coverUrl: string; + frameUrl: string; + areaName: string; + parentAreaName: string; + online: number; + attention: number; +}; + +// Nested type for Vip Label +interface VipLabel { + path: string; + text: string; + label_theme: string; + text_color: string; + bg_style: number; + bg_color: string; + border_color: string; + use_img_label: boolean; + img_label_uri_hans: string; + img_label_uri_hant: string; + img_label_uri_hans_static: string; + img_label_uri_hant_static: string; +} + +// Nested type for Avatar Icon +interface AvatarIcon { + icon_type: number; + // Assuming icon_resource could contain arbitrary data or be empty + icon_resource: Record | {}; +} + +// Nested type for Vip Info +interface VipInfo { + type: number; + status: number; + due_date: number; // Likely a Unix timestamp in milliseconds + vip_pay_type: number; + theme_type: number; + label: VipLabel; + avatar_subscript: number; + nickname_color: string; + role: number; + avatar_subscript_url: string; + tv_vip_status: number; + tv_vip_pay_type: number; + tv_due_date: number; // Likely a Unix timestamp in milliseconds or 0 + avatar_icon: AvatarIcon; +} + +// Nested type for Pendant Info +interface PendantInfo { + pid: number; + name: string; + image: string; // URL + expire: number; // Likely a timestamp or duration + image_enhance: string; // URL + image_enhance_frame: string; // URL or empty string + n_pid: number; +} + +// Nested type for Nameplate Info +interface NameplateInfo { + nid: number; + name: string; + image: string; // URL + image_small: string; // URL + level: string; + condition: string; +} + +// Nested type for Official Info +interface OfficialInfo { + role: number; + title: string; + desc: string; + type: number; +} + +// Nested type for Profession Info +interface ProfessionInfo { + id: number; + name: string; + show_name: string; + is_show: number; // Likely 0 or 1 + category_one: string; + realname: string; + title: string; + department: string; + certificate_no: string; + certificate_show: boolean; +} + +// Nested type for Honours Colour +interface HonoursColour { + dark: string; // Hex color code + normal: string; // Hex color code +} + +// Nested type for Honours Info +interface HonoursInfo { + mid: number; + colour: HonoursColour; + // Assuming tags could be an array of strings if not null + tags: string[] | null; + is_latest_100honour: number; // Likely 0 or 1 +} + +// Nested type for Attestation Common Info +interface CommonAttestationInfo { + title: string; + prefix: string; + prefix_title: string; +} + +// Nested type for Attestation Splice Info +interface SpliceAttestationInfo { + title: string; +} + +// Nested type for Attestation Info +interface AttestationInfo { + type: number; + common_info: CommonAttestationInfo; + splice_info: SpliceAttestationInfo; + icon: string; + desc: string; +} + +// Nested type for Expert Info +interface ExpertInfo { + title: string; + state: number; + type: number; + desc: string; +} + +// Nested type for Level Exp Info +interface LevelExpInfo { + current_level: number; + current_min: number; + current_exp: number; + next_exp: number; // -1 might indicate max level or data not applicable + level_up: number; // Likely a Unix timestamp +} + +// Main User Profile Type +export type BiliUserProfile = { + mid: number; + name: string; + sex: string; // Could be more specific like '男' | '女' | '保密' if desired + face: string; // URL + sign: string; + rank: number; + level: number; + jointime: number; // Likely a Unix timestamp or 0 + moral: number; + silence: number; // Likely 0 or 1 + email_status: number; // Likely 0 or 1 + tel_status: number; // Likely 0 or 1 + identification: number; // Likely 0 or 1 + vip: VipInfo; + pendant: PendantInfo; + nameplate: NameplateInfo; + official: OfficialInfo; + birthday: number; // Likely a Unix timestamp + is_tourist: number; // Likely 0 or 1 + is_fake_account: number; // Likely 0 or 1 + pin_prompting: number; // Likely 0 or 1 + is_deleted: number; // Likely 0 or 1 + in_reg_audit: number; // Likely 0 or 1 + is_rip_user: boolean; + profession: ProfessionInfo; + face_nft: number; + face_nft_new: number; + is_senior_member: number; // Likely 0 or 1 + honours: HonoursInfo; + digital_id: string; + digital_type: number; + attestation: AttestationInfo; + expert_info: ExpertInfo; + // Assuming name_render could be various types or null + name_render: any | null; + country_code: string; + level_exp: LevelExpInfo; + coins: number; // Can be float + following: number; + follower: number; +}; \ No newline at end of file diff --git a/src/client/data/notification.ts b/src/client/data/notification.ts new file mode 100644 index 0000000..217e35f --- /dev/null +++ b/src/client/data/notification.ts @@ -0,0 +1,27 @@ +import { QAInfo } from "@/api/api-models"; +import { useSettings } from "../store/useSettings"; +import { isPermissionGranted, onAction, sendNotification } from "@tauri-apps/plugin-notification"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { CN_HOST } from "@/data/constants"; + +export async function onReceivedQuestion(question: QAInfo) { + const setting = useSettings(); + if (setting.settings.notificationSettings.enableTypes.includes("question-box")) { + window.$notification.info({ + title: "收到提问", + description: '收到来自 [' + question.sender.name || '匿名用户' + '] 的提问', + duration: 5, + }); + let permissionGranted = await isPermissionGranted(); + if (permissionGranted) { + sendNotification({ + title: "收到提问", + body: '来自 [' + question.sender.name || '匿名用户' + '] 的提问', + silent: false, + extra: { type: 'question-box' }, + }); + + } + } + +} \ No newline at end of file diff --git a/src/client/data/utils.ts b/src/client/data/utils.ts new file mode 100644 index 0000000..6301b72 --- /dev/null +++ b/src/client/data/utils.ts @@ -0,0 +1,77 @@ +import { fetch } from '@tauri-apps/plugin-http'; +import { useBiliCookie } from '../store/useBiliCookie'; +import { QueryPostAPI } from '@/api/query'; +import { OPEN_LIVE_API_URL } from '@/data/constants'; +import { error } from '@tauri-apps/plugin-log'; + +export async function QueryBiliAPI(url: string, method: string = 'GET', cookie: string = '') { + const u = new URL(url); + return fetch(url, { + method: method, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + Origin: '', + Cookie: cookie || (await useBiliCookie().getBiliCookie()) || '', + 'Upgrade-Insecure-Requests': '1', + }, + }); +} + +export async function getRoomKey(roomId: number, cookie: string) { + try { + const result = await QueryBiliAPI( + 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=' + roomId + ); + const json = await result.json(); + if (json.code === 0) return json.data.token; + else { + error(`无法获取直播间key: ${json.message}`); + } + } catch (err) { + error(`无法获取直播间key: ${err}`); + } +} +export async function getBuvid() { + try { + const result = await QueryBiliAPI('https://api.bilibili.com/x/web-frontend/getbuvid'); + if (result.ok) { + const json = await result.json(); + if (json.code === 0) return json.data.buvid; + else { + error(`无法获取buvid: ${json.message}`); + } + } else { + error(`无法获取buvid: ${result.statusText}`); + } + } catch (err) { + error(`无法获取buvid: ${err}`); + } +} + +export async function getAuthInfo(): Promise<{ + data: any; + message: string; +}> { + try { + const data = await QueryPostAPI(OPEN_LIVE_API_URL + 'start'); + if (data.code == 200) { + console.log(`[open-live] 已获取认证信息`); + return { + data: data.data, + message: '', + }; + } else { + return { + data: null, + message: data.message, + }; + } + } catch (err) { + return { + data: null, + message: err?.toString() || '未知错误', + }; + } +} + diff --git a/src/client/store/useBiliCookie.ts b/src/client/store/useBiliCookie.ts new file mode 100644 index 0000000..598103c --- /dev/null +++ b/src/client/store/useBiliCookie.ts @@ -0,0 +1,581 @@ +import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; +import { useTauriStore } from './useTauriStore'; +import { error, info, warn, debug } from '@tauri-apps/plugin-log'; +import { AES, enc, MD5 } from 'crypto-js'; +import { QueryBiliAPI } from '../data/utils'; +import { BiliUserProfile } from '../data/models'; +import { defineStore, acceptHMRUpdate } from 'pinia'; +import { ref, computed, shallowRef } from 'vue'; + +// --- 常量定义 --- +// Tauri Store 存储键名 +export const BILI_COOKIE_KEY = 'user.bilibili.cookie'; +export const COOKIE_CLOUD_KEY = 'user.bilibili.cookie_cloud'; +export const USER_INFO_CACHE_KEY = 'cache.bilibili.userInfo'; + +// 检查周期 (毫秒) +const REGULAR_CHECK_INTERVAL = 60 * 1000; // 每分钟检查一次 Cookie 有效性 +const CLOUD_SYNC_INTERVAL_CHECKS = 30; // 每 30 次常规检查后 (约 30 分钟) 同步一次 CookieCloud + +// 用户信息缓存有效期 (毫秒) +const USER_INFO_CACHE_DURATION = 5 * 60 * 1000; // 缓存 5 分钟 + +// --- 类型定义 --- + +// Bilibili Cookie 存储数据结构 +type BiliCookieStoreData = { + cookie: string; + refreshToken?: string; // refreshToken 似乎未使用,设为可选 + lastRefresh?: Date; // 上次刷新时间,似乎未使用,设为可选 +}; + +// Cookie Cloud 配置数据结构 +export type CookieCloudConfig = { + key: string; + password: string; + host?: string; // CookieCloud 服务地址,可选,有默认值 +}; + +// CookieCloud 导出的 Cookie 单项结构 +export interface CookieCloudCookie { + domain: string; + expirationDate: number; + hostOnly: boolean; + httpOnly: boolean; + name: string; + path: string; + sameSite: string; + secure: boolean; + session: boolean; + storeId: string; + value: string; +} + +// CookieCloud 导出的完整数据结构 +interface CookieCloudExportData { + cookie_data: Record; // 按域名分组的 Cookie 数组 + local_storage_data?: Record; // 本地存储数据 (可选) + update_time: string; // 更新时间 ISO 8601 字符串 +} + +// 用户信息缓存结构 +type UserInfoCache = { + userInfo: BiliUserProfile; + accessedAt: number; // 使用时间戳 (Date.now()) 以方便比较 +}; + +// CookieCloud 状态类型 +type CookieCloudState = 'unset' | 'valid' | 'invalid' | 'syncing'; + +// --- Store 定义 --- + +export const useBiliCookie = defineStore('biliCookie', () => { + // --- 依赖和持久化存储实例 --- + // 使用 useTauriStore 获取持久化存储目标 + const biliCookieStore = useTauriStore().getTarget(BILI_COOKIE_KEY); + const cookieCloudStore = useTauriStore().getTarget(COOKIE_CLOUD_KEY); + const userInfoCacheStore = useTauriStore().getTarget(USER_INFO_CACHE_KEY); + + // --- 核心状态 --- + // 使用 shallowRef 存储用户信息对象,避免不必要的深度侦听,提高性能 + const _cachedUserInfo = shallowRef(); + // 是否已从存储加载了 Cookie (不代表有效) + const hasBiliCookie = ref(false); + // 当前 Cookie 是否通过 Bilibili API 验证有效 + const isCookieValid = ref(false); + // CookieCloud 配置及同步状态 + const cookieCloudState = ref('unset'); + // Bilibili 用户 ID + const uId = ref(); + + // --- 计算属性 --- + // 公开的用户信息,只读 + const userInfo = computed(() => _cachedUserInfo.value?.userInfo); + + // --- 内部状态和变量 --- + let _isInitialized = false; // 初始化标志,防止重复执行 + let _checkIntervalId: ReturnType | null = null; // 定时检查器 ID + let _checkCounter = 0; // 常规检查计数器,用于触发 CookieCloud 同步 + + // --- 私有辅助函数 --- + + /** + * @description 更新并持久化用户信息缓存 + * @param data Bilibili 用户信息 + */ + const _updateUserInfoCache = async (data: BiliUserProfile): Promise => { + const cacheData: UserInfoCache = { userInfo: data, accessedAt: Date.now() }; + _cachedUserInfo.value = cacheData; // 更新内存缓存 + uId.value = data.mid; // 更新 uId + try { + await userInfoCacheStore.set(cacheData); // 持久化缓存 + debug('[BiliCookie] 用户信息缓存已更新并持久化'); + } catch (err) { + error('[BiliCookie] 持久化用户信息缓存失败: ' + String(err)); + } + }; + + /** + * @description 清除用户信息缓存 (内存和持久化) + */ + const _clearUserInfoCache = async (): Promise => { + _cachedUserInfo.value = undefined; // 清除内存缓存 + uId.value = undefined; // 清除 uId + try { + await userInfoCacheStore.delete(); // 删除持久化缓存 + debug('[BiliCookie] 用户信息缓存已清除'); + } catch (err) { + error('[BiliCookie] 清除持久化用户信息缓存失败: ' + String(err)); + } + }; + + /** + * @description 更新 Cookie 存在状态和有效状态 + * @param hasCookie Cookie 是否存在 + * @param isValid Cookie 是否有效 + */ + const _updateCookieState = (hasCookie: boolean, isValid: boolean): void => { + hasBiliCookie.value = hasCookie; + isCookieValid.value = isValid; + if (!hasCookie || !isValid) { + // 如果 Cookie 不存在或无效,清除可能过时的用户信息缓存 + // 注意:这里采取了更严格的策略,无效则清除缓存,避免显示旧信息 + // _clearUserInfoCache(); // 考虑是否在无效时立即清除缓存 + debug(`[BiliCookie] Cookie 状态更新: hasCookie=${hasCookie}, isValid=${isValid}`); + } + }; + + /** + * @description 检查提供的 Bilibili Cookie 是否有效 + * @param cookie 要验证的 Cookie 字符串 + * @returns Promise<{ valid: boolean; data?: BiliUserProfile }> 验证结果和用户信息 (如果有效) + */ + const _checkCookieValidity = async (cookie: string): Promise<{ valid: boolean; data?: BiliUserProfile; }> => { + if (!cookie) { + return { valid: false }; + } + try { + // 使用传入的 cookie 调用 Bilibili API + const resp = await QueryBiliAPI('https://api.bilibili.com/x/space/myinfo', 'GET', cookie); + + const json = await resp.json(); + if (json.code === 0 && json.data) { + debug('[BiliCookie] Cookie 验证成功, 用户:', json.data.name); + // 验证成功,更新用户信息缓存 + await _updateUserInfoCache(json.data); + return { valid: true, data: json.data }; + } else { + warn(`[BiliCookie] Cookie 验证失败 (API 返回): ${json.message || `code: ${json.code}`}`); + return { valid: false }; + } + } catch (err) { + error('[BiliCookie] 验证 Cookie 时请求 Bilibili API 出错: ' + String(err)); + return { valid: false }; + } + }; + + + /** + * @description 从 CookieCloud 服务获取并解密 Bilibili Cookie + * @param config CookieCloud 配置 (如果提供,则使用此配置;否则使用已存储的配置) + * @returns Promise Bilibili Cookie 字符串 + * @throws 如果配置缺失、网络请求失败、解密失败或未找到 Bilibili Cookie,则抛出错误 + */ + const _fetchAndDecryptFromCloud = async (config?: CookieCloudConfig): Promise => { + const cloudConfig = config ?? await cookieCloudStore.get(); // 获取配置 + + if (!cloudConfig?.key || !cloudConfig?.password) { + throw new Error("CookieCloud 配置不完整 (缺少 Key 或 Password)"); + } + + const host = cloudConfig.host || "https://cookie.vtsuru.live"; // 默认 Host + const url = new URL(host); + url.pathname = `/get/${cloudConfig.key}`; + + info(`[BiliCookie] 正在从 CookieCloud (${url.hostname}) 获取 Cookie...`); + + try { + // 注意: 浏览器环境通常无法直接设置 User-Agent + // 使用 Tauri fetch 发送请求 + const response = await tauriFetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json' // 根据 CookieCloud API 要求可能需要调整 + } + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`CookieCloud 请求失败: ${response.status} ${response.statusText}. ${errorText}`); + } + + const json = await response.json() as any; // 类型断言需要谨慎 + + if (json.encrypted) { + // 执行解密 + try { + const keyMaterial = MD5(cloudConfig.key + '-' + cloudConfig.password).toString(); + const decryptionKey = keyMaterial.substring(0, 16); // 取前16位作为 AES 密钥 + const decrypted = AES.decrypt(json.encrypted, decryptionKey).toString(enc.Utf8); + + if (!decrypted) { + throw new Error("解密结果为空,可能是密钥不匹配"); + } + + const cookieData = JSON.parse(decrypted) as CookieCloudExportData; + + // 提取 bilibili.com 的 Cookie + const biliCookies = cookieData.cookie_data?.['bilibili.com']; + if (!biliCookies || biliCookies.length === 0) { + throw new Error("在 CookieCloud 数据中未找到 'bilibili.com' 的 Cookie"); + } + + // 拼接 Cookie 字符串 + const cookieString = biliCookies + .map(c => `${c.name}=${c.value}`) + .join('; '); + + info('[BiliCookie] CookieCloud Cookie 获取并解密成功'); + return cookieString; + + } catch (decryptErr) { + error('[BiliCookie] CookieCloud Cookie 解密失败: ' + String(decryptErr)); + throw new Error(`Cookie 解密失败: ${decryptErr instanceof Error ? decryptErr.message : String(decryptErr)}`); + } + } else if (json.cookie_data) { + // 处理未加密的情况 (如果 CookieCloud 支持) + warn('[BiliCookie] 从 CookieCloud 收到未加密的 Cookie 数据'); + const biliCookies = (json as CookieCloudExportData).cookie_data?.['bilibili.com']; + if (!biliCookies || biliCookies.length === 0) { + throw new Error("在 CookieCloud 数据中未找到 'bilibili.com' 的 Cookie"); + } + const cookieString = biliCookies + .map(c => `${c.name}=${c.value}`) + .join('; '); + return cookieString; + } + else { + // API 返回了非预期的数据结构 + throw new Error(json.message || "从 CookieCloud 获取 Cookie 失败,响应格式不正确"); + } + } catch (networkErr) { + error('[BiliCookie] 请求 CookieCloud 时出错: ' + String(networkErr)); + throw new Error(`请求 CookieCloud 时出错: ${networkErr instanceof Error ? networkErr.message : String(networkErr)}`); + } + }; + + /** + * @description 从已配置的 CookieCloud 同步 Cookie,并更新本地状态 + * @returns Promise 是否同步并验证成功 + */ + const _syncFromCookieCloud = async (): Promise => { + const config = await cookieCloudStore.get(); + if (!config?.key) { + debug('[BiliCookie] 未配置 CookieCloud 或缺少 key,跳过同步'); + // 如果从未设置过,保持 unset;如果之前设置过但现在无效,标记为 invalid + if (cookieCloudState.value !== 'unset') { + cookieCloudState.value = 'invalid'; // 假设配置被清空意味着无效 + } + return false; + } + + cookieCloudState.value = 'syncing'; // 标记为同步中 + try { + const cookieString = await _fetchAndDecryptFromCloud(config); + // 验证从 Cloud 获取的 Cookie + const validationResult = await _checkCookieValidity(cookieString); + + if (validationResult.valid) { + // 验证成功,保存 Cookie + await setBiliCookie(cookieString); // setBiliCookie 内部会处理状态更新和持久化 + cookieCloudState.value = 'valid'; // 标记为有效 + info('[BiliCookie] 从 CookieCloud 同步并验证 Cookie 成功'); + return true; + } else { + // 从 Cloud 获取的 Cookie 无效 + warn('[BiliCookie] 从 CookieCloud 获取的 Cookie 无效'); + cookieCloudState.value = 'invalid'; // 标记为无效 + // 不更新本地 Cookie,保留当前有效的或无效的状态 + _updateCookieState(hasBiliCookie.value, false); // 显式标记当前cookie状态可能因云端无效而变为无效 + return false; + } + } catch (err) { + error('[BiliCookie] CookieCloud 同步失败: ' + String(err)); + cookieCloudState.value = 'invalid'; // 同步出错,标记为无效 + // 同步失败不应影响当前的 isCookieValid 状态,除非需要强制失效 + // _updateCookieState(hasBiliCookie.value, false); // 可选:同步失败时强制本地cookie失效 + return false; + } + }; + + + // --- 公开方法 --- + + /** + * @description 初始化 BiliCookie Store + * - 加载持久化数据 (Cookie, Cloud 配置, 用户信息缓存) + * - 检查 CookieCloud 配置状态 + * - 进行首次 Cookie 有效性检查 (或使用缓存) + * - 启动定时检查任务 + */ + const init = async (): Promise => { + if (_isInitialized) { + debug('[BiliCookie] Store 已初始化,跳过'); + return; + } + _isInitialized = true; + info('[BiliCookie] Store 初始化开始...'); + + // 1. 加载持久化数据 + const [storedCookieData, storedCloudConfig, storedUserInfo] = await Promise.all([ + biliCookieStore.get(), + cookieCloudStore.get(), + userInfoCacheStore.get(), + ]); + + // 2. 处理 CookieCloud 配置 + if (storedCloudConfig?.key && storedCloudConfig?.password) { + // 这里仅设置初始状态,有效性将在后续检查或同步中确认 + cookieCloudState.value = 'valid'; // 假设配置存在即可能有效,待验证 + info('[BiliCookie] 检测到已配置 CookieCloud'); + } else { + cookieCloudState.value = 'unset'; + info('[BiliCookie] 未配置 CookieCloud'); + } + + // 3. 处理用户信息缓存 + if (storedUserInfo && (Date.now() - storedUserInfo.accessedAt < USER_INFO_CACHE_DURATION)) { + _cachedUserInfo.value = storedUserInfo; + uId.value = storedUserInfo.userInfo.mid; + info(`[BiliCookie] 从缓存加载有效用户信息: UID=${uId.value}`); + // 如果缓存有效,可以初步认为 Cookie 是有效的 (至少在缓存有效期内是) + _updateCookieState(!!storedCookieData?.cookie, true); + } else { + info('[BiliCookie] 无有效用户信息缓存'); + _updateCookieState(!!storedCookieData?.cookie, false); // 默认无效,待检查 + if (storedUserInfo) { + // 如果有缓存但已过期,清除它 + await _clearUserInfoCache(); + } + } + + + // 4. 处理 Bilibili Cookie + if (storedCookieData?.cookie) { + hasBiliCookie.value = true; // 标记存在 Cookie + info('[BiliCookie] 检测到已存储的 Bilibili Cookie'); + // 检查 Cookie 有效性,除非用户信息缓存有效且未过期 + if (!_cachedUserInfo.value) { // 只有在没有有效缓存时才立即检查 + debug('[BiliCookie] 无有效缓存,立即检查 Cookie 有效性...'); + const { valid } = await _checkCookieValidity(storedCookieData.cookie); + _updateCookieState(true, valid); // 更新状态 + } + } else { + _updateCookieState(false, false); // 没有 Cookie,自然无效 + info('[BiliCookie] 未找到存储的 Bilibili Cookie'); + } + + + // 5. 启动定时检查器 + if (_checkIntervalId) { + clearInterval(_checkIntervalId); // 清除旧的定时器 (理论上不应存在) + } + _checkIntervalId = setInterval(check, REGULAR_CHECK_INTERVAL); + info(`[BiliCookie] 定时检查已启动,周期: ${REGULAR_CHECK_INTERVAL / 1000} 秒`); + + info('[BiliCookie] Store 初始化完成'); + }; + + /** + * @description 定期检查 Cookie 有效性,并按需从 CookieCloud 同步 + * @param forceCheckCloud 是否强制立即尝试从 CookieCloud 同步 (通常由 init 调用) + */ + const check = async (forceCheckCloud: boolean = false): Promise => { + debug('[BiliCookie] 开始周期性检查...'); + _checkCounter++; + + let cloudSyncAttempted = false; + let cloudSyncSuccess = false; + + // 检查是否需要从 CookieCloud 同步 + const shouldSyncCloud = forceCheckCloud || (_checkCounter % CLOUD_SYNC_INTERVAL_CHECKS === 0); + + if (shouldSyncCloud && cookieCloudState.value !== 'unset' && cookieCloudState.value !== 'syncing') { + info(`[BiliCookie] 触发 CookieCloud 同步 (计数: ${_checkCounter}, 强制: ${forceCheckCloud})`); + cloudSyncAttempted = true; + cloudSyncSuccess = await _syncFromCookieCloud(); + // 同步后重置计数器,避免连续同步 + _checkCounter = 0; + } + + // 如果没有尝试云同步,或者云同步失败,则检查本地 Cookie + if (!cloudSyncAttempted || !cloudSyncSuccess) { + debug('[BiliCookie] 检查本地存储的 Cookie 有效性...'); + const storedCookie = (await biliCookieStore.get())?.cookie; + if (storedCookie) { + const { valid } = await _checkCookieValidity(storedCookie); + // 只有在云同步未成功时才更新状态,避免覆盖云同步设置的状态 + if (!cloudSyncSuccess) { + _updateCookieState(true, valid); + } + } else { + // 本地没有 Cookie + _updateCookieState(false, false); + // 如果本地没 cookie 但 cookieCloud 配置存在且非 syncing, 尝试一次同步 + if (!cloudSyncAttempted && cookieCloudState.value !== 'unset' && cookieCloudState.value !== 'syncing') { + info('[BiliCookie] 本地无 Cookie,尝试从 CookieCloud 获取...'); + await _syncFromCookieCloud(); // 尝试获取一次 + _checkCounter = 0; // 同步后重置计数器 + } + } + } + debug('[BiliCookie] 周期性检查结束'); + }; + + /** + * @description 设置新的 Bilibili Cookie + * @param cookie Cookie 字符串 + * @param refreshToken (可选) Bilibili refresh token + */ + const setBiliCookie = async (cookie: string, refreshToken?: string): Promise => { + info('[BiliCookie] 正在设置新的 Bilibili Cookie...'); + // 1. 验证新 Cookie 的有效性 + const { valid } = await _checkCookieValidity(cookie); + + if (valid) { + // 2. 如果有效,则持久化存储 + const dataToStore: BiliCookieStoreData = { + cookie, + ...(refreshToken && { refreshToken }), // 仅在提供时添加 refreshToken + lastRefresh: new Date() // 更新刷新时间戳 + }; + try { + await biliCookieStore.set(dataToStore); + info('[BiliCookie] 新 Bilibili Cookie 已验证并保存'); + _updateCookieState(true, true); // 更新状态为存在且有效 + } catch (err) { + error('[BiliCookie] 保存 Bilibili Cookie 失败: ' + String(err)); + // 保存失败,状态回滚或标记为错误?暂时保持验证结果 + _updateCookieState(true, false); // Cookie 存在但保存失败,标记无效可能更安全 + throw new Error("保存 Bilibili Cookie 失败"); // 向上抛出错误 + } + } else { + // 新 Cookie 无效,不保存,并标记状态 + _updateCookieState(hasBiliCookie.value, false); // 保持 hasBiliCookie 原样或设为 false?取决于策略 + warn('[BiliCookie] 尝试设置的 Bilibili Cookie 无效,未保存'); + // 可以选择抛出错误,让调用者知道设置失败 + // throw new Error("设置的 Bilibili Cookie 无效"); + } + }; + + /** + * @description 获取当前存储的 Bilibili Cookie (不保证有效性) + * @returns Promise Cookie 字符串或 undefined + */ + const getBiliCookie = async (): Promise => { + const data = await biliCookieStore.get(); + return data?.cookie; + }; + + /** + * @description 退出登录,清除 Bilibili Cookie 及相关状态和缓存 + */ + const logout = async (): Promise => { + info('[BiliCookie] 用户请求退出登录...'); + // 停止定时检查器 + if (_checkIntervalId) { + clearInterval(_checkIntervalId); + _checkIntervalId = null; + debug('[BiliCookie] 定时检查已停止'); + } + // 清除 Cookie 存储 + try { + await biliCookieStore.delete(); + } catch (err) { + error('[BiliCookie] 清除 Bilibili Cookie 存储失败: ' + String(err)); + } + // 清除用户信息缓存 + await _clearUserInfoCache(); + // 重置状态变量 + _updateCookieState(false, false); + // Cookie Cloud 状态是否重置?取决于产品逻辑,暂时保留 + // cookieCloudState.value = 'unset'; + // 重置初始化标志,允许重新 init + _isInitialized = false; + _checkCounter = 0; // 重置计数器 + info('[BiliCookie] 退出登录完成,状态已重置'); + }; + + /** + * @description 设置并验证 CookieCloud 配置 + * @param config CookieCloud 配置数据 + * @throws 如果配置无效或从 CookieCloud 获取/验证 Cookie 失败 + */ + const setCookieCloudConfig = async (config: CookieCloudConfig): Promise => { + info('[BiliCookie] 正在设置新的 CookieCloud 配置...'); + cookieCloudState.value = 'syncing'; // 标记为尝试同步/验证中 + + try { + // 1. 使用新配置尝试从 Cloud 获取 Cookie + const cookieString = await _fetchAndDecryptFromCloud(config); + // 2. 验证获取到的 Cookie + const validationResult = await _checkCookieValidity(cookieString); + + if (validationResult.valid && validationResult.data) { + // 3. 如果验证成功,保存 CookieCloud 配置 + await cookieCloudStore.set(config); + info('[BiliCookie] CookieCloud 配置验证成功并已保存. 用户:' + validationResult.data.name); + cookieCloudState.value = 'valid'; // 标记为有效 + + // 4. 使用从 Cloud 获取的有效 Cookie 更新本地 Cookie + // 注意:这里直接调用 setBiliCookie 会再次进行验证,但确保状态一致性 + await setBiliCookie(cookieString); + // 重置检查计数器,以便下次正常检查 + _checkCounter = 0; + } else { + // 从 Cloud 获取的 Cookie 无效 + cookieCloudState.value = 'invalid'; + warn('[BiliCookie] 使用新 CookieCloud 配置获取的 Cookie 无效'); + throw new Error('CookieCloud 配置无效:获取到的 Bilibili Cookie 无法通过验证'); + } + } catch (err) { + error('[BiliCookie] 设置 CookieCloud 配置失败: ' + String(err)); + cookieCloudState.value = 'invalid'; // 出错则标记为无效 + // 向上抛出错误,通知调用者失败 + throw err; // err 已经是 Error 类型或被包装过 + } + }; + async function clearCookieCloudConfig() { + info('[BiliCookie] 清除 CookieCloud 配置...'); + cookieCloudState.value = 'unset'; + // 清除持久化存储 + await cookieCloudStore.delete().catch(err => { + error('[BiliCookie] 清除 CookieCloud 配置失败: ' + String(err)); + }); + } + + // --- 返回 Store 的公开接口 --- + return { + // 只读状态和计算属性 + hasBiliCookie: computed(() => hasBiliCookie.value), // 只读 ref + isCookieValid: computed(() => isCookieValid.value), // 只读 ref + cookieCloudState: computed(() => cookieCloudState.value), // 只读 ref + uId: computed(() => uId.value), // 只读 ref + userInfo, // computed 属性本身就是只读的 + + // 方法 + init, + check, // 暴露 check 方法,允许手动触发检查 (例如,应用从后台恢复) + setBiliCookie, + getBiliCookie, // 获取原始 cookie 字符串的方法 + logout, + setCookieCloudConfig, + clearCookieCloudConfig, + // 注意:不再直接暴露 fetchBiliCookieFromCloud,其逻辑已整合到内部同步和设置流程中 + }; +}); + +// --- HMR 支持 --- +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useBiliCookie, import.meta.hot)); +} \ No newline at end of file diff --git a/src/client/store/useSettings.ts b/src/client/store/useSettings.ts new file mode 100644 index 0000000..758672b --- /dev/null +++ b/src/client/store/useSettings.ts @@ -0,0 +1,45 @@ +import { useTauriStore } from './useTauriStore'; + +export type NotificationType = 'question-box' | 'danmaku'; +export type NotificationSettings = { + enableTypes: NotificationType[]; +}; +export type VTsuruClientSettings = { + useDanmakuClientType: 'openlive' | 'direct'; + fallbackToOpenLive: boolean; + + danmakuHistorySize: number; + loginType: 'qrcode' | 'cookiecloud' + + enableNotification: boolean; + notificationSettings: NotificationSettings; +}; + +export const useSettings = defineStore('settings', () => { + const store = useTauriStore().getTarget('settings'); + const defaultSettings: VTsuruClientSettings = { + useDanmakuClientType: 'openlive', + fallbackToOpenLive: true, + + danmakuHistorySize: 100, + loginType: 'qrcode', + enableNotification: true, + notificationSettings: { + enableTypes: ['question-box', 'danmaku'], + }, + }; + const settings = ref(Object.assign({}, defaultSettings)); + + async function init() { + settings.value = (await store.get()) || Object.assign({}, defaultSettings); + settings.value.notificationSettings ??= defaultSettings.notificationSettings; + settings.value.notificationSettings.enableTypes ??= [ 'question-box', 'danmaku' ]; + } + async function save() { + await store.set(settings.value); + } + + return { init, save, settings }; +}); + +if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useSettings, import.meta.hot)); diff --git a/src/client/store/useTauriStore.ts b/src/client/store/useTauriStore.ts new file mode 100644 index 0000000..ee3f3f3 --- /dev/null +++ b/src/client/store/useTauriStore.ts @@ -0,0 +1,53 @@ +import { LazyStore } from '@tauri-apps/plugin-store'; + +export class StoreTarget { + constructor(key: string, target: LazyStore, defaultValue?: T) { + this.target = target; + this.key = key; + this.defaultValue = defaultValue; + } + protected target: LazyStore; + protected defaultValue: T | undefined; + + protected key: string; + + async set(value: T) { + return await this.target.set(this.key, value); + } + async get(): Promise { + const result = await this.target.get(this.key); + + if (result === undefined && this.defaultValue !== undefined) { + await this.set(this.defaultValue); + return this.defaultValue as T; + } + return result; + } + + async delete() { + return await this.target.delete(this.key); + } +} + +export const useTauriStore = defineStore('tauri', () => { + const store = new LazyStore('vtsuru.data.json', { + autoSave: true, + }); + async function set(key: string, value: any) { + await store.set(key, value); + } + async function get(key: string) { + return await store.get(key); + } + function getTarget(key: string, defaultValue?: T) { + return new StoreTarget(key, store, defaultValue); + } + return { + store, + set, + get, + getTarget, + }; +}); + +if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useTauriStore, import.meta.hot)); diff --git a/src/components.d.ts b/src/components.d.ts index 416af54..42de775 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -17,10 +17,29 @@ declare module 'vue' { FeedbackItem: typeof import('./components/FeedbackItem.vue')['default'] LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default'] MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default'] + NAlert: typeof import('naive-ui')['NAlert'] + NAvatar: typeof import('naive-ui')['NAvatar'] + NButton: typeof import('naive-ui')['NButton'] + NCard: typeof import('naive-ui')['NCard'] + NCheckbox: typeof import('naive-ui')['NCheckbox'] + NFlex: typeof import('naive-ui')['NFlex'] NFormItemG: typeof import('naive-ui')['NFormItemG'] NFormItemGi: typeof import('naive-ui')['NFormItemGi'] + NGridItem: typeof import('naive-ui')['NGridItem'] + NH4: typeof import('naive-ui')['NH4'] NIcon: typeof import('naive-ui')['NIcon'] + NImage: typeof import('naive-ui')['NImage'] + NInput: typeof import('naive-ui')['NInput'] + NLayoutContent: typeof import('naive-ui')['NLayoutContent'] + NPopconfirm: typeof import('naive-ui')['NPopconfirm'] + NRadioButton: typeof import('naive-ui')['NRadioButton'] + NRadioGroup: typeof import('naive-ui')['NRadioGroup'] + NScrollbar: typeof import('naive-ui')['NScrollbar'] + NSpace: typeof import('naive-ui')['NSpace'] + NTab: typeof import('naive-ui')['NTab'] NTag: typeof import('naive-ui')['NTag'] + NText: typeof import('naive-ui')['NText'] + NTooltip: typeof import('naive-ui')['NTooltip'] PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default'] PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default'] PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default'] diff --git a/src/components/TempComponent.vue b/src/components/TempComponent.vue index e5e90d8..5571540 100644 --- a/src/components/TempComponent.vue +++ b/src/components/TempComponent.vue @@ -15,6 +15,7 @@ onMounted(() => { window.$message = useMessage() window.$route = useRoute() window.$modal = useModal() + window.$notification = useNotification() const providerStore = useLoadingBarStore() providerStore.setLoadingBar(window.$loadingBar) }) diff --git a/src/data/DanmakuClients/BaseDanmakuClient.ts b/src/data/DanmakuClients/BaseDanmakuClient.ts index beccdaf..352ee4d 100644 --- a/src/data/DanmakuClients/BaseDanmakuClient.ts +++ b/src/data/DanmakuClients/BaseDanmakuClient.ts @@ -12,6 +12,7 @@ export default abstract class BaseDanmakuClient { 'padding' public abstract type: 'openlive' | 'direct' + public abstract serverUrl: string public eventsAsModel: { danmaku: ((arg1: EventModel, arg2?: any) => void)[] @@ -118,6 +119,7 @@ export default abstract class BaseDanmakuClient { this.client.close() this.client = null } + this.serverUrl = chatClient.connection.ws.ws.url return { success: !isError, message: errorMsg diff --git a/src/data/DanmakuClients/DirectClient.ts b/src/data/DanmakuClients/DirectClient.ts index b2ade79..c9652e2 100644 --- a/src/data/DanmakuClients/DirectClient.ts +++ b/src/data/DanmakuClients/DirectClient.ts @@ -11,6 +11,7 @@ export type DirectClientAuthInfo = { * 未实现除raw事件外的所有事件 */ export default class DirectClient extends BaseDanmakuClient { + public serverUrl: string = 'wss://broadcastlv.chat.bilibili.com/sub'; public onDanmaku(command: any): void { throw new Error('Method not implemented.') } diff --git a/src/data/DanmakuClients/OpenLiveClient.ts b/src/data/DanmakuClients/OpenLiveClient.ts index f9d6ade..3d5fe66 100644 --- a/src/data/DanmakuClients/OpenLiveClient.ts +++ b/src/data/DanmakuClients/OpenLiveClient.ts @@ -7,6 +7,7 @@ import { OPEN_LIVE_API_URL } from '../constants' import BaseDanmakuClient from './BaseDanmakuClient' export default class OpenLiveClient extends BaseDanmakuClient { + public serverUrl: string = ''; constructor(auth?: AuthInfo) { super() this.authInfo = auth diff --git a/src/main.ts b/src/main.ts index 1361c93..d5c68cb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,10 @@ import { useAuthStore } from './store/useAuthStore' import { useNotificationStore } from './store/useNotificationStore' const pinia = createPinia() +export const getPinia = () => pinia + +const app = createApp(App) +app.use(router).use(pinia).mount('#app') QueryGetAPI(`${BASE_API_URL}vtsuru/version`) .then((version) => { @@ -122,9 +126,6 @@ QueryGetAPI(`${BASE_API_URL}vtsuru/version`) UpdateAccountLoop() }) -const app = createApp(App) -app.use(router).use(pinia).mount('#app') - let currentVersion: string let isHaveNewVersion = false diff --git a/src/router/client.ts b/src/router/client.ts new file mode 100644 index 0000000..9a902cd --- /dev/null +++ b/src/router/client.ts @@ -0,0 +1,38 @@ +export default { + path: '/client', + name: 'client', + children: [ + { + path: '', + name: 'client-index', + component: () => import('@/client/ClientIndex.vue'), + meta: { + title: '首页', + } + }, + { + path: 'fetcher', + name: 'client-fetcher', + component: () => import('@/client/ClientFetcher.vue'), + meta: { + title: 'EventFetcher', + } + }, + { + path: 'settings', + name: 'client-settings', + component: () => import('@/client/ClientSettings.vue'), + meta: { + title: '设置', + } + }, + { + path: 'test', + name: 'client-test', + component: () => import('@/client/ClientTest.vue'), + meta: { + title: '测试', + } + }, + ] +} diff --git a/src/router/index.ts b/src/router/index.ts index 076ffdb..59c1a25 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,6 +6,7 @@ import user from './user' import obs from './obs' import open_live from './open_live' import singlePage from './singlePage' +import client from './client'; const routes: Array = [ { @@ -88,6 +89,7 @@ const routes: Array = [ manage, obs, open_live, + client, { path: '/@:id', name: 'user', diff --git a/src/store/useWebFetcher.ts b/src/store/useWebFetcher.ts index 04fdeff..bf35b89 100644 --- a/src/store/useWebFetcher.ts +++ b/src/store/useWebFetcher.ts @@ -1,270 +1,402 @@ -import { BASE_HUB_URL } from '@/data/constants' -import BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient' -import DirectClient, { - DirectClientAuthInfo -} from '@/data/DanmakuClients/DirectClient' -import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient' -import * as signalR from '@microsoft/signalr' -import * as msgpack from '@microsoft/signalr-protocol-msgpack' -import { useLocalStorage } from '@vueuse/core' -import { format } from 'date-fns' -import { defineStore } from 'pinia' -import { computed, ref } from 'vue' -import { useRoute } from 'vue-router' -import { compress } from 'brotli-compress' +import { defineStore } from 'pinia'; +import { ref, computed, shallowRef } from 'vue'; // shallowRef 用于非深度响应对象 +import { useLocalStorage } from '@vueuse/core'; +import { useRoute } from 'vue-router'; +import { compress } from 'brotli-compress'; +import { format } from 'date-fns'; +import * as signalR from '@microsoft/signalr'; +import * as msgpack from '@microsoft/signalr-protocol-msgpack'; +import { useAccount } from '@/api/account'; // 假设账户信息路径 +import { BASE_HUB_URL, isDev } from '@/data/constants'; // 假设常量路径 +import BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient'; // 假设弹幕客户端基类路径 +import DirectClient, { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient'; // 假设直连客户端路径 +import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient'; // 假设开放平台客户端路径 +import { error as logError, info as logInfo } from '@tauri-apps/plugin-log'; // 使用日志插件 +import { getEventType, recordEvent, streamingInfo } from '@/client/data/info'; +import { useWebRTC } from './useRTC'; export const useWebFetcher = defineStore('WebFetcher', () => { - const cookie = useLocalStorage('JWT_Token', '') - const route = useRoute() - const startedAt = ref() + const cookie = useLocalStorage('JWT_Token', ''); + const route = useRoute(); + const account = useAccount(); + const rtc = useWebRTC(); + const webfetcherType = ref<'openlive' | 'direct'>('openlive'); // 弹幕客户端类型 + // --- 连接与状态 --- + const state = ref<'disconnected' | 'connecting' | 'connected'>('disconnected'); // SignalR 连接状态 + const startedAt = ref(); // 本次启动时间 + const signalRClient = shallowRef(); // SignalR 客户端实例 (浅响应) + const client = shallowRef(); // 弹幕客户端实例 (浅响应) + let timer: any; // 事件发送定时器 + let disconnectedByServer = false; + let isFromClient = false; // 是否由Tauri客户端启动 - const client = ref() - const signalRClient = ref() + // --- 新增: 详细状态与信息 --- + /** 弹幕客户端内部状态 */ + const danmakuClientState = ref<'stopped' | 'connecting' | 'connected'>('stopped'); // 更详细的弹幕客户端状态 + /** 弹幕服务器连接地址 */ + const danmakuServerUrl = ref(); + /** SignalR 连接 ID */ + const signalRConnectionId = ref(); + // const heartbeatLatency = ref(null); // 心跳延迟暂不实现,复杂度较高 + + // --- 事件处理 --- + const events: string[] = []; // 待发送事件队列 + + // --- 新增: 会话统计 (在 Start 时重置) --- + /** 本次会话处理的总事件数 */ + const sessionEventCount = ref(0); + /** 本次会话各类型事件计数 */ + const sessionEventTypeCounts = ref<{ [key: string]: number; }>({}); + /** 本次会话成功上传次数 */ + const successfulUploads = ref(0); + /** 本次会话失败上传次数 */ + const failedUploads = ref(0); + /** 本次会话发送的总字节数 (压缩后) */ + const bytesSentSession = ref(0); + + const prefix = computed(() => isFromClient ? '[web-fetcher-iframe] ' : '[web-fetcher] '); - const events: string[] = [] - const isStarted = ref(false) - let timer: any - let disconnectedByServer = false - let useCookie = false /** - * 是否来自Tauri客户端 + * 启动 WebFetcher 服务 */ - let isFromClient = false - const prefix = computed(() => { - if (isFromClient) { - return '[web-fetcher-iframe] ' - } - return '[web-fetcher] ' - }) - async function restartDanmakuClient( - type: 'openlive' | 'direct', - directAuthInfo?: DirectClientAuthInfo - ) { - console.log(prefix.value + '正在重启弹幕客户端...') - if ( - client.value?.state === 'connected' || - client.value?.state === 'connecting' - ) { - client.value.Stop() - } - return await connectDanmakuClient(type, directAuthInfo) - } async function Start( type: 'openlive' | 'direct' = 'openlive', directAuthInfo?: DirectClientAuthInfo, _isFromClient: boolean = false - ): Promise<{ success: boolean; message: string }> { - if (isStarted.value) { - startedAt.value = new Date() - return { success: true, message: '已启动' } + ): Promise<{ success: boolean; message: string; }> { + if (state.value === 'connected' || state.value === 'connecting') { + logInfo(prefix.value + '已经启动,无需重复启动'); + return { success: true, message: '已启动' }; } - const result = await navigator.locks.request( - 'webFetcherStart', - async () => { - isFromClient = _isFromClient - while (!(await connectSignalR())) { - console.log(prefix.value + '连接失败, 5秒后重试') - await new Promise((resolve) => setTimeout(resolve, 5000)) - } - let result = await connectDanmakuClient(type, directAuthInfo) - while (!result?.success) { - console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试') - await new Promise((resolve) => setTimeout(resolve, 5000)) - result = await connectDanmakuClient(type, directAuthInfo) - } - isStarted.value = true - disconnectedByServer = false - return result + webfetcherType.value = type; // 设置弹幕客户端类型 + // 重置会话统计数据 + resetSessionStats(); + startedAt.value = new Date(); + isFromClient = _isFromClient; + state.value = 'connecting'; // 设置为连接中状态 + + // 使用 navigator.locks 确保同一时间只有一个 Start 操作执行 + const result = await navigator.locks.request('webFetcherStartLock', async () => { + logInfo(prefix.value + '开始启动...'); + while (!(await connectSignalR())) { + logInfo(prefix.value + '连接 SignalR 失败, 5秒后重试'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + // 如果用户手动停止,则退出重试循环 + if (state.value === 'disconnected') return { success: false, message: '用户手动停止' }; } - ) - return result - } - function Stop() { - if (!isStarted.value) { - return + + let danmakuResult = await connectDanmakuClient(type, directAuthInfo); + while (!danmakuResult?.success) { + logInfo(prefix.value + '弹幕客户端启动失败, 5秒后重试'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + // 如果用户手动停止,则退出重试循环 + if (state.value === 'disconnected') return { success: false, message: '用户手动停止' }; + danmakuResult = await connectDanmakuClient(type, directAuthInfo); + } + + // 只有在两个连接都成功后才设置为 connected + state.value = 'connected'; + disconnectedByServer = false; + logInfo(prefix.value + '启动成功'); + return { success: true, message: '启动成功' }; + }); + + // 如果启动过程中因为手动停止而失败,需要确保状态是 disconnected + if (!result.success) { + Stop(); // 确保清理资源 + return { success: false, message: result.message || '启动失败' }; } - isStarted.value = false - client.value?.Stop() - client.value = undefined - if (timer) { - clearInterval(timer) - timer = undefined - } - signalRClient.value?.stop() - signalRClient.value = undefined - startedAt.value = undefined + + return result; } - /************* ✨ Codeium Command ⭐ *************/ + /** - * Connects to the danmaku client based on the specified type. - * - * @param type - The type of danmaku client to connect, either 'openlive' or 'direct'. - * @param directConnectInfo - Optional authentication information required when connecting to a 'direct' type client. - * It should include a token, roomId, tokenUserId, and buvid. - * - * @returns A promise that resolves to an object containing a success flag and a message. - * If the connection and client start are successful, the client starts listening to danmaku events. - * If the connection fails or the authentication information is not provided for a 'direct' type client, - * the function returns with a failure message. + * 停止 WebFetcher 服务 + */ + function Stop() { + if (state.value === 'disconnected') return; + + logInfo(prefix.value + '正在停止...'); + state.value = 'disconnected'; // 立即设置状态,防止重连逻辑触发 + + // 清理定时器 + if (timer) { clearInterval(timer); timer = undefined; } + + // 停止弹幕客户端 + client.value?.Stop(); + client.value = undefined; + danmakuClientState.value = 'stopped'; + danmakuServerUrl.value = undefined; + + // 停止 SignalR 连接 + signalRClient.value?.stop(); + signalRClient.value = undefined; + signalRConnectionId.value = undefined; + + // 清理状态 + startedAt.value = undefined; + events.length = 0; // 清空事件队列 + // resetSessionStats(); // 会话统计在下次 Start 时重置 + + logInfo(prefix.value + '已停止'); + } + + /** 重置会话统计数据 */ + function resetSessionStats() { + sessionEventCount.value = 0; + sessionEventTypeCounts.value = {}; + successfulUploads.value = 0; + failedUploads.value = 0; + bytesSentSession.value = 0; + } + + /** + * 连接弹幕客户端 */ - /****** 3431380f-29f6-41b0-801a-7f081b59b4ff *******/ async function connectDanmakuClient( type: 'openlive' | 'direct', - directConnectInfo?: { - token: string - roomId: number - tokenUserId: number - buvid: string - } + directConnectInfo?: DirectClientAuthInfo ) { - if ( - client.value?.state === 'connected' || - client.value?.state === 'connecting' - ) { - return { success: true, message: '弹幕客户端已启动' } + if (client.value?.state === 'connected' || client.value?.state === 'connecting') { + logInfo(prefix.value + '弹幕客户端已连接或正在连接'); + return { success: true, message: '弹幕客户端已启动' }; } - console.log(prefix.value + '正在连接弹幕客户端...') + + logInfo(prefix.value + '正在连接弹幕客户端...'); + danmakuClientState.value = 'connecting'; + + // 如果实例存在但已停止,先清理 + if (client.value?.state === 'disconnected') { + client.value = undefined; + } + + // 创建实例并添加事件监听 (仅在首次创建时) if (!client.value) { - //只有在没有客户端的时候才创建, 并添加事件 - if (type == 'openlive') { - client.value = new OpenLiveClient() + if (type === 'openlive') { + client.value = new OpenLiveClient(); } else { if (!directConnectInfo) { - return { success: false, message: '未提供弹幕客户端认证信息' } + danmakuClientState.value = 'stopped'; + logError(prefix.value + '未提供直连弹幕客户端认证信息'); + return { success: false, message: '未提供弹幕客户端认证信息' }; } - client.value = new DirectClient(directConnectInfo) + client.value = new DirectClient(directConnectInfo); + // 直连地址通常包含 host 和 port,可以从 directConnectInfo 获取 + //danmakuServerUrl.value = `${directConnectInfo.host}:${directConnectInfo.port}`; } - client.value?.on('all', (data) => onGetDanmakus(data)) + // 监听所有事件,用于处理和转发 + client.value?.on('all', onGetDanmakus); } - const result = await client.value?.Start() + + // 启动客户端连接 + const result = await client.value?.Start(); + if (result?.success) { - console.log(prefix.value + '加载完成, 开始监听弹幕') - timer ??= setInterval(() => { - sendEvents() - }, 1500) + logInfo(prefix.value + '弹幕客户端连接成功, 开始监听弹幕'); + danmakuClientState.value = 'connected'; // 明确设置状态 + danmakuServerUrl.value = client.value.serverUrl; // 获取服务器地址 + // 启动事件发送定时器 (如果之前没有启动) + timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件 } else { - console.log(prefix.value + '弹幕客户端启动失败: ' + result?.message) + logError(prefix.value + '弹幕客户端启动失败: ' + result?.message); + danmakuClientState.value = 'stopped'; + danmakuServerUrl.value = undefined; + client.value = undefined; // 启动失败,清理实例,下次会重建 } - return result + return result; } + + /** + * 连接 SignalR 服务器 + */ async function connectSignalR() { - console.log(prefix.value + '正在连接到 vtsuru 服务器...') + if (signalRClient.value && signalRClient.value.state !== signalR.HubConnectionState.Disconnected) { + logInfo(prefix.value + "SignalR 已连接或正在连接"); + return true; + } + + logInfo(prefix.value + '正在连接到 vtsuru 服务器...'); const connection = new signalR.HubConnectionBuilder() - .withUrl(BASE_HUB_URL + 'web-fetcher?token=' + route.query.token, { - headers: { - Authorization: `Bearer ${cookie.value}` - }, + .withUrl(BASE_HUB_URL + 'web-fetcher?token=' + (route.query.token ?? account.value.token), { // 使用 account.token + headers: { Authorization: `Bearer ${cookie.value}` }, skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }) - .withAutomaticReconnect([0, 2000, 10000, 30000]) - .withHubProtocol(new msgpack.MessagePackHubProtocol()) - .build() - connection.on('Disconnect', (reason: unknown) => { - console.log(prefix.value + '被服务器断开连接: ' + reason) - disconnectedByServer = true - connection.stop() - signalRClient.value = undefined - }) - /*connection.on('ConnectClient', async () => { - if (client?.state === 'connected') { - return - } - let result = await connectDanmakuClient() - while (!result?.success) { - console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试') - await new Promise((resolve) => setTimeout(resolve, 5000)) - result = await connectDanmakuClient() - } - isStarted.value = true - disconnectedByServer = false - })*/ + .withAutomaticReconnect([0, 2000, 10000, 30000]) // 自动重连策略 + .withHubProtocol(new msgpack.MessagePackHubProtocol()) // 使用 MessagePack 协议 + .build(); - connection.onclose(reconnect) - try { - await connection.start() - console.log(prefix.value + '已连接到 vtsuru 服务器') - await connection.send('Finished') + // --- SignalR 事件监听 --- + connection.onreconnecting(error => { + logInfo(prefix.value + `与服务器断开,正在尝试重连... ${error?.message || ''}`); + state.value = 'connecting'; // 更新状态为连接中 + signalRConnectionId.value = undefined; // 连接断开,ID失效 + }); + + connection.onreconnected(connectionId => { + logInfo(prefix.value + `与服务器重新连接成功! ConnectionId: ${connectionId}`); + signalRConnectionId.value = connectionId ?? undefined; + state.value = 'connected'; // 更新状态为已连接 + // 重连成功后可能需要重新发送标识 if (isFromClient) { - // 如果来自Tauri客户端,设置自己为VTsuru客户端 - await connection.send('SetAsVTsuruClient') + connection.send('SetAsVTsuruClient').catch(err => logError(prefix.value + "Send SetAsVTsuruClient failed: " + err)); } - signalRClient.value = connection - return true - } catch (e) { - console.log(prefix.value + '无法连接到 vtsuru 服务器: ' + e) - return false - } - } - async function reconnect() { - if (disconnectedByServer) { - return - } - try { - await signalRClient.value?.start() - await signalRClient.value?.send('Reconnected') - if (isFromClient) { - await signalRClient.value?.send('SetAsVTsuruClient') - } - console.log(prefix.value + '已重新连接') - } catch (err) { - console.log(err) - setTimeout(reconnect, 5000) // 如果连接失败,则每5秒尝试一次重新启动连接 - } - } - function onGetDanmakus(command: any) { - events.push(command) - } - async function sendEvents() { - if (signalRClient.value?.state !== 'Connected') { - return - } - let tempEvents: string[] = [] - let count = events.length - if (events.length > 20) { - tempEvents = events.slice(0, 20) - count = 20 - } else { - tempEvents = events - count = events.length - } - if (tempEvents.length > 0) { - const compressed = await compress( - new TextEncoder().encode( - JSON.stringify({ - Events: tempEvents.map((e) => - typeof e === 'string' ? e : JSON.stringify(e) - ), - Version: '1.0.0', - OSInfo: navigator.userAgent, - UseCookie: useCookie - }) - ) - ) - const result = await signalRClient.value?.invoke<{ - Success: boolean - Message: string - }>('UploadEventsCompressed', compressed) - if (result?.Success) { - events.splice(0, count) - console.log( - `[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕` - ) + connection.send('Reconnected').catch(err => logError(prefix.value + "Send Reconnected failed: " + err)); + }); + + connection.onclose(async (error) => { + // 只有在不是由 Stop() 或服务器明确要求断开时才记录错误并尝试独立重连(虽然 withAutomaticReconnect 应该处理) + if (state.value !== 'disconnected' && !disconnectedByServer) { + logError(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 自动重连将处理.`); + state.value = 'connecting'; // 标记为连接中,等待自动重连 + signalRConnectionId.value = undefined; + // withAutomaticReconnect 会处理重连,这里不需要手动调用 reconnect + } else if (disconnectedByServer) { + logInfo(prefix.value + `连接已被服务器关闭.`); + Stop(); // 服务器要求断开,则彻底停止 } else { - console.error(prefix.value + '上传弹幕失败: ' + result?.Message) + logInfo(prefix.value + `连接已手动关闭.`); } + }); + + connection.on('Disconnect', (reason: unknown) => { + logInfo(prefix.value + '被服务器断开连接: ' + reason); + disconnectedByServer = true; // 标记是服务器主动断开 + Stop(); // 服务器要求断开,调用 Stop 清理所有资源 + }); + + // --- 尝试启动连接 --- + try { + await connection.start(); + logInfo(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + connection.connectionId); + console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + connection.connectionId); // 调试输出连接状态 + signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID + await connection.send('Finished'); // 通知服务器已准备好 + if (isFromClient) { + await connection.send('SetAsVTsuruClient'); // 如果是客户端,发送标识 + } + signalRClient.value = connection; // 保存实例 + // state.value = 'connected'; // 状态将在 Start 函数末尾统一设置 + return true; + } catch (e) { + logError(prefix.value + '无法连接到 vtsuru 服务器: ' + e); + signalRConnectionId.value = undefined; + signalRClient.value = undefined; + // state.value = 'disconnected'; // 保持 connecting 或由 Start 控制 + return false; } } + // async function reconnect() { // withAutomaticReconnect 存在时,此函数通常不需要手动调用 + // if (disconnectedByServer || state.value === 'disconnected') return; + // logInfo(prefix.value + '尝试手动重连...'); + // try { + // await signalRClient.value?.start(); + // logInfo(prefix.value + '手动重连成功'); + // signalRConnectionId.value = signalRClient.value?.connectionId ?? null; + // state.value = 'connected'; + // if (isFromClient) { + // await signalRClient.value?.send('SetAsVTsuruClient'); + // } + // await signalRClient.value?.send('Reconnected'); + // } catch (err) { + // logError(prefix.value + '手动重连失败: ' + err); + // setTimeout(reconnect, 10000); // 失败后10秒再次尝试 + // } + // } + + /** + * 接收到弹幕事件时的处理函数 + */ + function onGetDanmakus(command: any) { + if (isFromClient) { + // 1. 解析事件类型 + const eventType = getEventType(command); + + // 2. 记录到每日统计 (调用 statistics 模块) + recordEvent(eventType); + + // 3. 更新会话统计 + sessionEventCount.value++; + sessionEventTypeCounts.value[eventType] = (sessionEventTypeCounts.value[eventType] || 0) + 1; + } + // 4. 加入待发送队列 (确保是字符串) + const eventString = typeof command === 'string' ? command : JSON.stringify(command); + if (isDev) { + //console.log(prefix.value + '收到弹幕事件: ' + eventString); // 开发模式下打印所有事件 (可选) + } + if (events.length >= 10000) { + events.shift(); // 如果队列过长,移除最旧的事件 + } + events.push(eventString); + } + + /** + * 定期将队列中的事件发送到服务器 + */ + async function sendEvents() { + // 确保 SignalR 已连接 + if (!signalRClient.value || signalRClient.value.state !== signalR.HubConnectionState.Connected) { + return; + } + // 如果没有事件,则不发送 + if (events.length === 0) { + return; + } + + // 批量处理事件,每次最多发送20条 + const batchSize = 20; + const batch = events.slice(0, batchSize); + + try { + + const result = await signalRClient.value.invoke<{ Success: boolean; Message: string; }>( + 'UploadEvents', batch, webfetcherType.value === 'direct'? true : false + ); + if (result?.Success) { + events.splice(0, batch.length); // 从队列中移除已成功发送的事件 + successfulUploads.value++; + bytesSentSession.value += new TextEncoder().encode(batch.join()).length; + } else { + failedUploads.value++; + logError(prefix.value + '上传弹幕失败: ' + result?.Message); + } + } catch (err) { + failedUploads.value++; + logError(prefix.value + '发送事件时出错: ' + err); + } + } + + // --- 暴露给外部使用的状态和方法 --- return { Start, Stop, - restartDanmakuClient, - client, - signalRClient, - isStarted, - startedAt - } -}) + // restartDanmakuClient, // 如果需要重启单独的弹幕客户端,可以保留或实现 + + // 状态 + state, // Overall SignalR state + startedAt, + isStreaming: computed(() => streamingInfo.value?.status === 'streaming'), // 从 statistics 模块获取 + webfetcherType, + + // 连接详情 + danmakuClientState, + danmakuServerUrl, + //signalRConnectionId, + // heartbeatLatency, // 暂不暴露 + + // 会话统计 + sessionEventCount, + sessionEventTypeCounts, + successfulUploads, + failedUploads, + bytesSentSession, + + // 实例 (谨慎暴露,主要用于调试或特定场景) + signalRClient: computed(() => signalRClient.value), // 返回计算属性以防直接修改 + client: computed(() => client.value), + + }; +}); \ No newline at end of file diff --git a/src/views/client/ClientLayout.vue b/src/views/client/ClientLayout.vue deleted file mode 100644 index 2f6cd87..0000000 --- a/src/views/client/ClientLayout.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - \ No newline at end of file diff --git a/src/views/obs/WebFetcherOBS.vue b/src/views/obs/WebFetcherOBS.vue index e0fca9e..438ffa1 100644 --- a/src/views/obs/WebFetcherOBS.vue +++ b/src/views/obs/WebFetcherOBS.vue @@ -31,7 +31,7 @@ onMounted(async () => { rpc.expose('status', () => { return { - status: webFetcher.isStarted ? 'running' : 'stopped', + status: webFetcher.state, type: webFetcher.client?.type, roomId: webFetcher.client instanceof OpenLiveClient ? webFetcher.client.roomAuthInfo?.anchor_info.room_id : @@ -44,7 +44,8 @@ onMounted(async () => { rpc.expose('start', async (data: { type: 'openlive' | 'direct', directAuthInfo?: DirectClientAuthInfo, force: boolean }) => { console.log('[web-fetcher-iframe] 接收到 ' + (data.force ? '强制' : '') + '启动请求') - if (data.force && webFetcher.isStarted) { + if (data.force && webFetcher.state === 'connected') { + console.log('[web-fetcher-iframe] 强制启动, 停止当前实例') webFetcher.Stop() } return await webFetcher.Start(data.type, data.directAuthInfo, true).then((result) => { @@ -73,9 +74,9 @@ onMounted(async () => { } setTimeout(() => { // @ts-expect-error obs的东西 - if (!webFetcher.isStarted && window.obsstudio) { + if (webFetcher.state !== 'connected' && window.obsstudio) { timer = setInterval(() => { - if (webFetcher.isStarted) { + if (webFetcher.state === 'connected') { return } @@ -99,7 +100,7 @@ onUnmounted(() => {

@@ -1116,7 +1117,7 @@ html.dark .song-list-container { /* min-height: 200px; */ /* Might not be needed if max-height is set */ border-radius: 8px; /* Scrollbar styling specific to this inner table scroll if needed */ - /* ... */ + scroll-behavior: smooth; }