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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3525b88c..ebcfb617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ ## [Unreleased] +## [7.3.0] - 2026-6-14 + +### 新增 + +- 执行完任意粉丝勋章的粉丝团任务后,检查是否存在储蓄的亲密度以及是否达到储蓄上限,把结果输出到控制台日志 +- 直播任务【点赞】、【发弹幕】和【观看直播】支持”完成目标轮次”和”跟随每日上限”两种模式 +- 直播任务中的【点赞】任务支持等待开播后再点赞,【发弹幕】任务支持等待下播后再发弹幕 +- 直播任务【点亮熄灭勋章】、【点赞】、【发弹幕】和【观看直播】完成后会调用接口检查粉丝团点亮/升级任务是否真的完成了,若实际未完成不标记完成状态,下次运行时会再次尝试完成 +- 直播任务【点赞】、【发弹幕】和【观看直播】在开始执行每个粉丝勋章的任务前会先检查储蓄亲密度是否达到上限,如果已到上限,提示并跳过 + +### 修复 + +- 修复【观看直播】任务在计算已观看时长时会错误减少一些时长的bug(B站新版粉丝团任务的观看时长计算已精确到分钟) +- 修复【领取年度大会员权益】任务下次运行时间计算错误的bug + +### 调整 + +- 如果无法获取某个粉丝勋章的点亮任务,跳过该粉丝勋章 +- 手动重新运行【点亮熄灭勋章】时任务时会检测【点赞】和【发弹幕】任务是否正在运行,如果是则不允许重新运行 +- 🐧 交流群由QQ频道改为QQ群:[点击链接加入群聊【BLTH交流群】(920309361)](https://qm.qq.com/q/thu1A6aRBC) + ## [7.2.9] - 2026-5-24 ### 新增 diff --git a/CLAUDE.md b/CLAUDE.md index 2d5b8b1f..81d11b4e 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 直播页面(`https://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 = '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` +- 为逻辑复杂的代码编写注释 ## 编译 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) # 参与开发 diff --git a/eslint.config.ts b/eslint.config.ts index 548d08b9..849dfe0f 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' @@ -35,7 +34,7 @@ export default defineConfigWithVueTs( argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', - varsIgnorePattern: '^_', + varsIgnorePattern: '^_.+', ignoreRestSiblings: true, }, ], diff --git a/package-lock.json b/package-lock.json index c38115dd..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", @@ -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..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", @@ -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" } } diff --git a/src/components/LiveTasks.vue b/src/components/LiveTasks.vue index 4c924bce..3595ee61 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 { @@ -235,6 +235,37 @@ function handleRowClick(row: MedalInfoRow) { + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + 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/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..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(), } @@ -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') => { @@ -319,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/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 { 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/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/library/help-info/index.ts b/src/library/help-info/index.ts index e90877b0..e59f1163 100644 --- a/src/library/help-info/index.ts +++ b/src/library/help-info/index.ts @@ -10,265 +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', + '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄,且储蓄量存在上限(根据目前观察上限是100)。需要投喂一个粉丝灯牌才能领取这些亲密度。脚本会在控制台日志中提示储蓄的亲密度量以及是否达到上限。', + ), ]), - ), + ]), ]), - h('div', [ - h('strong', '注意:'), + }, + rounds: { + title: '跟随每日上限 / 完成目标轮次', + message: () => + h('p', [ + h( + 'div', + '一轮指通过完成任务获得 +1 亲密度所需的最小操作量。具体值脚本会在运行过程中实时获取。根据观察目前每轮的操作量如下:', + ), h('ul', [ - h('li', '点赞、发弹幕和观看直播任务都会等点亮熄灭勋章任务完成后再执行。'), - h( - 'li', - '熄灭的勋章被点亮后,通过完成任务获得的亲密度会被储蓄。需要投喂一个粉丝灯牌才能领取。', - ), + h('li', '点赞:30 次'), + h('li', '发弹幕:1 条'), + h('li', '观看直播:15 分钟'), ]), + h( + 'div', + '选择【跟随每日上限】时,脚本会尽量做到任务的当日上限(任务上限也是实时获取的)。', + ), + h( + 'div', + '选择【完成目标轮次】可自定义脚本当天至多完成几轮;若填写值超过任务上限,则还是做到任务的当日上限。', + ), ]), - ]), }, like: { title: '点赞', - message: h('p', [ - h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的点赞任务。'), - h('ul', [ - h('li', '只有主播开播时点赞才会计入任务进度,因此只会给开播中的直播间点赞。'), + message: () => + h('p', [ + h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的点赞任务。'), + h('div', '只有主播开播时点赞才会计入任务进度。'), + ]), + }, + likeWaitUntilLiving: { + title: '等待开播后再点赞', + message: () => + h('p', [ h( - 'li', - '脚本会根据 B 站返回的任务要求自动决定点赞次数。高等级的粉丝勋章拥有更高的任务上限,因此能获得更多的亲密度。', + 'div', + '开启该功能后,对于尚未开播的直播间,脚本会等主播开播后再点赞。关闭时会直接跳过。', ), ]), - ]), }, danmu: { title: '发弹幕', - message: h('p', [ - h('div', '为已点亮的粉丝勋章完成粉丝团升级任务中的发弹幕任务。'), - h('ul', [ - h( - 'li', - '脚本会根据 B 站返回的任务要求自动决定发送条数。高等级的粉丝勋章拥有更高的任务上限,因此能获得更多的亲密度。', - ), - h( - 'li', - '点击【编辑弹幕】按钮编辑要发送的弹幕,脚本会从中按顺序循环抽取弹幕发送。建议修改默认的弹幕。', - ), - 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', '此时再开启【等待下播后再发弹幕】,脚本会等直播中的主播下播后再发弹幕。'), ]), - ]), }, watch: { title: '观看直播', - message: h('p', [ - h('div', '为已点亮的粉丝勋章完成完成粉丝团升级任务中的观看直播任务。'), - h('ul', [ - h('li', '部分直播间因为没有设置直播分区导致任务无法完成。'), - h('li', '主播当前是否开播不会影响该任务的完成。'), - h('li', '脚本会根据 B 站返回的任务要求自动计算观看时长。'), - ]), - 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/library/storage/defaultValues.ts b/src/library/storage/defaultValues.ts index 8b265e4f..34234bcd 100644 --- a/src/library/storage/defaultValues.ts +++ b/src/library/storage/defaultValues.ts @@ -50,7 +50,7 @@ const defaultValues: DefaultValues = { '[妙]', '[哇]', '[爱]', - '[比心]', + '[大哭]', '[笑哭]', '[捂脸]', '[喝彩]', @@ -63,11 +63,13 @@ const defaultValues: DefaultValues = { ], isWhiteList: false, roomidList: [], - _lastEffectiveCompleteTime: 0, _lastCompleteTime: 0, }, like: { enabled: false, + useTargetRounds: false, + targetRounds: 1, + waitUntilLiving: false, isWhiteList: false, roomidList: [], _lastCompleteTime: 0, @@ -77,9 +79,9 @@ const defaultValues: DefaultValues = { danmuList: [ '(⌒▽⌒)', '( ̄▽ ̄)', - '(=・v・=)', + '(◕‿◕✿)', '(`・ω・´)', - '(〜 ̄△ ̄)~', + '(ノ ̄▽ ̄)', '(・∀・)', '(°∀°)ノ', '╮( ̄▽ ̄)╭', @@ -91,13 +93,18 @@ const defaultValues: DefaultValues = { '←◡←', `(●'◡'●)ノ♥`, ], + useTargetRounds: false, + targetRounds: 1, onlyWhenNotLiving: false, + waitUntilNotLiving: false, isWhiteList: false, roomidList: [], _lastCompleteTime: 0, }, watch: { enabled: false, + useTargetRounds: false, + targetRounds: 1, isWhiteList: false, roomidList: [], _watchingProgress: {}, 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/MedalModule.ts b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts index 0adf075b..22f36918 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/MedalModule.ts @@ -1,42 +1,53 @@ import BaseModule from '@/modules/BaseModule' import { useBiliStore, useModuleStore } from '@/stores' import { watch } from 'vue' -import type { MedalTaskSharedConfig, SharedMedalFilters } from './types' +import type { + MedalTaskSharedConfig, + RequestQueueKey, + SharedMedalFilters, + TaskJumpType, + WaitProbeResult, + 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, tsm } from '@/library/luxon' import _ from 'lodash' -type JumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' - class MedalModule extends BaseModule { /** 最大弹幕补发次数 */ protected static readonly DANMU_RETRY_LIMIT = 3 - - /** - * 任务信息共享缓存 - * - * target_id -> task_info Promise - */ - private static taskInfoCache = new Map< - number, - Promise - >() - - /** - * 简单限流队列 - * - * 串行执行获取任务信息请求 - */ - private static taskInfoRequestQueue: Promise = Promise.resolve() - - medalTasksConfig = useModuleStore().moduleConfig.DailyTasks.LiveTasks.medalTasks - - /** 所有粉丝勋章任务的公共配置 */ - declare protected config: MedalTaskSharedConfig - - protected SHARED_MEDAL_FILTERS: SharedMedalFilters = { + /** 粉丝团升级任务 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 + /** 单房间探测时相邻请求间的随机延迟 */ + private static get ROOM_STATUS_PROBE_DYNAMIC_DELAY() { + 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 + /** 发弹幕动态间隔时间 */ + 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 @@ -49,6 +60,88 @@ 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 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 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 + + /** 所有粉丝勋章任务的公共配置 */ + declare protected config: MedalTaskSharedConfig /** * 对粉丝勋章列表进行排序 @@ -60,6 +153,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) + } + /** * 等待粉丝勋章数据获取完毕 * @@ -68,11 +168,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() @@ -88,14 +188,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 @@ -103,21 +201,21 @@ 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) } /** - * 从任务 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 @@ -125,66 +223,300 @@ class MedalModule extends BaseModule { } /** - * 清空任务信息共享缓存 + * 刷新粉丝勋章列表 + * + * @returns 是否刷新成功 */ - protected static clearTaskInfoCache(): void { - MedalModule.taskInfoCache.clear() + protected async refreshFansMedals(): Promise { + const moduleStore = useModuleStore() + try { + await moduleStore.rerunModule('Default_FansMedals', true) + const status = useBiliStore().fansMedalsMeta.status + return status === 'loading' ? await this.waitForFansMedals() : status === 'loaded' + } catch (error) { + this.logger.error('刷新粉丝勋章列表失败', error) + return false + } } /** - * 将获取任务信息请求加入全局串行队列 + * 以随机顺序尝试多个接口,获取单个直播间的直播状态 + * + * @returns + * - `1`:直播中 + * - `0`/`2`:未开播/轮播中 + * - `null`:两个接口都获取失败 */ - private static enqueueTaskInfoRequest(requester: () => Promise): Promise { - const task = MedalModule.taskInfoRequestQueue - .catch(() => undefined) - .then(async () => { - await sleep(_.random(300, 500)) - return requester() - }) - - MedalModule.taskInfoRequestQueue = task.then( - () => undefined, - () => undefined, + protected async fetchRoomLiveStatus(roomid: number): Promise { + const fetchers = _.shuffle(this.ROOM_LIVE_STATUS_FETCHERS) + + for (let i = 0; i < fetchers.length; i++) { + const liveStatus = await fetchers[i](roomid) + if (liveStatus !== null) { + return liveStatus + } + if (i < fetchers.length - 1) { + await sleep(MedalModule.ROOM_STATUS_PROBE_DYNAMIC_DELAY) + } + } + + return null + } + + /** + * 计算等待探测策略 + * + * @param pendingCount 当前待探测的直播间数量 + */ + protected decideWaitStrategy(pendingCount: number): WaitStrategy { + const totalMedalCount = useBiliStore().filteredFansMedals.length + const fullRefreshCost = Math.ceil(totalMedalCount / 10) // 每次请求能获取 10 个粉丝勋章 + return pendingCount <= fullRefreshCost ? 'single-probe' : 'refresh-fans-medals' + } + + /** + * 判断粉丝勋章数据是否足够新鲜 + */ + private isMedalDataFresh(): boolean { + const meta = useBiliStore().fansMedalsMeta + return ( + meta.status === 'loaded' && + tsm() - meta.lastFetchFinishedAt! < MedalModule.RECENT_FETCH_THRESHOLD ) + } - return task + /** + * 从粉丝勋章 Map 构建探测结果 + * + * @param roomids 待探测的直播间ID列表 + * @param targetPredicate 直播状态判断函数 + * @param medalMap 粉丝勋章 Map + * @param probedLiveStatusMap 探测到的直播状态 Map + * - 为 undefined 时:使用 medal.room_info.living_status + * - 存在键 roomid 但值为 null 时:表示本轮实时探测失败,继续等待 + */ + private buildResultFromMedalMap( + roomids: number[], + targetPredicate: (liveStatus: number) => boolean, + medalMap: Map, + probedLiveStatusMap?: Map, + ): WaitProbeResult { + const readyMedals: LiveData.FansMedalPanel.List[] = [] + const pendingRoomids: number[] = [] + + for (const roomid of roomids) { + const medal = medalMap.get(roomid) + + if (!medal) { + this.logger.warn(`直播间 ${roomid} 未在粉丝勋章列表中找到,停止等待该房间`) + continue + } + if (!this.SHARED_MEDAL_FILTERS.isLighted(medal)) { + this.logger.log( + `粉丝勋章【${medal.medal.medal_name}】已熄灭,停止等待该房间(${roomid})`, + medal, + ) + continue + } + + // 是否有探测到的直播状态 Map + const hasProbedStatus = probedLiveStatusMap !== undefined + // 直播状态:有直播状态 Map 就从中取,否则使用粉丝勋章 Map 中的值 + const liveStatus = hasProbedStatus + ? probedLiveStatusMap.get(roomid) + : medal.room_info.living_status + + if (hasProbedStatus && _.isNil(liveStatus)) { + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】对应的直播间 ${roomid} 本轮实时探测失败,继续等待下次检查`, + medal, + ) + pendingRoomids.push(roomid) + } else if (!_.isNil(liveStatus) && targetPredicate(liveStatus)) { + readyMedals.push(medal) + } else { + pendingRoomids.push(roomid) + } + } + + return { + readyMedals, + pendingRoomids, + } } /** - * 获取指定粉丝勋章的任务信息 + * 对等待中的直播间做一轮探测 * - * 同一 target_id 的并发请求会共享同一个 Promise; - * 不同 target_id 的请求会进入全局串行队列,避免短时间内并发触发过多请求 + * @param roomids 待探测的直播间ID列表 + * @param targetPredicate 直播状态判断函数,返回"是否符合目标状态" * - * @param target_id 主播 uid - * @returns 成功返回任务信息数组,失败返回 null + * @returns 探测结果,包含符合目标状态的粉丝勋章列表和待处理的直播间ID列表 */ - protected fetchTaskInfo( - target_id: number, - ): Promise { - const cached = MedalModule.taskInfoCache.get(target_id) - if (cached) return cached + protected async probeWaitingMedals( + roomids: number[], + targetPredicate: (liveStatus: number) => boolean, + ): Promise { + const biliStore = useBiliStore() + + this.logger.log(`开始直播状态探测,待探测直播间数量:${roomids.length}`, { roomids }) + + // 如果有进行中的全量刷新,等待其完成 + if (MedalModule.ongoingFullRefreshPromise) { + this.logger.log('等待进行中的粉丝勋章刷新完成...') + await MedalModule.ongoingFullRefreshPromise + } + + // 检查粉丝勋章数据是否新鲜 + if (this.isMedalDataFresh()) { + this.logger.log('粉丝勋章数据较新,直接使用缓存数据') + return this.buildResultFromMedalMap(roomids, targetPredicate, biliStore.filteredFansMedalsMap) + } + + // 数据不新鲜,决定探测策略 + const strategy = this.decideWaitStrategy(roomids.length) + this.logger.log( + `粉丝勋章数据不新鲜,使用${strategy === 'single-probe' ? '单房间探测' : '刷新粉丝勋章'}策略`, + ) - const promise = MedalModule.enqueueTaskInfoRequest(async () => { + if (strategy === 'refresh-fans-medals') { + const promise = this.refreshFansMedals() + MedalModule.ongoingFullRefreshPromise = promise + try { + await promise + } finally { + if (MedalModule.ongoingFullRefreshPromise === promise) { + MedalModule.ongoingFullRefreshPromise = null + } + } + + if (biliStore.fansMedalsMeta.status === 'loaded') { + return this.buildResultFromMedalMap( + roomids, + targetPredicate, + biliStore.filteredFansMedalsMap, + ) + } + + return { readyMedals: [], pendingRoomids: roomids } + } + + // 单房间探测策略 + 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 this.buildResultFromMedalMap( + roomids, + targetPredicate, + currentMedalMap, + probedLiveStatusMap, + ) + } + + /** + * 获取指定粉丝勋章的信息 + * + * 请求会进入全局串行队列,避免短时间内并发触发过多请求 + * + * @param target_id 主播 uid + * @returns 成功返回完整 Data,失败返回 null + */ + 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) - 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 + /** + * 记录储蓄亲密度信息日志 + */ + 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},投喂一个粉丝灯牌即可领取这些亲密度`, + ) + } + } + + /** + * 等待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) + + 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) 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 + } + } + + this.logger.warn( + `粉丝勋章【${medal.medal.medal_name}】已执行 ${actionText} 任务,但B站仍认为该任务未完成(实际进度:${item.sub_title}),下次运行会继续尝试`, + ) + return false } /** @@ -209,13 +541,6 @@ class MedalModule extends BaseModule { (newStatus) => { if (newStatus === 'done' || newStatus === 'error') { unwatch() - - if (lightConfig._lastEffectiveCompleteTime === lightConfig._lastCompleteTime) { - // 如果点亮熄灭勋章模块确实进行了点亮操作 - // 重新获取粉丝勋章(主要是为了获取最新的点亮状态和直播状态) - // FansMedals 模块内部做了防重入,因此无需担心会重复获取 - moduleStore.rerunModule('Default_FansMedals') - } resolve() } }, diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts index b83dc0dc..5fc6cc75 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/danmuTask.ts @@ -1,11 +1,11 @@ -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, TaskExecutionResult } from './types' class DanmuTask extends MedalModule { config = this.medalTasksConfig.danmu @@ -15,20 +15,38 @@ 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)) { + // 开启了“仅在未开播的直播间发弹幕”且正在直播 + if (this.config.waitUntilNotLiving) { + // 开启了“等待下播后再发弹幕”,等下播了再发弹幕 + result.waitingMedals.push(medal) + } + // 没开“等待下播后再发弹幕”,跳过 + } else { + // 没开“仅在未开播的直播间发弹幕”或者直播间未开播,可立即发弹幕 + result.readyMedals.push(medal) + } + } + }) if (this.config.isWhiteList) { - this.sortMedals(result) + this.sortMedals(result.readyMedals) + this.sortMedals(result.waitingMedals) } return result @@ -36,6 +54,7 @@ class DanmuTask extends MedalModule { /** * 发弹幕 + * * @returns 是否发送成功 */ private async sendDanmu(medal: LiveData.FansMedalPanel.List, danmu: string): Promise { @@ -69,6 +88,130 @@ 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 { interrupted: true, verifiedCompleted: false } + } + + 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_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 } + } + + const item = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') + if (!item) { + this.logger.error( + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息中找到发弹幕任务,跳过发弹幕任务`, + ) + return { interrupted: false, verifiedCompleted: true } + } + + if (item.is_done) return { interrupted: false, verifiedCompleted: true } + + const parsed = MedalModule.parseDailyLimit(item.sub_title) + if (!parsed) { + this.logger.error( + `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${target_id},直播间:${room_id})的发弹幕任务的每日上限信息,跳过发弹幕任务`, + ) + return { interrupted: false, verifiedCompleted: true } + } + + if (parsed.current >= parsed.limit) return { interrupted: false, verifiedCompleted: true } + + 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()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') + return { interrupted: true, verifiedCompleted: false } + } + + const danmuText = this.config.danmuList[danmuIndexRef.value++ % this.config.danmuList.length] + if (await this.sendDanmu(medal, danmuText)) { + hasSuccessfulDanmu = true + } else { + if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { + this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) + break + } + remaining += 1 + } + + if (j < remaining - 1) { + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) + } + } + + if (hasSuccessfulDanmu) { + return { + interrupted: false, + verifiedCompleted: await this.confirmTaskCompletedAfterUpdate( + medal, + 'sendDanmu', + this.config.useTargetRounds ? target : undefined, + ), + } + } + + // 所有发弹幕尝试均失败,估计是有什么异常状况,跳过 + return { interrupted: false, verifiedCompleted: true } + } + + /** + * 顺序执行多个直播间的发弹幕任务 + * + * @returns 执行结果,包含是否中断以及是否都确认完成 + */ + private async executeDanmuTasks( + medals: LiveData.FansMedalPanel.List[], + danmuIndexRef: { value: number }, + ): Promise { + let verifiedCompleted = true + + for (let i = 0; i < medals.length; i++) { + const result = await this.executeDanmuTask(medals[i], danmuIndexRef) + if (result.interrupted) return result + if (!result.verifiedCompleted) { + verifiedCompleted = false + } + + if (i < medals.length - 1) { + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) + } + } + + return { interrupted: false, verifiedCompleted } + } + public async run(): Promise { this.logger.log('发弹幕模块开始运行') @@ -82,73 +225,60 @@ class DanmuTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() - const fansMedals = this.getMedals() + const { readyMedals, waitingMedals } = this.getMedals() let allCompleted = true - let danmuIndex = 0 + const danmuIndexRef = { value: 0 } + let pendingRoomids = waitingMedals.map((medal) => medal.room_info.room_id) + + const initialResult = await this.executeDanmuTasks(readyMedals, danmuIndexRef) + if (initialResult.interrupted) { + allCompleted = false + } else if (!initialResult.verifiedCompleted) { + allCompleted = false + } - outer: for (let i = 0; i < fansMedals.length; i++) { - if (isNowAfter(23, 55) || isNowBefore(0, 5)) { + while (allCompleted && pendingRoomids.length > 0) { + if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮发弹幕任务') allCompleted = false break } - const medal = fansMedals[i] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - if (!taskInfo) { - this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过发弹幕任务`, - ) - continue - } - - const item = MedalModule.findTaskInfo(taskInfo, '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 waitResult = await this.executeDanmuTasks(newlyReadyMedals, danmuIndexRef) + if (waitResult.interrupted) { + allCompleted = false + break + } + if (!waitResult.verifiedCompleted) { + 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 - } + if (pendingRoomids.length > 0) { + const medalMap = useBiliStore().filteredFansMedalsMap + const pendingRoomsInfo: Record = {} - 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++ + for (const roomid of pendingRoomids) { + pendingRoomsInfo[roomid] = medalMap.get(roomid)?.anchor_info.nick_name } - if (j < target - 1 || i < fansMedals.length - 1) { - await sleep(_.random(6000, 8000)) - } + this.logger.log( + `仍有 ${pendingRoomids.length} 个直播间未下播,${MedalModule.WAIT_POLL_INTERVAL / 1000} 秒后继续检查`, + { pendingRoomsInfo }, + ) + await sleep(MedalModule.WAIT_POLL_INTERVAL) } } + if (pendingRoomids.length > 0) { + allCompleted = false + } + if (allCompleted) { this.config._lastCompleteTime = tsm() this.status = 'done' diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts index a00b5e12..799b7118 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/lightTask.ts @@ -3,11 +3,9 @@ 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' - -type MedalsByLiving = [LiveData.FansMedalPanel.List[], LiveData.FansMedalPanel.List[]] +import type { GroupedMedals } from './types' class LightTask extends MedalModule { config = this.medalTasksConfig.light @@ -17,13 +15,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,20 +33,61 @@ 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 } + /** + * 点亮任务完成后,是否仍有下游任务需要最新的粉丝勋章点亮状态 + */ + private shouldRefreshFansMedals(): boolean { + const { like, danmu, watch } = this.medalTasksConfig + const downstreamTasks = [like, danmu, watch] + + 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 粉丝勋章 @@ -112,56 +152,103 @@ class LightTask extends MedalModule { /** * 给正在直播的直播间点赞 * @param medals - * @private */ - 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 taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - const item = MedalModule.findTaskInfo(taskInfo, 'like') - // 从 title 解析点亮所需点赞次数(如 "点赞30次" → 30),失败时 fallback 到 30 - const baseCount = MedalModule.parseTitleCount(item?.title) ?? 30 - await this.like(medal, _.random(baseCount, baseCount + 5)) + 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_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息,跳过点赞任务`, + ) + continue + } + + const likeItem = MedalModule.findTaskInfo(medalData.task_info, 'like') + if (!likeItem) { + this.logger.error( + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息中找到点赞任务,跳过点赞任务`, + ) + continue + } + const times = MedalModule.parseTitleCount(likeItem.title) ?? 30 + await this.like(medal, times) + attemptedMedals.push(medal) if (i < medals.length - 1) { - await sleep(_.random(15000, 20000)) + await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) } } + + return attemptedMedals } /** * 在未开播的直播间发弹幕 * @param medals - * @private */ - 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] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - const item = MedalModule.findTaskInfo(taskInfo, 'sendDanmu') - // 从 title 解析点亮所需弹幕条数(如 "发弹幕10次" → 10),失败时 fallback 到 10 - let target = MedalModule.parseTitleCount(item?.title) ?? 10 + 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_name}】 无法获取主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息,跳过发弹幕任务`, + ) + continue + } + + const danmuItem = MedalModule.findTaskInfo(medalData.task_info, 'sendDanmu') + if (!danmuItem) { + this.logger.error( + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团点亮任务信息中找到发弹幕任务,跳过发弹幕任务`, + ) + continue + } + + let remaining = MedalModule.parseTitleCount(danmuItem.title) ?? 10 let failedCount = 0 - for (let j = 0; j < 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))) { if (++failedCount > MedalModule.DANMU_RETRY_LIMIT) { this.logger.warn(`当前直播间(${medal.room_info.room_id})弹幕发送失败次数过多,跳过`) - await sleep(_.random(6000, 8000)) + if (i < medals.length - 1) await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) break } - target++ + remaining++ } - if (i < medals.length - 1 || j < target - 1) { - await sleep(_.random(6000, 8000)) + if (i < medals.length - 1 || j < remaining - 1) { + await sleep(MedalModule.SEND_DANMU_DYNAMIC_INTERVAL) } } + + attemptedMedals.push(medal) } + + return attemptedMedals } public async run(): Promise { @@ -175,20 +262,45 @@ class LightTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() - 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 + let allCompleted = true + if (isEffectiveRun) { - await Promise.allSettled([this.sendDanmuTask(fansMedals[0]), this.likeTask(fansMedals[1])]) + const [danmuResult, likeResult] = await Promise.allSettled([ + this.sendDanmuTask(notLivingMedals), + this.likeTask(livingMedals), + ]) + // 尝试点亮过的粉丝勋章 + const attemptedMedals = [ + ...(danmuResult.status === 'fulfilled' ? danmuResult.value : []), + ...(likeResult.status === 'fulfilled' ? likeResult.value : []), + ] + + if (attemptedMedals.length > 0 || this.shouldRefreshFansMedals()) { + // 刷新粉丝勋章,确保 点亮任务自身以及 点赞/发弹幕/观看直播 任务都能拿到最新勋章状态 + await sleep(MedalModule.WAIT_MEDAL_UPDATE_DELAY) + const refreshed = await this.refreshFansMedals() + + if (attemptedMedals.length > 0) { + if (refreshed) { + allCompleted = this.verifyLightedMedals(attemptedMedals) + } else { + this.logger.warn('无法确认点亮任务是否完成,本轮按未完成处理') + allCompleted = false + } + } + } } - this.config._lastCompleteTime = tsm() - if (isEffectiveRun) { - this.config._lastEffectiveCompleteTime = this.config._lastCompleteTime + 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 60df241b..1c015ad3 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/likeTask.ts @@ -1,11 +1,11 @@ -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, TaskExecutionResult } from './types' class LikeTask extends MedalModule { config = this.medalTasksConfig.like @@ -15,20 +15,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 @@ -36,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 @@ -49,12 +65,128 @@ 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 + } + + /** + * 执行单个直播间的点赞任务 + * + * @returns 执行结果,包含是否中断以及是否确认完成 + */ + private async executeLikeTask(medal: LiveData.FansMedalPanel.List): Promise { + if (this.shouldStopForCrossDay()) { + this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') + return { interrupted: true, verifiedCompleted: false } + } + + 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_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 } + } + + const item = MedalModule.findTaskInfo(medalData.task_info, 'like') + if (!item) { + this.logger.error( + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${target_id},直播间:${room_id})的粉丝团升级任务信息中找到点赞任务,跳过点赞任务`, + ) + return { interrupted: false, verifiedCompleted: true } + } + + if (item.is_done) return { interrupted: false, verifiedCompleted: true } + + const parsed = MedalModule.parseDailyLimit(item.sub_title) + if (!parsed) { + this.logger.error( + `粉丝勋章【${medal_name}】 无法解析主播【${nick_name}】(UID:${target_id},直播间:${room_id})的点赞任务的每日上限信息,跳过点赞任务`, + ) + return { interrupted: false, verifiedCompleted: true } + } + + if (parsed.current >= parsed.limit) return { interrupted: false, verifiedCompleted: true } + + const times = MedalModule.parseTitleCount(item.title) ?? 30 + 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 { interrupted: true, verifiedCompleted: false } + } + + if (await this.like(medal, times)) { + hasSuccessfulLike = true + } + + if (j < remaining - 1) { + await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) + } + } + + if (hasSuccessfulLike) { + return { + interrupted: false, + verifiedCompleted: await this.confirmTaskCompletedAfterUpdate( + medal, + 'like', + this.config.useTargetRounds ? target : undefined, + ), + } + } + + // 所有点赞尝试均失败,估计是有什么异常状况,跳过 + return { interrupted: false, verifiedCompleted: true } + } + + /** + * 顺序执行多个直播间的点赞任务 + * + * @returns 执行结果,包含是否中断以及是否都确认完成 + */ + private async executeLikeTasks( + medals: LiveData.FansMedalPanel.List[], + ): Promise { + let verifiedCompleted = true + + for (let i = 0; i < medals.length; i++) { + const result = await this.executeLikeTask(medals[i]) + if (result.interrupted) return result + if (!result.verifiedCompleted) { + verifiedCompleted = false + } + + if (i < medals.length - 1) { + await sleep(MedalModule.LIKE_DYNAMIC_INTERVAL) + } + } + + return { interrupted: false, verifiedCompleted } } public async run(): Promise { @@ -70,65 +202,59 @@ class LikeTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() - 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)) { + const initialResult = await this.executeLikeTasks(readyMedals) + if (initialResult.interrupted) { + allCompleted = false + } else if (!initialResult.verifiedCompleted) { + allCompleted = false + } + + while (allCompleted && pendingRoomids.length > 0) { + if (this.shouldStopForCrossDay()) { this.logger.log('即将或刚刚发生跨天,提早结束本轮点赞任务') allCompleted = false break } - const medal = fansMedals[i] - const taskInfo = await this.fetchTaskInfo(medal.medal.target_id) - if (!taskInfo) { - this.logger.error( - `无法获取主播【${medal.anchor_info.nick_name}】(UID:${medal.medal.target_id})的粉丝团升级任务信息,跳过点赞任务`, - ) - continue - } - - const item = MedalModule.findTaskInfo(taskInfo, '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 waitResult = await this.executeLikeTasks(newlyReadyMedals) + if (waitResult.interrupted) { + allCompleted = false + break + } + if (!waitResult.verifiedCompleted) { + 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 + 5)) + if (pendingRoomids.length > 0) { + const medalMap = useBiliStore().filteredFansMedalsMap + const pendingRoomsInfo: Record = {} - if (j < remaining - 1 || i < fansMedals.length - 1) { - await sleep(_.random(15000, 20000)) + 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) } } + if (pendingRoomids.length > 0) { + allCompleted = false + } + if (allCompleted) { this.config._lastCompleteTime = tsm() this.status = 'done' diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts index 95b3d92f..1d68e08a 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/types.d.ts @@ -16,4 +16,29 @@ interface SharedMedalFilters { isLiving: (medal: LiveData.FansMedalPanel.List) => boolean } -export { MedalTaskSharedConfig, SharedMedalFilters } +type GroupedMedals = Record + +type RequestQueueKey = 'taskInfo' | 'roomStatus' +type TaskJumpType = 'like' | 'sendDanmu' | 'watchLive' | 'feedLight' | 'sendGift' +type WaitStrategy = 'single-probe' | 'refresh-fans-medals' + +interface WaitProbeResult { + readyMedals: LiveData.FansMedalPanel.List[] + pendingRoomids: number[] +} + +interface TaskExecutionResult { + interrupted: boolean + verifiedCompleted: boolean +} + +export { + MedalTaskSharedConfig, + SharedMedalFilters, + GroupedMedals, + RequestQueueKey, + TaskJumpType, + WaitStrategy, + WaitProbeResult, + TaskExecutionResult, +} diff --git a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts index dd5d648e..e53eb217 100644 --- a/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts +++ b/src/modules/dailyTasks/liveTasks/medalTasks/watchTask.ts @@ -3,9 +3,8 @@ 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 _ from 'lodash' import MedalModule from '@/modules/dailyTasks/liveTasks/medalTasks/MedalModule' import type { LiveData } from '@/library/bili-api/data' @@ -69,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] @@ -84,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 } /** @@ -319,46 +321,50 @@ class WatchTask extends MedalModule { } this.status = 'running' - MedalModule.clearTaskInfoCache() 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() 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 } 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 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})的粉丝团升级任务信息,跳过观看直播任务`, + `粉丝勋章【${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 } - 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})的粉丝团升级任务信息中找到观看直播任务,跳过观看直播任务`, + `粉丝勋章【${medal_name}】 无法在主播【${nick_name}】(UID:${uid},直播间:${roomid})的粉丝团升级任务信息中找到观看直播任务,跳过观看直播任务`, ) continue } @@ -368,16 +374,20 @@ 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 } 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.useTargetRounds + ? Math.min(parsed.limit, this.config.targetRounds) + : parsed.limit + // 目标秒数 = 目标轮次 × minutes × 60s + const targetSeconds = target * minutes * 60 if ((this.config._watchingProgress[roomid] ?? 0) >= targetSeconds) { // 今日观看时间已达到目标值,跳过 @@ -389,10 +399,10 @@ 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} 分钟)`, ) - await new RoomHeart( + const hasWatchingProgress = await new RoomHeart( roomid, area_id, parent_area_id, @@ -400,11 +410,23 @@ class WatchTask extends MedalModule { this.config._watchingProgress[roomid] ?? 0, targetSeconds, ).start() + + if (hasWatchingProgress) { + const verifiedCompleted = await this.confirmTaskCompletedAfterUpdate( + medal, + 'watchLive', + this.config.useTargetRounds ? target : undefined, + ) + if (!verifiedCompleted) { + allCompleted = false + } + } else { + allCompleted = false + } } } - if (i === fansMedals.length) { - // 没有提早跳出循环,说明所有直播间的观看任务均已完成 + if (allCompleted) { this.config._lastCompleteTime = tsm() this.logger.log('观看直播任务已完成') this.status = 'done' 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/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/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/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/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/src/modules/default/fansMedals.ts b/src/modules/default/fansMedals.ts index 85fc4f4f..80749594 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' @@ -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 } @@ -49,7 +46,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 +68,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/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/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/useBiliStore.ts b/src/stores/useBiliStore.ts index 46c52ee9..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', () => { @@ -21,8 +21,12 @@ export const useBiliStore = defineStore('bili', () => { const filteredFansMedals = computed( () => fansMedals.value?.filter((m) => m.room_info.room_id !== 0) ?? [], ) - // 粉丝勋章获取状态(初始值:undefined,获取中:loading,获取成功:loaded,获取失败:error) - const fansMedalsStatus = ref() + // 过滤了不存在直播间的粉丝勋章Map(key:直播间号,value:粉丝勋章对象) + const filteredFansMedalsMap = computed>( + () => new Map(filteredFansMedals.value.map((m) => [m.room_info.room_id, m])), + ) + // 粉丝勋章相关元数据 + const fansMedalsMeta = ref({}) // wbi 签名所需的盐值 const wbiSalt = computed(() => { if (!userInfo.value) { @@ -52,7 +56,8 @@ export const useBiliStore = defineStore('bili', () => { dynamicVideos, fansMedals, filteredFansMedals, - fansMedalsStatus, + filteredFansMedalsMap, + 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/stores/useModuleStore.ts b/src/stores/useModuleStore.ts index 2370d3eb..02b3f95b 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[] = [] @@ -50,7 +52,7 @@ export const useModuleStore = defineStore('module', () => { // 模块实例映射(key: 模块名称, value: 模块实例) const moduleInstances = ref>({}) // 模块状态、运行记录重置和再运行 - const moduleReset = ref({ + const moduleReset: ModuleReset = { DailyTasks: { MainSiteTasks: { login: async () => { @@ -58,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 = '' @@ -68,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 = '' @@ -94,8 +96,14 @@ 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 rerunModule('Default_FansMedals', true) @@ -147,7 +155,7 @@ export const useModuleStore = defineStore('module', () => { }, }, }, - }) + } /** * 运行模块 @@ -175,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)!) } } @@ -248,7 +258,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 } } @@ -293,8 +303,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..a70410c8 100644 --- a/src/stores/usePlayerStore.ts +++ b/src/stores/usePlayerStore.ts @@ -44,7 +44,7 @@ export const usePlayerStore = defineStore('player', () => { const getPlayer = async ( config: PlayerStoreConfig['getPlayer'] = DEFAULT_CONFIG.getPlayer, ): Promise => { - _.defaults(config, DEFAULT_CONFIG.getPlayer) + const mergedConfig = _.defaults({}, config, DEFAULT_CONFIG.getPlayer) if (player.value) { return player.value @@ -69,12 +69,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 +89,18 @@ export const usePlayerStore = defineStore('player', () => { status: 0 | 1, config: PlayerStoreConfig['waitForLiveStatus'] = DEFAULT_CONFIG.waitForLiveStatus, ): Promise => { - _.defaults(config, DEFAULT_CONFIG.waitForLiveStatus) + const mergedConfig = _.defaults({}, 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 +110,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) }) } 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 } 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..394ed405 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -26,11 +26,13 @@ interface ModuleConfig { danmuList: string[] isWhiteList: boolean roomidList: number[] - _lastEffectiveCompleteTime: number _lastCompleteTime: number } like: { enabled: boolean + useTargetRounds: boolean + targetRounds: number + waitUntilLiving: boolean isWhiteList: boolean roomidList: number[] _lastCompleteTime: number @@ -38,13 +40,18 @@ interface ModuleConfig { danmu: { enabled: boolean danmuList: string[] + useTargetRounds: boolean + targetRounds: number onlyWhenNotLiving: boolean + waitUntilNotLiving: boolean isWhiteList: boolean roomidList: number[] _lastCompleteTime: number } watch: { enabled: boolean + useTargetRounds: boolean + targetRounds: number isWhiteList: boolean roomidList: number[] _watchingProgress: Record 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*',