From 75353a574cc5d20ea68909eb0a333321c7d04024 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Mon, 25 May 2026 20:04:03 +0800 Subject: [PATCH 01/30] =?UTF-8?q?=E2=9C=A8=20feat(MedalModule):=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BB=BB=E5=8A=A1=E4=BF=A1=E6=81=AF=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缓存意义不大;队列限速够用了 --- .../liveTasks/medalTasks/MedalModule.ts | 36 +++---------------- .../liveTasks/medalTasks/danmuTask.ts | 1 - .../liveTasks/medalTasks/lightTask.ts | 1 - .../liveTasks/medalTasks/likeTask.ts | 1 - .../liveTasks/medalTasks/watchTask.ts | 1 - 5 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 0adf075b..c8d7a1c4 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -14,16 +14,6 @@ class MedalModule extends BaseModule { /** 最大弹幕补发次数 */ protected static readonly DANMU_RETRY_LIMIT = 3 - /** - * 任务信息共享缓存 - * - * target_id -> task_info Promise - */ - private static taskInfoCache = new Map< - number, - Promise - >() - /** * 简单限流队列 * @@ -124,27 +114,20 @@ class MedalModule extends BaseModule { return Number(match[0]) } - /** - * 清空任务信息共享缓存 - */ - protected static clearTaskInfoCache(): void { - MedalModule.taskInfoCache.clear() - } - /** * 将获取任务信息请求加入全局串行队列 */ private static enqueueTaskInfoRequest(requester: () => Promise): Promise { const task = MedalModule.taskInfoRequestQueue - .catch(() => undefined) + .catch(() => {}) .then(async () => { await sleep(_.random(300, 500)) return requester() }) MedalModule.taskInfoRequestQueue = task.then( - () => undefined, - () => undefined, + () => {}, + () => {}, ) return task @@ -153,8 +136,7 @@ class MedalModule extends BaseModule { /** * 获取指定粉丝勋章的任务信息 * - * 同一 target_id 的并发请求会共享同一个 Promise; - * 不同 target_id 的请求会进入全局串行队列,避免短时间内并发触发过多请求 + * 请求会进入全局串行队列,避免短时间内并发触发过多请求 * * @param target_id 主播 uid * @returns 成功返回任务信息数组,失败返回 null @@ -162,10 +144,7 @@ class MedalModule extends BaseModule { protected fetchTaskInfo( target_id: number, ): Promise { - const cached = MedalModule.taskInfoCache.get(target_id) - if (cached) return cached - - const promise = MedalModule.enqueueTaskInfoRequest(async () => { + return MedalModule.enqueueTaskInfoRequest(async () => { try { const response = await BAPI.live.getActivatedMedalInfo(target_id) this.logger.log(`BAPI.live.getActivatedMedalInfo(${target_id}) response`, response) @@ -173,18 +152,13 @@ class MedalModule extends BaseModule { return response.data.task_info } else { this.logger.error(`BAPI.live.getActivatedMedalInfo(${target_id}) 失败`, response.message) - MedalModule.taskInfoCache.delete(target_id) return null } } catch (error) { this.logger.error(`BAPI.live.getActivatedMedalInfo(${target_id}) 出错`, error) - MedalModule.taskInfoCache.delete(target_id) return null } }) - - MedalModule.taskInfoCache.set(target_id, promise) - return promise } /** diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index b83dc0dc..640ee679 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -82,7 +82,6 @@ class DanmuTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() const fansMedals = this.getMedals() let allCompleted = true let danmuIndex = 0 diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index a00b5e12..aae23345 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -175,7 +175,6 @@ class LightTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() const fansMedals = this.getMedals() // 是否有需要点亮的粉丝勋章(是不是一次有效运行?) const isEffectiveRun = fansMedals.some((medals) => medals.length > 0) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index 60df241b..6e1b689a 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -70,7 +70,6 @@ class LikeTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() const fansMedals = this.getMedals() let allCompleted = true diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index dd5d648e..7cbdc581 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -319,7 +319,6 @@ class WatchTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() if (!isTimestampToday(this.config._lastWatchTime, 0, 0)) { // 如果上次观看(不是完成任务)的时间戳不在今天,将今天已观看的秒数置为0 From 86f9ebf32481749e7c793d53c307289f7617f92d Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Mon, 25 May 2026 20:05:15 +0800 Subject: [PATCH 02/30] =?UTF-8?q?=F0=9F=90=8E=20ci:=20=E6=9B=B4=E6=96=B0ac?= =?UTF-8?q?tion=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 32112e9b..0b38f480 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "24.13.0" @@ -46,7 +46,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 5a8bbcb4543706d3eb563bec3adc0843f8bdb0c1 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Tue, 26 May 2026 15:17:35 +0800 Subject: [PATCH 03/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=E5=82=A8=E8=93=84?= =?UTF-8?q?=E4=BA=B2=E5=AF=86=E5=BA=A6=E6=A3=80=E6=9F=A5=EF=BC=9B=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=8E=B7=E5=8F=96=E7=82=B9=E4=BA=AE=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=97=B6=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++ .../liveTasks/medalTasks/MedalModule.ts | 24 ++++++++++++---- .../liveTasks/medalTasks/danmuTask.ts | 8 ++++-- .../liveTasks/medalTasks/lightTask.ts | 28 +++++++++++++------ .../liveTasks/medalTasks/likeTask.ts | 10 ++++--- .../liveTasks/medalTasks/watchTask.ts | 8 ++++-- 6 files changed, 61 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3525b88c..1c23cc48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### 新增 + +- 执行完任意粉丝勋章的粉丝团任务后,检查是否存在储蓄的亲密度以及是否达到储蓄上限,把结果输出到控制台日志 + +### 调整 + +- 如果无法获取某个粉丝勋章的点亮任务,跳过该粉丝勋章 + ## [7.2.9] - 2026-5-24 ### 新增 diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index c8d7a1c4..6023cbae 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -134,22 +134,20 @@ class MedalModule extends BaseModule { } /** - * 获取指定粉丝勋章的任务信息 + * 获取指定粉丝勋章的信息 * * 请求会进入全局串行队列,避免短时间内并发触发过多请求 * * @param target_id 主播 uid - * @returns 成功返回任务信息数组,失败返回 null + * @returns 成功返回完整 Data,失败返回 null */ - protected fetchTaskInfo( - target_id: number, - ): Promise { + protected fetchMedalData(target_id: number): Promise { return MedalModule.enqueueTaskInfoRequest(async () => { try { const response = await BAPI.live.getActivatedMedalInfo(target_id) this.logger.log(`BAPI.live.getActivatedMedalInfo(${target_id}) response`, response) if (response.code === 0) { - return response.data.task_info + return response.data } else { this.logger.error(`BAPI.live.getActivatedMedalInfo(${target_id}) 失败`, response.message) return null @@ -161,6 +159,20 @@ class MedalModule extends BaseModule { }) } + /** + * 完成某粉丝勋章任务后,重新调用 fetchMedalData 检查储蓄亲密度,若存在则提示用户 + */ + protected async logFreeIntimacy(medal: LiveData.FansMedalPanel.List): Promise { + const data = await this.fetchMedalData(medal.medal.target_id) + + if (data && data.free_intimacy > 0) { + const reachLimitText = data.reach_free_intimacy_limit ? '(已达到储蓄亲密度上限)' : '' + this.logger.log( + `粉丝勋章【${medal.medal.medal_name}】储蓄了 ${data.free_intimacy} 亲密度${reachLimitText},投喂一个粉丝灯牌即可领取这些亲密度`, + ) + } + } + /** * 若点亮熄灭勋章任务已启用且今天未完成,等待其结束后再继续 * diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 640ee679..e3529156 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -94,15 +94,15 @@ class DanmuTask extends MedalModule { } const medal = fansMedals[i] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - if (!taskInfo) { + const medalData = await this.fetchMedalData(medal.medal.target_id) + if (!medalData) { this.logger.error( `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`, ) continue } - const item = MedalModule.findTaskInfo(taskInfo, 'sendDanmu') + const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') if (!item) { this.logger.error( `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, @@ -146,6 +146,8 @@ class DanmuTask extends MedalModule { await sleep(_.random(6000, 8000)) } } + + await this.logFreeIntimacy(medal) } if (allCompleted) { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index aae23345..4646cca8 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -112,16 +112,21 @@ class LightTask extends MedalModule { /** * 给正在直播的直播间点赞 * @param medals - * @private */ private async likeTask(medals: LiveData.FansMedalPanel.List[]): Promise { for (let i = 0; i < medals.length; i++) { const medal = medals[i] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - const item = MedalModule.findTaskInfo(taskInfo, 'like') - // 从 title 解析点亮所需点赞次数(如 "点赞30次" → 30),失败时 fallback 到 30 + const medalData = await this.fetchMedalData(medal.medal.target_id) + if (!medalData) { + this.logger.error( + `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团点亮任务信息,跳过点赞任务`, + ) + continue + } + + const item = MedalModule.findTaskInfo(medalData.task_info, 'like') const baseCount = MedalModule.parseTitleCount(item?.title) ?? 30 - await this.like(medal, _.random(baseCount, baseCount + 5)) + await this.like(medal, _.random(baseCount, baseCount + 3)) if (i < medals.length - 1) { await sleep(_.random(15000, 20000)) @@ -132,16 +137,21 @@ class LightTask extends MedalModule { /** * 在未开播的直播间发弹幕 * @param medals - * @private */ private async sendDanmuTask(medals: LiveData.FansMedalPanel.List[]): Promise { let danmuIndex = 0 for (let i = 0; i < medals.length; i++) { const medal = medals[i] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - const item = MedalModule.findTaskInfo(taskInfo, 'sendDanmu') - // 从 title 解析点亮所需弹幕条数(如 "发弹幕10次" → 10),失败时 fallback 到 10 + const medalData = await this.fetchMedalData(medal.medal.target_id) + if (!medalData) { + this.logger.error( + `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团点亮任务信息,跳过发弹幕任务`, + ) + continue + } + + const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') let target = MedalModule.parseTitleCount(item?.title) ?? 10 let failedCount = 0 diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index 6e1b689a..e81fb835 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -81,15 +81,15 @@ class LikeTask extends MedalModule { } const medal = fansMedals[i] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - if (!taskInfo) { + const medalData = await this.fetchMedalData(medal.medal.target_id) + if (!medalData) { this.logger.error( `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`, ) continue } - const item = MedalModule.findTaskInfo(taskInfo, 'like') + const item = MedalModule.findTaskInfo(medalData.task_info, 'like') if (!item) { this.logger.error( `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, @@ -120,12 +120,14 @@ class LikeTask extends MedalModule { break outer } - await this.like(medal, _.random(times, times + 5)) + await this.like(medal, _.random(times, times + 3)) if (j < remaining - 1 || i < fansMedals.length - 1) { await sleep(_.random(15000, 20000)) } } + + await this.logFreeIntimacy(medal) } if (allCompleted) { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index 7cbdc581..d4eafcbc 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -346,15 +346,15 @@ class WatchTask extends MedalModule { const roomid = medal.room_info.room_id const uid = medal.medal.target_id - const taskInfo = await this.fetchTaskInfo(uid) - if (!taskInfo) { + const medalData = await this.fetchMedalData(uid) + if (!medalData) { this.logger.error( `无法获取主播【${medal.anchor_info.nick_name}】(UID:${uid})的粉丝团升级任务信息,跳过观看直播任务`, ) continue } - const item = MedalModule.findTaskInfo(taskInfo, 'watchLive') + const item = MedalModule.findTaskInfo(medalData.task_info, 'watchLive') if (!item) { this.logger.error( `无法在主播【${medal.anchor_info.nick_name}】(UID:${uid})的粉丝团升级任务信息中找到观看直播任务,跳过观看直播任务`, @@ -399,6 +399,8 @@ class WatchTask extends MedalModule { this.config._watchingProgress[roomid] ?? 0, targetSeconds, ).start() + + await this.logFreeIntimacy(medal) } } From 8134ceffd1a71b9e7f812505583d303bb3424a18 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:05:15 +0800 Subject: [PATCH 04/30] =?UTF-8?q?=E2=9C=A8=20feat(API):=20getRoomPlayInfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 获取直播间的播放相关信息(清晰度、直播流地址等) - 计划后续用这个API来获取房间开播情况 --- src/library/bili-api/api.d.ts | 15 +++ src/library/bili-api/data.d.ts | 141 +++++++++++++++++++++++++++++ src/library/bili-api/index.ts | 34 +++++++ src/library/bili-api/response.d.ts | 7 ++ 4 files changed, 197 insertions(+) diff --git a/src/library/bili-api/api.d.ts b/src/library/bili-api/api.d.ts index 9d9e0dd8..dffe63f0 100644 --- a/src/library/bili-api/api.d.ts +++ b/src/library/bili-api/api.d.ts @@ -43,6 +43,21 @@ interface BapiMethods { getInfoByRoom: (room_id: number, web_location?: string) => Promise silver2coin: (visit_id?: string) => Promise coin2silver: (num: number, platform?: string, visit_id?: string) => Promise + getRoomPlayInfo: ( + room_id: number, + qn?: number, + protocol?: string, + format?: string, + codec?: string, + platform?: string, + ptype?: number, + dolby?: number, + panorama?: number, + eotf?: string, + req_reason?: number, + supported_drms?: string, + web_location?: string, + ) => Promise } liveTrace: { E: ( diff --git a/src/library/bili-api/data.d.ts b/src/library/bili-api/data.d.ts index ca42266f..a7cf165f 100644 --- a/src/library/bili-api/data.d.ts +++ b/src/library/bili-api/data.d.ts @@ -1198,6 +1198,147 @@ declare namespace LiveData { tid: string } } + + namespace GetRoomPlayInfo { + interface Data { + room_id: number + short_id: number + uid: number + is_hidden: boolean + is_locked: boolean + is_portrait: boolean + /** 0 未开播,1 直播中,2 轮播中 */ + live_status: number + hidden_till: number + lock_till: number + encrypted: boolean + pwd_verified: boolean + live_time: number + room_shield: number + all_special_types: number[] + /** 未开播时为 null */ + playurl_info: PlayurlInfo | null + official_type: number + official_room_id: number + risk_with_delay: number + multi_screen_info: string + pure_control_function: any + /** 降级播放地址信息,通常为 null */ + degraded_playurl: PlayurlInfo | null + subtitle_cfg: SubtitleCfg | null + relay_room_id: number + } + + interface PlayurlInfo { + conf_json: string + playurl: Playurl + expected_quality: ExpectedQuality + qn_desc_more_ab: number + } + + interface Playurl { + cid: number + g_qn_desc: QnDesc[] + stream: Stream[] + p2p_data: P2pData + dolby_qn: number[] | null + hot: boolean + hdr_metadata_degrade_desc: HdrMetadataDegradeDesc[] | null + } + + interface QnDesc { + qn: number + desc: string + hdr_desc: string + attr_desc: string[] | null + hdr_type: number + media_base_desc: MediaBaseDesc | null + eotf: number + } + + interface MediaBaseDesc { + detail_desc: { + desc: string + tag?: string[] + } + brief_desc: { + desc: string + badge?: string + } + } + + interface Stream { + protocol_name: string + format: Format[] + } + + interface Format { + format_name: string + codec: Codec[] + master_url: string + } + + interface Codec { + codec_name: string + current_qn: number + accept_qn: number[] + base_url: string + url_info: UrlInfo[] + hdr_qn: number[] | null + dolby_type: number + attr_name: string + hdr_type: number + drm: boolean + drm_key_systems: any + video_codecs: CodecDesc | null + audio_codecs: CodecDesc | null + session: string + is_pushing: boolean + stream_resource_req: any + video_color_info: VideoColorInfo | null + } + + interface CodecDesc { + base: string + ext?: string[] + } + + interface UrlInfo { + host: string + extra: string + stream_ttl: number + } + + interface VideoColorInfo { + colorspace?: string + eotf?: number + metadata_types: string[] + } + + interface P2pData { + p2p: boolean + p2p_type: number + m_p2p: boolean + m_servers: any[] | null + } + + interface HdrMetadataDegradeDesc { + type: string + display_badge: string + } + + interface ExpectedQuality { + qn: number + hdr_type: number + eotf: number + } + + interface SubtitleCfg { + status: number + subtitle_buffer: number + skip_frame_threshold: number + } + } } declare namespace LiveTraceData { diff --git a/src/library/bili-api/index.ts b/src/library/bili-api/index.ts index 93f16630..caf46912 100644 --- a/src/library/bili-api/index.ts +++ b/src/library/bili-api/index.ts @@ -122,6 +122,40 @@ const BAPI: BapiMethods = { visit_id, }) }, + getRoomPlayInfo: ( + room_id, + qn = 0, + protocol = '0,1', + format = '0,1,2', + codec = '0,1,2', + platform = 'web', + ptype = 8, + dolby = 5, + panorama = 1, + eotf = '0,1,2', + req_reason = 0, + supported_drms = '0,1,2,3', + web_location = '444.8', + ) => { + return request.live.get( + '/xlive/web-room/v2/index/getRoomPlayInfo', + wbiSign({ + room_id, + protocol, + format, + codec, + qn, + platform, + ptype, + dolby, + panorama, + eotf, + req_reason, + supported_drms, + web_location, + }), + ) + }, }, liveTrace: { E: (id, device, ruid, is_patch = 0, heart_beat = [], web_location = '444.8') => { diff --git a/src/library/bili-api/response.d.ts b/src/library/bili-api/response.d.ts index 24a5d349..f2f36598 100644 --- a/src/library/bili-api/response.d.ts +++ b/src/library/bili-api/response.d.ts @@ -47,6 +47,13 @@ declare namespace Live { data: { silver: number } message: string } + + interface GetRoomPlayInfo { + code: number + message: string + ttl: number + data: LiveData.GetRoomPlayInfo.Data + } } declare namespace LiveTrace { From 13d9fc8428e6d743cf2899196ab3c2ffd3cd555e Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:41:52 +0800 Subject: [PATCH 05/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=9B=B4=E6=92=AD?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=A4=A7=E9=87=8F=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 直播任务【点赞】、【发弹幕】和【观看直播】支持“完成目标轮次”和“跟随每日上限”两种模式 - 直播任务中的【点赞】任务支持等待开播后再点赞,【发弹幕】任务支持等待下播后再发弹幕 - 修复【观看任务】会错误减少一些已观看时长的bug(B站新版粉丝团任务的观看时长计算已精确到分钟) --- CHANGELOG.md | 6 + src/components/LiveTasks.vue | 90 +++- src/library/help-info/index.ts | 56 ++- src/library/storage/defaultValues.ts | 8 + .../liveTasks/medalTasks/MedalModule.ts | 415 +++++++++++++++++- .../liveTasks/medalTasks/danmuTask.ts | 215 ++++++--- .../liveTasks/medalTasks/lightTask.ts | 43 +- .../liveTasks/medalTasks/likeTask.ts | 188 +++++--- .../liveTasks/medalTasks/types.d.ts | 29 +- .../liveTasks/medalTasks/watchTask.ts | 17 +- src/stores/useBiliStore.ts | 5 + src/stores/useModuleStore.ts | 4 +- src/types/helpInfo.d.ts | 3 + src/types/storage.d.ts | 8 + 14 files changed, 898 insertions(+), 189 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c23cc48..5a4a1164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ ### 新增 - 执行完任意粉丝勋章的粉丝团任务后,检查是否存在储蓄的亲密度以及是否达到储蓄上限,把结果输出到控制台日志 +- 直播任务【点赞】、【发弹幕】和【观看直播】支持“完成目标轮次”和“跟随每日上限”两种模式 +- 直播任务中的【点赞】任务支持等待开播后再点赞,【发弹幕】任务支持等待下播后再发弹幕 + +### 修复 + +- 修复【观看任务】会错误减少一些已观看时长的bug(B站新版粉丝团任务的观看时长计算已精确到分钟) ### 调整 diff --git a/src/components/LiveTasks.vue b/src/components/LiveTasks.vue index 4c924bce..c3adbf89 100644 --- a/src/components/LiveTasks.vue +++ b/src/components/LiveTasks.vue @@ -235,6 +235,37 @@ function handleRowClick(row: MedalInfoRow) { + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/help-info/index.ts b/src/library/help-info/index.ts index e90877b0..5e153531 100644 --- a/src/library/help-info/index.ts +++ b/src/library/help-info/index.ts @@ -123,17 +123,42 @@ const helpInfo: HelpInfo = { ]), ]), }, + rounds: { + title: '跟随每日上限 / 完成目标轮次', + message: h('p', [ + h( + 'div', + '一轮指通过完成任务获得 +1 亲密度所需的最小操作量。具体值脚本会在运行过程中实时获取。根据观察目前每轮的操作量如下:', + ), + h('ul', [ + h('li', '点赞:30 次'), + h('li', '发弹幕:1 条'), + h('li', '观看直播:15 分钟'), + ]), + h( + 'div', + '选择【跟随每日上限】时,脚本会尽量做到任务的当日上限(任务上限也是实时获取的)。', + ), + h( + 'div', + '选择【完成目标轮次】可自定义脚本当天至多完成几轮;若填写值超过任务上限,则还是做到任务的当日上限。', + ), + ]), + }, like: { title: '点赞', message: h('p', [ h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的点赞任务。'), - h('ul', [ - h('li', '只有主播开播时点赞才会计入任务进度,因此只会给开播中的直播间点赞。'), - h( - 'li', - '脚本会根据 B 站返回的任务要求自动决定点赞次数。高等级的粉丝勋章拥有更高的任务上限,因此能获得更多的亲密度。', - ), - ]), + h('div', '只有主播开播时点赞才会计入任务进度。'), + ]), + }, + likeWaitUntilLiving: { + title: '等待开播后再点赞', + message: h('p', [ + h( + 'div', + '开启该功能后,对于尚未开播的直播间,脚本会等主播开播后再点赞。关闭时会直接跳过。', + ), ]), }, danmu: { @@ -141,22 +166,22 @@ const helpInfo: HelpInfo = { message: h('p', [ h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的发弹幕任务。'), h('ul', [ - h( - 'li', - '脚本会根据 B 站返回的任务要求自动决定发送条数。高等级的粉丝勋章拥有更高的任务上限,因此能获得更多的亲密度。', - ), h( 'li', '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。', ), - h( - 'li', - '若担心直播中刷屏被禁言,可开启【仅在未开播的直播间发弹幕】,此时会跳过当前正在直播的直播间。', - ), h('li', '弹幕发送失败(比如被屏蔽词拦截)会自动小幅补发。'), ]), ]), }, + danmuWaitUntilNotLiving: { + title: '等待下播后再发弹幕', + message: h('p', [ + h('div', '此选项只有在开启【仅在未开播的直播间发弹幕】后才会生效。'), + h('div', '只开启【仅在未开播的直播间发弹幕】时,脚本会直接跳过当前开播中的直播间。'), + h('div', '此时再开启【等待下播后再发弹幕】,脚本会等直播中的主播下播后再发弹幕。'), + ]), + }, watch: { title: '观看直播', message: h('p', [ @@ -164,7 +189,6 @@ const helpInfo: HelpInfo = { h('ul', [ h('li', '部分直播间因为没有设置直播分区导致任务无法完成。'), h('li', '主播当前是否开播不会影响该任务的完成。'), - h('li', '脚本会根据 B 站返回的任务要求自动计算观看时长。'), ]), h('div', [ h('strong', '注意:'), diff --git a/src/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts index 8b265e4f..f52d89ea 100644 --- a/src/library/storage/defaultValues.ts +++ b/src/library/storage/defaultValues.ts @@ -68,6 +68,9 @@ const defaultValues: DefaultValues = { }, like: { enabled: false, + dailyLimitOrTargetRounds: false, + targetRounds: 1, + waitUntilLiving: false, isWhiteList: false, roomidList: [], _lastCompleteTime: 0, @@ -91,13 +94,18 @@ const defaultValues: DefaultValues = { '←◡←', `(●'◡'●)ノ♥`, ], + dailyLimitOrTargetRounds: false, + targetRounds: 1, onlyWhenNotLiving: false, + waitUntilNotLiving: false, isWhiteList: false, roomidList: [], _lastCompleteTime: 0, }, watch: { enabled: false, + dailyLimitOrTargetRounds: false, + targetRounds: 1, isWhiteList: false, roomidList: [], _watchingProgress: {}, diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 6023cbae..2fba0449 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -1,32 +1,43 @@ import BaseModule from '@/modules/BaseModule' import { useBiliStore, useModuleStore } from '@/stores' import { watch } from 'vue' -import type { MedalTaskSharedConfig, SharedMedalFilters } from './types' +import type { + MedalTaskSharedConfig, + SharedMedalFilters, + TaskJumpType, + WaitProbeResult, + WaitProbeSnapshot, + WaitStrategy, +} from './types' import { arrayToMap, sleep } from '@/library/utils' import type { LiveData } from '@/library/bili-api/data' import BAPI from '@/library/bili-api' -import { isTimestampToday } from '@/library/luxon' +import { isNowAfter, isNowBefore, isTimestampToday } from '@/library/luxon' import _ from 'lodash' -type JumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' - class MedalModule extends BaseModule { /** 最大弹幕补发次数 */ protected static readonly DANMU_RETRY_LIMIT = 3 - - /** - * 简单限流队列 - * - * 串行执行获取任务信息请求 - */ - private static taskInfoRequestQueue: Promise = Promise.resolve() - - medalTasksConfig = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks - - /** 所有粉丝勋章任务的公共配置 */ - declare protected config: MedalTaskSharedConfig - - protected SHARED_MEDAL_FILTERS: SharedMedalFilters = { + /** 等待开播/下播时单房间探测的间隔 */ + protected static readonly WAIT_POLL_INTERVAL = 120_000 + /** 共享等待探测快照的短期有效时间 */ + private static readonly WAIT_PROBE_SNAPSHOT_TTL = 60_000 + /** 单房间探测时相邻请求间的随机延迟 */ + private static get ROOM_STATUS_PROBE_DYNAMIC_DELAY() { + return _.random(600, 1000) + } + /** 等待B站更新粉丝勋章数据的延迟 */ + protected static readonly WAIT_MEDAL_UPDATE_DELAY = 3000 + /** 发弹幕动态间隔时间 */ + protected static get SEND_DANMU_DYNAMIC_INTERVAL() { + return _.random(6000, 8000) + } + /** 点赞动态间隔时间 */ + protected static get LIKE_DYNAMIC_INTERVAL() { + return _.random(15000, 20000) + } + /** 共享的粉丝勋章过滤器 */ + protected readonly SHARED_MEDAL_FILTERS: SharedMedalFilters = { // 包含在白名单中或不包含在黑名单中返回true,否则返回false meetWhiteOrBlackList: (m) => this.config.isWhiteList @@ -39,6 +50,52 @@ class MedalModule extends BaseModule { // 直播中返回true,否则返回false isLiving: (medal) => medal.room_info.living_status === 1, } + /** 直播间直播状态获取函数列表(获取失败时返回null) */ + private readonly ROOM_LIVE_STATUS_FETCHERS: Array<(roomid: number) => Promise> = [ + 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 + } + this.logger.warn(`BAPI.live.getInfoByRoom(${roomid}) 失败`, response.message) + } catch (error) { + this.logger.warn(`BAPI.live.getInfoByRoom(${roomid}) 出错`, error) + } + + return null + }, + async (roomid) => { + try { + 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 + } + this.logger.warn(`BAPI.live.getRoomPlayInfo(${roomid}) 失败`, response.message) + } catch (error) { + this.logger.warn(`BAPI.live.getRoomPlayInfo(${roomid}) 出错`, error) + } + + return null + }, + ] + /** + * 简单限流队列 + * + * 串行执行获取任务信息请求 + */ + private static taskInfoRequestQueue: Promise = Promise.resolve() + /** 当前进行中的等待探测 */ + private static sharedWaitProbePromise: Promise | null = null + /** 最近一次等待探测快照 */ + private static sharedWaitProbeSnapshot: WaitProbeSnapshot | null = null + + medalTasksConfig = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks + + /** 所有粉丝勋章任务的公共配置 */ + declare protected config: MedalTaskSharedConfig /** * 对粉丝勋章列表进行排序 @@ -50,6 +107,13 @@ class MedalModule extends BaseModule { medals.sort((a, b) => orderMap.get(a.room_info.room_id)! - orderMap.get(b.room_info.room_id)!) } + /** + * 是否需要因跨天而提前停止 + */ + protected shouldStopForCrossDay(): boolean { + return isNowAfter(23, 55) || isNowBefore(0, 5) + } + /** * 等待粉丝勋章数据获取完毕 * @@ -93,11 +157,11 @@ class MedalModule extends BaseModule { } /** - * 按 jump_type 在 task_info 中查找任务项 + * 按 jump_type 在粉丝勋章任务信息 task_info 中查找任务项 */ protected static findTaskInfo( task_info: LiveData.GetActivatedMedalInfo.TaskInfo[] | null, - jump_type: JumpType, + jump_type: TaskJumpType, ): LiveData.GetActivatedMedalInfo.TaskInfo | undefined { return task_info?.find((t) => t.jump_type === jump_type) } @@ -133,6 +197,312 @@ class MedalModule extends BaseModule { return task } + /** + * 刷新粉丝勋章列表 + * + * @returns 是否刷新成功 + */ + protected async refreshFansMedals(): Promise { + const moduleStore = useModuleStore() + try { + await moduleStore.rerunModule('Default_FansMedals', true) + return useBiliStore().fansMedalsStatus === 'loaded' + } catch (error) { + this.logger.error('刷新粉丝勋章列表失败', error) + return false + } + } + + /** + * 以随机顺序尝试多个接口,获取单个直播间的直播状态 + * + * @returns + * - `1`:直播中 + * - `0`/`2`:未开播/轮播中 + * - `null`:两个接口都获取失败 + */ + protected async fetchRoomLiveStatus(roomid: number): Promise { + const fetchers = _.shuffle(this.ROOM_LIVE_STATUS_FETCHERS) + + for (const fetchStatus of fetchers) { + const liveStatus = await fetchStatus(roomid) + if (liveStatus !== null) { + return liveStatus + } + await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + } + + return null + } + + /** + * 计算等待探测策略 + */ + protected decideWaitStrategy(pendingCount: number): WaitStrategy { + const totalMedalCount = useBiliStore().filteredFansMedals.length + const fullRefreshCost = Math.ceil(totalMedalCount / 10) + return pendingCount <= fullRefreshCost ? 'single-probe' : 'refresh-fans-medals' + } + + /** + * 判断快照是否仍可复用 + */ + private static canReuseWaitProbeSnapshot( + snapshot: WaitProbeSnapshot, + roomids: number[], + skipTTL = false, + ): boolean { + const uncovered = MedalModule.getUncoveredRoomidsFromSnapshot(snapshot, roomids, skipTTL) + return uncovered !== null && uncovered.length === 0 + } + + /** + * 获取快照中尚未覆盖的房间列表 + * + * 仅成功快照才允许参与部分复用 + */ + private static getUncoveredRoomidsFromSnapshot( + snapshot: WaitProbeSnapshot, + roomids: number[], + skipTTL = false, + ): number[] | null { + if (!snapshot.isSuccessful) { + return null + } + if (!skipTTL && Date.now() - snapshot.createdAt > MedalModule.WAIT_PROBE_SNAPSHOT_TTL) { + return null + } + + const coveredRoomids = new Set(snapshot.roomids) + return roomids.filter((roomid) => !coveredRoomids.has(roomid)) + } + + /** + * 合并多份等待探测快照 + * + * 后面的快照会覆盖前面的同房间数据 + */ + private static mergeWaitProbeSnapshots(...snapshots: WaitProbeSnapshot[]): WaitProbeSnapshot { + const roomids = [...new Set(snapshots.flatMap((snapshot) => snapshot.roomids))] + const createdAt = Math.min(...snapshots.map((snapshot) => snapshot.createdAt)) + const strategy = snapshots.at(-1)!.strategy + const isSuccessful = snapshots.some((snapshot) => snapshot.isSuccessful) + const liveStatusMap = new Map() + const medalMap = new Map() + + snapshots.forEach((snapshot) => { + snapshot.liveStatusMap.forEach((liveStatus, roomid) => { + liveStatusMap.set(roomid, liveStatus) + }) + snapshot.medalMap.forEach((medal, roomid) => { + medalMap.set(roomid, medal) + }) + }) + + return { + createdAt, + roomids, + strategy, + isSuccessful, + liveStatusMap, + medalMap, + } + } + + /** + * 生成一轮新的等待探测快照 + */ + private async createWaitProbeSnapshot(roomids: number[]): Promise { + const strategy = this.decideWaitStrategy(roomids.length) + const medalMap = new Map() + const liveStatusMap = new Map() + + if (strategy === 'refresh-fans-medals') { + if (!(await this.refreshFansMedals())) { + return { + createdAt: Date.now(), + roomids, + strategy, + isSuccessful: false, + liveStatusMap, + medalMap, + } + } + + const latestMedalMap = useBiliStore().filteredFansMedalsMap + roomids.forEach((roomid) => { + const medal = latestMedalMap.get(roomid) + if (medal) { + medalMap.set(roomid, medal) + liveStatusMap.set(roomid, medal.room_info.living_status) + } + }) + + return { + createdAt: Date.now(), + roomids, + strategy, + isSuccessful: true, + liveStatusMap, + medalMap, + } + } + + const currentMedalMap = useBiliStore().filteredFansMedalsMap + + for (let i = 0; i < roomids.length; i++) { + const roomid = roomids[i] + const medal = currentMedalMap.get(roomid) + + if (medal) { + medalMap.set(roomid, medal) + liveStatusMap.set(roomid, await this.fetchRoomLiveStatus(roomid)) + } + + if (i < roomids.length - 1) { + await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + } + } + + return { + createdAt: Date.now(), + roomids, + strategy, + isSuccessful: true, + liveStatusMap, + medalMap, + } + } + + /** + * 获取可在多个任务模块之间复用的一轮等待探测快照 + */ + private async getSharedWaitProbeSnapshot(roomids: number[]): Promise { + const reusableSnapshots: WaitProbeSnapshot[] = [] + const cachedSnapshot = MedalModule.sharedWaitProbeSnapshot + if (cachedSnapshot && MedalModule.canReuseWaitProbeSnapshot(cachedSnapshot, roomids)) { + // 旧快照可以完全复用 + return cachedSnapshot + } + if ( + cachedSnapshot && + MedalModule.getUncoveredRoomidsFromSnapshot(cachedSnapshot, roomids) !== null + ) { + reusableSnapshots.push(cachedSnapshot) + } + + if (MedalModule.sharedWaitProbePromise) { + const inflightSnapshot = await MedalModule.sharedWaitProbePromise + if (MedalModule.canReuseWaitProbeSnapshot(inflightSnapshot, roomids, true)) { + return inflightSnapshot + } + if ( + inflightSnapshot !== cachedSnapshot && + MedalModule.getUncoveredRoomidsFromSnapshot(inflightSnapshot, roomids, true) !== null + ) { + reusableSnapshots.push(inflightSnapshot) + } + } + + if ( + this.decideWaitStrategy(roomids.length) === 'single-probe' && + reusableSnapshots.length > 0 + ) { + const mergedReusableSnapshot = MedalModule.mergeWaitProbeSnapshots(...reusableSnapshots) + const uncoveredRoomids = MedalModule.getUncoveredRoomidsFromSnapshot( + mergedReusableSnapshot, + roomids, + true, + ) + + if (uncoveredRoomids && uncoveredRoomids.length === 0) { + MedalModule.sharedWaitProbeSnapshot = mergedReusableSnapshot + return mergedReusableSnapshot + } + + if (uncoveredRoomids && uncoveredRoomids.length > 0) { + const probePromise = this.createWaitProbeSnapshot(uncoveredRoomids).then((patchSnapshot) => + MedalModule.mergeWaitProbeSnapshots(mergedReusableSnapshot, patchSnapshot), + ) + MedalModule.sharedWaitProbePromise = probePromise + + try { + const mergedSnapshot = await probePromise + MedalModule.sharedWaitProbeSnapshot = mergedSnapshot + return mergedSnapshot + } finally { + if (MedalModule.sharedWaitProbePromise === probePromise) { + MedalModule.sharedWaitProbePromise = null + } + } + } + } + + const probePromise = this.createWaitProbeSnapshot(roomids) + MedalModule.sharedWaitProbePromise = probePromise + + try { + const snapshot = await probePromise + MedalModule.sharedWaitProbeSnapshot = snapshot + return snapshot + } finally { + if (MedalModule.sharedWaitProbePromise === probePromise) { + MedalModule.sharedWaitProbePromise = null + } + } + } + + /** + * 对等待中的直播间做一轮探测 + * + * @param roomids 待探测的直播间ID列表 + * @param targetPredicate 直播状态判断函数,返回“是否符合目标状态“ + * + * @returns 探测结果,包含符合目标状态的粉丝勋章列表和待处理的直播间ID列表 + */ + protected async probeWaitingMedals( + roomids: number[], + targetPredicate: (liveStatus?: number | null) => boolean, + ): Promise { + const snapshot = await this.getSharedWaitProbeSnapshot(roomids) + this.logger.log( + `待处理房间 ${roomids.length} 个,本轮使用${snapshot.strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, + ) + + const readyMedals: LiveData.FansMedalPanel.List[] = [] + const pendingRoomids: number[] = [] + + if (!snapshot.isSuccessful) { + return { readyMedals, pendingRoomids: roomids } + } + + for (const roomid of roomids) { + const medal = snapshot.medalMap.get(roomid) + + if (!medal) { + this.logger.warn(`直播间 ${roomid} 未在粉丝勋章列表中找到,停止等待该房间`) + continue + } + if (!this.SHARED_MEDAL_FILTERS.isLighted(medal)) { + this.logger.log(`直播间 ${roomid} 对应的粉丝勋章已熄灭,停止等待该房间`) + continue + } + + const liveStatus = snapshot.liveStatusMap.get(roomid) + + if (targetPredicate(liveStatus)) { + readyMedals.push(medal) + } else { + pendingRoomids.push(roomid) + } + } + + return { + readyMedals, + pendingRoomids, + } + } + /** * 获取指定粉丝勋章的信息 * @@ -192,7 +562,7 @@ class MedalModule extends BaseModule { return new Promise((resolve) => { const unwatch = watch( () => moduleStore.moduleStatus.DailyTasks.LiveTasks.medalTasks.light, - (newStatus) => { + async (newStatus) => { if (newStatus === 'done' || newStatus === 'error') { unwatch() @@ -200,7 +570,8 @@ class MedalModule extends BaseModule { // 如果点亮熄灭勋章模块确实进行了点亮操作 // 重新获取粉丝勋章(主要是为了获取最新的点亮状态和直播状态) // FansMedals 模块内部做了防重入,因此无需担心会重复获取 - moduleStore.rerunModule('Default_FansMedals') + await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) + moduleStore.rerunModule('Default_FansMedals', true) } resolve() } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index e3529156..5b768f8f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -1,11 +1,12 @@ -import { delayToNextMoment, isNowAfter, isNowBefore, isTimestampToday, tsm } from '@/library/luxon' +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' import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' +import { sleep } from '@/library/utils' +import type { GroupedMedals } from './types' class DanmuTask extends MedalModule { config = this.medalTasksConfig.danmu @@ -15,20 +16,34 @@ class DanmuTask extends MedalModule { } /** - * 获取已点亮的粉丝勋章;开启 `onlyWhenNotLiving` 时排除开播中的 + * 获取已点亮的粉丝勋章,并按是否可立即发弹幕分组 */ - private getMedals(): LiveData.FansMedalPanel.List[] { + private getMedals(): GroupedMedals<'readyMedals' | 'waitingMedals'> { const fansMedals = useBiliStore().filteredFansMedals - const result = fansMedals.filter( - (medal) => + const result: GroupedMedals<'readyMedals' | 'waitingMedals'> = { + readyMedals: [], + waitingMedals: [], + } + + fansMedals.forEach((medal) => { + if ( this.SHARED_MEDAL_FILTERS.meetWhiteOrBlackList(medal) && this.SHARED_MEDAL_FILTERS.levelLt120(medal) && - this.SHARED_MEDAL_FILTERS.isLighted(medal) && - (!this.config.onlyWhenNotLiving || !this.SHARED_MEDAL_FILTERS.isLiving(medal)), - ) + this.SHARED_MEDAL_FILTERS.isLighted(medal) + ) { + if (this.config.onlyWhenNotLiving && this.SHARED_MEDAL_FILTERS.isLiving(medal)) { + // 如果开启了“仅在未开播的直播间发弹幕”且当前直播间正在直播,需要等到下播后再发弹幕 + result.waitingMedals.push(medal) + } else if (this.config.waitUntilNotLiving) { + // 否则可立即发弹幕 + result.readyMedals.push(medal) + } + } + }) if (this.config.isWhiteList) { - this.sortMedals(result) + this.sortMedals(result.readyMedals) + this.sortMedals(result.waitingMedals) } return result @@ -69,6 +84,100 @@ class DanmuTask extends MedalModule { return false } + /** + * 执行单个直播间的发弹幕任务 + * + * @returns 是否中断任务 + */ + private async executeDanmuTask( + medal: LiveData.FansMedalPanel.List, + danmuIndexRef: { value: number }, + ): Promise { + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') + return true + } + + const medalData = await this.fetchMedalData(medal.medal.target_id) + if (!medalData) { + this.logger.error( + `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`, + ) + return false + } + + const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') + if (!item) { + this.logger.error( + `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, + ) + return false + } + + if (item.is_done) return false + + const parsed = MedalModule.parseDailyLimit(item.sub_title) + if (!parsed) { + this.logger.error( + `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, + ) + return false + } + + if (parsed.current >= parsed.limit) return false + + const target = this.config.dailyLimitOrTargetRounds + ? Math.min(parsed.limit, this.config.targetRounds) + : parsed.limit + let remaining = target - parsed.current + let failedCount = 0 + + for (let j = 0; j < remaining; j++) { + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') + return true + } + + const danmuText = this.config.danmuList[danmuIndexRef.value++ % this.config.danmuList.length] + if (!(await this.sendDanmu(medal, danmuText))) { + if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { + this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) + break + } + remaining += 1 + } + + if (j < remaining - 1) { + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) + } + } + + sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + return false + } + + /** + * 顺序执行多个直播间的发弹幕任务 + * + * @returns 是否中断任务 + */ + private async executeDanmuTasks( + medals: LiveData.FansMedalPanel.List[], + danmuIndexRef: { value: number }, + ): Promise { + for (let i = 0; i < medals.length; i++) { + const isInterrupted = await this.executeDanmuTask(medals[i], danmuIndexRef) + if (isInterrupted) return true + + if (i < medals.length - 1) { + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) + } + } + + return false + } + public async run(): Promise { this.logger.log('发弹幕模块开始运行') @@ -82,72 +191,52 @@ class DanmuTask extends MedalModule { } this.status = 'running' - const fansMedals = this.getMedals() + const { readyMedals, waitingMedals } = this.getMedals() let allCompleted = true - let danmuIndex = 0 + const danmuIndexRef = { value: 0 } + let pendingRoomids = + this.config.onlyWhenNotLiving && this.config.waitUntilNotLiving + ? waitingMedals.map((medal) => medal.room_info.room_id) + : [] - outer: for (let i = 0; i < fansMedals.length; i++) { - if (isNowAfter(23, 55) || isNowBefore(0, 5)) { - this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') + const isInitialInterrupted = await this.executeDanmuTasks(readyMedals, danmuIndexRef) + if (isInitialInterrupted) { + allCompleted = false + } + + while (allCompleted && pendingRoomids.length > 0) { + if (!this.config.onlyWhenNotLiving || !this.config.waitUntilNotLiving) { allCompleted = false + this.logger.log('等待下播后再发弹幕已关闭,结束本轮等待') break } - - const medal = fansMedals[i] - const medalData = await this.fetchMedalData(medal.medal.target_id) - if (!medalData) { - this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`, - ) - continue + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') + allCompleted = false + break } - const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') - if (!item) { - this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, - ) - continue - } + const { readyMedals: newlyReadyMedals, pendingRoomids: nextPendingRoomids } = + await this.probeWaitingMedals(pendingRoomids, (liveStatus) => liveStatus !== 1) - if (item.is_done) continue + pendingRoomids = nextPendingRoomids - const parsed = MedalModule.parseDailyLimit(item.sub_title) - if (!parsed) { - this.logger.error( - `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, - ) - continue + const isWaitInterrupted = await this.executeDanmuTasks(newlyReadyMedals, danmuIndexRef) + if (isWaitInterrupted) { + allCompleted = false + break } - if (parsed.current >= parsed.limit) continue - - let target = parsed.limit - parsed.current - let failedCount = 0 - - for (let j = 0; j < target; j++) { - if (isNowAfter(23, 55) || isNowBefore(0, 5)) { - this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') - allCompleted = false - break outer - } - - const danmuText = this.config.danmuList[danmuIndex++ % this.config.danmuList.length] - if (!(await this.sendDanmu(medal, danmuText))) { - if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { - this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) - await sleep(_.random(6000, 8000)) - break - } - target++ - } - - if (j < target - 1 || i < fansMedals.length - 1) { - await sleep(_.random(6000, 8000)) - } + if (pendingRoomids.length > 0) { + this.logger.log( + `仍有 ${pendingRoomids.length} 个直播间未下播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + ) + await sleep(MedalModule.WAIT_POLL_INTERVAL) } + } - await this.logFreeIntimacy(medal) + if (pendingRoomids.length > 0) { + allCompleted = false } if (allCompleted) { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index 4646cca8..29bea431 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -6,8 +6,7 @@ import type { ModuleStatusTypes } from '@/types' import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' - -type MedalsByLiving = [LiveData.FansMedalPanel.List[], LiveData.FansMedalPanel.List[]] +import type { GroupedMedals } from './types' class LightTask extends MedalModule { config = this.medalTasksConfig.light @@ -17,13 +16,14 @@ class LightTask extends MedalModule { } /** - * 获取未点亮的粉丝勋章 - * - * @returns [[主播未开播的粉丝勋章], [主播开播中的粉丝勋章]] + * 获取未点亮的粉丝勋章,并按是否开播分组 */ - private getMedals(): MedalsByLiving { + private getMedals(): GroupedMedals<'notLivingMedals' | 'livingMedals'> { const fansMedals = useBiliStore().filteredFansMedals - const result: MedalsByLiving = [[], []] + const result: GroupedMedals<'notLivingMedals' | 'livingMedals'> = { + notLivingMedals: [], + livingMedals: [], + } fansMedals.forEach((medal) => { if ( @@ -34,15 +34,18 @@ class LightTask extends MedalModule { return } - // 根据直播状态划分 - const index = this.SHARED_MEDAL_FILTERS.isLiving(medal) ? 1 : 0 - result[index].push(medal) + // 根据直播状态分组 + if (this.SHARED_MEDAL_FILTERS.isLiving(medal)) { + result.livingMedals.push(medal) + } else { + result.notLivingMedals.push(medal) + } }) if (this.config.isWhiteList) { // 白名单排序 - this.sortMedals(result[0]) - this.sortMedals(result[1]) + this.sortMedals(result.livingMedals) + this.sortMedals(result.notLivingMedals) } return result @@ -125,11 +128,11 @@ class LightTask extends MedalModule { } const item = MedalModule.findTaskInfo(medalData.task_info, 'like') - const baseCount = MedalModule.parseTitleCount(item?.title) ?? 30 - await this.like(medal, _.random(baseCount, baseCount + 3)) + const times = MedalModule.parseTitleCount(item?.title) ?? 30 + await this.like(medal, times) if (i < medals.length - 1) { - await sleep(_.random(15000, 20000)) + await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) } } } @@ -161,14 +164,14 @@ class LightTask extends MedalModule { if (!(await this.sendDanmu(medal, danmuText))) { if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) - await sleep(_.random(6000, 8000)) + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) break } target++ } if (i < medals.length - 1 || j < target - 1) { - await sleep(_.random(6000, 8000)) + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) } } } @@ -185,11 +188,11 @@ class LightTask extends MedalModule { } this.status = 'running' - const fansMedals = this.getMedals() + const { notLivingMedals, livingMedals } = this.getMedals() // 是否有需要点亮的粉丝勋章(是不是一次有效运行?) - const isEffectiveRun = fansMedals.some((medals) => medals.length > 0) + const isEffectiveRun = notLivingMedals.length > 0 || livingMedals.length > 0 if (isEffectiveRun) { - await Promise.allSettled([this.sendDanmuTask(fansMedals[0]), this.likeTask(fansMedals[1])]) + await Promise.allSettled([this.sendDanmuTask(notLivingMedals), this.likeTask(livingMedals)]) } this.config._lastCompleteTime = tsm() diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index e81fb835..aacf7cfe 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -1,11 +1,12 @@ -import { delayToNextMoment, isNowAfter, isNowBefore, isTimestampToday, tsm } from '@/library/luxon' +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' import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' +import { sleep } from '@/library/utils' +import type { GroupedMedals } from './types' class LikeTask extends MedalModule { config = this.medalTasksConfig.like @@ -15,20 +16,34 @@ class LikeTask extends MedalModule { } /** - * 获取已点亮且主播开播中的粉丝勋章 + * 获取已点亮的粉丝勋章,并按是否可立即点赞分组 */ - private getMedals(): LiveData.FansMedalPanel.List[] { + private getMedals(): GroupedMedals<'readyMedals' | 'waitingMedals'> { const fansMedals = useBiliStore().filteredFansMedals - const result = fansMedals.filter( - (medal) => + const result: GroupedMedals<'readyMedals' | 'waitingMedals'> = { + readyMedals: [], + waitingMedals: [], + } + + fansMedals.forEach((medal) => { + if ( this.SHARED_MEDAL_FILTERS.meetWhiteOrBlackList(medal) && this.SHARED_MEDAL_FILTERS.levelLt120(medal) && - this.SHARED_MEDAL_FILTERS.isLighted(medal) && - this.SHARED_MEDAL_FILTERS.isLiving(medal), - ) + this.SHARED_MEDAL_FILTERS.isLighted(medal) + ) { + if (this.SHARED_MEDAL_FILTERS.isLiving(medal)) { + // 如果当前直播间正在直播,可立即点赞 + result.readyMedals.push(medal) + } else if (this.config.waitUntilLiving) { + // 否则需要等到直播间开播后再点赞 + result.waitingMedals.push(medal) + } + } + }) if (this.config.isWhiteList) { - this.sortMedals(result) + this.sortMedals(result.readyMedals) + this.sortMedals(result.waitingMedals) } return result @@ -57,6 +72,85 @@ class LikeTask extends MedalModule { } } + /** + * 执行单个直播间的点赞任务 + * + * @returns 是否中断任务 + */ + private async executeLikeTask(medal: LiveData.FansMedalPanel.List): Promise { + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') + return true + } + + const medalData = await this.fetchMedalData(medal.medal.target_id) + if (!medalData) { + this.logger.error( + `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`, + ) + return false + } + + const item = MedalModule.findTaskInfo(medalData.task_info, 'like') + if (!item) { + this.logger.error( + `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, + ) + return false + } + + if (item.is_done) return false + + const parsed = MedalModule.parseDailyLimit(item.sub_title) + if (!parsed) { + this.logger.error( + `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的点赞任务的每日上限信息,跳过点赞任务`, + ) + return false + } + + if (parsed.current >= parsed.limit) return false + + const times = MedalModule.parseTitleCount(item.title) ?? 30 + const target = this.config.dailyLimitOrTargetRounds + ? Math.min(parsed.limit, this.config.targetRounds) + : parsed.limit + const remaining = target - parsed.current + for (let j = 0; j < remaining; j++) { + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') + return true + } + + await this.like(medal, times) + + if (j < remaining - 1) { + await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) + } + } + + sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + return false + } + + /** + * 顺序执行多个直播间的点赞任务 + * + * @returns 是否中断任务 + */ + private async executeLikeTasks(medals: LiveData.FansMedalPanel.List[]): Promise { + for (let i = 0; i < medals.length; i++) { + const isInterrupted = await this.executeLikeTask(medals[i]) + if (isInterrupted) return true + + if (i < medals.length - 1) { + await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) + } + } + + return false + } + public async run(): Promise { this.logger.log('点赞模块开始运行') @@ -70,64 +164,48 @@ class LikeTask extends MedalModule { } this.status = 'running' - const fansMedals = this.getMedals() + const { readyMedals, waitingMedals } = this.getMedals() + let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) let allCompleted = true - outer: for (let i = 0; i < fansMedals.length; i++) { - if (isNowAfter(23, 55) || isNowBefore(0, 5)) { - this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') + const isInitialInterrupted = await this.executeLikeTasks(readyMedals) + if (isInitialInterrupted) { + allCompleted = false + } + + while (allCompleted && pendingRoomids.length > 0) { + if (!this.config.waitUntilLiving) { allCompleted = false + this.logger.log('等待开播后再点赞已关闭,结束本轮等待') break } - - const medal = fansMedals[i] - const medalData = await this.fetchMedalData(medal.medal.target_id) - if (!medalData) { - this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`, - ) - continue + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') + allCompleted = false + break } - const item = MedalModule.findTaskInfo(medalData.task_info, 'like') - if (!item) { - this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, - ) - continue - } + const { readyMedals: newlyReadyMedals, pendingRoomids: nextPendingRoomids } = + await this.probeWaitingMedals(pendingRoomids, (liveStatus) => liveStatus === 1) - if (item.is_done) continue + pendingRoomids = nextPendingRoomids - const parsed = MedalModule.parseDailyLimit(item.sub_title) - if (!parsed) { - this.logger.error( - `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的点赞任务的每日上限信息,跳过点赞任务`, - ) - continue + const isWaitInterrupted = await this.executeLikeTasks(newlyReadyMedals) + if (isWaitInterrupted) { + allCompleted = false + break } - if (parsed.current >= parsed.limit) continue - - // 每轮点赞次数 - const times = MedalModule.parseTitleCount(item.title) ?? 30 - // 剩余点赞轮数 - const remaining = parsed.limit - parsed.current - for (let j = 0; j < remaining; j++) { - if (isNowAfter(23, 55) || isNowBefore(0, 5)) { - this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') - allCompleted = false - break outer - } - - await this.like(medal, _.random(times, times + 3)) - - if (j < remaining - 1 || i < fansMedals.length - 1) { - await sleep(_.random(15000, 20000)) - } + if (pendingRoomids.length > 0) { + this.logger.log( + `仍有 ${pendingRoomids.length} 个直播间未开播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + ) + await sleep(MedalModule.WAIT_POLL_INTERVAL) } + } - await this.logFreeIntimacy(medal) + if (pendingRoomids.length > 0) { + allCompleted = false } if (allCompleted) { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 95b3d92f..6d1bb0dc 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -16,4 +16,31 @@ interface SharedMedalFilters { isLiving: (medal: LiveData.FansMedalPanel.List) => boolean } -export { MedalTaskSharedConfig, SharedMedalFilters } +type GroupedMedals = Record + +type TaskJumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' +type WaitStrategy = 'single-probe' | 'refresh-fans-medals' + +interface WaitProbeSnapshot { + createdAt: number + roomids: number[] + strategy: WaitStrategy + isSuccessful: boolean + liveStatusMap: Map + medalMap: Map +} + +interface WaitProbeResult { + readyMedals: LiveData.FansMedalPanel.List[] + pendingRoomids: number[] +} + +export { + MedalTaskSharedConfig, + SharedMedalFilters, + GroupedMedals, + TaskJumpType, + WaitStrategy, + WaitProbeSnapshot, + WaitProbeResult, +} diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index d4eafcbc..8b6a893f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -323,11 +323,6 @@ class WatchTask extends MedalModule { if (!isTimestampToday(this.config._lastWatchTime, 0, 0)) { // 如果上次观看(不是完成任务)的时间戳不在今天,将今天已观看的秒数置为0 this.config._watchingProgress = {} - } else { - // 因为连续看 5 分钟(300秒)才能加亲密度,所以上一次观看时最后不足 5 分钟的时间是无效的 - _.forOwn(this.config._watchingProgress, (value, key, object) => { - object[key] -= value % 300 - }) } this.config._lastWatchTime = tsm() @@ -373,10 +368,14 @@ class WatchTask extends MedalModule { } if (parsed.current >= parsed.limit) continue + // 一轮的观看时间(默认 15 分钟) const minutes = MedalModule.parseTitleCount(item.title) ?? 15 - - // 目标秒数 = 每日上限 × minutes × 60s + 60s 缓冲(补偿心跳计时误差) - const targetSeconds = parsed.limit * minutes * 60 + 60 + // 目标轮次(每日上限或配置的目标轮次) + const target = this.config.dailyLimitOrTargetRounds + ? Math.min(parsed.limit, this.config.targetRounds) + : parsed.limit + // 目标秒数 = 目标轮次 × minutes × 60s + const targetSeconds = target * minutes * 60 if ((this.config._watchingProgress[roomid] ?? 0) >= targetSeconds) { // 今日观看时间已达到目标值,跳过 @@ -400,7 +399,7 @@ class WatchTask extends MedalModule { targetSeconds, ).start() - await this.logFreeIntimacy(medal) + sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) } } diff --git a/src/stores/useBiliStore.ts b/src/stores/useBiliStore.ts index 46c52ee9..28c46588 100644 --- a/src/stores/useBiliStore.ts +++ b/src/stores/useBiliStore.ts @@ -21,6 +21,10 @@ export const useBiliStore = defineStore('bili', () => { const filteredFansMedals = computed( () => fansMedals.value?.filter((m) => m.room_info.room_id !== 0) ?? [], ) + // 过滤了不存在直播间的粉丝勋章Map(key:直播间号,value:粉丝勋章对象) + const filteredFansMedalsMap = computed>( + () => new Map(filteredFansMedals.value.map((m) => [m.room_info.room_id, m])), + ) // 粉丝勋章获取状态(初始值:undefined,获取中:loading,获取成功:loaded,获取失败:error) const fansMedalsStatus = ref() // wbi 签名所需的盐值 @@ -52,6 +56,7 @@ export const useBiliStore = defineStore('bili', () => { dynamicVideos, fansMedals, filteredFansMedals, + filteredFansMedalsMap, fansMedalsStatus, wbiSalt, } diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index 2370d3eb..eff8d384 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -50,7 +50,7 @@ export const useModuleStore = defineStore('module', () => { // 模块实例映射(key: 模块名称, value: 模块实例) const moduleInstances = ref>({}) // 模块状态、运行记录重置和再运行 - const moduleReset = ref({ + const moduleReset: ModuleReset = { DailyTasks: { MainSiteTasks: { login: async () => { @@ -147,7 +147,7 @@ export const useModuleStore = defineStore('module', () => { }, }, }, - }) + } /** * 运行模块 diff --git a/src/types/helpInfo.d.ts b/src/types/helpInfo.d.ts index b202f84b..218404d8 100644 --- a/src/types/helpInfo.d.ts +++ b/src/types/helpInfo.d.ts @@ -17,8 +17,11 @@ interface HelpInfo { medalTasks: { list: HelpInfoItem light: HelpInfoItem + rounds: HelpInfoItem like: HelpInfoItem + likeWaitUntilLiving: HelpInfoItem danmu: HelpInfoItem + danmuWaitUntilNotLiving: HelpInfoItem watch: HelpInfoItem } } diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index 25017d64..799767af 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -31,6 +31,9 @@ interface ModuleConfig { } like: { enabled: boolean + dailyLimitOrTargetRounds: boolean + targetRounds: number + waitUntilLiving: boolean isWhiteList: boolean roomidList: number[] _lastCompleteTime: number @@ -38,13 +41,18 @@ interface ModuleConfig { danmu: { enabled: boolean danmuList: string[] + dailyLimitOrTargetRounds: boolean + targetRounds: number onlyWhenNotLiving: boolean + waitUntilNotLiving: boolean isWhiteList: boolean roomidList: number[] _lastCompleteTime: number } watch: { enabled: boolean + dailyLimitOrTargetRounds: boolean + targetRounds: number isWhiteList: boolean roomidList: number[] _watchingProgress: Record From 8ddce602dd4ca18a4ca8c789f8d47ad7724eefb1 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:04:58 +0800 Subject: [PATCH 06/30] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E7=9B=B4=E6=92=AD?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=A4=9A=E5=A4=84=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复发弹幕任务`getMedals()`分组逻辑错误 - MedalModule的probeWaitingMedals方法的targetPredicate参数只允许数字,内部判断时统一处理null/undefined - 发弹幕和点赞任务在执行过程中不主动检查配置项的更改 - 删除多处未使用的lodash导入 --- .../liveTasks/medalTasks/MedalModule.ts | 4 ++-- .../liveTasks/medalTasks/danmuTask.ts | 23 ++++++++----------- .../liveTasks/medalTasks/lightTask.ts | 1 - .../liveTasks/medalTasks/likeTask.ts | 10 ++------ .../liveTasks/medalTasks/watchTask.ts | 1 - 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 2fba0449..842aa46c 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -462,7 +462,7 @@ class MedalModule extends BaseModule { */ protected async probeWaitingMedals( roomids: number[], - targetPredicate: (liveStatus?: number | null) => boolean, + targetPredicate: (liveStatus: number) => boolean, ): Promise { const snapshot = await this.getSharedWaitProbeSnapshot(roomids) this.logger.log( @@ -490,7 +490,7 @@ class MedalModule extends BaseModule { const liveStatus = snapshot.liveStatusMap.get(roomid) - if (targetPredicate(liveStatus)) { + if (!_.isNil(liveStatus) && targetPredicate(liveStatus)) { readyMedals.push(medal) } else { pendingRoomids.push(roomid) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 5b768f8f..916c7830 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -2,7 +2,6 @@ import { delayToNextMoment, isNowBefore, isTimestampToday, tsm } from '@/library import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore } from '@/stores' import type { ModuleStatusTypes } from '@/types' -import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' import { sleep } from '@/library/utils' @@ -32,10 +31,14 @@ class DanmuTask extends MedalModule { this.SHARED_MEDAL_FILTERS.isLighted(medal) ) { if (this.config.onlyWhenNotLiving && this.SHARED_MEDAL_FILTERS.isLiving(medal)) { - // 如果开启了“仅在未开播的直播间发弹幕”且当前直播间正在直播,需要等到下播后再发弹幕 - result.waitingMedals.push(medal) - } else if (this.config.waitUntilNotLiving) { - // 否则可立即发弹幕 + // 开启了“仅在未开播的直播间发弹幕”且正在直播 + if (this.config.waitUntilNotLiving) { + // 开启了“等待下播后再发弹幕”,等下播了再发弹幕 + result.waitingMedals.push(medal) + } + // 没开“等待下播后再发弹幕”,跳过 + } else { + // 没开“仅在未开播的直播间发弹幕”或者直播间未开播,可立即发弹幕 result.readyMedals.push(medal) } } @@ -194,10 +197,7 @@ class DanmuTask extends MedalModule { const { readyMedals, waitingMedals } = this.getMedals() let allCompleted = true const danmuIndexRef = { value: 0 } - let pendingRoomids = - this.config.onlyWhenNotLiving && this.config.waitUntilNotLiving - ? waitingMedals.map((medal) => medal.room_info.room_id) - : [] + let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) const isInitialInterrupted = await this.executeDanmuTasks(readyMedals, danmuIndexRef) if (isInitialInterrupted) { @@ -205,11 +205,6 @@ class DanmuTask extends MedalModule { } while (allCompleted && pendingRoomids.length > 0) { - if (!this.config.onlyWhenNotLiving || !this.config.waitUntilNotLiving) { - allCompleted = false - this.logger.log('等待下播后再发弹幕已关闭,结束本轮等待') - break - } if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') allCompleted = false diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index 29bea431..3fa7b0b9 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -3,7 +3,6 @@ import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore } from '@/stores' import { sleep } from '@/library/utils' import type { ModuleStatusTypes } from '@/types' -import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' import type { GroupedMedals } from './types' diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index aacf7cfe..f5eff310 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -2,7 +2,6 @@ import { delayToNextMoment, isNowBefore, isTimestampToday, tsm } from '@/library import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore } from '@/stores' import type { ModuleStatusTypes } from '@/types' -import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' import { sleep } from '@/library/utils' @@ -32,10 +31,10 @@ class LikeTask extends MedalModule { this.SHARED_MEDAL_FILTERS.isLighted(medal) ) { if (this.SHARED_MEDAL_FILTERS.isLiving(medal)) { - // 如果当前直播间正在直播,可立即点赞 + // 当前直播间正在直播,可立即点赞 result.readyMedals.push(medal) } else if (this.config.waitUntilLiving) { - // 否则需要等到直播间开播后再点赞 + // 当前直播未开播但开启了“等待开播后再点赞”,等直播间开播后再点赞 result.waitingMedals.push(medal) } } @@ -174,11 +173,6 @@ class LikeTask extends MedalModule { } while (allCompleted && pendingRoomids.length > 0) { - if (!this.config.waitUntilLiving) { - allCompleted = false - this.logger.log('等待开播后再点赞已关闭,结束本轮等待') - break - } if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') allCompleted = false diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index 8b6a893f..b2b5d51f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -5,7 +5,6 @@ import Logger from '@/library/logger' import CryptoJS from 'crypto-js' import { sleep, uuid } from '@/library/utils' import type { ModuleStatusTypes, RunAtMoment } from '@/types' -import _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' From 285a98febf50e1f0176b37cc8c24fc013b5fb169 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:03:08 +0800 Subject: [PATCH 07/30] =?UTF-8?q?=F0=9F=8E=88=20perf(MedalModule):=20?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E6=9C=BA=E5=88=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liveTasks/medalTasks/MedalModule.ts | 125 +++++++++++++----- .../liveTasks/medalTasks/types.d.ts | 3 + 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 842aa46c..e55c086f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -7,6 +7,7 @@ import type { TaskJumpType, WaitProbeResult, WaitProbeSnapshot, + WaitProbeSnapshotReusability, WaitStrategy, } from './types' import { arrayToMap, sleep } from '@/library/utils' @@ -237,6 +238,8 @@ class MedalModule extends BaseModule { /** * 计算等待探测策略 + * + * @param pendingCount 当前待探测的直播间数量 */ protected decideWaitStrategy(pendingCount: number): WaitStrategy { const totalMedalCount = useBiliStore().filteredFansMedals.length @@ -245,21 +248,45 @@ class MedalModule extends BaseModule { } /** - * 判断快照是否仍可复用 + * 判断等待探测快照是否可复用 + * + * @param snapshot 待判断的快照 + * @param roomids 当前待探测的直播间ID列表 */ - private static canReuseWaitProbeSnapshot( + private static waitProbeSnapshotReuseCheck( snapshot: WaitProbeSnapshot, roomids: number[], - skipTTL = false, - ): boolean { - const uncovered = MedalModule.getUncoveredRoomidsFromSnapshot(snapshot, roomids, skipTTL) - return uncovered !== null && uncovered.length === 0 + ): WaitProbeSnapshotReusability { + const uncovered = MedalModule.getUncoveredRoomidsFromSnapshot(snapshot, roomids) + if (uncovered === null) return 'none' + return uncovered.length === 0 ? 'full' : 'partial' } /** - * 获取快照中尚未覆盖的房间列表 + * 检查等待探测快照是否在有效期内 * - * 仅成功快照才允许参与部分复用 + * @param snapshot 待判断的快照 + * @param skipTTL 是否跳过快照有效时间的判断 + * @returns 快照在有效期内返回 true,否则返回 false + */ + private static waitProbeSnapshotTimeCheck(snapshot: WaitProbeSnapshot, skipTTL = false): boolean { + if (skipTTL || Date.now() - snapshot.createdAt < MedalModule.WAIT_PROBE_SNAPSHOT_TTL) { + return true + } + + return false + } + + /** + * 获取等待探测快照中尚未覆盖的房间列表 + * + * 仅有效(成功且未过期)快照才允许参与部分复用 + * + * @param snapshot 待判断的快照 + * @param roomids 当前待探测的直播间ID列表 + * @param skipTTL 是否跳过快照有效时间的判断 + * + * @returns 快照不可用返回 null,否则返回尚未覆盖的房间ID列表 */ private static getUncoveredRoomidsFromSnapshot( snapshot: WaitProbeSnapshot, @@ -269,7 +296,8 @@ class MedalModule extends BaseModule { if (!snapshot.isSuccessful) { return null } - if (!skipTTL && Date.now() - snapshot.createdAt > MedalModule.WAIT_PROBE_SNAPSHOT_TTL) { + + if (!MedalModule.waitProbeSnapshotTimeCheck(snapshot, skipTTL)) { return null } @@ -283,10 +311,10 @@ class MedalModule extends BaseModule { * 后面的快照会覆盖前面的同房间数据 */ private static mergeWaitProbeSnapshots(...snapshots: WaitProbeSnapshot[]): WaitProbeSnapshot { - const roomids = [...new Set(snapshots.flatMap((snapshot) => snapshot.roomids))] - const createdAt = Math.min(...snapshots.map((snapshot) => snapshot.createdAt)) - const strategy = snapshots.at(-1)!.strategy - const isSuccessful = snapshots.some((snapshot) => snapshot.isSuccessful) + const roomids = [...new Set(snapshots.flatMap((snapshot) => snapshot.roomids))] // 合并房间列表并去重 + const createdAt = Math.min(...snapshots.map((snapshot) => snapshot.createdAt)) // 取最早的创建时间 + const strategy = snapshots.at(-1)!.strategy // 取最后一个快照的策略 + const isSuccessful = snapshots.some((snapshot) => snapshot.isSuccessful) // 只要有一份快照成功就认为合并后的快照成功(方便部分复用快照) const liveStatusMap = new Map() const medalMap = new Map() @@ -378,28 +406,41 @@ class MedalModule extends BaseModule { * 获取可在多个任务模块之间复用的一轮等待探测快照 */ private async getSharedWaitProbeSnapshot(roomids: number[]): Promise { + // 可复用快照列表,最多包含两份快照:[旧快照 cachedSnapshot, 先前正在探测中、刚刚完成探测的快照 inflightSnapshot] const reusableSnapshots: WaitProbeSnapshot[] = [] + // 旧快照(最近一次等待探测快照) const cachedSnapshot = MedalModule.sharedWaitProbeSnapshot - if (cachedSnapshot && MedalModule.canReuseWaitProbeSnapshot(cachedSnapshot, roomids)) { - // 旧快照可以完全复用 - return cachedSnapshot - } - if ( - cachedSnapshot && - MedalModule.getUncoveredRoomidsFromSnapshot(cachedSnapshot, roomids) !== null - ) { - reusableSnapshots.push(cachedSnapshot) + + if (cachedSnapshot) { + const cachedSnapshotReusability = MedalModule.waitProbeSnapshotReuseCheck( + cachedSnapshot, + roomids, + ) + + if (cachedSnapshotReusability === 'full') { + // 旧快照可以完全复用 + return cachedSnapshot + } + if (cachedSnapshotReusability === 'partial') { + // 旧快照可以部分复用 + reusableSnapshots.push(cachedSnapshot) + } } if (MedalModule.sharedWaitProbePromise) { + // 存在正在进行中的等待探测,先等该探测完成 const inflightSnapshot = await MedalModule.sharedWaitProbePromise - if (MedalModule.canReuseWaitProbeSnapshot(inflightSnapshot, roomids, true)) { + const inflightSnapshotReusability = MedalModule.waitProbeSnapshotReuseCheck( + inflightSnapshot, + roomids, + ) + + if (inflightSnapshotReusability === 'full') { + // 该轮探测结果可以完全复用 return inflightSnapshot } - if ( - inflightSnapshot !== cachedSnapshot && - MedalModule.getUncoveredRoomidsFromSnapshot(inflightSnapshot, roomids, true) !== null - ) { + if (inflightSnapshotReusability === 'partial') { + // 该轮探测结果可以部分复用 reusableSnapshots.push(inflightSnapshot) } } @@ -408,28 +449,40 @@ class MedalModule extends BaseModule { this.decideWaitStrategy(roomids.length) === 'single-probe' && reusableSnapshots.length > 0 ) { + // 使用单房间探测策略且存在可复用的探测结果快照,先合并这些快照 const mergedReusableSnapshot = MedalModule.mergeWaitProbeSnapshots(...reusableSnapshots) + // 计算合并后的快照尚未覆盖的房间列表 const uncoveredRoomids = MedalModule.getUncoveredRoomidsFromSnapshot( mergedReusableSnapshot, roomids, - true, + true, // 此时旧快照可能已经过期,最后使用它一次,跳过快照有效时间的判断 ) if (uncoveredRoomids && uncoveredRoomids.length === 0) { - MedalModule.sharedWaitProbeSnapshot = mergedReusableSnapshot + // 合并后的快照已经覆盖了所有待探测的房间,直接使用该快照,无需再发起新的探测 + if (MedalModule.waitProbeSnapshotTimeCheck(mergedReusableSnapshot)) { + // 合并后的快照在有效期内,更新“最近一次等待探测快照”为合并后的快照 + MedalModule.sharedWaitProbeSnapshot = mergedReusableSnapshot + } else { + // 否则取最近一次等待探测快照,可能是过期的旧快照,也可能是先前正在探测中、刚刚完成探测的快照(这份新鲜出炉的快照一定没过期) + MedalModule.sharedWaitProbeSnapshot = reusableSnapshots.at(-1)! + } + return mergedReusableSnapshot } if (uncoveredRoomids && uncoveredRoomids.length > 0) { - const probePromise = this.createWaitProbeSnapshot(uncoveredRoomids).then((patchSnapshot) => - MedalModule.mergeWaitProbeSnapshots(mergedReusableSnapshot, patchSnapshot), - ) + // 合并后的快照尚未覆盖所有待探测的房间,对尚未覆盖的房间发起新的探测 + const probePromise = this.createWaitProbeSnapshot(uncoveredRoomids) MedalModule.sharedWaitProbePromise = probePromise - try { - const mergedSnapshot = await probePromise - MedalModule.sharedWaitProbeSnapshot = mergedSnapshot - return mergedSnapshot + const patchSnapshot = await probePromise + // 将“最近一次等待探测快照”更新为刚刚对尚未覆盖的房间发起新的探测得到的快照 + // 这里舍弃了之前合并得到的快照 mergedReusableSnapshot,原因是我们刚刚等“尚未覆盖的房间发起新的探测”完成导致又流逝了一段时间, + // 此时 mergedReusableSnapshot 要么已经过期,要么接近过期了(虽然这点在小数据量下可能不成立),不如直接使用新鲜出炉的 patchSnapshot + MedalModule.sharedWaitProbeSnapshot = patchSnapshot + // 此处最后使用一次 mergedReusableSnapshot 中的数据,将它和 patchSnapshot 合并后返回,避免之前可复用的探测结果数据被浪费 + return MedalModule.mergeWaitProbeSnapshots(mergedReusableSnapshot, patchSnapshot) } finally { if (MedalModule.sharedWaitProbePromise === probePromise) { MedalModule.sharedWaitProbePromise = null diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 6d1bb0dc..8fd62654 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -30,6 +30,8 @@ interface WaitProbeSnapshot { medalMap: Map } +type WaitProbeSnapshotReusability = 'full' | 'partial' | 'none' + interface WaitProbeResult { readyMedals: LiveData.FansMedalPanel.List[] pendingRoomids: number[] @@ -42,5 +44,6 @@ export { TaskJumpType, WaitStrategy, WaitProbeSnapshot, + WaitProbeSnapshotReusability, WaitProbeResult, } From 4dd0cddb84ae185a9e8c8e1322234d9e5fbbb971 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:52:30 +0800 Subject: [PATCH 08/30] =?UTF-8?q?=F0=9F=94=A7=20build:=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=BE=9D=E8=B5=96=E5=92=8Ceslint=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.ts | 3 +- package-lock.json | 482 +++++++++++++++++++++------------------------- package.json | 25 ++- 3 files changed, 234 insertions(+), 276 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 548d08b9..4479bcaf 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,5 +1,4 @@ -import { globalIgnores } from 'eslint/config' -import { includeIgnoreFile } from '@eslint/compat' +import { globalIgnores, includeIgnoreFile } from 'eslint/config' import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' import pluginVue from 'eslint-plugin-vue' import skipFormatting from 'eslint-config-prettier/flat' diff --git a/package-lock.json b/package-lock.json index c38115dd..17642c40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,32 +19,31 @@ "vue-draggable-plus": "^0.6.1" }, "devDependencies": { - "@eslint/compat": "^2.0.2", "@tsconfig/node24": "^24.0.4", "@types/crypto-js": "^4.2.2", "@types/luxon": "^3.7.1", - "@types/node": "^24.12.3", - "@vitejs/plugin-vue": "^6.0.6", + "@types/node": "^24.13.1", + "@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue-jsx": "^5.1.5", - "@vue/eslint-config-typescript": "^14.7.0", + "@vue/eslint-config-typescript": "^14.8.0", "@vue/tsconfig": "^0.9.1", - "eslint": "^10.2.0", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-vue": "~10.8.0", - "jiti": "^2.6.1", + "eslint-plugin-vue": "~10.9.0", + "jiti": "^2.7.0", "npm-run-all2": "^8.0.4", "prettier": "^3.8.3", - "stylelint": "^17.7.0", + "stylelint": "^17.13.0", "stylelint-config-recess-order": "^7.7.0", "stylelint-config-recommended-vue": "^1.6.1", "stylelint-config-standard": "^40.0.0", "stylelint-order": "^8.1.1", "svgo": "^4.0.1", - "terser": "^5.47.1", + "terser": "^5.48.0", "typescript": "~6.0.3", - "vite": "^8.0.11", - "vite-plugin-monkey": "^8.0.2", - "vue-tsc": "^3.2.8" + "vite": "^8.0.16", + "vite-plugin-monkey": "^8.0.6", + "vue-tsc": "^3.3.4" } }, "node_modules/@babel/code-frame": { @@ -501,9 +500,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", - "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -548,9 +547,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", - "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", "dev": true, "funding": [ { @@ -743,27 +742,6 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/compat": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.1.0.tgz", - "integrity": "sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "peerDependencies": { - "eslint": "^8.40 || 9 || 10" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, "node_modules/@eslint/config-array": { "version": "0.23.5", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", @@ -780,9 +758,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -816,9 +794,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1045,9 +1023,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.128.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", - "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -1066,9 +1044,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1083,9 +1061,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1100,9 +1078,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1117,9 +1095,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1134,9 +1112,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", - "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1151,9 +1129,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -1171,9 +1149,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -1191,9 +1169,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -1211,9 +1189,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -1231,9 +1209,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -1251,9 +1229,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -1271,9 +1249,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -1288,9 +1266,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", - "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -1307,9 +1285,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -1324,9 +1302,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1341,9 +1319,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.13", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", - "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -1818,13 +1796,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.3.tgz", - "integrity": "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==", + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz", + "integrity": "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/sortablejs": { @@ -1840,17 +1818,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1863,7 +1841,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", + "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1879,16 +1857,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -1904,14 +1882,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -1926,14 +1904,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1944,9 +1922,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -1961,15 +1939,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1986,9 +1964,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", "engines": { @@ -2000,16 +1978,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2028,9 +2006,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz", + "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==", "dev": true, "license": "ISC", "bin": { @@ -2041,16 +2019,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2065,13 +2043,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2096,13 +2074,13 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", - "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.13" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -2299,15 +2277,15 @@ } }, "node_modules/@vue/eslint-config-typescript": { - "version": "14.7.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz", - "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==", + "version": "14.8.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.8.0.tgz", + "integrity": "sha512-yIquzhXH7ZsrwSSm+rYvoGCRY6wcuF4qBi76e0l7hHLq7YU0f9aC+RcR5fL+XJNfmBZxgX5cVl4sppt4x7ZCBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.56.0", + "@typescript-eslint/utils": "^8.60.0", "fast-glob": "^3.3.3", - "typescript-eslint": "^8.56.0", + "typescript-eslint": "^8.60.0", "vue-eslint-parser": "^10.4.0" }, "engines": { @@ -2325,16 +2303,16 @@ } }, "node_modules/@vue/language-core": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.8.tgz", - "integrity": "sha512-9OiSPQFiAAWNVnXb0d2dcTmcKnFQamhuNES6ayyISrb/mwPWVgoGdAqSfCWqKhQpa3D5gDTcYD+w7ObiheZ81g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.4.tgz", + "integrity": "sha512-IuHqQ5zGGOE7CXP72VX6A42IVeIzYv4WAhO6arej11TRNqtdZfGyH8Yr2FOCaDX0dSQG+JwULLoFHGY1igYVjQ==", "dev": true, "license": "MIT", "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", - "alien-signals": "^3.1.2", + "alien-signals": "^3.2.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" @@ -2519,9 +2497,9 @@ } }, "node_modules/alien-signals": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", - "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", "dev": true, "license": "MIT" }, @@ -3233,18 +3211,18 @@ } }, "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3305,9 +3283,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", - "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz", + "integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==", "dev": true, "license": "MIT", "dependencies": { @@ -3325,7 +3303,7 @@ "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "vue-eslint-parser": "^10.0.0" + "vue-eslint-parser": "^10.3.0" }, "peerDependenciesMeta": { "@stylistic/eslint-plugin": { @@ -4021,16 +3999,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-what": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", @@ -5020,9 +4988,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -5039,7 +5007,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5313,14 +5281,14 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", - "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.128.0", - "@rolldown/pluginutils": "1.0.0-rc.18" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5329,29 +5297,22 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-x64": "1.0.0-rc.18", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", - "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", - "dev": true, - "license": "MIT" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } }, "node_modules/rollup": { "version": "4.60.3", @@ -5631,9 +5592,9 @@ } }, "node_modules/stylelint": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.0.tgz", - "integrity": "sha512-/3czzmbF9XdGWvReDF3Ex4R23Ajolo7j8RB2bFNEqk6Ht356nlpVV+G5bG2Qt8AW1ofJzXztBRDnAtd7cgowWA==", + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.13.0.tgz", + "integrity": "sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==", "dev": true, "funding": [ { @@ -5647,9 +5608,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.2.0", + "@csstools/css-calc": "^3.2.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@csstools/css-syntax-patches-for-csstree": "^1.1.4", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", @@ -5661,20 +5622,19 @@ "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^11.1.2", + "file-entry-cache": "^11.1.3", "global-modules": "^2.0.0", "globby": "^16.2.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", - "is-plain-object": "^5.0.0", "mathml-tag-names": "^4.0.0", "meow": "^14.1.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.5.13", + "postcss": "^8.5.15", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", @@ -6040,9 +6000,9 @@ } }, "node_modules/terser": { - "version": "5.47.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", - "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6066,9 +6026,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -6175,16 +6135,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6199,9 +6159,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -6267,17 +6227,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", - "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.0-rc.18", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -6345,9 +6305,9 @@ } }, "node_modules/vite-plugin-monkey": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/vite-plugin-monkey/-/vite-plugin-monkey-8.0.2.tgz", - "integrity": "sha512-K2jgAdOXy1dxuy/EriZleGjxDiCdB26PSli1K+/40l5LEFsMlJdMPSbEpLnyH0B5FYlZMhmyOQTHtdVLC5+cTA==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/vite-plugin-monkey/-/vite-plugin-monkey-8.0.6.tgz", + "integrity": "sha512-DpNKGLpATKMiFh0m0UaT85oEZYUJ4gtKeaTmL/zyR2jmCmuuybJGC1iI5+wJzLASZpdkTF055UDxVfHtF1RlNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6594,14 +6554,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.8.tgz", - "integrity": "sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.4.tgz", + "integrity": "sha512-XA/JqmQwS2GZmfgpjOEGdrKwaTSEuPwxpHa7/t6f4yiGrJb3gVHTPb9wBfByMNZwQ+xDXs41b8gaS2DKsOozUw==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.2.8" + "@vue/language-core": "3.3.4" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/package.json b/package.json index ab87caa0..2849380b 100644 --- a/package.json +++ b/package.json @@ -32,28 +32,27 @@ "@tsconfig/node24": "^24.0.4", "@types/crypto-js": "^4.2.2", "@types/luxon": "^3.7.1", - "@types/node": "^24.12.3", - "@vitejs/plugin-vue": "^6.0.6", + "@types/node": "^24.13.1", + "@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue-jsx": "^5.1.5", - "@vue/eslint-config-typescript": "^14.7.0", + "@vue/eslint-config-typescript": "^14.8.0", "@vue/tsconfig": "^0.9.1", - "@eslint/compat": "^2.0.2", - "eslint": "^10.2.0", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-vue": "~10.8.0", - "jiti": "^2.6.1", + "eslint-plugin-vue": "~10.9.0", + "jiti": "^2.7.0", "npm-run-all2": "^8.0.4", "prettier": "^3.8.3", - "stylelint": "^17.7.0", + "stylelint": "^17.13.0", "stylelint-config-recess-order": "^7.7.0", - "stylelint-config-standard": "^40.0.0", "stylelint-config-recommended-vue": "^1.6.1", + "stylelint-config-standard": "^40.0.0", "stylelint-order": "^8.1.1", "svgo": "^4.0.1", - "terser": "^5.47.1", + "terser": "^5.48.0", "typescript": "~6.0.3", - "vite": "^8.0.11", - "vite-plugin-monkey": "^8.0.2", - "vue-tsc": "^3.2.8" + "vite": "^8.0.16", + "vite-plugin-monkey": "^8.0.6", + "vue-tsc": "^3.3.4" } } From 115bcc36118f4c05b33eb8d0cfdf77ef5fe5a2cf Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:07:17 +0800 Subject: [PATCH 09/30] =?UTF-8?q?=F0=9F=90=B3=20chore:=20eslint=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改no-unused-vars的varsIgnorePattern,不允许单下划线变量 --- eslint.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.ts b/eslint.config.ts index 4479bcaf..849dfe0f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -34,7 +34,7 @@ export default defineConfigWithVueTs( argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', - varsIgnorePattern: '^_', + varsIgnorePattern: '^_.+', ignoreRestSiblings: true, }, ], From 93f552e927d86585874c6af83f759edef91a6ccc Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:38:50 +0800 Subject: [PATCH 10/30] =?UTF-8?q?=E2=9C=A8=20feat(MedalModule):=20?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化逻辑 - 充分利用刷新粉丝勋章得到的数据 - 增加共享等待探测快照的有效时间 --- .../liveTasks/medalTasks/MedalModule.ts | 176 +++--------------- .../liveTasks/medalTasks/types.d.ts | 3 - 2 files changed, 23 insertions(+), 156 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index e55c086f..7294f93c 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -7,7 +7,6 @@ import type { TaskJumpType, WaitProbeResult, WaitProbeSnapshot, - WaitProbeSnapshotReusability, WaitStrategy, } from './types' import { arrayToMap, sleep } from '@/library/utils' @@ -22,7 +21,7 @@ class MedalModule extends BaseModule { /** 等待开播/下播时单房间探测的间隔 */ protected static readonly WAIT_POLL_INTERVAL = 120_000 /** 共享等待探测快照的短期有效时间 */ - private static readonly WAIT_PROBE_SNAPSHOT_TTL = 60_000 + private static readonly WAIT_PROBE_SNAPSHOT_TTL = 90_000 /** 单房间探测时相邻请求间的随机延迟 */ private static get ROOM_STATUS_PROBE_DYNAMIC_DELAY() { return _.random(600, 1000) @@ -248,93 +247,37 @@ class MedalModule extends BaseModule { } /** - * 判断等待探测快照是否可复用 + * 判断等待探测快照是否可完全复用 * - * @param snapshot 待判断的快照 - * @param roomids 当前待探测的直播间ID列表 - */ - private static waitProbeSnapshotReuseCheck( - snapshot: WaitProbeSnapshot, - roomids: number[], - ): WaitProbeSnapshotReusability { - const uncovered = MedalModule.getUncoveredRoomidsFromSnapshot(snapshot, roomids) - if (uncovered === null) return 'none' - return uncovered.length === 0 ? 'full' : 'partial' - } - - /** - * 检查等待探测快照是否在有效期内 - * - * @param snapshot 待判断的快照 - * @param skipTTL 是否跳过快照有效时间的判断 - * @returns 快照在有效期内返回 true,否则返回 false - */ - private static waitProbeSnapshotTimeCheck(snapshot: WaitProbeSnapshot, skipTTL = false): boolean { - if (skipTTL || Date.now() - snapshot.createdAt < MedalModule.WAIT_PROBE_SNAPSHOT_TTL) { - return true - } - - return false - } - - /** - * 获取等待探测快照中尚未覆盖的房间列表 - * - * 仅有效(成功且未过期)快照才允许参与部分复用 + * 仅成功且未过期,且已覆盖所有待探测房间的快照允许复用 * * @param snapshot 待判断的快照 * @param roomids 当前待探测的直播间ID列表 - * @param skipTTL 是否跳过快照有效时间的判断 - * - * @returns 快照不可用返回 null,否则返回尚未覆盖的房间ID列表 */ - private static getUncoveredRoomidsFromSnapshot( + private static canFullyReuseWaitProbeSnapshot( snapshot: WaitProbeSnapshot, roomids: number[], - skipTTL = false, - ): number[] | null { + ): boolean { if (!snapshot.isSuccessful) { - return null + return false } - if (!MedalModule.waitProbeSnapshotTimeCheck(snapshot, skipTTL)) { - return null + if (!MedalModule.waitProbeSnapshotTimeCheck(snapshot)) { + return false } const coveredRoomids = new Set(snapshot.roomids) - return roomids.filter((roomid) => !coveredRoomids.has(roomid)) + return roomids.every((roomid) => coveredRoomids.has(roomid)) } /** - * 合并多份等待探测快照 + * 检查等待探测快照是否在有效期内 * - * 后面的快照会覆盖前面的同房间数据 + * @param snapshot 待判断的快照 + * @returns 快照在有效期内返回 true,否则返回 false */ - private static mergeWaitProbeSnapshots(...snapshots: WaitProbeSnapshot[]): WaitProbeSnapshot { - const roomids = [...new Set(snapshots.flatMap((snapshot) => snapshot.roomids))] // 合并房间列表并去重 - const createdAt = Math.min(...snapshots.map((snapshot) => snapshot.createdAt)) // 取最早的创建时间 - const strategy = snapshots.at(-1)!.strategy // 取最后一个快照的策略 - const isSuccessful = snapshots.some((snapshot) => snapshot.isSuccessful) // 只要有一份快照成功就认为合并后的快照成功(方便部分复用快照) - const liveStatusMap = new Map() - const medalMap = new Map() - - snapshots.forEach((snapshot) => { - snapshot.liveStatusMap.forEach((liveStatus, roomid) => { - liveStatusMap.set(roomid, liveStatus) - }) - snapshot.medalMap.forEach((medal, roomid) => { - medalMap.set(roomid, medal) - }) - }) - - return { - createdAt, - roomids, - strategy, - isSuccessful, - liveStatusMap, - medalMap, - } + private static waitProbeSnapshotTimeCheck(snapshot: WaitProbeSnapshot): boolean { + return Date.now() - snapshot.createdAt < MedalModule.WAIT_PROBE_SNAPSHOT_TTL } /** @@ -358,17 +301,16 @@ class MedalModule extends BaseModule { } const latestMedalMap = useBiliStore().filteredFansMedalsMap - roomids.forEach((roomid) => { - const medal = latestMedalMap.get(roomid) - if (medal) { - medalMap.set(roomid, medal) - liveStatusMap.set(roomid, medal.room_info.living_status) - } + const snapshotRoomids = [...latestMedalMap.keys()] + // 将所有粉丝勋章数据放入快照,充分利用刷新粉丝勋章得到的数据 + latestMedalMap.forEach((medal, roomid) => { + medalMap.set(roomid, medal) + liveStatusMap.set(roomid, medal.room_info.living_status) }) return { createdAt: Date.now(), - roomids, + roomids: snapshotRoomids, strategy, isSuccessful: true, liveStatusMap, @@ -406,89 +348,17 @@ class MedalModule extends BaseModule { * 获取可在多个任务模块之间复用的一轮等待探测快照 */ private async getSharedWaitProbeSnapshot(roomids: number[]): Promise { - // 可复用快照列表,最多包含两份快照:[旧快照 cachedSnapshot, 先前正在探测中、刚刚完成探测的快照 inflightSnapshot] - const reusableSnapshots: WaitProbeSnapshot[] = [] - // 旧快照(最近一次等待探测快照) const cachedSnapshot = MedalModule.sharedWaitProbeSnapshot - if (cachedSnapshot) { - const cachedSnapshotReusability = MedalModule.waitProbeSnapshotReuseCheck( - cachedSnapshot, - roomids, - ) - - if (cachedSnapshotReusability === 'full') { - // 旧快照可以完全复用 - return cachedSnapshot - } - if (cachedSnapshotReusability === 'partial') { - // 旧快照可以部分复用 - reusableSnapshots.push(cachedSnapshot) - } + if (cachedSnapshot && MedalModule.canFullyReuseWaitProbeSnapshot(cachedSnapshot, roomids)) { + return cachedSnapshot } if (MedalModule.sharedWaitProbePromise) { - // 存在正在进行中的等待探测,先等该探测完成 const inflightSnapshot = await MedalModule.sharedWaitProbePromise - const inflightSnapshotReusability = MedalModule.waitProbeSnapshotReuseCheck( - inflightSnapshot, - roomids, - ) - - if (inflightSnapshotReusability === 'full') { - // 该轮探测结果可以完全复用 + if (MedalModule.canFullyReuseWaitProbeSnapshot(inflightSnapshot, roomids)) { return inflightSnapshot } - if (inflightSnapshotReusability === 'partial') { - // 该轮探测结果可以部分复用 - reusableSnapshots.push(inflightSnapshot) - } - } - - if ( - this.decideWaitStrategy(roomids.length) === 'single-probe' && - reusableSnapshots.length > 0 - ) { - // 使用单房间探测策略且存在可复用的探测结果快照,先合并这些快照 - const mergedReusableSnapshot = MedalModule.mergeWaitProbeSnapshots(...reusableSnapshots) - // 计算合并后的快照尚未覆盖的房间列表 - const uncoveredRoomids = MedalModule.getUncoveredRoomidsFromSnapshot( - mergedReusableSnapshot, - roomids, - true, // 此时旧快照可能已经过期,最后使用它一次,跳过快照有效时间的判断 - ) - - if (uncoveredRoomids && uncoveredRoomids.length === 0) { - // 合并后的快照已经覆盖了所有待探测的房间,直接使用该快照,无需再发起新的探测 - if (MedalModule.waitProbeSnapshotTimeCheck(mergedReusableSnapshot)) { - // 合并后的快照在有效期内,更新“最近一次等待探测快照”为合并后的快照 - MedalModule.sharedWaitProbeSnapshot = mergedReusableSnapshot - } else { - // 否则取最近一次等待探测快照,可能是过期的旧快照,也可能是先前正在探测中、刚刚完成探测的快照(这份新鲜出炉的快照一定没过期) - MedalModule.sharedWaitProbeSnapshot = reusableSnapshots.at(-1)! - } - - return mergedReusableSnapshot - } - - if (uncoveredRoomids && uncoveredRoomids.length > 0) { - // 合并后的快照尚未覆盖所有待探测的房间,对尚未覆盖的房间发起新的探测 - const probePromise = this.createWaitProbeSnapshot(uncoveredRoomids) - MedalModule.sharedWaitProbePromise = probePromise - try { - const patchSnapshot = await probePromise - // 将“最近一次等待探测快照”更新为刚刚对尚未覆盖的房间发起新的探测得到的快照 - // 这里舍弃了之前合并得到的快照 mergedReusableSnapshot,原因是我们刚刚等“尚未覆盖的房间发起新的探测”完成导致又流逝了一段时间, - // 此时 mergedReusableSnapshot 要么已经过期,要么接近过期了(虽然这点在小数据量下可能不成立),不如直接使用新鲜出炉的 patchSnapshot - MedalModule.sharedWaitProbeSnapshot = patchSnapshot - // 此处最后使用一次 mergedReusableSnapshot 中的数据,将它和 patchSnapshot 合并后返回,避免之前可复用的探测结果数据被浪费 - return MedalModule.mergeWaitProbeSnapshots(mergedReusableSnapshot, patchSnapshot) - } finally { - if (MedalModule.sharedWaitProbePromise === probePromise) { - MedalModule.sharedWaitProbePromise = null - } - } - } } const probePromise = this.createWaitProbeSnapshot(roomids) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 8fd62654..6d1bb0dc 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -30,8 +30,6 @@ interface WaitProbeSnapshot { medalMap: Map } -type WaitProbeSnapshotReusability = 'full' | 'partial' | 'none' - interface WaitProbeResult { readyMedals: LiveData.FansMedalPanel.List[] pendingRoomids: number[] @@ -44,6 +42,5 @@ export { TaskJumpType, WaitStrategy, WaitProbeSnapshot, - WaitProbeSnapshotReusability, WaitProbeResult, } From 426523f83bc7587413c79a4af6ffecca54e31a61 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:52:50 +0800 Subject: [PATCH 11/30] =?UTF-8?q?=F0=9F=90=B3=20chore:=20=E5=BC=B9?= =?UTF-8?q?=E5=B9=95=E9=BB=98=E8=AE=A4=E5=80=BC=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改了一些容易被屏蔽的弹幕 --- src/library/storage/defaultValues.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts index f52d89ea..92221e43 100644 --- a/src/library/storage/defaultValues.ts +++ b/src/library/storage/defaultValues.ts @@ -50,7 +50,7 @@ const defaultValues: DefaultValues = { '[妙]', '[哇]', '[爱]', - '[比心]', + '[大哭]', '[笑哭]', '[捂脸]', '[喝彩]', @@ -80,9 +80,9 @@ const defaultValues: DefaultValues = { danmuList: [ '(⌒▽⌒)', '( ̄▽ ̄)', - '(=・v・=)', + '(>y<)', '(`・ω・´)', - '(〜 ̄△ ̄)~', + '(ノ ̄▽ ̄)', '(・∀・)', '(°∀°)ノ', '╮( ̄▽ ̄)╭', From 60522e399b2dc50e9f58c5eb59f3722bbf5b5a13 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:40:44 +0800 Subject: [PATCH 12/30] =?UTF-8?q?=E2=9C=A8=20feat:=20useBiliStore=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=B2=89=E4=B8=9D=E5=8B=8B=E7=AB=A0=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=20FansMedalsMeta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 其中新增lastFetchStartedAt和lastFetchFinishedAt --- src/components/LiveTasks.vue | 2 +- .../liveTasks/medalTasks/MedalModule.ts | 16 ++++++++-------- src/modules/default/fansMedals.ts | 15 ++++++++++----- src/stores/useBiliStore.ts | 8 ++++---- src/stores/useCacheStore.ts | 7 ++++--- src/types/bili.d.ts | 9 ++++++++- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/components/LiveTasks.vue b/src/components/LiveTasks.vue index c3adbf89..66c69a6f 100644 --- a/src/components/LiveTasks.vue +++ b/src/components/LiveTasks.vue @@ -152,7 +152,7 @@ const handleEditList = async (key: MedalTaskKey) => { }, { once: true }, ) - if (!biliStore.fansMedalsStatus) { + if (!biliStore.fansMedalsMeta.status) { moduleStore.rerunModule('Default_FansMedals', true) } } else { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 7294f93c..1654cdd4 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -12,7 +12,7 @@ import type { import { arrayToMap, sleep } from '@/library/utils' import type { LiveData } from '@/library/bili-api/data' import BAPI from '@/library/bili-api' -import { isNowAfter, isNowBefore, isTimestampToday } from '@/library/luxon' +import { isNowAfter, isNowBefore, isTimestampToday, tsm } from '@/library/luxon' import _ from 'lodash' class MedalModule extends BaseModule { @@ -122,11 +122,11 @@ class MedalModule extends BaseModule { protected waitForFansMedals(): Promise { return new Promise((resolve) => { const biliStore = useBiliStore() - if (biliStore.fansMedalsStatus === 'loaded') { + if (biliStore.fansMedalsMeta.status === 'loaded') { resolve(true) } else { const unwatch = watch( - () => biliStore.fansMedalsStatus, + () => biliStore.fansMedalsMeta.status, (newValue) => { if (newValue === 'loaded') { unwatch() @@ -206,7 +206,7 @@ class MedalModule extends BaseModule { const moduleStore = useModuleStore() try { await moduleStore.rerunModule('Default_FansMedals', true) - return useBiliStore().fansMedalsStatus === 'loaded' + return useBiliStore().fansMedalsMeta.status === 'loaded' } catch (error) { this.logger.error('刷新粉丝勋章列表失败', error) return false @@ -277,7 +277,7 @@ class MedalModule extends BaseModule { * @returns 快照在有效期内返回 true,否则返回 false */ private static waitProbeSnapshotTimeCheck(snapshot: WaitProbeSnapshot): boolean { - return Date.now() - snapshot.createdAt < MedalModule.WAIT_PROBE_SNAPSHOT_TTL + return tsm() - snapshot.createdAt < MedalModule.WAIT_PROBE_SNAPSHOT_TTL } /** @@ -291,7 +291,7 @@ class MedalModule extends BaseModule { if (strategy === 'refresh-fans-medals') { if (!(await this.refreshFansMedals())) { return { - createdAt: Date.now(), + createdAt: tsm(), roomids, strategy, isSuccessful: false, @@ -309,7 +309,7 @@ class MedalModule extends BaseModule { }) return { - createdAt: Date.now(), + createdAt: tsm(), roomids: snapshotRoomids, strategy, isSuccessful: true, @@ -335,7 +335,7 @@ class MedalModule extends BaseModule { } return { - createdAt: Date.now(), + createdAt: tsm(), roomids, strategy, isSuccessful: true, diff --git a/src/modules/default/fansMedals.ts b/src/modules/default/fansMedals.ts index 85fc4f4f..7c2bf160 100644 --- a/src/modules/default/fansMedals.ts +++ b/src/modules/default/fansMedals.ts @@ -1,7 +1,7 @@ import { useBiliStore, useModuleStore } from '@/stores' import BAPI from '@/library/bili-api' import type { LiveData } from '@/library/bili-api/data' -import { delayToNextMoment, isTimestampToday } from '@/library/luxon' +import { delayToNextMoment, isTimestampToday, tsm } from '@/library/luxon' import { sleep } from '@/library/utils' import BaseModule from '@/modules/BaseModule' import ModuleError from '@/library/error/ModuleError' @@ -49,7 +49,7 @@ class FansMedals extends BaseModule { } return fansMedalList } catch (error: any) { - useBiliStore().fansMedalsStatus = 'error' + useBiliStore().fansMedalsMeta.status = 'error' throw new ModuleError(this.moduleName, `获取粉丝勋章列表出错: ${error.message}`) } } @@ -71,9 +71,14 @@ class FansMedals extends BaseModule { // 开启了任意粉丝勋章相关功能且今天没完成过 taskValues.some((t) => t.enabled && !isTimestampToday(t._lastCompleteTime, 0, 4)) ) { - biliStore.fansMedalsStatus = 'loading' - biliStore.fansMedals = await this.getFansMedals() - biliStore.fansMedalsStatus = 'loaded' + biliStore.fansMedalsMeta.status = 'loading' + biliStore.fansMedalsMeta.lastFetchStartedAt = tsm() + try { + biliStore.fansMedals = await this.getFansMedals() + biliStore.fansMedalsMeta.status = 'loaded' + } finally { + biliStore.fansMedalsMeta.lastFetchFinishedAt = tsm() + } } this.nextRunTimer = setTimeout( diff --git a/src/stores/useBiliStore.ts b/src/stores/useBiliStore.ts index 28c46588..bbecb443 100644 --- a/src/stores/useBiliStore.ts +++ b/src/stores/useBiliStore.ts @@ -1,7 +1,7 @@ import { acceptHMRUpdate, defineStore } from 'pinia' import { computed, ref } from 'vue' import type { LiveData, MainData } from '@/library/bili-api/data' -import type { BiliCookies, FansMedalsStatus } from '@/types' +import type { BiliCookies, FansMedalsMeta } from '@/types' import { getFilenameFromUrl } from '@/library/utils' export const useBiliStore = defineStore('bili', () => { @@ -25,8 +25,8 @@ export const useBiliStore = defineStore('bili', () => { const filteredFansMedalsMap = computed>( () => new Map(filteredFansMedals.value.map((m) => [m.room_info.room_id, m])), ) - // 粉丝勋章获取状态(初始值:undefined,获取中:loading,获取成功:loaded,获取失败:error) - const fansMedalsStatus = ref() + // 粉丝勋章相关元数据 + const fansMedalsMeta = ref({}) // wbi 签名所需的盐值 const wbiSalt = computed(() => { if (!userInfo.value) { @@ -57,7 +57,7 @@ export const useBiliStore = defineStore('bili', () => { fansMedals, filteredFansMedals, filteredFansMedalsMap, - fansMedalsStatus, + fansMedalsMeta, wbiSalt, } }) diff --git a/src/stores/useCacheStore.ts b/src/stores/useCacheStore.ts index fe600483..53299d2b 100644 --- a/src/stores/useCacheStore.ts +++ b/src/stores/useCacheStore.ts @@ -3,6 +3,7 @@ import { ref, watch } from 'vue' import Storage from '@/library/storage' import type { Cache } from '@/types' import { unsafeWindow } from '$' +import { tsm } from '@/library/luxon' type ScriptType = 'Main' | 'SubMain' | 'Other' @@ -28,11 +29,11 @@ export const useCacheStore = defineStore('cache', () => { * Main BLTH 存活心跳 */ function startMainBLTHAliveHeartBeat(): void { - cache.value.lastAliveHeartBeatTime = Date.now() + cache.value.lastAliveHeartBeatTime = tsm() // 每隔5秒写一次时间戳,表示有一个Main BLTH正在运行 // 之所以写时间戳而不是布尔值,是因为出现类似于浏览器崩溃的情况时 window.onunload 不会触发 // 那样就会留下一个永久的有脚本在运行的标记 - const timer = setInterval(() => (cache.value.lastAliveHeartBeatTime = Date.now()), 5000) + const timer = setInterval(() => (cache.value.lastAliveHeartBeatTime = tsm()), 5000) window.addEventListener('unload', () => { clearInterval(timer) @@ -47,7 +48,7 @@ export const useCacheStore = defineStore('cache', () => { function checkCurrentScriptType(): void { if ( cache.value.lastAliveHeartBeatTime !== 0 && - Date.now() - cache.value.lastAliveHeartBeatTime < 8000 // 允许最多3秒的误差 + tsm() - cache.value.lastAliveHeartBeatTime < 8000 // 允许最多3秒的误差 ) { // 存在 Main BLTH if (cache.value.mainScriptLocation === unsafeWindow.top!.location.pathname) { diff --git a/src/types/bili.d.ts b/src/types/bili.d.ts index 01cf22c1..eaabfd50 100644 --- a/src/types/bili.d.ts +++ b/src/types/bili.d.ts @@ -2,6 +2,13 @@ type CookieNames = 'bili_jct' | 'LIVE_BUVID' | 'buvid3' type BiliCookies = Record +// 获取中:loading,获取成功:loaded,获取失败:error type FansMedalsStatus = 'loading' | 'loaded' | 'error' -export { BiliCookies, FansMedalsStatus } +interface FansMedalsMeta { + status?: FansMedalsStatus + lastFetchStartedAt?: number + lastFetchFinishedAt?: number +} + +export { BiliCookies, FansMedalsStatus, FansMedalsMeta } From 232361237d11d1c4e2b4d3833ee8e5cd3142e06a Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:22:34 +0800 Subject: [PATCH 13/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E7=9B=B4=E6=92=AD=E7=8A=B6=E6=80=81=E6=8E=A2=E6=B5=8B=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 取消快照机制 --- .../liveTasks/medalTasks/MedalModule.ts | 265 ++++++++---------- .../liveTasks/medalTasks/types.d.ts | 10 - 2 files changed, 123 insertions(+), 152 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 1654cdd4..0bb1e78f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -6,7 +6,6 @@ import type { SharedMedalFilters, TaskJumpType, WaitProbeResult, - WaitProbeSnapshot, WaitStrategy, } from './types' import { arrayToMap, sleep } from '@/library/utils' @@ -20,8 +19,6 @@ class MedalModule extends BaseModule { protected static readonly DANMU_RETRY_LIMIT = 3 /** 等待开播/下播时单房间探测的间隔 */ protected static readonly WAIT_POLL_INTERVAL = 120_000 - /** 共享等待探测快照的短期有效时间 */ - private static readonly WAIT_PROBE_SNAPSHOT_TTL = 90_000 /** 单房间探测时相邻请求间的随机延迟 */ private static get ROOM_STATUS_PROBE_DYNAMIC_DELAY() { return _.random(600, 1000) @@ -87,10 +84,12 @@ class MedalModule extends BaseModule { * 串行执行获取任务信息请求 */ private static taskInfoRequestQueue: Promise = Promise.resolve() - /** 当前进行中的等待探测 */ - private static sharedWaitProbePromise: Promise | null = null - /** 最近一次等待探测快照 */ - private static sharedWaitProbeSnapshot: WaitProbeSnapshot | null = null + /** 粉丝勋章数据在多久内视为新鲜(毫秒) */ + private static readonly RECENT_FETCH_THRESHOLD = 90_000 + /** 进行中的全量刷新粉丝勋章 Promise */ + private static ongoingFullRefreshPromise: Promise | null = null + /** 单房间直播状态探测限流队列 */ + private static roomStatusProbeQueue: Promise = Promise.resolve() medalTasksConfig = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks @@ -197,6 +196,25 @@ class MedalModule extends BaseModule { return task } + /** + * 将单房间直播状态探测请求加入串行限流队列 + */ + private static enqueueRoomStatusProbe(requester: () => Promise): Promise { + const task = MedalModule.roomStatusProbeQueue + .catch(() => {}) + .then(async () => { + await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + return requester() + }) + + MedalModule.roomStatusProbeQueue = task.then( + () => {}, + () => {}, + ) + + return task + } + /** * 刷新粉丝勋章列表 * @@ -247,131 +265,67 @@ class MedalModule extends BaseModule { } /** - * 判断等待探测快照是否可完全复用 - * - * 仅成功且未过期,且已覆盖所有待探测房间的快照允许复用 - * - * @param snapshot 待判断的快照 - * @param roomids 当前待探测的直播间ID列表 + * 判断粉丝勋章数据是否足够新鲜 */ - private static canFullyReuseWaitProbeSnapshot( - snapshot: WaitProbeSnapshot, - roomids: number[], - ): boolean { - if (!snapshot.isSuccessful) { - return false - } - - if (!MedalModule.waitProbeSnapshotTimeCheck(snapshot)) { - return false - } - - const coveredRoomids = new Set(snapshot.roomids) - return roomids.every((roomid) => coveredRoomids.has(roomid)) + private isMedalDataFresh(): boolean { + const meta = useBiliStore().fansMedalsMeta + return ( + meta.status === 'loaded' && + tsm() - meta.lastFetchFinishedAt! < MedalModule.RECENT_FETCH_THRESHOLD + ) } /** - * 检查等待探测快照是否在有效期内 + * 从粉丝勋章 Map 构建探测结果 * - * @param snapshot 待判断的快照 - * @returns 快照在有效期内返回 true,否则返回 false + * @param roomids 待探测的直播间ID列表 + * @param targetPredicate 直播状态判断函数 + * @param medalMap 粉丝勋章 Map + * @param probedLiveStatusMap 探测到的直播状态 Map + * - 为 undefined 时:使用 medal.room_info.living_status + * - 存在键 roomid 但值为 null 时:表示本轮实时探测失败,继续等待 */ - private static waitProbeSnapshotTimeCheck(snapshot: WaitProbeSnapshot): boolean { - return tsm() - snapshot.createdAt < MedalModule.WAIT_PROBE_SNAPSHOT_TTL - } + private buildResultFromMedalMap( + roomids: number[], + targetPredicate: (liveStatus: number) => boolean, + medalMap: Map, + probedLiveStatusMap?: Map, + ): WaitProbeResult { + const readyMedals: LiveData.FansMedalPanel.List[] = [] + const pendingRoomids: number[] = [] - /** - * 生成一轮新的等待探测快照 - */ - private async createWaitProbeSnapshot(roomids: number[]): Promise { - const strategy = this.decideWaitStrategy(roomids.length) - const medalMap = new Map() - const liveStatusMap = new Map() + for (const roomid of roomids) { + const medal = medalMap.get(roomid) - if (strategy === 'refresh-fans-medals') { - if (!(await this.refreshFansMedals())) { - return { - createdAt: tsm(), - roomids, - strategy, - isSuccessful: false, - liveStatusMap, - medalMap, - } + if (!medal) { + this.logger.warn(`直播间 ${roomid} 未在粉丝勋章列表中找到,停止等待该房间`) + continue } - - const latestMedalMap = useBiliStore().filteredFansMedalsMap - const snapshotRoomids = [...latestMedalMap.keys()] - // 将所有粉丝勋章数据放入快照,充分利用刷新粉丝勋章得到的数据 - latestMedalMap.forEach((medal, roomid) => { - medalMap.set(roomid, medal) - liveStatusMap.set(roomid, medal.room_info.living_status) - }) - - return { - createdAt: tsm(), - roomids: snapshotRoomids, - strategy, - isSuccessful: true, - liveStatusMap, - medalMap, + if (!this.SHARED_MEDAL_FILTERS.isLighted(medal)) { + this.logger.log(`直播间 ${roomid} 对应的粉丝勋章已熄灭,停止等待该房间`) + continue } - } - - const currentMedalMap = useBiliStore().filteredFansMedalsMap - - for (let i = 0; i < roomids.length; i++) { - const roomid = roomids[i] - const medal = currentMedalMap.get(roomid) - if (medal) { - medalMap.set(roomid, medal) - liveStatusMap.set(roomid, await this.fetchRoomLiveStatus(roomid)) - } + // 是否有探测到的直播状态 Map + const hasProbedStatus = probedLiveStatusMap !== undefined + // 直播状态:有直播状态 Map 就从中取,否则使用粉丝勋章 Map 中的值 + const liveStatus = hasProbedStatus + ? probedLiveStatusMap.get(roomid) + : medal.room_info.living_status - if (i < roomids.length - 1) { - await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + if (hasProbedStatus && _.isNil(liveStatus)) { + this.logger.warn(`直播间 ${roomid} 本轮实时探测失败,继续等待下次检查`) + pendingRoomids.push(roomid) + } else if (!_.isNil(liveStatus) && targetPredicate(liveStatus)) { + readyMedals.push(medal) + } else { + pendingRoomids.push(roomid) } } return { - createdAt: tsm(), - roomids, - strategy, - isSuccessful: true, - liveStatusMap, - medalMap, - } - } - - /** - * 获取可在多个任务模块之间复用的一轮等待探测快照 - */ - private async getSharedWaitProbeSnapshot(roomids: number[]): Promise { - const cachedSnapshot = MedalModule.sharedWaitProbeSnapshot - - if (cachedSnapshot && MedalModule.canFullyReuseWaitProbeSnapshot(cachedSnapshot, roomids)) { - return cachedSnapshot - } - - if (MedalModule.sharedWaitProbePromise) { - const inflightSnapshot = await MedalModule.sharedWaitProbePromise - if (MedalModule.canFullyReuseWaitProbeSnapshot(inflightSnapshot, roomids)) { - return inflightSnapshot - } - } - - const probePromise = this.createWaitProbeSnapshot(roomids) - MedalModule.sharedWaitProbePromise = probePromise - - try { - const snapshot = await probePromise - MedalModule.sharedWaitProbeSnapshot = snapshot - return snapshot - } finally { - if (MedalModule.sharedWaitProbePromise === probePromise) { - MedalModule.sharedWaitProbePromise = null - } + readyMedals, + pendingRoomids, } } @@ -379,7 +333,7 @@ class MedalModule extends BaseModule { * 对等待中的直播间做一轮探测 * * @param roomids 待探测的直播间ID列表 - * @param targetPredicate 直播状态判断函数,返回“是否符合目标状态“ + * @param targetPredicate 直播状态判断函数,返回"是否符合目标状态" * * @returns 探测结果,包含符合目标状态的粉丝勋章列表和待处理的直播间ID列表 */ @@ -387,43 +341,70 @@ class MedalModule extends BaseModule { roomids: number[], targetPredicate: (liveStatus: number) => boolean, ): Promise { - const snapshot = await this.getSharedWaitProbeSnapshot(roomids) - this.logger.log( - `待处理房间 ${roomids.length} 个,本轮使用${snapshot.strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, - ) + const biliStore = useBiliStore() - const readyMedals: LiveData.FansMedalPanel.List[] = [] - const pendingRoomids: number[] = [] + // 如果有进行中的全量刷新,等待其完成 + if (MedalModule.ongoingFullRefreshPromise) { + this.logger.log('等待进行中的粉丝勋章刷新完成...') + await MedalModule.ongoingFullRefreshPromise + } - if (!snapshot.isSuccessful) { - return { readyMedals, pendingRoomids: roomids } + // 检查粉丝勋章数据是否新鲜 + if (this.isMedalDataFresh()) { + this.logger.log('粉丝勋章数据较新,直接使用缓存数据') + return this.buildResultFromMedalMap(roomids, targetPredicate, biliStore.filteredFansMedalsMap) } - for (const roomid of roomids) { - const medal = snapshot.medalMap.get(roomid) + // 数据不新鲜,决定探测策略 + const strategy = this.decideWaitStrategy(roomids.length) + this.logger.log( + `待处理房间 ${roomids.length} 个,粉丝勋章数据不新鲜,使用${strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, + ) - if (!medal) { - this.logger.warn(`直播间 ${roomid} 未在粉丝勋章列表中找到,停止等待该房间`) - continue + if (strategy === 'refresh-fans-medals') { + const promise = this.refreshFansMedals() + MedalModule.ongoingFullRefreshPromise = promise + try { + await promise + } finally { + if (MedalModule.ongoingFullRefreshPromise === promise) { + MedalModule.ongoingFullRefreshPromise = null + } } - if (!this.SHARED_MEDAL_FILTERS.isLighted(medal)) { - this.logger.log(`直播间 ${roomid} 对应的粉丝勋章已熄灭,停止等待该房间`) - continue + + if (biliStore.fansMedalsMeta.status === 'loaded') { + return this.buildResultFromMedalMap( + roomids, + targetPredicate, + biliStore.filteredFansMedalsMap, + ) } - const liveStatus = snapshot.liveStatusMap.get(roomid) + return { readyMedals: [], pendingRoomids: roomids } + } - if (!_.isNil(liveStatus) && targetPredicate(liveStatus)) { - readyMedals.push(medal) - } else { - pendingRoomids.push(roomid) + // 单房间探测策略 + const currentMedalMap = biliStore.filteredFansMedalsMap + const probedLiveStatusMap = new Map() + + for (let i = 0; i < roomids.length; i++) { + const roomid = roomids[i] + const medal = currentMedalMap.get(roomid) + + if (medal) { + probedLiveStatusMap.set( + roomid, + await MedalModule.enqueueRoomStatusProbe(() => this.fetchRoomLiveStatus(roomid)), + ) } } - return { - readyMedals, - pendingRoomids, - } + return this.buildResultFromMedalMap( + roomids, + targetPredicate, + currentMedalMap, + probedLiveStatusMap, + ) } /** diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 6d1bb0dc..3bdb31ee 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -21,15 +21,6 @@ type GroupedMedals = Record type TaskJumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' type WaitStrategy = 'single-probe' | 'refresh-fans-medals' -interface WaitProbeSnapshot { - createdAt: number - roomids: number[] - strategy: WaitStrategy - isSuccessful: boolean - liveStatusMap: Map - medalMap: Map -} - interface WaitProbeResult { readyMedals: LiveData.FansMedalPanel.List[] pendingRoomids: number[] @@ -41,6 +32,5 @@ export { GroupedMedals, TaskJumpType, WaitStrategy, - WaitProbeSnapshot, WaitProbeResult, } From 91d9937ffeac5457dc6d1c0da1a3f25c62e59cad Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:03:45 +0800 Subject: [PATCH 14/30] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E9=87=8D=E5=A4=8D=E8=BF=90=E8=A1=8C=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E8=BE=B9=E7=95=8C=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MedalModule的refreshFansMedals()若遇到正在刷新的情况,等刷新完成 - 重新运行light,like,danmu前检查无法同时运行的模块是否正在运行 --- .../liveTasks/medalTasks/MedalModule.ts | 3 ++- src/stores/useModuleStore.ts | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 0bb1e78f..e927128d 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -224,7 +224,8 @@ class MedalModule extends BaseModule { const moduleStore = useModuleStore() try { await moduleStore.rerunModule('Default_FansMedals', true) - return useBiliStore().fansMedalsMeta.status === 'loaded' + const status = useBiliStore().fansMedalsMeta.status + return status === 'loading' ? await this.waitForFansMedals() : status === 'loaded' } catch (error) { this.logger.error('刷新粉丝勋章列表失败', error) return false diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index eff8d384..b1bde1b3 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -39,6 +39,8 @@ const defaultModuleStatus: ModuleStatus = { }, } +const logger = new Logger('ModuleStore') + // 在所有 frame 或顶层 frame 上运行的被加载的模块名称 const allAndTopFrameModuleNames: string[] = [] @@ -94,6 +96,13 @@ export const useModuleStore = defineStore('module', () => { LiveTasks: { medalTasks: { light: () => { + const medalTasksStatus = moduleStatus.value.DailyTasks.LiveTasks.medalTasks + + if (medalTasksStatus.like === 'running' || medalTasksStatus.danmu === 'running') { + logger.warn('【点赞】或【发弹幕】模块仍在运行中,无法重新运行【点亮熄灭勋章】模块') + return + } + moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light = '' moduleConfig.value.DailyTasks.LiveTasks.medalTasks.light._lastEffectiveCompleteTime = 0 moduleConfig.value.DailyTasks.LiveTasks.medalTasks.light._lastCompleteTime = 0 @@ -102,6 +111,11 @@ export const useModuleStore = defineStore('module', () => { rerunModule('DailyTask_LiveTask_LightTask') }, like: () => { + if (moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light === 'running') { + logger.warn('【点亮熄灭勋章】模块仍在运行中,无法重新运行【点赞】模块') + return + } + moduleStatus.value.DailyTasks.LiveTasks.medalTasks.like = '' moduleConfig.value.DailyTasks.LiveTasks.medalTasks.like._lastCompleteTime = 0 @@ -109,6 +123,11 @@ export const useModuleStore = defineStore('module', () => { rerunModule('DailyTask_LiveTask_LikeTask') }, danmu: () => { + if (moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light === 'running') { + logger.warn('【点亮熄灭勋章】模块仍在运行中,无法重新运行【发弹幕】模块') + return + } + moduleStatus.value.DailyTasks.LiveTasks.medalTasks.danmu = '' moduleConfig.value.DailyTasks.LiveTasks.medalTasks.danmu._lastCompleteTime = 0 @@ -248,7 +267,7 @@ export const useModuleStore = defineStore('module', () => { new Logger(error.moduleName).error(error.message) } else { // 意外错误,停止运行(可能是默认模块编写有误) - new Logger('ModuleStore').error(`意外错误: ${error.message}`) + logger.error(`意外错误: ${error.message}`) return } } From 779968960d2e23d4cd19335383807049722a778c Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Fri, 12 Jun 2026 01:33:04 +0800 Subject: [PATCH 15/30] =?UTF-8?q?=F0=9F=90=B3=20chore:=20MedalModule?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E8=B0=83=E6=95=B4=E5=92=8C=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E9=98=9F=E5=88=97=E9=97=B4=E9=9A=94=E4=BC=98=E5=8C=96=EF=BC=9B?= =?UTF-8?q?=E8=A1=A5CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- .../liveTasks/medalTasks/MedalModule.ts | 42 ++++++++----------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a4a1164..81c1493a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ ### 修复 -- 修复【观看任务】会错误减少一些已观看时长的bug(B站新版粉丝团任务的观看时长计算已精确到分钟) +- 修复【观看直播】任务在计算已观看时长时会错误减少一些时长的bug(B站新版粉丝团任务的观看时长计算已精确到分钟) ### 调整 - 如果无法获取某个粉丝勋章的点亮任务,跳过该粉丝勋章 +- 手动重新运行【点亮熄灭勋章】、【点赞】和【发弹幕】任务时会检测是否与其它正在运行的模块冲突 ## [7.2.9] - 2026-5-24 diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index e927128d..a176ac19 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -21,7 +21,11 @@ class MedalModule extends BaseModule { protected static readonly WAIT_POLL_INTERVAL = 120_000 /** 单房间探测时相邻请求间的随机延迟 */ private static get ROOM_STATUS_PROBE_DYNAMIC_DELAY() { - return _.random(600, 1000) + return _.random(600, 800) + } + /** 任务信息请求队列中相邻请求间的随机延迟 */ + private static get TASK_INFO_REQUEST_DYNAMIC_DELAY() { + return _.random(300, 500) } /** 等待B站更新粉丝勋章数据的延迟 */ protected static readonly WAIT_MEDAL_UPDATE_DELAY = 3000 @@ -181,17 +185,11 @@ class MedalModule extends BaseModule { * 将获取任务信息请求加入全局串行队列 */ private static enqueueTaskInfoRequest(requester: () => Promise): Promise { - const task = MedalModule.taskInfoRequestQueue + const task = MedalModule.taskInfoRequestQueue.catch(() => {}).then(() => requester()) + + MedalModule.taskInfoRequestQueue = task .catch(() => {}) - .then(async () => { - await sleep(_.random(300, 500)) - return requester() - }) - - MedalModule.taskInfoRequestQueue = task.then( - () => {}, - () => {}, - ) + .then(() => sleep(MedalModule.TASK_INFO_REQUEST_DYNAMIC_DELAY)) return task } @@ -200,17 +198,11 @@ class MedalModule extends BaseModule { * 将单房间直播状态探测请求加入串行限流队列 */ private static enqueueRoomStatusProbe(requester: () => Promise): Promise { - const task = MedalModule.roomStatusProbeQueue - .catch(() => {}) - .then(async () => { - await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) - return requester() - }) + const task = MedalModule.roomStatusProbeQueue.catch(() => {}).then(() => requester()) - MedalModule.roomStatusProbeQueue = task.then( - () => {}, - () => {}, - ) + MedalModule.roomStatusProbeQueue = task + .catch(() => {}) + .then(() => sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY)) return task } @@ -243,12 +235,14 @@ class MedalModule extends BaseModule { protected async fetchRoomLiveStatus(roomid: number): Promise { const fetchers = _.shuffle(this.ROOM_LIVE_STATUS_FETCHERS) - for (const fetchStatus of fetchers) { - const liveStatus = await fetchStatus(roomid) + for (let i = 0; i < fetchers.length; i++) { + const liveStatus = await fetchers[i](roomid) if (liveStatus !== null) { return liveStatus } - await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + if (i < fetchers.length - 1) { + await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + } } return null From 13b077b174959d10d1d1fcc8887475568396f1a3 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:47:46 +0800 Subject: [PATCH 16/30] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E5=A4=9A=E9=A1=B9?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复FansMedals模块获取第2页之后的勋章时还在用第1页的response做判断的bug - 把刷新粉丝勋章的职责从waitForLightTask()移到点亮模块内部,避免观看直播启动过晚取不到新粉丝勋章数据的问题 - 只有任务真正执行成功后才触发logFreeIntimacy --- src/components/LiveTasks.vue | 12 ++++++------ src/library/storage/defaultValues.ts | 7 +++---- .../liveTasks/medalTasks/MedalModule.ts | 10 +--------- .../liveTasks/medalTasks/danmuTask.ts | 13 +++++++++---- .../liveTasks/medalTasks/lightTask.ts | 17 +++++++++++++++-- .../liveTasks/medalTasks/likeTask.ts | 18 ++++++++++++++---- .../liveTasks/medalTasks/watchTask.ts | 17 +++++++++++------ src/modules/default/fansMedals.ts | 7 ++----- src/stores/useModuleStore.ts | 1 - src/types/storage.d.ts | 7 +++---- 10 files changed, 64 insertions(+), 45 deletions(-) diff --git a/src/components/LiveTasks.vue b/src/components/LiveTasks.vue index 66c69a6f..3595ee61 100644 --- a/src/components/LiveTasks.vue +++ b/src/components/LiveTasks.vue @@ -239,7 +239,7 @@ function handleRowClick(row: MedalInfoRow) { @@ -250,7 +250,7 @@ function handleRowClick(row: MedalInfoRow) { :step="1" step-strictly :controls="false" - :disabled="!config.medalTasks.like.dailyLimitOrTargetRounds" + :disabled="!config.medalTasks.like.useTargetRounds" /> @@ -298,7 +298,7 @@ function handleRowClick(row: MedalInfoRow) { @@ -309,7 +309,7 @@ function handleRowClick(row: MedalInfoRow) { :step="1" step-strictly :controls="false" - :disabled="!config.medalTasks.danmu.dailyLimitOrTargetRounds" + :disabled="!config.medalTasks.danmu.useTargetRounds" /> @@ -364,7 +364,7 @@ function handleRowClick(row: MedalInfoRow) { @@ -375,7 +375,7 @@ function handleRowClick(row: MedalInfoRow) { :step="1" step-strictly :controls="false" - :disabled="!config.medalTasks.watch.dailyLimitOrTargetRounds" + :disabled="!config.medalTasks.watch.useTargetRounds" /> diff --git a/src/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts index 92221e43..8c3106cb 100644 --- a/src/library/storage/defaultValues.ts +++ b/src/library/storage/defaultValues.ts @@ -63,12 +63,11 @@ const defaultValues: DefaultValues = { ], isWhiteList: false, roomidList: [], - _lastEffectiveCompleteTime: 0, _lastCompleteTime: 0, }, like: { enabled: false, - dailyLimitOrTargetRounds: false, + useTargetRounds: false, targetRounds: 1, waitUntilLiving: false, isWhiteList: false, @@ -94,7 +93,7 @@ const defaultValues: DefaultValues = { '←◡←', `(●'◡'●)ノ♥`, ], - dailyLimitOrTargetRounds: false, + useTargetRounds: false, targetRounds: 1, onlyWhenNotLiving: false, waitUntilNotLiving: false, @@ -104,7 +103,7 @@ const defaultValues: DefaultValues = { }, watch: { enabled: false, - dailyLimitOrTargetRounds: false, + useTargetRounds: false, targetRounds: 1, isWhiteList: false, roomidList: [], diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index a176ac19..5c4b5b90 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -461,17 +461,9 @@ class MedalModule extends BaseModule { return new Promise((resolve) => { const unwatch = watch( () => moduleStore.moduleStatus.DailyTasks.LiveTasks.medalTasks.light, - async (newStatus) => { + (newStatus) => { if (newStatus === 'done' || newStatus === 'error') { unwatch() - - if (lightConfig._lastEffectiveCompleteTime === lightConfig._lastCompleteTime) { - // 如果点亮熄灭勋章模块确实进行了点亮操作 - // 重新获取粉丝勋章(主要是为了获取最新的点亮状态和直播状态) - // FansMedals 模块内部做了防重入,因此无需担心会重复获取 - await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) - moduleStore.rerunModule('Default_FansMedals', true) - } resolve() } }, diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 916c7830..3971d48f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -54,6 +54,7 @@ class DanmuTask extends MedalModule { /** * 发弹幕 + * * @returns 是否发送成功 */ private async sendDanmu(medal: LiveData.FansMedalPanel.List, danmu: string): Promise { @@ -129,11 +130,12 @@ class DanmuTask extends MedalModule { if (parsed.current >= parsed.limit) return false - const target = this.config.dailyLimitOrTargetRounds + const target = this.config.useTargetRounds ? Math.min(parsed.limit, this.config.targetRounds) : parsed.limit let remaining = target - parsed.current let failedCount = 0 + let hasSuccessfulDanmu = false for (let j = 0; j < remaining; j++) { if (this.shouldStopForCrossDay()) { @@ -142,10 +144,11 @@ class DanmuTask extends MedalModule { } const danmuText = this.config.danmuList[danmuIndexRef.value++ % this.config.danmuList.length] - if (!(await this.sendDanmu(medal, danmuText))) { + if (await this.sendDanmu(medal, danmuText)) { + hasSuccessfulDanmu = true + } else { if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) - await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) break } remaining += 1 @@ -156,7 +159,9 @@ class DanmuTask extends MedalModule { } } - sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + if (hasSuccessfulDanmu) { + sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + } return false } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index 3fa7b0b9..50e7f328 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -50,6 +50,16 @@ class LightTask extends MedalModule { return result } + /** + * 点亮任务完成后,是否仍有任务需要最新的粉丝勋章点亮状态 + */ + private shouldRefreshFansMedals(): boolean { + const { like, danmu, watch } = this.medalTasksConfig + const downstreamTasks = [like, danmu, watch] + + return downstreamTasks.some((task) => task.enabled && !isTimestampToday(task._lastCompleteTime)) + } + /** * 点赞 * @param medal 粉丝勋章 @@ -195,8 +205,11 @@ class LightTask extends MedalModule { } this.config._lastCompleteTime = tsm() - if (isEffectiveRun) { - this.config._lastEffectiveCompleteTime = this.config._lastCompleteTime + + if (isEffectiveRun && this.shouldRefreshFansMedals()) { + // 刷新粉丝勋章,确保 点赞/发弹幕/观看直播 任务运行时能获取到最新的点亮状态和直播状态 + await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) + await this.refreshFansMedals() } this.status = 'done' this.logger.log('点亮熄灭勋章任务已完成') diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index f5eff310..756f503e 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -50,8 +50,10 @@ class LikeTask extends MedalModule { /** * 点赞 + * + * @returns 是否点赞成功 */ - private async like(medal: LiveData.FansMedalPanel.List, click_time: number): Promise { + 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 @@ -63,12 +65,15 @@ class LikeTask extends MedalModule { 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 } /** @@ -111,24 +116,29 @@ class LikeTask extends MedalModule { if (parsed.current >= parsed.limit) return false const times = MedalModule.parseTitleCount(item.title) ?? 30 - const target = this.config.dailyLimitOrTargetRounds + const target = this.config.useTargetRounds ? Math.min(parsed.limit, this.config.targetRounds) : parsed.limit const remaining = target - parsed.current + let hasSuccessfulLike = false for (let j = 0; j < remaining; j++) { if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') return true } - await this.like(medal, times) + if (await this.like(medal, times)) { + hasSuccessfulLike = true + } if (j < remaining - 1) { await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) } } - sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + if (hasSuccessfulLike) { + sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + } return false } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index b2b5d51f..c939dc84 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -83,13 +83,16 @@ class RoomHeart { /** * 开始心跳 + * + * @returns 是否成功观看过任意时长 */ - public start(): Promise { + public async start(): Promise { if (!this.buvid) { this.logger.error(`缺少buvid,无法为直播间 ${this.roomID} 执行观看直播任务,请尝试刷新页面`) - return Promise.resolve() + return false } - return this.E() + await this.E() + return this.watchedSeconds > 0 } /** @@ -370,7 +373,7 @@ class WatchTask extends MedalModule { // 一轮的观看时间(默认 15 分钟) const minutes = MedalModule.parseTitleCount(item.title) ?? 15 // 目标轮次(每日上限或配置的目标轮次) - const target = this.config.dailyLimitOrTargetRounds + const target = this.config.useTargetRounds ? Math.min(parsed.limit, this.config.targetRounds) : parsed.limit // 目标秒数 = 目标轮次 × minutes × 60s @@ -389,7 +392,7 @@ class WatchTask extends MedalModule { `粉丝勋章【${medal.medal.medal_name}】 开始直播间 ${roomid}(主播【${medal.anchor_info.nick_name}】,UID:${uid})的观看直播任务,目标时长 ${targetSeconds / 60} 分钟)`, ) - await new RoomHeart( + const hasWatchingProgress = await new RoomHeart( roomid, area_id, parent_area_id, @@ -398,7 +401,9 @@ class WatchTask extends MedalModule { targetSeconds, ).start() - sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + if (hasWatchingProgress) { + sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + } } } diff --git a/src/modules/default/fansMedals.ts b/src/modules/default/fansMedals.ts index 7c2bf160..80749594 100644 --- a/src/modules/default/fansMedals.ts +++ b/src/modules/default/fansMedals.ts @@ -34,13 +34,10 @@ class FansMedals extends BaseModule { for (let page = 2; page <= Math.min(total_page, pages); page++) { const response = await BAPI.live.fansMedalPanel(page) this.logger.log(`BAPI.live.fansMedalPanel(${page}) response`, response) - if (firstPageResponse.code === 0) { + if (response.code === 0) { fansMedalList.push(...response.data.list) } else { - this.logger.error( - `获取粉丝勋章列表第${page}页失败,提前结束获取`, - firstPageResponse.message, - ) + this.logger.error(`获取粉丝勋章列表第${page}页失败,提前结束获取`, response.message) // 中途出错,返回已获取的粉丝勋章列表,不抛出错误 return fansMedalList } diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index b1bde1b3..4c8c8086 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -104,7 +104,6 @@ export const useModuleStore = defineStore('module', () => { } moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light = '' - moduleConfig.value.DailyTasks.LiveTasks.medalTasks.light._lastEffectiveCompleteTime = 0 moduleConfig.value.DailyTasks.LiveTasks.medalTasks.light._lastCompleteTime = 0 rerunModule('Default_FansMedals', true) diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index 799767af..394ed405 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -26,12 +26,11 @@ interface ModuleConfig { danmuList: string[] isWhiteList: boolean roomidList: number[] - _lastEffectiveCompleteTime: number _lastCompleteTime: number } like: { enabled: boolean - dailyLimitOrTargetRounds: boolean + useTargetRounds: boolean targetRounds: number waitUntilLiving: boolean isWhiteList: boolean @@ -41,7 +40,7 @@ interface ModuleConfig { danmu: { enabled: boolean danmuList: string[] - dailyLimitOrTargetRounds: boolean + useTargetRounds: boolean targetRounds: number onlyWhenNotLiving: boolean waitUntilNotLiving: boolean @@ -51,7 +50,7 @@ interface ModuleConfig { } watch: { enabled: boolean - dailyLimitOrTargetRounds: boolean + useTargetRounds: boolean targetRounds: number isWhiteList: boolean roomidList: number[] From 15569f2150c56e4daf8eb7308e035b3b1eb317fd Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:37:38 +0800 Subject: [PATCH 17/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=9B=B4=E6=92=AD?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90=E5=90=8E=E4=BC=9A=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=8E=A5=E5=8F=A3=E6=A3=80=E6=9F=A5=E7=B2=89=E4=B8=9D?= =?UTF-8?q?=E5=9B=A2=E5=8D=87=E7=BA=A7=E4=BB=BB=E5=8A=A1=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E7=9C=9F=E7=9A=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 点亮任务是全部点亮后一起检查 - 点赞、发弹幕和观看直播是完成一个粉丝勋章的任务就检查一次 --- CHANGELOG.md | 1 + .../liveTasks/medalTasks/MedalModule.ts | 75 ++++++++++++--- .../liveTasks/medalTasks/danmuTask.ts | 57 +++++++----- .../liveTasks/medalTasks/lightTask.ts | 91 ++++++++++++++++--- .../liveTasks/medalTasks/likeTask.ts | 59 +++++++----- .../liveTasks/medalTasks/types.d.ts | 6 ++ .../liveTasks/medalTasks/watchTask.ts | 18 +++- 7 files changed, 233 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c1493a..0fe9921f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 执行完任意粉丝勋章的粉丝团任务后,检查是否存在储蓄的亲密度以及是否达到储蓄上限,把结果输出到控制台日志 - 直播任务【点赞】、【发弹幕】和【观看直播】支持“完成目标轮次”和“跟随每日上限”两种模式 - 直播任务中的【点赞】任务支持等待开播后再点赞,【发弹幕】任务支持等待下播后再发弹幕 +- 直播任务【点亮熄灭勋章】、【点赞】、【发弹幕】和【观看直播】完成后会调用接口检查粉丝团升级任务是否真的完成了,若实际未完成不标记完成状态,下次运行时会再次尝试完成 ### 修复 diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 5c4b5b90..69cd53fa 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -17,6 +17,14 @@ import _ from 'lodash' class MedalModule extends BaseModule { /** 最大弹幕补发次数 */ protected static readonly DANMU_RETRY_LIMIT = 3 + /** 粉丝团升级任务 jump_type 对应的中文文案 */ + private static readonly TASK_ACTION_TEXT_MAP: Record = { + like: '点赞', + sendDanmu: '发弹幕', + watchLive: '观看直播', + feedLight: '投喂粉丝灯牌', // unused + sendGift: '投喂礼物', // unused + } /** 等待开播/下播时单房间探测的间隔 */ protected static readonly WAIT_POLL_INTERVAL = 120_000 /** 单房间探测时相邻请求间的随机延迟 */ @@ -85,7 +93,7 @@ class MedalModule extends BaseModule { /** * 简单限流队列 * - * 串行执行获取任务信息请求 + * 串行执行获取粉丝团升级任务信息请求 */ private static taskInfoRequestQueue: Promise = Promise.resolve() /** 粉丝勋章数据在多久内视为新鲜(毫秒) */ @@ -145,14 +153,12 @@ class MedalModule extends BaseModule { } /** - * 解析每日任务进度文案 "每日上限 X/Y" + * 从粉丝团升级任务 sub_title 解析每日任务进度文案 "每日上限 X/Y" * * @param sub_title API 返回的 sub_title * @returns 解析成功返回 `{ current, limit }`,否则返回 `null` */ - protected static parseDailyLimit( - sub_title: string | undefined, - ): { current: number; limit: number } | null { + protected static parseDailyLimit(sub_title?: string): { current: number; limit: number } | null { if (!sub_title) return null const match = sub_title.match(/(\d+)\s*\/\s*(\d+)/) if (!match) return null @@ -160,7 +166,7 @@ class MedalModule extends BaseModule { } /** - * 按 jump_type 在粉丝勋章任务信息 task_info 中查找任务项 + * 按 jump_type 在粉丝团升级任务信息 task_info 中查找任务项 */ protected static findTaskInfo( task_info: LiveData.GetActivatedMedalInfo.TaskInfo[] | null, @@ -170,11 +176,11 @@ class MedalModule extends BaseModule { } /** - * 从任务 title 中解析数字(如 "点赞30次" → 30,"发弹幕10次" → 10) + * 从粉丝团升级任务 title 中解析数字(如 "点赞30次" → 30,"发弹幕10次" → 10) * * @returns 解析成功返回数字,否则返回 null */ - protected static parseTitleCount(title: string | undefined): number | null { + protected static parseTitleCount(title?: string): number | null { if (!title) return null const match = title.match(/\d+/) if (!match) return null @@ -182,7 +188,7 @@ class MedalModule extends BaseModule { } /** - * 将获取任务信息请求加入全局串行队列 + * 将获取粉丝团升级任务信息请求加入全局串行队列 */ private static enqueueTaskInfoRequest(requester: () => Promise): Promise { const task = MedalModule.taskInfoRequestQueue.catch(() => {}).then(() => requester()) @@ -429,12 +435,13 @@ class MedalModule extends BaseModule { } /** - * 完成某粉丝勋章任务后,重新调用 fetchMedalData 检查储蓄亲密度,若存在则提示用户 + * 记录储蓄亲密度信息日志 */ - protected async logFreeIntimacy(medal: LiveData.FansMedalPanel.List): Promise { - const data = await this.fetchMedalData(medal.medal.target_id) - - if (data && data.free_intimacy > 0) { + private logFreeIntimacyFromData( + medal: LiveData.FansMedalPanel.List, + data: LiveData.GetActivatedMedalInfo.Data, + ): void { + if (data.free_intimacy > 0) { const reachLimitText = data.reach_free_intimacy_limit ? '(已达到储蓄亲密度上限)' : '' this.logger.log( `粉丝勋章【${medal.medal.medal_name}】储蓄了 ${data.free_intimacy} 亲密度${reachLimitText},投喂一个粉丝灯牌即可领取这些亲密度`, @@ -442,6 +449,46 @@ class MedalModule extends BaseModule { } } + /** + * 等待B站更新任务状态后,重新获取粉丝团升级任务信息确认任务是否真的完成 + * + * @returns 任务实际是否完成 + */ + protected async confirmTaskCompletedAfterUpdate( + medal: LiveData.FansMedalPanel.List, + jump_type: TaskJumpType, + ): Promise { + await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) + + const actionText = MedalModule.TASK_ACTION_TEXT_MAP[jump_type] + const data = await this.fetchMedalData(medal.medal.target_id) + if (!data) { + this.logger.error( + `粉丝勋章【${medal.medal.medal_name}】${actionText}后,无法获取最新粉丝团升级任务信息确认任务是否完成,默认已完成`, + ) + return true + } + + this.logFreeIntimacyFromData(medal, data) + + const item = MedalModule.findTaskInfo(data.task_info, jump_type) + if (!item) { + this.logger.error( + `粉丝勋章【${medal.medal.medal_name}】${actionText}后,最新粉丝团升级任务信息中缺少对应任务,默认已完成`, + ) + return true + } + + if (!item.is_done) { + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】已执行 ${actionText} 任务,但B站仍认为该任务未完成(实际进度:${item.sub_title}),下次运行会继续尝试`, + ) + return false + } + + return true + } + /** * 若点亮熄灭勋章任务已启用且今天未完成,等待其结束后再继续 * diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 3971d48f..2e0665d3 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -5,7 +5,7 @@ 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 } from './types' +import type { GroupedMedals, TaskExecutionResult } from './types' class DanmuTask extends MedalModule { config = this.medalTasksConfig.danmu @@ -91,15 +91,15 @@ class DanmuTask extends MedalModule { /** * 执行单个直播间的发弹幕任务 * - * @returns 是否中断任务 + * @returns 执行结果,包含是否中断以及是否确认完成 */ private async executeDanmuTask( medal: LiveData.FansMedalPanel.List, danmuIndexRef: { value: number }, - ): Promise { + ): Promise { if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') - return true + return { interrupted: true, verifiedCompleted: false } } const medalData = await this.fetchMedalData(medal.medal.target_id) @@ -107,7 +107,7 @@ class DanmuTask extends MedalModule { this.logger.error( `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`, ) - return false + return { interrupted: false, verifiedCompleted: true } } const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') @@ -115,20 +115,20 @@ class DanmuTask extends MedalModule { this.logger.error( `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, ) - return false + return { interrupted: false, verifiedCompleted: true } } - if (item.is_done) return false + if (item.is_done) return { interrupted: false, verifiedCompleted: true } const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, ) - return false + return { interrupted: false, verifiedCompleted: true } } - if (parsed.current >= parsed.limit) return false + if (parsed.current >= parsed.limit) return { interrupted: false, verifiedCompleted: true } const target = this.config.useTargetRounds ? Math.min(parsed.limit, this.config.targetRounds) @@ -140,7 +140,7 @@ class DanmuTask extends MedalModule { for (let j = 0; j < remaining; j++) { if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') - return true + return { interrupted: true, verifiedCompleted: false } } const danmuText = this.config.danmuList[danmuIndexRef.value++ % this.config.danmuList.length] @@ -160,30 +160,39 @@ class DanmuTask extends MedalModule { } if (hasSuccessfulDanmu) { - sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + return { + interrupted: false, + verifiedCompleted: await this.confirmTaskCompletedAfterUpdate(medal, 'sendDanmu'), + } } - return false + + return { interrupted: false, verifiedCompleted: true } } /** * 顺序执行多个直播间的发弹幕任务 * - * @returns 是否中断任务 + * @returns 执行结果,包含是否中断以及是否都确认完成 */ private async executeDanmuTasks( medals: LiveData.FansMedalPanel.List[], danmuIndexRef: { value: number }, - ): Promise { + ): Promise { + let verifiedCompleted = true + for (let i = 0; i < medals.length; i++) { - const isInterrupted = await this.executeDanmuTask(medals[i], danmuIndexRef) - if (isInterrupted) return true + const result = await this.executeDanmuTask(medals[i], danmuIndexRef) + if (result.interrupted) return result + if (!result.verifiedCompleted) { + verifiedCompleted = false + } if (i < medals.length - 1) { await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) } } - return false + return { interrupted: false, verifiedCompleted } } public async run(): Promise { @@ -204,8 +213,10 @@ class DanmuTask extends MedalModule { const danmuIndexRef = { value: 0 } let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) - const isInitialInterrupted = await this.executeDanmuTasks(readyMedals, danmuIndexRef) - if (isInitialInterrupted) { + const initialResult = await this.executeDanmuTasks(readyMedals, danmuIndexRef) + if (initialResult.interrupted) { + allCompleted = false + } else if (!initialResult.verifiedCompleted) { allCompleted = false } @@ -221,8 +232,12 @@ class DanmuTask extends MedalModule { pendingRoomids = nextPendingRoomids - const isWaitInterrupted = await this.executeDanmuTasks(newlyReadyMedals, danmuIndexRef) - if (isWaitInterrupted) { + const waitResult = await this.executeDanmuTasks(newlyReadyMedals, danmuIndexRef) + if (waitResult.interrupted) { + allCompleted = false + break + } + if (!waitResult.verifiedCompleted) { allCompleted = false break } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index 50e7f328..c6c0b08f 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -51,7 +51,7 @@ class LightTask extends MedalModule { } /** - * 点亮任务完成后,是否仍有任务需要最新的粉丝勋章点亮状态 + * 点亮任务完成后,是否仍有下游任务需要最新的粉丝勋章点亮状态 */ private shouldRefreshFansMedals(): boolean { const { like, danmu, watch } = this.medalTasksConfig @@ -60,6 +60,34 @@ class LightTask extends MedalModule { return downstreamTasks.some((task) => task.enabled && !isTimestampToday(task._lastCompleteTime)) } + /** + * 校验本轮尝试点亮的粉丝勋章是否都已点亮 + */ + private verifyLightedMedals(medals: LiveData.FansMedalPanel.List[]): boolean { + const latestMedalMap = useBiliStore().filteredFansMedalsMap + let allVerified = true + + medals.forEach((medal) => { + const latestMedal = latestMedalMap.get(medal.room_info.room_id) + if (!latestMedal) { + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】点亮后,未在刷新后的粉丝勋章列表中找到该勋章,忽略该勋章`, + medal, + ) + return + } + + if (latestMedal.medal.is_lighted !== 1) { + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】已执行点亮任务,但刷新粉丝勋章后仍未点亮,下次运行会继续尝试点亮`, + ) + allVerified = false + } + }) + + return allVerified + } + /** * 点赞 * @param medal 粉丝勋章 @@ -125,7 +153,11 @@ class LightTask extends MedalModule { * 给正在直播的直播间点赞 * @param medals */ - private async likeTask(medals: LiveData.FansMedalPanel.List[]): Promise { + private async likeTask( + medals: LiveData.FansMedalPanel.List[], + ): Promise { + const attemptedMedals: LiveData.FansMedalPanel.List[] = [] + for (let i = 0; i < medals.length; i++) { const medal = medals[i] const medalData = await this.fetchMedalData(medal.medal.target_id) @@ -139,19 +171,25 @@ class LightTask extends MedalModule { const item = MedalModule.findTaskInfo(medalData.task_info, 'like') const times = MedalModule.parseTitleCount(item?.title) ?? 30 await this.like(medal, times) + attemptedMedals.push(medal) if (i < medals.length - 1) { await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) } } + + return attemptedMedals } /** * 在未开播的直播间发弹幕 * @param medals */ - private async sendDanmuTask(medals: LiveData.FansMedalPanel.List[]): Promise { + private async sendDanmuTask( + medals: LiveData.FansMedalPanel.List[], + ): Promise { let danmuIndex = 0 + const attemptedMedals: LiveData.FansMedalPanel.List[] = [] for (let i = 0; i < medals.length; i++) { const medal = medals[i] @@ -173,7 +211,7 @@ class LightTask extends MedalModule { if (!(await this.sendDanmu(medal, danmuText))) { if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) - await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) + if (i < medals.length - 1) await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) break } target++ @@ -183,7 +221,11 @@ class LightTask extends MedalModule { await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) } } + + attemptedMedals.push(medal) } + + return attemptedMedals } public async run(): Promise { @@ -200,19 +242,42 @@ class LightTask extends MedalModule { const { notLivingMedals, livingMedals } = this.getMedals() // 是否有需要点亮的粉丝勋章(是不是一次有效运行?) const isEffectiveRun = notLivingMedals.length > 0 || livingMedals.length > 0 + let allCompleted = true + if (isEffectiveRun) { - await Promise.allSettled([this.sendDanmuTask(notLivingMedals), this.likeTask(livingMedals)]) - } + const [danmuResult, likeResult] = await Promise.allSettled([ + this.sendDanmuTask(notLivingMedals), + this.likeTask(livingMedals), + ]) + // 尝试点亮过的粉丝勋章 + const attemptedMedals = [ + ...(danmuResult.status === 'fulfilled' ? danmuResult.value : []), + ...(likeResult.status === 'fulfilled' ? likeResult.value : []), + ] - this.config._lastCompleteTime = tsm() + if (attemptedMedals.length > 0 || this.shouldRefreshFansMedals()) { + // 刷新粉丝勋章,确保 点亮任务自身以及 点赞/发弹幕/观看直播 任务都能拿到最新勋章状态 + await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) + const refreshed = await this.refreshFansMedals() - if (isEffectiveRun && this.shouldRefreshFansMedals()) { - // 刷新粉丝勋章,确保 点赞/发弹幕/观看直播 任务运行时能获取到最新的点亮状态和直播状态 - await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) - await this.refreshFansMedals() + if (attemptedMedals.length > 0) { + if (refreshed) { + allCompleted = this.verifyLightedMedals(attemptedMedals) + } else { + this.logger.warn('无法确认点亮任务是否完成,本轮按未完成处理') + allCompleted = false + } + } + } + } + + if (allCompleted) { + this.config._lastCompleteTime = tsm() + this.status = 'done' + this.logger.log('点亮熄灭勋章任务已完成') + } else { + this.status = '' } - this.status = 'done' - this.logger.log('点亮熄灭勋章任务已完成') } else { if (isNowBefore(0, 5)) { this.logger.log('昨天的给点亮熄灭勋章任务已经完成过了,等到今天的00:05再执行') diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index 756f503e..bfc81b07 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -5,7 +5,7 @@ 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 } from './types' +import type { GroupedMedals, TaskExecutionResult } from './types' class LikeTask extends MedalModule { config = this.medalTasksConfig.like @@ -79,12 +79,12 @@ class LikeTask extends MedalModule { /** * 执行单个直播间的点赞任务 * - * @returns 是否中断任务 + * @returns 执行结果,包含是否中断以及是否确认完成 */ - private async executeLikeTask(medal: LiveData.FansMedalPanel.List): Promise { + private async executeLikeTask(medal: LiveData.FansMedalPanel.List): Promise { if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') - return true + return { interrupted: true, verifiedCompleted: false } } const medalData = await this.fetchMedalData(medal.medal.target_id) @@ -92,7 +92,7 @@ class LikeTask extends MedalModule { this.logger.error( `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`, ) - return false + return { interrupted: false, verifiedCompleted: true } } const item = MedalModule.findTaskInfo(medalData.task_info, 'like') @@ -100,20 +100,20 @@ class LikeTask extends MedalModule { this.logger.error( `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, ) - return false + return { interrupted: false, verifiedCompleted: true } } - if (item.is_done) return false + if (item.is_done) return { interrupted: false, verifiedCompleted: true } const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的点赞任务的每日上限信息,跳过点赞任务`, ) - return false + return { interrupted: false, verifiedCompleted: true } } - if (parsed.current >= parsed.limit) return false + if (parsed.current >= parsed.limit) return { interrupted: false, verifiedCompleted: true } const times = MedalModule.parseTitleCount(item.title) ?? 30 const target = this.config.useTargetRounds @@ -124,7 +124,7 @@ class LikeTask extends MedalModule { for (let j = 0; j < remaining; j++) { if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') - return true + return { interrupted: true, verifiedCompleted: false } } if (await this.like(medal, times)) { @@ -137,27 +137,38 @@ class LikeTask extends MedalModule { } if (hasSuccessfulLike) { - sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + return { + interrupted: false, + verifiedCompleted: await this.confirmTaskCompletedAfterUpdate(medal, 'like'), + } } - return false + + return { interrupted: false, verifiedCompleted: true } } /** * 顺序执行多个直播间的点赞任务 * - * @returns 是否中断任务 + * @returns 执行结果,包含是否中断以及是否都确认完成 */ - private async executeLikeTasks(medals: LiveData.FansMedalPanel.List[]): Promise { + private async executeLikeTasks( + medals: LiveData.FansMedalPanel.List[], + ): Promise { + let verifiedCompleted = true + for (let i = 0; i < medals.length; i++) { - const isInterrupted = await this.executeLikeTask(medals[i]) - if (isInterrupted) return true + const result = await this.executeLikeTask(medals[i]) + if (result.interrupted) return result + if (!result.verifiedCompleted) { + verifiedCompleted = false + } if (i < medals.length - 1) { await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) } } - return false + return { interrupted: false, verifiedCompleted } } public async run(): Promise { @@ -177,8 +188,10 @@ class LikeTask extends MedalModule { let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) let allCompleted = true - const isInitialInterrupted = await this.executeLikeTasks(readyMedals) - if (isInitialInterrupted) { + const initialResult = await this.executeLikeTasks(readyMedals) + if (initialResult.interrupted) { + allCompleted = false + } else if (!initialResult.verifiedCompleted) { allCompleted = false } @@ -194,8 +207,12 @@ class LikeTask extends MedalModule { pendingRoomids = nextPendingRoomids - const isWaitInterrupted = await this.executeLikeTasks(newlyReadyMedals) - if (isWaitInterrupted) { + const waitResult = await this.executeLikeTasks(newlyReadyMedals) + if (waitResult.interrupted) { + allCompleted = false + break + } + if (!waitResult.verifiedCompleted) { allCompleted = false break } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 3bdb31ee..4141563d 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -26,6 +26,11 @@ interface WaitProbeResult { pendingRoomids: number[] } +interface TaskExecutionResult { + interrupted: boolean + verifiedCompleted: boolean +} + export { MedalTaskSharedConfig, SharedMedalFilters, @@ -33,4 +38,5 @@ export { TaskJumpType, WaitStrategy, WaitProbeResult, + TaskExecutionResult, } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index c939dc84..a26c7b62 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -331,11 +331,12 @@ class WatchTask extends MedalModule { const fansMedals = this.getMedals() if (fansMedals.length > 0) { - let i: number + let allCompleted = true - for (i = 0; i < fansMedals.length; i++) { + for (let i = 0; i < fansMedals.length; i++) { if (isNowAfter(23, 55) || isNowBefore(0, 5)) { this.logger.log('即将或刚刚发生跨天,提早结束本轮观看直播任务') + allCompleted = false break } @@ -402,13 +403,20 @@ class WatchTask extends MedalModule { ).start() if (hasWatchingProgress) { - sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY).then(() => this.logFreeIntimacy(medal)) + const verifiedCompleted = await this.confirmTaskCompletedAfterUpdate( + medal, + 'watchLive', + ) + if (!verifiedCompleted) { + allCompleted = false + } + } else { + allCompleted = false } } } - if (i === fansMedals.length) { - // 没有提早跳出循环,说明所有直播间的观看任务均已完成 + if (allCompleted) { this.config._lastCompleteTime = tsm() this.logger.log('观看直播任务已完成') this.status = 'done' From 994c6a4efda2892c4d7d2bd2990d88920b0ec66e Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 02:39:21 +0800 Subject: [PATCH 18/30] =?UTF-8?q?=F0=9F=93=83=20docs:=20CLAUDE.md=E5=92=8C?= =?UTF-8?q?CONTRIBUTING.md=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 29 +++++++++++++++++++++-------- CONTRIBUTING.md | 20 ++++++++++---------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2d5b8b1f..093b2afb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,25 +1,38 @@ # 项目概述 -BLTH(Bilibili Live Tasks Helper)是基于 Vue 3 + Vite + vite-plugin-monkey + Element Plus + TypeScript 的用户脚本,运行在 Bilibili 直播页面(`live.bilibili.com/直播间号`),实现自动完成每日任务、观看体验优化和页面元素清理。 +BLTH(Bilibili Live Tasks Helper)是基于 Vue 3 + Vite + vite-plugin-monkey + Element Plus + TypeScript 的**用户脚本(Userscript)**,通过用户脚本管理器(Tampermonkey/Violentmonkey)运行在 Bilibili 直播页面(`live.bilibili.com`),实现自动完成每日任务、观看体验优化和页面元素清理。 # 常用命令 ```sh -npm run dev # 启动 Vite 开发服务器(watch模式生成用户脚本) +npm run dev # 启动 Vite 开发服务器并安装脚本(支持 HMR) npm run build # type-check 与 build+minify 并行 npm run type-check # 仅 Vue TypeScript 类型检查 npm run lint # ESLint + Stylelint 自动修复 npm run format # 使用 Prettier 格式化 src/ -npm run preview # 提供编译产物安装链接的本地预览页 +npm run preview # 打开提供编译产物安装链接的本地预览网页 ``` 无自动化测试。 # 架构 -- 所有功能模块继承 `src/modules/BaseModule.ts`,由 `useModuleStore` 管理生命周期 -- `main.ts` 分两阶段加载模块:先加载早期模块,再在 `document-body` 后加载默认模块 -- 脚本分为三类:Main,SubMain,Other。同时打开多个直播间页面时,`useCacheStore.checkCurrentScriptType()` 会选出唯一 Main,避免部分模块重复执行 +## 模块系统 + +所有模块继承 `src/modules/BaseModule.ts`,由 `src/stores/useModuleStore.ts` 管理生命周期。 + +模块分两类: + +- **默认模块**(`src/modules/default/`):做准备工作,如基础数据获取,先于大多数功能模块加载 +- **功能模块**:实现具体业务逻辑,如完成每日任务 + +## 配置持久化 + +`src/library/storage/` 通过 `GM_setValue`/`GM_getValue` 存取配置。 + +## 多标签页安全 + +脚本分三类:`Main`,`SubMain`,`Other`。同时打开多个直播间页面时,`src/stores/useCacheStore.ts` 会选出唯一 `Main` 脚本,避免部分模块重复执行。 # 分支 @@ -34,5 +47,5 @@ npm run preview # 提供编译产物安装链接的本地预览页 # 易踩坑点 -- 新增第三方库须在 `vite.config.ts` 的 `externalGlobals` / `externalResource` 中通过 `@require` / `@resource` 引入,不打包进脚本 -- 调试 `runAt: 'document-start'` 等早期模块时 dev 模式注入太晚,请用 `npm run build` 的产物测试 +- 新增第三方库须写进 `vite.config.ts` 的 `externalGlobals`/`externalResource` 中,使其通过 `@require`/`@resource` 引入,不打包进脚本 +- 调试早期模块(`runAt: RunAtMoment = 'document-start'`)时 dev 模式注入太晚,请用 `npm run build` 的产物测试 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47e84f93..9a0eb898 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,8 +55,8 @@ npm run dev ### 分支 -- **master**: `master` 分支是发布分支,准备发版时会通过 Pull Request 将 `dev` 合并到 `master`,合并后会触发 GitHub Actions 构建并创建 Release。 -- **dev**: `dev` 分支是开发分支。日常开发请基于 `upstream/dev` 创建独立的功能分支(不要直接在 fork 的 `master` 上改动),发起 PR 时记得把目标分支设置为 `dev`。 +- **master**: `master` 分支是发布分支,准备发版时会通过 Pull Request 将 `dev` 合并到 `master`,合并后会触发 GitHub Actions 构建并创建 Release +- **dev**: `dev` 分支是开发分支。日常开发请基于 `upstream/dev` 创建独立的功能分支(不要直接在 fork 的 `master` 上改动),发起 PR 时记得把目标分支设置为 `dev` ### 目录结构 @@ -72,7 +72,7 @@ BLTH │ └─icons 图标组件 ├─library 库文件 │ ├─... 每个文件夹代表一个库,此处省略 - ├─modules 功能模块 + ├─modules 模块 │ ├─... 这里有很多模块,此处省略 │ └─default 默认模块,性质较特殊 ├─stores Pinia store @@ -85,7 +85,7 @@ BLTH 假设现在B站给主站每日任务新添加了一个任务:给视频点赞。 -首先打开 `src/modules` 文件夹,你会发现已经有了 `dailyTasks/mainSiteTasks` 这个文件夹,那么你只需在这个文件夹下新建一个 ts 文件即可。内容大概如下: +首先打开 `src/modules` 文件夹,你会发现已经有了 `dailyTasks/mainSiteTasks/` 这个文件夹,那么你只需在这个文件夹下新建一个 ts 文件即可。内容大概如下: ```ts import BaseModule from '@/modules/BaseModule' @@ -104,9 +104,9 @@ export default LikeTask 接着打开 `src/library/storage/defaultValue.ts`,加上该模块配置信息的默认值。 -点赞涉及到对B站API的请求。在 `src/library/bili-api` 中找找看有没有给视频点赞的API,如果没有得自己添加。参考已有的API即可。 +点赞涉及到对B站API的请求。在 `src/library/bili-api/` 中找找看有没有给视频点赞的API,如果没有得自己添加。参考已有的API即可。 -点赞的视频从哪来?如果仔细阅读代码你会发现 `src/stores/useBiliStore` 的 `dynamicVideos` 里已经存储了许多动态视频,你可以直接用。如果有特殊需求,自己获取视频当然也是可以的。 +点赞的视频从哪来?如果仔细阅读代码你会发现 `src/stores/useBiliStore.ts` 的 `dynamicVideos` 里已经存储了许多动态视频,你可以直接用。如果有特殊需求,自己获取视频当然也是可以的。 完成模块的编写后,在 `src/modules/dailyTasks/mainSiteTasks/index.ts` 中添加新模块的导出。 @@ -134,13 +134,13 @@ npm run format ### 强制性 -- 对于`src/library/bili-api`中的每个 API,必须为其请求参数和响应内容编写详细的类型定义。[transform](https://github.com/ritz078/transform) 和 [quicktype](https://github.com/quicktype/quicktype) 或许会有帮助。 -- 如果添加了新的第三方库或资源文件,请修改`vite.config.ts`使其通过`@require`或`@resource`引入。 +- 对于 `src/library/bili-api/` 中的每个 API,必须为其请求参数和响应内容编写详细的类型定义。[transform](https://github.com/ritz078/transform) 和 [quicktype](https://github.com/quicktype/quicktype) 或许会有帮助 +- 如果添加了新的第三方库或资源文件,请修改 `vite.config.ts` 使其通过 `@require` 或 `@resource` 引入 ### 建议性 -- 尽可能详细地写类型定义,必要时才使用`any`/`unknown`。 -- 为逻辑复杂的代码编写注释。 +- 尽可能详细地写类型定义,必要时才使用`any`/`unknown` +- 为逻辑复杂的代码编写注释 ## 编译 From 3c3067b7c073d34f8642f98475c4a75ee37d7293 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:53:38 +0800 Subject: [PATCH 19/30] =?UTF-8?q?=F0=9F=8E=88=20perf:=20=E5=8F=91=E5=BC=B9?= =?UTF-8?q?=E5=B9=95=E5=92=8C=E7=82=B9=E8=B5=9E=E6=A8=A1=E5=9D=97=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dailyTasks/liveTasks/medalTasks/MedalModule.ts | 14 +++++++++++--- .../dailyTasks/liveTasks/medalTasks/danmuTask.ts | 8 ++++++++ .../dailyTasks/liveTasks/medalTasks/likeTask.ts | 8 ++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 69cd53fa..426d5c71 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -303,7 +303,10 @@ class MedalModule extends BaseModule { continue } if (!this.SHARED_MEDAL_FILTERS.isLighted(medal)) { - this.logger.log(`直播间 ${roomid} 对应的粉丝勋章已熄灭,停止等待该房间`) + this.logger.log( + `粉丝勋章【${medal.medal.medal_name}】已熄灭,停止等待该房间(${roomid})`, + medal, + ) continue } @@ -315,7 +318,10 @@ class MedalModule extends BaseModule { : medal.room_info.living_status if (hasProbedStatus && _.isNil(liveStatus)) { - this.logger.warn(`直播间 ${roomid} 本轮实时探测失败,继续等待下次检查`) + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】对应的直播间 ${roomid} 本轮实时探测失败,继续等待下次检查`, + medal, + ) pendingRoomids.push(roomid) } else if (!_.isNil(liveStatus) && targetPredicate(liveStatus)) { readyMedals.push(medal) @@ -344,6 +350,8 @@ class MedalModule extends BaseModule { ): Promise { const biliStore = useBiliStore() + this.logger.log(`开始直播状态探测,待探测直播间数量:${roomids.length}`, { roomids }) + // 如果有进行中的全量刷新,等待其完成 if (MedalModule.ongoingFullRefreshPromise) { this.logger.log('等待进行中的粉丝勋章刷新完成...') @@ -359,7 +367,7 @@ class MedalModule extends BaseModule { // 数据不新鲜,决定探测策略 const strategy = this.decideWaitStrategy(roomids.length) this.logger.log( - `待处理房间 ${roomids.length} 个,粉丝勋章数据不新鲜,使用${strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, + `粉丝勋章数据不新鲜,使用${strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, ) if (strategy === 'refresh-fans-medals') { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 2e0665d3..60acc379 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -243,8 +243,16 @@ class DanmuTask extends MedalModule { } 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 + } + this.logger.log( `仍有 ${pendingRoomids.length} 个直播间未下播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + pendingRoomsInfo, ) await sleep(MedalModule.WAIT_POLL_INTERVAL) } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index bfc81b07..e681b416 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -218,8 +218,16 @@ class LikeTask extends MedalModule { } 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 + } + this.logger.log( `仍有 ${pendingRoomids.length} 个直播间未开播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + pendingRoomsInfo, ) await sleep(MedalModule.WAIT_POLL_INTERVAL) } From 8e84ae9ce1cbef2fd659d6d928da0d34d83dbb31 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:18:52 +0800 Subject: [PATCH 20/30] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E4=BF=AE=E5=A4=8Dl?= =?UTF-8?q?ightTask=E4=B8=ADfindTaskInfo=E6=9C=AA=E8=B7=B3=E8=BF=87?= =?UTF-8?q?=E5=8B=8B=E7=AB=A0=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=9B=AE=E6=A0=87=E8=BD=AE=E6=AC=A1=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91=E8=AF=AF=E5=88=A4?= =?UTF-8?q?=E6=9C=AA=E5=AE=8C=E6=88=90=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liveTasks/medalTasks/MedalModule.ts | 25 ++++++++++++++----- .../liveTasks/medalTasks/danmuTask.ts | 6 ++++- .../liveTasks/medalTasks/lightTask.ts | 20 ++++++++++++--- .../liveTasks/medalTasks/likeTask.ts | 6 ++++- .../liveTasks/medalTasks/watchTask.ts | 1 + 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 426d5c71..697b780a 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -460,11 +460,15 @@ class MedalModule extends BaseModule { /** * 等待B站更新任务状态后,重新获取粉丝团升级任务信息确认任务是否真的完成 * + * @param medal 粉丝勋章 + * @param jump_type 任务类型 + * @param requiredRounds 要求完成的目标轮次(仅"完成目标轮次"模式下需要传入) * @returns 任务实际是否完成 */ protected async confirmTaskCompletedAfterUpdate( medal: LiveData.FansMedalPanel.List, jump_type: TaskJumpType, + requiredRounds?: number, ): Promise { await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) @@ -487,14 +491,23 @@ class MedalModule extends BaseModule { return true } - if (!item.is_done) { - this.logger.warn( - `粉丝勋章【${medal.medal.medal_name}】已执行 ${actionText} 任务,但B站仍认为该任务未完成(实际进度:${item.sub_title}),下次运行会继续尝试`, - ) - return false + if (item.is_done) return true + + // "完成目标轮次"模式下,即使B站标为未完成,只要达到目标轮次也视为完成 + if (requiredRounds !== undefined) { + const parsed = MedalModule.parseDailyLimit(item.sub_title) + if (parsed && parsed.current >= requiredRounds) { + this.logger.log( + `粉丝勋章【${medal.medal.medal_name}】已达到目标轮次 ${requiredRounds}(实际进度:${item.sub_title}),视为完成`, + ) + return true + } } - return true + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】已执行 ${actionText} 任务,但B站仍认为该任务未完成(实际进度:${item.sub_title}),下次运行会继续尝试`, + ) + return false } /** diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 60acc379..f35ca40d 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -162,7 +162,11 @@ class DanmuTask extends MedalModule { if (hasSuccessfulDanmu) { return { interrupted: false, - verifiedCompleted: await this.confirmTaskCompletedAfterUpdate(medal, 'sendDanmu'), + verifiedCompleted: await this.confirmTaskCompletedAfterUpdate( + medal, + 'sendDanmu', + this.config.useTargetRounds ? target : undefined, + ), } } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index c6c0b08f..f1263c9d 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -168,8 +168,14 @@ class LightTask extends MedalModule { continue } - const item = MedalModule.findTaskInfo(medalData.task_info, 'like') - const times = MedalModule.parseTitleCount(item?.title) ?? 30 + const likeItem = MedalModule.findTaskInfo(medalData.task_info, 'like') + if (!likeItem) { + this.logger.error( + `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, + ) + continue + } + const times = MedalModule.parseTitleCount(likeItem.title) ?? 30 await this.like(medal, times) attemptedMedals.push(medal) @@ -201,8 +207,14 @@ class LightTask extends MedalModule { continue } - const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') - let target = MedalModule.parseTitleCount(item?.title) ?? 10 + const danmuItem = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') + if (!danmuItem) { + this.logger.error( + `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, + ) + continue + } + let target = MedalModule.parseTitleCount(danmuItem.title) ?? 10 let failedCount = 0 for (let j = 0; j < target; j++) { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index e681b416..d134d4e2 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -139,7 +139,11 @@ class LikeTask extends MedalModule { if (hasSuccessfulLike) { return { interrupted: false, - verifiedCompleted: await this.confirmTaskCompletedAfterUpdate(medal, 'like'), + verifiedCompleted: await this.confirmTaskCompletedAfterUpdate( + medal, + 'like', + this.config.useTargetRounds ? target : undefined, + ), } } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index a26c7b62..f9e800ec 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -406,6 +406,7 @@ class WatchTask extends MedalModule { const verifiedCompleted = await this.confirmTaskCompletedAfterUpdate( medal, 'watchLive', + this.config.useTargetRounds ? target : undefined, ) if (!verifiedCompleted) { allCompleted = false From 3a30acd75b54aa770597e3aed10179dcd5496337 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:29:22 +0800 Subject: [PATCH 21/30] =?UTF-8?q?=F0=9F=8E=88=20perf:=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=85=AC=E5=85=B1=E4=B8=B2=E8=A1=8C=E8=AF=B7=E6=B1=82=E9=99=90?= =?UTF-8?q?=E6=B5=81=E9=98=9F=E5=88=97=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liveTasks/medalTasks/MedalModule.ts | 73 +++++++++++-------- .../liveTasks/medalTasks/types.d.ts | 2 + 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 697b780a..1962079d 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -3,6 +3,7 @@ import { useBiliStore, useModuleStore } from '@/stores' import { watch } from 'vue' import type { MedalTaskSharedConfig, + RequestQueueKey, SharedMedalFilters, TaskJumpType, WaitProbeResult, @@ -91,17 +92,51 @@ class MedalModule extends BaseModule { }, ] /** - * 简单限流队列 - * - * 串行执行获取粉丝团升级任务信息请求 + * 串行执行请求的限流队列 */ - private static taskInfoRequestQueue: Promise = Promise.resolve() + private static readonly requestQueues: Record> = { + taskInfo: Promise.resolve(), + roomStatus: Promise.resolve(), + } /** 粉丝勋章数据在多久内视为新鲜(毫秒) */ private static readonly RECENT_FETCH_THRESHOLD = 90_000 /** 进行中的全量刷新粉丝勋章 Promise */ private static ongoingFullRefreshPromise: Promise | null = null - /** 单房间直播状态探测限流队列 */ - private static roomStatusProbeQueue: Promise = Promise.resolve() + + /** + * 将异步请求加入串行队列,相邻请求之间加入随机延迟 + */ + private static enqueueRequest( + queueKey: RequestQueueKey, + getDelay: () => Promise, + requester: () => Promise, + ): Promise { + const task = MedalModule.requestQueues[queueKey].catch(() => {}).then(() => requester()) + MedalModule.requestQueues[queueKey] = task.catch(() => {}).then(() => getDelay()) + return task + } + + /** + * 将获取粉丝团升级任务信息请求加入全局串行队列 + */ + private static enqueueTaskInfoRequest(requester: () => Promise): Promise { + return MedalModule.enqueueRequest( + 'taskInfo', + () => sleep(MedalModule.TASK_INFO_REQUEST_DYNAMIC_DELAY), + requester, + ) + } + + /** + * 将单房间直播状态探测请求加入串行限流队列 + */ + private static enqueueRoomStatusProbe(requester: () => Promise): Promise { + return MedalModule.enqueueRequest( + 'roomStatus', + () => sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY), + requester, + ) + } medalTasksConfig = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks @@ -187,32 +222,6 @@ class MedalModule extends BaseModule { return Number(match[0]) } - /** - * 将获取粉丝团升级任务信息请求加入全局串行队列 - */ - private static enqueueTaskInfoRequest(requester: () => Promise): Promise { - const task = MedalModule.taskInfoRequestQueue.catch(() => {}).then(() => requester()) - - MedalModule.taskInfoRequestQueue = task - .catch(() => {}) - .then(() => sleep(MedalModule.TASK_INFO_REQUEST_DYNAMIC_DELAY)) - - return task - } - - /** - * 将单房间直播状态探测请求加入串行限流队列 - */ - private static enqueueRoomStatusProbe(requester: () => Promise): Promise { - const task = MedalModule.roomStatusProbeQueue.catch(() => {}).then(() => requester()) - - MedalModule.roomStatusProbeQueue = task - .catch(() => {}) - .then(() => sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY)) - - return task - } - /** * 刷新粉丝勋章列表 * diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 4141563d..1d68e08a 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -18,6 +18,7 @@ interface SharedMedalFilters { type GroupedMedals = Record +type RequestQueueKey = 'taskInfo' | 'roomStatus' type TaskJumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' type WaitStrategy = 'single-probe' | 'refresh-fans-medals' @@ -35,6 +36,7 @@ export { MedalTaskSharedConfig, SharedMedalFilters, GroupedMedals, + RequestQueueKey, TaskJumpType, WaitStrategy, WaitProbeResult, From ac0ff33b959842e7eabe97a8ff45aa53a4af454d Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:47:32 +0800 Subject: [PATCH 22/30] =?UTF-8?q?=F0=9F=8E=88=20perf:=20=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts | 2 +- src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index f35ca40d..4d371a58 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -256,7 +256,7 @@ class DanmuTask extends MedalModule { this.logger.log( `仍有 ${pendingRoomids.length} 个直播间未下播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, - pendingRoomsInfo, + { pendingRoomsInfo }, ) await sleep(MedalModule.WAIT_POLL_INTERVAL) } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index d134d4e2..20c773e8 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -231,7 +231,7 @@ class LikeTask extends MedalModule { this.logger.log( `仍有 ${pendingRoomids.length} 个直播间未开播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, - pendingRoomsInfo, + { pendingRoomsInfo }, ) await sleep(MedalModule.WAIT_POLL_INTERVAL) } From 12756e57d986fa5c6f2be59dedebd1cd7ff2de29 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:59:25 +0800 Subject: [PATCH 23/30] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E3=80=90=E7=82=B9?= =?UTF-8?q?=E4=BA=AE=E7=86=84=E7=81=AD=E5=8B=8B=E7=AB=A0=E3=80=91=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E4=B8=8D=E5=BA=94=E8=AF=A5=E9=98=BB=E6=AD=A2?= =?UTF-8?q?=E3=80=90=E7=82=B9=E8=B5=9E=E3=80=91=E5=92=8C=E3=80=90=E5=8F=91?= =?UTF-8?q?=E5=BC=B9=E5=B9=95=E3=80=91=E9=87=8D=E6=96=B0=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 因为这两个任务有等点亮任务结束后再运行的功能 --- CHANGELOG.md | 2 +- src/stores/useModuleStore.ts | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe9921f..1a1cca97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ ### 调整 - 如果无法获取某个粉丝勋章的点亮任务,跳过该粉丝勋章 -- 手动重新运行【点亮熄灭勋章】、【点赞】和【发弹幕】任务时会检测是否与其它正在运行的模块冲突 +- 手动重新运行【点亮熄灭勋章】时任务时会检测【点赞】和【发弹幕】任务是否正在运行,如果是则不允许重新运行 ## [7.2.9] - 2026-5-24 diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index 4c8c8086..45c2508e 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -110,11 +110,6 @@ export const useModuleStore = defineStore('module', () => { rerunModule('DailyTask_LiveTask_LightTask') }, like: () => { - if (moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light === 'running') { - logger.warn('【点亮熄灭勋章】模块仍在运行中,无法重新运行【点赞】模块') - return - } - moduleStatus.value.DailyTasks.LiveTasks.medalTasks.like = '' moduleConfig.value.DailyTasks.LiveTasks.medalTasks.like._lastCompleteTime = 0 @@ -122,11 +117,6 @@ export const useModuleStore = defineStore('module', () => { rerunModule('DailyTask_LiveTask_LikeTask') }, danmu: () => { - if (moduleStatus.value.DailyTasks.LiveTasks.medalTasks.light === 'running') { - logger.warn('【点亮熄灭勋章】模块仍在运行中,无法重新运行【发弹幕】模块') - return - } - moduleStatus.value.DailyTasks.LiveTasks.medalTasks.danmu = '' moduleConfig.value.DailyTasks.LiveTasks.medalTasks.danmu._lastCompleteTime = 0 From e577a7c8ae86cc67b64bd202141b3f8763127e55 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:33:19 +0800 Subject: [PATCH 24/30] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E5=A4=A7=E9=87=8F?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E9=81=97=E7=95=99bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useModuleStore: clearStatus 遍历了错误的对象 - GetYearVipPrivilegeTask: 时间偏移量错误 - BAPI.main.coinAdd: url末尾多了一个空格 - fetch-hook: unProxy 删除了错误的处理器 - 主站任务watch,share,coin: biliStore.dynamicVideos缺少空数组判断 - RemoveGameParty: runOnMultiplePages属性拼写错误 - usePlayerStore: getPlayer和waitForLiveStatus会原地修改config参数 - useCacheStore: unsafeWindow.top可能为null --- src/library/bili-api/index.ts | 2 +- src/library/fetch-hook/index.ts | 16 ++++++++-------- .../liveTasks/medalTasks/danmuTask.ts | 1 + .../liveTasks/medalTasks/likeTask.ts | 2 ++ .../dailyTasks/mainSiteTasks/coinTask.ts | 2 +- .../dailyTasks/mainSiteTasks/shareTask.ts | 2 +- .../dailyTasks/mainSiteTasks/watchTask.ts | 2 +- .../otherTasks/getYearVipPrivilegeTask.ts | 2 +- src/modules/removeElement/removeGameParty.ts | 2 +- src/stores/useModuleStore.ts | 4 ++-- src/stores/usePlayerStore.ts | 19 +++++++++---------- 11 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/library/bili-api/index.ts b/src/library/bili-api/index.ts index caf46912..fa9cc934 100644 --- a/src/library/bili-api/index.ts +++ b/src/library/bili-api/index.ts @@ -353,7 +353,7 @@ const BAPI: BapiMethods = { ga = 1, ) => { const bili_jct = useBiliStore().cookies!.bili_jct - return request.main.post('/x/web-interface/coin/add ', { + return request.main.post('/x/web-interface/coin/add', { aid, multiply: num, select_like, diff --git a/src/library/fetch-hook/index.ts b/src/library/fetch-hook/index.ts index 819d0330..51246e6d 100644 --- a/src/library/fetch-hook/index.ts +++ b/src/library/fetch-hook/index.ts @@ -148,16 +148,16 @@ const fproxy = ( return { unProxy: () => { if (proxy.onRequest) { - onRequestHandlers.splice( - onRequestHandlers.findIndex((handler) => handler === proxy.onRequest), - 1, - ) + const index = onRequestHandlers.findIndex((handler) => handler === proxy.onRequest) + if (index !== -1) { + onRequestHandlers.splice(index, 1) + } } if (proxy.onResponse) { - onResponseHandlers.splice( - onResponseHandlers.findIndex((handler) => handler === proxy.onResponse), - 1, - ) + const index = onResponseHandlers.findIndex((handler) => handler === proxy.onResponse) + if (index !== -1) { + onResponseHandlers.splice(index, 1) + } } }, unHook: () => { diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 4d371a58..238accf9 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -170,6 +170,7 @@ class DanmuTask extends MedalModule { } } + // 所有发弹幕尝试均失败,估计是有什么异常状况,跳过 return { interrupted: false, verifiedCompleted: true } } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index 20c773e8..1dafd613 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -121,6 +121,7 @@ class LikeTask extends MedalModule { : parsed.limit const remaining = target - parsed.current let hasSuccessfulLike = false + for (let j = 0; j < remaining; j++) { if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') @@ -147,6 +148,7 @@ class LikeTask extends MedalModule { } } + // 所有点赞尝试均失败,估计是有什么异常状况,跳过 return { interrupted: false, verifiedCompleted: true } } diff --git a/src/modules/dailyTasks/mainSiteTasks/coinTask.ts b/src/modules/dailyTasks/mainSiteTasks/coinTask.ts index 787302cb..60170234 100644 --- a/src/modules/dailyTasks/mainSiteTasks/coinTask.ts +++ b/src/modules/dailyTasks/mainSiteTasks/coinTask.ts @@ -128,7 +128,7 @@ class CoinTask extends BaseModule { this.status = 'error' return false } - if (!biliStore.dynamicVideos) { + if (!biliStore.dynamicVideos || biliStore.dynamicVideos.length === 0) { this.logger.error('动态视频数据不存在,不执行每日投币任务') this.status = 'error' return false diff --git a/src/modules/dailyTasks/mainSiteTasks/shareTask.ts b/src/modules/dailyTasks/mainSiteTasks/shareTask.ts index 6de7b445..84b69797 100644 --- a/src/modules/dailyTasks/mainSiteTasks/shareTask.ts +++ b/src/modules/dailyTasks/mainSiteTasks/shareTask.ts @@ -45,7 +45,7 @@ class ShareTask extends BaseModule { return false } - if (!biliStore.dynamicVideos) { + if (!biliStore.dynamicVideos || biliStore.dynamicVideos.length === 0) { this.logger.error('动态视频数据不存在,不执行每日分享视频任务') this.status = 'error' return false diff --git a/src/modules/dailyTasks/mainSiteTasks/watchTask.ts b/src/modules/dailyTasks/mainSiteTasks/watchTask.ts index 4674716d..132702ba 100644 --- a/src/modules/dailyTasks/mainSiteTasks/watchTask.ts +++ b/src/modules/dailyTasks/mainSiteTasks/watchTask.ts @@ -45,7 +45,7 @@ class WatchTask extends BaseModule { this.status = 'error' return false } - if (!biliStore.dynamicVideos) { + if (!biliStore.dynamicVideos || biliStore.dynamicVideos.length === 0) { this.logger.error('动态视频数据不存在,不执行每日观看视频任务') this.status = 'error' return false diff --git a/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts b/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts index f86df1b4..2a1b47ec 100644 --- a/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts +++ b/src/modules/dailyTasks/otherTasks/getYearVipPrivilegeTask.ts @@ -212,7 +212,7 @@ class GetYearVipPrivilegeTask extends BaseModule { } } - const diff = this.config._nextReceiveTime - ts() + 3e5 // 增加5分钟延时 + const diff = this.config._nextReceiveTime - ts() + 300 // 增加5分钟延时 if (diff < 86400) { this.logger.log( '领取年度大会员权益模块下次运行时间:', diff --git a/src/modules/removeElement/removeGameParty.ts b/src/modules/removeElement/removeGameParty.ts index bd27c746..4a0f013c 100644 --- a/src/modules/removeElement/removeGameParty.ts +++ b/src/modules/removeElement/removeGameParty.ts @@ -3,7 +3,7 @@ import BaseModule from '@/modules/BaseModule' import { useModuleStore } from '@/stores' class RemoveGameParty extends BaseModule { - static runMultiple = true + static runOnMultiplePages = true config = useModuleStore().moduleConfig.RemoveElement.removeGameParty diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index 45c2508e..c7fcca7a 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -301,8 +301,8 @@ export const useModuleStore = defineStore('module', () => { */ ;(function clearStatus() { setTimeout(() => { - deepestIterate(moduleStatus, (_value: ModuleStatusTypes, path: string) => { - _.set(moduleStatus, path, '') + deepestIterate(moduleStatus.value, (_value: ModuleStatusTypes, path: string) => { + _.set(moduleStatus.value, path, '') }) clearStatus() }, delayToNextMoment(0, 0).ms) diff --git a/src/stores/usePlayerStore.ts b/src/stores/usePlayerStore.ts index 9e29fa97..bd179459 100644 --- a/src/stores/usePlayerStore.ts +++ b/src/stores/usePlayerStore.ts @@ -2,7 +2,6 @@ import { unsafeWindow } from '$' import { acceptHMRUpdate, defineStore } from 'pinia' import { waitForMoment } from '@/library/utils' import { ref } from 'vue' -import _ from 'lodash' export interface PlayerStoreConfig { getPlayer: { @@ -44,7 +43,7 @@ export const usePlayerStore = defineStore('player', () => { const getPlayer = async ( config: PlayerStoreConfig['getPlayer'] = DEFAULT_CONFIG.getPlayer, ): Promise => { - _.defaults(config, DEFAULT_CONFIG.getPlayer) + const mergedConfig = Object.assign({}, config, DEFAULT_CONFIG.getPlayer) if (player.value) { return player.value @@ -69,12 +68,12 @@ export const usePlayerStore = defineStore('player', () => { player.value = unsafeWindow.livePlayer resolve(player.value) } - }, config.interval) + }, mergedConfig.interval) const timeoutTimer = setTimeout(() => { clearInterval(findPlayerTimer) reject('等待播放器超时') - }, config.timeout) + }, mergedConfig.timeout) }) } @@ -89,18 +88,18 @@ export const usePlayerStore = defineStore('player', () => { status: 0 | 1, config: PlayerStoreConfig['waitForLiveStatus'] = DEFAULT_CONFIG.waitForLiveStatus, ): Promise => { - _.defaults(config, DEFAULT_CONFIG.waitForLiveStatus) + const mergedConfig = Object.assign({}, config, DEFAULT_CONFIG.waitForLiveStatus) const player = await getPlayer() const liveStatus = player.getPlayerInfo().liveStatus if (liveStatus === status) { - config.onImmediateSuccess?.() + mergedConfig.onImmediateSuccess?.() return true } - config.onNeedWait?.() + mergedConfig.onNeedWait?.() return new Promise((resolve) => { const liveStatusTimer = setInterval(() => { @@ -110,14 +109,14 @@ export const usePlayerStore = defineStore('player', () => { clearInterval(liveStatusTimer) resolve(true) } - }, config.interval) + }, mergedConfig.interval) const timeoutTimer = - config.timeout && + mergedConfig.timeout && setTimeout(() => { clearInterval(liveStatusTimer) resolve(false) - }, config.timeout) + }, mergedConfig.timeout) }) } From 77d8c816ab12f435ef06f159e6753a7b09cfb8bf Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:40:57 +0800 Subject: [PATCH 25/30] =?UTF-8?q?=F0=9F=8E=88=20perf:=20=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E7=BB=86=E8=8A=82=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/icons/InfoIcon.vue | 4 +- src/library/bili-api/index.ts | 2 +- src/library/error/ModuleCriticalError.ts | 3 - src/library/help-info/index.ts | 422 +++++++++--------- src/library/request/index.ts | 4 +- .../liveTasks/medalTasks/MedalModule.ts | 2 +- .../liveTasks/medalTasks/lightTask.ts | 9 +- .../dailyTasks/mainSiteTasks/loginTask.ts | 7 +- src/modules/default/bilibiliLive.ts | 4 +- vite.config.ts | 2 +- 10 files changed, 236 insertions(+), 223 deletions(-) diff --git a/src/components/icons/InfoIcon.vue b/src/components/icons/InfoIcon.vue index ac1150bd..2f6db8bf 100644 --- a/src/components/icons/InfoIcon.vue +++ b/src/components/icons/InfoIcon.vue @@ -13,8 +13,8 @@ const open = () => { const { title, message } = props.item ElMessageBox({ - title: title, - message: message, + title, + message, lockScroll: false, autofocus: true, confirmButtonText: 'OK', diff --git a/src/library/bili-api/index.ts b/src/library/bili-api/index.ts index fa9cc934..bf4a9853 100644 --- a/src/library/bili-api/index.ts +++ b/src/library/bili-api/index.ts @@ -8,7 +8,7 @@ import _ from 'lodash' const request: Requests = { live: new Request('https://api.live.bilibili.com', 'https://live.bilibili.com'), liveTrace: new Request('https://live-trace.bilibili.com', 'https://live.bilibili.com'), - passport: new Request('https://passport.bilibili.com', 'https://passport.bilibili.com/'), + passport: new Request('https://passport.bilibili.com', 'https://passport.bilibili.com'), main: new Request('https://api.bilibili.com', 'https://www.bilibili.com'), raw: new Request(), } diff --git a/src/library/error/ModuleCriticalError.ts b/src/library/error/ModuleCriticalError.ts index 92b68cc4..0b39ef21 100644 --- a/src/library/error/ModuleCriticalError.ts +++ b/src/library/error/ModuleCriticalError.ts @@ -5,12 +5,9 @@ import ModuleError from '@/library/error/ModuleError' */ class ModuleCriticalError extends ModuleError { public name: string = 'ModuleCriticalError' - public moduleName: string constructor(moduleName: string, message?: string) { super(moduleName, message) - - this.moduleName = moduleName } } diff --git a/src/library/help-info/index.ts b/src/library/help-info/index.ts index 5e153531..efd1eb0a 100644 --- a/src/library/help-info/index.ts +++ b/src/library/help-info/index.ts @@ -10,289 +10,305 @@ const helpInfo: HelpInfo = { }, watch: { title: '每日观看视频', - message: h('p', [ - h('div', '完成主站的每日观看视频任务。'), - h('div', '从动态中选取视频观看,会产生观看历史记录。'), - ]), + message: () => + h('p', [ + h('div', '完成主站的每日观看视频任务。'), + h('div', '从动态中选取视频观看,会产生观看历史记录。'), + ]), }, coin: { title: '每日投币', - message: h('p', [ - h('div', '完成主站的每日投币任务。'), - h('div', '从动态中选取视频投币,会根据你今天已经投过的币的数量计算还要投几个币。'), - ]), + message: () => + h('p', [ + h('div', '完成主站的每日投币任务。'), + h('div', '从动态中选取视频投币,会根据你今天已经投过的币的数量计算还要投几个币。'), + ]), }, share: { title: '每日分享视频', - message: h('p', [ - h('div', '完成主站的每日分享视频任务。'), - h('div', '不会真的分享到某处。'), - ]), + message: () => + h('p', [h('div', '完成主站的每日分享视频任务。'), h('div', '不会真的分享到某处。')]), }, }, LiveTasks: { medalTasks: { list: { title: '黑白名单 / 排序模式', - message: h('p', [ - h('div', '每个直播任务都有自己独立的黑名单或白名单。'), - h('ul', [ - h('li', [ - h('span', '黑名单:仅为'), - h('strong', '不在'), - h('span', '名单中的粉丝勋章执行任务。'), + message: () => + h('p', [ + h('div', '每个直播任务都有自己独立的黑名单或白名单。'), + h('ul', [ + h('li', [ + h('span', '黑名单:仅为'), + h('strong', '不在'), + h('span', '名单中的粉丝勋章执行任务。'), + ]), + h('li', [ + h('span', '白名单:仅为'), + h('strong', '在'), + h('span', '名单中的粉丝勋章执行任务。'), + ]), ]), - h('li', [ - h('span', '白名单:仅为'), - h('strong', '在'), - h('span', '名单中的粉丝勋章执行任务。'), + h('div', '点击【编辑名单】按钮,然后使用第一列的多选框即可编辑名单中的粉丝勋章。'), + h( + 'div', + '使用白名单模式时,点击【编辑粉丝勋章名单】窗口右下角的开关即可在【常规模式】和【排序模式】之间切换。', + ), + h('div', '在排序模式下,你可以调整脚本执行任务的粉丝勋章顺序。'), + h('ul', [ + h('li', '使用鼠标拖拽表格中的行来调整顺序。'), + h('li', '拖拽行至表格的顶部和底部可以触发滚动。'), ]), ]), - h('div', '点击【编辑名单】按钮,然后使用第一列的多选框即可编辑名单中的粉丝勋章。'), - h( - 'div', - '使用白名单模式时,点击【编辑粉丝勋章名单】窗口右下角的开关即可在【常规模式】和【排序模式】之间切换。', - ), - h('div', '在排序模式下,你可以调整脚本执行任务的粉丝勋章顺序。'), - h('ul', [ - h('li', '使用鼠标拖拽表格中的行来调整顺序。'), - h('li', '拖拽行至表格的顶部和底部可以触发滚动。'), - ]), - ]), }, light: { title: '点亮熄灭勋章', - message: h('p', [ - h('div', '在你的每个已熄灭的粉丝勋章对应的直播间完成点亮任务,从而点亮粉丝勋章。'), - h('div', '根据直播间是否开播,脚本会自动选择点亮方式:'), - h('ol', [ - h( - 'li', - h('p', [ - h('strong', '点赞'), - h('div', '对于正在直播的房间,给直播间点赞。'), - h('div', '脚本会根据 B 站返回的任务要求自动决定点赞次数。'), - ]), - ), - h( - 'li', - h('p', [ - h('strong', '发送弹幕'), - h('div', '对于未开播的房间,发送数条弹幕。'), - h( - 'div', - '脚本会根据 B 站返回的任务要求自动决定发送条数。发送失败时会自动小幅补发。', - ), - h('div', [ + message: () => + h('p', [ + h('div', '在你的每个已熄灭的粉丝勋章对应的直播间完成点亮任务,从而点亮粉丝勋章。'), + h('div', '根据直播间是否开播,脚本会自动选择点亮方式:'), + h('ol', [ + h( + 'li', + h('p', [ + h('strong', '点赞'), + h('div', '对于正在直播的房间,给直播间点赞。'), + h('div', '脚本会根据 B 站返回的任务要求自动决定点赞次数。'), + ]), + ), + h( + 'li', + h('p', [ + h('strong', '发送弹幕'), + h('div', '对于未开播的房间,发送数条弹幕。'), h( 'div', - '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。', + '脚本会根据 B 站返回的任务要求自动决定发送条数。发送失败时会自动小幅补发。', ), - h('div', '部分直播间无法完成该任务,可能的原因有:'), - h('ul', [ - h('li', '你被禁言了'), - h('li', '发言有粉丝勋章等级要求'), - h('li', '发送的弹幕包含屏蔽词'), - h('li', [ - h('span', '特殊直播间(比如'), - h( - 'a', - { - href: 'https://live.bilibili.com/54', - rel: 'noreferrer', - target: '_blank', - }, - '54', - ), - h('span', ')'), + h('div', [ + h( + 'div', + '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。', + ), + h('div', '部分直播间无法完成该任务,可能的原因有:'), + h('ul', [ + h('li', '你被禁言了'), + h('li', '发言有粉丝勋章等级要求'), + h('li', '发送的弹幕包含屏蔽词'), + h('li', [ + h('span', '特殊直播间(比如'), + h( + 'a', + { + href: 'https://live.bilibili.com/54', + rel: 'noreferrer', + target: '_blank', + }, + '54', + ), + h('span', ')'), + ]), ]), ]), ]), - ]), - ), - ]), - h('div', [ - h('strong', '注意:'), - h('ul', [ - h('li', '点赞、发弹幕和观看直播任务都会等点亮熄灭勋章任务完成后再执行。'), - h( - 'li', - '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄。需要投喂一个粉丝灯牌才能领取。', ), ]), + h('div', [ + h('strong', '注意:'), + h('ul', [ + h('li', '点赞、发弹幕和观看直播任务都会等点亮熄灭勋章任务完成后再执行。'), + h( + 'li', + '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄。需要投喂一个粉丝灯牌才能领取。', + ), + ]), + ]), ]), - ]), }, rounds: { title: '跟随每日上限 / 完成目标轮次', - message: h('p', [ - h( - 'div', - '一轮指通过完成任务获得 +1 亲密度所需的最小操作量。具体值脚本会在运行过程中实时获取。根据观察目前每轮的操作量如下:', - ), - h('ul', [ - h('li', '点赞:30 次'), - h('li', '发弹幕:1 条'), - h('li', '观看直播:15 分钟'), + message: () => + h('p', [ + h( + 'div', + '一轮指通过完成任务获得 +1 亲密度所需的最小操作量。具体值脚本会在运行过程中实时获取。根据观察目前每轮的操作量如下:', + ), + h('ul', [ + h('li', '点赞:30 次'), + h('li', '发弹幕:1 条'), + h('li', '观看直播:15 分钟'), + ]), + h( + 'div', + '选择【跟随每日上限】时,脚本会尽量做到任务的当日上限(任务上限也是实时获取的)。', + ), + h( + 'div', + '选择【完成目标轮次】可自定义脚本当天至多完成几轮;若填写值超过任务上限,则还是做到任务的当日上限。', + ), ]), - h( - 'div', - '选择【跟随每日上限】时,脚本会尽量做到任务的当日上限(任务上限也是实时获取的)。', - ), - h( - 'div', - '选择【完成目标轮次】可自定义脚本当天至多完成几轮;若填写值超过任务上限,则还是做到任务的当日上限。', - ), - ]), }, like: { title: '点赞', - message: h('p', [ - h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的点赞任务。'), - h('div', '只有主播开播时点赞才会计入任务进度。'), - ]), + message: () => + h('p', [ + h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的点赞任务。'), + h('div', '只有主播开播时点赞才会计入任务进度。'), + ]), }, likeWaitUntilLiving: { title: '等待开播后再点赞', - message: h('p', [ - h( - 'div', - '开启该功能后,对于尚未开播的直播间,脚本会等主播开播后再点赞。关闭时会直接跳过。', - ), - ]), + message: () => + h('p', [ + h( + 'div', + '开启该功能后,对于尚未开播的直播间,脚本会等主播开播后再点赞。关闭时会直接跳过。', + ), + ]), }, danmu: { title: '发弹幕', - message: h('p', [ - h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的发弹幕任务。'), - h('ul', [ - h( - 'li', - '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。', - ), - h('li', '弹幕发送失败(比如被屏蔽词拦截)会自动小幅补发。'), + message: () => + h('p', [ + h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的发弹幕任务。'), + h('ul', [ + h( + 'li', + '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。', + ), + h('li', '弹幕发送失败(比如被屏蔽词拦截)会自动小幅补发。'), + ]), ]), - ]), }, danmuWaitUntilNotLiving: { title: '等待下播后再发弹幕', - message: h('p', [ - h('div', '此选项只有在开启【仅在未开播的直播间发弹幕】后才会生效。'), - h('div', '只开启【仅在未开播的直播间发弹幕】时,脚本会直接跳过当前开播中的直播间。'), - h('div', '此时再开启【等待下播后再发弹幕】,脚本会等直播中的主播下播后再发弹幕。'), - ]), + message: () => + h('p', [ + h('div', '此选项只有在开启【仅在未开播的直播间发弹幕】后才会生效。'), + h('div', '只开启【仅在未开播的直播间发弹幕】时,脚本会直接跳过当前开播中的直播间。'), + h('div', '此时再开启【等待下播后再发弹幕】,脚本会等直播中的主播下播后再发弹幕。'), + ]), }, watch: { title: '观看直播', - message: h('p', [ - h('div', '为已点亮的粉丝勋章完成完成粉丝团升级任务中的观看直播任务。'), - h('ul', [ - h('li', '部分直播间因为没有设置直播分区导致任务无法完成。'), - h('li', '主播当前是否开播不会影响该任务的完成。'), - ]), - h('div', [ - h('strong', '注意:'), - h( - 'span', - '使用本功能时不能以任何方式观看直播(网页、APP、电视),否则可能无法获得任何亲密度。', - ), + message: () => + h('p', [ + h('div', '为已点亮的粉丝勋章完成完成粉丝团升级任务中的观看直播任务。'), + h('ul', [ + h('li', '部分直播间因为没有设置直播分区导致任务无法完成。'), + h('li', '主播当前是否开播不会影响该任务的完成。'), + ]), + h('div', [ + h('strong', '注意:'), + h( + 'span', + '使用本功能时不能以任何方式观看直播(网页、APP、电视),否则可能无法获得任何亲密度。', + ), + ]), ]), - ]), }, }, }, OtherTasks: { silverToCoin: { title: '银瓜子换硬币', - message: h('p', [ - h('div', '把银瓜子兑换为硬币。'), - h('div', '具体兑换规则请点击直播间页面右下角的“电池图标→银瓜子商店”查看。'), - ]), + message: () => + h('p', [ + h('div', '把银瓜子兑换为硬币。'), + h('div', '具体兑换规则请点击直播间页面右下角的“电池图标→银瓜子商店”查看。'), + ]), }, coinToSilver: { title: '硬币换银瓜子', - message: h('p', [ - h('div', '把硬币兑换为银瓜子。'), - h('div', '具体兑换规则请点击直播间页面右下角的“电池图标→银瓜子商店”查看。'), - ]), + message: () => + h('p', [ + h('div', '把硬币兑换为银瓜子。'), + h('div', '具体兑换规则请点击直播间页面右下角的“电池图标→银瓜子商店”查看。'), + ]), }, getYearVipPrivilege: { title: '领取年度大会员权益', - message: h('p', [ - h('div', '自动领取年度大会员权益。'), - h('div', [ - h('span', '具体权益请前往'), - h( - 'a', - { - href: 'https://account.bilibili.com/account/big/myPackage', - rel: 'noreferrer', - target: '_blank', - }, - '卡券包', - ), - h('span', '查看。'), + message: () => + h('p', [ + h('div', '自动领取年度大会员权益。'), + h('div', [ + h('span', '具体权益请前往'), + h( + 'a', + { + href: 'https://account.bilibili.com/account/big/myPackage', + rel: 'noreferrer', + target: '_blank', + }, + '卡券包', + ), + h('span', '查看。'), + ]), ]), - ]), }, }, }, EnhanceExperience: { switchLiveStreamQuality: { title: '自动切换画质', - message: h('p', [ - h('div', '打开直播间后自动把播放器切换到目标画质。'), - h('div', '如果目标画质不存在,先查找无HDR版本的画质,仍没有则使用B站的默认画质。'), - ]), + message: () => + h('p', [ + h('div', '打开直播间后自动把播放器切换到目标画质。'), + h('div', '如果目标画质不存在,先查找无HDR版本的画质,仍没有则使用B站的默认画质。'), + ]), }, banp2p: { title: '禁用P2P', - message: h('p', [ - h('div', '禁用直播流的P2P上传/下载'), - h( - 'div', - 'B站使用WebRTC技术把许多浏览器点对点(P2P)地连接起来,实现视频流和音频流的传输。这样做是为了减轻B站服务器的压力,但是会占用你一定的上行带宽(大约几百kb每秒)。如果你不想被占用上行带宽,可以开启该功能。若开启后发现观看直播时有明显卡顿,请关闭。', - ), - ]), + message: () => + h('p', [ + h('div', '禁用直播流的P2P上传/下载'), + h( + 'div', + 'B站使用WebRTC技术把许多浏览器点对点(P2P)地连接起来,实现视频流和音频流的传输。这样做是为了减轻B站服务器的压力,但是会占用你一定的上行带宽(大约几百kb每秒)。如果你不想被占用上行带宽,可以开启该功能。若开启后发现观看直播时有明显卡顿,请关闭。', + ), + ]), }, noReport: { title: '拦截日志数据上报', - message: h('p', [ - h('div', '禁止B站上报日志数据。'), - h('div', [ - h( - 'span', - 'B站会实时地上报大量日志信息,比如直播观看情况、代码报错等等。开启本功能后绝大多数日志上报都会被劫持或拦截并返回一个成功的响应。相比于带有广告拦截功能的浏览器拓展,比如', - ), - h( - 'a', - { - href: 'https://github.com/uBlockOrigin/uBOL-home', - rel: 'noreferrer', - target: '_blank', - }, - 'uBlock Origin Lite', - ), - h( - 'span', - ',本功能会通过劫持的方式从根源上阻止部分日志上报,并模拟成功的响应来尽可能地减少B站代码的报错。理论上来说这样做会有更好的性能表现。', - ), + message: () => + h('p', [ + h('div', '禁止B站上报日志数据。'), + h('div', [ + h( + 'span', + 'B站会实时地上报大量日志信息,比如直播观看情况、代码报错等等。开启本功能后绝大多数日志上报都会被劫持或拦截并返回一个成功的响应。相比于带有广告拦截功能的浏览器拓展,比如', + ), + h( + 'a', + { + href: 'https://github.com/uBlockOrigin/uBOL-home', + rel: 'noreferrer', + target: '_blank', + }, + 'uBlock Origin Lite', + ), + h( + 'span', + ',本功能会通过劫持的方式从根源上阻止部分日志上报,并模拟成功的响应来尽可能地减少B站代码的报错。理论上来说这样做会有更好的性能表现。', + ), + ]), ]), - ]), }, noSleep: { title: '屏蔽挂机检测', - message: h('p', [ - h('div', '屏蔽B站直播间的挂机检测。'), - h( - 'div', - '如果长时间没有操作,会提示“检测到您已离开当前屏幕,倒计时后即将暂停播放”。此外切换标签页或把浏览器切到后台导致直播间页面不可见时,播放器可能会自动切换到低画质或暂停播放。开启本功能即可避免这类情况。', - ), - ]), + message: () => + h('p', [ + h('div', '屏蔽B站直播间的挂机检测。'), + h( + 'div', + '如果长时间没有操作,会提示“检测到您已离开当前屏幕,倒计时后即将暂停播放”。此外切换标签页或把浏览器切到后台导致直播间页面不可见时,播放器可能会自动切换到低画质或暂停播放。开启本功能即可避免这类情况。', + ), + ]), }, invisibility: { title: '隐身入场', - message: h('p', [h('div', '进入直播间时其他人不会收到提示,但还是会出现在高能用户榜单上。')]), + message: () => + h('p', [h('div', '进入直播间时其他人不会收到提示,但还是会出现在高能用户榜单上。')]), }, }, RemoveElement: { diff --git a/src/library/request/index.ts b/src/library/request/index.ts index e40b879e..1e5590a2 100644 --- a/src/library/request/index.ts +++ b/src/library/request/index.ts @@ -10,9 +10,9 @@ class Request { */ private readonly origin: string - constructor(url_prefix?: string, orgin?: string) { + constructor(url_prefix?: string, origin?: string) { this.url_prefix = url_prefix ?? '' - this.origin = orgin ?? 'https://bilibili.com' + this.origin = origin ?? 'https://bilibili.com' } /** diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 1962079d..22f36918 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -270,7 +270,7 @@ class MedalModule extends BaseModule { */ protected decideWaitStrategy(pendingCount: number): WaitStrategy { const totalMedalCount = useBiliStore().filteredFansMedals.length - const fullRefreshCost = Math.ceil(totalMedalCount / 10) + const fullRefreshCost = Math.ceil(totalMedalCount / 10) // 每次请求能获取 10 个粉丝勋章 return pendingCount <= fullRefreshCost ? 'single-probe' : 'refresh-fans-medals' } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index f1263c9d..52c51491 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -214,10 +214,11 @@ class LightTask extends MedalModule { ) continue } - let target = MedalModule.parseTitleCount(danmuItem.title) ?? 10 + + let remaining = MedalModule.parseTitleCount(danmuItem.title) ?? 10 let failedCount = 0 - for (let j = 0; j < target; j++) { + for (let j = 0; j < remaining; j++) { const danmuText = this.config.danmuList[danmuIndex++ % this.config.danmuList.length] if (!(await this.sendDanmu(medal, danmuText))) { @@ -226,10 +227,10 @@ class LightTask extends MedalModule { if (i < medals.length - 1) await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) break } - target++ + remaining++ } - if (i < medals.length - 1 || j < target - 1) { + if (i < medals.length - 1 || j < remaining - 1) { await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) } } diff --git a/src/modules/dailyTasks/mainSiteTasks/loginTask.ts b/src/modules/dailyTasks/mainSiteTasks/loginTask.ts index 45fcdfe7..cd854d0d 100644 --- a/src/modules/dailyTasks/mainSiteTasks/loginTask.ts +++ b/src/modules/dailyTasks/mainSiteTasks/loginTask.ts @@ -10,7 +10,7 @@ class LoginTask extends BaseModule { useModuleStore().moduleStatus.DailyTasks.MainSiteTasks.login = s } - private async login(): Promise { + private login(): void { // 触发每日登录任务的请求是 BAPI.main.nav // 这个请求脚本在默认模块 userInfo 中每天都会发送 // 而且打开任意B站页面的时候都会有这个请求 @@ -19,10 +19,9 @@ class LoginTask extends BaseModule { // 记录当前时间戳 this.config._lastCompleteTime = tsm() this.status = 'done' - return Promise.resolve() } - public async run(): Promise { + public run(): void { this.logger.log('每日登录模块开始运行') // 上一次完成每日登录任务的时间不在今天 @@ -39,7 +38,7 @@ class LoginTask extends BaseModule { if (!biliStore.dailyRewardInfo!.login) { // 每日登录任务未完成 - await this.login() + this.login() } else { // 用户在运行脚本前已经完成了任务,也记录完成时间 this.config._lastCompleteTime = tsm() diff --git a/src/modules/default/bilibiliLive.ts b/src/modules/default/bilibiliLive.ts index 1cc83f36..c8cba906 100644 --- a/src/modules/default/bilibiliLive.ts +++ b/src/modules/default/bilibiliLive.ts @@ -19,8 +19,8 @@ class BilibiliLive extends BaseModule { return } unsafeWindow.BilibiliLive = new Proxy(unsafeWindow.BilibiliLive, { - set(target: Window['BilibiliLive'], prop: keyof Window['BilibiliLive'], value: never) { - target[prop] = value + set(target: Window['BilibiliLive'], prop: keyof Window['BilibiliLive'], value: any) { + Reflect.set(target, prop, value) // UID 是最后被赋值的属性,等 UID 被赋值后 BilibiliLive 的所有属性就都被初始化了 if (prop === 'UID') { // 取消代理,还原为 Object diff --git a/vite.config.ts b/vite.config.ts index 8121e5d6..060cf047 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,7 +32,7 @@ export default defineConfig({ downloadURL: 'https://github.com/andywang425/BLTH/releases/latest/download/bilibili-live-tasks-helper.min.user.js', icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDEuMDY1IiBoZWlnaHQ9IjEyNy43NDciIHZpZXdCb3g9IjAgMCAzNy4zMjQgMzMuOCI+PHBhdGggZmlsbD0iIzIwYjBlMyIgZD0iTTg2Ljk2MiAxMTIuMzMyYTIuNjYxIDIuNjYxIDAgMCAxIDIuMjYyIDAgNS41MzYgNS41MzYgMCAwIDEgMS4zODQgMS4wMTFsNS4zMjMgNC42NThoMy44MDVsNS4zMjMtNC42NThhNS41ODkgNS41ODkgMCAwIDEgMS4zODQtMS4wMTEgMi42NjEgMi42NjEgMCAwIDEgMy41NCAyLjIwOSAyLjY2MSAyLjY2MSAwIDAgMS0uNTg2IDEuNzgzIDE0Ljg3NyAxNC44NzcgMCAwIDEtMS4xNzEgMS4wNjUgNy42OTEgNy42OTEgMCAwIDEtLjc0NS42MTJoMy4zMjZhNS42NDIgNS42NDIgMCAwIDEgMy45MTIgMS43NTYgNS42NjkgNS42NjkgMCAwIDEgMS43ODQgMy45MTJ2MTUuMzAzYTEwLjc3OCAxMC43NzggMCAwIDEtLjEzNCAyLjMxNSA1LjkwOCA1LjkwOCAwIDAgMS0yLjY2IDMuNzI2IDUuNzIyIDUuNzIyIDAgMCAxLTMuMDYxLjg1Mkg4Ni4yMTdhMTEuMjg0IDExLjI4NCAwIDAgMS0yLjM5Ni0uMTMzIDUuODgyIDUuODgyIDAgMCAxLTMuNjcyLTIuNjYyIDUuNjk1IDUuNjk1IDAgMCAxLS45MDUtMy4wNnYtMTUuMTQzYTExLjkyMyAxMS45MjMgMCAwIDEgMC0yLjIwOSA1Ljg1NSA1Ljg1NSAwIDAgMSA1LjMyMy00LjczN2gzLjQ4NmMtLjU1OS0uNC0xLjAzOC0uODc4LTEuNTQ0LTEuMzA0YTIuNjYxIDIuNjYxIDAgMCAxLS44NTEtMi4xODMgMi42NjEgMi42NjEgMCAwIDEgMS4zMDQtMi4xMDJtLS42MTIgMTAuMzI2YTIuNjYxIDIuNjYxIDAgMCAwLTIuMTAzIDEuOTE2IDMuNTkzIDMuNTkzIDAgMCAwIDAgMS4wMTF2MTIuNTg4YTIuNjYxIDIuNjYxIDAgMCAwIDEuODM3IDIuNjYyIDMuNTEzIDMuNTEzIDAgMCAwIDEuMTQ0LjE4NmgyMS42MzdhMi42NjEgMi42NjEgMCAwIDAgMi41MjgtMS41NyAzLjcyNiAzLjcyNiAwIDAgMCAuMjY2LTEuNzU3di0xMS43MWE0LjQ3MSA0LjQ3MSAwIDAgMCAwLTEuMjc3IDIuNjYxIDIuNjYxIDAgMCAwLTEuNzMtMS44MSA0LjI4NSA0LjI4NSAwIDAgMC0xLjY1LS4yMzlIODcuNjAxYTguODg5IDguODg5IDAgMCAwLTEuMjUxIDB6bTAgMCIgc3R5bGU9InN0cm9rZS13aWR0aDouMDMzMDcyOSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTc5LjE5MyAtMTEyLjA4KSIvPjxwYXRoIGQ9Ik04OC45NyAxMjguNjM2Yy4zNjMuMzc3Ljc0NS43NDcgMS4wODggMS4xNDIuNTk3LjY4NyAxLjExOCAxLjE5NyAxLjY2NiAxLjgwOS0uMTI5LTEuMTE3IDEuMzA0LTEuMTk4LjA3NC0xLjc1Ny0uNDA4LjQxNy0uOTQxLjg4NC0xLjM2IDEuMjIzLS4zOTIuMzE2LS44NjMuNjctMS4yMzUuOTUyLTEuOTA3IDEuNDQzLjIyNiA0LjA1MyAyLjEzIDIuNjA3IDAgMCAyLTEuNTM1IDIuODA3LTIuMzAxLjQ0LS40MTcuNjgtLjk1Ni43Mi0xLjU5Mi4wNC0uNjU0LS41MzUtMS4yNC0uNzk0LTEuNDk4LS45Mi0uOTE0LTEuNzQzLTEuOTY4LTIuNTUtMi44MTItMS41NzUtMS44LTQuMTIuNDI4LTIuNTQ2IDIuMjI3ek0xMDYuOTc5IDEyOC42MzZjLS4zNjMuMzc3LS43NDUuNzQ3LTEuMDg4IDEuMTQyLS41OTcuNjg3LTEuMTE4IDEuMTk3LTEuNjY2IDEuODA5LjEyOS0xLjExNy0xLjMwNC0xLjE5OC0uMDc0LTEuNzU3LjQwOC40MTcuOTQxLjg4NCAxLjM2IDEuMjIzLjM5Mi4zMTYuODYzLjY3IDEuMjM1Ljk1MiAxLjkwNyAxLjQ0My0uMjI2IDQuMDUzLTIuMTMgMi42MDcgMCAwLTItMS41MzUtMi44MDctMi4zMDEtLjQ0LS40MTctLjY4LS45NTYtLjcyLTEuNTkyLS4wNC0uNjU0LjUzNS0xLjI0Ljc5NC0xLjQ5OC45Mi0uOTE0IDEuNzQzLTEuOTY4IDIuNTUtMi44MTIgMS41NzUtMS44IDQuMTIuNDI4IDIuNTQ2IDIuMjI3eiIgc3R5bGU9ImZpbGw6IzIwYjBlMztmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6LjUyNDE1OTtzdHJva2UtZGFzaGFycmF5Om5vbmUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC03OS4xOTMgLTExMi4wOCkiLz48L3N2Zz4NCg==', - copyright: '2025, andywang425 (https://github.com/andywang425)', + copyright: '2026, andywang425 (https://github.com/andywang425)', license: 'MIT', match: [ '*://live.bilibili.com/1*', From 17a6d5a205b55387d34be06a7802ad04c4e54aef Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:08:14 +0800 Subject: [PATCH 26/30] =?UTF-8?q?=F0=9F=90=B3=20chore:=20=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=B0=8F=E4=BC=98=E5=8C=96=E5=92=8C=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除uuid(),使用crypto.randomUUID() - SilverToCoinTask和SwitchLiveStreamQuality加上await确保异常会被捕获 - usePlayerStore中Object.assign参数顺序错误,改用_.defaults - 一些注释调整 --- src/library/utils/index.ts | 12 ------------ .../dailyTasks/liveTasks/medalTasks/watchTask.ts | 4 ++-- .../dailyTasks/otherTasks/silverToCoinTask.ts | 4 ++-- src/modules/enhanceExperience/banP2P.ts | 2 +- .../enhanceExperience/switchLiveStreamQuality.ts | 3 ++- src/stores/useModuleStore.ts | 6 ++++-- src/stores/usePlayerStore.ts | 5 +++-- 7 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/library/utils/index.ts b/src/library/utils/index.ts index 758beb9e..43a6e3a0 100644 --- a/src/library/utils/index.ts +++ b/src/library/utils/index.ts @@ -4,17 +4,6 @@ import type { RunAtMoment } from '@/types' import { ts } from '@/library/luxon' import { useBiliStore } from '@/stores' -/** - * 生成一个 version 4 uuid - * @returns uuid - */ -function uuid(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) { - const randomInt = (16 * Math.random()) | 0 - return ('x' === char ? randomInt : (3 & randomInt) | 8).toString(16) - }) -} - /** * 生成一个随机的 32 位 hash * @returns 32 位十六进制字符串 @@ -235,7 +224,6 @@ function arrayToMap(arr: T[]): Map { } export { - uuid, random32Hash, sleep, getFilenameFromUrl, diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index f9e800ec..ac669e59 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -3,7 +3,7 @@ import BAPI from '@/library/bili-api' import { useBiliStore, useModuleStore, usePlayerStore } from '@/stores' import Logger from '@/library/logger' import CryptoJS from 'crypto-js' -import { sleep, uuid } from '@/library/utils' +import { sleep } from '@/library/utils' import type { ModuleStatusTypes, RunAtMoment } from '@/types' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' @@ -68,7 +68,7 @@ class RoomHeart { /** Cookie LIVE_BUVID */ private buvid?: string = useBiliStore().cookies!.LIVE_BUVID - private uuid = uuid() + private uuid = crypto.randomUUID() /** 计算签名和发送请求时均需要 JSON.stringify */ private device: string[] = [this.buvid as string, this.uuid] diff --git a/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts b/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts index 52d5ad4a..e6844c5a 100644 --- a/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts +++ b/src/modules/dailyTasks/otherTasks/silverToCoinTask.ts @@ -34,12 +34,12 @@ class SilverToCoinTask extends BaseModule { } } - public run(): void { + public async run(): Promise { this.logger.log('银瓜子换硬币模块开始运行') if (!isTimestampToday(this.config._lastCompleteTime)) { this.status = 'running' - this.exchange() + await this.exchange() } else { if (isNowBefore(0, 5)) { this.logger.log('昨天的银瓜子换硬币任务已经完成过了,等到今天的00:05再执行') diff --git a/src/modules/enhanceExperience/banP2P.ts b/src/modules/enhanceExperience/banP2P.ts index ce7bd92b..c294af2d 100644 --- a/src/modules/enhanceExperience/banP2P.ts +++ b/src/modules/enhanceExperience/banP2P.ts @@ -374,7 +374,7 @@ class BanP2P extends BaseModule { } /** - * 返回空 sender,占位满足 addTrack 的返回类型要求。 + * 返回空 sender,占位满足 addTrack 的返回类型要求 */ addTrack(): RTCRtpSender { return {} as RTCRtpSender diff --git a/src/modules/enhanceExperience/switchLiveStreamQuality.ts b/src/modules/enhanceExperience/switchLiveStreamQuality.ts index 4623ab12..7add8f70 100644 --- a/src/modules/enhanceExperience/switchLiveStreamQuality.ts +++ b/src/modules/enhanceExperience/switchLiveStreamQuality.ts @@ -1,6 +1,7 @@ import type { OnFrameTypes, RunAtMoment } from '@/types' import BaseModule from '@/modules/BaseModule' import { usePlayerStore, useModuleStore } from '@/stores' + class SwitchLiveStreamQuality extends BaseModule { static runOnMultiplePages: boolean = true static runAt: RunAtMoment = 'window-load' @@ -62,7 +63,7 @@ class SwitchLiveStreamQuality extends BaseModule { try { const livePlayer = await this.playerStore.getPlayer() - this.switchQuality(livePlayer) + await this.switchQuality(livePlayer) } catch (e) { this.logger.error('自动切换画质模块出错', e) } diff --git a/src/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index c7fcca7a..02b3f95b 100644 --- a/src/stores/useModuleStore.ts +++ b/src/stores/useModuleStore.ts @@ -60,7 +60,7 @@ export const useModuleStore = defineStore('module', () => { moduleConfig.value.DailyTasks.MainSiteTasks.login._lastCompleteTime = 0 await rerunModule('Default_DailyRewardInfo', true) - await rerunModule('DailyTask_MainSiteTask_LoginTask') + rerunModule('DailyTask_MainSiteTask_LoginTask') }, watch: async () => { moduleStatus.value.DailyTasks.MainSiteTasks.watch = '' @@ -70,7 +70,7 @@ export const useModuleStore = defineStore('module', () => { rerunModule('Default_DailyRewardInfo', true), rerunModule('Default_DynamicVideos', true), ]) - await rerunModule('DailyTask_MainSiteTask_WatchTask') + rerunModule('DailyTask_MainSiteTask_WatchTask') }, coin: async () => { moduleStatus.value.DailyTasks.MainSiteTasks.coin = '' @@ -183,6 +183,8 @@ export const useModuleStore = defineStore('module', () => { const promiseArray: Promise[] = [] for (const [name, module] of Object.entries(defaultModules)) { if (module.runOnMultiplePages || cacheStore.currentScriptType !== 'Other') { + // 默认模块一定会返回一个 Promise + // 即使意外返回 undefined,Promise.allSettled 会将其当作已 fulfilled 的 Promise promiseArray.push(_runModule(module, name)!) } } diff --git a/src/stores/usePlayerStore.ts b/src/stores/usePlayerStore.ts index bd179459..a70410c8 100644 --- a/src/stores/usePlayerStore.ts +++ b/src/stores/usePlayerStore.ts @@ -2,6 +2,7 @@ import { unsafeWindow } from '$' import { acceptHMRUpdate, defineStore } from 'pinia' import { waitForMoment } from '@/library/utils' import { ref } from 'vue' +import _ from 'lodash' export interface PlayerStoreConfig { getPlayer: { @@ -43,7 +44,7 @@ export const usePlayerStore = defineStore('player', () => { const getPlayer = async ( config: PlayerStoreConfig['getPlayer'] = DEFAULT_CONFIG.getPlayer, ): Promise => { - const mergedConfig = Object.assign({}, config, DEFAULT_CONFIG.getPlayer) + const mergedConfig = _.defaults({}, config, DEFAULT_CONFIG.getPlayer) if (player.value) { return player.value @@ -88,7 +89,7 @@ export const usePlayerStore = defineStore('player', () => { status: 0 | 1, config: PlayerStoreConfig['waitForLiveStatus'] = DEFAULT_CONFIG.waitForLiveStatus, ): Promise => { - const mergedConfig = Object.assign({}, config, DEFAULT_CONFIG.waitForLiveStatus) + const mergedConfig = _.defaults({}, config, DEFAULT_CONFIG.waitForLiveStatus) const player = await getPlayer() From d5ab9332ac78a8b2788b68894fbe5ba22f6c09cc Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:16:38 +0800 Subject: [PATCH 27/30] =?UTF-8?q?=F0=9F=93=83=20docs:=20CLAUDE.md=E5=BE=AE?= =?UTF-8?q?=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 093b2afb..81d11b4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # 项目概述 -BLTH(Bilibili Live Tasks Helper)是基于 Vue 3 + Vite + vite-plugin-monkey + Element Plus + TypeScript 的**用户脚本(Userscript)**,通过用户脚本管理器(Tampermonkey/Violentmonkey)运行在 Bilibili 直播页面(`live.bilibili.com`),实现自动完成每日任务、观看体验优化和页面元素清理。 +BLTH(Bilibili Live Tasks Helper)是基于 Vue 3 + Vite + vite-plugin-monkey + Element Plus + TypeScript 的用户脚本(Userscript),通过用户脚本管理器(Tampermonkey/Violentmonkey)运行在 Bilibili 直播页面(`https://live.bilibili.com/直播间号`),实现自动完成每日任务、观看体验优化和页面元素清理。 # 常用命令 @@ -32,7 +32,7 @@ npm run preview # 打开提供编译产物安装链接的本地预览网页 ## 多标签页安全 -脚本分三类:`Main`,`SubMain`,`Other`。同时打开多个直播间页面时,`src/stores/useCacheStore.ts` 会选出唯一 `Main` 脚本,避免部分模块重复执行。 +脚本分三类:`Main`,`SubMain`,`Other`。同时打开多个直播间页面时,`src/stores/useCacheStore.ts` 会选出唯一 `Main` 脚本,避免部分模块重复加载。 # 分支 @@ -47,5 +47,5 @@ npm run preview # 打开提供编译产物安装链接的本地预览网页 # 易踩坑点 -- 新增第三方库须写进 `vite.config.ts` 的 `externalGlobals`/`externalResource` 中,使其通过 `@require`/`@resource` 引入,不打包进脚本 -- 调试早期模块(`runAt: RunAtMoment = 'document-start'`)时 dev 模式注入太晚,请用 `npm run build` 的产物测试 +- 新增第三方库须写进 `vite.config.ts` 的 `externalGlobals`/`externalResource` 中,通过 `@require`/`@resource` 引入,不打包进脚本 +- 调试早期模块(`runAt = 'document-start'`)时 dev 模式注入太晚,请用 `npm run build` 的产物测试 From 410c87f67b1178e6b72dbde25fcc4624b27203a0 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:48:43 +0800 Subject: [PATCH 28/30] =?UTF-8?q?=F0=9F=93=83=20docs(README):=20QQ?= =?UTF-8?q?=E7=BE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f62dfad6..8e26b73d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ # 交流群 -- QQ 频道:[点我加入 aw 的频道](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=1W7eVLs&businessType=9&from=181074&biz=ka&shareSource=5)。 +- QQ 群:[点击链接加入群聊【BLTH交流群】(920309361)](https://qm.qq.com/q/thu1A6aRBC) # 参与开发 From e74bfd7d98c9771c3e580c854d1158e1a6ff0d18 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:48:07 +0800 Subject: [PATCH 29/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=9B=B4=E6=92=AD?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=9C=A8=E5=BC=80=E5=A7=8B=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=AF=8F=E4=B8=AA=E7=B2=89=E4=B8=9D=E5=8B=8B=E7=AB=A0=E7=9A=84?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=89=8D=E5=85=88=E6=A3=80=E6=9F=A5=E5=82=A8?= =?UTF-8?q?=E8=93=84=E4=BA=B2=E5=AF=86=E5=BA=A6=E6=98=AF=E5=90=A6=E8=BE=BE?= =?UTF-8?q?=E5=88=B0=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 ++++- src/library/help-info/index.ts | 2 +- src/library/storage/defaultValues.ts | 2 +- .../liveTasks/medalTasks/danmuTask.ts | 20 +++++++++++++---- .../liveTasks/medalTasks/lightTask.ts | 22 ++++++++++++++----- .../liveTasks/medalTasks/likeTask.ts | 20 +++++++++++++---- .../liveTasks/medalTasks/watchTask.ts | 17 ++++++++++---- 7 files changed, 67 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a1cca97..e4ac5f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,19 @@ - 执行完任意粉丝勋章的粉丝团任务后,检查是否存在储蓄的亲密度以及是否达到储蓄上限,把结果输出到控制台日志 - 直播任务【点赞】、【发弹幕】和【观看直播】支持“完成目标轮次”和“跟随每日上限”两种模式 - 直播任务中的【点赞】任务支持等待开播后再点赞,【发弹幕】任务支持等待下播后再发弹幕 -- 直播任务【点亮熄灭勋章】、【点赞】、【发弹幕】和【观看直播】完成后会调用接口检查粉丝团升级任务是否真的完成了,若实际未完成不标记完成状态,下次运行时会再次尝试完成 +- 直播任务【点亮熄灭勋章】、【点赞】、【发弹幕】和【观看直播】完成后会调用接口检查粉丝团点亮/升级任务是否真的完成了,若实际未完成不标记完成状态,下次运行时会再次尝试完成 +- 直播任务【点赞】、【发弹幕】和【观看直播】在开始执行每个粉丝勋章的任务前会先检查储蓄亲密度是否达到上限,如果已到上限,提示并跳过 ### 修复 - 修复【观看直播】任务在计算已观看时长时会错误减少一些时长的bug(B站新版粉丝团任务的观看时长计算已精确到分钟) +- 修复【领取年度大会员权益】任务下次运行时间计算错误的bug ### 调整 - 如果无法获取某个粉丝勋章的点亮任务,跳过该粉丝勋章 - 手动重新运行【点亮熄灭勋章】时任务时会检测【点赞】和【发弹幕】任务是否正在运行,如果是则不允许重新运行 +- 🐧 交流群由QQ频道改为QQ群:[点击链接加入群聊【BLTH交流群】(920309361)](https://qm.qq.com/q/thu1A6aRBC) ## [7.2.9] - 2026-5-24 diff --git a/src/library/help-info/index.ts b/src/library/help-info/index.ts index efd1eb0a..e59f1163 100644 --- a/src/library/help-info/index.ts +++ b/src/library/help-info/index.ts @@ -119,7 +119,7 @@ const helpInfo: HelpInfo = { h('li', '点赞、发弹幕和观看直播任务都会等点亮熄灭勋章任务完成后再执行。'), h( 'li', - '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄。需要投喂一个粉丝灯牌才能领取。', + '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄,且储蓄量存在上限(根据目前观察上限是100)。需要投喂一个粉丝灯牌才能领取这些亲密度。脚本会在控制台日志中提示储蓄的亲密度量以及是否达到上限。', ), ]), ]), diff --git a/src/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts index 8c3106cb..34234bcd 100644 --- a/src/library/storage/defaultValues.ts +++ b/src/library/storage/defaultValues.ts @@ -79,7 +79,7 @@ const defaultValues: DefaultValues = { danmuList: [ '(⌒▽⌒)', '( ̄▽ ̄)', - '(>y<)', + '(◕‿◕✿)', '(`・ω・´)', '(ノ ̄▽ ̄)', '(・∀・)', diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index 238accf9..5fc6cc75 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -102,10 +102,22 @@ class DanmuTask extends MedalModule { return { interrupted: true, verifiedCompleted: false } } - const medalData = await this.fetchMedalData(medal.medal.target_id) + 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 medalData = await this.fetchMedalData(target_id) if (!medalData) { this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`, + `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息,跳过发弹幕任务`, + ) + return { interrupted: false, verifiedCompleted: true } + } + + 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 } } @@ -113,7 +125,7 @@ class DanmuTask extends MedalModule { const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') if (!item) { this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, ) return { interrupted: false, verifiedCompleted: true } } @@ -123,7 +135,7 @@ class DanmuTask extends MedalModule { const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( - `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, + `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${target_id},直播间:${room_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, ) return { interrupted: false, verifiedCompleted: true } } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index 52c51491..799b7118 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -160,10 +160,15 @@ class LightTask extends MedalModule { for (let i = 0; i < medals.length; i++) { const medal = medals[i] - const medalData = await this.fetchMedalData(medal.medal.target_id) + 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 medalData = await this.fetchMedalData(target_id) if (!medalData) { this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团点亮任务信息,跳过点赞任务`, + `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息,跳过点赞任务`, ) continue } @@ -171,7 +176,7 @@ class LightTask extends MedalModule { const likeItem = MedalModule.findTaskInfo(medalData.task_info, 'like') if (!likeItem) { this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息中找到点赞任务,跳过点赞任务`, ) continue } @@ -199,10 +204,15 @@ class LightTask extends MedalModule { for (let i = 0; i < medals.length; i++) { const medal = medals[i] - const medalData = await this.fetchMedalData(medal.medal.target_id) + 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 medalData = await this.fetchMedalData(target_id) if (!medalData) { this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团点亮任务信息,跳过发弹幕任务`, + `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息,跳过发弹幕任务`, ) continue } @@ -210,7 +220,7 @@ class LightTask extends MedalModule { const danmuItem = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') if (!danmuItem) { this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息中找到发弹幕任务,跳过发弹幕任务`, ) continue } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts index 1dafd613..1c015ad3 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -87,10 +87,22 @@ class LikeTask extends MedalModule { return { interrupted: true, verifiedCompleted: false } } - const medalData = await this.fetchMedalData(medal.medal.target_id) + 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 medalData = await this.fetchMedalData(target_id) if (!medalData) { this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`, + `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息,跳过点赞任务`, + ) + return { interrupted: false, verifiedCompleted: true } + } + + 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 } } @@ -98,7 +110,7 @@ class LikeTask extends MedalModule { const item = MedalModule.findTaskInfo(medalData.task_info, 'like') if (!item) { this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, ) return { interrupted: false, verifiedCompleted: true } } @@ -108,7 +120,7 @@ class LikeTask extends MedalModule { const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( - `无法解析主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的点赞任务的每日上限信息,跳过点赞任务`, + `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${target_id},直播间:${room_id})的点赞任务的每日上限信息,跳过点赞任务`, ) return { interrupted: false, verifiedCompleted: true } } diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index ac669e59..e53eb217 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -343,11 +343,20 @@ class WatchTask extends MedalModule { const medal = fansMedals[i] const roomid = medal.room_info.room_id const uid = medal.medal.target_id + const nick_name = medal.anchor_info.nick_name + const medal_name = medal.medal.medal_name const medalData = await this.fetchMedalData(uid) if (!medalData) { this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${uid})的粉丝团升级任务信息,跳过观看直播任务`, + `粉丝勋章【${medal_name}】 无法获取主播【${nick_name}】(UID:${uid},直播间:${roomid})的粉丝团升级任务信息,跳过观看直播任务`, + ) + continue + } + + if (medalData.reach_free_intimacy_limit) { + this.logger.warn( + `粉丝勋章【${medal_name}】(主播【${nick_name}】,UID:${uid},直播间:${roomid})已达到储蓄亲密度上限(已储蓄 ${medalData.free_intimacy} 亲密度,投喂一个粉丝灯牌即可领取这些亲密度),无法通过观看直播获取更多亲密度,跳过观看直播任务`, ) continue } @@ -355,7 +364,7 @@ class WatchTask extends MedalModule { const item = MedalModule.findTaskInfo(medalData.task_info, 'watchLive') if (!item) { this.logger.error( - `无法在主播【${medal.anchor_info.nick_name}】(UID:${uid})的粉丝团升级任务信息中找到观看直播任务,跳过观看直播任务`, + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${uid},直播间:${roomid})的粉丝团升级任务信息中找到观看直播任务,跳过观看直播任务`, ) continue } @@ -365,7 +374,7 @@ class WatchTask extends MedalModule { const parsed = MedalModule.parseDailyLimit(item.sub_title) if (!parsed) { this.logger.error( - `无法解析主播【${medal.anchor_info.nick_name}】(UID:${uid})的观看直播任务的每日上限信息,跳过观看直播任务`, + `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${uid},直播间:${roomid})的观看直播任务的每日上限信息,跳过观看直播任务`, ) continue } @@ -390,7 +399,7 @@ class WatchTask extends MedalModule { if (area_id > 0 && parent_area_id > 0) { // area_id 和 parent_area_id 都大于 0,说明直播间设置了分区,心跳有效 this.logger.log( - `粉丝勋章【${medal.medal.medal_name}】 开始直播间 ${roomid}(主播【${medal.anchor_info.nick_name}】,UID:${uid})的观看直播任务,目标时长 ${targetSeconds / 60} 分钟)`, + `粉丝勋章【${medal_name}】 开始直播间 ${roomid}(主播【${nick_name}】,UID:${uid})的观看直播任务,目标时长 ${targetSeconds / 60} 分钟)`, ) const hasWatchingProgress = await new RoomHeart( From 83a3949a035ac2bc53ed0e36a353cfc9fc562aa3 Mon Sep 17 00:00:00 2001 From: andywang425 <56548456+andywang425@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:52:19 +0800 Subject: [PATCH 30/30] =?UTF-8?q?=F0=9F=90=B3=20chore:=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=89=8D=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +++- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ac5f7a..ebcfb617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ ## [Unreleased] +## [7.3.0] - 2026-6-14 + ### 新增 - 执行完任意粉丝勋章的粉丝团任务后,检查是否存在储蓄的亲密度以及是否达到储蓄上限,把结果输出到控制台日志 -- 直播任务【点赞】、【发弹幕】和【观看直播】支持“完成目标轮次”和“跟随每日上限”两种模式 +- 直播任务【点赞】、【发弹幕】和【观看直播】支持”完成目标轮次”和”跟随每日上限”两种模式 - 直播任务中的【点赞】任务支持等待开播后再点赞,【发弹幕】任务支持等待下播后再发弹幕 - 直播任务【点亮熄灭勋章】、【点赞】、【发弹幕】和【观看直播】完成后会调用接口检查粉丝团点亮/升级任务是否真的完成了,若实际未完成不标记完成状态,下次运行时会再次尝试完成 - 直播任务【点赞】、【发弹幕】和【观看直播】在开始执行每个粉丝勋章的任务前会先检查储蓄亲密度是否达到上限,如果已到上限,提示并跳过 diff --git a/package-lock.json b/package-lock.json index 17642c40..7545723b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bilibili-live-tasks-helper", - "version": "7.2.9", + "version": "7.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bilibili-live-tasks-helper", - "version": "7.2.9", + "version": "7.3.0", "dependencies": { "ajax-hook": "^3.0.3", "crypto-js": "^4.2.0", diff --git a/package.json b/package.json index 2849380b..a3d80112 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bilibili-live-tasks-helper", "private": true, - "version": "7.2.9", + "version": "7.3.0", "type": "module", "scripts": { "dev": "vite",