diff --git a/CHANGELOG.md b/CHANGELOG.md index ebcfb617..1a9805c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +## [7.3.1] - 2026-6-25 + +### 新增 + +- 直播任务【点亮熄灭勋章】、【点赞】和【发弹幕】会在执行每个粉丝勋章的任务前校验直播状态,确保执行任务时直播状态符合预期 + +### 修复 + +- 修复【观看直播】任务心跳发送时机计算错误的bug + +### 调整 + +- 【观看直播】的任务进度校验现在完全依赖于API数据(舍弃本地观看时长记录逻辑) +- 优化了直播任务【点亮熄灭勋章】、【点赞】和【发弹幕】的房间直播状态探测逻辑 + ## [7.3.0] - 2026-6-14 ### 新增 diff --git a/package-lock.json b/package-lock.json index 7545723b..35565f3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bilibili-live-tasks-helper", - "version": "7.3.0", + "version": "7.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bilibili-live-tasks-helper", - "version": "7.3.0", + "version": "7.3.1", "dependencies": { "ajax-hook": "^3.0.3", "crypto-js": "^4.2.0", diff --git a/package.json b/package.json index a3d80112..a941017c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bilibili-live-tasks-helper", "private": true, - "version": "7.3.0", + "version": "7.3.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src/library/bili-api/index.ts b/src/library/bili-api/index.ts index bf4a9853..d1967489 100644 --- a/src/library/bili-api/index.ts +++ b/src/library/bili-api/index.ts @@ -193,7 +193,7 @@ const BAPI: BapiMethods = { id: JSON.stringify(id), device: JSON.stringify(device), ruid, // 主播 uid - ets: ets, + ets, benchmark, time, ts, diff --git a/src/library/error/index.ts b/src/library/error/index.ts new file mode 100644 index 00000000..0ca5fda6 --- /dev/null +++ b/src/library/error/index.ts @@ -0,0 +1,4 @@ +import ModuleError from './ModuleError' +import ModuleCriticalError from './ModuleCriticalError' + +export { ModuleError, ModuleCriticalError } diff --git a/src/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts index 34234bcd..e76e52a3 100644 --- a/src/library/storage/defaultValues.ts +++ b/src/library/storage/defaultValues.ts @@ -107,8 +107,6 @@ const defaultValues: DefaultValues = { targetRounds: 1, isWhiteList: false, roomidList: [], - _watchingProgress: {}, - _lastWatchTime: 0, _lastCompleteTime: 0, }, }, diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 22f36918..528315cf 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -2,12 +2,14 @@ import BaseModule from '@/modules/BaseModule' import { useBiliStore, useModuleStore } from '@/stores' import { watch } from 'vue' import type { + LiveStatusSnapshot, MedalTaskSharedConfig, + PreExecuteVerdict, RequestQueueKey, SharedMedalFilters, TaskJumpType, - WaitProbeResult, - WaitStrategy, + AfterExecutionAction, + BatchExecutionResult, } from './types' import { arrayToMap, sleep } from '@/library/utils' import type { LiveData } from '@/library/bili-api/data' @@ -60,14 +62,102 @@ class MedalModule extends BaseModule { // 直播中返回true,否则返回false isLiving: (medal) => medal.room_info.living_status === 1, } - /** 直播间直播状态获取函数列表(获取失败时返回null) */ + /** + * 通过获取指定页的粉丝勋章,探测目标直播间的直播状态 + * + * @param page 粉丝勋章页码 + * @param roomid 目标直播间ID + * @returns 直播状态和是否可以继续获取下一页的粉丝勋章 + */ + private async fetchMedalPageForLiveStatus( + page: number, + roomid: number, + ): Promise<{ status: number | null; canTryNextPage: boolean }> { + try { + const response = await BAPI.live.fansMedalPanel(page) + this.logger.log(`BAPI.live.fansMedalPanel(${page}) response`, response) + let status = null + + if (response.code === 0) { + const medals = [...response.data.special_list, ...response.data.list] + const observedAt = tsm() + + for (const medal of medals) { + const medalRoomid = medal.room_info.room_id + const medalLiveStatus = medal.room_info.living_status + + MedalModule.liveStatusSnapshots.set(medalRoomid, { + liveStatus: medalLiveStatus, + observedAt, + }) + + if (medalRoomid === roomid) { + status = medalLiveStatus + } + } + + const currentPage = response.data.page_info.current_page + const totalPage = response.data.page_info.total_page + const hasUnlightedMedal = medals.some((m) => !m.medal.is_lighted) + + return { + status, + // 目前B站的粉丝勋章排序规则为:点亮的勋章排在前面,未点亮的排在后面,然后分别再按等级由高到低排序 + // 如果当前页上有未点亮的粉丝勋章,后面的页就都是未点亮的勋章 + // 而点赞、发弹幕任务都是给点亮的勋章做的 + // 因此如果当前页上有未点亮的粉丝勋章,就没必要再看后面的页了 + canTryNextPage: currentPage < totalPage && !hasUnlightedMedal, + } + } + this.logger.warn(`BAPI.live.fansMedalPanel(${page}) 失败`, response.message) + } catch (error) { + this.logger.warn(`BAPI.live.fansMedalPanel(${page}) 出错`, error) + } + return { status: null, canTryNextPage: false } + } + /** + * 直播间直播状态获取函数列表(获取失败时返回null) + * + * - 特别说明:第一个 fetcher 通过粉丝勋章API获取直播状态,一次获取一页(10个)房间的直播状态并全部写入状态快照(但是仅返回目标房间的直播状态) + */ private readonly ROOM_LIVE_STATUS_FETCHERS: Array<(roomid: number) => Promise> = [ + async (roomid) => { + // 计算 roomid 对应的粉丝勋章在第几页 + const roomids = useBiliStore().filteredFansMedalsMap.keys() + let index = -1 + for (const r of roomids) { + index++ + if (r === roomid) { + break + } + } + + // 获取粉丝勋章 API 每页返回 10 个勋章,页码从 1 开始 + const page = Math.floor(index / 10) + 1 + + const { status, canTryNextPage } = await this.fetchMedalPageForLiveStatus(page, roomid) + + if (status !== null) { + return status + } + + if (canTryNextPage) { + await sleep(_.random(300, 500)) + const { status } = await this.fetchMedalPageForLiveStatus(page + 1, roomid) + + return status + } + + return null + }, async (roomid) => { try { const response = await BAPI.live.getInfoByRoom(roomid) this.logger.log(`BAPI.live.getInfoByRoom(${roomid}) response`, response) if (response.code === 0) { - return response.data.room_info.live_status + const liveStatus = response.data.room_info.live_status + MedalModule.liveStatusSnapshots.set(roomid, { liveStatus, observedAt: tsm() }) + return liveStatus } this.logger.warn(`BAPI.live.getInfoByRoom(${roomid}) 失败`, response.message) } catch (error) { @@ -81,7 +171,9 @@ class MedalModule extends BaseModule { const response = await BAPI.live.getRoomPlayInfo(roomid) this.logger.log(`BAPI.live.getRoomPlayInfo(${roomid}) response`, response) if (response.code === 0) { - return response.data.live_status + const liveStatus = response.data.live_status + MedalModule.liveStatusSnapshots.set(roomid, { liveStatus, observedAt: tsm() }) + return liveStatus } this.logger.warn(`BAPI.live.getRoomPlayInfo(${roomid}) 失败`, response.message) } catch (error) { @@ -98,11 +190,35 @@ class MedalModule extends BaseModule { taskInfo: Promise.resolve(), roomStatus: Promise.resolve(), } - /** 粉丝勋章数据在多久内视为新鲜(毫秒) */ - private static readonly RECENT_FETCH_THRESHOLD = 90_000 - /** 进行中的全量刷新粉丝勋章 Promise */ - private static ongoingFullRefreshPromise: Promise | null = null + /** + * 直播状态快照在多久内视为新鲜、可直接复用(毫秒) + * + * - 本阈值控制「执行前校验」是否需要做一次直播状态探测 + */ + private static readonly LIVE_STATUS_SNAPSHOT_FRESHNESS_THRESHOLD = 30_000 + /** + * 直播状态快照(key:直播间号) + */ + private static readonly liveStatusSnapshots = new Map() + /** + * 用粉丝勋章列表初始化直播状态快照 + */ + protected static initSnapshotsWithFansMedalsData(): void { + const biliStore = useBiliStore() + // 把开始获取粉丝勋章列表的时间作为直播状态快照的观测时间 + const observedAt = biliStore.fansMedalsMeta.lastFetchStartedAt! + for (const medal of biliStore.filteredFansMedals) { + const snapshot = MedalModule.liveStatusSnapshots.get(medal.room_info.room_id) + + if (!snapshot || snapshot.observedAt < observedAt) { + MedalModule.liveStatusSnapshots.set(medal.room_info.room_id, { + liveStatus: medal.room_info.living_status, + observedAt, + }) + } + } + } /** * 将异步请求加入串行队列,相邻请求之间加入随机延迟 */ @@ -156,7 +272,7 @@ class MedalModule extends BaseModule { /** * 是否需要因跨天而提前停止 */ - protected shouldStopForCrossDay(): boolean { + protected static shouldStopForCrossDay(): boolean { return isNowAfter(23, 55) || isNowBefore(0, 5) } @@ -165,7 +281,7 @@ class MedalModule extends BaseModule { * * @returns 是否获取成功 */ - protected waitForFansMedals(): Promise { + protected static waitForFansMedals(): Promise { return new Promise((resolve) => { const biliStore = useBiliStore() if (biliStore.fansMedalsMeta.status === 'loaded') { @@ -187,11 +303,81 @@ class MedalModule extends BaseModule { }) } + /** + * 点赞 + * + * @param medal 粉丝勋章 + * @param click_time 点赞次数 + * + * @returns 是否点赞成功 + */ + protected async like(medal: LiveData.FansMedalPanel.List, click_time: number): Promise { + const room_id = medal.room_info.room_id + const target_id = medal.medal.target_id + const nick_name = medal.anchor_info.nick_name + const medal_name = medal.medal.medal_name + const logMessage = `粉丝勋章【${medal_name}】 给主播【${nick_name}】(UID:${target_id})的直播间(${room_id})点赞 ${click_time} 次` + + try { + const response = await BAPI.live.likeReport(room_id, target_id, click_time) + this.logger.log(`BAPI.live.likeReport(${room_id}, ${target_id}, ${click_time})`, response) + if (response.code === 0) { + this.logger.log(`点赞 ${logMessage} 成功`) + return true + } else { + this.logger.error(`点赞 ${logMessage} 失败`, response.message) + } + } catch (error) { + this.logger.error(`点赞 ${logMessage} 出错`, error) + } + + return false + } + + /** + * 发弹幕 + * + * @param medal 粉丝勋章 + * @param danmu 弹幕内容 + * + * @returns 是否发送成功 + */ + protected async sendDanmu(medal: LiveData.FansMedalPanel.List, danmu: string): Promise { + const room_id = medal.room_info.room_id + const target_id = medal.medal.target_id + const nick_name = medal.anchor_info.nick_name + const medal_name = medal.medal.medal_name + const logMessage = `粉丝勋章【${medal_name}】 在主播【${nick_name}】(UID:${target_id})的直播间(${room_id})发送弹幕 ${danmu}` + + try { + const response = await BAPI.live.sendMsg(danmu, room_id) + this.logger.log(`BAPI.live.sendMsg(${danmu}, ${room_id})`, response) + if (response.code === 0) { + if (response.msg === '') { + this.logger.log(`发弹幕 ${logMessage} 成功`) + return true + } else if (response.msg === 'k') { + this.logger.warn(`发弹幕 ${logMessage} 异常,弹幕可能包含屏蔽词`) + } else if (response.msg === 'f') { + this.logger.warn(`发弹幕 ${logMessage} 异常,弹幕被过滤`) + } else { + this.logger.warn(`发弹幕 ${logMessage} 异常,未知错误:${response.msg}`) + } + } else { + this.logger.error(`发弹幕 ${logMessage} 失败`, response.message) + } + } catch (error) { + this.logger.error(`发弹幕 ${logMessage} 出错`, error) + } + + return false + } + /** * 从粉丝团升级任务 sub_title 解析每日任务进度文案 "每日上限 X/Y" * * @param sub_title API 返回的 sub_title - * @returns 解析成功返回 `{ current, limit }`,否则返回 `null` + * @returns 解析成功返回当前任务进度和任务上限,否则返回 `null` */ protected static parseDailyLimit(sub_title?: string): { current: number; limit: number } | null { if (!sub_title) return null @@ -204,7 +390,7 @@ class MedalModule extends BaseModule { * 按 jump_type 在粉丝团升级任务信息 task_info 中查找任务项 */ protected static findTaskInfo( - task_info: LiveData.GetActivatedMedalInfo.TaskInfo[] | null, + task_info: LiveData.GetActivatedMedalInfo.TaskInfo[], jump_type: TaskJumpType, ): LiveData.GetActivatedMedalInfo.TaskInfo | undefined { return task_info?.find((t) => t.jump_type === jump_type) @@ -215,7 +401,7 @@ class MedalModule extends BaseModule { * * @returns 解析成功返回数字,否则返回 null */ - protected static parseTitleCount(title?: string): number | null { + protected static parseTitleCount(title: string): number | null { if (!title) return null const match = title.match(/\d+/) if (!match) return null @@ -232,7 +418,7 @@ class MedalModule extends BaseModule { try { await moduleStore.rerunModule('Default_FansMedals', true) const status = useBiliStore().fansMedalsMeta.status - return status === 'loading' ? await this.waitForFansMedals() : status === 'loaded' + return status === 'loading' ? await MedalModule.waitForFansMedals() : status === 'loaded' } catch (error) { this.logger.error('刷新粉丝勋章列表失败', error) return false @@ -240,15 +426,25 @@ class MedalModule extends BaseModule { } /** - * 以随机顺序尝试多个接口,获取单个直播间的直播状态 + * 获取单个直播间的直播状态 + * + * - 默认以随机顺序尝试多个接口 + * + * @param roomid 直播间号 + * @param preferMedalAPI 是否优先使用粉丝勋章API(其余接口仍随机),默认 false * * @returns * - `1`:直播中 * - `0`/`2`:未开播/轮播中 - * - `null`:两个接口都获取失败 + * - `null`:所有接口都获取失败 */ - protected async fetchRoomLiveStatus(roomid: number): Promise { - const fetchers = _.shuffle(this.ROOM_LIVE_STATUS_FETCHERS) + private async fetchRoomLiveStatus( + roomid: number, + preferMedalAPI = false, + ): Promise { + const fetchers = preferMedalAPI + ? [this.ROOM_LIVE_STATUS_FETCHERS[0], ..._.shuffle(this.ROOM_LIVE_STATUS_FETCHERS.slice(1))] + : _.shuffle(this.ROOM_LIVE_STATUS_FETCHERS) for (let i = 0; i < fetchers.length; i++) { const liveStatus = await fetchers[i](roomid) @@ -264,165 +460,126 @@ class MedalModule extends BaseModule { } /** - * 计算等待探测策略 + * 解析直播间当前的直播状态(执行前校验使用) + * + * - 快照仍新鲜:直接复用 + * - 快照过期或不存在:探测并更新快照 * - * @param pendingCount 当前待探测的直播间数量 + * @param roomid 直播间号 + * @param preferMedalAPI 是否优先使用粉丝勋章API获取直播状态,默认 false + * + * @returns 直播状态;探测失败时返回 null */ - protected decideWaitStrategy(pendingCount: number): WaitStrategy { - const totalMedalCount = useBiliStore().filteredFansMedals.length - const fullRefreshCost = Math.ceil(totalMedalCount / 10) // 每次请求能获取 10 个粉丝勋章 - return pendingCount <= fullRefreshCost ? 'single-probe' : 'refresh-fans-medals' - } + private async resolveLiveStatus(roomid: number, preferMedalAPI = false): Promise { + const snapshot = MedalModule.liveStatusSnapshots.get(roomid) + if ( + snapshot && + tsm() - snapshot.observedAt < MedalModule.LIVE_STATUS_SNAPSHOT_FRESHNESS_THRESHOLD + ) { + return snapshot.liveStatus + } - /** - * 判断粉丝勋章数据是否足够新鲜 - */ - private isMedalDataFresh(): boolean { - const meta = useBiliStore().fansMedalsMeta - return ( - meta.status === 'loaded' && - tsm() - meta.lastFetchFinishedAt! < MedalModule.RECENT_FETCH_THRESHOLD + return await MedalModule.enqueueRoomStatusProbe(() => + this.fetchRoomLiveStatus(roomid, preferMedalAPI), ) } /** - * 从粉丝勋章 Map 构建探测结果 + * 执行前直播状态校验 * - * @param roomids 待探测的直播间ID列表 - * @param targetPredicate 直播状态判断函数 - * @param medalMap 粉丝勋章 Map - * @param probedLiveStatusMap 探测到的直播状态 Map - * - 为 undefined 时:使用 medal.room_info.living_status - * - 存在键 roomid 但值为 null 时:表示本轮实时探测失败,继续等待 + * @param roomid 直播间号 + * @param targetPredicate 目标状态判断函数 + * @param preferMedalAPI 是否优先使用粉丝勋章API获取直播状态,默认 false + * + * @returns 校验结论 */ - private buildResultFromMedalMap( - roomids: number[], + protected async preExecuteVerify( + roomid: number, targetPredicate: (liveStatus: number) => boolean, - medalMap: Map, - probedLiveStatusMap?: Map, - ): WaitProbeResult { - const readyMedals: LiveData.FansMedalPanel.List[] = [] - const pendingRoomids: number[] = [] - - for (const roomid of roomids) { - const medal = medalMap.get(roomid) - - if (!medal) { - this.logger.warn(`直播间 ${roomid} 未在粉丝勋章列表中找到,停止等待该房间`) - continue - } - if (!this.SHARED_MEDAL_FILTERS.isLighted(medal)) { - this.logger.log( - `粉丝勋章【${medal.medal.medal_name}】已熄灭,停止等待该房间(${roomid})`, - medal, - ) - continue - } + preferMedalAPI = false, + ): Promise { + const liveStatus = await this.resolveLiveStatus(roomid, preferMedalAPI) - // 是否有探测到的直播状态 Map - const hasProbedStatus = probedLiveStatusMap !== undefined - // 直播状态:有直播状态 Map 就从中取,否则使用粉丝勋章 Map 中的值 - const liveStatus = hasProbedStatus - ? probedLiveStatusMap.get(roomid) - : medal.room_info.living_status - - if (hasProbedStatus && _.isNil(liveStatus)) { - this.logger.warn( - `粉丝勋章【${medal.medal.medal_name}】对应的直播间 ${roomid} 本轮实时探测失败,继续等待下次检查`, - medal, - ) - pendingRoomids.push(roomid) - } else if (!_.isNil(liveStatus) && targetPredicate(liveStatus)) { - readyMedals.push(medal) - } else { - pendingRoomids.push(roomid) - } + if (liveStatus === null) { + // 探测失败 + return 'error' } - return { - readyMedals, - pendingRoomids, - } + return targetPredicate(liveStatus) ? 'pass' : 'fail' } /** - * 对等待中的直播间做一轮探测 + * 遍历一轮等待中的直播间,命中目标状态后都立即执行该房间任务 * * @param roomids 待探测的直播间ID列表 - * @param targetPredicate 直播状态判断函数,返回"是否符合目标状态" - * - * @returns 探测结果,包含符合目标状态的粉丝勋章列表和待处理的直播间ID列表 + * @param targetPredicate 目标状态判断函数 + * @param runOne 执行单个直播间任务的回调(跳过执行前直播状态校验) */ - protected async probeWaitingMedals( + protected async runWaitingRound( roomids: number[], targetPredicate: (liveStatus: number) => boolean, - ): Promise { + runOne: ( + medal: LiveData.FansMedalPanel.List, + ) => Promise>, + ): Promise { const biliStore = useBiliStore() - this.logger.log(`开始直播状态探测,待探测直播间数量:${roomids.length}`, { roomids }) - - // 如果有进行中的全量刷新,等待其完成 - if (MedalModule.ongoingFullRefreshPromise) { - this.logger.log('等待进行中的粉丝勋章刷新完成...') - await MedalModule.ongoingFullRefreshPromise - } - - // 检查粉丝勋章数据是否新鲜 - if (this.isMedalDataFresh()) { - this.logger.log('粉丝勋章数据较新,直接使用缓存数据') - return this.buildResultFromMedalMap(roomids, targetPredicate, biliStore.filteredFansMedalsMap) - } + this.logger.log(`开始探测等待中的直播间,待探测直播间数量:${roomids.length}`, { roomids }) - // 数据不新鲜,决定探测策略 - const strategy = this.decideWaitStrategy(roomids.length) - this.logger.log( - `粉丝勋章数据不新鲜,使用${strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, - ) + const medalMap = biliStore.filteredFansMedalsMap + const requeueRoomids: number[] = [] + let markUncompleted = false - if (strategy === 'refresh-fans-medals') { - const promise = this.refreshFansMedals() - MedalModule.ongoingFullRefreshPromise = promise - try { - await promise - } finally { - if (MedalModule.ongoingFullRefreshPromise === promise) { - MedalModule.ongoingFullRefreshPromise = null - } + for (let i = 0; i < roomids.length; i++) { + if (MedalModule.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,不再等待剩余直播间,提早结束本轮任务') + return { stop: true } } - if (biliStore.fansMedalsMeta.status === 'loaded') { - return this.buildResultFromMedalMap( - roomids, - targetPredicate, - biliStore.filteredFansMedalsMap, - ) + const roomid = roomids[i] + const medal = medalMap.get(roomid) + + if (!medal) { + this.logger.error(`直播间 ${roomid} 未在粉丝勋章列表中找到,停止等待该房间`) + continue } - return { readyMedals: [], pendingRoomids: roomids } - } + const target_id = medal.medal.target_id + const nick_name = medal.anchor_info.nick_name + const medal_name = medal.medal.medal_name + + // 遍历等待中的房间时优先用粉丝勋章API,一次获取一页(10个)房间的直播状态 + // 因为在这种情况下房间直播状态不符合预期的可能性大,经常会连续判断多个房间的直播状态,能复用之前的探测结果 + // 而在给第一批房间执行任务时,直播状态大概率正确, + // 执行完一个房间的任务后,先前获取的直播状态数据多半(具体概率和目标轮次相关)已经不新鲜了,又要重新探测直播状态, + // 所以没必要优先使用粉丝勋章API获取多个房间的直播状态 + const verdict = await this.preExecuteVerify(roomid, targetPredicate, true) + + if (verdict !== 'pass') { + if (verdict === 'error') { + this.logger.error( + `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${roomid})的直播状态,回到等待队列;可能遭遇风控,休眠 5 分钟再继续`, + ) + await sleep(300e3) + } - // 单房间探测策略 - const currentMedalMap = biliStore.filteredFansMedalsMap - const probedLiveStatusMap = new Map() + requeueRoomids.push(roomid) + continue + } - for (let i = 0; i < roomids.length; i++) { - const roomid = roomids[i] - const medal = currentMedalMap.get(roomid) + this.logger.log( + `粉丝勋章【${medal_name}】 主播【${nick_name}】(UID:${target_id})的直播间 ${roomid} 已达到目标状态,立即执行`, + ) + const action = await runOne(medal) - if (medal) { - probedLiveStatusMap.set( - roomid, - await MedalModule.enqueueRoomStatusProbe(() => this.fetchRoomLiveStatus(roomid)), - ) + if (action === 'stop') { + return { stop: true } + } else if (action === 'markUncompleted') { + markUncompleted = true } } - return this.buildResultFromMedalMap( - roomids, - targetPredicate, - currentMedalMap, - probedLiveStatusMap, - ) + return { markUncompleted, requeueRoomids } } /** diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 5fc6cc75..69a149df 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -1,11 +1,10 @@ import { delayToNextMoment, isNowBefore, isTimestampToday, tsm } from '@/library/luxon' -import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore } from '@/stores' import type { ModuleStatusTypes } from '@/types' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' import { sleep } from '@/library/utils' -import type { GroupedMedals, TaskExecutionResult } from './types' +import type { BatchExecutionResult, GroupedMedals, AfterExecutionAction } from './types' class DanmuTask extends MedalModule { config = this.medalTasksConfig.danmu @@ -52,54 +51,33 @@ class DanmuTask extends MedalModule { return result } - /** - * 发弹幕 - * - * @returns 是否发送成功 - */ - private async sendDanmu(medal: LiveData.FansMedalPanel.List, danmu: string): Promise { - const room_id = medal.room_info.room_id - const target_id = medal.medal.target_id - const nick_name = medal.anchor_info.nick_name - const medal_name = medal.medal.medal_name - const logMessage = `粉丝勋章【${medal_name}】 在主播【${nick_name}】(UID:${target_id})的直播间(${room_id})发送弹幕 ${danmu}` - - try { - const response = await BAPI.live.sendMsg(danmu, room_id) - this.logger.log(`BAPI.live.sendMsg(${danmu}, ${room_id})`, response) - if (response.code === 0) { - if (response.msg === '') { - this.logger.log(`发弹幕 ${logMessage} 成功`) - return true - } else if (response.msg === 'k') { - this.logger.warn(`发弹幕 ${logMessage} 异常,弹幕可能包含屏蔽词`) - } else if (response.msg === 'f') { - this.logger.warn(`发弹幕 ${logMessage} 异常,弹幕被过滤`) - } else { - this.logger.warn(`发弹幕 ${logMessage} 异常,未知错误:${response.msg}`) - } - } else { - this.logger.error(`发弹幕 ${logMessage} 失败`, response.message) - } - } catch (error) { - this.logger.error(`发弹幕 ${logMessage} 出错`, error) - } - - return false - } - /** * 执行单个直播间的发弹幕任务 * - * @returns 执行结果,包含是否中断以及是否确认完成 + * @param medal 粉丝勋章 + * @param danmuIndexRef 弹幕索引引用,用于记录当前正在发送的弹幕索引 + * @param skipPreVerify 是否跳过执行前直播状态校验,默认 false + * + * @returns 执行结果 */ private async executeDanmuTask( medal: LiveData.FansMedalPanel.List, danmuIndexRef: { value: number }, - ): Promise { - if (this.shouldStopForCrossDay()) { + skipPreVerify?: false, + ): Promise + private async executeDanmuTask( + medal: LiveData.FansMedalPanel.List, + danmuIndexRef: { value: number }, + skipPreVerify?: true, + ): Promise> + private async executeDanmuTask( + medal: LiveData.FansMedalPanel.List, + danmuIndexRef: { value: number }, + skipPreVerify = false, + ): Promise> { + if (MedalModule.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') - return { interrupted: true, verifiedCompleted: false } + return 'stop' } const room_id = medal.room_info.room_id @@ -112,14 +90,14 @@ class DanmuTask extends MedalModule { this.logger.error( `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息,跳过发弹幕任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } if (medalData.reach_free_intimacy_limit) { this.logger.warn( `粉丝勋章【${medal_name}】(主播【${nick_name}】,UID:${target_id},直播间:${room_id})已达到储蓄亲密度上限(已储蓄 ${medalData.free_intimacy} 亲密度,投喂一个粉丝灯牌即可领取这些亲密度),无法通过发弹幕获取更多亲密度,跳过发弹幕任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') @@ -127,20 +105,40 @@ class DanmuTask extends MedalModule { this.logger.error( `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } - if (item.is_done) return { interrupted: false, verifiedCompleted: true } + if (item.is_done) return 'skipSleep' const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${target_id},直播间:${room_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } - if (parsed.current >= parsed.limit) return { interrupted: false, verifiedCompleted: true } + if (parsed.current >= parsed.limit) return 'skipSleep' + + if (!skipPreVerify) { + if (this.config.onlyWhenNotLiving) { + // 开启了「仅在未开播的直播间发弹幕」,校验直播状态 + const verdict = await this.preExecuteVerify(room_id, (liveStatus) => liveStatus !== 1) + if (verdict === 'error') { + this.logger.error( + `粉丝勋章【${medal_name}】 执行前校验:无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的直播状态,${this.config.waitUntilNotLiving ? '回到等待队列' : '跳过发弹幕任务'};可能遭遇风控,休眠 5 分钟再继续`, + ) + await sleep(300e3) + + return this.config.waitUntilNotLiving ? 'requeue' : 'skipSleep' + } else if (verdict === 'fail') { + this.logger.log( + `粉丝勋章【${medal_name}】 执行前校验:主播【${nick_name}】(UID:${target_id},直播间:${room_id})当前正在直播,${this.config.waitUntilNotLiving ? '回到等待队列' : '跳过发弹幕任务'}`, + ) + return this.config.waitUntilNotLiving ? 'requeue' : 'skipSleep' + } + } + } const target = this.config.useTargetRounds ? Math.min(parsed.limit, this.config.targetRounds) @@ -150,9 +148,9 @@ class DanmuTask extends MedalModule { let hasSuccessfulDanmu = false for (let j = 0; j < remaining; j++) { - if (this.shouldStopForCrossDay()) { + if (MedalModule.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') - return { interrupted: true, verifiedCompleted: false } + return 'stop' } const danmuText = this.config.danmuList[danmuIndexRef.value++ % this.config.danmuList.length] @@ -172,44 +170,47 @@ class DanmuTask extends MedalModule { } if (hasSuccessfulDanmu) { - return { - interrupted: false, - verifiedCompleted: await this.confirmTaskCompletedAfterUpdate( - medal, - 'sendDanmu', - this.config.useTargetRounds ? target : undefined, - ), - } + return (await this.confirmTaskCompletedAfterUpdate( + medal, + 'sendDanmu', + this.config.useTargetRounds ? target : undefined, + )) + ? null + : 'markUncompleted' } // 所有发弹幕尝试均失败,估计是有什么异常状况,跳过 - return { interrupted: false, verifiedCompleted: true } + return null } /** * 顺序执行多个直播间的发弹幕任务 * - * @returns 执行结果,包含是否中断以及是否都确认完成 + * @returns 执行结果 */ private async executeDanmuTasks( medals: LiveData.FansMedalPanel.List[], danmuIndexRef: { value: number }, - ): Promise { - let verifiedCompleted = true + ): Promise { + let markUncompleted = false + const requeueRoomids: number[] = [] for (let i = 0; i < medals.length; i++) { - const result = await this.executeDanmuTask(medals[i], danmuIndexRef) - if (result.interrupted) return result - if (!result.verifiedCompleted) { - verifiedCompleted = false + const action = await this.executeDanmuTask(medals[i], danmuIndexRef) + if (action === 'stop') { + return { stop: true } + } else if (action === 'requeue') { + requeueRoomids.push(medals[i].room_info.room_id) + } else if (action === 'markUncompleted') { + markUncompleted = true } - if (i < medals.length - 1) { + if (action !== 'skipSleep' && i < medals.length - 1) { await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) } } - return { interrupted: false, verifiedCompleted } + return { markUncompleted, requeueRoomids } } public async run(): Promise { @@ -218,67 +219,68 @@ class DanmuTask extends MedalModule { if (!isTimestampToday(this.config._lastCompleteTime)) { await this.waitForLightTask() - if (!(await this.waitForFansMedals())) { + if (!(await MedalModule.waitForFansMedals())) { this.logger.error('粉丝勋章数据不存在,不执行发弹幕任务') this.status = 'error' return } this.status = 'running' + MedalModule.initSnapshotsWithFansMedalsData() + const { readyMedals, waitingMedals } = this.getMedals() + let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) let allCompleted = true const danmuIndexRef = { value: 0 } - let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) - const initialResult = await this.executeDanmuTasks(readyMedals, danmuIndexRef) - if (initialResult.interrupted) { - allCompleted = false - } else if (!initialResult.verifiedCompleted) { - allCompleted = false - } + const { stop, markUncompleted, requeueRoomids } = await this.executeDanmuTasks( + readyMedals, + danmuIndexRef, + ) - while (allCompleted && pendingRoomids.length > 0) { - if (this.shouldStopForCrossDay()) { - this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') + if (stop) { + allCompleted = false + } else { + if (markUncompleted) { allCompleted = false - break + } + if (requeueRoomids) { + pendingRoomids.push(...requeueRoomids) } - const { readyMedals: newlyReadyMedals, pendingRoomids: nextPendingRoomids } = - await this.probeWaitingMedals(pendingRoomids, (liveStatus) => liveStatus !== 1) + while (pendingRoomids.length > 0) { + const { stop, markUncompleted, requeueRoomids } = await this.runWaitingRound( + pendingRoomids, + (liveStatus) => liveStatus !== 1, + (medal) => this.executeDanmuTask(medal, danmuIndexRef, true), + ) + if (stop) { + allCompleted = false + break + } + if (markUncompleted) { + allCompleted = false + } - pendingRoomids = nextPendingRoomids + pendingRoomids = requeueRoomids! - const waitResult = await this.executeDanmuTasks(newlyReadyMedals, danmuIndexRef) - if (waitResult.interrupted) { - allCompleted = false - break - } - if (!waitResult.verifiedCompleted) { - allCompleted = false - break - } + if (pendingRoomids.length > 0) { + const medalMap = useBiliStore().filteredFansMedalsMap + const pendingRoomsInfo: Record = {} - if (pendingRoomids.length > 0) { - const medalMap = useBiliStore().filteredFansMedalsMap - const pendingRoomsInfo: Record = {} + for (const roomid of pendingRoomids) { + pendingRoomsInfo[roomid] = medalMap.get(roomid)?.anchor_info.nick_name + } - for (const roomid of pendingRoomids) { - pendingRoomsInfo[roomid] = medalMap.get(roomid)?.anchor_info.nick_name + this.logger.log( + `仍有 ${pendingRoomids.length} 个直播间未下播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + { pendingRoomsInfo }, + ) + await sleep(MedalModule.WAIT_POLL_INTERVAL) } - - this.logger.log( - `仍有 ${pendingRoomids.length} 个直播间未下播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, - { pendingRoomsInfo }, - ) - await sleep(MedalModule.WAIT_POLL_INTERVAL) } } - if (pendingRoomids.length > 0) { - allCompleted = false - } - if (allCompleted) { this.config._lastCompleteTime = tsm() this.status = 'done' diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index 799b7118..d2265e7a 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -1,5 +1,4 @@ import { delayToNextMoment, isNowBefore, isTimestampToday, tsm } from '@/library/luxon' -import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore } from '@/stores' import { sleep } from '@/library/utils' import type { ModuleStatusTypes } from '@/types' @@ -88,75 +87,15 @@ class LightTask extends MedalModule { return allVerified } - /** - * 点赞 - * @param medal 粉丝勋章 - * @param click_time 点赞次数 - */ - private async like(medal: LiveData.FansMedalPanel.List, click_time: number): Promise { - const room_id = medal.room_info.room_id - const target_id = medal.medal.target_id - const nick_name = medal.anchor_info.nick_name - const medal_name = medal.medal.medal_name - const logMessage = `粉丝勋章【${medal_name}】 给主播【${nick_name}】(UID:${target_id})的直播间(${room_id})点赞 ${click_time} 次` - - try { - const response = await BAPI.live.likeReport(room_id, target_id, click_time) - this.logger.log(`BAPI.live.likeReport(${room_id}, ${target_id}, ${click_time})`, response) - if (response.code === 0) { - this.logger.log(`点亮熄灭勋章-点赞 ${logMessage} 成功`) - } else { - this.logger.error(`点亮熄灭勋章-点赞 ${logMessage} 失败`, response.message) - } - } catch (error) { - this.logger.error(`点亮熄灭勋章-点赞 ${logMessage} 出错`, error) - } - } - - /** - * 发弹幕 - * @param medal 粉丝勋章 - * @param danmu 弹幕内容 - */ - private async sendDanmu(medal: LiveData.FansMedalPanel.List, danmu: string): Promise { - const room_id = medal.room_info.room_id - const target_id = medal.medal.target_id - const nick_name = medal.anchor_info.nick_name - const medal_name = medal.medal.medal_name - const logMessage = `粉丝勋章【${medal_name}】 在主播【${nick_name}】(UID:${target_id})的直播间(${room_id})发送弹幕 ${danmu}` - - try { - const response = await BAPI.live.sendMsg(danmu, room_id) - this.logger.log(`BAPI.live.sendMsg(${danmu}, ${room_id})`, response) - if (response.code === 0) { - if (response.msg === '') { - this.logger.log(`点亮熄灭勋章-发弹幕 ${logMessage} 成功`) - return true - } else if (response.msg === 'k') { - this.logger.warn(`点亮熄灭勋章-发弹幕 ${logMessage} 异常,弹幕可能包含屏蔽词`) - } else if (response.msg === 'f') { - this.logger.warn(`点亮熄灭勋章-发弹幕 ${logMessage} 异常,弹幕被过滤`) - } else { - this.logger.warn(`点亮熄灭勋章-发弹幕 ${logMessage} 异常,未知错误:${response.msg}`) - } - } else { - this.logger.error(`点亮熄灭勋章-发送弹幕 ${logMessage} 失败`, response.message) - } - } catch (error) { - this.logger.error(`点亮熄灭勋章-发送弹幕 ${logMessage} 出错`, error) - } - - return false - } - /** * 给正在直播的直播间点赞 * @param medals */ private async likeTask( medals: LiveData.FansMedalPanel.List[], - ): Promise { + ): Promise> { const attemptedMedals: LiveData.FansMedalPanel.List[] = [] + const skippedByStatusMedals: LiveData.FansMedalPanel.List[] = [] for (let i = 0; i < medals.length; i++) { const medal = medals[i] @@ -180,6 +119,24 @@ class LightTask extends MedalModule { ) continue } + + const verdict = await this.preExecuteVerify(room_id, (liveStatus) => liveStatus === 1) + + if (verdict === 'error') { + skippedByStatusMedals.push(medal) + this.logger.error( + `粉丝勋章【${medal_name}】 执行前校验:无法确认主播【${nick_name}】(UID:${target_id},直播间:${room_id})当前是否在直播,转交下一轮发弹幕点亮;可能遭遇风控,休眠 5 分钟再继续`, + ) + await sleep(300e3) + continue + } else if (verdict === 'fail') { + skippedByStatusMedals.push(medal) + this.logger.log( + `粉丝勋章【${medal_name}】 执行前校验:主播【${nick_name}】(UID:${target_id},直播间:${room_id})当前不在直播,转交下一轮发弹幕点亮`, + ) + continue + } + const times = MedalModule.parseTitleCount(likeItem.title) ?? 30 await this.like(medal, times) attemptedMedals.push(medal) @@ -189,18 +146,21 @@ class LightTask extends MedalModule { } } - return attemptedMedals + return { attemptedMedals, skippedByStatusMedals } } /** * 在未开播的直播间发弹幕 - * @param medals + * + * @param medals 待发弹幕的粉丝勋章 + * @param danmuIndexRef 弹幕索引引用,用于记录当前正在发送的弹幕索引 */ private async sendDanmuTask( medals: LiveData.FansMedalPanel.List[], - ): Promise { - let danmuIndex = 0 + danmuIndexRef: { value: number }, + ): Promise> { const attemptedMedals: LiveData.FansMedalPanel.List[] = [] + const skippedByStatusMedals: LiveData.FansMedalPanel.List[] = [] for (let i = 0; i < medals.length; i++) { const medal = medals[i] @@ -225,11 +185,29 @@ class LightTask extends MedalModule { continue } + const verdict = await this.preExecuteVerify(room_id, (liveStatus) => liveStatus !== 1) + + if (verdict === 'error') { + skippedByStatusMedals.push(medal) + this.logger.error( + `粉丝勋章【${medal_name}】 执行前校验:无法确认主播【${nick_name}】(UID:${target_id},直播间:${room_id})当前是否在直播,转交下一轮点赞点亮;可能遭遇风控,休眠 5 分钟再继续`, + ) + await sleep(300e3) + continue + } else if (verdict === 'fail') { + skippedByStatusMedals.push(medal) + this.logger.log( + `粉丝勋章【${medal_name}】 执行前校验:主播【${nick_name}】(UID:${target_id},直播间:${room_id})当前正在直播,转交下一轮点赞点亮`, + ) + continue + } + let remaining = MedalModule.parseTitleCount(danmuItem.title) ?? 10 let failedCount = 0 for (let j = 0; j < remaining; j++) { - const danmuText = this.config.danmuList[danmuIndex++ % this.config.danmuList.length] + const danmuText = + this.config.danmuList[danmuIndexRef.value++ % this.config.danmuList.length] if (!(await this.sendDanmu(medal, danmuText))) { if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { @@ -237,7 +215,7 @@ class LightTask extends MedalModule { if (i < medals.length - 1) await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) break } - remaining++ + remaining += 1 } if (i < medals.length - 1 || j < remaining - 1) { @@ -248,35 +226,58 @@ class LightTask extends MedalModule { attemptedMedals.push(medal) } - return attemptedMedals + return { attemptedMedals, skippedByStatusMedals } } public async run(): Promise { this.logger.log('点亮熄灭勋章模块开始运行') if (!isTimestampToday(this.config._lastCompleteTime)) { - if (!(await this.waitForFansMedals())) { + if (!(await MedalModule.waitForFansMedals())) { this.logger.error('粉丝勋章数据不存在,不执行点亮熄灭勋章任务') this.status = 'error' return } this.status = 'running' - const { notLivingMedals, livingMedals } = this.getMedals() + MedalModule.initSnapshotsWithFansMedalsData() + + let { notLivingMedals, livingMedals } = this.getMedals() // 是否有需要点亮的粉丝勋章(是不是一次有效运行?) const isEffectiveRun = notLivingMedals.length > 0 || livingMedals.length > 0 let allCompleted = true if (isEffectiveRun) { - const [danmuResult, likeResult] = await Promise.allSettled([ - this.sendDanmuTask(notLivingMedals), - this.likeTask(livingMedals), - ]) - // 尝试点亮过的粉丝勋章 - const attemptedMedals = [ - ...(danmuResult.status === 'fulfilled' ? danmuResult.value : []), - ...(likeResult.status === 'fulfilled' ? likeResult.value : []), - ] + const attemptedMedals: LiveData.FansMedalPanel.List[] = [] + const danmuIndexRef = { value: 0 } + + while (notLivingMedals.length > 0 || livingMedals.length > 0) { + const [danmuResult, likeResult] = await Promise.allSettled([ + this.sendDanmuTask(notLivingMedals, danmuIndexRef), + this.likeTask(livingMedals), + ]) + if (danmuResult.status === 'rejected' || likeResult.status === 'rejected') { + allCompleted = false + break + } + + attemptedMedals.push( + ...danmuResult.value.attemptedMedals, + ...likeResult.value.attemptedMedals, + ) + + livingMedals = danmuResult.value.skippedByStatusMedals + notLivingMedals = likeResult.value.skippedByStatusMedals + if (notLivingMedals.length > 0 || livingMedals.length > 0) { + this.logger.log( + `有 ${notLivingMedals.length + livingMedals.length} 个粉丝勋章因执行前校验发现直播状态不符被跳过,立即按最新状态重试`, + { + retryDanmuMedals: notLivingMedals, + retryLikeMedals: livingMedals, + }, + ) + } + } if (attemptedMedals.length > 0 || this.shouldRefreshFansMedals()) { // 刷新粉丝勋章,确保 点亮任务自身以及 点赞/发弹幕/观看直播 任务都能拿到最新勋章状态 diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index 1c015ad3..0fc059ec 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -1,11 +1,10 @@ import { delayToNextMoment, isNowBefore, isTimestampToday, tsm } from '@/library/luxon' -import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore } from '@/stores' import type { ModuleStatusTypes } from '@/types' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' import { sleep } from '@/library/utils' -import type { GroupedMedals, TaskExecutionResult } from './types' +import type { AfterExecutionAction, BatchExecutionResult, GroupedMedals } from './types' class LikeTask extends MedalModule { config = this.medalTasksConfig.like @@ -48,43 +47,29 @@ class LikeTask extends MedalModule { return result } - /** - * 点赞 - * - * @returns 是否点赞成功 - */ - private async like(medal: LiveData.FansMedalPanel.List, click_time: number): Promise { - const room_id = medal.room_info.room_id - const target_id = medal.medal.target_id - const nick_name = medal.anchor_info.nick_name - const medal_name = medal.medal.medal_name - const logMessage = `粉丝勋章【${medal_name}】 给主播【${nick_name}】(UID:${target_id})的直播间(${room_id})点赞 ${click_time} 次` - - try { - const response = await BAPI.live.likeReport(room_id, target_id, click_time) - this.logger.log(`BAPI.live.likeReport(${room_id}, ${target_id}, ${click_time})`, response) - if (response.code === 0) { - this.logger.log(`点赞 ${logMessage} 成功`) - return true - } else { - this.logger.error(`点赞 ${logMessage} 失败`, response.message) - } - } catch (error) { - this.logger.error(`点赞 ${logMessage} 出错`, error) - } - - return false - } - /** * 执行单个直播间的点赞任务 * - * @returns 执行结果,包含是否中断以及是否确认完成 + * @param medal 粉丝勋章 + * @param skipPreVerify 是否跳过执行前直播状态校验,默认 false + * + * @returns 执行结果 */ - private async executeLikeTask(medal: LiveData.FansMedalPanel.List): Promise { - if (this.shouldStopForCrossDay()) { + private async executeLikeTask( + medal: LiveData.FansMedalPanel.List, + skipPreVerify?: false, + ): Promise + private async executeLikeTask( + medal: LiveData.FansMedalPanel.List, + skipPreVerify?: true, + ): Promise> + private async executeLikeTask( + medal: LiveData.FansMedalPanel.List, + skipPreVerify = false, + ): Promise> { + if (MedalModule.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') - return { interrupted: true, verifiedCompleted: false } + return 'stop' } const room_id = medal.room_info.room_id @@ -97,14 +82,14 @@ class LikeTask extends MedalModule { this.logger.error( `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息,跳过点赞任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } if (medalData.reach_free_intimacy_limit) { this.logger.warn( `粉丝勋章【${medal_name}】(主播【${nick_name}】,UID:${target_id},直播间:${room_id})已达到储蓄亲密度上限(已储蓄 ${medalData.free_intimacy} 亲密度,投喂一个粉丝灯牌即可领取这些亲密度),无法通过点赞获取更多亲密度,跳过点赞任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } const item = MedalModule.findTaskInfo(medalData.task_info, 'like') @@ -112,20 +97,38 @@ class LikeTask extends MedalModule { this.logger.error( `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } - if (item.is_done) return { interrupted: false, verifiedCompleted: true } + if (item.is_done) return 'skipSleep' const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${target_id},直播间:${room_id})的点赞任务的每日上限信息,跳过点赞任务`, ) - return { interrupted: false, verifiedCompleted: true } + return 'skipSleep' } - if (parsed.current >= parsed.limit) return { interrupted: false, verifiedCompleted: true } + if (parsed.current >= parsed.limit) return 'skipSleep' + + if (!skipPreVerify) { + const verdict = await this.preExecuteVerify(room_id, (liveStatus) => liveStatus === 1) + + if (verdict === 'error') { + this.logger.error( + `粉丝勋章【${medal_name}】 执行前校验:无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的直播状态,${this.config.waitUntilLiving ? '回到等待队列' : '跳过点赞任务'};可能遭遇风控,休眠 5 分钟再继续`, + ) + await sleep(300e3) + + return this.config.waitUntilLiving ? 'requeue' : 'skipSleep' + } else if (verdict === 'fail') { + this.logger.log( + `粉丝勋章【${medal_name}】 执行前校验:主播【${nick_name}】(UID:${target_id},直播间:${room_id})当前不在直播,${this.config.waitUntilLiving ? '回到等待队列' : '跳过点赞任务'}`, + ) + return this.config.waitUntilLiving ? 'requeue' : 'skipSleep' + } + } const times = MedalModule.parseTitleCount(item.title) ?? 30 const target = this.config.useTargetRounds @@ -135,9 +138,9 @@ class LikeTask extends MedalModule { let hasSuccessfulLike = false for (let j = 0; j < remaining; j++) { - if (this.shouldStopForCrossDay()) { + if (MedalModule.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') - return { interrupted: true, verifiedCompleted: false } + return 'stop' } if (await this.like(medal, times)) { @@ -150,43 +153,46 @@ class LikeTask extends MedalModule { } if (hasSuccessfulLike) { - return { - interrupted: false, - verifiedCompleted: await this.confirmTaskCompletedAfterUpdate( - medal, - 'like', - this.config.useTargetRounds ? target : undefined, - ), - } + return (await this.confirmTaskCompletedAfterUpdate( + medal, + 'like', + this.config.useTargetRounds ? target : undefined, + )) + ? null + : 'markUncompleted' } // 所有点赞尝试均失败,估计是有什么异常状况,跳过 - return { interrupted: false, verifiedCompleted: true } + return null } /** * 顺序执行多个直播间的点赞任务 * - * @returns 执行结果,包含是否中断以及是否都确认完成 + * @returns 执行结果 */ private async executeLikeTasks( medals: LiveData.FansMedalPanel.List[], - ): Promise { - let verifiedCompleted = true + ): Promise { + let markUncompleted = false + const requeueRoomids: number[] = [] for (let i = 0; i < medals.length; i++) { - const result = await this.executeLikeTask(medals[i]) - if (result.interrupted) return result - if (!result.verifiedCompleted) { - verifiedCompleted = false + const action = await this.executeLikeTask(medals[i]) + if (action === 'stop') { + return { stop: true } + } else if (action === 'requeue') { + requeueRoomids.push(medals[i].room_info.room_id) + } else if (action === 'markUncompleted') { + markUncompleted = true } - if (i < medals.length - 1) { + if (action !== 'skipSleep' && i < medals.length - 1) { await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) } } - return { interrupted: false, verifiedCompleted } + return { markUncompleted, requeueRoomids } } public async run(): Promise { @@ -195,66 +201,64 @@ class LikeTask extends MedalModule { if (!isTimestampToday(this.config._lastCompleteTime)) { await this.waitForLightTask() - if (!(await this.waitForFansMedals())) { + if (!(await MedalModule.waitForFansMedals())) { this.logger.error('粉丝勋章数据不存在,不执行点赞任务') this.status = 'error' return } this.status = 'running' + MedalModule.initSnapshotsWithFansMedalsData() + const { readyMedals, waitingMedals } = this.getMedals() let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) let allCompleted = true - const initialResult = await this.executeLikeTasks(readyMedals) - if (initialResult.interrupted) { - allCompleted = false - } else if (!initialResult.verifiedCompleted) { - allCompleted = false - } + const { stop, markUncompleted, requeueRoomids } = await this.executeLikeTasks(readyMedals) - while (allCompleted && pendingRoomids.length > 0) { - if (this.shouldStopForCrossDay()) { - this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') + if (stop) { + allCompleted = false + } else { + if (markUncompleted) { allCompleted = false - break + } + if (requeueRoomids) { + pendingRoomids.push(...requeueRoomids) } - const { readyMedals: newlyReadyMedals, pendingRoomids: nextPendingRoomids } = - await this.probeWaitingMedals(pendingRoomids, (liveStatus) => liveStatus === 1) + while (pendingRoomids.length > 0) { + const { stop, markUncompleted, requeueRoomids } = await this.runWaitingRound( + pendingRoomids, + (liveStatus) => liveStatus === 1, + (medal) => this.executeLikeTask(medal, true), + ) + if (stop) { + allCompleted = false + break + } + if (markUncompleted) { + allCompleted = false + } - pendingRoomids = nextPendingRoomids + pendingRoomids = requeueRoomids! - const waitResult = await this.executeLikeTasks(newlyReadyMedals) - if (waitResult.interrupted) { - allCompleted = false - break - } - if (!waitResult.verifiedCompleted) { - allCompleted = false - break - } + if (pendingRoomids.length > 0) { + const medalMap = useBiliStore().filteredFansMedalsMap + const pendingRoomsInfo: Record = {} - if (pendingRoomids.length > 0) { - const medalMap = useBiliStore().filteredFansMedalsMap - const pendingRoomsInfo: Record = {} + for (const roomid of pendingRoomids) { + pendingRoomsInfo[roomid] = medalMap.get(roomid)?.anchor_info.nick_name + } - for (const roomid of pendingRoomids) { - pendingRoomsInfo[roomid] = medalMap.get(roomid)?.anchor_info.nick_name + this.logger.log( + `仍有 ${pendingRoomids.length} 个直播间未开播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + { pendingRoomsInfo }, + ) + await sleep(MedalModule.WAIT_POLL_INTERVAL) } - - this.logger.log( - `仍有 ${pendingRoomids.length} 个直播间未开播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, - { pendingRoomsInfo }, - ) - await sleep(MedalModule.WAIT_POLL_INTERVAL) } } - if (pendingRoomids.length > 0) { - allCompleted = false - } - if (allCompleted) { this.config._lastCompleteTime = tsm() this.status = 'done' diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 1d68e08a..152638a7 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -20,16 +20,40 @@ type GroupedMedals = Record type RequestQueueKey = 'taskInfo' | 'roomStatus' type TaskJumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' -type WaitStrategy = 'single-probe' | 'refresh-fans-medals' -interface WaitProbeResult { - readyMedals: LiveData.FansMedalPanel.List[] - pendingRoomids: number[] +/** 直播间直播状态快照 */ +interface LiveStatusSnapshot { + /** 直播状态:1 直播中,0/2 未开播/轮播 */ + liveStatus: number + /** 观测到该状态的时刻(毫秒时间戳) */ + observedAt: number } -interface TaskExecutionResult { - interrupted: boolean - verifiedCompleted: boolean +/** + * 直播间任务执行前直播状态校验结论 + * + * - `pass`:符合目标状态 + * - `fail`:不符合目标状态 + * - `error`:探测失败 + */ +type PreExecuteVerdict = 'pass' | 'fail' | 'error' + +/** + * 直播间任务执行后操作 + * + * - `stop`:终止后续直播间任务(也意味着当前任务未完成) + * - `requeue`:把直播间放到等待队列 + * - `markUncompleted`:把当前任务(点赞/发弹幕)标记为未完成 + * - `skipSleep`:跳过执行下一个直播间任务之间的等待时间 + */ +type AfterExecutionAction = 'stop' | 'requeue' | 'markUncompleted' | 'skipSleep' | null + +/** 直播间任务批量执行结果 */ +interface BatchExecutionResult { + stop?: boolean + markUncompleted?: boolean + /** 放到等待队列的直播间id列表 */ + requeueRoomids?: number[] } export { @@ -38,7 +62,8 @@ export { GroupedMedals, RequestQueueKey, TaskJumpType, - WaitStrategy, - WaitProbeResult, - TaskExecutionResult, + LiveStatusSnapshot, + PreExecuteVerdict, + AfterExecutionAction, + BatchExecutionResult, } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index e53eb217..585aa843 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -24,23 +24,21 @@ class RoomHeart { areaID: number, parentID: number, ruid: number, - watchedSeconds: number, targetSeconds: number, ) { this.roomID = roomID this.areaID = areaID this.parentID = parentID this.ruid = ruid - this.watchedSeconds = watchedSeconds this.targetSeconds = targetSeconds } private logger = new Logger('RoomHeart') - /** 今日当前直播间已观看时间(秒) */ - private watchedSeconds: number + /** 已观看时间(秒) */ + private watchedSeconds: number = 0 - /** 当前直播间观看目标秒数 */ + /** 目标观看时间(秒) */ private readonly targetSeconds: number private readonly areaID: number @@ -55,16 +53,6 @@ class RoomHeart { return [this.parentID, this.areaID, this.seq, this.roomID] } - /** 更新当前直播间的观看任务进度 */ - private updateProgress() { - // 记录观看时间 - this.watchedSeconds += this.heartBeatInterval - - useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks.watch._watchingProgress[ - this.roomID - ] = this.watchedSeconds - } - /** Cookie LIVE_BUVID */ private buvid?: string = useBiliStore().cookies!.LIVE_BUVID @@ -80,6 +68,11 @@ class RoomHeart { private secretRule!: number[] /** ets */ private timestamp!: number + /** 下一次 X 心跳的目标发送时间 */ + private nextHeartbeatAt = 0 + + private static readonly HEARTBEAT_WAIT_SLICE_MS = 1000 + private static readonly HEARTBEAT_DRIFT_WARN_MS = 5000 /** * 开始心跳 @@ -95,6 +88,46 @@ class RoomHeart { return this.watchedSeconds > 0 } + private setHeartbeatState(data: { + heartbeat_interval: number + secret_key: string + secret_rule: number[] + timestamp: number + }) { + ;({ + heartbeat_interval: this.heartBeatInterval, + secret_key: this.secretKey, + secret_rule: this.secretRule, + timestamp: this.timestamp, + } = data) + + // 根据观察,心跳响应中的 timestamp = 上次心跳响应中的 timestamp + 上次心跳响应中的 heartbeat_interval + // 如果是第一次心跳,timestamp = 发心跳时的秒级时间戳 -1或-2(可能是B站服务器的时间不准?误差不大实测可忽略) + // 总是使用最新心跳中返回的 timestamp 和 heartBeatInterval 来计算下次发送的时间 + // 如果每次心跳后固定等待 heartBeatInterval 秒,发送时间误差会随时间推移变得越来越大 + this.nextHeartbeatAt = (this.timestamp + this.heartBeatInterval) * 1000 + } + + /** + * 等待下一次心跳的发送时间到来,期间每隔一段时间检查一次剩余时间 + */ + private async waitForNextHeartbeat(): Promise { + while (true) { + const remaining = this.nextHeartbeatAt - tsm() + + if (remaining <= 0) { + const drift = -remaining + if (drift >= RoomHeart.HEARTBEAT_DRIFT_WARN_MS) { + this.logger.warn( + `直播间 ${this.roomID} 的 X 心跳发送已滞后 ${(drift / 1000).toFixed(2)} 秒,下次心跳可能触发服务端 time check failed`, + ) + } + return + } + await sleep(Math.min(remaining, RoomHeart.HEARTBEAT_WAIT_SLICE_MS)) + } + } + /** * E心跳,开始时发送一次 */ @@ -107,22 +140,21 @@ class RoomHeart { ) if (response.code === 0) { this.seq += 1 - ;({ - heartbeat_interval: this.heartBeatInterval, - secret_key: this.secretKey, - secret_rule: this.secretRule, - timestamp: this.timestamp, - } = response.data) - await sleep(this.heartBeatInterval * 1000) + this.setHeartbeatState(response.data) + await this.waitForNextHeartbeat() return this.X() } else { this.logger.error( `BAPI.liveTrace.E(${this.id}, ${this.device}, ${this.ruid}) 失败`, response.message, ) + this.logger.error( + `直播间 ${this.roomID} 的 E 心跳失败,无法继续执行观看直播任务,跳过该房间`, + ) } } catch (error) { this.logger.error(`BAPI.liveTrace.E(${this.id}, ${this.device}, ${this.ruid}) 出错`, error) + this.logger.error(`直播间 ${this.roomID} 的 E 心跳失败,无法继续执行观看直播任务,跳过该房间`) } } @@ -130,64 +162,72 @@ class RoomHeart { * X心跳,E心跳过后都是X心跳 */ private async X(): Promise { - if (isNowAfter(23, 58) || isNowBefore(0, 5)) { - this.logger.log(`即将或刚刚发生跨天,停止直播间 ${this.roomID} 的X心跳`) - return - } - - try { - const spyderData: SpyderData = { - id: JSON.stringify(this.id), - device: JSON.stringify(this.device), - ets: this.timestamp, - benchmark: this.secretKey, - time: this.heartBeatInterval, - ts: tsm(), - ua: this.ua, + while (true) { + if (isNowAfter(23, 58) || isNowBefore(0, 5)) { + this.logger.log(`即将或刚刚发生跨天,停止直播间 ${this.roomID} 的X心跳`) + return } - // 签名 - const s = this.spyder(JSON.stringify(spyderData), this.secretRule) - - const response = await BAPI.liveTrace.X( - s, - this.id, - this.device, - this.ruid, - this.timestamp, - this.secretKey, - this.heartBeatInterval, - spyderData.ts, - ) - this.logger.log( - `BAPI.liveTrace.X(${s}, ${this.id}, ${this.device}, ${this.ruid}, ${this.timestamp}, ${this.secretKey}, ${this.heartBeatInterval}, ${spyderData.ts}) response`, - response, - ) - if (response.code === 0) { - this.seq += 1 - this.updateProgress() - if (this.watchedSeconds >= this.targetSeconds) { - // 达到目标观看时间,结束 + + try { + const spyderData: SpyderData = { + id: JSON.stringify(this.id), + device: JSON.stringify(this.device), + ets: this.timestamp, + benchmark: this.secretKey, + time: this.heartBeatInterval, + ts: tsm(), + ua: this.ua, + } + // 签名 + const s = this.spyder(JSON.stringify(spyderData), this.secretRule) + + const response = await BAPI.liveTrace.X( + s, + this.id, + this.device, + this.ruid, + this.timestamp, + this.secretKey, + this.heartBeatInterval, + spyderData.ts, + ) + this.logger.log( + `BAPI.liveTrace.X(${s}, ${this.id}, ${this.device}, ${this.ruid}, ${this.timestamp}, ${this.secretKey}, ${this.heartBeatInterval}, ${spyderData.ts}) response`, + response, + ) + if (response.code === 0) { + this.seq += 1 + this.watchedSeconds += this.heartBeatInterval + this.logger.log( + `直播间 ${this.roomID} 的第 ${this.seq - 1} 次 X 心跳成功,已观看 ${this.watchedSeconds} 秒`, + ) + if (this.watchedSeconds >= this.targetSeconds) { + // 达到目标观看时间,结束 + return + } + this.setHeartbeatState(response.data) + await this.waitForNextHeartbeat() + // 继续下一轮 X 心跳 + } else { + this.logger.error( + `BAPI.liveTrace.X(${s}, ${this.id}, ${this.device}, ${this.ruid}, ${this.timestamp}, ${this.secretKey}, ${this.heartBeatInterval}) 失败`, + response.message, + ) + this.logger.error( + `直播间 ${this.roomID} 的 X 心跳失败,无法继续执行观看直播任务,跳过该房间(目前已观看 ${this.watchedSeconds} 秒)`, + ) return } - ;({ - heartbeat_interval: this.heartBeatInterval, - secret_key: this.secretKey, - secret_rule: this.secretRule, - timestamp: this.timestamp, - } = response.data) - await sleep(this.heartBeatInterval * 1000) - return this.X() - } else { + } catch (error) { this.logger.error( - `BAPI.liveTrace.X(${s}, ${this.id}, ${this.device}, ${this.ruid}, ${this.timestamp}, ${this.secretKey}, ${this.heartBeatInterval}) 失败`, - response.message, + `BAPI.liveTrace.X(s, ${this.id}, ${this.device}, ${this.ruid}, ${this.timestamp}, ${this.secretKey}, ${this.heartBeatInterval}) 出错`, + error, + ) + this.logger.error( + `直播间 ${this.roomID} 的 X 心跳失败,无法继续执行观看直播任务,跳过该房间(目前已观看 ${this.watchedSeconds} 秒)`, ) + return } - } catch (error) { - this.logger.error( - `BAPI.liveTrace.X(s, ${this.id}, ${this.device}, ${this.ruid}, ${this.timestamp}, ${this.secretKey}, ${this.heartBeatInterval}) 出错`, - error, - ) } } @@ -314,20 +354,13 @@ class WatchTask extends MedalModule { if (!isTimestampToday(this.config._lastCompleteTime)) { await this.waitForLightTask() - if (!(await this.waitForFansMedals())) { + if (!(await MedalModule.waitForFansMedals())) { this.logger.error('粉丝勋章数据不存在,不执行观看直播任务') this.status = 'error' return } this.status = 'running' - - if (!isTimestampToday(this.config._lastWatchTime, 0, 0)) { - // 如果上次观看(不是完成任务)的时间戳不在今天,将今天已观看的秒数置为0 - this.config._watchingProgress = {} - } - - this.config._lastWatchTime = tsm() const fansMedals = this.getMedals() if (fansMedals.length > 0) { @@ -386,10 +419,10 @@ class WatchTask extends MedalModule { const target = this.config.useTargetRounds ? Math.min(parsed.limit, this.config.targetRounds) : parsed.limit - // 目标秒数 = 目标轮次 × minutes × 60s - const targetSeconds = target * minutes * 60 + // 剩余观看秒数 = (目标轮次 - 已完成轮次) × 分钟数 × 60s + const remainingSeconds = (target - parsed.current) * minutes * 60 - if ((this.config._watchingProgress[roomid] ?? 0) >= targetSeconds) { + if (remainingSeconds <= 0) { // 今日观看时间已达到目标值,跳过 continue } @@ -399,7 +432,7 @@ class WatchTask extends MedalModule { if (area_id > 0 && parent_area_id > 0) { // area_id 和 parent_area_id 都大于 0,说明直播间设置了分区,心跳有效 this.logger.log( - `粉丝勋章【${medal_name}】 开始直播间 ${roomid}(主播【${nick_name}】,UID:${uid})的观看直播任务,目标时长 ${targetSeconds / 60} 分钟)`, + `粉丝勋章【${medal_name}】 开始直播间 ${roomid}(主播【${nick_name}】,UID:${uid})的观看直播任务,目标时长 ${remainingSeconds / 60} 分钟`, ) const hasWatchingProgress = await new RoomHeart( @@ -407,8 +440,7 @@ class WatchTask extends MedalModule { area_id, parent_area_id, uid, - this.config._watchingProgress[roomid] ?? 0, - targetSeconds, + remainingSeconds, ).start() if (hasWatchingProgress) { diff --git a/src/modules/dailyTasks/mainSiteTasks/coinTask.ts b/src/modules/dailyTasks/mainSiteTasks/coinTask.ts index 60170234..18c7151d 100644 --- a/src/modules/dailyTasks/mainSiteTasks/coinTask.ts +++ b/src/modules/dailyTasks/mainSiteTasks/coinTask.ts @@ -66,7 +66,7 @@ class CoinTask extends BaseModule { // 剩余可投硬币数量 const allowed_coin_num = this.MAX_COIN - coined_num if (allowed_coin_num > 0) { - // 剩余可投硬币数和剩余所需投币数取较小者 + // 投币数量:剩余可投硬币数和剩余所需投币数取较小者 const coin_num = Math.min(allowed_coin_num, left_coin_num) const result = await this.coin(aid, coin_num) if (result === 0) { @@ -83,15 +83,12 @@ class CoinTask extends BaseModule { this.logger.warn('硬币余额不足,每日投币任务终止') this.status = 'error' break + } else { + // 接口报错或网络错误 + break } } } - - if (left_coin_num > 0) { - // 硬币余额不足,终止任务,不记录完成时间 - this.logger.warn('硬币余额不足,每日投币任务终止') - this.status = 'error' - } } /** @@ -153,7 +150,7 @@ class CoinTask extends BaseModule { // 剩余要投的硬币数量 const left_coin_num = this.config.num - total_coined_num // 拥有的硬币数量 - const money = biliStore.userInfo!.money ?? 5 + const money = biliStore.userInfo!.money if (left_coin_num > money) { this.logger.log('硬币余额不足,不执行每日投币任务') this.status = 'done' diff --git a/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts b/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts index 2a1b47ec..336b12ee 100644 --- a/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts +++ b/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts @@ -163,52 +163,52 @@ class GetYearVipPrivilegeTask extends BaseModule { // 当前时间已经超过了上次记录的下次领取时间,领取权益 this.status = 'running' const list = await this.myPrivilege() - if (list) { - const isSuperVip = this.isSuperVip() + if (!list) return - for (const privilege of list) { - if (!this.canReceivePrivilege(privilege, isSuperVip)) { - continue - } + const isSuperVip = this.isSuperVip() - if (privilege.state === 0) { - if (privilege.type === 9) { - // 专属等级加速包 - await this.addExperience() - } else { - await this.receivePrivilege(privilege.type) - } - } else if (privilege.state === 1) { - this.logger.log( - `该权益(type = ${privilege.type}, ${this.getPrivilegeName(privilege.type)})已经领取过了`, - ) + for (const privilege of list) { + if (!this.canReceivePrivilege(privilege, isSuperVip)) { + continue + } + + if (privilege.state === 0) { + if (privilege.type === 9) { + // 专属等级加速包 + await this.addExperience() } else { - // 需要完成任务才能领取 - if (privilege.type === 9) { - const watchTaskConfig = useModuleStore().moduleConfig.DailyTasks.MainSiteTasks.watch - if (watchTaskConfig.enabled) { - this.logger.log('等待观看视频任务完成后再领取专属等级加速包(10主站经验)...') - watch( - () => watchTaskConfig._lastCompleteTime, - () => sleep(3000).then(() => this.addExperience()), - { once: true }, - ) - } else { - this.logger.warn( - '领取专属等级加速包(10主站经验)前需要观看任意一个视频,请打开【主站任务】中的【每日观看视频】,或是在运行脚本前手动观看', - ) - } - } + await this.receivePrivilege(privilege.type) } - await sleep(500) - } - this.status = 'done' - const nextReceiveTimes = list.map((i) => i.period_end_unix).filter((unix) => unix > ts()) - if (nextReceiveTimes.length > 0) { - this.config._nextReceiveTime = Math.min(...nextReceiveTimes) + } else if (privilege.state === 1) { + this.logger.log( + `该权益(type = ${privilege.type}, ${this.getPrivilegeName(privilege.type)})已经领取过了`, + ) } else { - this.config._nextReceiveTime = ts() + 86400 + // 需要完成任务才能领取 + if (privilege.type === 9) { + const watchTaskConfig = useModuleStore().moduleConfig.DailyTasks.MainSiteTasks.watch + if (watchTaskConfig.enabled) { + this.logger.log('等待观看视频任务完成后再领取专属等级加速包(10主站经验)...') + watch( + () => watchTaskConfig._lastCompleteTime, + () => sleep(3000).then(() => this.addExperience()), + { once: true }, + ) + } else { + this.logger.warn( + '领取专属等级加速包(10主站经验)前需要观看任意一个视频,请打开【主站任务】中的【每日观看视频】,或是在运行脚本前手动观看', + ) + } + } } + await sleep(500) + } + this.status = 'done' + const nextReceiveTimes = list.map((i) => i.period_end_unix).filter((unix) => unix > ts()) + if (nextReceiveTimes.length > 0) { + this.config._nextReceiveTime = Math.min(...nextReceiveTimes) + } else { + this.config._nextReceiveTime = ts() + 86400 } } diff --git a/src/modules/default/dailyRewardInfo.ts b/src/modules/default/dailyRewardInfo.ts index fa853aec..9419ede2 100644 --- a/src/modules/default/dailyRewardInfo.ts +++ b/src/modules/default/dailyRewardInfo.ts @@ -3,7 +3,7 @@ import BAPI from '@/library/bili-api' import type { MainData } from '@/library/bili-api/data' import { delayToNextMoment, isTimestampToday } from '@/library/luxon' import BaseModule from '@/modules/BaseModule' -import ModuleError from '@/library/error/ModuleError' +import { ModuleError } from '@/library/error' class DailyRewardInfo extends BaseModule { /** diff --git a/src/modules/default/dynamicVideos.ts b/src/modules/default/dynamicVideos.ts index 0946eb35..b435166f 100644 --- a/src/modules/default/dynamicVideos.ts +++ b/src/modules/default/dynamicVideos.ts @@ -3,7 +3,7 @@ import BAPI from '@/library/bili-api' import type { MainData } from '@/library/bili-api/data' import { delayToNextMoment, isTimestampToday } from '@/library/luxon' import BaseModule from '@/modules/BaseModule' -import ModuleError from '@/library/error/ModuleError' +import { ModuleError } from '@/library/error' class DynamicVideos extends BaseModule { /** diff --git a/src/modules/default/fansMedals.ts b/src/modules/default/fansMedals.ts index 80749594..5cd25127 100644 --- a/src/modules/default/fansMedals.ts +++ b/src/modules/default/fansMedals.ts @@ -4,7 +4,7 @@ import type { LiveData } from '@/library/bili-api/data' import { delayToNextMoment, isTimestampToday, tsm } from '@/library/luxon' import { sleep } from '@/library/utils' import BaseModule from '@/modules/BaseModule' -import ModuleError from '@/library/error/ModuleError' +import { ModuleError } from '@/library/error' import _ from 'lodash' class FansMedals extends BaseModule { diff --git a/src/modules/default/userInfo.ts b/src/modules/default/userInfo.ts index 0b048d19..3e0ea0c1 100644 --- a/src/modules/default/userInfo.ts +++ b/src/modules/default/userInfo.ts @@ -3,7 +3,7 @@ import BAPI from '@/library/bili-api' import type { MainData } from '@/library/bili-api/data' import { delayToNextMoment } from '@/library/luxon' import BaseModule from '@/modules/BaseModule' -import ModuleCriticalError from '@/library/error/ModuleCriticalError' +import { ModuleCriticalError } from '@/library/error' class UserInfo extends BaseModule { /** diff --git a/src/modules/enhanceExperience/noReport.ts b/src/modules/enhanceExperience/noReport.ts index a0485cbb..daba4db9 100644 --- a/src/modules/enhanceExperience/noReport.ts +++ b/src/modules/enhanceExperience/noReport.ts @@ -222,9 +222,6 @@ class NoReport extends BaseModule { handler.next(config) } }, - onResponse(response, handler) { - handler.next(response) - }, } proxy(ajaxHookProxyConfig, unsafeWindow) diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index 02b3f95b..3b180e24 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -12,8 +12,7 @@ import { deepestIterate, waitForMoment } from '@/library/utils' import { useCacheStore } from './useCacheStore' import { isSelfTopFrame } from '@/library/dom' import type BaseModule from '@/modules/BaseModule' -import ModuleCriticalError from '@/library/error/ModuleCriticalError' -import ModuleError from '@/library/error/ModuleError' +import { ModuleError, ModuleCriticalError } from '@/library/error' const defaultModuleStatus: ModuleStatus = { DailyTasks: { @@ -50,7 +49,7 @@ export const useModuleStore = defineStore('module', () => { // 模块状态 const moduleStatus = ref(defaultModuleStatus) // 模块实例映射(key: 模块名称, value: 模块实例) - const moduleInstances = ref>({}) + const moduleInstances: Record = {} // 模块状态、运行记录重置和再运行 const moduleReset: ModuleReset = { DailyTasks: { @@ -126,8 +125,6 @@ export const useModuleStore = defineStore('module', () => { watch: () => { moduleStatus.value.DailyTasks.LiveTasks.medalTasks.watch = '' moduleConfig.value.DailyTasks.LiveTasks.medalTasks.watch._lastCompleteTime = 0 - moduleConfig.value.DailyTasks.LiveTasks.medalTasks.watch._lastWatchTime = 0 - moduleConfig.value.DailyTasks.LiveTasks.medalTasks.watch._watchingProgress = {} rerunModule('Default_FansMedals', true) rerunModule('DailyTask_LiveTask_WatchTask') @@ -166,7 +163,7 @@ export const useModuleStore = defineStore('module', () => { */ function _runModule(module: typeof BaseModule, name: string): Promise | void { const moduleInstance = new module(name) - moduleInstances.value[name] = moduleInstance + moduleInstances[name] = moduleInstance if (moduleInstance.isEnabled()) { return moduleInstance.run() @@ -278,7 +275,7 @@ export const useModuleStore = defineStore('module', () => { * @param args `run()` 方法参数 */ function rerunModule(moduleName: string, ...args: any[]): Promise | void { - const moduleInstance = moduleInstances.value[moduleName] + const moduleInstance = moduleInstances[moduleName] if (moduleInstance) { clearTimeout(moduleInstance.nextRunTimer) diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index 394ed405..3a6ff8df 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -54,8 +54,6 @@ interface ModuleConfig { targetRounds: number isWhiteList: boolean roomidList: number[] - _watchingProgress: Record - _lastWatchTime: number _lastCompleteTime: number } }