mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
add web-fetcher
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"@microsoft/signalr-protocol-msgpack": "^8.0.0",
|
||||
"@types/node": "^20.11.19",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@vicons/fluent": "^0.12.0",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ref } from 'vue'
|
||||
import { APIRoot, AccountInfo, FunctionTypes } from './api-models'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export const ACCOUNT = ref<AccountInfo>()
|
||||
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
|
||||
export const isLoadingAccount = ref(true)
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@@ -30,6 +30,20 @@ export interface UserInfo {
|
||||
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 {
|
||||
isEmailVerified: boolean
|
||||
isBiliVerified: boolean
|
||||
@@ -41,11 +55,7 @@ export interface AccountInfo extends UserInfo {
|
||||
biliAuthCode?: string
|
||||
biliAuthCodeStatus: BiliAuthCodeStatusType
|
||||
|
||||
eventFetcherOnline: boolean
|
||||
eventFetcherStatus: string
|
||||
eventFetcherStatusV3: { [errorCode: string]: string }
|
||||
eventFetcherTodayReceive: number
|
||||
eventFetcherVersion?: string
|
||||
eventFetcherState: EventFetcherStateModel
|
||||
|
||||
nextSendEmailTime?: number
|
||||
isServerFetcherOnline: boolean
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function QueryPostAPIWithParams<T>(
|
||||
const url = new URL(urlString)
|
||||
url.search = getParams(params)
|
||||
headers ??= []
|
||||
headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
|
||||
if (contentType) headers?.push(['Content-Type', contentType])
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function QueryGetAPI<T>(
|
||||
url.search = getParams(params)
|
||||
if (cookie.value) {
|
||||
headers ??= []
|
||||
headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
}
|
||||
try {
|
||||
const data = await fetch(url.toString(), {
|
||||
@@ -81,6 +81,9 @@ function getParams(params?: [string, string][]) {
|
||||
if (urlParams.has('as')) {
|
||||
resultParams.set('as', urlParams.get('as') || '')
|
||||
}
|
||||
if (urlParams.has('token')) {
|
||||
resultParams.set('token', urlParams.get('token') || '')
|
||||
}
|
||||
return resultParams.toString()
|
||||
}
|
||||
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account'
|
||||
import { EventFetcherType } from '@/api/api-models'
|
||||
import { FlashCheckmark16Filled, Info24Filled } from '@vicons/fluent'
|
||||
import { NAlert, NButton, NDivider, NIcon, NTag, NText, NTooltip } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const accountInfo = useAccount()
|
||||
const state = accountInfo.value?.eventFetcherState
|
||||
|
||||
const status = computed(() => {
|
||||
if (!accountInfo.value) return 'error'
|
||||
if (accountInfo.value.eventFetcherOnline == true || accountInfo.value.isServerFetcherOnline == true) {
|
||||
if (accountInfo.value.eventFetcherStatus) {
|
||||
if (state.online == true) {
|
||||
if (state.status == undefined || state.status == null) {
|
||||
return 'warning'
|
||||
} else if (Object.keys(accountInfo.value.eventFetcherStatusV3 ?? {}).length > 0) {
|
||||
} else if (Object.keys(state.status ?? {}).length > 0) {
|
||||
return 'warning'
|
||||
} else {
|
||||
return 'success'
|
||||
@@ -23,7 +24,7 @@ const status = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert :type="status">
|
||||
<NAlert :type="status" v-if="status">
|
||||
<template #header>
|
||||
EVENT-FETCHER 状态
|
||||
<NTooltip>
|
||||
@@ -50,7 +51,7 @@ const status = computed(() => {
|
||||
<template #trigger>
|
||||
<NTag size="small" :color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }">
|
||||
<NIcon :component="FlashCheckmark16Filled" />
|
||||
{{ accountInfo?.eventFetcherVersion ?? '未知' }}
|
||||
{{ state.type == EventFetcherType.OBS ? 'OBS/网页端' : state.version ?? '未知' }}
|
||||
</NTag>
|
||||
</template>
|
||||
你所使用的版本
|
||||
@@ -58,7 +59,7 @@ const status = computed(() => {
|
||||
<NDivider vertical />
|
||||
</template>
|
||||
<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">
|
||||
<template #trigger>
|
||||
@@ -75,19 +76,17 @@ const status = computed(() => {
|
||||
</NText>
|
||||
| 今日已接收
|
||||
<NText color="white" strong>
|
||||
{{ accountInfo?.eventFetcherTodayReceive }}
|
||||
{{ state.todayReceive }}
|
||||
</NText>
|
||||
条
|
||||
</template>
|
||||
<template v-else-if="status == 'warning'">
|
||||
<template v-if="accountInfo?.eventFetcherStatusV3">
|
||||
异常: {{ Object.values(accountInfo.eventFetcherStatusV3).join('; ') }}
|
||||
</template>
|
||||
<template v-if="state.status"> 异常: {{ Object.values(state.status).join('; ') }} </template>
|
||||
</template>
|
||||
<template v-else-if="status == 'info'"> 未连接 </template>
|
||||
</template>
|
||||
</NTag>
|
||||
<template v-if="accountInfo?.eventFetcherOnline != true">
|
||||
<template v-if="!state.online">
|
||||
<NDivider vertical />
|
||||
<NButton
|
||||
type="info"
|
||||
|
||||
@@ -159,6 +159,7 @@ interface DanmakuEventsMap {
|
||||
gift: (arg1: GiftInfo, arg2?: any) => void
|
||||
sc: (arg1: SCInfo, arg2?: any) => void
|
||||
guard: (arg1: GuardInfo, arg2?: any) => void
|
||||
all: (arg1: any) => void
|
||||
}
|
||||
|
||||
export default class DanmakuClient {
|
||||
@@ -167,54 +168,83 @@ export default class DanmakuClient {
|
||||
}
|
||||
|
||||
private client: any
|
||||
private authInfo: AuthInfo | null
|
||||
private timer: any | undefined
|
||||
private isStarting = false
|
||||
|
||||
public authInfo: AuthInfo | null
|
||||
public roomAuthInfo = ref<RoomAuthInfo>({} as RoomAuthInfo)
|
||||
public authCode: string | undefined
|
||||
|
||||
public isRunning: boolean = false
|
||||
|
||||
private 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: [],
|
||||
}
|
||||
private 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.client) {
|
||||
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
|
||||
const result = await this.initClient()
|
||||
if (result.success) {
|
||||
this.timer = setInterval(() => {
|
||||
this.sendHeartbeat()
|
||||
}, 20 * 1000)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
|
||||
if (this.isRunning) {
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端已被启动过',
|
||||
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()
|
||||
@@ -223,35 +253,51 @@ export default class DanmakuClient {
|
||||
}
|
||||
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.client) {
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
|
||||
: QueryGetAPI<OpenLiveInfo>(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()
|
||||
}
|
||||
})
|
||||
if (!this.isRunning) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
return
|
||||
}
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
|
||||
: QueryGetAPI<OpenLiveInfo>(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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private onRawMessage = (command: any) => {
|
||||
this.eventsAsModel.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
this.events.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
}
|
||||
|
||||
private onDanmaku = (command: any) => {
|
||||
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: 'sc', 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]) {
|
||||
this.eventsAsModel[eventName] = []
|
||||
}
|
||||
@@ -418,7 +465,7 @@ export default class DanmakuClient {
|
||||
message: '',
|
||||
}
|
||||
} else {
|
||||
console.log('[OPEN-LIVE] 无法开启场次')
|
||||
console.log('[OPEN-LIVE] 无法开启场次: ' + auth.message)
|
||||
return {
|
||||
success: false,
|
||||
message: auth.message,
|
||||
@@ -456,5 +503,6 @@ export default class DanmakuClient {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,13 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
}
|
||||
this.onDelSuperChat({ ids })
|
||||
}
|
||||
|
||||
rawMessageCallback(command) {
|
||||
if (!this.onRawMessage) {
|
||||
return
|
||||
}
|
||||
this.onRawMessage(command)
|
||||
}
|
||||
}
|
||||
|
||||
const CMD_CALLBACK_MAP = {
|
||||
@@ -142,4 +149,5 @@ const CMD_CALLBACK_MAP = {
|
||||
LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback,
|
||||
LIVE_OPEN_PLATFORM_SUPER_CHAT: ChatClientDirectOpenLive.prototype.superChatCallback,
|
||||
LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL: ChatClientDirectOpenLive.prototype.superChatDelCallback,
|
||||
RAW_MESSAGE: ChatClientDirectOpenLive.prototype.rawMessageCallback,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// @ts-nocheck
|
||||
import { BrotliDecode } from './brotli_decode'
|
||||
import { setInterval, clearInterval, setTimeout, clearTimeout } from 'worker-timers'
|
||||
|
||||
@@ -145,7 +147,7 @@ export default class ChatClientOfficialBase {
|
||||
this.sendAuth()
|
||||
this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
console.log('ws 已连接')
|
||||
//console.log('ws 已连接')
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
@@ -270,7 +272,10 @@ export default class ChatClientOfficialBase {
|
||||
// 没压缩过的直接反序列化
|
||||
if (body.length !== 0) {
|
||||
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)
|
||||
} catch (e) {
|
||||
console.error('body=', body)
|
||||
@@ -299,6 +304,7 @@ export default class ChatClientOfficialBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
onRawMessage(command) {}
|
||||
|
||||
handlerCommand(command) {
|
||||
let cmd = command.cmd || ''
|
||||
|
||||
@@ -2,8 +2,8 @@ import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemp
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
|
||||
const debugAPI = import.meta.env.VITE_DEBUG_API
|
||||
const releseAPI = `https://vtsuru.suki.club/api/`
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/api/`
|
||||
const releseAPI = `https://vtsuru.suki.club/`
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/`
|
||||
|
||||
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 apiFail = ref(false)
|
||||
|
||||
export const BASE_API = () =>
|
||||
process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI
|
||||
export const BASE_API = {
|
||||
toString: () =>
|
||||
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'api/',
|
||||
}
|
||||
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 USER_API_URL = { toString: () => `${BASE_API()}user/` }
|
||||
export const ACCOUNT_API_URL = { toString: () => `${BASE_API()}account/` }
|
||||
export const BILI_API_URL = { toString: () => `${BASE_API()}bili/` }
|
||||
export const SONG_API_URL = { toString: () => `${BASE_API()}song-list/` }
|
||||
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API()}notifaction/` }
|
||||
export const QUESTION_API_URL = { toString: () => `${BASE_API()}qa/` }
|
||||
export const LOTTERY_API_URL = { toString: () => `${BASE_API()}lottery/` }
|
||||
export const HISTORY_API_URL = { toString: () => `${BASE_API()}history/` }
|
||||
export const SCHEDULE_API_URL = { toString: () => `${BASE_API()}schedule/` }
|
||||
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API()}video-collect/` }
|
||||
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API()}open-live/` }
|
||||
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API()}song-request/` }
|
||||
export const QUEUE_API_URL = { toString: () => `${BASE_API()}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API()}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API()}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API()}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API()}vtsuru/` }
|
||||
export const POINT_API_URL = { toString: () => `${BASE_API()}point/` }
|
||||
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API()}bili-auth/` }
|
||||
export const USER_API_URL = { toString: () => `${BASE_API}user/` }
|
||||
export const ACCOUNT_API_URL = { toString: () => `${BASE_API}account/` }
|
||||
export const BILI_API_URL = { toString: () => `${BASE_API}bili/` }
|
||||
export const SONG_API_URL = { toString: () => `${BASE_API}song-list/` }
|
||||
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API}notifaction/` }
|
||||
export const QUESTION_API_URL = { toString: () => `${BASE_API}qa/` }
|
||||
export const LOTTERY_API_URL = { toString: () => `${BASE_API}lottery/` }
|
||||
export const HISTORY_API_URL = { toString: () => `${BASE_API}history/` }
|
||||
export const SCHEDULE_API_URL = { toString: () => `${BASE_API}schedule/` }
|
||||
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API}video-collect/` }
|
||||
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API}open-live/` }
|
||||
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API}song-request/` }
|
||||
export const QUEUE_API_URL = { toString: () => `${BASE_API}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API}vtsuru/` }
|
||||
export const POINT_API_URL = { toString: () => `${BASE_API}point/` }
|
||||
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API}bili-auth/` }
|
||||
|
||||
export const ScheduleTemplateMap = {
|
||||
'': {
|
||||
|
||||
@@ -19,7 +19,7 @@ let currentVersion: string
|
||||
let isHaveNewVersion = false
|
||||
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
|
||||
QueryGetAPI<string>(BASE_API + 'vtsuru/version')
|
||||
.then((version) => {
|
||||
if (version.code == 200) {
|
||||
currentVersion = version.data
|
||||
@@ -42,7 +42,7 @@ QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
|
||||
if (isHaveNewVersion) {
|
||||
return
|
||||
}
|
||||
QueryGetAPI<string>(BASE_API() + 'vtsuru/version').then((keepCheckData) => {
|
||||
QueryGetAPI<string>(BASE_API + 'vtsuru/version').then((keepCheckData) => {
|
||||
if (keepCheckData.code == 200 && keepCheckData.data != currentVersion) {
|
||||
isHaveNewVersion = true
|
||||
currentVersion = version.data
|
||||
|
||||
@@ -42,5 +42,13 @@ export default {
|
||||
title: '棉花糖展示',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'web-fetcher',
|
||||
name: 'obs-web-fetcher',
|
||||
component: () => import('@/views/obs/WebFetcherOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕收集器 (OBS版',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
150
src/store/useWebFetcher.ts
Normal file
150
src/store/useWebFetcher.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
@@ -454,7 +454,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<NPageHeader>
|
||||
<template #title>
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
|
||||
let client = new DanmakuClient(null)
|
||||
const client = new DanmakuClient(null)
|
||||
const isClientLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -54,12 +54,15 @@ onMounted(() => {
|
||||
}, 1000)
|
||||
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onVisibilityChange = function (visibility: boolean) {
|
||||
visiable.value = visibility
|
||||
}
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onActiveChange = function (a: boolean) {
|
||||
active.value = a
|
||||
if (window.obsstudio) {
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onVisibilityChange = function (visibility: boolean) {
|
||||
visiable.value = visibility
|
||||
}
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onActiveChange = function (a: boolean) {
|
||||
active.value = a
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
|
||||
41
src/views/obs/WebFetcherOBS.vue
Normal file
41
src/views/obs/WebFetcherOBS.vue
Normal 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
0
src/views/others
Normal file
161
yarn.lock
161
yarn.lock
@@ -754,6 +754,36 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.1.5
|
||||
resolution: "@nodelib/fs.scandir@npm:2.1.5"
|
||||
@@ -2826,6 +2856,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.2.4
|
||||
resolution: "evtd@npm:0.2.4"
|
||||
@@ -2920,6 +2957,16 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.0.1
|
||||
resolution: "file-entry-cache@npm:6.0.1"
|
||||
@@ -4171,6 +4218,20 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 10.0.1
|
||||
resolution: "node-gyp@npm:10.0.1"
|
||||
@@ -4510,6 +4571,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.3.1
|
||||
resolution: "punycode.js@npm:2.3.1"
|
||||
@@ -4517,7 +4585,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode@npm:^2.1.0":
|
||||
"punycode@npm:^2.1.0, punycode@npm:^2.1.1":
|
||||
version: 2.3.1
|
||||
resolution: "punycode@npm:2.3.1"
|
||||
checksum: 14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
|
||||
@@ -4533,6 +4601,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.2.3
|
||||
resolution: "queue-microtask@npm:1.2.3"
|
||||
@@ -4609,6 +4684,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.3.0
|
||||
resolution: "resize-detector@npm:0.3.0"
|
||||
@@ -4817,6 +4899,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.1
|
||||
resolution: "set-function-length@npm:1.1.1"
|
||||
@@ -5198,6 +5287,25 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.3.11
|
||||
resolution: "treemate@npm:0.3.11"
|
||||
@@ -5367,6 +5475,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.26.0
|
||||
resolution: "unplugin-vue-markdown@npm:0.26.0"
|
||||
@@ -5419,6 +5534,16 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.2
|
||||
resolution: "util-deprecate@npm:1.0.2"
|
||||
@@ -5522,6 +5647,8 @@ __metadata:
|
||||
resolution: "vtsuru.live@workspace:."
|
||||
dependencies:
|
||||
"@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/node": "npm:^20.11.19"
|
||||
"@types/obs-studio": "npm:^2.17.2"
|
||||
@@ -5765,6 +5892,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.2.3
|
||||
resolution: "webpack-sources@npm:3.2.3"
|
||||
@@ -5779,6 +5913,16 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.2
|
||||
resolution: "which-boxed-primitive@npm:1.0.2"
|
||||
@@ -5904,6 +6048,21 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.18.5
|
||||
resolution: "xlsx@npm:0.18.5"
|
||||
|
||||
Reference in New Issue
Block a user