From 7d35fe286dcdfbfa98074cddbe07b6e5595893dc Mon Sep 17 00:00:00 2001 From: Megghy Date: Thu, 19 Dec 2024 10:18:05 +0800 Subject: [PATCH] progessing on client --- bun.lockb | Bin 499234 -> 521919 bytes package.json | 48 +- src/api/account.ts | 2 +- src/data/DanmakuClients/BaseDanmakuClient.ts | 175 +++++ src/data/DanmakuClients/DirectClient.ts | 65 ++ .../OpenLiveClient.ts} | 665 ++++++++---------- src/data/chat/ChatClientDirectOpenLive.js | 9 +- src/data/chat/ChatClientDirectWeb.js | 176 +++++ .../chat/ChatClientOfficialBase/index.js.new | 374 ++++++++++ .../obs/blivechat => data/chat}/models.js | 30 +- src/store/useDanmakuClient.ts | 19 +- src/store/useDirectDanmakuClient.ts | 234 ++++++ src/store/useWebFetcher.ts | 201 ++++-- src/views/IndexView.vue | 6 +- src/views/obs/DanmujiOBS.vue | 2 +- src/views/obs/LiveRequestOBS.vue | 6 +- src/views/obs/WebFetcherOBS.vue | 81 ++- src/views/obs/blivechat/ImgShadow.vue | 2 +- src/views/open_live/MusicRequest.vue | 2 +- src/views/open_live/OpenLiveIndex.vue | 2 +- src/views/open_live/OpenLottery.vue | 2 +- src/views/open_live/OpenQueue.vue | 2 +- src/views/open_live/ReadDanmaku.vue | 2 +- tsconfig.json | 23 +- vite.config.mts | 10 +- 25 files changed, 1634 insertions(+), 504 deletions(-) create mode 100644 src/data/DanmakuClients/BaseDanmakuClient.ts create mode 100644 src/data/DanmakuClients/DirectClient.ts rename src/data/{DanmakuClient.ts => DanmakuClients/OpenLiveClient.ts} (66%) create mode 100644 src/data/chat/ChatClientDirectWeb.js create mode 100644 src/data/chat/ChatClientOfficialBase/index.js.new rename src/{views/obs/blivechat => data/chat}/models.js (79%) create mode 100644 src/store/useDirectDanmakuClient.ts diff --git a/bun.lockb b/bun.lockb index 48281884946f6c764e990d29dc9c0dcbc33429c1..e1c531f36eae7540e778db501f9483368e5c0ab9 100644 GIT binary patch delta 111240 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 diff --git a/package.json b/package.json index 427ef01..a66c73c 100644 --- a/package.json +++ b/package.json @@ -11,22 +11,24 @@ "dependencies": { "@microsoft/signalr": "^8.0.7", "@microsoft/signalr-protocol-msgpack": "^8.0.7", - "@types/node": "^22.9.0", - "@typescript-eslint/eslint-plugin": "^8.13.0", + "@mixer/postmessage-rpc": "^1.1.4", + "@typescript-eslint/eslint-plugin": "^8.17.0", "@vicons/fluent": "^0.12.0", - "@vitejs/plugin-basic-ssl": "^1.1.0", - "@vitejs/plugin-vue": "^5.1.4", + "@vitejs/plugin-basic-ssl": "^1.2.0", + "@vitejs/plugin-vue": "^5.2.1", "@vue/cli": "^5.0.8", - "@vueuse/core": "^11.2.0", - "@vueuse/router": "^11.2.0", + "@vueuse/core": "^12.0.0", + "@vueuse/router": "^12.0.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", + "bilibili-live-ws": "^6.3.1", + "brotli-compress": "^1.3.3", "date-fns": "^4.1.0", "easy-speech": "^2.4.0", "echarts": "^5.5.1", - "eslint": "^9.14.0", + "eslint": "^9.16.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-oxlint": "^0.11.0", + "eslint-plugin-oxlint": "^0.14.0", "eslint-plugin-prettier": "^5.2.1", "fast-xml-parser": "^4.5.0", "file-saver": "^2.0.5", @@ -37,19 +39,19 @@ "monaco-editor": "^0.52.0", "music-metadata-browser": "^2.5.11", "peerjs": "^1.5.4", - "pinia": "^2.2.6", - "prettier": "^3.3.3", + "pinia": "^2.2.8", + "prettier": "^3.4.1", "qrcode.vue": "^3.6.0", "queue-typescript": "^1.0.1", - "unplugin-vue-markdown": "^0.26.2", - "uuid": "^11.0.2", - "vite": "^5.4.10", + "unplugin-vue-markdown": "^0.27.1", + "uuid": "^11.0.3", + "vite": "5.4.11", "vite-plugin-monaco-editor": "^1.1.0", "vite-svg-loader": "^5.1.0", - "vue": "3.5.12", + "vue": "3.5.13", "vue-echarts": "^7.0.3", "vue-request": "^2.0.4", - "vue-router": "^4.4.5", + "vue-router": "^4.5.0", "vue-turnstile": "^1.0.11", "vue3-aplayer": "^1.7.3", "vue3-marquee": "^4.2.2", @@ -58,19 +60,19 @@ "xlsx": "^0.18.5" }, "devDependencies": { - "@eslint/eslintrc": "^3.1.0", - "@types/bun": "^1.1.13", + "@eslint/eslintrc": "^3.2.0", + "@types/bun": "^1.1.14", "@types/eslint": "^9.6.1", "@types/obs-studio": "^2.17.2", "@types/uuid": "^10.0.0", - "@typescript-eslint/parser": "^8.13.0", + "@typescript-eslint/parser": "^8.17.0", "@vicons/ionicons5": "^0.12.0", - "@vitejs/plugin-vue-jsx": "^4.0.1", - "@vue/eslint-config-typescript": "^14.1.3", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "@vue/eslint-config-typescript": "^14.1.4", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-vue": "^9.30.0", - "naive-ui": "^2.40.1", + "eslint-plugin-vue": "^9.32.0", + "naive-ui": "^2.40.3", "stylus": "^0.64.0", - "typescript": "^5.6.3" + "typescript": "^5.7.2" } } \ No newline at end of file diff --git a/src/api/account.ts b/src/api/account.ts index d5311fa..80b34fd 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -9,6 +9,7 @@ import { useRoute } from 'vue-router' export const ACCOUNT = ref({} as AccountInfo) export const isLoadingAccount = ref(true) +const route = useRoute() const { message } = createDiscreteApi(['message']) const cookie = useLocalStorage('JWT_Token', '') @@ -46,7 +47,6 @@ export async function GetSelfAccount() { } export function UpdateAccountLoop() { setInterval(() => { - const route = useRoute() if (ACCOUNT.value && route?.name != 'question-display') { // 防止在问题详情页刷新 GetSelfAccount() diff --git a/src/data/DanmakuClients/BaseDanmakuClient.ts b/src/data/DanmakuClients/BaseDanmakuClient.ts new file mode 100644 index 0000000..322c17c --- /dev/null +++ b/src/data/DanmakuClients/BaseDanmakuClient.ts @@ -0,0 +1,175 @@ +import { EventModel } from '@/api/api-models' +import { KeepLiveWS } from 'bilibili-live-ws/browser' + +export default abstract class BaseDanmakuClient { + constructor() { + this.client = null + } + + public client: KeepLiveWS | null + + public state: 'padding' | 'connected' | 'connecting' | 'disconnected' = + 'padding' + + public abstract type: 'openlive' | 'direct' + + public eventsAsModel: { + danmaku: ((arg1: EventModel, arg2?: any) => void)[] + gift: ((arg1: EventModel, arg2?: any) => void)[] + sc: ((arg1: EventModel, arg2?: any) => void)[] + guard: ((arg1: EventModel, arg2?: any) => void)[] + all: ((arg1: any) => void)[] + } = { + danmaku: [], + gift: [], + sc: [], + guard: [], + all: [] + } + + public async Start(): Promise<{ success: boolean; message: string }> { + if (this.state == 'connected') { + return { + success: true, + message: '弹幕客户端已启动' + } + } + if (this.state == 'connecting') { + return { + success: false, + message: '弹幕客户端正在启动' + } + } + this.state = 'connecting' + try { + if (!this.client) { + console.log(`[${this.type}] 正在启动弹幕客户端`) + const result = await this.initClient() + if (result.success) { + this.state = 'connected' + } + return result + } else { + console.warn(`[${this.type}] 弹幕客户端已被启动过`) + this.state = 'connected' + return { + success: false, + message: '弹幕客户端已被启动过' + } + } + } catch (err) { + console.error(err) + return { + success: false, + message: err ? err.toString() : '未知错误' + } + } + } + public Stop() { + if (this.state === 'disconnected') { + return + } + this.state = 'disconnected' + if (this.client) { + console.log(`[${this.type}] 正在停止弹幕客户端`) + this.client.close() + } else { + console.warn(`[${this.type}] 弹幕客户端未被启动, 忽略`) + } + this.eventsAsModel = { + danmaku: [], + gift: [], + sc: [], + guard: [], + all: [] + } + } + protected abstract initClient(): Promise<{ + success: boolean + message: string + }> + protected async initClientInner( + chatClient: KeepLiveWS + ): Promise<{ success: boolean; message: string }> { + let isConnected = false + let isError = false + let errorMsg = '' + chatClient.on('error', (err: any) => { + console.error(err) + isError = true + errorMsg = err + }) + chatClient.on('live', () => { + isConnected = true + }) + chatClient.on('close', () => { + console.log(`[${this.type}] 弹幕客户端已关闭`) + }) + chatClient.on('msg', (cmd) => this.onRawMessage(cmd)) + + this.client = chatClient + while (!isConnected && !isError) { + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + } + if (isError) { + this.client.close() + this.client = null + } + return { + success: !isError, + message: errorMsg + } + } + + public onRawMessage = (command: any) => { + this.eventsAsModel.all?.forEach((d) => { + d(command) + }) + } + + public abstract onDanmaku(command: any): void + public abstract onGift(command: any): void + public abstract onSC(command: any): void + public abstract onGuard(command: any): void + public on( + eventName: 'danmaku', + listener: (arg1: EventModel, arg2?: any) => void + ): this + public on( + eventName: 'gift', + listener: (arg1: EventModel, arg2?: any) => void + ): this + public on( + eventName: 'sc', + listener: (arg1: EventModel, arg2?: any) => void + ): this + public on( + eventName: 'guard', + listener: (arg1: EventModel, arg2?: any) => void + ): this + public on(eventName: 'all', listener: (arg1: any) => void): this + public on( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', + listener: (...args: any[]) => void + ): this { + if (!this.eventsAsModel[eventName]) { + this.eventsAsModel[eventName] = [] + } + this.eventsAsModel[eventName].push(listener) + return this + } + public off( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', + listener: (...args: any[]) => void + ): this { + if (this.eventsAsModel[eventName]) { + const index = this.eventsAsModel[eventName].indexOf(listener) + if (index > -1) { + this.eventsAsModel[eventName].splice(index, 1) + } + } + return this + } +} diff --git a/src/data/DanmakuClients/DirectClient.ts b/src/data/DanmakuClients/DirectClient.ts new file mode 100644 index 0000000..b2ade79 --- /dev/null +++ b/src/data/DanmakuClients/DirectClient.ts @@ -0,0 +1,65 @@ +import { KeepLiveWS } from 'bilibili-live-ws/browser' +import BaseDanmakuClient from './BaseDanmakuClient' +export type DirectClientAuthInfo = { + token: string + roomId: number + tokenUserId: number + buvid: string +} +/** 直播间弹幕客户端, 只能在vtsuru.client环境使用 + * + * 未实现除raw事件外的所有事件 + */ +export default class DirectClient extends BaseDanmakuClient { + public onDanmaku(command: any): void { + throw new Error('Method not implemented.') + } + public onGift(command: any): void { + throw new Error('Method not implemented.') + } + public onSC(command: any): void { + throw new Error('Method not implemented.') + } + public onGuard(command: any): void { + throw new Error('Method not implemented.') + } + constructor(auth: DirectClientAuthInfo) { + super() + this.authInfo = auth + } + + public type = 'direct' as const + + public readonly authInfo: DirectClientAuthInfo + + protected async initClient(): Promise<{ success: boolean; message: string }> { + if (this.authInfo) { + const chatClient = new KeepLiveWS(this.authInfo.roomId, { + key: this.authInfo.token, + buvid: this.authInfo.buvid, + uid: this.authInfo.tokenUserId, + protover: 3 + }) + + chatClient.on('live', () => { + console.log('[DIRECT] 已连接房间: ' + this.authInfo.roomId) + }) + /*chatClient.on('DANMU_MSG', this.onDanmaku) + chatClient.on('SEND_GIFT', this.onGift) + chatClient.on('GUARD_BUY', this.onGuard) + chatClient.on('SUPER_CHAT_MESSAGE', this.onSC) + chatClient.on('msg', (data) => { + this.events.all?.forEach((d) => { + d(data) + }) + })*/ + return await super.initClientInner(chatClient) + } else { + console.log('[DIRECT] 无法开启场次, 未提供弹幕客户端认证信息') + return { + success: false, + message: '未提供弹幕客户端认证信息' + } + } + } +} diff --git a/src/data/DanmakuClient.ts b/src/data/DanmakuClients/OpenLiveClient.ts similarity index 66% rename from src/data/DanmakuClient.ts rename to src/data/DanmakuClients/OpenLiveClient.ts index d33f1dd..66e97a6 100644 --- a/src/data/DanmakuClient.ts +++ b/src/data/DanmakuClients/OpenLiveClient.ts @@ -1,10 +1,284 @@ -import { EventDataTypes, EventModel, OpenLiveInfo } from '@/api/api-models' +import { EventDataTypes, OpenLiveInfo } from '@/api/api-models' import { QueryGetAPI, QueryPostAPI } from '@/api/query' -// @ts-expect-error 忽略js错误 -import ChatClientDirectOpenLive from '@/data/chat/ChatClientDirectOpenLive.js' import { GuidUtils } from '@/Utils' +import { KeepLiveWS } from 'bilibili-live-ws/browser' import { clearInterval, setInterval } from 'worker-timers' -import { OPEN_LIVE_API_URL } from './constants' +import { OPEN_LIVE_API_URL } from '../constants' +import BaseDanmakuClient from './BaseDanmakuClient' + +export default class OpenLiveClient extends BaseDanmakuClient { + constructor(auth?: AuthInfo) { + super() + this.authInfo = auth + this.events = { danmaku: [], gift: [], sc: [], guard: [], all: [] } + } + + public type = 'openlive' as const + + private timer: any | undefined + + public authInfo: AuthInfo | undefined + public roomAuthInfo: RoomAuthInfo | undefined + public authCode: string | undefined + + public events: { + danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[] + gift: ((arg1: GiftInfo, arg2?: any) => void)[] + sc: ((arg1: SCInfo, arg2?: any) => void)[] + guard: ((arg1: GuardInfo, arg2?: any) => void)[] + all: ((arg1: any) => void)[] + } + + public async Start(): Promise<{ success: boolean; message: string }> { + const result = await super.Start() + if (result.success) { + this.timer ??= setInterval(() => { + this.sendHeartbeat() + }, 20 * 1000) + } + return result + } + public Stop() { + super.Stop() + this.events = { + danmaku: [], + gift: [], + sc: [], + guard: [], + all: [] + } + } + + protected async initClient(): Promise<{ success: boolean; message: string }> { + const auth = await this.getAuthInfo() + if (auth.data) { + const chatClient = new KeepLiveWS(auth.data.anchor_info.room_id, { + authBody: JSON.parse(auth.data.websocket_info.auth_body), + address: auth.data.websocket_info.wss_link[0] + }) + chatClient.on('LIVE_OPEN_PLATFORM_DM', (cmd) => this.onDanmaku(cmd)) + chatClient.on('LIVE_OPEN_PLATFORM_GIFT', (cmd) => this.onGift(cmd)) + chatClient.on('LIVE_OPEN_PLATFORM_GUARD', (cmd) => this.onGuard(cmd)) + chatClient.on('LIVE_OPEN_PLATFORM_SC', (cmd) => this.onSC(cmd)) + chatClient.on('msg', (data) => { + this.events.all?.forEach((d) => { + d(data) + }) + }) // 广播所有事件 + chatClient.on('live', () => { + console.log( + `[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}` + ) + }) + + return await super.initClientInner(chatClient) + } else { + console.log(`[${this.type}] 无法开启场次: ` + auth.message) + return { + success: false, + message: auth.message + } + } + } + private async getAuthInfo(): Promise<{ + data: OpenLiveInfo | null + message: string + }> { + try { + const data = await QueryPostAPI( + OPEN_LIVE_API_URL + 'start', + this.authInfo?.Code ? this.authInfo : undefined + ) + if (data.code == 200) { + console.log(`[${this.type}] 已获取场次信息`) + return { + data: data.data, + message: '' + } + } else { + return { + data: null, + message: data.message + } + } + } catch (err) { + return { + data: null, + message: err?.toString() || '未知错误' + } + } + } + private sendHeartbeat() { + if (this.state !== 'connected') { + clearInterval(this.timer) + this.timer = undefined + return + } + const query = this.authInfo + ? QueryPostAPI( + OPEN_LIVE_API_URL + 'heartbeat', + this.authInfo + ) + : QueryGetAPI(OPEN_LIVE_API_URL + 'heartbeat-internal') + query.then((data) => { + if (data.code != 200) { + console.error(`[${this.type}] 心跳失败, 将重新连接`) + this.client?.close() + this.client = null + this.initClient() + } + }) + } + + public onDanmaku(command: any) { + const data = command.data as DanmakuInfo + this.events.danmaku?.forEach((d) => { + d(data, command) + }) + this.eventsAsModel.danmaku?.forEach((d) => { + d( + { + type: EventDataTypes.Message, + name: data.uname, + uid: data.uid, + msg: data.msg, + price: 0, + num: 0, + time: data.timestamp, + guard_level: data.guard_level, + fans_medal_level: data.fans_medal_level, + fans_medal_name: data.fans_medal_name, + fans_medal_wearing_status: data.fans_medal_wearing_status, + emoji: data.dm_type == 1 ? data.emoji_img_url : undefined, + uface: data.uface, + open_id: data.open_id, + ouid: data.open_id ?? GuidUtils.numToGuid(data.uid) + }, + command + ) + }) + } + public onGift(command: any) { + const data = command.data as GiftInfo + const price = (data.price * data.gift_num) / 1000 + this.events.gift?.forEach((d) => { + d(data, command) + }) + this.eventsAsModel.gift?.forEach((d) => { + d( + { + type: EventDataTypes.Gift, + name: data.uname, + uid: data.uid, + msg: data.gift_name, + price: data.paid ? price : -price, + num: data.gift_num, + time: data.timestamp, + guard_level: data.guard_level, + fans_medal_level: data.fans_medal_level, + fans_medal_name: data.fans_medal_name, + fans_medal_wearing_status: data.fans_medal_wearing_status, + uface: data.uface, + open_id: data.open_id, + ouid: data.open_id ?? GuidUtils.numToGuid(data.uid) + }, + command + ) + }) + } + public onSC(command: any) { + const data = command.data as SCInfo + this.events.sc?.forEach((d) => { + d(data, command) + }) + this.eventsAsModel.sc?.forEach((d) => { + d( + { + type: EventDataTypes.SC, + name: data.uname, + uid: data.uid, + msg: data.message, + price: data.rmb, + num: 1, + time: data.timestamp, + guard_level: data.guard_level, + fans_medal_level: data.fans_medal_level, + fans_medal_name: data.fans_medal_name, + fans_medal_wearing_status: data.fans_medal_wearing_status, + uface: data.uface, + open_id: data.open_id, + ouid: data.open_id ?? GuidUtils.numToGuid(data.uid) + }, + command + ) + }) + } + public onGuard(command: any) { + const data = command.data as GuardInfo + this.events.guard?.forEach((d) => { + d(data, command) + }) + this.eventsAsModel.guard?.forEach((d) => { + d( + { + type: EventDataTypes.Guard, + name: data.user_info.uname, + uid: data.user_info.uid, + msg: + data.guard_level == 1 + ? '总督' + : data.guard_level == 2 + ? '提督' + : data.guard_level == 3 + ? '舰长' + : '', + price: 0, + num: data.guard_num, + time: data.timestamp, + guard_level: data.guard_level, + fans_medal_level: data.fans_medal_level, + fans_medal_name: data.fans_medal_name, + fans_medal_wearing_status: data.fans_medal_wearing_status, + uface: data.user_info.uface, + open_id: data.user_info.open_id, + ouid: + data.user_info.open_id ?? GuidUtils.numToGuid(data.user_info.uid) + }, + command + ) + }) + } + public onEvent( + eventName: 'danmaku', + listener: DanmakuEventsMap['danmaku'] + ): this + public onEvent(eventName: 'gift', listener: DanmakuEventsMap['gift']): this + public onEvent(eventName: 'sc', listener: DanmakuEventsMap['sc']): this + public onEvent(eventName: 'guard', listener: DanmakuEventsMap['guard']): this + public onEvent(eventName: 'all', listener: (arg1: any) => void): this + public onEvent( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', + listener: (...args: any[]) => void + ): this { + if (!this.events[eventName]) { + this.events[eventName] = [] + } + this.events[eventName].push(listener) + return this + } + public offEvent( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', + listener: (...args: any[]) => void + ): this { + if (this.events[eventName]) { + const index = this.events[eventName].indexOf(listener) + if (index > -1) { + this.events[eventName].splice(index, 1) + } + } + return this + } +} export interface DanmakuInfo { room_id: number @@ -162,386 +436,3 @@ export interface DanmakuEventsMap { guard: (arg1: GuardInfo, arg2?: any) => void all: (arg1: any) => void } - -export default class DanmakuClient { - constructor(auth: AuthInfo | null) { - this.authInfo = auth - } - - private client: any - private timer: any | undefined - private isStarting = false - - public authInfo: AuthInfo | null - public roomAuthInfo: RoomAuthInfo | undefined - public authCode: string | undefined - - public isRunning: boolean = false - - public events: { - danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[] - gift: ((arg1: GiftInfo, arg2?: any) => void)[] - sc: ((arg1: SCInfo, arg2?: any) => void)[] - guard: ((arg1: GuardInfo, arg2?: any) => void)[] - all: ((arg1: any) => void)[] - } = { - danmaku: [], - gift: [], - sc: [], - guard: [], - all: [] - } - public eventsAsModel: { - danmaku: ((arg1: EventModel, arg2?: any) => void)[] - gift: ((arg1: EventModel, arg2?: any) => void)[] - sc: ((arg1: EventModel, arg2?: any) => void)[] - guard: ((arg1: EventModel, arg2?: any) => void)[] - all: ((arg1: any) => void)[] - } = { - danmaku: [], - gift: [], - sc: [], - guard: [], - all: [] - } - - public async Start(): Promise<{ success: boolean; message: string }> { - if (this.isRunning) { - return { - success: true, - message: '弹幕客户端已启动' - } - } - if (this.isStarting) { - return { - success: false, - message: '弹幕客户端正在启动' - } - } - this.isStarting = true - try { - if (!this.client) { - console.log('[OPEN-LIVE] 正在启动弹幕客户端') - const result = await this.initClient() - if (result.success) { - this.isRunning = true - this.timer ??= setInterval(() => { - this.sendHeartbeat() - }, 20 * 1000) - } - return result - } else { - console.warn('[OPEN-LIVE] 弹幕客户端已被启动过') - return { - success: false, - message: '弹幕客户端已被启动过' - } - } - } finally { - this.isStarting = false - } - } - public Stop() { - if (!this.isRunning) { - return - } - this.isRunning = false - if (this.client) { - console.log('[OPEN-LIVE] 正在停止弹幕客户端') - this.client.stop() - } else { - console.warn('[OPEN-LIVE] 弹幕客户端未被启动, 忽略') - } - if (this.timer) { - clearInterval(this.timer) - this.timer = undefined - } - this.events = { - danmaku: [], - gift: [], - sc: [], - guard: [], - all: [] - } - this.eventsAsModel = { - danmaku: [], - gift: [], - sc: [], - guard: [], - all: [] - } - } - private sendHeartbeat() { - if (!this.isRunning) { - clearInterval(this.timer) - this.timer = undefined - return - } - const query = this.authInfo - ? QueryPostAPI( - OPEN_LIVE_API_URL + 'heartbeat', - this.authInfo - ) - : QueryGetAPI(OPEN_LIVE_API_URL + 'heartbeat-internal') - query.then((data) => { - if (data.code != 200) { - console.error('[OPEN-LIVE] 心跳失败, 将重新连接') - this.client?.stop() - this.client = null - this.initClient() - } - }) - } - - public onRawMessage = (command: any) => { - this.eventsAsModel.all?.forEach((d) => { - d(command) - }) - this.events.all?.forEach((d) => { - d(command) - }) - } - - public onDanmaku = (command: any) => { - const data = command.data as DanmakuInfo - - this.events.danmaku?.forEach((d) => { - d(data, command) - }) - this.eventsAsModel.danmaku?.forEach((d) => { - d( - { - type: EventDataTypes.Message, - name: data.uname, - uid: data.uid, - msg: data.msg, - price: 0, - num: 0, - time: data.timestamp, - guard_level: data.guard_level, - fans_medal_level: data.fans_medal_level, - fans_medal_name: data.fans_medal_name, - fans_medal_wearing_status: data.fans_medal_wearing_status, - emoji: data.dm_type == 1 ? data.emoji_img_url : undefined, - uface: data.uface, - open_id: data.open_id, - ouid: data.open_id ?? GuidUtils.numToGuid(data.uid) - }, - command - ) - }) - } - public onGift = (command: any) => { - const data = command.data as GiftInfo - const price = (data.price * data.gift_num) / 1000 - this.events.gift?.forEach((d) => { - d(data, command) - }) - this.eventsAsModel.gift?.forEach((d) => { - d( - { - type: EventDataTypes.Gift, - name: data.uname, - uid: data.uid, - msg: data.gift_name, - price: data.paid ? price : -price, - num: data.gift_num, - time: data.timestamp, - guard_level: data.guard_level, - fans_medal_level: data.fans_medal_level, - fans_medal_name: data.fans_medal_name, - fans_medal_wearing_status: data.fans_medal_wearing_status, - uface: data.uface, - open_id: data.open_id, - ouid: data.open_id ?? GuidUtils.numToGuid(data.uid) - }, - command - ) - }) - } - public onSC = (command: any) => { - const data = command.data as SCInfo - this.events.sc?.forEach((d) => { - d(data, command) - }) - this.eventsAsModel.sc?.forEach((d) => { - d( - { - type: EventDataTypes.SC, - name: data.uname, - uid: data.uid, - msg: data.message, - price: data.rmb, - num: 1, - time: data.timestamp, - guard_level: data.guard_level, - fans_medal_level: data.fans_medal_level, - fans_medal_name: data.fans_medal_name, - fans_medal_wearing_status: data.fans_medal_wearing_status, - uface: data.uface, - open_id: data.open_id, - ouid: data.open_id ?? GuidUtils.numToGuid(data.uid) - }, - command - ) - }) - } - public onGuard = (command: any) => { - const data = command.data as GuardInfo - this.events.guard?.forEach((d) => { - d(data, command) - }) - this.eventsAsModel.guard?.forEach((d) => { - d( - { - type: EventDataTypes.Guard, - name: data.user_info.uname, - uid: data.user_info.uid, - msg: - data.guard_level == 1 - ? '总督' - : data.guard_level == 2 - ? '提督' - : data.guard_level == 3 - ? '舰长' - : '', - price: 0, - num: data.guard_num, - time: data.timestamp, - guard_level: data.guard_level, - fans_medal_level: data.fans_medal_level, - fans_medal_name: data.fans_medal_name, - fans_medal_wearing_status: data.fans_medal_wearing_status, - uface: data.user_info.uface, - open_id: data.user_info.open_id, - ouid: - data.user_info.open_id ?? GuidUtils.numToGuid(data.user_info.uid) - }, - command - ) - }) - } - public on(eventName: 'danmaku', listener: DanmakuEventsMap['danmaku']): this - public on(eventName: 'gift', listener: DanmakuEventsMap['gift']): this - public on(eventName: 'sc', listener: DanmakuEventsMap['sc']): this - public on(eventName: 'guard', listener: DanmakuEventsMap['guard']): this - public on( - eventName: 'danmaku' | 'gift' | 'sc' | 'guard', - listener: (...args: any[]) => void - ): this { - if (!this.events[eventName]) { - this.events[eventName] = [] - } - this.events[eventName].push(listener) - return this - } - public onEvent( - eventName: 'danmaku', - listener: (arg1: EventModel, arg2?: any) => void - ): this - public onEvent( - eventName: 'gift', - listener: (arg1: EventModel, arg2?: any) => void - ): this - public onEvent( - eventName: 'sc', - listener: (arg1: EventModel, arg2?: any) => void - ): this - public onEvent( - eventName: 'guard', - listener: (arg1: EventModel, arg2?: any) => void - ): this - public onEvent(eventName: 'all', listener: (arg1: any) => void): this - public onEvent( - eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', - listener: (...args: any[]) => void - ): this { - if (!this.eventsAsModel[eventName]) { - this.eventsAsModel[eventName] = [] - } - this.eventsAsModel[eventName].push(listener) - return this - } - public off( - eventName: 'danmaku' | 'gift' | 'sc' | 'guard', - listener: (...args: any[]) => void - ): this { - if (this.events[eventName]) { - const index = this.events[eventName].indexOf(listener) - if (index > -1) { - this.events[eventName].splice(index, 1) - } - } - return this - } - public offEvent( - eventName: 'danmaku' | 'gift' | 'sc' | 'guard', - listener: (...args: any[]) => void - ): this { - if (this.eventsAsModel[eventName]) { - const index = this.eventsAsModel[eventName].indexOf(listener) - if (index > -1) { - this.eventsAsModel[eventName].splice(index, 1) - } - } - return this - } - private async initClient(): Promise<{ success: boolean; message: string }> { - const auth = await this.getAuthInfo() - if (auth.data) { - const chatClient = new ChatClientDirectOpenLive(auth.data) - //chatClient.msgHandler = this; - chatClient.CMD_CALLBACK_MAP = this.CMD_CALLBACK_MAP - chatClient.start() - this.roomAuthInfo = auth.data as RoomAuthInfo - this.client = chatClient - console.log('[OPEN-LIVE] 已连接房间: ' + auth.data.anchor_info.room_id) - return { - success: true, - message: '' - } - } else { - console.log('[OPEN-LIVE] 无法开启场次: ' + auth.message) - return { - success: false, - message: auth.message - } - } - } - private async getAuthInfo(): Promise<{ - data: OpenLiveInfo | null - message: string - }> { - try { - const data = await QueryPostAPI( - OPEN_LIVE_API_URL + 'start', - this.authInfo?.Code ? this.authInfo : undefined - ) - 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() || '未知错误' - } - } - } - - private CMD_CALLBACK_MAP = { - LIVE_OPEN_PLATFORM_DM: this.onDanmaku.bind(this), - LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift.bind(this), - LIVE_OPEN_PLATFORM_SUPER_CHAT: this.onSC.bind(this), - LIVE_OPEN_PLATFORM_GUARD: this.onGuard.bind(this), - RAW_MESSAGE: this.onRawMessage.bind(this) - } -} diff --git a/src/data/chat/ChatClientDirectOpenLive.js b/src/data/chat/ChatClientDirectOpenLive.js index 8d2f56c..f634e9e 100644 --- a/src/data/chat/ChatClientDirectOpenLive.js +++ b/src/data/chat/ChatClientDirectOpenLive.js @@ -1,4 +1,5 @@ import ChatClientOfficialBase, * as base from './ChatClientOfficialBase' +import { processAvatarUrl } from './models' export default class ChatClientDirectOpenLive extends ChatClientOfficialBase { constructor(authInfo) { @@ -49,7 +50,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase { } data = { - avatarUrl: chat.processAvatarUrl(data.uface), + avatarUrl: processAvatarUrl(data.uface), timestamp: data.timestamp, authorName: data.uname, authorType: authorType, @@ -79,7 +80,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase { data = { id: data.msg_id, - avatarUrl: chat.processAvatarUrl(data.uface), + avatarUrl: processAvatarUrl(data.uface), timestamp: data.timestamp, authorName: data.uname, totalCoin: data.price, @@ -97,7 +98,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase { let data = command.data data = { id: data.msg_id, - avatarUrl: chat.processAvatarUrl(data.user_info.uface), + avatarUrl: processAvatarUrl(data.user_info.uface), timestamp: data.timestamp, authorName: data.user_info.uname, privilegeType: data.guard_level, @@ -113,7 +114,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase { let data = command.data data = { id: data.message_id.toString(), - avatarUrl: chat.processAvatarUrl(data.uface), + avatarUrl: processAvatarUrl(data.uface), timestamp: data.start_time, authorName: data.uname, price: data.rmb, diff --git a/src/data/chat/ChatClientDirectWeb.js b/src/data/chat/ChatClientDirectWeb.js new file mode 100644 index 0000000..3bdc6c9 --- /dev/null +++ b/src/data/chat/ChatClientDirectWeb.js @@ -0,0 +1,176 @@ +import * as chat from './ChatClientOfficialBase' +import * as chatModels from './models.js' +import * as base from './ChatClientOfficialBase' +import ChatClientOfficialBase from './ChatClientOfficialBase' + +export default class ChatClientDirectWeb extends ChatClientOfficialBase { + constructor(roomId) { + super() + this.CMD_CALLBACK_MAP = CMD_CALLBACK_MAP + + // 调用initRoom后初始化,如果失败,使用这里的默认值 + this.roomId = roomId + this.roomOwnerUid = -1 + this.hostServerList = [ + { + host: 'broadcastlv.chat.bilibili.com', + port: 2243, + wss_port: 443, + ws_port: 2244 + } + ] + this.hostServerToken = null + this.buvid = '' + } + + async initRoom() { + let res + try { + res = await ( + await fetch('/api/room_info?room_id=' + this.roomId, { method: 'GET' }) + ).json() + } catch { + return true + } + this.roomId = res.roomId + this.roomOwnerUid = res.ownerUid + if (res.hostServerList.length !== 0) { + this.hostServerList = res.hostServerList + } + this.hostServerToken = res.hostServerToken + this.buvid = res.buvid + return true + } + + async onBeforeWsConnect() { + // 重连次数太多则重新init_room,保险 + let reinitPeriod = Math.max(3, (this.hostServerList || []).length) + if (this.retryCount > 0 && this.retryCount % reinitPeriod === 0) { + this.needInitRoom = true + } + return super.onBeforeWsConnect() + } + + getWsUrl() { + let hostServer = + this.hostServerList[this.retryCount % this.hostServerList.length] + return `wss://${hostServer.host}:${hostServer.wss_port}/sub` + } + + sendAuth() { + let authParams = { + uid: 0, + roomid: this.roomId, + protover: 3, + platform: 'web', + type: 2, + buvid: this.buvid + } + if (this.hostServerToken !== null) { + authParams.key = this.hostServerToken + } + this.websocket.send(this.makePacket(authParams, base.OP_AUTH)) + } + + async danmuMsgCallback(command) { + let info = command.info + + let roomId, medalLevel + if (info[3]) { + roomId = info[3][3] + medalLevel = info[3][0] + } else { + roomId = medalLevel = 0 + } + + let uid = info[2][0] + let isAdmin = info[2][2] + let privilegeType = info[7] + let authorType + if (uid === this.roomOwnerUid) { + authorType = 3 + } else if (isAdmin) { + authorType = 2 + } else if (privilegeType !== 0) { + authorType = 1 + } else { + authorType = 0 + } + + let authorName = info[2][1] + let content = info[1] + let data = new chatModels.AddTextMsg({ + avatarUrl: await chat.getAvatarUrl(uid, authorName), + timestamp: info[0][4] / 1000, + authorName: authorName, + authorType: authorType, + content: content, + privilegeType: privilegeType, + isGiftDanmaku: + Boolean(info[0][9]) || chat.isGiftDanmakuByContent(content), + authorLevel: info[4][0], + isNewbie: info[2][5] < 10000, + isMobileVerified: Boolean(info[2][6]), + medalLevel: roomId === this.roomId ? medalLevel : 0, + emoticon: info[0][13].url || null + }) + this.msgHandler.onAddText(data) + } + + sendGiftCallback(command) { + let data = command.data + let isPaidGift = data.coin_type === 'gold' + data = new chatModels.AddGiftMsg({ + avatarUrl: chat.processAvatarUrl(data.face), + timestamp: data.timestamp, + authorName: data.uname, + totalCoin: isPaidGift ? data.total_coin : 0, + totalFreeCoin: !isPaidGift ? data.total_coin : 0, + giftName: data.giftName, + num: data.num + }) + this.msgHandler.onAddGift(data) + } + + async guardBuyCallback(command) { + let data = command.data + data = new chatModels.AddMemberMsg({ + avatarUrl: await chat.getAvatarUrl(data.uid, data.username), + timestamp: data.start_time, + authorName: data.username, + privilegeType: data.guard_level + }) + this.msgHandler.onAddMember(data) + } + + superChatMessageCallback(command) { + let data = command.data + data = new chatModels.AddSuperChatMsg({ + id: data.id.toString(), + avatarUrl: chat.processAvatarUrl(data.user_info.face), + timestamp: data.start_time, + authorName: data.user_info.uname, + price: data.price, + content: data.message + }) + this.msgHandler.onAddSuperChat(data) + } + + superChatMessageDeleteCallback(command) { + let ids = [] + for (let id of command.data.ids) { + ids.push(id.toString()) + } + let data = new chatModels.DelSuperChatMsg({ ids }) + this.msgHandler.onDelSuperChat(data) + } +} + +const CMD_CALLBACK_MAP = { + DANMU_MSG: ChatClientDirectWeb.prototype.danmuMsgCallback, + SEND_GIFT: ChatClientDirectWeb.prototype.sendGiftCallback, + GUARD_BUY: ChatClientDirectWeb.prototype.guardBuyCallback, + SUPER_CHAT_MESSAGE: ChatClientDirectWeb.prototype.superChatMessageCallback, + SUPER_CHAT_MESSAGE_DELETE: + ChatClientDirectWeb.prototype.superChatMessageDeleteCallback +} diff --git a/src/data/chat/ChatClientOfficialBase/index.js.new b/src/data/chat/ChatClientOfficialBase/index.js.new new file mode 100644 index 0000000..972210b --- /dev/null +++ b/src/data/chat/ChatClientOfficialBase/index.js.new @@ -0,0 +1,374 @@ +import { BrotliDecode } from './brotli_decode' + +import { + setInterval, + clearInterval, + setTimeout, + clearTimeout +} from 'worker-timers' +import * as chatModels from '../models' + +const HEADER_SIZE = 16 + +export const WS_BODY_PROTOCOL_VERSION_NORMAL = 0 +export const WS_BODY_PROTOCOL_VERSION_HEARTBEAT = 1 +export const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2 +export const WS_BODY_PROTOCOL_VERSION_BROTLI = 3 + +export const OP_HANDSHAKE = 0 +export const OP_HANDSHAKE_REPLY = 1 +export const OP_HEARTBEAT = 2 +export const OP_HEARTBEAT_REPLY = 3 +export const OP_SEND_MSG = 4 +export const OP_SEND_MSG_REPLY = 5 +export const OP_DISCONNECT_REPLY = 6 +export const OP_AUTH = 7 +export const OP_AUTH_REPLY = 8 +export const OP_RAW = 9 +export const OP_PROTO_READY = 10 +export const OP_PROTO_FINISH = 11 +export const OP_CHANGE_ROOM = 12 +export const OP_CHANGE_ROOM_REPLY = 13 +export const OP_REGISTER = 14 +export const OP_REGISTER_REPLY = 15 +export const OP_UNREGISTER = 16 +export const OP_UNREGISTER_REPLY = 17 +// B站业务自定义OP +// export const MinBusinessOp = 1000 +// export const MaxBusinessOp = 10000 + +export const AUTH_REPLY_CODE_OK = 0 +export const AUTH_REPLY_CODE_TOKEN_ERROR = -101 + +const HEARTBEAT_INTERVAL = 10 * 1000 +const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + (5 * 1000) + +let textEncoder = new TextEncoder() +let textDecoder = new TextDecoder() + +export default class ChatClientOfficialBase { + constructor() { + this.CMD_CALLBACK_MAP = {} + + this.msgHandler = chat.getDefaultMsgHandler() + + this.needInitRoom = true + this.websocket = null + this.retryCount = 0 + this.totalRetryCount = 0 + this.isDestroying = false + this.heartbeatTimerId = null + this.receiveTimeoutTimerId = null + } + + start() { + this.wsConnect() + } + + stop() { + this.isDestroying = true + if (this.websocket) { + this.websocket.close() + } + } + + async initRoom() { + throw new Error('Not implemented') + } + + makePacket(data, operation) { + if (typeof data === 'object') { + data = JSON.stringify(data) + } + let bodyArr = textEncoder.encode(data) + + let headerBuf = new ArrayBuffer(HEADER_SIZE) + let headerView = new DataView(headerBuf) + headerView.setUint32(0, HEADER_SIZE + bodyArr.byteLength) // pack_len + headerView.setUint16(4, HEADER_SIZE) // raw_header_size + headerView.setUint16(6, 1) // ver + headerView.setUint32(8, operation) // operation + headerView.setUint32(12, 1) // seq_id + + // 这里如果直接返回 new Blob([headerBuf, bodyArr]),在Chrome抓包会显示空包,实际上是有数据的,为了调试体验最好还是拷贝一遍 + let headerArr = new Uint8Array(headerBuf) + let packetArr = new Uint8Array(bodyArr.length + headerArr.length) + packetArr.set(headerArr) + packetArr.set(bodyArr, headerArr.length) + return packetArr + } + + sendAuth() { + throw new Error('Not implemented') + } + + addDebugMsg(content) { + this.msgHandler.onDebugMsg(new chatModels.DebugMsg({ content })) + } + + async wsConnect() { + if (this.isDestroying) { + return + } + + this.addDebugMsg('Connecting') + await this.onBeforeWsConnect() + if (this.isDestroying) { + return + } + + this.websocket = new WebSocket(this.getWsUrl()) + this.websocket.binaryType = 'arraybuffer' + this.websocket.onopen = this.onWsOpen.bind(this) + this.websocket.onclose = this.onWsClose.bind(this) + this.websocket.onmessage = this.onWsMessage.bind(this) + } + + async onBeforeWsConnect() { + if (!this.needInitRoom) { + return + } + + let res + try { + res = await this.initRoom() + } catch (e) { + res = false + console.error('initRoom exception:', e) + if (e instanceof chatModels.ChatClientFatalError) { + this.stop() + this.msgHandler.onFatalError(e) + } + } + + if (!res) { + setTimeout(() => this.onWsClose(), 0) + throw new Error('initRoom failed') + } + this.needInitRoom = false + } + + getWsUrl() { + throw new Error('Not implemented') + } + + onWsOpen() { + this.addDebugMsg('Connected and authenticating') + + this.sendAuth() + if (this.heartbeatTimerId === null) { + this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL) + } + this.refreshReceiveTimeoutTimer() + } + + sendHeartbeat() { + this.websocket.send(this.makePacket({}, OP_HEARTBEAT)) + } + + refreshReceiveTimeoutTimer() { + if (this.receiveTimeoutTimerId) { + clearTimeout(this.receiveTimeoutTimerId) + } + this.receiveTimeoutTimerId = setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT) + } + + onReceiveTimeout() { + console.warn('接收消息超时') + this.addDebugMsg('Receiving message timed out') + + this.discardWebsocket() + } + + discardWebsocket() { + if (this.receiveTimeoutTimerId) { + clearTimeout(this.receiveTimeoutTimerId) + this.receiveTimeoutTimerId = null + } + + if (this.websocket) { + if (this.websocket.onclose) { + setTimeout(() => this.onWsClose(), 0) + } + // 直接丢弃阻塞的websocket,不等onclose回调了 + this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null + this.websocket.close() + } + } + + onWsClose() { + this.addDebugMsg('Disconnected') + + this.websocket = null + if (this.heartbeatTimerId) { + clearInterval(this.heartbeatTimerId) + this.heartbeatTimerId = null + } + if (this.receiveTimeoutTimerId) { + clearTimeout(this.receiveTimeoutTimerId) + this.receiveTimeoutTimerId = null + } + + if (this.isDestroying) { + return + } + this.retryCount++ + this.totalRetryCount++ + console.warn(`掉线重连中 retryCount=${this.retryCount}, totalRetryCount=${this.totalRetryCount}`) + + // 防止无限重连的保险措施。30次重连大概会断线500秒,应该够了 + if (this.totalRetryCount > 30) { + this.stop() + let error = new chatModels.ChatClientFatalError( + chatModels.FATAL_ERROR_TYPE_TOO_MANY_RETRIES, 'The connection has lost too many times' + ) + this.msgHandler.onFatalError(error) + return + } + + this.delayReconnect() + } + + delayReconnect() { + this.addDebugMsg(`Scheduling reconnection. The page is ${document.visibilityState === 'visible' ? 'visible' : 'invisible'}`) + + if (document.visibilityState === 'visible') { + setTimeout(this.wsConnect.bind(this), this.getReconnectInterval()) + return + } + + // 页面不可见就先不重连了,即使重连也会心跳超时 + let listener = () => { + if (document.visibilityState !== 'visible') { + return + } + document.removeEventListener('visibilitychange', listener) + this.wsConnect() + } + document.addEventListener('visibilitychange', listener) + } + + getReconnectInterval() { + // 不用retryCount了,防止意外的连接成功,导致retryCount重置 + let interval = Math.min(1000 + ((this.totalRetryCount - 1) * 2000), 20 * 1000) + // 加上随机延迟,防止同时请求导致雪崩 + interval += Math.random() * 3000 + return interval + } + + onWsMessage(event) { + if (!(event.data instanceof ArrayBuffer)) { + console.warn('未知的websocket消息类型,data=', event.data) + return + } + + let data = new Uint8Array(event.data) + this.parseWsMessage(data) + + // 至少成功处理1条消息 + this.retryCount = 0 + } + + parseWsMessage(data) { + let offset = 0 + let dataView = new DataView(data.buffer) + let packLen = dataView.getUint32(0) + let rawHeaderSize = dataView.getUint16(4) + // let ver = dataView.getUint16(6) + let operation = dataView.getUint32(8) + // let seqId = dataView.getUint32(12) + + switch (operation) { + case OP_AUTH_REPLY: + case OP_SEND_MSG_REPLY: { + // 业务消息,可能有多个包一起发,需要分包 + while (true) { // eslint-disable-line no-constant-condition + let body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize) + this.parseBusinessMessage(dataView, body) + + offset += packLen + if (offset >= data.byteLength) { + break + } + + dataView = new DataView(data.buffer, offset) + packLen = dataView.getUint32(0) + rawHeaderSize = dataView.getUint16(4) + } + break + } + case OP_HEARTBEAT_REPLY: { + // 服务器心跳包,包含人气值,这里没用 + this.refreshReceiveTimeoutTimer() + break + } + default: { + // 未知消息 + let body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize) + console.warn('未知包类型,operation=', operation, dataView, body) + break + } + } + } + + parseBusinessMessage(dataView, body) { + let ver = dataView.getUint16(6) + let operation = dataView.getUint32(8) + + switch (operation) { + case OP_SEND_MSG_REPLY: { + // 业务消息 + if (ver == WS_BODY_PROTOCOL_VERSION_BROTLI) { + // 压缩过的先解压 + body = BrotliDecode(body) + this.parseWsMessage(body) + } /*else if (ver == WS_BODY_PROTOCOL_VERSION_DEFLATE) { + // web端已经不用zlib压缩了,但是开放平台会用 + body = inflate(body) + this.parseWsMessage(body) + } */else { + // 没压缩过的直接反序列化 + if (body.length !== 0) { + try { + body = JSON.parse(textDecoder.decode(body)) + this.handlerCommand(body) + } catch (e) { + console.error('body=', body) + throw e + } + } + } + break + } + case OP_AUTH_REPLY: { + // 认证响应 + body = JSON.parse(textDecoder.decode(body)) + if (body.code !== AUTH_REPLY_CODE_OK) { + console.error('认证响应错误,body=', body) + this.needInitRoom = true + this.discardWebsocket() + throw new Error('认证响应错误') + } + this.sendHeartbeat() + break + } + default: { + // 未知消息 + console.warn('未知包类型,operation=', operation, dataView, body) + break + } + } + } + + handlerCommand(command) { + let cmd = command.cmd || '' + let pos = cmd.indexOf(':') + if (pos != -1) { + cmd = cmd.substr(0, pos) + } + let callback = this.CMD_CALLBACK_MAP[cmd] + if (callback) { + callback.call(this, command) + } + } +} diff --git a/src/views/obs/blivechat/models.js b/src/data/chat/models.js similarity index 79% rename from src/views/obs/blivechat/models.js rename to src/data/chat/models.js index 821c9a6..19877c5 100644 --- a/src/views/obs/blivechat/models.js +++ b/src/data/chat/models.js @@ -1,7 +1,23 @@ -import { getUuid4Hex } from './utils' -import * as constants from './constants' +import { getUuid4Hex } from '../../views/obs/blivechat/utils' +import * as constants from '../../views/obs/blivechat/constants' -export const DEFAULT_AVATAR_URL = 'https://i0.hdslb.com/bfs/face/member/noface.jpg@64w_64h' +export function getDefaultMsgHandler() { + let dummyFunc = () => {} + return { + onAddText: dummyFunc, + onAddGift: dummyFunc, + onAddMember: dummyFunc, + onAddSuperChat: dummyFunc, + onDelSuperChat: dummyFunc, + onUpdateTranslation: dummyFunc, + + onFatalError: dummyFunc, + onDebugMsg: dummyFunc + } +} + +export const DEFAULT_AVATAR_URL = + 'https://i0.hdslb.com/bfs/face/member/noface.jpg@64w_64h' export class AddTextMsg { constructor({ @@ -124,3 +140,11 @@ export class DebugMsg { this.content = content } } +export function processAvatarUrl(avatarUrl) { + // 去掉协议,兼容HTTP、HTTPS + let m = avatarUrl.match(/(?:https?:)?(.*)/) + if (m) { + avatarUrl = m[1] + } + return avatarUrl +} diff --git a/src/store/useDanmakuClient.ts b/src/store/useDanmakuClient.ts index 413f235..cdaa2dd 100644 --- a/src/store/useDanmakuClient.ts +++ b/src/store/useDanmakuClient.ts @@ -1,5 +1,8 @@ import { useAccount } from '@/api/account' -import DanmakuClient, { AuthInfo, RoomAuthInfo } from '@/data/DanmakuClient' +import OpenLiveClient, { + AuthInfo, + RoomAuthInfo +} from '@/data/DanmakuClients/OpenLiveClient' import { defineStore } from 'pinia' import { computed, ref } from 'vue' @@ -9,7 +12,7 @@ export interface BCMessage { } export const useDanmakuClient = defineStore('DanmakuClient', () => { - const danmakuClient = ref(new DanmakuClient(null)) + const danmakuClient = ref(new OpenLiveClient()) let bc: BroadcastChannel const isOwnedDanmakuClient = ref(false) const status = ref<'waiting' | 'initializing' | 'listening' | 'running'>( @@ -77,8 +80,10 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => { async (lock) => { if (lock) { status.value = 'initializing' - bc = new BroadcastChannel('vtsuru.danmaku.' + accountInfo.value?.id) - console.log('[DanmakuClient] 创建 BroadcastChannel: ' + bc.name) + bc = new BroadcastChannel( + 'vtsuru.danmaku.open-live' + accountInfo.value?.id + ) + console.log('[DanmakuClient] 创建 BroadcastChannel: ' + bc.name) bc.onmessage = (event) => { const message: BCMessage = event.data as BCMessage const data = message.data ? JSON.parse(message.data) : {} @@ -86,7 +91,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => { case 'check-client': sendBCMessage('response-client-status', { status: status.value, - auth: authInfo.value, + auth: authInfo.value }) break case 'response-client-status': @@ -178,7 +183,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => { const events = danmakuClient.value.events const eventsAsModel = danmakuClient.value.eventsAsModel - danmakuClient.value = new DanmakuClient(auth || null) + danmakuClient.value = new OpenLiveClient(auth) danmakuClient.value.events = events danmakuClient.value.eventsAsModel = eventsAsModel @@ -192,7 +197,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => { status: 'running', auth: authInfo.value }) - danmakuClient.value.onEvent('all', (data) => { + danmakuClient.value.on('all', (data) => { sendBCMessage('on-danmaku', data) }) return true diff --git a/src/store/useDirectDanmakuClient.ts b/src/store/useDirectDanmakuClient.ts new file mode 100644 index 0000000..76380dc --- /dev/null +++ b/src/store/useDirectDanmakuClient.ts @@ -0,0 +1,234 @@ +import { useAccount } from '@/api/account' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export interface BCMessage { + type: string + data: string +} + +export const useDirectDanmakuClient = defineStore('DirectDanmakuClient', () => { + const danmakuClient = ref(new OpenLiveClient(null)) + let bc: BroadcastChannel + const isOwnedDirectDanmakuClient = ref(false) + const status = ref<'waiting' | 'initializing' | 'listening' | 'running'>( + 'waiting' + ) + const connected = computed( + () => status.value === 'running' || status.value === 'listening' + ) + const authInfo = ref() + const accountInfo = useAccount() + + let existOtherClient = false + let isInitializing = false + + function on( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard', + listener: (...args: any[]) => void + ) { + if (!danmakuClient.value.events[eventName]) { + danmakuClient.value.events[eventName] = [] + } + danmakuClient.value.events[eventName].push(listener) + } + function onEvent( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', + listener: (...args: any[]) => void + ) { + if (!danmakuClient.value.eventsAsModel[eventName]) { + danmakuClient.value.eventsAsModel[eventName] = [] + } + danmakuClient.value.eventsAsModel[eventName].push(listener) + } + + function off( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard', + listener: (...args: any[]) => void + ) { + if (danmakuClient.value.events[eventName]) { + const index = danmakuClient.value.events[eventName].indexOf(listener) + if (index > -1) { + danmakuClient.value.events[eventName].splice(index, 1) + } + } + } + + function offEvent( + eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', + listener: (...args: any[]) => void + ) { + if (danmakuClient.value.eventsAsModel[eventName]) { + const index = + danmakuClient.value.eventsAsModel[eventName].indexOf(listener) + if (index > -1) { + danmakuClient.value.eventsAsModel[eventName].splice(index, 1) + } + } + } + + async function initClient(auth?: AuthInfo) { + if (!isInitializing && !connected.value) { + isInitializing = true + navigator.locks.request( + 'danmakuClientInit', + { ifAvailable: true }, + async (lock) => { + if (lock) { + status.value = 'initializing' + bc = new BroadcastChannel('vtsuru.danmaku.open-live' + accountInfo.value?.id) + console.log('[DirectDanmakuClient] 创建 BroadcastChannel: ' + bc.name) + bc.onmessage = (event) => { + const message: BCMessage = event.data as BCMessage + const data = message.data ? JSON.parse(message.data) : {} + switch (message.type) { + case 'check-client': + sendBCMessage('response-client-status', { + status: status.value, + auth: authInfo.value, + }) + break + case 'response-client-status': + switch ( + data.status //如果存在已经在运行或者正在启动的客户端, 状态设为 listening + ) { + case 'running': + case 'initializing': + status.value = 'listening' + existOtherClient = true + authInfo.value = data.auth + break + } + break + case 'on-danmaku': + const danmaku = JSON.parse(data) + switch (danmaku.cmd) { + case 'LIVE_OPEN_PLATFORM_DM': + danmakuClient.value.onDanmaku(danmaku) + break + case 'LIVE_OPEN_PLATFORM_SEND_GIFT': + danmakuClient.value.onGift(danmaku) + break + case 'LIVE_OPEN_PLATFORM_SUPER_CHAT': + danmakuClient.value.onSC(danmaku) + break + case 'LIVE_OPEN_PLATFORM_GUARD': + danmakuClient.value.onGuard(danmaku) + break + default: + danmakuClient.value.onRawMessage(danmaku) + break + } + break + } + } + console.log('[DirectDanmakuClient] 正在检查客户端状态...') + sendBCMessage('check-client') + setTimeout(() => { + if (!connected.value) { + isOwnedDirectDanmakuClient.value = true + initClientInternal(auth) + } else { + console.log( + '[DirectDanmakuClient] 已存在其他页面弹幕客户端, 开始监听 BroadcastChannel...' + ) + } + + setInterval(checkClientStatus, 500) + }, 1000) + } + } + ) + } + isInitializing = false + return useDirectDanmakuClient() + } + function sendBCMessage(type: string, data?: any) { + bc.postMessage({ + type, + data: JSON.stringify(data) + }) + } + function checkClientStatus() { + if (!existOtherClient && !isOwnedDirectDanmakuClient.value) { + //当不存在其他客户端, 且自己不是弹幕客户端 + //则自己成为新的弹幕客户端 + if (status.value != 'initializing') { + console.log('[DirectDanmakuClient] 其他 Client 离线, 开始初始化...') + initClientInternal() + } + } else { + existOtherClient = false //假设其他客户端不存在 + sendBCMessage('check-client') //检查其他客户端是否存在 + } + } + + async function initClientInternal(auth?: AuthInfo) { + status.value = 'initializing' + await navigator.locks.request( + 'danmakuClientInitInternal', + { + ifAvailable: true + }, + async (lock) => { + if (lock) { + // 有锁 + isOwnedDirectDanmakuClient.value = true + const events = danmakuClient.value.events + const eventsAsModel = danmakuClient.value.eventsAsModel + + danmakuClient.value = new OpenLiveClient(auth || null) + + danmakuClient.value.events = events + danmakuClient.value.eventsAsModel = eventsAsModel + const init = async () => { + const result = await danmakuClient.value.Start() + if (result.success) { + authInfo.value = danmakuClient.value.roomAuthInfo + status.value = 'running' + console.log('[DirectDanmakuClient] 初始化成功') + sendBCMessage('response-client-status', { + status: 'running', + auth: authInfo.value + }) + danmakuClient.value.onEvent('all', (data) => { + sendBCMessage('on-danmaku', data) + }) + return true + } else { + console.log( + '[DirectDanmakuClient] 初始化失败, 5秒后重试: ' + result.message + ) + return false + } + } + while (!(await init())) { + await new Promise((resolve) => { + setTimeout(() => { + resolve(true) + }, 5000) + }) + } + } else { + // 无锁 + console.log('[DirectDanmakuClient] 正在等待其他页面弹幕客户端初始化...') + status.value = 'listening' + isOwnedDirectDanmakuClient.value = false + } + } + ) + } + + return { + danmakuClient, + isOwnedDirectDanmakuClient, + status, + connected, + authInfo, + on, + off, + onEvent, + offEvent, + initClient + } +}) diff --git a/src/store/useWebFetcher.ts b/src/store/useWebFetcher.ts index 434d143..04fdeff 100644 --- a/src/store/useWebFetcher.ts +++ b/src/store/useWebFetcher.ts @@ -1,102 +1,200 @@ -import DanmakuClient from '@/data/DanmakuClient' 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 { ref } from 'vue' +import { computed, ref } from 'vue' import { useRoute } from 'vue-router' +import { compress } from 'brotli-compress' export const useWebFetcher = defineStore('WebFetcher', () => { const cookie = useLocalStorage('JWT_Token', '') const route = useRoute() + const startedAt = ref() - const client = new DanmakuClient(null) + const client = ref() + const signalRClient = ref() const events: string[] = [] const isStarted = ref(false) let timer: any - let signalRClient: signalR.HubConnection | null = null let disconnectedByServer = false - - async function Start() { + let useCookie = false + /** + * 是否来自Tauri客户端 + */ + 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) { - return - } - while (!(await connectSignalR())) { - console.log('[WEB-FETCHER] 连接失败, 5秒后重试') - await new Promise((resolve) => setTimeout(resolve, 5000)) + startedAt.value = new Date() + 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 + } + ) + return result } function Stop() { if (!isStarted.value) { return } isStarted.value = false - client.Stop() + client.value?.Stop() + client.value = undefined if (timer) { clearInterval(timer) timer = undefined } - signalRClient?.stop() - signalRClient = null + signalRClient.value?.stop() + signalRClient.value = undefined + startedAt.value = undefined } - async function connectDanmakuClient() { - console.log('[WEB-FETCHER] 正在连接弹幕客户端...') - const result = await client.Start() - if (result.success) { - console.log('[WEB-FETCHER] 加载完成, 开始监听弹幕') - client.onEvent('all', onGetDanmakus) + /************* ✨ 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. + */ + /****** 3431380f-29f6-41b0-801a-7f081b59b4ff *******/ + async function connectDanmakuClient( + type: 'openlive' | 'direct', + directConnectInfo?: { + token: string + roomId: number + tokenUserId: number + buvid: string + } + ) { + if ( + client.value?.state === 'connected' || + client.value?.state === 'connecting' + ) { + return { success: true, message: '弹幕客户端已启动' } + } + console.log(prefix.value + '正在连接弹幕客户端...') + if (!client.value) { + //只有在没有客户端的时候才创建, 并添加事件 + if (type == 'openlive') { + client.value = new OpenLiveClient() + } else { + if (!directConnectInfo) { + return { success: false, message: '未提供弹幕客户端认证信息' } + } + client.value = new DirectClient(directConnectInfo) + } + + client.value?.on('all', (data) => onGetDanmakus(data)) + } + + const result = await client.value?.Start() + if (result?.success) { + console.log(prefix.value + '加载完成, 开始监听弹幕') timer ??= setInterval(() => { sendEvents() }, 1500) } else { - console.log('[WEB-FETCHER] 弹幕客户端启动失败: ' + result.message) + console.log(prefix.value + '弹幕客户端启动失败: ' + result?.message) } return result } async function connectSignalR() { - console.log('[WEB-FETCHER] 正在连接到 vtsuru 服务器...') + console.log(prefix.value + '正在连接到 vtsuru 服务器...') const connection = new signalR.HubConnectionBuilder() .withUrl(BASE_HUB_URL + 'web-fetcher?token=' + route.query.token, { headers: { - Authorization: `Bearer ${cookie.value}`, + Authorization: `Bearer ${cookie.value}` }, skipNegotiation: true, - transport: signalR.HttpTransportType.WebSockets, + transport: signalR.HttpTransportType.WebSockets }) .withAutomaticReconnect([0, 2000, 10000, 30000]) .withHubProtocol(new msgpack.MessagePackHubProtocol()) .build() connection.on('Disconnect', (reason: unknown) => { - console.log('[WEB-FETCHER] 被服务器断开连接: ' + reason) + console.log(prefix.value + '被服务器断开连接: ' + reason) disconnectedByServer = true connection.stop() - signalRClient = null + signalRClient.value = undefined }) - connection.on('ConnectClient', async () => { - if (client.isRunning) { + /*connection.on('ConnectClient', async () => { + if (client?.state === 'connected') { return } let result = await connectDanmakuClient() - while (!result.success) { - console.log('[WEB-FETCHER] 弹幕客户端启动失败, 5秒后重试') + while (!result?.success) { + console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试') await new Promise((resolve) => setTimeout(resolve, 5000)) result = await connectDanmakuClient() } isStarted.value = true disconnectedByServer = false - }) + })*/ connection.onclose(reconnect) try { await connection.start() - console.log('[WEB-FETCHER] 已连接到 vtsuru 服务器') - signalRClient = connection + console.log(prefix.value + '已连接到 vtsuru 服务器') + await connection.send('Finished') + if (isFromClient) { + // 如果来自Tauri客户端,设置自己为VTsuru客户端 + await connection.send('SetAsVTsuruClient') + } + signalRClient.value = connection return true } catch (e) { - console.log('[WEB-FETCHER] 无法连接到 vtsuru 服务器: ' + e) + console.log(prefix.value + '无法连接到 vtsuru 服务器: ' + e) return false } } @@ -105,8 +203,12 @@ export const useWebFetcher = defineStore('WebFetcher', () => { return } try { - await signalRClient?.start() - console.log('[WEB-FETCHER] 已重新连接') + 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秒尝试一次重新启动连接 @@ -116,7 +218,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => { events.push(command) } async function sendEvents() { - if (signalRClient?.state !== 'Connected') { + if (signalRClient.value?.state !== 'Connected') { return } let tempEvents: string[] = [] @@ -129,15 +231,29 @@ export const useWebFetcher = defineStore('WebFetcher', () => { count = events.length } if (tempEvents.length > 0) { - const result = await signalRClient?.invoke<{ + 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 - }>('UploadEvents', tempEvents, false) + }>('UploadEventsCompressed', compressed) if (result?.Success) { events.splice(0, count) - console.log(`[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕`) + console.log( + `[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕` + ) } else { - console.error('[WEB-FETCHER] 上传弹幕失败: ' + result?.Message) + console.error(prefix.value + '上传弹幕失败: ' + result?.Message) } } } @@ -145,7 +261,10 @@ export const useWebFetcher = defineStore('WebFetcher', () => { return { Start, Stop, + restartDanmakuClient, client, + signalRClient, isStarted, + startedAt } }) diff --git a/src/views/IndexView.vue b/src/views/IndexView.vue index 9ea2e39..96f50ea 100644 --- a/src/views/IndexView.vue +++ b/src/views/IndexView.vue @@ -6,17 +6,15 @@ import { BookCoins20Filled, Info24Filled, Lottery24Filled, - MoneyOff24Filled, MoreHorizontal24Filled, TabletSpeaker24Filled, VehicleShip24Filled, - VideoAdd20Filled, + VideoAdd20Filled } from '@vicons/fluent' import { AnalyticsSharp, Calendar, Chatbox, ListCircle, MusicalNote } from '@vicons/ionicons5' import { useWindowSize } from '@vueuse/core' -import { NButton, NCard, NDivider, NEllipsis, NFlex, NGradientText, NGrid, NGridItem, NIcon, NNumberAnimation, NSpace, NText, NTooltip } from 'naive-ui' +import { NButton, NDivider, NEllipsis, NFlex, NGradientText, NGrid, NGridItem, NIcon, NNumberAnimation, NSpace, NText, NTooltip } from 'naive-ui' import { onMounted, ref } from 'vue' -import { stream } from 'xlsx' const { width } = useWindowSize() diff --git a/src/views/obs/DanmujiOBS.vue b/src/views/obs/DanmujiOBS.vue index dc6c171..00b76b5 100644 --- a/src/views/obs/DanmujiOBS.vue +++ b/src/views/obs/DanmujiOBS.vue @@ -5,7 +5,7 @@ import { useDanmakuClient } from '@/store/useDanmakuClient'; // @ts-ignore import * as constants from './blivechat/constants'; // @ts-ignore -import * as chatModels from './blivechat/models'; +import * as chatModels from '../../data/chat/models'; // @ts-ignore import * as pronunciation from './blivechat/utils/pronunciation' // @ts-ignore diff --git a/src/views/obs/LiveRequestOBS.vue b/src/views/obs/LiveRequestOBS.vue index 333fa0d..7542767 100644 --- a/src/views/obs/LiveRequestOBS.vue +++ b/src/views/obs/LiveRequestOBS.vue @@ -12,7 +12,7 @@ import { useElementSize } from '@vueuse/core' import { computed, onMounted, onUnmounted, ref } from 'vue' import { useRoute } from 'vue-router' import { Vue3Marquee } from 'vue3-marquee' -import { NCard, NDivider, NEmpty, NSpace, NText, useMessage } from 'naive-ui' +import { NCard, NDivider, NEmpty, NMessageProvider, NSpace, NText, useMessage } from 'naive-ui' import { List } from 'linqts' const props = defineProps<{ @@ -25,6 +25,7 @@ const currentId = computed(() => { return props.id ?? route.query.id }) +const cardRef = ref() const listContainerRef = ref() const { height, width } = useElementSize(listContainerRef) const itemHeight = 40 @@ -140,7 +141,8 @@ onUnmounted(() => {