feat: 修复图片url, 开始弹幕机编写

This commit is contained in:
2025-04-13 21:59:47 +08:00
parent 2a022e0448
commit c13fcb90c8
27 changed files with 2331 additions and 915 deletions

View File

@@ -1,178 +1,350 @@
import { EventModel } from '@/api/api-models'
import { KeepLiveWS } from 'bilibili-live-ws/browser'
// BaseDanmakuClient.ts
import { EventModel, EventDataTypes } from '@/api/api-models'; // 导入事件模型和类型枚举
import { KeepLiveWS } from 'bilibili-live-ws/browser'; // 导入 bilibili-live-ws 库
// 定义基础弹幕客户端抽象类
export default abstract class BaseDanmakuClient {
constructor() {
this.client = null
this.client = null; // 初始化客户端实例为 null
// 初始化两套事件监听器存储
this.eventsAsModel = this.createEmptyEventModelListeners();
this.eventsRaw = this.createEmptyRawEventlisteners();
}
public client: KeepLiveWS | null
// WebSocket 客户端实例
public client: KeepLiveWS | null;
// 客户端连接状态
public state: 'padding' | 'connected' | 'connecting' | 'disconnected' =
'padding'
'padding';
public abstract type: 'openlive' | 'direct'
public abstract serverUrl: string
// 客户端类型 (由子类实现)
public abstract type: 'openlive' | 'direct';
// 目标服务器地址 (由子类实现)
public abstract serverUrl: string;
// --- 事件系统 1: 使用 EventModel ---
// 事件监听器集合 (统一使用 EventModel)
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: []
}
danmaku: ((arg1: EventModel, arg2?: any) => void)[];
gift: ((arg1: EventModel, arg2?: any) => void)[];
sc: ((arg1: EventModel, arg2?: any) => void)[];
guard: ((arg1: EventModel, arg2?: any) => void)[];
enter: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: 用户进入事件
scDel: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: SC 删除事件
all: ((arg1: any) => void)[]; // '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)
this.state = 'disconnected'
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 = {
// --- 事件系统 2: 使用原始数据类型 ---
// 事件监听器集合 (使用原始数据结构, 类型设为 any, 由具体实现和调用者保证)
public eventsRaw: {
danmaku: ((arg1: any, arg2?: any) => void)[];
gift: ((arg1: any, arg2?: any) => void)[];
sc: ((arg1: any, arg2?: any) => void)[];
guard: ((arg1: any, arg2?: any) => void)[];
enter: ((arg1: any, arg2?: any) => void)[]; // 新增: 用户进入事件
scDel: ((arg1: any, arg2?: any) => void)[]; // 新增: SC 删除事件
all: ((arg1: any) => void)[]; // 'all' 事件监听器接收原始消息或特定事件包
};
// 创建空的 EventModel 监听器对象
public createEmptyEventModelListeners() {
return {
danmaku: [],
gift: [],
sc: [],
guard: [],
all: []
enter: [],
scDel: [],
all: [],
};
}
// 创建空的 RawEvent 监听器对象
public createEmptyRawEventlisteners() {
return {
danmaku: [],
gift: [],
sc: [],
guard: [],
enter: [],
scDel: [],
all: [],
};
}
/**
* 启动弹幕客户端连接
* @returns Promise<{ success: boolean; message: string }> 启动结果
*/
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 {
// 确保 client 为 null 才初始化
if (!this.client) {
console.log(`[${this.type}] 正在启动弹幕客户端`);
// 调用子类实现的初始化方法
const result = await this.initClient();
if (result.success) {
this.state = 'connected';
console.log(`[${this.type}] 弹幕客户端启动成功`);
} else {
this.state = 'disconnected';
console.error(`[${this.type}] 弹幕客户端启动失败: ${result.message}`);
}
return result;
} else {
console.warn(`[${this.type}] 客户端实例已存在但状态异常,尝试重置状态`);
this.state = 'disconnected';
return {
success: false,
message: '客户端实例状态异常,请尝试重新启动',
};
}
} catch (err: any) {
console.error(`[${this.type}] 启动过程中发生异常:`, err);
this.state = 'disconnected';
if (this.client) {
try { this.client.close(); } catch { }
this.client = null;
}
return {
success: false,
message: err?.message || err?.toString() || '未知错误',
};
}
}
/**
* 停止弹幕客户端连接
*/
public Stop() {
// 如果已断开,则无需操作
if (this.state === 'disconnected') {
return;
}
// 设置状态为已断开
this.state = 'disconnected';
if (this.client) {
console.log(`[${this.type}] 正在停止弹幕客户端`);
try {
this.client.close(); // 关闭 WebSocket 连接
} catch (err) {
console.error(`[${this.type}] 关闭客户端时发生错误:`, err);
}
this.client = null; // 将客户端实例置为 null
} else {
console.warn(`[${this.type}] 弹幕客户端未被启动, 忽略停止操作`);
}
// 注意: 清空所有事件监听器
this.eventsAsModel = this.createEmptyEventModelListeners();
this.eventsRaw = this.createEmptyRawEventlisteners();
}
/**
* 初始化客户端实例 (抽象方法,由子类实现具体的创建逻辑)
* @returns Promise<{ success: boolean; message: string }> 初始化结果
*/
protected abstract initClient(): Promise<{
success: boolean
message: string
}>
success: boolean;
message: string;
}>;
/**
* 内部通用的客户端事件绑定和连接状态等待逻辑
* @param chatClient - 已创建的 KeepLiveWS 实例
* @returns Promise<{ success: boolean; message: string }> 连接结果
*/
protected async initClientInner(
chatClient: KeepLiveWS
): Promise<{ success: boolean; message: string }> {
let isConnected = false
let isError = false
let errorMsg = ''
): 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
})
console.error(`[${this.type}] 客户端发生错误:`, err);
isError = true;
errorMsg = err?.message || err?.toString() || '未知错误';
});
// 监听连接成功事件
chatClient.on('live', () => {
isConnected = true
})
console.log(`[${this.type}] 弹幕客户端连接成功`);
isConnected = true;
});
// 监听连接关闭事件
chatClient.on('close', () => {
console.log(`[${this.type}] 弹幕客户端已关闭`)
})
chatClient.on('msg', (cmd) => this.onRawMessage(cmd))
console.log(`[${this.type}] 弹幕客户端连接已关闭`);
if (this.state !== 'disconnected') {
this.state = 'disconnected';
this.client = null;
}
isConnected = false; // 标记为未连接
});
this.client = chatClient
// 监听原始消息事件 (通用)
// 注意: 子类可能也会监听特定事件名, 这里的 'msg' 是备用或处理未被特定监听器捕获的事件
chatClient.on('msg', (command: any) => this.onRawMessage(command));
this.client = chatClient; // 保存客户端实例
// 等待连接成功或发生错误
const timeout = 30000; // 30 秒超时
const startTime = Date.now();
while (!isConnected && !isError) {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
if (Date.now() - startTime > timeout) {
isError = true;
errorMsg = '连接超时';
console.error(`[${this.type}] ${errorMsg}`);
break;
}
await new Promise((resolve) => { setTimeout(resolve, 500); });
}
if (isError) {
this.client.close()
this.client = null
// 如果连接过程中发生错误,清理客户端实例
if (isError && this.client) {
try { this.client.close(); } catch { }
this.client = null;
this.state = 'disconnected';
}
this.serverUrl = chatClient.connection.ws.ws.url
// 返回连接结果
return {
success: !isError,
message: errorMsg
}
success: isConnected && !isError,
message: errorMsg,
};
}
/**
* 处理接收到的原始消息,并根据类型分发 (主要用于 'msg' 事件)
* @param command - 原始消息对象 (类型为 any)
*/
public onRawMessage = (command: any) => {
this.eventsAsModel.all?.forEach((d) => {
d(command)
})
// 触发 'all' 事件监听器 (两套系统都触发)
try {
this.eventsAsModel.all?.forEach((listener) => { listener(command); });
this.eventsRaw.all?.forEach((listener) => { listener(command); });
} catch (err) {
console.error(`[${this.type}] 处理 'all' 事件监听器时出错:`, err, command);
}
};
// --- 抽象处理方法 (子类实现) ---
// 这些方法负责接收原始数据, 触发 RawEvent, 转换数据, 触发 ModelEvent
/**
* 处理弹幕消息 (子类实现)
* @param data - 原始消息数据部分 (any 类型)
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
*/
public abstract onDanmaku(comand: any): void;
/**
* 处理礼物消息 (子类实现)
* @param data - 原始消息数据部分 (any 类型)
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
*/
public abstract onGift(comand: any): void;
/**
* 处理 Super Chat 消息 (子类实现)
* @param data - 原始消息数据部分 (any 类型)
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
*/
public abstract onSC(comand: any): void;
/**
* 处理上舰/舰队消息 (子类实现)
* @param data - 原始消息数据部分 (any 类型)
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
*/
public abstract onGuard(comand: any): void;
/**
* 处理用户进入消息 (子类实现)
* @param data - 原始消息数据部分 (any 类型)
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
*/
public abstract onEnter(comand: any): void;
/**
* 处理 SC 删除消息 (子类实现)
* @param data - 原始消息数据部分 (any 类型) - 通常可能只包含 message_id
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
*/
public abstract onScDel(comand: any): void;
// --- 事件系统 1: on/off (使用 EventModel) ---
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: 'enter', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
public onEvent(eventName: 'scDel', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
public onEvent(eventName: 'all', listener: (arg1: any) => void): this;
public onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
if (!this.eventsAsModel[eventName]) {
// @ts-ignore
this.eventsAsModel[eventName] = [];
}
// @ts-ignore
this.eventsAsModel[eventName].push(listener);
return this;
}
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)
public offEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
if (this.eventsAsModel[eventName]?.length) {
// @ts-ignore
const index = this.eventsAsModel[eventName].indexOf(listener);
if (index > -1) {
this.eventsAsModel[eventName].splice(index, 1)
this.eventsAsModel[eventName].splice(index, 1);
}
}
return this
return this;
}
}
// --- 事件系统 2: on/off (使用原始数据) ---
// 注意: listener 的 arg1 类型为 any, 需要调用者根据 eventName 自行转换或处理
public on(eventName: 'danmaku', listener: (arg1: any, arg2?: any) => void): this;
public on(eventName: 'gift', listener: (arg1: any, arg2?: any) => void): this;
public on(eventName: 'sc', listener: (arg1: any, arg2?: any) => void): this;
public on(eventName: 'guard', listener: (arg1: any, arg2?: any) => void): this;
public on(eventName: 'enter', listener: (arg1: any, arg2?: any) => void): this; // 新增
public on(eventName: 'scDel', listener: (arg1: any, arg2?: any) => void): this; // 新增
public on(eventName: 'all', listener: (arg1: any) => void): this;
public on(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
if (!this.eventsRaw[eventName]) {
// @ts-ignore
this.eventsRaw[eventName] = [];
}
// @ts-ignore
this.eventsRaw[eventName].push(listener);
return this;
}
public off(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
if (this.eventsRaw[eventName]?.length) {
// @ts-ignore
const index = this.eventsRaw[eventName].indexOf(listener);
if (index > -1) {
this.eventsRaw[eventName].splice(index, 1);
}
}
return this;
}
}

View File

@@ -1,66 +1,207 @@
import { KeepLiveWS } from 'bilibili-live-ws/browser'
import BaseDanmakuClient from './BaseDanmakuClient'
import { KeepLiveWS } from 'bilibili-live-ws/browser';
import BaseDanmakuClient from './BaseDanmakuClient';
import { EventDataTypes } from '@/api/api-models';
import { getUserAvatarUrl, GuidUtils } from '@/Utils';
import { AVATAR_URL } from '../constants';
export type DirectClientAuthInfo = {
token: string
roomId: number
tokenUserId: number
buvid: string
}
token: string;
roomId: number;
tokenUserId: number;
buvid: string;
};
/** 直播间弹幕客户端, 只能在vtsuru.client环境使用
*
* 未实现除raw事件外的所有事件
*/
export default class DirectClient extends BaseDanmakuClient {
public serverUrl: string = 'wss://broadcastlv.chat.bilibili.com/sub';
public onDanmaku(command: any): void {
throw new Error('Method not implemented.')
}
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
super();
this.authInfo = auth;
}
public type = 'direct' as const
public type = 'direct' as const;
public readonly authInfo: DirectClientAuthInfo
public readonly authInfo: DirectClientAuthInfo;
protected async initClient(): Promise<{ success: boolean; message: string }> {
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)
console.log('[DIRECT] 已连接房间: ' + this.authInfo.roomId);
});
chatClient.on('DANMU_MSG', (data) => this.onDanmaku(data));
chatClient.on('SEND_GIFT', (data) => this.onGift(data));
chatClient.on('GUARD_BUY', (data) => this.onGuard(data));
chatClient.on('SUPER_CHAT_MESSAGE', (data) => this.onSC(data));
chatClient.on('INTERACT_WORD', (data) => this.onEnter(data));
chatClient.on('SUPER_CHAT_MESSAGE_DELETE', (data) => this.onScDel(data));
return await super.initClientInner(chatClient);
} else {
console.log('[DIRECT] 无法开启场次, 未提供弹幕客户端认证信息')
console.log('[DIRECT] 无法开启场次, 未提供弹幕客户端认证信息');
return {
success: false,
message: '未提供弹幕客户端认证信息'
}
};
}
}
public onDanmaku(command: any): void {
const data = command.data;
const info = data.info;
this.eventsRaw?.danmaku?.forEach((d) => { d(data, command); });
this.eventsAsModel.danmaku?.forEach((d) => {
d(
{
type: EventDataTypes.Message,
name: info[2][1],
uid: info[2][0],
msg: info[1],
price: 0,
num: 1,
time: Date.now(),
guard_level: info[7],
fans_medal_level: info[0][15].medal?.level,
fans_medal_name: info[0][15].medal?.name,
fans_medal_wearing_status: info[0][15].medal?.is_light === 1,
emoji: info[0]?.[13]?.url?.replace("http://", "https://") || '',
uface: info[0][15].user.base.face.replace("http://", "https://"),
open_id: '',
ouid: GuidUtils.numToGuid(info[2][0])
},
command
);
});
}
public onGift(command: any): void {
const data = command.data;
this.eventsRaw?.gift?.forEach((d) => { d(data, command); });
this.eventsAsModel.gift?.forEach((d) => {
d(
{
type: EventDataTypes.Gift,
name: data.uname,
uid: data.uid,
msg: data.giftName,
price: data.giftId,
num: data.num,
time: Date.now(),
guard_level: data.guard_level,
fans_medal_level: data.medal_info.medal_level,
fans_medal_name: data.medal_info.medal_name,
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
uface: data.face.replace("http://", "https://"),
open_id: '',
ouid: GuidUtils.numToGuid(data.uid)
},
command
);
});
}
public onSC(command: any): void {
const data = command.data;
this.eventsRaw?.sc?.forEach((d) => { d(data, command); });
this.eventsAsModel.sc?.forEach((d) => {
d(
{
type: EventDataTypes.SC,
name: data.user_info.uname,
uid: data.uid,
msg: data.message,
price: data.price,
num: 1,
time: Date.now(),
guard_level: data.user_info.guard_level,
fans_medal_level: data.medal_info.medal_level,
fans_medal_name: data.medal_info.medal_name,
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
uface: data.user_info.face.replace("http://", "https://"),
open_id: '',
ouid: GuidUtils.numToGuid(data.uid)
},
command
);
});
}
public onGuard(command: any): void {
const data = command.data;
this.eventsRaw?.guard?.forEach((d) => { d(data, command); });
this.eventsAsModel.guard?.forEach((d) => {
d(
{
type: EventDataTypes.Guard,
name: data.username,
uid: data.uid,
msg: data.gift_name,
price: data.price / 1000,
num: data.num,
time: Date.now(),
guard_level: data.guard_level,
fans_medal_level: 0,
fans_medal_name: '',
fans_medal_wearing_status: false,
uface: AVATAR_URL + data.uid,
open_id: '',
ouid: GuidUtils.numToGuid(data.uid)
},
command
);
});
}
public onEnter(command: any): void {
const data = command.data;
this.eventsRaw?.enter?.forEach((d) => { d(data, command); });
this.eventsAsModel.enter?.forEach((d) => {
d(
{
type: EventDataTypes.Enter,
name: data.uname,
uid: data.uid,
msg: '',
price: 0,
num: 1,
time: Date.now(),
guard_level: 0,
fans_medal_level: data.fans_medal?.medal_level || 0,
fans_medal_name: data.fans_medal?.medal_name || '',
fans_medal_wearing_status: false,
uface: getUserAvatarUrl(data.uid),
open_id: '',
ouid: GuidUtils.numToGuid(data.uid)
},
command
);
});
}
public onScDel(command: any): void {
const data = command.data;
this.eventsRaw?.scDel?.forEach((d) => { d(data, command); });
this.eventsAsModel.scDel?.forEach((d) => {
d(
{
type: EventDataTypes.SCDel,
name: '',
uid: 0,
msg: JSON.stringify(data.ids),
price: 0,
num: 1,
time: Date.now(),
guard_level: 0,
fans_medal_level: 0,
fans_medal_name: '',
fans_medal_wearing_status: false,
uface: '',
open_id: '',
ouid: ''
},
command
);
});
}
}

View File

@@ -1,143 +1,126 @@
import { EventDataTypes, OpenLiveInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
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 BaseDanmakuClient from './BaseDanmakuClient'
import { EventDataTypes, OpenLiveInfo } from '@/api/api-models';
import { QueryGetAPI, QueryPostAPI } from '@/api/query';
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 BaseDanmakuClient from './BaseDanmakuClient';
export default class OpenLiveClient extends BaseDanmakuClient {
public serverUrl: string = '';
constructor(auth?: AuthInfo) {
super()
this.authInfo = auth
this.events = { danmaku: [], gift: [], sc: [], guard: [], all: [] }
super();
this.authInfo = auth;
}
public type = 'openlive' as const
public type = 'openlive' as const;
private timer: any | undefined
private timer: any | undefined;
public authInfo: AuthInfo | undefined
public roomAuthInfo: OpenLiveInfo | undefined
public authCode: string | undefined
public authInfo: AuthInfo | undefined;
public roomAuthInfo: OpenLiveInfo | 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()
public async Start(): Promise<{ success: boolean; message: string; }> {
const result = await super.Start();
if (result.success) {
this.timer ??= setInterval(() => {
this.sendHeartbeat()
}, 20 * 1000)
this.sendHeartbeat();
}, 20 * 1000);
}
return result
return result;
}
public Stop() {
super.Stop()
this.events = {
danmaku: [],
gift: [],
sc: [],
guard: [],
all: []
}
super.Stop();
clearInterval(this.timer);
this.timer = undefined;
this.roomAuthInfo = undefined;
}
protected async initClient(): Promise<{ success: boolean; message: string }> {
const auth = await this.getAuthInfo()
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_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('LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER', (cmd) => this.onEnter(cmd));
chatClient.on('LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL', (cmd) => this.onScDel(cmd));
chatClient.on('live', () => {
console.log(
`[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}`
)
})
);
});
this.roomAuthInfo = auth.data
this.roomAuthInfo = auth.data;
return await super.initClientInner(chatClient)
return await super.initClientInner(chatClient);
} else {
console.log(`[${this.type}] 无法开启场次: ` + auth.message)
console.log(`[${this.type}] 无法开启场次: ` + auth.message);
return {
success: false,
message: auth.message
}
};
}
}
private async getAuthInfo(): Promise<{
data: OpenLiveInfo | null
message: string
data: OpenLiveInfo | null;
message: string;
}> {
try {
const data = await QueryPostAPI<OpenLiveInfo>(
OPEN_LIVE_API_URL + 'start',
this.authInfo?.Code ? this.authInfo : undefined
)
);
if (data.code == 200) {
console.log(`[${this.type}] 已获取场次信息`)
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
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')
OPEN_LIVE_API_URL + 'heartbeat',
this.authInfo
)
: QueryGetAPI<OpenLiveInfo>(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()
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)
})
const data = command.data as DanmakuInfo;
this.eventsRaw.danmaku?.forEach((d) => {
d(data, command);
});
this.eventsAsModel.danmaku?.forEach((d) => {
d(
{
@@ -158,15 +141,15 @@ export default class OpenLiveClient extends BaseDanmakuClient {
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)
})
const data = command.data as GiftInfo;
const price = (data.price * data.gift_num) / 1000;
this.eventsRaw.gift?.forEach((d) => {
d(data, command);
});
this.eventsAsModel.gift?.forEach((d) => {
d(
{
@@ -186,14 +169,14 @@ export default class OpenLiveClient extends BaseDanmakuClient {
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)
})
const data = command.data as SCInfo;
this.eventsRaw.sc?.forEach((d) => {
d(data, command);
});
this.eventsAsModel.sc?.forEach((d) => {
d(
{
@@ -213,14 +196,14 @@ export default class OpenLiveClient extends BaseDanmakuClient {
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)
})
const data = command.data as GuardInfo;
this.eventsRaw.guard?.forEach((d) => {
d(data, command);
});
this.eventsAsModel.guard?.forEach((d) => {
d(
{
@@ -235,7 +218,7 @@ export default class OpenLiveClient extends BaseDanmakuClient {
: data.guard_level == 3
? '舰长'
: '',
price: 0,
price: data.price / 1000,
num: data.guard_num,
time: data.timestamp,
guard_level: data.guard_level,
@@ -248,134 +231,165 @@ export default class OpenLiveClient extends BaseDanmakuClient {
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 onEnter(command: any): void {
const data = command.data as EnterInfo;
this.eventsRaw.enter?.forEach((d) => {
d(data);
});
this.eventsAsModel.enter?.forEach((d) => {
d(
{
type: EventDataTypes.Enter,
name: data.uname,
msg: '',
price: 0,
num: 0,
time: data.timestamp,
guard_level: 0,
fans_medal_level: 0,
fans_medal_name: '',
fans_medal_wearing_status: false,
uface: data.uface,
open_id: data.open_id,
uid: 0,
ouid: data.open_id
},
command
);
});
}
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
public onScDel(command: any): void {
const data = command.data as SCDelInfo;
this.eventsRaw.scDel?.forEach((d) => {
d(data, command);
});
this.eventsAsModel.scDel?.forEach((d) => {
d(
{
type: EventDataTypes.Enter,
name: '',
msg: JSON.stringify(data.message_ids),
price: 0,
num: 0,
time: Date.now(),
guard_level: 0,
fans_medal_level: 0,
fans_medal_name: '',
fans_medal_wearing_status: false,
uface: '',
open_id: '',
uid: 0,
ouid: ''
},
command
);
});
}
}
export interface DanmakuInfo {
room_id: number
uid: number
open_id: string
uname: string
msg: string
msg_id: string
fans_medal_level: number
fans_medal_name: string
fans_medal_wearing_status: boolean
guard_level: number
timestamp: number
uface: string
emoji_img_url: string
dm_type: number
room_id: number;
uid: number;
open_id: string;
uname: string;
msg: string;
msg_id: string;
fans_medal_level: number;
fans_medal_name: string;
fans_medal_wearing_status: boolean;
guard_level: number;
timestamp: number;
uface: string;
emoji_img_url: string;
dm_type: number;
}
export interface GiftInfo {
room_id: number
uid: number
open_id: string
uname: string
uface: string
gift_id: number
gift_name: string
gift_num: number
price: number
paid: boolean
fans_medal_level: number
fans_medal_name: string
fans_medal_wearing_status: boolean
guard_level: number
timestamp: number
msg_id: string
room_id: number;
uid: number;
open_id: string;
uname: string;
uface: string;
gift_id: number;
gift_name: string;
gift_num: number;
price: number;
paid: boolean;
fans_medal_level: number;
fans_medal_name: string;
fans_medal_wearing_status: boolean;
guard_level: number;
timestamp: number;
msg_id: string;
anchor_info: {
uid: number
uname: string
uface: string
}
gift_icon: string
combo_gift: boolean
uid: number;
uname: string;
uface: string;
};
gift_icon: string;
combo_gift: boolean;
combo_info: {
combo_base_num: number
combo_count: number
combo_id: string
combo_timeout: number
}
combo_base_num: number;
combo_count: number;
combo_id: string;
combo_timeout: number;
};
}
export interface SCInfo {
room_id: number // 直播间id
uid: number // 购买用户UID
open_id: string
uname: string // 购买的用户昵称
uface: string // 购买用户头像
message_id: number // 留言id(风控场景下撤回留言需要)
message: string // 留言内容
msg_id: string // 消息唯一id
rmb: number // 支付金额(元)
timestamp: number // 赠送时间秒级
start_time: number // 生效开始时间
end_time: number // 生效结束时间
guard_level: number // 对应房间大航海登记 (新增)
fans_medal_level: number // 对应房间勋章信息 (新增)
fans_medal_name: string // 对应房间勋章名字 (新增)
fans_medal_wearing_status: boolean // 该房间粉丝勋章佩戴情况 (新增)
room_id: number; // 直播间id
uid: number; // 购买用户UID
open_id: string;
uname: string; // 购买的用户昵称
uface: string; // 购买用户头像
message_id: number; // 留言id(风控场景下撤回留言需要)
message: string; // 留言内容
msg_id: string; // 消息唯一id
rmb: number; // 支付金额(元)
timestamp: number; // 赠送时间秒级
start_time: number; // 生效开始时间
end_time: number; // 生效结束时间
guard_level: number; // 对应房间大航海登记 (新增)
fans_medal_level: number; // 对应房间勋章信息 (新增)
fans_medal_name: string; // 对应房间勋章名字 (新增)
fans_medal_wearing_status: boolean; // 该房间粉丝勋章佩戴情况 (新增)
}
export interface GuardInfo {
user_info: {
uid: number // 用户uid
open_id: string
uname: string // 用户昵称
uface: string // 用户头像
}
guard_level: number // 对应的大航海等级 1总督 2提督 3舰长
guard_num: number
guard_unit: string // (个月)
fans_medal_level: number // 粉丝勋章等级
fans_medal_name: string // 粉丝勋章
fans_medal_wearing_status: boolean // 该房间粉丝勋章佩戴情况
timestamp: number
room_id: number
msg_id: string // 消息唯一id
uid: number; // 用户uid
open_id: string;
uname: string; // 用户昵称
uface: string; // 用户头像
};
guard_level: number; // 对应的大航海等级 1总督 2提督 3舰长
guard_num: number;
price: number; // 购买金额(1000=1元)
guard_unit: string; // (个月)
fans_medal_level: number; // 粉丝勋章等级
fans_medal_name: string; // 粉丝勋章
fans_medal_wearing_status: boolean; // 该房间粉丝勋章佩戴情况
timestamp: number;
room_id: number;
msg_id: string; // 消息唯一id
}
export interface EnterInfo {
open_id: string;
uname: string;
uface: string;
timestamp: number;
room_id: number;
}
// 假设的 SC 删除事件原始信息结构 (需要根据实际情况调整)
export interface SCDelInfo {
room_id: number;
message_ids: number[]; // 被删除的 SC 的 message_id
msg_id: string; // 删除操作的消息 ID
}
export interface AuthInfo {
Timestamp: string
Code: string
Mid: string
Caller: string
CodeSign: string
}
export interface DanmakuEventsMap {
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
}
Timestamp: string;
Code: string;
Mid: string;
Caller: string;
CodeSign: string;
}

View File

@@ -14,7 +14,7 @@ export const isDev = import.meta.env.MODE === 'development';
export const isTauri = window.__TAURI__ !== undefined || window.__TAURI_INTERNAL__ !== undefined;
export const AVATAR_URL = 'https://workers.vrp.moe/api/bilibili/avatar/';
export const FILE_BASE_URL = 'https://files.vtsuru.live';
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club';
export const IMGUR_URL = FILE_BASE_URL + '/imgur/';
export const THINGS_URL = FILE_BASE_URL + '/things/';
export const apiFail = ref(false);