add web-fetcher

This commit is contained in:
2024-02-28 16:28:21 +08:00
parent 40fdc93fec
commit f1c06deffd
18 changed files with 527 additions and 84 deletions

View File

@@ -11,6 +11,8 @@
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@microsoft/signalr": "^8.0.0",
"@microsoft/signalr-protocol-msgpack": "^8.0.0",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/eslint-plugin": "^7.0.1",
"@vicons/fluent": "^0.12.0", "@vicons/fluent": "^0.12.0",

View File

@@ -7,7 +7,7 @@ import { ref } from 'vue'
import { APIRoot, AccountInfo, FunctionTypes } from './api-models' import { APIRoot, AccountInfo, FunctionTypes } from './api-models'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
export const ACCOUNT = ref<AccountInfo>() export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
export const isLoadingAccount = ref(true) export const isLoadingAccount = ref(true)
const route = useRoute() const route = useRoute()

View File

@@ -30,6 +30,20 @@ export interface UserInfo {
streamerInfo?: StreamerModel streamerInfo?: StreamerModel
} }
} }
export interface EventFetcherStateModel {
online: boolean
status: { [errorCode: string]: string }
version?: string
todayReceive: number
useCookie: boolean
type: EventFetcherType
}
export enum EventFetcherType {
Application,
OBS,
Server,
}
export interface AccountInfo extends UserInfo { export interface AccountInfo extends UserInfo {
isEmailVerified: boolean isEmailVerified: boolean
isBiliVerified: boolean isBiliVerified: boolean
@@ -41,11 +55,7 @@ export interface AccountInfo extends UserInfo {
biliAuthCode?: string biliAuthCode?: string
biliAuthCodeStatus: BiliAuthCodeStatusType biliAuthCodeStatus: BiliAuthCodeStatusType
eventFetcherOnline: boolean eventFetcherState: EventFetcherStateModel
eventFetcherStatus: string
eventFetcherStatusV3: { [errorCode: string]: string }
eventFetcherTodayReceive: number
eventFetcherVersion?: string
nextSendEmailTime?: number nextSendEmailTime?: number
isServerFetcherOnline: boolean isServerFetcherOnline: boolean

View File

@@ -23,7 +23,7 @@ export async function QueryPostAPIWithParams<T>(
const url = new URL(urlString) const url = new URL(urlString)
url.search = getParams(params) url.search = getParams(params)
headers ??= [] headers ??= []
headers?.push(['Authorization', `Bearer ${cookie.value}`]) if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
if (contentType) headers?.push(['Content-Type', contentType]) if (contentType) headers?.push(['Content-Type', contentType])
@@ -55,7 +55,7 @@ export async function QueryGetAPI<T>(
url.search = getParams(params) url.search = getParams(params)
if (cookie.value) { if (cookie.value) {
headers ??= [] headers ??= []
headers?.push(['Authorization', `Bearer ${cookie.value}`]) if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
} }
try { try {
const data = await fetch(url.toString(), { const data = await fetch(url.toString(), {
@@ -81,6 +81,9 @@ function getParams(params?: [string, string][]) {
if (urlParams.has('as')) { if (urlParams.has('as')) {
resultParams.set('as', urlParams.get('as') || '') resultParams.set('as', urlParams.get('as') || '')
} }
if (urlParams.has('token')) {
resultParams.set('token', urlParams.get('token') || '')
}
return resultParams.toString() return resultParams.toString()
} }
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> { export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> {

View File

@@ -1,17 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { EventFetcherType } from '@/api/api-models'
import { FlashCheckmark16Filled, Info24Filled } from '@vicons/fluent' import { FlashCheckmark16Filled, Info24Filled } from '@vicons/fluent'
import { NAlert, NButton, NDivider, NIcon, NTag, NText, NTooltip } from 'naive-ui' import { NAlert, NButton, NDivider, NIcon, NTag, NText, NTooltip } from 'naive-ui'
import { computed } from 'vue' import { computed } from 'vue'
const accountInfo = useAccount() const accountInfo = useAccount()
const state = accountInfo.value?.eventFetcherState
const status = computed(() => { const status = computed(() => {
if (!accountInfo.value) return 'error' if (state.online == true) {
if (accountInfo.value.eventFetcherOnline == true || accountInfo.value.isServerFetcherOnline == true) { if (state.status == undefined || state.status == null) {
if (accountInfo.value.eventFetcherStatus) {
return 'warning' return 'warning'
} else if (Object.keys(accountInfo.value.eventFetcherStatusV3 ?? {}).length > 0) { } else if (Object.keys(state.status ?? {}).length > 0) {
return 'warning' return 'warning'
} else { } else {
return 'success' return 'success'
@@ -23,7 +24,7 @@ const status = computed(() => {
</script> </script>
<template> <template>
<NAlert :type="status"> <NAlert :type="status" v-if="status">
<template #header> <template #header>
EVENT-FETCHER 状态 EVENT-FETCHER 状态
<NTooltip> <NTooltip>
@@ -50,7 +51,7 @@ const status = computed(() => {
<template #trigger> <template #trigger>
<NTag size="small" :color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }"> <NTag size="small" :color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }">
<NIcon :component="FlashCheckmark16Filled" /> <NIcon :component="FlashCheckmark16Filled" />
{{ accountInfo?.eventFetcherVersion ?? '未知' }} {{ state.type == EventFetcherType.OBS ? 'OBS/网页端' : state.version ?? '未知' }}
</NTag> </NTag>
</template> </template>
你所使用的版本 你所使用的版本
@@ -58,7 +59,7 @@ const status = computed(() => {
<NDivider vertical /> <NDivider vertical />
</template> </template>
<NTag :type="status"> <NTag :type="status">
<template v-if="accountInfo?.eventFetcherOnline == true && accountInfo?.eventFetcherStatus"> <template v-if="state?.online == true && (state?.status == null || state?.status == undefined)">
此版本已过期, 请更新 此版本已过期, 请更新
<NTooltip trigger="click"> <NTooltip trigger="click">
<template #trigger> <template #trigger>
@@ -75,19 +76,17 @@ const status = computed(() => {
</NText> </NText>
| 今日已接收 | 今日已接收
<NText color="white" strong> <NText color="white" strong>
{{ accountInfo?.eventFetcherTodayReceive }} {{ state.todayReceive }}
</NText> </NText>
</template> </template>
<template v-else-if="status == 'warning'"> <template v-else-if="status == 'warning'">
<template v-if="accountInfo?.eventFetcherStatusV3"> <template v-if="state.status"> 异常: {{ Object.values(state.status).join('; ') }} </template>
异常: {{ Object.values(accountInfo.eventFetcherStatusV3).join('; ') }}
</template>
</template> </template>
<template v-else-if="status == 'info'"> 未连接 </template> <template v-else-if="status == 'info'"> 未连接 </template>
</template> </template>
</NTag> </NTag>
<template v-if="accountInfo?.eventFetcherOnline != true"> <template v-if="!state.online">
<NDivider vertical /> <NDivider vertical />
<NButton <NButton
type="info" type="info"

View File

@@ -159,6 +159,7 @@ interface DanmakuEventsMap {
gift: (arg1: GiftInfo, arg2?: any) => void gift: (arg1: GiftInfo, arg2?: any) => void
sc: (arg1: SCInfo, arg2?: any) => void sc: (arg1: SCInfo, arg2?: any) => void
guard: (arg1: GuardInfo, arg2?: any) => void guard: (arg1: GuardInfo, arg2?: any) => void
all: (arg1: any) => void
} }
export default class DanmakuClient { export default class DanmakuClient {
@@ -167,41 +168,63 @@ export default class DanmakuClient {
} }
private client: any private client: any
private authInfo: AuthInfo | null
private timer: any | undefined private timer: any | undefined
private isStarting = false
public authInfo: AuthInfo | null
public roomAuthInfo = ref<RoomAuthInfo>({} as RoomAuthInfo) public roomAuthInfo = ref<RoomAuthInfo>({} as RoomAuthInfo)
public authCode: string | undefined public authCode: string | undefined
public isRunning: boolean = false
private events: { private events: {
danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[] danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[]
gift: ((arg1: GiftInfo, arg2?: any) => void)[] gift: ((arg1: GiftInfo, arg2?: any) => void)[]
sc: ((arg1: SCInfo, arg2?: any) => void)[] sc: ((arg1: SCInfo, arg2?: any) => void)[]
guard: ((arg1: GuardInfo, arg2?: any) => void)[] guard: ((arg1: GuardInfo, arg2?: any) => void)[]
all: ((arg1: any) => void)[]
} = { } = {
danmaku: [], danmaku: [],
gift: [], gift: [],
sc: [], sc: [],
guard: [], guard: [],
all: [],
} }
private eventsAsModel: { private eventsAsModel: {
danmaku: ((arg1: EventModel, arg2?: any) => void)[] danmaku: ((arg1: EventModel, arg2?: any) => void)[]
gift: ((arg1: EventModel, arg2?: any) => void)[] gift: ((arg1: EventModel, arg2?: any) => void)[]
sc: ((arg1: EventModel, arg2?: any) => void)[] sc: ((arg1: EventModel, arg2?: any) => void)[]
guard: ((arg1: EventModel, arg2?: any) => void)[] guard: ((arg1: EventModel, arg2?: any) => void)[]
all: ((arg1: any) => void)[]
} = { } = {
danmaku: [], danmaku: [],
gift: [], gift: [],
sc: [], sc: [],
guard: [], guard: [],
all: [],
} }
public async Start(): Promise<{ success: boolean; message: string }> { public async Start(): Promise<{ success: boolean; message: string }> {
if (this.isRunning) {
return {
success: false,
message: '弹幕客户端已启动',
}
}
if (this.isStarting) {
return {
success: false,
message: '弹幕客户端正在启动',
}
}
this.isStarting = true
try {
if (!this.client) { if (!this.client) {
console.log('[OPEN-LIVE] 正在启动弹幕客户端') console.log('[OPEN-LIVE] 正在启动弹幕客户端')
const result = await this.initClient() const result = await this.initClient()
if (result.success) { if (result.success) {
this.timer = setInterval(() => { this.isRunning = true
this.timer ??= setInterval(() => {
this.sendHeartbeat() this.sendHeartbeat()
}, 20 * 1000) }, 20 * 1000)
} }
@@ -213,8 +236,15 @@ export default class DanmakuClient {
message: '弹幕客户端已被启动过', message: '弹幕客户端已被启动过',
} }
} }
} finally {
this.isStarting = false
}
} }
public Stop() { public Stop() {
if (!this.isRunning) {
return
}
this.isRunning = false
if (this.client) { if (this.client) {
console.log('[OPEN-LIVE] 正在停止弹幕客户端') console.log('[OPEN-LIVE] 正在停止弹幕客户端')
this.client.stop() this.client.stop()
@@ -223,35 +253,51 @@ export default class DanmakuClient {
} }
if (this.timer) { if (this.timer) {
clearInterval(this.timer) clearInterval(this.timer)
this.timer = undefined
} }
this.events = { this.events = {
danmaku: [], danmaku: [],
gift: [], gift: [],
sc: [], sc: [],
guard: [], guard: [],
all: [],
} }
this.eventsAsModel = { this.eventsAsModel = {
danmaku: [], danmaku: [],
gift: [], gift: [],
sc: [], sc: [],
guard: [], guard: [],
all: [],
} }
} }
private sendHeartbeat() { private sendHeartbeat() {
if (this.client) { if (!this.isRunning) {
clearInterval(this.timer)
this.timer = undefined
return
}
const query = this.authInfo const query = this.authInfo
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo) ? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal') : QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
query.then((data) => { query.then((data) => {
if (data.code != 200) { if (data.code != 200) {
console.error('[OPEN-LIVE] 心跳失败') console.error('[OPEN-LIVE] 心跳失败, 将重新连接')
this.client.stop() this.client?.stop()
this.client = null this.client = null
this.initClient() this.initClient()
} }
}) })
} }
private onRawMessage = (command: any) => {
this.eventsAsModel.all?.forEach((d) => {
d(command)
})
this.events.all?.forEach((d) => {
d(command)
})
} }
private onDanmaku = (command: any) => { private onDanmaku = (command: any) => {
const data = command.data as DanmakuInfo const data = command.data as DanmakuInfo
@@ -378,7 +424,8 @@ export default class DanmakuClient {
public onEvent(eventName: 'gift', 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: 'sc', listener: (arg1: EventModel, arg2?: any) => void): this
public onEvent(eventName: 'guard', listener: (arg1: EventModel, arg2?: any) => void): this public onEvent(eventName: 'guard', listener: (arg1: EventModel, arg2?: any) => void): this
public onEvent(eventName: 'danmaku' | 'gift' | 'sc' | 'guard', listener: (...args: 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]) { if (!this.eventsAsModel[eventName]) {
this.eventsAsModel[eventName] = [] this.eventsAsModel[eventName] = []
} }
@@ -418,7 +465,7 @@ export default class DanmakuClient {
message: '', message: '',
} }
} else { } else {
console.log('[OPEN-LIVE] 无法开启场次') console.log('[OPEN-LIVE] 无法开启场次: ' + auth.message)
return { return {
success: false, success: false,
message: auth.message, message: auth.message,
@@ -456,5 +503,6 @@ export default class DanmakuClient {
LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift.bind(this), LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift.bind(this),
LIVE_OPEN_PLATFORM_SUPER_CHAT: this.onSC.bind(this), LIVE_OPEN_PLATFORM_SUPER_CHAT: this.onSC.bind(this),
LIVE_OPEN_PLATFORM_GUARD: this.onGuard.bind(this), LIVE_OPEN_PLATFORM_GUARD: this.onGuard.bind(this),
RAW_MESSAGE: this.onRawMessage.bind(this),
} }
} }

View File

@@ -134,6 +134,13 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
} }
this.onDelSuperChat({ ids }) this.onDelSuperChat({ ids })
} }
rawMessageCallback(command) {
if (!this.onRawMessage) {
return
}
this.onRawMessage(command)
}
} }
const CMD_CALLBACK_MAP = { const CMD_CALLBACK_MAP = {
@@ -142,4 +149,5 @@ const CMD_CALLBACK_MAP = {
LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback, LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback,
LIVE_OPEN_PLATFORM_SUPER_CHAT: ChatClientDirectOpenLive.prototype.superChatCallback, LIVE_OPEN_PLATFORM_SUPER_CHAT: ChatClientDirectOpenLive.prototype.superChatCallback,
LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL: ChatClientDirectOpenLive.prototype.superChatDelCallback, LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL: ChatClientDirectOpenLive.prototype.superChatDelCallback,
RAW_MESSAGE: ChatClientDirectOpenLive.prototype.rawMessageCallback,
} }

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// @ts-nocheck
import { BrotliDecode } from './brotli_decode' import { BrotliDecode } from './brotli_decode'
import { setInterval, clearInterval, setTimeout, clearTimeout } from 'worker-timers' import { setInterval, clearInterval, setTimeout, clearTimeout } from 'worker-timers'
@@ -145,7 +147,7 @@ export default class ChatClientOfficialBase {
this.sendAuth() this.sendAuth()
this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL) this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
this.refreshReceiveTimeoutTimer() this.refreshReceiveTimeoutTimer()
console.log('ws 已连接') //console.log('ws 已连接')
} }
sendHeartbeat() { sendHeartbeat() {
@@ -270,7 +272,10 @@ export default class ChatClientOfficialBase {
// 没压缩过的直接反序列化 // 没压缩过的直接反序列化
if (body.length !== 0) { if (body.length !== 0) {
try { try {
body = JSON.parse(textDecoder.decode(body)) const text = textDecoder.decode(body)
this.onRawMessage(text)
this.CMD_CALLBACK_MAP['RAW_MESSAGE']?.call(this, text)
body = JSON.parse(text)
this.handlerCommand(body) this.handlerCommand(body)
} catch (e) { } catch (e) {
console.error('body=', body) console.error('body=', body)
@@ -299,6 +304,7 @@ export default class ChatClientOfficialBase {
} }
} }
} }
onRawMessage(command) {}
handlerCommand(command) { handlerCommand(command) {
let cmd = command.cmd || '' let cmd = command.cmd || ''

View File

@@ -2,8 +2,8 @@ import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemp
import { defineAsyncComponent, ref } from 'vue' import { defineAsyncComponent, ref } from 'vue'
const debugAPI = import.meta.env.VITE_DEBUG_API const debugAPI = import.meta.env.VITE_DEBUG_API
const releseAPI = `https://vtsuru.suki.club/api/` const releseAPI = `https://vtsuru.suki.club/`
const failoverAPI = `https://failover-api.vtsuru.suki.club/api/` const failoverAPI = `https://failover-api.vtsuru.suki.club/`
export const isBackendUsable = ref(true) export const isBackendUsable = ref(true)
@@ -13,32 +13,38 @@ export const IMGUR_URL = FILE_BASE_URL + '/imgur/'
export const THINGS_URL = FILE_BASE_URL + '/things/' export const THINGS_URL = FILE_BASE_URL + '/things/'
export const apiFail = ref(false) export const apiFail = ref(false)
export const BASE_API = () => export const BASE_API = {
process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI toString: () =>
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'api/',
}
export const FETCH_API = 'https://fetch.vtsuru.live/' export const FETCH_API = 'https://fetch.vtsuru.live/'
export const BASE_HUB_URL = {
toString: () =>
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'hub/',
}
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0' export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
export const USER_API_URL = { toString: () => `${BASE_API()}user/` } export const USER_API_URL = { toString: () => `${BASE_API}user/` }
export const ACCOUNT_API_URL = { toString: () => `${BASE_API()}account/` } export const ACCOUNT_API_URL = { toString: () => `${BASE_API}account/` }
export const BILI_API_URL = { toString: () => `${BASE_API()}bili/` } export const BILI_API_URL = { toString: () => `${BASE_API}bili/` }
export const SONG_API_URL = { toString: () => `${BASE_API()}song-list/` } export const SONG_API_URL = { toString: () => `${BASE_API}song-list/` }
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API()}notifaction/` } export const NOTIFACTION_API_URL = { toString: () => `${BASE_API}notifaction/` }
export const QUESTION_API_URL = { toString: () => `${BASE_API()}qa/` } export const QUESTION_API_URL = { toString: () => `${BASE_API}qa/` }
export const LOTTERY_API_URL = { toString: () => `${BASE_API()}lottery/` } export const LOTTERY_API_URL = { toString: () => `${BASE_API}lottery/` }
export const HISTORY_API_URL = { toString: () => `${BASE_API()}history/` } export const HISTORY_API_URL = { toString: () => `${BASE_API}history/` }
export const SCHEDULE_API_URL = { toString: () => `${BASE_API()}schedule/` } export const SCHEDULE_API_URL = { toString: () => `${BASE_API}schedule/` }
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API()}video-collect/` } export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API}video-collect/` }
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API()}open-live/` } export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API}open-live/` }
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API()}song-request/` } export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API}song-request/` }
export const QUEUE_API_URL = { toString: () => `${BASE_API()}queue/` } export const QUEUE_API_URL = { toString: () => `${BASE_API}queue/` }
export const EVENT_API_URL = { toString: () => `${BASE_API()}event/` } export const EVENT_API_URL = { toString: () => `${BASE_API}event/` }
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` } export const LIVE_API_URL = { toString: () => `${BASE_API}live/` }
export const FEEDBACK_API_URL = { toString: () => `${BASE_API()}feedback/` } export const FEEDBACK_API_URL = { toString: () => `${BASE_API}feedback/` }
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API()}music-request/` } export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API}music-request/` }
export const VTSURU_API_URL = { toString: () => `${BASE_API()}vtsuru/` } export const VTSURU_API_URL = { toString: () => `${BASE_API}vtsuru/` }
export const POINT_API_URL = { toString: () => `${BASE_API()}point/` } export const POINT_API_URL = { toString: () => `${BASE_API}point/` }
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API()}bili-auth/` } export const BILI_AUTH_API_URL = { toString: () => `${BASE_API}bili-auth/` }
export const ScheduleTemplateMap = { export const ScheduleTemplateMap = {
'': { '': {

View File

@@ -19,7 +19,7 @@ let currentVersion: string
let isHaveNewVersion = false let isHaveNewVersion = false
const { notification } = createDiscreteApi(['notification']) const { notification } = createDiscreteApi(['notification'])
QueryGetAPI<string>(BASE_API() + 'vtsuru/version') QueryGetAPI<string>(BASE_API + 'vtsuru/version')
.then((version) => { .then((version) => {
if (version.code == 200) { if (version.code == 200) {
currentVersion = version.data currentVersion = version.data
@@ -42,7 +42,7 @@ QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
if (isHaveNewVersion) { if (isHaveNewVersion) {
return return
} }
QueryGetAPI<string>(BASE_API() + 'vtsuru/version').then((keepCheckData) => { QueryGetAPI<string>(BASE_API + 'vtsuru/version').then((keepCheckData) => {
if (keepCheckData.code == 200 && keepCheckData.data != currentVersion) { if (keepCheckData.code == 200 && keepCheckData.data != currentVersion) {
isHaveNewVersion = true isHaveNewVersion = true
currentVersion = version.data currentVersion = version.data

View File

@@ -42,5 +42,13 @@ export default {
title: '棉花糖展示', title: '棉花糖展示',
}, },
}, },
{
path: 'web-fetcher',
name: 'obs-web-fetcher',
component: () => import('@/views/obs/WebFetcherOBS.vue'),
meta: {
title: '弹幕收集器 (OBS版',
},
},
], ],
} }

150
src/store/useWebFetcher.ts Normal file
View File

@@ -0,0 +1,150 @@
import DanmakuClient from '@/data/DanmakuClient'
import { BASE_HUB_URL } from '@/data/constants'
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 { useRoute } from 'vue-router'
export const useWebFetcher = defineStore('WebFetcher', () => {
const cookie = useLocalStorage('JWT_Token', '')
const route = useRoute()
const client = new DanmakuClient(null)
const events: string[] = []
const isStarted = ref(false)
let timer: any
let signalRClient: signalR.HubConnection | null = null
let disconnectedByServer = false
async function Start() {
if (isStarted.value) {
return
}
while (!(await connectSignalR())) {
console.log('[WEB-FETCHER] 连接失败, 5秒后重试')
await new Promise((resolve) => setTimeout(resolve, 5000))
}
}
function Stop() {
if (!isStarted.value) {
return
}
isStarted.value = false
client.Stop()
if (timer) {
clearInterval(timer)
timer = undefined
}
signalRClient?.stop()
signalRClient = null
}
async function connectDanmakuClient() {
console.log('[WEB-FETCHER] 正在连接弹幕客户端...')
const result = await client.Start()
if (result.success) {
console.log('[WEB-FETCHER] 加载完成, 开始监听弹幕')
client.onEvent('all', onGetDanmakus)
timer ??= setInterval(() => {
sendEvents()
}, 1500)
} else {
console.log('[WEB-FETCHER] 弹幕客户端启动失败: ' + result.message)
}
return result
}
async function connectSignalR() {
console.log('[WEB-FETCHER] 正在连接到 vtsuru 服务器...')
const connection = new signalR.HubConnectionBuilder()
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + route.query.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('[WEB-FETCHER] 被服务器断开连接: ' + reason)
disconnectedByServer = true
connection.stop()
signalRClient = null
})
connection.on('ConnectClient', async () => {
if (client.isRunning) {
return
}
let result = await connectDanmakuClient()
while (!result.success) {
console.log('[WEB-FETCHER] 弹幕客户端启动失败, 5秒后重试')
await new Promise((resolve) => setTimeout(resolve, 5000))
result = await connectDanmakuClient()
}
isStarted.value = true
})
connection.onclose(reconnect)
try {
await connection.start()
console.log('[WEB-FETCHER] 已连接到 vtsuru 服务器')
signalRClient = connection
return true
} catch (e) {
console.log('[WEB-FETCHER] 无法连接到 vtsuru 服务器: ' + e)
return false
}
}
async function reconnect() {
if (disconnectedByServer) {
return
}
try {
await signalRClient?.start()
console.log('[WEB-FETCHER] 已重新连接')
} catch (err) {
console.log(err)
setTimeout(reconnect, 5000) // 如果连接失败则每5秒尝试一次重新启动连接
}
}
function onGetDanmakus(command: any) {
events.push(command)
}
async function sendEvents() {
if (signalRClient?.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 result = await signalRClient?.invoke<{
Success: boolean
Message: string
}>('UploadEvents', tempEvents, false)
if (result?.Success) {
events.splice(0, count)
console.log(`[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕`)
} else {
console.error('[WEB-FETCHER] 上传弹幕失败: ' + result?.Message)
}
}
}
return {
Start,
Stop,
client,
isStarted,
}
})

View File

@@ -454,7 +454,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<NLayout v-if="accountInfo" style="height: 100vh"> <NLayout v-if="accountInfo.id" style="height: 100vh">
<NLayoutHeader bordered style="height: 50px; padding: 10px 15px 5px 15px"> <NLayoutHeader bordered style="height: 50px; padding: 10px 15px 5px 15px">
<NPageHeader> <NPageHeader>
<template #title> <template #title>

View File

@@ -11,7 +11,7 @@ const props = defineProps<{
const accountInfo = useAccount() const accountInfo = useAccount()
const message = useMessage() const message = useMessage()
let client = new DanmakuClient(null) const client = new DanmakuClient(null)
const isClientLoading = ref(true) const isClientLoading = ref(true)
onMounted(async () => { onMounted(async () => {

View File

@@ -53,6 +53,8 @@ onMounted(() => {
checkIfChanged() checkIfChanged()
}, 1000) }, 1000)
//@ts-expect-error 这里获取不了
if (window.obsstudio) {
//@ts-expect-error 这里获取不了 //@ts-expect-error 这里获取不了
window.obsstudio.onVisibilityChange = function (visibility: boolean) { window.obsstudio.onVisibilityChange = function (visibility: boolean) {
visiable.value = visibility visiable.value = visibility
@@ -61,6 +63,7 @@ onMounted(() => {
window.obsstudio.onActiveChange = function (a: boolean) { window.obsstudio.onActiveChange = function (a: boolean) {
active.value = a active.value = a
} }
}
}) })
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer) clearInterval(timer)

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { useWebFetcher } from '@/store/useWebFetcher'
import { onMounted, onUnmounted, ref } from 'vue'
const webFetcher = useWebFetcher()
onMounted(() => {
webFetcher.Start()
})
onUnmounted(() => {})
</script>
<template>
<div
class="web-fetcher-status"
:style="{
backgroundColor: webFetcher.isStarted ? '#6dc56d' : '#e34a4a',
}"
></div>
</template>
<style scoped>
.web-fetcher-status {
width: 30px;
height: 30px;
border-radius: 50%;
box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.2);
border: 3px solid #00000033;
animation: animated-border 3s infinite;
transition: background-color 0.5s;
}
@keyframes animated-border {
0% {
box-shadow: 0 0 0px #589580;
}
100% {
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0);
}
}
</style>

0
src/views/others Normal file
View File

161
yarn.lock
View File

@@ -754,6 +754,36 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@microsoft/signalr-protocol-msgpack@npm:^8.0.0":
version: 8.0.0
resolution: "@microsoft/signalr-protocol-msgpack@npm:8.0.0"
dependencies:
"@microsoft/signalr": "npm:>=8.0.0"
"@msgpack/msgpack": "npm:^2.7.0"
checksum: 9ee6846464dc2bcd6da8db691aad4a4e6c1d0b3baa1d291cf588e1cf3999b03d28e7f6e47b31730f4a4d5529bc67ca7b60f8acae1322fd9d05e48b30fcdd4537
languageName: node
linkType: hard
"@microsoft/signalr@npm:>=8.0.0, @microsoft/signalr@npm:^8.0.0":
version: 8.0.0
resolution: "@microsoft/signalr@npm:8.0.0"
dependencies:
abort-controller: "npm:^3.0.0"
eventsource: "npm:^2.0.2"
fetch-cookie: "npm:^2.0.3"
node-fetch: "npm:^2.6.7"
ws: "npm:^7.4.5"
checksum: 215ac66c9262803ae41c8499739be6fa9a66eeb4f5cb9a9c4f3bd23683a6ad011bb61631fe8257dceda53cbfdb59068b7902d8a5cfc94e2a61d3500196cba172
languageName: node
linkType: hard
"@msgpack/msgpack@npm:^2.7.0":
version: 2.8.0
resolution: "@msgpack/msgpack@npm:2.8.0"
checksum: 5964ed3daad9ccf314238da81c91152dc693bca167b2469445c1d3ce0495443612e543d052281061a91ec48ed44a6a49dd3a334b5d0dbe2dc2db6ea6143e5787
languageName: node
linkType: hard
"@nodelib/fs.scandir@npm:2.1.5": "@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5 version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5" resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2826,6 +2856,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eventsource@npm:^2.0.2":
version: 2.0.2
resolution: "eventsource@npm:2.0.2"
checksum: 0b8c70b35e45dd20f22ff64b001be9d530e33b92ca8bdbac9e004d0be00d957ab02ef33c917315f59bf2f20b178c56af85c52029bc8e6cc2d61c31d87d943573
languageName: node
linkType: hard
"evtd@npm:^0.2.2, evtd@npm:^0.2.4": "evtd@npm:^0.2.2, evtd@npm:^0.2.4":
version: 0.2.4 version: 0.2.4
resolution: "evtd@npm:0.2.4" resolution: "evtd@npm:0.2.4"
@@ -2920,6 +2957,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fetch-cookie@npm:^2.0.3":
version: 2.2.0
resolution: "fetch-cookie@npm:2.2.0"
dependencies:
set-cookie-parser: "npm:^2.4.8"
tough-cookie: "npm:^4.0.0"
checksum: bb6bec943ae0fc0d442661838b8ecc43310d34b0a2509124f6f79dabae012dd23e54b7827d20236fb0f98fb07fe493460056d70634b59ba7421862a3dfa68dd9
languageName: node
linkType: hard
"file-entry-cache@npm:^6.0.1": "file-entry-cache@npm:^6.0.1":
version: 6.0.1 version: 6.0.1
resolution: "file-entry-cache@npm:6.0.1" resolution: "file-entry-cache@npm:6.0.1"
@@ -4171,6 +4218,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"node-fetch@npm:^2.6.7":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
languageName: node
linkType: hard
"node-gyp@npm:latest": "node-gyp@npm:latest":
version: 10.0.1 version: 10.0.1
resolution: "node-gyp@npm:10.0.1" resolution: "node-gyp@npm:10.0.1"
@@ -4510,6 +4571,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"psl@npm:^1.1.33":
version: 1.9.0
resolution: "psl@npm:1.9.0"
checksum: 6a3f805fdab9442f44de4ba23880c4eba26b20c8e8e0830eff1cb31007f6825dace61d17203c58bfe36946842140c97a1ba7f67bc63ca2d88a7ee052b65d97ab
languageName: node
linkType: hard
"punycode.js@npm:^2.3.1": "punycode.js@npm:^2.3.1":
version: 2.3.1 version: 2.3.1
resolution: "punycode.js@npm:2.3.1" resolution: "punycode.js@npm:2.3.1"
@@ -4517,7 +4585,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"punycode@npm:^2.1.0": "punycode@npm:^2.1.0, punycode@npm:^2.1.1":
version: 2.3.1 version: 2.3.1
resolution: "punycode@npm:2.3.1" resolution: "punycode@npm:2.3.1"
checksum: 14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 checksum: 14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
@@ -4533,6 +4601,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"querystringify@npm:^2.1.1":
version: 2.2.0
resolution: "querystringify@npm:2.2.0"
checksum: 3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2": "queue-microtask@npm:^1.2.2":
version: 1.2.3 version: 1.2.3
resolution: "queue-microtask@npm:1.2.3" resolution: "queue-microtask@npm:1.2.3"
@@ -4609,6 +4684,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
checksum: b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267
languageName: node
linkType: hard
"resize-detector@npm:^0.3.0": "resize-detector@npm:^0.3.0":
version: 0.3.0 version: 0.3.0
resolution: "resize-detector@npm:0.3.0" resolution: "resize-detector@npm:0.3.0"
@@ -4817,6 +4899,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"set-cookie-parser@npm:^2.4.8":
version: 2.6.0
resolution: "set-cookie-parser@npm:2.6.0"
checksum: 739da029f0e56806a103fcd5501d9c475e19e77bd8274192d7ae5c374ae714a82bba9a7ac00b0330a18227c5644b08df9e442240527be578f5a6030f9bb2bb80
languageName: node
linkType: hard
"set-function-length@npm:^1.1.1": "set-function-length@npm:^1.1.1":
version: 1.1.1 version: 1.1.1
resolution: "set-function-length@npm:1.1.1" resolution: "set-function-length@npm:1.1.1"
@@ -5198,6 +5287,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tough-cookie@npm:^4.0.0":
version: 4.1.3
resolution: "tough-cookie@npm:4.1.3"
dependencies:
psl: "npm:^1.1.33"
punycode: "npm:^2.1.1"
universalify: "npm:^0.2.0"
url-parse: "npm:^1.5.3"
checksum: 4fc0433a0cba370d57c4b240f30440c848906dee3180bb6e85033143c2726d322e7e4614abb51d42d111ebec119c4876ed8d7247d4113563033eebbc1739c831
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
languageName: node
linkType: hard
"treemate@npm:^0.3.11": "treemate@npm:^0.3.11":
version: 0.3.11 version: 0.3.11
resolution: "treemate@npm:0.3.11" resolution: "treemate@npm:0.3.11"
@@ -5367,6 +5475,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"universalify@npm:^0.2.0":
version: 0.2.0
resolution: "universalify@npm:0.2.0"
checksum: cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe
languageName: node
linkType: hard
"unplugin-vue-markdown@npm:^0.26.0": "unplugin-vue-markdown@npm:^0.26.0":
version: 0.26.0 version: 0.26.0
resolution: "unplugin-vue-markdown@npm:0.26.0" resolution: "unplugin-vue-markdown@npm:0.26.0"
@@ -5419,6 +5534,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"url-parse@npm:^1.5.3":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
dependencies:
querystringify: "npm:^2.1.1"
requires-port: "npm:^1.0.0"
checksum: bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2": "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "util-deprecate@npm:1.0.2" resolution: "util-deprecate@npm:1.0.2"
@@ -5522,6 +5647,8 @@ __metadata:
resolution: "vtsuru.live@workspace:." resolution: "vtsuru.live@workspace:."
dependencies: dependencies:
"@eslint/eslintrc": "npm:^3.0.1" "@eslint/eslintrc": "npm:^3.0.1"
"@microsoft/signalr": "npm:^8.0.0"
"@microsoft/signalr-protocol-msgpack": "npm:^8.0.0"
"@types/eslint": "npm:^8.56.2" "@types/eslint": "npm:^8.56.2"
"@types/node": "npm:^20.11.19" "@types/node": "npm:^20.11.19"
"@types/obs-studio": "npm:^2.17.2" "@types/obs-studio": "npm:^2.17.2"
@@ -5765,6 +5892,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: 5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
languageName: node
linkType: hard
"webpack-sources@npm:^3.2.3": "webpack-sources@npm:^3.2.3":
version: 3.2.3 version: 3.2.3
resolution: "webpack-sources@npm:3.2.3" resolution: "webpack-sources@npm:3.2.3"
@@ -5779,6 +5913,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: 1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
languageName: node
linkType: hard
"which-boxed-primitive@npm:^1.0.2": "which-boxed-primitive@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "which-boxed-primitive@npm:1.0.2" resolution: "which-boxed-primitive@npm:1.0.2"
@@ -5904,6 +6048,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ws@npm:^7.4.5":
version: 7.5.9
resolution: "ws@npm:7.5.9"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494
languageName: node
linkType: hard
"xlsx@npm:^0.18.5": "xlsx@npm:^0.18.5":
version: 0.18.5 version: 0.18.5
resolution: "xlsx@npm:0.18.5" resolution: "xlsx@npm:0.18.5"