├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── configuration.yml │ ├── docs.yml │ └── need.yml ├── pull_request_template.md └── workflows │ ├── build-dev.yaml │ ├── build-docker.yaml │ ├── dependency-review.yml │ ├── gh-release.yaml │ └── npm.yaml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.mjs ├── build ├── .baidu.babelrc.mjs ├── .yarnclean ├── babel.base.mjs └── rollup.config.base.ts ├── config └── config.single.json5 ├── docker-compose.yml ├── docker-entrypoint.sh ├── jest.config.ts ├── package.json ├── patches └── @catlair+bilicomic-dataflow+0.0.4.patch ├── rollup.config.ts ├── src ├── __test__ │ ├── args.test.ts │ ├── array.test.ts │ ├── bili.test.ts │ ├── code.test.ts │ ├── config.test.ts │ ├── cookie.test.ts │ ├── delay.test.ts │ ├── gzip.test.ts │ ├── header.test.ts │ ├── object.test.ts │ ├── stringify.test.ts │ └── version.test.ts ├── bin.ts ├── bin │ ├── fork.ts │ ├── inputTask.ts │ └── util.ts ├── config │ ├── config.ts │ ├── configOffFun.ts │ ├── globalVar.ts │ ├── setConfig.ts │ └── systemConfig.ts ├── constant │ ├── bangumi.ts │ ├── biliUri.ts │ ├── dm.ts │ └── index.ts ├── dto │ ├── activity-lottery.dto.ts │ ├── big-point.dto.ts │ ├── bili-base-prop.ts │ ├── bili-ws.dto.ts │ ├── coin.dto.ts │ ├── daily-battery.dto.ts │ ├── intimacy.dto.ts │ ├── jury.dto.ts │ ├── live.dto.ts │ ├── manga.dto.ts │ ├── match-game.dto.ts │ ├── red-packet.dto.ts │ ├── reservation.dto.ts │ ├── session.dto.ts │ ├── sup-group.dto.ts │ ├── user-info.dto.ts │ ├── video.dto.ts │ └── vip-privilege.dto.ts ├── enums │ ├── activity-lottery.emum.ts │ ├── big-point.emum.ts │ ├── coin.emum.ts │ ├── env.emum.ts │ ├── http.enum.ts │ ├── intimacy.emum.ts │ ├── jury.emum.ts │ ├── live-lottery.enum.ts │ ├── manga-game.emum.ts │ ├── manga.emum.ts │ └── packet.enum.ts ├── env.d.ts ├── index.cfc.ts ├── index.fc.ts ├── index.ql.ts ├── index.scf.ts ├── index.ts ├── main.d.ts ├── main.ts ├── net │ ├── activity-lottery.request.ts │ ├── api.ts │ ├── big-point.request.ts │ ├── coin.request.ts │ ├── daily-battery.request.ts │ ├── intimacy.request.ts │ ├── jury.request.ts │ ├── live-heart.request.ts │ ├── live.request.ts │ ├── manga.request.ts │ ├── match-game.request.ts │ ├── red-packet.request.ts │ ├── reservation.request.ts │ ├── session.request.ts │ ├── sup-group.request.ts │ ├── user-info.request.ts │ ├── video.request.ts │ └── vip.request.ts ├── service │ ├── activity-lottery.service.ts │ ├── auth.service.ts │ ├── balance.service.ts │ ├── bangumi.service.ts │ ├── big-point.service.ts │ ├── coin.service.ts │ ├── daily-battery.service.ts │ ├── dm.service.ts │ ├── intimacy.service.ts │ ├── jury.service.ts │ ├── live-lottery.service.ts │ ├── live.service.ts │ ├── manga.service.ts │ ├── nav.service.ts │ ├── red-pack.service.ts │ ├── reservation.service.ts │ ├── reward.service.ts │ ├── session.service.ts │ ├── tags.service.ts │ └── ws.service.ts ├── store │ └── red-packet.ts ├── task │ ├── LiveReservation.ts │ ├── activityLottery.ts │ ├── addCoins.ts │ ├── batchUnfollow.ts │ ├── beforeTask.ts │ ├── bigPoint.ts │ ├── dailyBattery.ts │ ├── dailyTask.ts │ ├── exchangeCoupon.ts │ ├── getNewCookie.ts │ ├── getVipPrivilege.ts │ ├── giveGift.ts │ ├── index.ts │ ├── judgement.ts │ ├── liveIntimacy.ts │ ├── liveLottery.ts │ ├── liveRedPack.ts │ ├── liveSignTask.ts │ ├── loginTask.ts │ ├── mangaTask.ts │ ├── matchGame.ts │ ├── noLoginTask.ts │ ├── shareAndWatch.ts │ ├── silver2Coin.ts │ ├── supGroupSign.ts │ └── useCouponBp.ts ├── types │ ├── @alicloud__fc2.d.ts │ ├── LiveHeart.ts │ ├── config.ts │ ├── fc.ts │ ├── global.d.ts │ ├── got.ts │ ├── index.ts │ ├── log.ts │ ├── request.ts │ ├── scf.ts │ ├── sls.d.ts │ └── type.d.ts ├── util.ts └── utils │ ├── args.ts │ ├── bili.ts │ ├── cookie.ts │ ├── effect.ts │ ├── env.ts │ ├── file.ts │ ├── got │ ├── BiliGot.ts │ ├── bili.ts │ ├── config.ts │ └── index.ts │ ├── gzip.ts │ ├── http │ ├── def.ts │ └── index.ts │ ├── index.ts │ ├── is.ts │ ├── json5.ts │ ├── log │ ├── def.ts │ ├── file.ts │ ├── index.ts │ └── std.ts │ ├── login.ts │ ├── path.ts │ ├── pure.ts │ ├── request.ts │ ├── sendNotify.ts │ ├── serverless │ ├── index.ts │ ├── updateFcTrigger.ts │ └── updateScfTrigger.ts │ ├── version.ts │ └── ws │ └── index.ts ├── tools ├── bilitools_login.js ├── bilitools_npm.js ├── download.ts ├── giteeConfig.ts ├── giteeRelease.ts ├── pkgclean.js └── processConfig.js ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .github/ 3 | README.md 4 | .prettierrc 5 | .git 6 | .gitignore 7 | .dockerignore 8 | Dockerfile 9 | .env 10 | LICENSE 11 | test/ 12 | dist/ 13 | temp/ 14 | config/config.demo.jsonc 15 | config/config.json 16 | config/config.txt 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | /temp/** 3 | /config/* 4 | .idea 5 | .vscode 6 | .git 7 | .gitignore 8 | node_modules/** 9 | qinglong/**/*.js 10 | src/utils/sendNotify.* 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es6: true, 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | 'plugin:prettier/recommended', 12 | ], 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | 'no-empty': ['error', { allowEmptyCatch: true }], 16 | '@typescript-eslint/no-unused-vars': [ 17 | 'error', 18 | { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^ignore' }, 19 | ], 20 | '@typescript-eslint/ban-ts-comment': 'off', 21 | '@typescript-eslint/no-var-requires': 'off', 22 | '@typescript-eslint/no-explicit-any': [ 23 | 'off', 24 | { 25 | ignoreRestArgs: true, 26 | fixToUnknown: true, 27 | }, 28 | ], 29 | '@typescript-eslint/no-non-null-assertion': 'off', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 错误报告 2 | description: 如果您发现了一个错误,请在此提交您的错误报告。 3 | title: '[Bug]: ' 4 | labels: ['bug'] 5 | assignees: 6 | - octocat 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间填写这个 bug 报告! 12 | 在你填写之前你应该确认看过相关文档,然后查找 issue,确定该问题没有被提到(除非原问题已经被关闭)然后在提交该报告。 13 | - type: textarea 14 | id: env 15 | attributes: 16 | label: 运行环境和版本 17 | description: 您的程序是运行在什么环境下的,是什么版本的软件?如果在使用 npm 可以将 `bilitools -v` 的结果输入。 18 | placeholder: 腾讯云函数、本地 Windows11、MacOS、青龙面板 2.13.2 等,版本 0.5.1。 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: what-happened 23 | attributes: 24 | label: 发生了什么 25 | description: 还有,告诉我们,你希望发生什么,能否复现? 26 | placeholder: 告诉我们你看到了什么! 27 | value: '' 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: what-expected 32 | attributes: 33 | label: 预期的结果 34 | description: 如果没有错误,将会是什么样的? 35 | placeholder: 告诉我们它应该是怎么样的! 36 | value: '' 37 | - type: textarea 38 | id: logs 39 | attributes: 40 | label: 相关的日志输出 41 | description: 请复制并粘贴任何相关的日志输出。 这将自动格式化为代码,因此无需背板。 42 | render: shell 43 | - type: textarea 44 | id: config 45 | attributes: 46 | label: 相关的配置,注意个人隐私。 47 | description: 请复制并粘贴任何相关配置(例如 jury 问题直接贴出 jury 部分的配置)。 48 | render: json 49 | - type: textarea 50 | id: help-me 51 | attributes: 52 | label: 有效的帮助 53 | description: 对此您能提供什么有效的帮助? 54 | placeholder: 告诉我们您能提供的帮助! 55 | value: '这个接口是这样的。。。' 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 文档 4 | url: https://btdocs.vercel.app/ 5 | about: 仔细阅读文档 6 | - name: 其他联系方式 7 | url: https://btdocs.vercel.app/about.html 8 | about: 希望能够在Github之外联系 9 | - name: 说点什么 10 | url: https://github.com/KudouRan/BiliTools/discussions 11 | about: 如果您不是反馈问题,只是单纯聊聊,请使用 discussions 频道 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/configuration.yml: -------------------------------------------------------------------------------- 1 | name: 配置问题 2 | description: 如果您配置出错或者想要了解到底发生了什么,请在此提交您的配置问题。 3 | title: '[配置]: ' 4 | labels: ['疑问'] 5 | assignees: 6 | - octocat 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间填写这个配置报告!我们希望您已经做到应该做的事情。例如: 12 | - 已经校验了 json5 文件。 13 | - 当您使用环境变量时,是已经压缩了配置的。 14 | - 已经再三确认了配置文件,确认路径是正确的。 15 | - type: checkboxes 16 | id: terms 17 | attributes: 18 | label: 是否已经阅读文档 19 | description: 已经阅读过[文档](https://btdocs.vercel.app/),确定没有找到想要的答案。 20 | options: 21 | - label: 我确定阅读过文档 22 | required: true 23 | - type: textarea 24 | id: env 25 | attributes: 26 | label: 运行环境和版本 27 | description: 您的程序是运行在什么环境下的,是什么版本的软件?如果在使用 npm 可以将 `bilitools -v` 的结果输入。 28 | placeholder: 腾讯云函数、本地 Windows11、MacOS、青龙面板 2.13.2 等,版本 0.5.1。 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: what-happened 33 | attributes: 34 | label: 发生了什么 35 | description: 告诉我们你做了什么,发生了什么。还有,告诉我们,你希望发生什么? 36 | placeholder: 告诉我们你看到了什么! 37 | value: '我对此存在疑问。' 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: logs 42 | attributes: 43 | label: 相关的日志输出 44 | description: 请复制并粘贴任何相关的日志输出。 这将自动格式化为代码,因此无需背板。 45 | render: shell 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yml: -------------------------------------------------------------------------------- 1 | name: 文档改进 2 | description: 如果您发现了一个文档错误,或者想要改进文档,请在此提交您的错误报告。 3 | title: '[文档]: ' 4 | labels: ['文档'] 5 | assignees: 6 | - octocat 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间填写这个报告! 12 | 如果想要直接 PR 请提交 13 | - type: textarea 14 | id: what-happened 15 | attributes: 16 | label: 问题描述 17 | description: 详细的描述您的问题。 18 | placeholder: 告诉问题出在哪里,以及您确定它是问题的理由 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: help-me 23 | attributes: 24 | label: 有效的帮助 25 | description: 对此您能提供什么有效的帮助? 26 | placeholder: 告诉我们您能提供的帮助! 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/need.yml: -------------------------------------------------------------------------------- 1 | name: 需求请求 2 | description: 如果您有什么好的想法或者需求,请在此提交您的需求。 3 | title: '[新增]: ' 4 | labels: ['新增'] 5 | assignees: 6 | - octocat 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间填写这个需求报告!我们希望您已经查看文档和 Issue,确认该需求是新的。 12 | - type: input 13 | id: exclusive 14 | attributes: 15 | label: 需求是否是平台独享受的 16 | description: 例如这个需求是否只能在阿里云函数实现? 17 | placeholder: 腾讯云函数、本地 Windows11、MacOS等 18 | - type: textarea 19 | id: what-happened 20 | attributes: 21 | label: 需求描述 22 | description: 详细的描述您的需求。 23 | placeholder: 告诉我们您想说的话! 24 | value: '我想。。。' 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: help-me 29 | attributes: 30 | label: 有效的帮助 31 | description: 对此您能提供什么有效的帮助? 32 | placeholder: 告诉我们您能提供的帮助! 33 | value: '这个接口是这样的。。。' 34 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 描述一下做了什么 2 | -------------------------------------------------------------------------------- /.github/workflows/build-dev.yaml: -------------------------------------------------------------------------------- 1 | name: build-npm-docker 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | beta: 7 | description: '是否使用 beta 版本' 8 | required: true 9 | default: 'true' 10 | tag: 11 | description: 'tag' 12 | required: true 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | environment: Build 18 | env: 19 | IS_BETA: ${{ github.event.inputs.beta == 'true' }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | # Setup .npmrc file to publish to npm 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 'lts/*' 26 | registry-url: 'https://registry.npmjs.org' 27 | - name: Install dependencies 28 | run: | 29 | rm yarn.lock 30 | cat package.json | jq "del( .optionalDependencies ) | .version = \"${{ github.event.inputs.tag }}\"" > temp.json 31 | mv temp.json package.json 32 | yarn install 33 | rm yarn.lock 34 | - name: Build and Handle 35 | run: | 36 | yarn build 37 | - run: npm publish ${{ env.IS_BETA == 'true' && '--tag beta' || '' }} 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | build-docker: 41 | runs-on: ubuntu-latest 42 | needs: build 43 | environment: Build 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v2 47 | - name: set Docker tag 48 | run: | 49 | sed -i 2a\echo\ version=${{ github.event.inputs.tag }} docker-entrypoint.sh 50 | echo ${{ github.event.inputs.tag }} >> src/version.txt 51 | - name: Set up QEMU 52 | uses: docker/setup-qemu-action@v1 53 | - name: Set up Docker Buildx 54 | uses: docker/setup-buildx-action@v1 55 | - name: Login to DockerHub 56 | uses: docker/login-action@v1 57 | with: 58 | username: ${{ secrets.DOCKER_USERNAME }} 59 | password: ${{ secrets.DOCKER_PASSWORD }} 60 | - name: Build and Push 61 | id: docker_build 62 | uses: docker/build-push-action@v2 63 | with: 64 | context: . 65 | push: true 66 | platforms: ${{ secrets.DOCKER_PLATFORMS || 'linux/amd64,linux/arm64/v8' }} 67 | tags: catlair/bilitools:${{ github.event.inputs.beta == 'true' && 'dev' || 'latest' }},catlair/bilitools:${{ github.event.inputs.tag }} 68 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | name: build-docker 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | beta: 7 | description: '是否使用 beta 版本' 8 | required: true 9 | default: 'true' 10 | tag: 11 | description: 'tag' 12 | required: true 13 | 14 | jobs: 15 | build-docker: 16 | runs-on: ubuntu-latest 17 | environment: Build 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | - name: set Docker tag 22 | run: | 23 | sed -i 2a\echo\ version=${{ github.event.inputs.tag }} docker-entrypoint.sh 24 | echo ${{ github.event.inputs.tag }} >> src/version.txt 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v1 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v1 29 | - name: Login to DockerHub 30 | uses: docker/login-action@v1 31 | with: 32 | username: ${{ secrets.DOCKER_USERNAME }} 33 | password: ${{ secrets.DOCKER_PASSWORD }} 34 | - name: Build and Push 35 | id: docker_build 36 | uses: docker/build-push-action@v2 37 | with: 38 | context: . 39 | push: true 40 | platforms: ${{ secrets.DOCKER_PLATFORMS || 'linux/amd64,linux/arm64/v8' }} 41 | tags: catlair/bilitools:${{ github.event.inputs.beta == 'true' && 'dev' || 'latest' }},catlair/bilitools:${{ github.event.inputs.tag }} 42 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v1 21 | -------------------------------------------------------------------------------- /.github/workflows/npm.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | environment: Build 12 | env: 13 | IS_RC: ${{ contains(github.ref, '-rc') }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | # Setup .npmrc file to publish to npm 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 'lts/*' 20 | registry-url: 'https://registry.npmjs.org' 21 | - name: set version 22 | run: | 23 | echo BILI_VERSION=${{ github.ref }} | sed -e "s/refs\/tags\/v//g" >> $GITHUB_ENV 24 | - name: Install dependencies 25 | run: | 26 | rm yarn.lock 27 | cat package.json | jq "del( .optionalDependencies ) | .version = \"${{ env.BILI_VERSION }}\"" > temp.json 28 | mv temp.json package.json 29 | yarn install 30 | rm yarn.lock 31 | - name: Build and Handle 32 | run: | 33 | yarn build 34 | rm -rf yarn.lock node_modules 35 | - run: npm publish ${{ env.IS_RC == 'true' && '--tag beta' || '' }} 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | build-docker: 39 | if: ${{ contains(github.ref, '-rc') == false }} 40 | needs: build 41 | runs-on: ubuntu-latest 42 | environment: Build 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v2 46 | - name: set Docker version tag 47 | run: | 48 | echo DOCKER_TAG1=${{ github.ref }} | sed -e "s/refs\/tags\/v//g" >> $GITHUB_ENV 49 | - name: set Docker latest tag 50 | run: | 51 | echo DOCKER_TAG2=${{ contains(env.DOCKER_TAG1, 'rc') && 'test' || 'latest' }} >> $GITHUB_ENV 52 | sed -i 2a\echo\ version=${{ env.DOCKER_TAG1 }} docker-entrypoint.sh 53 | echo ${{ env.DOCKER_TAG1 }} >> src/version.txt 54 | - name: Set up QEMU 55 | uses: docker/setup-qemu-action@v1 56 | - name: Set up Docker Buildx 57 | uses: docker/setup-buildx-action@v1 58 | - name: Login to DockerHub 59 | uses: docker/login-action@v1 60 | with: 61 | username: ${{ secrets.DOCKER_USERNAME }} 62 | password: ${{ secrets.DOCKER_PASSWORD }} 63 | - name: Build and Push 64 | id: docker_build 65 | uses: docker/build-push-action@v2 66 | with: 67 | context: . 68 | push: true 69 | platforms: ${{ secrets.DOCKER_PLATFORMS || 'linux/amd64,linux/arm64/v8' }} 70 | tags: catlair/bilitools:${{ env.DOCKER_TAG1 }},catlair/bilitools:${{ env.DOCKER_TAG2 }} 71 | - name: Image digest 72 | run: | 73 | echo "镜像对应hash" 74 | echo ${{ steps.docker_build.outputs.digest }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/** 3 | dist/ 4 | temp/** 5 | /config/* 6 | !config/config.example.json[$|5] 7 | !config/config.single.json[$|5] 8 | .idea 9 | /src/config/config.json 10 | yarn-error.log 11 | /logs 12 | /bin 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.12.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | /temp/** 3 | .idea 4 | dist/ 5 | qinglong/cat_bili_ql.js 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "printWidth": 100, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "statusBar.debuggingBackground": "#c06f98", 4 | "statusBar.foreground": "#a2b4bd", 5 | "editorBracketMatch.background": "#5a6f75", 6 | "activityBar.background": "#05351E", 7 | "titleBar.activeBackground": "#074A2A", 8 | "titleBar.activeForeground": "#EFFEF7" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | WORKDIR /usr/src/app 3 | ENV SERVERLESS_PLATFORM_VENDOR=tencent 4 | # 国内构建 5 | # RUN npm config set registry https://registry.npm.taobao.org 6 | COPY ./docker-entrypoint.sh ./tools/processConfig.js ./ 7 | RUN chmod +x docker-entrypoint.sh \ 8 | && npm install -g @catlair/bilitools \ 9 | && npm cache clean --force 10 | ENTRYPOINT ["./docker-entrypoint.sh"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OceanJiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.mjs: -------------------------------------------------------------------------------- 1 | import { baseConfig } from './build/babel.base.mjs'; 2 | 3 | export default function () { 4 | return baseConfig(); 5 | } 6 | -------------------------------------------------------------------------------- /build/.baidu.babelrc.mjs: -------------------------------------------------------------------------------- 1 | import { baseConfig } from './babel.base.mjs'; 2 | 3 | export default function () { 4 | return baseConfig({ node: '12.2' }); 5 | } 6 | -------------------------------------------------------------------------------- /build/.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | appveyor.yml 29 | circle.yml 30 | codeship-services.yml 31 | codeship-steps.yml 32 | wercker.yml 33 | .tern-project 34 | .gitattributes 35 | .editorconfig 36 | .*ignore 37 | .eslintrc 38 | .jshintrc 39 | .flowconfig 40 | .documentup.json 41 | .yarn-metadata.json 42 | .travis.yml 43 | 44 | # misc 45 | *.md 46 | 47 | @types 48 | *.d.ts 49 | *.ts 50 | LICENSE 51 | .eslintrc.json 52 | bower.json 53 | .babelrc 54 | .idea 55 | *.html 56 | prettier.config.js 57 | .prettierrc.js 58 | tsconfig.json 59 | .nycrc 60 | .github 61 | *.svg 62 | *.markdown 63 | *.log 64 | .yarn-integrity 65 | tslint.json 66 | *.map 67 | yarn.lock 68 | README.md.bak 69 | .jscs.json 70 | LICENSE-MIT 71 | LICENSE-MIT.txt 72 | README 73 | LICENSE.txt 74 | CopyrightNotice.txt 75 | .gitkeep 76 | .dockerignore 77 | .prettierignore 78 | .prettierrc 79 | .eslintignore 80 | .eslintrc.js 81 | MIT-LICENSE.txt 82 | .zuul.yml 83 | .prettierrc.yaml 84 | LICENSE.MIT 85 | LICENSE.APACHE2 86 | LICENSE. 87 | LICENSE.BSD 88 | .jscsrc 89 | AUTHORS 90 | .package-lock.json 91 | -------------------------------------------------------------------------------- /build/babel.base.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {{ node?: string; }} [options] 3 | */ 4 | export function baseConfig(options = {}) { 5 | const { node = '14' } = options; 6 | return { 7 | presets: [ 8 | [ 9 | '@babel/env', 10 | { 11 | useBuiltIns: 'usage', 12 | corejs: 3.26, 13 | targets: { 14 | node, 15 | }, 16 | }, 17 | ], 18 | '@babel/preset-typescript', 19 | ], 20 | comments: false, 21 | plugins: [ 22 | [ 23 | 'module-resolver', 24 | { 25 | alias: { 26 | '@': './src', 27 | '#': './src/types', 28 | }, 29 | }, 30 | ], 31 | ], 32 | ignore: ['**/__test__', '**/*.test.ts', '**/*.spec.ts', '**/types', '**/dto'], 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /build/rollup.config.base.ts: -------------------------------------------------------------------------------- 1 | import type { RollupOptions } from 'rollup'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import json from '@rollup/plugin-json'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import babel from '@rollup/plugin-babel'; 6 | import nodeResolve from '@rollup/plugin-node-resolve'; 7 | import sizes from 'rollup-plugin-sizes'; 8 | import typescript from '@rollup/plugin-typescript'; 9 | import replace from '@rollup/plugin-replace'; 10 | import { wasm } from '@rollup/plugin-wasm'; 11 | 12 | let pkgJson: Record; 13 | try { 14 | pkgJson = require('../package.json'); 15 | } catch { 16 | pkgJson = require('./package.json'); 17 | } 18 | 19 | export const extensions = ['.ts', '.js']; 20 | export const plugins = ({ node, replaceValues = {}, noTerser }: BaseConfigOption) => { 21 | return [ 22 | nodeResolve({ 23 | preferBuiltins: true, 24 | }), 25 | typescript({ 26 | module: 'ESNext', 27 | }), 28 | wasm({ 29 | maxFileSize: 50 * 1024, 30 | targetEnv: 'node', 31 | }), 32 | replace({ 33 | preventAssignment: true, 34 | values: { 35 | __BILI_VERSION__: `v${pkgJson.version}`, 36 | ...replaceValues, 37 | }, 38 | }), 39 | babel({ 40 | extensions, 41 | babelHelpers: 'bundled', 42 | exclude: 'node_modules/**', 43 | babelrc: false, 44 | presets: [ 45 | [ 46 | '@babel/env', 47 | { 48 | useBuiltIns: 'usage', 49 | corejs: 3.25, 50 | targets: { 51 | node: node || '14', 52 | }, 53 | modules: false, 54 | }, 55 | ], 56 | ], 57 | }), 58 | commonjs({ 59 | extensions, 60 | }), 61 | json(), 62 | noTerser ? undefined : terser(), 63 | sizes(), 64 | ].filter(Boolean); 65 | }; 66 | const optionalDependencies = Object.keys(pkgJson.optionalDependencies); 67 | const EXTERNAL = [...Object.keys(pkgJson.dependencies), ...optionalDependencies]; 68 | // 手动排除这些以前就有的依赖,减少体积 69 | const baseDependencies = ['core-js', 'got', 'nodemailer', 'tunnel']; 70 | export const vmDependencies = [...optionalDependencies, ...baseDependencies]; 71 | 72 | interface BaseConfigOption { 73 | input: string; 74 | output: string; 75 | node?: string; 76 | external?: boolean | string[]; 77 | replaceValues?: Record; 78 | noTerser?: boolean; 79 | } 80 | 81 | export function createBaseConfig( 82 | config: BaseConfigOption, 83 | options: RollupOptions = {}, 84 | callback?: (option: RollupOptions) => RollupOptions, 85 | ): RollupOptions { 86 | const external = Array.isArray(config.external) 87 | ? config.external 88 | : config.external === false 89 | ? optionalDependencies 90 | : EXTERNAL; 91 | const rOptions: RollupOptions = { 92 | plugins: plugins(config), 93 | input: `src/${config.input}`, 94 | output: { 95 | file: `dist/rollup/${config.output}`, 96 | format: 'cjs', 97 | inlineDynamicImports: true, 98 | }, 99 | external, 100 | }; 101 | return { 102 | ...rOptions, 103 | ...options, 104 | ...(callback?.(rOptions) || {}), 105 | }; 106 | } 107 | 108 | export function createRollupOption(options: RollupOptions[]) { 109 | return options; 110 | } 111 | -------------------------------------------------------------------------------- /config/config.single.json5: -------------------------------------------------------------------------------- 1 | { 2 | function: { 3 | silver2Coin: false, 4 | liveSignTask: true, 5 | addCoins: false, 6 | }, 7 | dailyRunTime: '09:17:19-20:23:21', 8 | apiDelay: [4, 10], 9 | userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36 Edg/91.0.864.48', 10 | cookie: "(这是虚假的,请不要把cookie上传到github)_uuid=EAJKLS4-4515-5321-8A5D-9FE2087F964051402infoc; buvid3=2F6A89C5-5E75-4000-9082-D917B2691ASKLDJA1infoc; sid=5p6e1yhf; buvid_fp=2F6A89C5-5E75-4000-9082-D917B2691ASKLDJA1infoc; DedeUserID=12312312; DedeUserID__ckMd5=123h4h3g5hf5y435k; SESSDATA=sad23sdf%asdasdasdasd%2C1afeb*61; bili_jct=asdask2l3h42g34j1l2h4k; fingerprint3=3lk4j5h1g31fh; buvid_fp_plain=2F6A89C5-5E75-4000-9082-D917B2691F3613451infoc; CURRENT_FNVAL=80; blackside_state=1; rpdid=|(ku|k~k|)uk0J'uYkJYYYlJ); PVID=1; fingerprint=8a3f1f0c1c51667648fb746216071380; fingerprint_s=77bd96f129ea0e4aa7f0d53a356454f3; LIVE_BUVID=AUTO23546232463931; bp_video_offset_17248133=553263870170650890", 11 | message: {}, 12 | coin: { 13 | /** 自定义高优先级用户列表 */ 14 | customizeUp: [], 15 | /** 投币操作重试次数 默认 4 */ 16 | retryNum: 4, 17 | /** 目标等级 默认6级 */ 18 | targetLevel: 6, 19 | /** 最低剩余硬币数,默认0 */ 20 | stayCoins: 300, 21 | /** 预计投币数,默认5 */ 22 | targetCoins: 5, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | bili_task: 4 | image: catlair/bilitools # 按需选择版本 5 | container_name: catlair_bilitools 6 | volumes: # 请注意路径是否正确 7 | - ./config/:/usr/src/app/config/ 8 | tty: true 9 | env_file: .env # 请注意路径是否正确 10 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # 如果 config 目录不存在,则创建 5 | if [ ! -d "./config" ]; then 6 | mkdir -p ./config 7 | fi 8 | 9 | node processConfig.js 10 | 11 | bilitools -v 12 | 13 | bilitools -c /usr/src/app/config/config.json $@ 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: 'src/', 3 | transform: { 4 | '^.+\\.js?$': 'babel-jest', 5 | '^.+\\.(ts|tsx)$': 'ts-jest', 6 | }, 7 | moduleNameMapper: { 8 | '@/(.*)$': '/$1', 9 | '#/(.*)$': '/types/$1', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /patches/@catlair+bilicomic-dataflow+0.0.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@catlair/bilicomic-dataflow/bilicomic_dataflow.js b/node_modules/@catlair/bilicomic-dataflow/bilicomic_dataflow.js 2 | index e48bf3f..6d1148c 100644 3 | --- a/node_modules/@catlair/bilicomic-dataflow/bilicomic_dataflow.js 4 | +++ b/node_modules/@catlair/bilicomic-dataflow/bilicomic_dataflow.js 5 | @@ -1,7 +1,7 @@ 6 | let imports = {}; 7 | imports['__wbindgen_placeholder__'] = module.exports; 8 | let wasm; 9 | -const { fetch } = require(String.raw`./snippets/bilicomic-dataflow-eb673a8c566a4244/defined-in-js.js`); 10 | +const { fetch } = require('./snippets/bilicomic-dataflow-eb673a8c566a4244/defined-in-js.js'); 11 | const { TextDecoder, TextEncoder } = require(`util`); 12 | 13 | const heap = new Array(32).fill(undefined); 14 | @@ -298,11 +298,10 @@ module.exports.__wbindgen_closure_wrapper65 = function(arg0, arg1, arg2) { 15 | return addHeapObject(ret); 16 | }; 17 | 18 | -const path = require('path').join(__dirname, 'bilicomic_dataflow_bg.wasm'); 19 | -const bytes = require('fs').readFileSync(path); 20 | +const bilicomic_dataflow_bg = require('./bilicomic_dataflow_bg.wasm'); 21 | 22 | -const wasmModule = new WebAssembly.Module(bytes); 23 | -const wasmInstance = new WebAssembly.Instance(wasmModule, imports); 24 | -wasm = wasmInstance.exports; 25 | -module.exports.__wasm = wasm; 26 | +bilicomic_dataflow_bg({ ...imports }).then(({ instance }) => { 27 | + wasm = instance.exports 28 | +}); 29 | 30 | +module.exports.__wasm = wasm; 31 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createBaseConfig, createRollupOption } from './build/rollup.config.base'; 2 | 3 | export default createRollupOption([ 4 | createBaseConfig({ 5 | input: 'index.cfc.ts', 6 | output: 'index.agc.js', 7 | replaceValues: { 8 | __IS_AGC__: 'true', 9 | }, 10 | external: false, 11 | }), 12 | ]); 13 | -------------------------------------------------------------------------------- /src/__test__/args.test.ts: -------------------------------------------------------------------------------- 1 | import { getArg, isArg } from '../utils/args'; 2 | 3 | const nowArgv = [...process.argv]; 4 | const initArgv = () => (process.argv = [...nowArgv]); 5 | 6 | describe('node 参数处理', () => { 7 | test('获取参数值', () => { 8 | initArgv(); 9 | process.argv.push('--config=./config.json'); 10 | expect(getArg('config')).toBe(`./config.json`); 11 | initArgv(); 12 | process.argv.push('--config', './config.json'); 13 | expect(getArg('config')).toBe(`./config.json`); 14 | initArgv(); 15 | process.argv.push('-c=./config.json'); 16 | expect(getArg('config')).toBe(`./config.json`); 17 | initArgv(); 18 | process.argv.push('-c', './config.json'); 19 | expect(getArg('config')).toBe(`./config.json`); 20 | initArgv(); 21 | process.argv.push('-T', './config.json'); 22 | expect(getArg('demo', 'T')).toBe(`./config.json`); 23 | }); 24 | 25 | test('是否存在参数', () => { 26 | initArgv(); 27 | process.argv.push('--config=./config.json'); 28 | expect(isArg('config')).toBeTruthy(); 29 | initArgv(); 30 | process.argv.push('--config', './config.json'); 31 | expect(isArg('config')).toBeTruthy(); 32 | initArgv(); 33 | process.argv.push('-c=./config.json'); 34 | expect(isArg('config')).toBeTruthy(); 35 | initArgv(); 36 | process.argv.push('-c', './config.json'); 37 | expect(isArg('config')).toBeTruthy(); 38 | initArgv(); 39 | process.argv.push('-T', './config.json'); 40 | expect(isArg('demo', 'T')).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__test__/array.test.ts: -------------------------------------------------------------------------------- 1 | import { mergeArray } from '@/utils/pure'; 2 | 3 | /** 4 | * 对象中: 5 | * sid 为唯一标识,如果有重复,则合并 6 | * id 为唯一标识,目的是用来判断到底谁把谁给覆盖了。 7 | * subjoin 为附加信息,用于验证非共用属性如何合并。 8 | */ 9 | 10 | function getTargetArray() { 11 | return [ 12 | { 13 | sid: 'newLottery_3c4f311a-041b-11ed-9251-a4ae12675bc2', 14 | id: 1, 15 | title: '爱配音,上戏鲸APP!', 16 | subjoin: [123], 17 | }, 18 | { 19 | sid: 'newLottery_8b3e37a8-e30f-11ec-9251-a4ae12675bc2', 20 | id: 2, 21 | title: '正义的算法', 22 | }, 23 | { 24 | sid: 'newLottery_0444c4a9-f6c0-11ec-9251-a4ae12675bc2', 25 | id: 3, 26 | title: '我的租房日记·第二期', 27 | }, 28 | { 29 | sid: 'newLottery_3c4f311a-041b-11ed-9251-a4ae12675bc2', 30 | id: 4, 31 | title: '谁便一个名字', 32 | }, 33 | ]; 34 | } 35 | 36 | const resultLeft = [ 37 | { 38 | sid: 'newLottery_3c4f311a-041b-11ed-9251-a4ae12675bc2', 39 | id: 4, 40 | title: '谁便一个名字', 41 | subjoin: [123], 42 | }, 43 | { 44 | sid: 'newLottery_8b3e37a8-e30f-11ec-9251-a4ae12675bc2', 45 | id: 2, 46 | title: '正义的算法', 47 | }, 48 | { 49 | sid: 'newLottery_0444c4a9-f6c0-11ec-9251-a4ae12675bc2', 50 | id: 3, 51 | title: '我的租房日记·第二期', 52 | }, 53 | ]; 54 | 55 | const resultRight = [ 56 | { 57 | sid: 'newLottery_3c4f311a-041b-11ed-9251-a4ae12675bc2', 58 | id: 1, 59 | title: '爱配音,上戏鲸APP!', 60 | subjoin: [123], 61 | }, 62 | { 63 | sid: 'newLottery_0444c4a9-f6c0-11ec-9251-a4ae12675bc2', 64 | id: 3, 65 | title: '我的租房日记·第二期', 66 | }, 67 | { 68 | sid: 'newLottery_8b3e37a8-e30f-11ec-9251-a4ae12675bc2', 69 | id: 2, 70 | title: '正义的算法', 71 | }, 72 | ]; 73 | 74 | describe('Array 方法测试', () => { 75 | test('mergeArray 合并数组', () => { 76 | expect(mergeArray(getTargetArray(), 'sid')).toEqual(resultLeft); 77 | expect(mergeArray(getTargetArray(), 'sid', false, 'right')).toEqual(resultRight); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/__test__/bili.test.ts: -------------------------------------------------------------------------------- 1 | import { getSign } from '../utils/bili'; 2 | 3 | const appkey = '1d8b6e7d45233436'; 4 | const appsec = '560c52ccd288fed045859ed18bffd973'; 5 | const params = { 6 | id: 114514, 7 | str: '1919810', 8 | test: 'いいよ,こいよ', 9 | appkey, 10 | }; 11 | 12 | describe('bili 工具测试', () => { 13 | test('app sign', () => { 14 | expect(getSign(params, appsec).query).toBe( 15 | `appkey=1d8b6e7d45233436&id=114514&str=1919810&test=%E3%81%84%E3%81%84%E3%82%88%EF%BC%8C%E3%81%93%E3%81%84%E3%82%88&sign=01479cf20504d865519ac50f33ba3a7d`, 16 | ); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__test__/code.test.ts: -------------------------------------------------------------------------------- 1 | import { base64Decode, base64Encode, md5 } from '../utils/pure'; 2 | 3 | describe('编码', () => { 4 | test('md5 编码测试', () => { 5 | expect(md5('123')).toBe('202cb962ac59075b964b07152d234b70'); 6 | }); 7 | 8 | test('base64 编码测试', () => { 9 | expect(base64Encode('15|6|1|0')).toBe('MTV8NnwxfDA='); 10 | expect(base64Decode('MTV8NnwxfDA=')).toBe('15|6|1|0'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__test__/cookie.test.ts: -------------------------------------------------------------------------------- 1 | import { encodeCookie, getCookieItem, getCookie } from '@/utils/cookie'; 2 | 3 | const DEF_COOKIE = `SESSDATA=4a480e95,1639884061,5f394*61; bili_jct=20caaccb9a577494ebfff10d8794b309; DedeUserID=781236152; DedeUserID__ckMd5=794ce27c5f185238; sid=66crlaql; fingerprint3=dc07b734860f34040b7892876e6862c5; fingerprint=76bd2c43c7f94fa7eeee73ec281992de`; 4 | 5 | const ENCODE_COOKIE = `SESSDATA=4a480e95%2C1639884061%2C5f394%2A61; bili_jct=20caaccb9a577494ebfff10d8794b309; DedeUserID=781236152; DedeUserID__ckMd5=794ce27c5f185238; sid=66crlaql; fingerprint3=dc07b734860f34040b7892876e6862c5; fingerprint=76bd2c43c7f94fa7eeee73ec281992de`; 6 | 7 | const CHARY_COOKIE = `SESSDATA=4a480e95,1639884061,5f394*61;bili_jct=20caaccb9a577494ebfff10d8794b309;DedeUserID=781236152;DedeUserID__ckMd5=794ce27c5f185238;sid=66crlaql;fingerprint3=dc07b734860f34040b7892876e6862c5;fingerprint=76bd2c43c7f94fa7eeee73ec281992de`; 8 | 9 | describe('cookie 工具测试', () => { 10 | test('获取 value', () => { 11 | expect(getCookieItem(DEF_COOKIE, 'SESSDATA')).toBe('4a480e95,1639884061,5f394*61'); 12 | expect(getCookieItem(CHARY_COOKIE, 'SESSDATA')).toBe('4a480e95,1639884061,5f394*61'); 13 | }); 14 | 15 | test('使用 setCookie 更新', () => { 16 | const newCookie = getCookie(DEF_COOKIE, ['DedeUserID=123456789; path=/']); 17 | expect(getCookieItem(newCookie, 'DedeUserID')).toBe('123456789'); 18 | const newCookie2 = getCookie(CHARY_COOKIE, ['DedeUserID=123456789; path=/']); 19 | expect(getCookieItem(newCookie2, 'DedeUserID')).toBe('123456789'); 20 | }); 21 | 22 | test('使用 encodeCookie 编码值', () => { 23 | const newCookie = encodeCookie(DEF_COOKIE); 24 | expect(getCookieItem(newCookie, 'SESSDATA')).toBe('4a480e95%2C1639884061%2C5f394%2A61'); 25 | expect(newCookie).toBe(ENCODE_COOKIE); 26 | expect(ENCODE_COOKIE).toBe(ENCODE_COOKIE); 27 | const charyCookie = encodeCookie(CHARY_COOKIE); 28 | expect(charyCookie).toBe(ENCODE_COOKIE); 29 | expect(getCookieItem(charyCookie, 'SESSDATA')).toBe('4a480e95%2C1639884061%2C5f394%2A61'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__test__/delay.test.ts: -------------------------------------------------------------------------------- 1 | import { getDelayTime } from '@/utils/pure'; 2 | 3 | describe('delayTime 测试', () => { 4 | test('delayTime 返回值是否在区间', () => { 5 | // 测试 ms 6 | expect(getDelayTime('1ms')).toEqual([1]); 7 | expect(getDelayTime('1ms-200ms')).toEqual([1, 200]); 8 | 9 | // 测试 s 10 | expect(getDelayTime('1s')).toEqual([1000]); 11 | expect(getDelayTime('1s-200s')).toEqual([1000, 200000]); 12 | 13 | // 测试 m 14 | expect(getDelayTime('10m')).toEqual([600000]); 15 | expect(getDelayTime('10m-20m')).toEqual([600000, 1200000]); 16 | 17 | // 测试无单位 18 | expect(getDelayTime('10')).toEqual([600000]); 19 | expect(getDelayTime('10-20')).toEqual([600000, 1200000]); 20 | 21 | // 测试 h 22 | expect(getDelayTime('1h')).toEqual([3600000]); 23 | expect(getDelayTime('1h-4h')).toEqual([3600000, 14400000]); 24 | 25 | // 测试混合单位 26 | expect(getDelayTime('1ms-1')).toEqual([1, 60000]); 27 | expect(getDelayTime('1m-2h')).toEqual([60000, 7200000]); 28 | 29 | // 错误处理 30 | expect(getDelayTime('1d')).toEqual([0]); 31 | expect(getDelayTime('1m-')).toEqual([60000, 0]); 32 | expect(getDelayTime('-1m')).toEqual([0, 60000]); 33 | expect(getDelayTime('-')).toEqual([0, 0]); 34 | expect(getDelayTime('asdasdjk')).toEqual([0]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__test__/gzip.test.ts: -------------------------------------------------------------------------------- 1 | import { gzipDecode, gzipEncode } from '../utils/gzip'; 2 | 3 | // 来自百度的压缩结果 4 | const zip = 'H4sIAAAAAAAAA1MtdXMzMFRFIgEi7dsyEgAAAA=='; 5 | const str = '!!!'; 6 | const emojiZip = 'H4sIAAAAAAAAA1MtdbEwdlEtdXE1tlQFsZ2BpJupmypM3NncFME2sQQA96V+EzAAAAA='; 7 | const emojiStr = '😹🍟👵👉'; 8 | 9 | describe('配置 gzip 测试', () => { 10 | test('压缩以后是否一致', () => { 11 | expect(gzipEncode(str)).not.toBe(zip); 12 | expect(gzipEncode(emojiStr)).not.toBe(emojiZip); 13 | }); 14 | 15 | test('解压以后是否一致', () => { 16 | expect(gzipDecode(zip)).toBe(str); 17 | expect(gzipDecode(emojiZip)).toBe(emojiStr); 18 | }); 19 | 20 | test('压缩后解压是否一致', () => { 21 | expect(gzipDecode(gzipEncode(str))).toBe(str); 22 | expect(gzipDecode(gzipEncode(emojiStr))).toBe(emojiStr); 23 | }); 24 | 25 | test('解压后再压缩是否一致', () => { 26 | expect(gzipEncode(gzipDecode(zip))).not.toBe(zip); 27 | expect(gzipEncode(gzipDecode(emojiZip))).not.toBe(emojiZip); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/__test__/header.test.ts: -------------------------------------------------------------------------------- 1 | import { mergeHeaders } from '../utils/pure'; 2 | 3 | const headers1 = { 4 | 'Content-Type': 'application/json', 5 | 'X-Requested-With': 'XMLHttpRequest', 6 | }; 7 | 8 | const headers2 = { 9 | 'content-type': 'application/x-www-form-urlencoded', 10 | 'user-agent': 'xxxxxxxxxxxxxxxxx', 11 | }; 12 | 13 | const headers = { 14 | 'x-requested-with': 'XMLHttpRequest', 15 | 'content-type': 'application/x-www-form-urlencoded', 16 | 'user-agent': 'xxxxxxxxxxxxxxxxx', 17 | }; 18 | 19 | describe('请求 header 测试', () => { 20 | test('合并 header', () => { 21 | expect(mergeHeaders(headers1, headers2)).toEqual(headers); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__test__/object.test.ts: -------------------------------------------------------------------------------- 1 | import { deepSetObject } from '../utils/pure'; 2 | 3 | const obj1 = { 4 | demo: 1, 5 | statistics: { 6 | appId: 1, 7 | }, 8 | }; 9 | 10 | const obj2 = { 11 | demo: 2, 12 | statistics: { 13 | appId: 2, 14 | platform: 3, 15 | }, 16 | time: 3, 17 | }; 18 | 19 | describe('object 测试', () => { 20 | test('为对象设置属性', () => { 21 | expect(deepSetObject(obj1, obj2)).toEqual({ 22 | demo: 1, 23 | statistics: { 24 | appId: 1, 25 | platform: 3, 26 | }, 27 | time: 3, 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/__test__/stringify.test.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from '../utils/pure'; 2 | 3 | const value = { 4 | demo: 1, 5 | statistics: { 6 | appId: 1, 7 | platform: 3, 8 | version: '6.74.0', 9 | abtest: '', 10 | }, 11 | }; 12 | 13 | const result = 14 | 'demo=1&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.74.0%22%2C%22abtest%22%3A%22%22%7D'; 15 | 16 | describe('stringify 测试', () => { 17 | test('stringify 是否达到预期', () => { 18 | expect(stringify(value)).toBe(result); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__test__/version.test.ts: -------------------------------------------------------------------------------- 1 | import { checkVersion } from '../utils/version'; 2 | 3 | describe('版本测试', () => { 4 | test('检查版本是否需要更新', () => { 5 | const version1 = 'v6.74.0'; 6 | const latestTag1 = 'v6.74.0'; 7 | // 如果版本相同,则不需要更新 8 | expect(checkVersion(version1, latestTag1)).toBeFalsy(); 9 | const version2a = 'v6.74.2'; 10 | const version2b = 'v6.74.2-rc0'; 11 | const version2c = 'v6.74.3-beta.0'; 12 | const latestTag2 = 'v6.74.0'; 13 | // 如果版本更新,则不需要更新 14 | expect(checkVersion(version2a, latestTag2)).toBeFalsy(); 15 | expect(checkVersion(version2b, latestTag2)).toBeFalsy(); 16 | expect(checkVersion(version2c, latestTag2)).toBeFalsy(); 17 | const version3a = 'v2.74.3'; 18 | const version3b = 'v6.73.3'; 19 | const version3c = 'v6.74.2'; 20 | const version3d = 'v6.74.2-rc0'; 21 | const latestTag3 = 'v6.74.3'; 22 | // 如果版本更旧,则需要更新 23 | expect(checkVersion(version3a, latestTag3)).toBeTruthy(); 24 | expect(checkVersion(version3b, latestTag3)).toBeTruthy(); 25 | expect(checkVersion(version3c, latestTag3)).toBeTruthy(); 26 | expect(checkVersion(version3d, latestTag3)).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/bin/fork.ts: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const { initialize } = await import('../config/globalVar'); 3 | initialize(JSON.parse(process.env.__BT_CONFIG__)); 4 | const task = await import('../task/dailyTask'); 5 | await task.dailyTasks(); 6 | // 替换 cookie 为最新的 7 | const { replaceNewCookie } = await import('./util'); 8 | replaceNewCookie(); 9 | process.send?.(true); 10 | })(); 11 | -------------------------------------------------------------------------------- /src/bin/inputTask.ts: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const { initialize } = await import('../config/globalVar'); 3 | initialize(JSON.parse(process.env.__BT_CONFIG__)); 4 | const task = await import('../task/'); 5 | await task.runInputBiliTask(process.env.__BT_TASKS_STRING__); 6 | // 替换 cookie 为最新的 7 | const { replaceNewCookie } = await import('./util'); 8 | replaceNewCookie(); 9 | process.send?.(true); 10 | })(); 11 | -------------------------------------------------------------------------------- /src/bin/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 替换最新的 cookie 3 | */ 4 | export async function replaceNewCookie() { 5 | if (!process.env.__BT_CONFIG_PATH__) { 6 | return; 7 | } 8 | const { BiliCookieJar } = await import('@/config/globalVar'); 9 | const { getCookieItem } = await import('@/utils/cookie'); 10 | const { replaceAllCookie } = await import('@/utils/file'); 11 | const cookieJar = new BiliCookieJar(), 12 | cookie = await cookieJar.getCookieString(), 13 | mid = getCookieItem(cookie, 'DedeUserID'); 14 | mid && replaceAllCookie(process.env.__BT_CONFIG_PATH__, mid, cookie); 15 | } 16 | -------------------------------------------------------------------------------- /src/config/configOffFun.ts: -------------------------------------------------------------------------------- 1 | import { biliTasks } from '@/task'; 2 | import { TaskConfig } from './globalVar'; 3 | 4 | function funHandle() { 5 | const functionConfig = TaskConfig.function; 6 | // TODO: 兼容处理 liveSendMessage 7 | if (functionConfig.liveIntimacy && TaskConfig.intimacy.liveSendMessage) { 8 | functionConfig.liveSendMessage = false; 9 | } 10 | // TODO: 兼容处理 mangaSign 11 | if (functionConfig.mangaTask && TaskConfig.manga.sign) { 12 | functionConfig.mangaSign = false; 13 | } 14 | return functionConfig; 15 | } 16 | 17 | /** 18 | * 按照配置清空函数 19 | */ 20 | export function getWaitRuningFunc() { 21 | // 这是使用了黑魔法,也是迫不得已 22 | // rollup 压缩代码后可能导致变量名变化,就找不到相应函数的,所以才改用下标的方式记录 23 | const functionConfig = funHandle(); 24 | const result: Array<() => Promise> = []; 25 | biliTasks.forEach((task, key) => functionConfig[key] && result.push(task)); 26 | return result.map(async func => (await func()).default); 27 | } 28 | -------------------------------------------------------------------------------- /src/config/globalVar.ts: -------------------------------------------------------------------------------- 1 | import type { Config, UserConfig } from '../types'; 2 | import { getConfig } from './setConfig'; 3 | import { mergeConfig, setCookieValue } from './config'; 4 | import { getAndroidUA } from '@/constant/biliUri'; 5 | import getCookie, { encodeCookie, getCookieItem } from '@/utils/cookie'; 6 | import { createBuvid } from '@/utils/pure'; 7 | 8 | export type TaskConfigType = Config & { 9 | mobileUA: string; 10 | buvid: string; 11 | }; 12 | 13 | let _taskConfig: TaskConfigType; 14 | export const TaskConfig = new Proxy({} as TaskConfigType, { 15 | get(_target, key) { 16 | if (!_taskConfig) { 17 | initialize(); 18 | } 19 | return Reflect.get(_taskConfig, key); 20 | }, 21 | set(_target, key, value) { 22 | if (key === 'config' && value) { 23 | initialize(value); 24 | return true; // 否则 config 会被覆盖 25 | } 26 | if (key === 'cookie' && value) { 27 | _taskConfig.cookie = getCookie(_taskConfig.cookie, value?.split(';')); 28 | setCookieValue(_taskConfig, value); 29 | return true; 30 | } 31 | return Reflect.set(_taskConfig, key, value); 32 | }, 33 | }); 34 | 35 | /** 任务完成情况统计 */ 36 | abstract class TaskModuleTemplate { 37 | /**拥有硬币数量 */ 38 | static money = 0; 39 | /**还需要投币数量,初值BILI_TARGET_COINS */ 40 | static coinsTask = 5; 41 | /**今日是否已经分享视频 */ 42 | static share = false; 43 | /**今日是否已经观看视频 */ 44 | static watch = false; 45 | /** B币券余额 */ 46 | static couponBalance = 0; 47 | /** 0为无,1为月度,2为年度 */ 48 | static vipType = 0; 49 | /** 大会员状态 0 无 1 有 */ 50 | static vipStatus = 0; 51 | /** 充电留言 token */ 52 | static chargeOrderNo: string; 53 | /** 用户等级 */ 54 | static userLevel = 0; 55 | /** 用户昵称 */ 56 | static nickname: string; 57 | /** 用于观看的 aid */ 58 | static videoAid: number; 59 | /** 推送消息标题附加 */ 60 | static pushTitle: string[] = []; 61 | /** 存在错误 */ 62 | static hasError = false; 63 | } 64 | 65 | export let TaskModule: typeof TaskModuleTemplate; 66 | 67 | export function initialize(config?: UserConfig) { 68 | if (!config) { 69 | config = getConfig(false); 70 | } 71 | // TODO: 配置方式兼容 72 | const userConfig = mergeConfig(config) as Config; 73 | 74 | const buvid = getCookieBuvid(userConfig.cookie); 75 | 76 | _taskConfig = { 77 | ...userConfig, 78 | mobileUA: getAndroidUA(), 79 | cookie: encodeCookie(`${userConfig.cookie}; Buvid=${buvid}`), 80 | buvid, 81 | }; 82 | TaskModule = class extends TaskModuleTemplate {}; 83 | TaskModule.coinsTask = _taskConfig.coin.targetCoins; 84 | } 85 | 86 | function getCookieBuvid(cookie: string) { 87 | const buvid = getCookieItem(cookie, 'Buvid'); 88 | if (buvid && buvid !== 'undefined') return buvid; 89 | return createBuvid(); 90 | } 91 | 92 | /** 93 | * 改变配置 94 | */ 95 | export function changeConfig(index: string) { 96 | const config = getConfig(); 97 | if (config[index]) { 98 | initialize(config[index]); 99 | } 100 | } 101 | 102 | export class BiliCookieJar { 103 | async getCookieString() { 104 | return TaskConfig.cookie; 105 | } 106 | 107 | async setCookie(rawCookie: string) { 108 | TaskConfig.cookie = getCookie(TaskConfig.cookie, rawCookie); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/config/systemConfig.ts: -------------------------------------------------------------------------------- 1 | import { isQingLongPanel, setConfigFileName } from '../utils/env'; 2 | 3 | export abstract class SystemConfig { 4 | static readonly configFileName = setConfigFileName(); 5 | static readonly isQingLongPanel = isQingLongPanel(); 6 | } 7 | 8 | export default SystemConfig; 9 | -------------------------------------------------------------------------------- /src/constant/biliUri.ts: -------------------------------------------------------------------------------- 1 | import { ContentTypeEnum } from '../enums/http.enum'; 2 | 3 | export const OriginURLs = { 4 | account: 'https://account.bilibili.com', 5 | live: 'https://live.bilibili.com', 6 | space: 'https://space.bilibili.com', 7 | message: 'https://message.bilibili.com', 8 | www: 'https://www.bilibili.com', 9 | manga: 'https://manga.bilibili.com', 10 | }; 11 | 12 | export const RefererURLs = { 13 | www: 'https://www.bilibili.com/', 14 | bigPoint: 'https://big.bilibili.com/mobile/bigPoint', 15 | bigPointTask: 'https://big.bilibili.com/mobile/bigPoint/task', 16 | judge: 'https://www.bilibili.com/judgement/', 17 | }; 18 | 19 | export const baseURLs = { 20 | account: 'https://account.bilibili.com', 21 | live: 'https://api.live.bilibili.com', 22 | api: 'https://api.bilibili.com', 23 | manga: 'https://manga.bilibili.com', 24 | vc: 'https://api.vc.bilibili.com', 25 | liveTrace: 'https://live-trace.bilibili.com', 26 | }; 27 | 28 | export const defaultHeaders = { 29 | 'user-agent': 30 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.34', 31 | 'content-type': ContentTypeEnum.FORM_URLENCODED, 32 | 'accept-language': 'zh-CN,zh;q=0.9', 33 | 'accept-encoding': 'gzip, deflate, br', 34 | }; 35 | 36 | interface UAOption { 37 | version?: string; 38 | phone?: string; 39 | build?: string | number; 40 | channel?: string; 41 | os?: string; 42 | } 43 | 44 | export function getAndroidUA({ 45 | version = '6.79.0', 46 | phone = 'MI 10 Pro', 47 | build = '6790300', 48 | channel = 'xiaomi', 49 | os = '10', 50 | }: UAOption = {}) { 51 | return `Mozilla/5.0 BiliDroid/${version} (bbcallen@gmail.com) os/android model/${phone} mobi_app/android build/${build} channel/${channel} innerVer/${channel} osVer/${os} network/2`; 52 | } 53 | -------------------------------------------------------------------------------- /src/constant/dm.ts: -------------------------------------------------------------------------------- 1 | export const DMEmoji = [ 2 | { emoji: '赞', emoticon_id: 147, emoticon_unique: 'official_147' }, 3 | { emoji: '妙啊', emoticon_id: 109, emoticon_unique: 'official_109' }, 4 | { emoji: '有点东西', emoticon_id: 113, emoticon_unique: 'official_113' }, 5 | { emoji: '离谱', emoticon_id: 120, emoticon_unique: 'official_120' }, 6 | { emoji: '很有精神', emoticon_id: 150, emoticon_unique: 'official_150' }, 7 | { emoji: '泪目', emoticon_id: 103, emoticon_unique: 'official_103' }, 8 | { emoji: '赢麻了', emoticon_id: 128, emoticon_unique: 'official_128' }, 9 | { emoji: '钝角', emoticon_id: 133, emoticon_unique: 'official_133' }, 10 | { emoji: '干杯', emoticon_id: 149, emoticon_unique: 'official_149' }, 11 | { emoji: '2333', emoticon_id: 124, emoticon_unique: 'official_124' }, 12 | { emoji: '打call', emoticon_id: 146, emoticon_unique: 'official_146' }, 13 | { emoji: '多谢款待', emoticon_id: 148, emoticon_unique: 'official_148' }, 14 | { emoji: 'awsl', emoticon_id: 102, emoticon_unique: 'official_102' }, 15 | { emoji: '笑死', emoticon_id: 121, emoticon_unique: 'official_121' }, 16 | { emoji: '鸡汤来咯', emoticon_id: 137, emoticon_unique: 'official_137' }, 17 | { emoji: '雀食', emoticon_id: 118, emoticon_unique: 'official_118' }, 18 | { emoji: '烦死了', emoticon_id: 129, emoticon_unique: 'official_129' }, 19 | { emoji: '禁止套娃', emoticon_id: 108, emoticon_unique: 'official_108' }, 20 | { emoji: '暗中观察', emoticon_id: 104, emoticon_unique: 'official_104' }, 21 | { emoji: '保熟吗', emoticon_id: 105, emoticon_unique: 'official_105' }, 22 | { emoji: '比心', emoticon_id: 106, emoticon_unique: 'official_106' }, 23 | { emoji: 'what', emoticon_id: 114, emoticon_unique: 'official_114' }, 24 | { emoji: '好耶', emoticon_id: 107, emoticon_unique: 'official_107' }, 25 | { emoji: '咸鱼翻身', emoticon_id: 110, emoticon_unique: 'official_110' }, 26 | { emoji: 'mua', emoticon_id: 111, emoticon_unique: 'official_111' }, 27 | { emoji: '打扰了', emoticon_id: 136, emoticon_unique: 'official_136' }, 28 | { emoji: '来了来了', emoticon_id: 115, emoticon_unique: 'official_115' }, 29 | { emoji: '贴贴', emoticon_id: 116, emoticon_unique: 'official_116' }, 30 | { emoji: '牛牛牛', emoticon_id: 117, emoticon_unique: 'official_117' }, 31 | { emoji: '颠个勺', emoticon_id: 119, emoticon_unique: 'official_119' }, 32 | { emoji: '好家伙', emoticon_id: 122, emoticon_unique: 'official_122' }, 33 | { emoji: '那我走', emoticon_id: 123, emoticon_unique: 'official_123' }, 34 | { emoji: '下次一定', emoticon_id: 125, emoticon_unique: 'official_125' }, 35 | { emoji: '不上Ban', emoticon_id: 126, emoticon_unique: 'official_126' }, 36 | { emoji: '就这', emoticon_id: 127, emoticon_unique: 'official_127' }, 37 | { emoji: '上热榜', emoticon_id: 134, emoticon_unique: 'official_134' }, 38 | { emoji: '中奖喷雾', emoticon_id: 135, emoticon_unique: 'official_135' }, 39 | { emoji: '我不理解', emoticon_id: 138, emoticon_unique: 'official_138' }, 40 | ]; 41 | -------------------------------------------------------------------------------- /src/constant/index.ts: -------------------------------------------------------------------------------- 1 | export const defaultComments = [ 2 | '棒', 3 | '棒唉', 4 | '棒耶', 5 | '加油~', 6 | 'UP加油!', 7 | '支持~', 8 | '支持支持!', 9 | '催更啦', 10 | '顶顶', 11 | '留下脚印~', 12 | '干杯', 13 | 'bilibili干杯', 14 | 'o(* ̄▽ ̄*)o', 15 | '(。・∀・)ノ゙嗨', 16 | '(●ˇ∀ˇ●)', 17 | '( •̀ ω •́ )y', 18 | '(ง •_•)ง', 19 | '>.<', 20 | '^_~', 21 | ]; 22 | 23 | export const kaomoji = [ 24 | '(⌒▽⌒)', 25 | '( ̄▽ ̄)', 26 | '(=・ω・=)', 27 | '(`・ω・´)', 28 | '(〜 ̄△ ̄)〜', 29 | '(・∀・)', 30 | '(°∀°)ノ', 31 | '( ̄3 ̄)', 32 | '╮( ̄▽ ̄)╭', 33 | '_(:3」∠)_', 34 | '( ´_ゝ`)', 35 | '←_←', 36 | '→_→', 37 | '(<_<)', 38 | '(>_>)', 39 | '(;¬_¬)', 40 | '(゚Д゚≡゚д゚)!?', 41 | 'Σ(゚д゚;)', 42 | 'Σ(  ̄□ ̄||)', 43 | '(´;ω;`)', 44 | '(/TДT)/', 45 | '(^・ω・^ )', 46 | '(。・ω・。)', 47 | '(● ̄(エ) ̄●)', 48 | 'ε=ε=(ノ≧∇≦)ノ', 49 | '(´・_・`)', 50 | '(-_-#)', 51 | '( ̄へ ̄)', 52 | '( ̄ε(# ̄) Σ', 53 | 'ヽ(`Д´)ノ', 54 | '(#-_-)┯━┯', 55 | '(╯°口°)╯(┴—┴', 56 | '←◡←', 57 | '( ♥д♥)', 58 | 'Σ>―(〃°ω°〃)♡→', 59 | '⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄', 60 | '(╬゚д゚)▄︻┻┳═一', 61 | '・*・:≡( ε:)', 62 | '(汗)', 63 | '(苦笑)', 64 | ]; 65 | 66 | export const HEART_TRIGGER_NAME = 'heart_bili_timer'; 67 | export const DAILY_RUN_TIME = '17:30:00-23:40:00'; 68 | export const HEART_RUN_TIME = '12:00:00-20:00:00'; 69 | /** 毫秒与天数进制 */ 70 | export const MS2DATE = 86_400_000; 71 | /** 毫秒与小时进制 */ 72 | export const MS2HOUR = 3_600_000; 73 | 74 | /** 天选排除奖品 */ 75 | export const LOTTERY_EXCLUDE = [ 76 | '舰', 77 | '船', 78 | '航海', 79 | '代金券', 80 | '优惠券', 81 | '自拍', 82 | '照', 83 | '写真', 84 | '图', 85 | '提督', 86 | '车车一局', 87 | '再来一局', 88 | '游戏道具', 89 | ]; 90 | 91 | export const LOTTERY_INCLUDE = ['谢']; 92 | 93 | // https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/docs/configuration.md#344-%E5%A4%A9%E9%80%89%E7%AD%B9%E6%8A%BD%E5%A5%96%E4%B8%BB%E6%92%ADuid%E9%BB%91%E5%90%8D%E5%8D%95 94 | /** up 黑名单 */ 95 | export const LOTTERY_UP_BLACKLIST = [65566781, 1277481241, 1643654862, 603676925]; 96 | 97 | export const TODAY_MAX_FEED = 1500; 98 | -------------------------------------------------------------------------------- /src/dto/activity-lottery.dto.ts: -------------------------------------------------------------------------------- 1 | import type { ApiBaseProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 抽奖结果 5 | */ 6 | export type LotteryResult = ApiBaseProp< 7 | { 8 | id: number; 9 | mid: number; 10 | ip: number; 11 | num: number; 12 | gift_id: number; 13 | gift_name: string; 14 | /** 0 没有 */ 15 | gift_type: number; 16 | img_url: string; 17 | type: number; 18 | ctime: number; 19 | cid: number; 20 | extra: unknown; 21 | award_info?: any; 22 | order_no: string; 23 | }[] 24 | >; 25 | -------------------------------------------------------------------------------- /src/dto/big-point.dto.ts: -------------------------------------------------------------------------------- 1 | import type { ApiBaseProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 查看任务详情 5 | */ 6 | export type TaskCombineDto = ApiBaseProp<{ 7 | vip_info: Vipinfo; 8 | point_info: Pointinfo; 9 | task_info: Taskinfo; 10 | current_ts: number; 11 | }>; 12 | 13 | export interface Taskinfo { 14 | modules: Module[]; 15 | sing_task_item: SingTaskItem; 16 | score_month: number; 17 | score_limit: number; 18 | } 19 | 20 | export interface CommonTaskItem { 21 | task_code: 22 | | 'bonus' 23 | | 'privilege' 24 | | '626act' 25 | | 'animatetab' 26 | | 'filmtab' 27 | | 'vipmallview' 28 | | 'ogvwatch' 29 | | 'tvodbuy' 30 | | 'vipmallbuy'; 31 | /** 0 未领取 1 未完成 2 未知 3 已领取 */ 32 | state: number; 33 | title: string; 34 | icon: string; 35 | subtitle: string; 36 | explain: string; 37 | /** 估计是需要会员等级 */ 38 | vip_limit: number; 39 | /** 已完成次数 */ 40 | complete_times: number; 41 | /** 最大次数 */ 42 | max_times: number; 43 | recall_num: number; 44 | link?: string; 45 | } 46 | 47 | interface SingTaskItem { 48 | histories: SingTaskHistory[]; 49 | count: number; 50 | base_score: number; 51 | } 52 | 53 | export interface SingTaskHistory { 54 | day: string; 55 | signed: boolean; 56 | score: number; 57 | is_today?: boolean; 58 | } 59 | 60 | interface Module { 61 | module_title: string; 62 | common_task_item: CommonTaskItem[]; 63 | module_sub_title?: string; 64 | } 65 | 66 | interface Pointinfo { 67 | /** 目前积分 */ 68 | point: number; 69 | expire_point: number; 70 | expire_time: number; 71 | expire_days: number; 72 | } 73 | 74 | interface Vipinfo { 75 | /** 0 普通 1 月度 2 年度 */ 76 | type: number; 77 | /** 0 不存在 1 存在 */ 78 | status: number; 79 | due_date: number; 80 | vip_pay_type: number; 81 | start_time: number; 82 | paid_type: number; 83 | mid: number; 84 | role: number; 85 | tv_vip_status: number; 86 | tv_vip_pay_type: number; 87 | tv_due_date: number; 88 | } 89 | 90 | export type PointListDto = ApiBaseProp<{ 91 | total: number; 92 | big_point_list: Bigpointlist[]; 93 | }>; 94 | 95 | interface Bigpointlist { 96 | point: number; 97 | change_time: number; 98 | remark: string; 99 | order_no: string; 100 | image_url: string; 101 | } 102 | -------------------------------------------------------------------------------- /src/dto/bili-base-prop.ts: -------------------------------------------------------------------------------- 1 | export interface AnyProp { 2 | code?: number; 3 | message?: string; 4 | data?: T; 5 | ttl?: number; 6 | ts?: number; 7 | msg?: string; 8 | status?: boolean; 9 | } 10 | 11 | export interface ApiBaseProp { 12 | /** 13 | * -658 Token 过期 14 | */ 15 | code: number; 16 | message: string; 17 | ttl: number; 18 | data: T; 19 | } 20 | 21 | export interface OnlyMsg { 22 | code: number; 23 | msg: string; 24 | data: T; 25 | } 26 | 27 | export interface AccountBaseProp { 28 | code: number; 29 | status: boolean; 30 | data: T; 31 | } 32 | 33 | export interface DoubleMessageProp { 34 | code: number; 35 | msg: string; 36 | message: string; 37 | data: T; 38 | } 39 | 40 | export interface PureDataProp { 41 | code: number; 42 | message: string; 43 | data: T; 44 | } 45 | 46 | /** 47 | * 0:请求成功 48 | * -101:账号未登录 49 | * -111:csrf校验失败 50 | * -400:请求错误 51 | */ 52 | export type ResponseCode = 0 | -101 | -111 | -400; 53 | 54 | /** 55 | * result 请求 56 | */ 57 | export interface ResponseResult { 58 | code: number; 59 | message: string; 60 | result: T; 61 | } 62 | -------------------------------------------------------------------------------- /src/dto/bili-ws.dto.ts: -------------------------------------------------------------------------------- 1 | export interface RedPackListDto { 2 | lot_id: number; 3 | total_num: number; 4 | winner_info: [number, string, number, number][]; 5 | awards: Awards; 6 | version: number; 7 | } 8 | 9 | type Awards = Record; 10 | 11 | interface AwardType { 12 | award_type: number; 13 | award_name: string; 14 | award_pic: string; 15 | award_big_pic: string; 16 | award_price: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/dto/daily-battery.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 任务进程 5 | */ 6 | export type UserTaskProgress = ApiBaseProp<{ 7 | is_surplus: number; 8 | /** 0 未开始 1 进行中 2 可领取 3 已领取 */ 9 | status: number; 10 | progress: number; 11 | target: number; 12 | wallet: { 13 | gold: number; 14 | silver: number; 15 | }; 16 | linked_actions_progress: null; 17 | }>; 18 | -------------------------------------------------------------------------------- /src/dto/intimacy.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 分享直播间 5 | */ 6 | export interface ShareLiveRoomRes extends ApiBaseProp { 7 | data: { 8 | /** 1 */ 9 | allow_mock: number; 10 | }; 11 | } 12 | 13 | /** 14 | * 直播心跳 15 | */ 16 | export interface LiveHeartBeatRes extends ApiBaseProp { 17 | data: { 18 | /** 60 */ 19 | heartbeat_interval: number; 20 | timestamp: number; 21 | secret_rule: [number, number, number, number, number]; 22 | /** axoaadsffcazxksectbbb */ 23 | secret_key: string; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/dto/jury.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 个人仲裁信息 5 | */ 6 | export type JuryInfoDto = ApiBaseProp<{ 7 | /** 总仲裁数量 */ 8 | caseTotal: number; 9 | face: string; 10 | /**当前剩余天数 */ 11 | restDays: number; 12 | /**当前裁决胜率 */ 13 | rightRadio: number; 14 | /**是否具有资格 1:有 2:过期 */ 15 | status: 1 | 2; 16 | uname: string; 17 | }>; 18 | 19 | export type JuryCaseOpinionDto = ApiBaseProp<{ 20 | total: number; 21 | list: JuryCaseOpinion[]; 22 | }>; 23 | 24 | export interface JuryCaseOpinion { 25 | opid: number; 26 | mid: number; 27 | uname: string; 28 | face: string; 29 | vote: number; 30 | vote_text: string; 31 | content: string; 32 | /** 匿名 */ 33 | anonymous: 0 | 1; 34 | like: number; 35 | hate: number; 36 | like_status: number; 37 | vote_time: number; 38 | /** 会看 */ 39 | insiders: 0 | 1; 40 | } 41 | 42 | export type JuryCaseNextDto = ApiBaseProp<{ 43 | case_id: string; 44 | }>; 45 | 46 | export type JuryVotedCaseDto = ApiBaseProp<{ 47 | total: number; 48 | list: JuryVotedCase[]; 49 | }>; 50 | 51 | export interface JuryVotedCase { 52 | case_id: string; 53 | case_type: number; 54 | status: number; 55 | vote: number; 56 | vote_text: string; 57 | vote_time: number; 58 | } 59 | 60 | export type JuryCaseInfoDto = ApiBaseProp<{ 61 | case_id: string; 62 | case_type: number; 63 | jury_state: number; 64 | participate: number; 65 | vote_items: VoteItem[]; 66 | default_vote: number; 67 | status: number; 68 | origin_start: number; 69 | avid: number; 70 | cid: number; 71 | vote_cd: number; 72 | result: number; 73 | result_text: string; 74 | title: string; 75 | case_info: CaseInfo; 76 | }>; 77 | 78 | interface CaseInfo { 79 | comment?: any; 80 | danmu_img: string; 81 | comments: Comment[]; 82 | single_danmu?: any; 83 | } 84 | 85 | interface Comment { 86 | mid: number; 87 | uname: string; 88 | face: string; 89 | content: string; 90 | child_comments: Childcomment[]; 91 | emote?: Record; 92 | } 93 | 94 | interface Childcomment { 95 | mid: number; 96 | uname: string; 97 | face: string; 98 | content: string; 99 | emote?: Record; 100 | } 101 | 102 | interface EmoteItem { 103 | id: number; 104 | package_id: number; 105 | state: number; 106 | type: number; 107 | attr: number; 108 | text: string; 109 | url: string; 110 | meta: { 111 | size: number; 112 | }; 113 | mtime: number; 114 | gif_url?: string; 115 | jump_title: string; 116 | } 117 | 118 | interface VoteItem { 119 | vote: number; 120 | vote_text: string; 121 | } 122 | -------------------------------------------------------------------------------- /src/dto/match-game.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 比赛集合 5 | */ 6 | export interface GuessCollectionDto extends ApiBaseProp { 7 | data: { 8 | list: { 9 | contest: { 10 | id: number; 11 | }; 12 | questions: [ 13 | { 14 | id: number; 15 | /** 竞猜标题,eg xxx vs xxx */ 16 | title: string; 17 | stake_type: number; 18 | /** 是否已经竞猜 0 未 */ 19 | is_guess: number; 20 | details: GuessDetails[]; 21 | guess_season: unknown; 22 | }, 23 | ]; 24 | }[]; 25 | page: { num: number; size: number; total: number }; 26 | }; 27 | } 28 | 29 | export interface GuessDetails { 30 | /** id */ 31 | detail_id: number; 32 | /** 赔率 float*/ 33 | odds: number; 34 | team_id: number; 35 | /** 队伍名称 */ 36 | option: string; 37 | stake: number; 38 | income: number; 39 | correct: number; 40 | } 41 | -------------------------------------------------------------------------------- /src/dto/red-packet.dto.ts: -------------------------------------------------------------------------------- 1 | export interface AugRedPacket2022Controller { 2 | code: number; 3 | message: string; 4 | data: { 5 | list: { 6 | lotId: string; 7 | ruid: string; 8 | roomId: string; 9 | runame: string; 10 | face: string; 11 | countDown: number; 12 | }[]; 13 | }; 14 | errors: Record; 15 | } 16 | -------------------------------------------------------------------------------- /src/dto/reservation.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp, DoubleMessageProp } from './bili-base-prop'; 2 | 3 | export type ReservationDto = ApiBaseProp; 4 | 5 | export interface Reservation { 6 | sid: number; 7 | name: string; 8 | total: number; 9 | stime: number; 10 | etime: number; 11 | /** 是否参与 */ 12 | is_follow: number; 13 | state: number; 14 | oid: string; 15 | type: number; 16 | up_mid: number; 17 | /** 参与时间 unix */ 18 | reserve_record_ctime: number; 19 | /** 开奖时间 unix */ 20 | live_plan_start_time: number; 21 | lottery_type: number; 22 | lottery_prize_info: { 23 | text: string; 24 | lottery_icon: string; 25 | jump_url: string; 26 | }; 27 | show_total: boolean; 28 | subtitle: string; 29 | attached_badge_text: string; 30 | } 31 | 32 | /** 33 | * code: 7604003 操作失败,请稍后重试 34 | */ 35 | export type ReserveButtonDto = DoubleMessageProp<{ 36 | btn_mode: number; 37 | /** 1-未预约 2-已预约 */ 38 | final_btn_status: 1 | 2; 39 | desc_update: string; 40 | reserve_update: number; 41 | toast: string; 42 | has_activity: boolean; 43 | _gt_: number; 44 | }>; 45 | -------------------------------------------------------------------------------- /src/dto/session.dto.ts: -------------------------------------------------------------------------------- 1 | import { DoubleMessageProp } from './bili-base-prop'; 2 | 3 | export interface SessionDto { 4 | code: number; 5 | msg: string; 6 | message: string; 7 | ttl: number; 8 | data: SessionlistData; 9 | } 10 | 11 | interface SessionlistData { 12 | session_list: Sessionlist[]; 13 | has_more: number; 14 | anti_disturb_cleaning: boolean; 15 | is_address_list_empty: number; 16 | show_level: boolean; 17 | } 18 | 19 | export interface Sessionlist { 20 | talker_id: number; 21 | session_type: number; 22 | at_seqno: number; 23 | top_ts: number; 24 | group_name: string; 25 | group_cover: string; 26 | is_follow: number; 27 | is_dnd: number; 28 | ack_seqno: number; 29 | ack_ts: number; 30 | session_ts: number; 31 | unread_count: number; 32 | last_msg: Lastmsg; 33 | group_type: number; 34 | can_fold: number; 35 | status: number; 36 | max_seqno: number; 37 | new_push_msg: number; 38 | setting: number; 39 | is_guardian: number; 40 | is_intercept: number; 41 | is_trust: number; 42 | system_msg_type: number; 43 | live_status: number; 44 | biz_msg_unread_count: number; 45 | user_label?: any; 46 | } 47 | 48 | interface Lastmsg { 49 | sender_uid: number; 50 | receiver_type: number; 51 | receiver_id: number; 52 | msg_type: number; 53 | content: string; 54 | msg_seqno: number; 55 | timestamp: number; 56 | at_uids?: any; 57 | msg_key: number; 58 | msg_status: number; 59 | notify_code: string; 60 | new_face_version: number; 61 | } 62 | 63 | export type SessionHistoryDto = DoubleMessageProp<{ 64 | messages: SessionMessageDto[]; 65 | has_more: number; 66 | min_seqno: number; 67 | max_seqno: number; 68 | }>; 69 | 70 | export interface SessionMessageDto { 71 | sender_uid: number; 72 | receiver_type: number; 73 | receiver_id: number; 74 | msg_type: number; 75 | content: string; 76 | msg_seqno: number; 77 | timestamp: number; 78 | at_uids: number[]; 79 | msg_key: number; 80 | msg_status: number; 81 | notify_code: string; 82 | new_face_version: number; 83 | } 84 | 85 | /** 86 | * message type 10 87 | */ 88 | export interface SessionMessage10Dto { 89 | title: string; 90 | text: string; 91 | jump_text: string; 92 | jump_uri: string; 93 | modules?: any; 94 | jump_text_2: string; 95 | jump_uri_2: string; 96 | jump_text_3: string; 97 | jump_uri_3: string; 98 | notifier?: any; 99 | jump_uri_config: { 100 | all_uri: string; 101 | text: string; 102 | }; 103 | 104 | jump_uri_2_config: { 105 | text: string; 106 | }; 107 | jump_uri_3_config: { 108 | text: string; 109 | }; 110 | biz_content?: any; 111 | } 112 | 113 | /** 114 | * message type 1 115 | */ 116 | export interface SessionMessage1Dto { 117 | content: string; 118 | } 119 | -------------------------------------------------------------------------------- /src/dto/sup-group.dto.ts: -------------------------------------------------------------------------------- 1 | import { PureDataProp } from './bili-base-prop'; 2 | 3 | /** 4 | * 自己加入的应援团 5 | */ 6 | export interface SupGroupsDto extends PureDataProp { 7 | data: { 8 | list: { 9 | group_id: number; 10 | owner_uid: number; 11 | group_type: number; 12 | group_level: number; 13 | group_cover: string; 14 | group_name: string; 15 | group_notice: string; 16 | group_status: number; 17 | room_id: number; 18 | is_living: number; 19 | /** 粉丝勋章名字 */ 20 | fans_medal_name: string; 21 | }[]; 22 | }; 23 | } 24 | 25 | /** 26 | * 应援团签到 27 | */ 28 | export interface SupGroupsSignDto extends PureDataProp { 29 | data: { 30 | /** 增加的经验值 */ 31 | add_num: number; 32 | /** 33 | * 0 签到成功 34 | * 1 已经签到过了 35 | */ 36 | status: 0 | 1; 37 | _gt_: number; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/dto/video.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp, ResponseResult } from './bili-base-prop'; 2 | 3 | /** 传递视频播放时长 */ 4 | export interface HeartbeatDto extends ApiBaseProp { 5 | data: null; 6 | } 7 | 8 | /** 分享 */ 9 | export interface ShareAddDto extends ApiBaseProp { 10 | data: number | null; 11 | } 12 | 13 | /** 投币 */ 14 | export interface AddCoinDto extends ApiBaseProp { 15 | data: { 16 | like: boolean; 17 | }; 18 | } 19 | 20 | /** 已经给该视频投币数量 */ 21 | export interface DonatedCoinsForVideoDto extends ApiBaseProp { 22 | data: { 23 | multiply: 0 | 1 | 2; 24 | }; 25 | } 26 | 27 | /** 获取排行榜上的视频 */ 28 | export interface RegionRankingVideosDto extends ApiBaseProp { 29 | data: Array<{ 30 | aid: string; 31 | bvid: string; 32 | typename: string; 33 | title: string; 34 | subtitle: string; 35 | play: number; 36 | review: number; 37 | video_review: number; 38 | favorites: number; 39 | mid: number; 40 | author: string; 41 | description: string; 42 | create: string; 43 | pic: string; 44 | coins: number; 45 | }>; 46 | } 47 | 48 | /** 指定up的视频 */ 49 | export interface VideoByUpDto extends ApiBaseProp { 50 | data: { 51 | media_list: Array<{ 52 | id: number; 53 | index: number; 54 | title: string; 55 | type: number; 56 | bv_id: number; 57 | upper: { 58 | mid: number; 59 | name: string; 60 | }; 61 | }>; 62 | }; 63 | } 64 | 65 | /** 66 | * 推荐视频 67 | */ 68 | export type RecommendVideoDto = ApiBaseProp<{ 69 | item: RecommendVideo[]; 70 | business_card?: any; 71 | floor_info?: any; 72 | user_feature: string; 73 | abtest: { 74 | group: string; 75 | }; 76 | preload_expose_pct: number; 77 | preload_floor_expose_pct: number; 78 | }>; 79 | 80 | interface RecommendVideo { 81 | id: number; 82 | bvid: string; 83 | cid: number; 84 | goto: string; 85 | uri: string; 86 | pic: string; 87 | title: string; 88 | duration: number; 89 | pubdate: number; 90 | owner: { 91 | mid: number; 92 | name: string; 93 | face: string; 94 | }; 95 | stat: { 96 | view: number; 97 | like: number; 98 | danmaku: number; 99 | }; 100 | av_feature: string; 101 | is_followed: number; 102 | rcmd_reason: { 103 | reason_type: number; 104 | }; 105 | show_info: number; 106 | track_id: string; 107 | pos: number; 108 | room_info?: any; 109 | ogv_info?: any; 110 | } 111 | 112 | /** 113 | * 番剧/影视操作 114 | */ 115 | export type BangumiFollowDto = ResponseResult<{ 116 | fmid: number; 117 | relation: boolean; 118 | /** 2 追番成功,0 取消追番 */ 119 | status: number; 120 | /** 操作后的提示 */ 121 | toast: string; 122 | }>; 123 | -------------------------------------------------------------------------------- /src/dto/vip-privilege.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiBaseProp, PureDataProp, ResponseCode } from './bili-base-prop'; 2 | 3 | /** 领取每月会员权益 */ 4 | export interface ReceiveVipPrivilegeDto extends ApiBaseProp { 5 | data: null; 6 | } 7 | 8 | /** 领取每月会员权益状态 */ 9 | export interface ReceiveVipMyDto extends ApiBaseProp { 10 | data: { 11 | /** type 1-5 除了 type 和 state 都是固定的 */ 12 | list: { 13 | type: number; 14 | /** 1 已经领取 */ 15 | state: 0 | 1; 16 | // 过期时间 17 | expire_time: 1656604799; 18 | // vip_type 19 | vip_type: 2; 20 | // 距离下次领取时间 单位天 21 | next_receive_days: number; 22 | /** 下次领取时间,eg. 1656777600 **/ 23 | period_end_unix: number; 24 | }[]; 25 | is_short_vip: boolean; 26 | is_freight_open: boolean; 27 | }; 28 | } 29 | 30 | /** 给 UP 充电 */ 31 | export interface ChargingDto extends ApiBaseProp { 32 | message: '0'; 33 | data: { 34 | /** 用户ID */ 35 | mid: number; 36 | /** 目标ID */ 37 | up_mid: number; 38 | /** 用于添加充电留言 */ 39 | order_no: string; 40 | /** 充电贝壳数 */ 41 | bp_num: number; 42 | /** 获得经验数 */ 43 | exp: number; 44 | /** 4 成功 -2 低于20电池 -4 B币不足 */ 45 | status: 4 | -2 | -4; 46 | /** 错误信息或空 '' */ 47 | msg: string; 48 | }; 49 | } 50 | 51 | export interface ChargingMessageDto extends PureDataProp { 52 | message: '0'; 53 | /** 88203:不能重复留言 */ 54 | code: ResponseCode | 88203; 55 | } 56 | -------------------------------------------------------------------------------- /src/enums/activity-lottery.emum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 返回状态码 3 | */ 4 | export const ActivityLotteryStatus = { 5 | Success: 0, 6 | /** 请求过于频繁 */ 7 | TooManyRequests: -509, 8 | /** 不存在 */ 9 | NotExist: 170001, 10 | /** 结束 */ 11 | End: 175003, 12 | /** 抽奖频率过高 */ 13 | Frequent: 170400, 14 | /** 获取次数已达上限 */ 15 | Max: 170405, 16 | /** 抽奖次数用完 */ 17 | NoTimes: 170415, 18 | /** 412 */ 19 | PreconditionFailed: 412, 20 | /** 网络 */ 21 | NetworkError: 'NET_ERR', 22 | } as const; 23 | -------------------------------------------------------------------------------- /src/enums/big-point.emum.ts: -------------------------------------------------------------------------------- 1 | export const TaskCode = { 2 | ogvwatch: '观看正片', 3 | tvodbuy: '购买视频', 4 | vipmallbuy: '购买商品', 5 | bonus: '大会员福利', 6 | privilege: '浏览会员权益', 7 | '626act': '浏览“我的哔哩小屋”', 8 | animatetab: '浏览追番', 9 | filmtab: '浏览影视', 10 | vipmallview: '浏览会员购', 11 | }; 12 | 13 | export type TaskCodeType = keyof typeof TaskCode; 14 | -------------------------------------------------------------------------------- /src/enums/coin.emum.ts: -------------------------------------------------------------------------------- 1 | export const TypeEnum = { 2 | video: '视频', 3 | audio: '音乐', 4 | article: '专栏', 5 | 视频: 'video', 6 | 音乐: 'audio', 7 | 专栏: 'article', 8 | } as const; 9 | -------------------------------------------------------------------------------- /src/enums/env.emum.ts: -------------------------------------------------------------------------------- 1 | export const ThisContext = { 2 | // 本地 3 | LOCAL: 'local', 4 | // 青龙 5 | QING_LONG: 'qinglong', 6 | // 腾讯云函数 7 | TENCENT_SCF: 'tencent-scf', 8 | // 阿里云函数 9 | ALI_FC: 'ali-fc', 10 | // 百度云函数 11 | BAIDU_CFC: 'baidu-cfc', 12 | // 华为云函数 13 | HUAWEI_FG: 'huawei-hg', 14 | // 华为 agc 云函数 15 | HUAWEI_AGC: 'huawei-agc', 16 | }; 17 | -------------------------------------------------------------------------------- /src/enums/http.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 请求方法 3 | */ 4 | export const RequestEnum = { 5 | GET: 'GET', 6 | POST: 'POST', 7 | PUT: 'PUT', 8 | PATCH: 'PATCH', 9 | DELETE: 'DELETE', 10 | }; 11 | 12 | /** 13 | * 数据类型 14 | */ 15 | export const ContentTypeEnum = { 16 | // form-data qs 17 | FORM_URLENCODED: 'application/x-www-form-urlencoded; charset=UTF-8', 18 | }; 19 | -------------------------------------------------------------------------------- /src/enums/intimacy.emum.ts: -------------------------------------------------------------------------------- 1 | export const SeedMessageResult = { 2 | Success: 0, 3 | // 不可抗力 4 | Unresistant: 11000, 5 | // 发送过于频繁 6 | TooFrequently: 10030, 7 | // 未知 8 | Unknown: -1, 9 | } as const; 10 | -------------------------------------------------------------------------------- /src/enums/jury.emum.ts: -------------------------------------------------------------------------------- 1 | export const JuryVote = { 2 | 1: '合适', 3 | 2: '一般', 4 | 3: '不合适', 5 | 4: '无法判断', 6 | 11: '好', 7 | 12: '普通', 8 | 13: '差', 9 | 14: '无法判断', 10 | }; 11 | 12 | /** 13 | * 投票返回结果 14 | */ 15 | export const JuryVoteResult = { 16 | UNKNOWN: -2, 17 | ERROR: -1, 18 | SUCCESS: 0, 19 | NO_OPINION: 1, 20 | /** Opinion 太少 */ 21 | FEW_OPINION: 2, 22 | 被排除: 3, 23 | 不参考: 4, 24 | } as const; 25 | 26 | /** 27 | * 获取 vote 返回结果 28 | */ 29 | export const VoteResCode = { 30 | 成功: 0, 31 | 没有资格: 25005, 32 | 资格过期: 25006, 33 | 没有新案件: 25008, 34 | 已完成: 25014, 35 | } as const; 36 | -------------------------------------------------------------------------------- /src/enums/live-lottery.enum.ts: -------------------------------------------------------------------------------- 1 | export const RequireType = { 2 | // 无 3 | None: 0, 4 | // 关注 5 | Follow: 1, 6 | // 粉丝牌,等级要求 7 | Fans: 2, 8 | // 舰长,提督等 9 | Captain: 3, 10 | // 主站等级要求 11 | Level: 5, 12 | }; 13 | 14 | export const TianXuanStatus = { 15 | // 开启 16 | Enabled: 1, 17 | // 未开启 18 | Disabled: 2, 19 | }; 20 | 21 | export const PendentID = { 22 | // 天选时刻 23 | Time: 504, 24 | // 红包 25 | RedPacket: 1096, 26 | // 当前分区Top10 27 | Top10: 0, 28 | // 素人总榜十强 29 | Top10_SR: 1170, 30 | // 原神高能团 31 | YS_GaoNeng: 968, 32 | // 国服绝活哥 33 | GFK_JueHuo: 1121, 34 | // 生日会 35 | Birthday: 863, 36 | // Major解说团 37 | Major: 1152, 38 | }; 39 | -------------------------------------------------------------------------------- /src/enums/manga-game.emum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 石头剪刀布手势 3 | */ 4 | export const RPS = { 5 | 石头: 1, 6 | 剪刀: 2, 7 | 布: 3, 8 | 0: '错误', 9 | 1: '石头', 10 | 2: '剪刀', 11 | 3: '布', 12 | } as const; 13 | 14 | /** 15 | * 猜拳输赢 16 | */ 17 | export const RPSResult = { 18 | 输: 1, 19 | 赢: 2, 20 | 平: 3, 21 | 1: '输', 22 | 2: '赢', 23 | 3: '平', 24 | } as const; 25 | 26 | /** 27 | * 回合输赢 28 | */ 29 | export const RoundResult = { 30 | 赢: 1, 31 | 输: 2, 32 | 1: '赢', 33 | 2: '输', 34 | } as const; 35 | -------------------------------------------------------------------------------- /src/enums/manga.emum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 漫画商城物品 id 与 物品名称的对应关系 3 | */ 4 | export const GoodsIdNameMap = { 5 | 195: '积分兑换', 6 | 1007: '商城5元无门槛券', 7 | 1008: '商城满99立减10元券', 8 | 1009: '商城5魔晶', 9 | 积分兑换: 195, 10 | 商城5元无门槛券: 1007, 11 | 商城满99立减10元券: 1008, 12 | 商城5魔晶: 1009, 13 | } as const; 14 | -------------------------------------------------------------------------------- /src/enums/packet.enum.ts: -------------------------------------------------------------------------------- 1 | export const PacketCmdEnum = { 2 | 节奏风暴: 'NOTICE_MSG', 3 | 弹幕消息: 'DANMU_MSG', 4 | 红包开始: 'POPULARITY_RED_POCKET_START', 5 | 红包中奖名单: 'POPULARITY_RED_POCKET_WINNER_LIST', 6 | 公共通知: 'COMMON_NOTICE_DANMAKU', 7 | 停止直播列表: 'STOP_LIVE_ROOM_LIST', 8 | 热门排行改变: 'HOT_RANK_CHANGED_V2', 9 | 观看数改变: 'WATCHED_CHANGE', 10 | 交互: 'INTERACT_WORD', 11 | 横幅活动: 'WIDGET_BANNER', 12 | 免费流量包推广: 'SPREAD_SHOW_FEET_V2', 13 | }; 14 | 15 | /** 16 | * 状态枚举 17 | */ 18 | export const ReturnStatus = { 19 | 退出: 0, 20 | 中场休眠: -1, 21 | 风控休眠: -2, 22 | 未获取到房间: -3, 23 | }; 24 | 25 | /** 26 | * ws 操作码 27 | */ 28 | export const OpType = { 29 | 心跳: 2, 30 | 接收cmd: 5, 31 | 认证: 7, 32 | }; 33 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | PUSHPLUS_TOKEN?: string; 5 | IS_LOCAL?: string; 6 | BILITOOLS_CONFIG?: string; 7 | LIVE_HEART_FORCE?: string; 8 | TENCENT_SECRET_ID?: string; 9 | TENCENT_SECRET_KEY?: string; 10 | ALI_SECRET_ID?: string; 11 | ALI_SECRET_KEY?: string; 12 | CONFIG_ITEM_INDEX?: string; 13 | USE_NETWORK_CODE?: string; 14 | BILITOOLS_IS_ASYNC?: string; 15 | BILIOUTILS_LOG_CLEAR_DAY?: string; 16 | /** 私有变量 */ 17 | __BT_CONFIG__: string; 18 | __BT_TASKS_STRING__: string; 19 | __BT_CONFIG_PATH__: string; 20 | __BT_CONFIG_ITEM__: string; 21 | /** VM 相关私有变量 */ 22 | __BT_VM_CONTEXT__: string; 23 | } 24 | } 25 | } 26 | export {}; 27 | -------------------------------------------------------------------------------- /src/index.cfc.ts: -------------------------------------------------------------------------------- 1 | export async function handler(event, _context, callback) { 2 | try { 3 | const { dailyTasks } = await import('./task/dailyTask'); 4 | const message = await dailyTasks(); 5 | callback?.(null, message); 6 | } catch (error) { 7 | callback?.(error); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/index.fc.ts: -------------------------------------------------------------------------------- 1 | import type { FCCallback, FCContext, FCEvent } from './types/fc'; 2 | import { defLogger } from './utils/log/def'; 3 | import { JSON5 } from './utils/json5'; 4 | 5 | /** 6 | * 公告 7 | */ 8 | const notice = async (msg?: string) => { 9 | defLogger.info(msg || `阿里云 FC 测试ing`); 10 | }; 11 | 12 | export async function dailyMain() { 13 | notice(); 14 | const { dailyHandler } = await import('./utils/serverless'); 15 | return await dailyHandler.run(); 16 | } 17 | 18 | export async function handler(event: Buffer, context: FCContext, callback: FCCallback) { 19 | try { 20 | const eventJson: FCEvent = JSON5.parse(event.toString()); 21 | const { dailyHandler } = await import('./utils/serverless'); 22 | dailyHandler.init({ 23 | event: eventJson, 24 | context, 25 | slsType: 'fc', 26 | }); 27 | let isReturn = false; 28 | if (eventJson.payload) { 29 | isReturn = await runTasks(eventJson.payload); 30 | } 31 | if (isReturn) { 32 | callback(null, 'success'); 33 | return; 34 | } 35 | const message = await dailyMain(); 36 | callback(null, message); 37 | } catch (error) { 38 | callback(error); 39 | } 40 | } 41 | 42 | export async function runTasks(payload: string) { 43 | try { 44 | const { runInputBiliTask } = await import('./task'); 45 | const payloadJson = JSON5.parse(payload); 46 | if (payloadJson.task) { 47 | await runInputBiliTask(payloadJson.task); 48 | return true; 49 | } 50 | } catch {} 51 | return false; 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ql.ts: -------------------------------------------------------------------------------- 1 | import { config, runTask, waitForArgs } from './util'; 2 | import { getArg, isArg } from './utils/args'; 3 | 4 | process.env.IS_QING_LONG = 'true'; 5 | 6 | (async () => { 7 | if (isArg('config')) { 8 | await waitForArgs(); 9 | const configs = await config(); 10 | if (!configs) { 11 | return; 12 | } 13 | const taskArg = getArg('task'); 14 | if (taskArg) { 15 | return await runTask(configs, './bin/inputTask', taskArg); 16 | } 17 | return await runTask(configs); 18 | } 19 | await waitForArgs(); 20 | const { getConfig } = await import('./config/setConfig'); 21 | const configs = getConfig(true); 22 | return await runTask(configs); 23 | })(); 24 | -------------------------------------------------------------------------------- /src/index.scf.ts: -------------------------------------------------------------------------------- 1 | import type { SCFContext, SCFEvent } from './types/scf'; 2 | import { defLogger } from './utils/log/def'; 3 | import { JSON5 } from './utils/json5'; 4 | 5 | /** 6 | * 公告 7 | */ 8 | const notice = async (msg?: string) => { 9 | defLogger.warn(msg || `SCF从9月开始会对日志进行收费!`); 10 | }; 11 | 12 | export async function dailyMain() { 13 | notice(); 14 | const { dailyHandler } = await import('./utils/serverless'); 15 | return await dailyHandler.run(); 16 | } 17 | 18 | export async function main_handler(event: SCFEvent, context: SCFContext) { 19 | const { dailyHandler } = await import('./utils/serverless'); 20 | dailyHandler.init({ 21 | event, 22 | context, 23 | slsType: 'scf', 24 | }); 25 | let isReturn = false; 26 | if (event.Message) { 27 | isReturn = await runTasks(event.Message); 28 | } 29 | if (isReturn) return 'success'; 30 | return dailyMain(); 31 | } 32 | 33 | export async function runTasks(payload: string) { 34 | try { 35 | const { runInputBiliTask } = await import('./task'); 36 | const payloadJson = JSON5.parse(payload); 37 | if (payloadJson.task) { 38 | await runInputBiliTask(payloadJson.task); 39 | return true; 40 | } 41 | } catch {} 42 | return false; 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { runTask } from './util'; 2 | process.env.IS_LOCAL = 'true'; 3 | 4 | (async function dailyMain() { 5 | await runTask(); 6 | })(); 7 | -------------------------------------------------------------------------------- /src/main.d.ts: -------------------------------------------------------------------------------- 1 | export declare function scf_handler(): any; 2 | export declare function fc_handler(): any; 3 | export declare function cfc_handler(): any; 4 | export declare function qinglong(): void; 5 | export declare function run(): void; 6 | export declare function sacnLogin(): Promise; 7 | export declare function runInputTasks(task: string): void; 8 | export declare const agc_handler: typeof cfc_handler; 9 | export declare const fg_handler: typeof cfc_handler; 10 | export declare const ql: typeof qinglong; 11 | export default run; 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | export function scf_handler() { 2 | return require('./index.scf').main_handler; 3 | } 4 | 5 | export function fc_handler() { 6 | return require('./index.fc').handler; 7 | } 8 | 9 | export function cfc_handler() { 10 | return require('./index.cfc').handler; 11 | } 12 | 13 | export function qinglong() { 14 | require('./index.ql'); 15 | } 16 | 17 | export function run() { 18 | require('./index'); 19 | } 20 | 21 | export function sacnLogin() { 22 | return require('./utils/login').scanLogin(); 23 | } 24 | 25 | export function runInputTasks(task: string) { 26 | return require('./task').runInputBiliTask(task); 27 | } 28 | 29 | export const agc_handler = cfc_handler; 30 | export const fg_handler = cfc_handler; 31 | export const ql = qinglong; 32 | 33 | export default run; 34 | -------------------------------------------------------------------------------- /src/net/activity-lottery.request.ts: -------------------------------------------------------------------------------- 1 | import type { ApiBaseProp } from '@/dto/bili-base-prop'; 2 | import type { LotteryResult } from '@/dto/activity-lottery.dto'; 3 | import { TaskConfig } from '@/config/globalVar'; 4 | import { OriginURLs } from '../constant/biliUri'; 5 | import { biliApi } from './api'; 6 | 7 | const wwwHeader = { 8 | origin: OriginURLs.www, 9 | }; 10 | /** 11 | * 检测抽奖次数 12 | */ 13 | export function getLotteryMyTimes(sid: string) { 14 | return biliApi.get>(`x/lottery/mytimes?sid=${sid}`, { 15 | headers: wwwHeader, 16 | }); 17 | } 18 | 19 | /** 20 | * 完成 21 | */ 22 | export function doLottery(sid: string, num = 1) { 23 | return biliApi.post( 24 | 'x/lottery/do', 25 | { 26 | sid, 27 | num, 28 | csrf: TaskConfig.BILIJCT, 29 | }, 30 | { 31 | headers: wwwHeader, 32 | }, 33 | ); 34 | } 35 | 36 | /** 37 | * 增加次数 38 | */ 39 | export function addLotteryTimes(sid: string, action_type = 3) { 40 | return biliApi.post>( 41 | 'x/lottery/addtimes', 42 | { 43 | sid, 44 | action_type, 45 | }, 46 | { 47 | headers: wwwHeader, 48 | }, 49 | ); 50 | } 51 | 52 | /** 53 | * 未知 54 | */ 55 | export function awardConf(sid: string) { 56 | return biliApi.get>(`x/lottery/award/conf?sid=${sid}`, { 57 | headers: wwwHeader, 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/net/api.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { baseURLs, RefererURLs } from '../constant/biliUri'; 3 | import { biliHttp, createRequest, defHttp } from '../utils/http'; 4 | 5 | const accountApi = createRequest({ 6 | baseURL: baseURLs.account, 7 | headers: { 8 | 'user-agent': TaskConfig.userAgent, 9 | }, 10 | }); 11 | 12 | const liveApi = createRequest({ 13 | baseURL: baseURLs.live, 14 | headers: { 15 | Referer: RefererURLs.www, 16 | 'user-agent': TaskConfig.userAgent, 17 | }, 18 | }); 19 | 20 | const biliApi = createRequest({ 21 | baseURL: baseURLs.api, 22 | headers: { 23 | Referer: RefererURLs.www, 24 | 'user-agent': TaskConfig.userAgent, 25 | }, 26 | }); 27 | 28 | const mangaApi = createRequest({ 29 | baseURL: baseURLs.manga, 30 | headers: { 31 | 'user-agent': TaskConfig.userAgent, 32 | }, 33 | }); 34 | 35 | const vcApi = createRequest({ 36 | baseURL: baseURLs.vc, 37 | headers: { 38 | 'user-agent': TaskConfig.userAgent, 39 | Referer: RefererURLs.www, 40 | }, 41 | }); 42 | 43 | const liveTraceApi = createRequest({ 44 | baseURL: baseURLs.liveTrace, 45 | headers: { 46 | 'user-agent': TaskConfig.userAgent, 47 | }, 48 | }); 49 | 50 | export { biliApi, vcApi, accountApi, mangaApi, liveApi, biliHttp, defHttp, liveTraceApi }; 51 | -------------------------------------------------------------------------------- /src/net/big-point.request.ts: -------------------------------------------------------------------------------- 1 | import type { PointListDto, TaskCombineDto } from '@/dto/big-point.dto'; 2 | import type { PureDataProp } from '@/dto/bili-base-prop'; 3 | import { biliApi, biliHttp } from './api'; 4 | import { TaskConfig } from '@/config/globalVar'; 5 | import type { TaskCodeType } from '@/enums/big-point.emum'; 6 | import { appSignString } from '@/utils/bili'; 7 | import { RefererURLs } from '@/constant/biliUri'; 8 | import { getUnixTime } from '@/utils/pure'; 9 | 10 | const baseHeader = { 11 | 'app-key': 'android64', 12 | env: 'prod', 13 | 'user-agent': TaskConfig.mobileUA, 14 | }; 15 | 16 | /** 17 | * 大积分签到 18 | */ 19 | export function signIn() { 20 | return biliApi.post>( 21 | 'pgc/activity/score/task/sign', 22 | appSignString({ 23 | csrf: TaskConfig.BILIJCT, 24 | }), 25 | { 26 | headers: baseHeader, 27 | }, 28 | ); 29 | } 30 | 31 | /** 32 | * 大积分领取任务 33 | */ 34 | export function receiveTask(taskCode: TaskCodeType = 'ogvwatch') { 35 | return biliApi.post( 36 | 'pgc/activity/score/task/receive', 37 | { 38 | csrf: TaskConfig.BILIJCT, 39 | ts: getUnixTime(), 40 | taskCode, 41 | }, 42 | { 43 | http2: true, 44 | headers: { 45 | 'Content-Type': 'application/json; charset=utf-8', 46 | Referer: RefererURLs.bigPointTask, 47 | ...baseHeader, 48 | navtive_api_from: 'h5', 49 | }, 50 | }, 51 | ); 52 | } 53 | 54 | export function susWin() { 55 | return biliApi.post>>( 56 | 'pgc/activity/deliver/susWin/receive', 57 | appSignString({ 58 | csrf: TaskConfig.BILIJCT, 59 | }), 60 | { 61 | headers: baseHeader, 62 | }, 63 | ); 64 | } 65 | 66 | /** 67 | * 完成大积分每日任务 68 | */ 69 | export function complete(position: string) { 70 | return biliApi.post( 71 | 'pgc/activity/deliver/task/complete', 72 | appSignString({ 73 | csrf: TaskConfig.BILIJCT, 74 | position, 75 | }), 76 | { 77 | headers: { 78 | ...baseHeader, 79 | referer: RefererURLs.bigPoint, 80 | }, 81 | }, 82 | ); 83 | } 84 | 85 | /** 86 | * 提交事件 87 | */ 88 | export function showDispatch(eventId: string) { 89 | return biliHttp.post<{ 90 | code: number; 91 | message: string; 92 | data: never; 93 | errtag: number; 94 | ttl: number; 95 | }>( 96 | `https://show.bilibili.com/api/activity/fire/common/event/dispatch?${appSignString({ 97 | csrf: TaskConfig.BILIJCT, 98 | })}`, 99 | { 100 | eventId, 101 | }, 102 | { 103 | headers: { 104 | 'content-type': 'application/json; charset=utf-8', 105 | ...baseHeader, 106 | }, 107 | }, 108 | ); 109 | } 110 | 111 | /** 112 | * 获取大积分任务列表 113 | */ 114 | export function getTaskCombine() { 115 | return biliApi.get('x/vip_point/task/combine', { 116 | headers: baseHeader, 117 | }); 118 | } 119 | 120 | /** 121 | * 积分记录 122 | */ 123 | export function getPointList() { 124 | return biliApi.get( 125 | `x/vip_point/list?${appSignString({ 126 | csrf: TaskConfig.BILIJCT, 127 | change_type: 1, 128 | pn: 1, 129 | ps: 10, 130 | })}`, 131 | { 132 | headers: baseHeader, 133 | }, 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/net/daily-battery.request.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { UserTaskProgress } from '@/dto/daily-battery.dto'; 3 | import { liveApi } from './api'; 4 | 5 | /** 6 | * 获取任务进度 7 | */ 8 | export function getUserTaskProgress() { 9 | return liveApi.get('xlive/app-ucenter/v1/userTask/GetUserTaskProgress'); 10 | } 11 | 12 | /** 13 | * 领取任务奖励 14 | */ 15 | export function receiveTaskReward() { 16 | return liveApi.post('xlive/app-ucenter/v1/userTask/UserTaskReceiveRewards', { 17 | task_id: TaskConfig.USERID, 18 | csrf: TaskConfig.BILIJCT, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/net/intimacy.request.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import type { ApiBaseProp } from '@/dto/bili-base-prop'; 3 | import type { LiveHeartBeatRes, ShareLiveRoomRes } from '@/dto/intimacy.dto'; 4 | import { clientSign } from '@/utils/bili'; 5 | import { createUUID, createBuvid, randomString } from '@/utils/pure'; 6 | import { liveApi, liveTraceApi } from './api'; 7 | 8 | /** 9 | * 分享直播间 10 | */ 11 | export function shareLiveRoom(roomid: number) { 12 | return liveApi.post(`xlive/web-room/v1/index/TrigerInteract`, { 13 | roomid, 14 | interact_type: 3, 15 | csrf: TaskConfig.BILIJCT, 16 | csrf_token: TaskConfig.BILIJCT, 17 | visit_id: '', 18 | }); 19 | } 20 | 21 | /** 22 | * 点赞直播间 23 | */ 24 | export function likeLiveRoom(roomid: number) { 25 | return liveApi.post(`xlive/web-ucenter/v1/interact/likeInteract`, { 26 | roomid, 27 | csrf: TaskConfig.BILIJCT, 28 | csrf_token: TaskConfig.BILIJCT, 29 | }); 30 | } 31 | 32 | export interface MobileHeartBeatParams { 33 | buvid?: string; 34 | gu_id?: string; 35 | visit_id?: string; 36 | uuid?: string; 37 | click_id?: string; 38 | room_id: number | string; 39 | up_id: number | string; 40 | uid?: number | string; 41 | up_level?: number | string; 42 | up_session?: string; 43 | parent_id?: number | string; 44 | area_id?: number | string; 45 | } 46 | 47 | /** 48 | * 直播心跳(移动端) 49 | */ 50 | export function liveMobileHeartBeat({ 51 | buvid = createBuvid(), 52 | gu_id = randomString(43).toLocaleUpperCase(), 53 | visit_id = randomString(32).toLocaleLowerCase(), 54 | uuid = createUUID(), 55 | click_id = createUUID(), 56 | room_id, 57 | up_id, 58 | uid, 59 | up_level = 40, 60 | up_session = '', 61 | parent_id = 11, 62 | area_id = 376, 63 | }: MobileHeartBeatParams) { 64 | const baseData = { 65 | platform: 'android', 66 | uuid, 67 | buvid, 68 | seq_id: '1', 69 | room_id, 70 | parent_id, 71 | area_id, 72 | timestamp: String(parseInt(String(new Date().getTime() / 1000)) - 60), 73 | secret_key: 'axoaadsffcazxksectbbb', 74 | watch_time: '60', 75 | up_id: up_id || !uid, 76 | up_level, 77 | jump_from: '30000', 78 | gu_id, 79 | play_type: '0', 80 | play_url: '', 81 | s_time: '0', 82 | data_behavior_id: '', 83 | data_source_id: '', 84 | up_session, 85 | visit_id, 86 | watch_status: '', 87 | click_id, 88 | session_id: '-99998', 89 | player_type: '0', 90 | client_ts: String(parseInt(String(new Date().getTime() / 1000))), 91 | }; 92 | const data = { 93 | ...baseData, 94 | ts: parseInt(String(new Date().getTime() / 1000)), 95 | client_sign: clientSign(baseData), 96 | csrf_token: TaskConfig.BILIJCT, 97 | csrf: TaskConfig.BILIJCT, 98 | }; 99 | return liveTraceApi.post( 100 | 'xlive/data-interface/v1/heartbeat/mobileHeartBeat', 101 | data, 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/net/jury.request.ts: -------------------------------------------------------------------------------- 1 | import type { ApiBaseProp } from '@/dto/bili-base-prop'; 2 | import type { 3 | JuryCaseOpinionDto, 4 | JuryCaseNextDto, 5 | JuryInfoDto, 6 | JuryVotedCaseDto, 7 | JuryCaseInfoDto, 8 | } from '@/dto/jury.dto'; 9 | import { TaskConfig } from '@/config/globalVar'; 10 | import { biliApi } from './api'; 11 | import { OriginURLs, RefererURLs } from '@/constant/biliUri'; 12 | 13 | function getCaseDetailHeader(case_id: string) { 14 | return { 15 | Origin: OriginURLs.www, 16 | Referer: `${RefererURLs.judge}case-detail/${case_id}`, 17 | }; 18 | } 19 | 20 | /** 21 | * 获取当前账户风纪委员状态 22 | */ 23 | export function getJury() { 24 | return biliApi.get('x/credit/v2/jury/jury', { 25 | headers: { 26 | Origin: OriginURLs.www, 27 | Referer: RefererURLs.judge, 28 | }, 29 | }); 30 | } 31 | 32 | /** 33 | * 申请风纪委员资格 34 | */ 35 | export function applyJury() { 36 | return biliApi.post( 37 | 'x/credit/v2/jury/apply', 38 | { 39 | csrf: TaskConfig.BILIJCT, 40 | }, 41 | { 42 | headers: { 43 | Origin: OriginURLs.www, 44 | }, 45 | }, 46 | ); 47 | } 48 | 49 | /** 50 | * 获取风纪委员案件信息 51 | */ 52 | export function getJuryCase(case_id: string) { 53 | return biliApi.get(`x/credit/v2/jury/case/info?case_id=${case_id}`, { 54 | headers: getCaseDetailHeader(case_id), 55 | }); 56 | } 57 | 58 | /** 59 | * 拉取一个案件用于风纪委员投票 60 | */ 61 | export function getJuryCaseVote() { 62 | return biliApi.get(`x/credit/v2/jury/case/next?csrf=${TaskConfig.BILIJCT}`, { 63 | headers: { 64 | Origin: OriginURLs.www, 65 | Referer: RefererURLs.judge + 'index', 66 | }, 67 | }); 68 | } 69 | 70 | /** 71 | * 获取风纪委员案件众议观点 72 | */ 73 | export function getJuryCaseViewpoint(case_id: string) { 74 | return biliApi.get( 75 | `x/credit/v2/jury/case/opinion?case_id=${case_id}&pn=1&ps=20`, 76 | { 77 | headers: getCaseDetailHeader(case_id), 78 | }, 79 | ); 80 | } 81 | 82 | /** 83 | * 风纪委员案件投票 84 | */ 85 | export function juryCaseVote(case_id: string, vote: number, insiders = 0, anonymous = 0) { 86 | return biliApi.post>( 87 | 'x/credit/v2/jury/vote', 88 | { 89 | case_id, 90 | vote, 91 | csrf: TaskConfig.BILIJCT, 92 | insiders, 93 | anonymous, 94 | }, 95 | { 96 | headers: getCaseDetailHeader(case_id), 97 | }, 98 | ); 99 | } 100 | 101 | /** 102 | * 获取最近20条已投票案件 103 | */ 104 | export function getJuryCaseList() { 105 | return biliApi.get('x/credit/v2/jury/case/list?pn=1&ps=20', { 106 | headers: { 107 | Origin: OriginURLs.www, 108 | Referer: RefererURLs.judge + 'index', 109 | }, 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /src/net/live-heart.request.ts: -------------------------------------------------------------------------------- 1 | import type { LiveHeartERequest, LiveHeartXRequest } from '../types/LiveHeart'; 2 | import type { LiveHeartEDto } from '../dto/live.dto'; 3 | import { biliHttp, liveApi, liveTraceApi } from './api'; 4 | import { appSignString } from '@/utils/bili'; 5 | import { TaskConfig } from '@/config/globalVar'; 6 | import { createUUID, getUnixTime } from '@/utils'; 7 | import { LiveHeartBeatRes } from '@/dto/intimacy.dto'; 8 | 9 | /** 10 | * 发送 E 请求 11 | */ 12 | export function postE(postData: LiveHeartERequest): Promise { 13 | return biliHttp.post('https://live-trace.bilibili.com/xlive/data-interface/v1/x25Kn/E', postData); 14 | } 15 | 16 | /** 17 | * 发送 X 请求 18 | */ 19 | export function postX(postData: LiveHeartXRequest): Promise { 20 | return biliHttp.post('https://live-trace.bilibili.com/xlive/data-interface/v1/x25Kn/X', postData); 21 | } 22 | 23 | /** 24 | * 不知道有啥用 25 | */ 26 | export function heartBeat() { 27 | return biliHttp.get('https://api.live.bilibili.com/relation/v1/Feed/heartBeat'); 28 | } 29 | 30 | /** 31 | * 这是啥(移动端) 32 | */ 33 | export function entryRoomMoblie({ 34 | room_id, 35 | up_id = 0, 36 | parent_id = 0, 37 | area_id = 0, 38 | }: { 39 | room_id: number; 40 | up_id?: number; 41 | parent_id?: number; 42 | area_id?: number; 43 | }) { 44 | return liveTraceApi.post( 45 | 'xlive/data-interface/v1/heartbeat/mobileEntry', 46 | appSignString({ 47 | csrf: TaskConfig.BILIJCT, 48 | buvid: TaskConfig.buvid, 49 | client_ts: getUnixTime(), 50 | seq_id: 0, 51 | uuid: createUUID(), 52 | parent_id, 53 | room_id, 54 | up_id, 55 | is_patch: 0, 56 | heart_beat: [], 57 | area_id, 58 | mobi_app: 'android', 59 | }), 60 | ); 61 | } 62 | 63 | /** 64 | * 进入直播间(移动端) 65 | */ 66 | export function entryActionMobile({ 67 | room_id, 68 | jumpFrom = 1546736, 69 | }: { 70 | room_id: number; 71 | jumpFrom?: number; 72 | }) { 73 | return liveApi.post( 74 | 'xlive/app-room/v1/index/roomEntryAction', 75 | appSignString({ 76 | csrf: TaskConfig.BILIJCT, 77 | jumpFrom, 78 | noHistory: 0, 79 | room_id, 80 | }), 81 | ); 82 | } 83 | 84 | /** 85 | * 进入直播间( PC 端) 86 | */ 87 | export function entryRoomPc(room_id: number) { 88 | return liveApi.post('xlive/web-room/v1/index/roomEntryAction', { 89 | csrf: TaskConfig.BILIJCT, 90 | csrf_token: TaskConfig.BILIJCT, 91 | room_id, 92 | visit_id: '', 93 | platform: 'pc', 94 | }); 95 | } 96 | 97 | /** 98 | * 未知 99 | */ 100 | export function getAnchorInRoom(roomid: number) { 101 | return liveApi.get(`live_user/v1/UserInfo/get_anchor_in_room?roomid=${roomid}}`); 102 | } 103 | -------------------------------------------------------------------------------- /src/net/match-game.request.ts: -------------------------------------------------------------------------------- 1 | import { biliApi } from './api'; 2 | import { GuessCollectionDto } from '../dto/match-game.dto'; 3 | import { ApiBaseProp } from '../dto/bili-base-prop'; 4 | import { TaskConfig } from '../config/globalVar'; 5 | 6 | /** 7 | * 获取比赛集合 8 | * @param stime 开始时间 9 | * @param etime 结束时间 10 | */ 11 | export function getGuessCollection(stime = '', etime = ''): Promise { 12 | return biliApi.get( 13 | `/x/esports/guess/collection/question?pn=1&ps=50&gid=&sids=&stime=${stime}&etime=${etime}`, 14 | ); 15 | } 16 | 17 | /** 18 | * 预测 19 | * @param oid contestId 20 | * @param main_id questionId 21 | * @param detail_id team.detail_id 22 | * @param count 硬币数量 23 | */ 24 | export function guessAdd( 25 | oid: number, 26 | main_id: number, 27 | detail_id: number, 28 | count: number, 29 | ): Promise { 30 | const csrf = TaskConfig.BILIJCT; 31 | const postData = { 32 | is_fav: 0, 33 | main_id, 34 | oid, 35 | detail_id, 36 | count, 37 | csrf, 38 | csrf_token: csrf, 39 | }; 40 | return biliApi.post('/x/esports/guess/add', postData); 41 | } 42 | -------------------------------------------------------------------------------- /src/net/red-packet.request.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import type { AugRedPacket2022Controller } from '@/dto/red-packet.dto'; 3 | import { logger } from '@/utils/log'; 4 | import { biliHttp } from './api'; 5 | 6 | export async function getRedPacketController() { 7 | if (TaskConfig.redPack.uri === '') { 8 | return; 9 | } 10 | try { 11 | const resp = await biliHttp.get<{ _ts_rpc_return_: AugRedPacket2022Controller }>( 12 | TaskConfig.redPack.uri, 13 | ); 14 | const { code, message, data } = getRedPacket(resp) || {}; 15 | if (code !== 0) { 16 | logger.debug(`${code} ${message}`); 17 | return; 18 | } 19 | return data?.list; 20 | } catch (error) { 21 | logger.error(`获取红包列表异常: ${error.message}`); 22 | } 23 | } 24 | 25 | function getRedPacket(resp: { 26 | _ts_rpc_return_: AugRedPacket2022Controller; 27 | }): AugRedPacket2022Controller | undefined { 28 | if (Reflect.has(resp, '_ts_rpc_return_')) { 29 | return resp._ts_rpc_return_; 30 | } 31 | if (Reflect.has(resp, 'data') && Reflect.has(resp, 'code')) { 32 | return resp as unknown as AugRedPacket2022Controller; 33 | } 34 | return; 35 | } 36 | -------------------------------------------------------------------------------- /src/net/reservation.request.ts: -------------------------------------------------------------------------------- 1 | import type { ReservationDto, ReserveButtonDto } from '@/dto/reservation.dto'; 2 | import type { ApiBaseProp } from '@/dto/bili-base-prop'; 3 | import { TaskConfig } from '@/config/globalVar'; 4 | import { OriginURLs } from '@/constant/biliUri'; 5 | import { biliApi, vcApi } from './api'; 6 | 7 | function getHeader(vmid: number | string) { 8 | return { 9 | origin: OriginURLs.space, 10 | referer: OriginURLs.space + `/${vmid}/`, 11 | }; 12 | } 13 | 14 | /** 15 | * 获取预约列表 16 | */ 17 | export function reservation(vmid: number | string) { 18 | return biliApi.get(`x/space/reservation?vmid=${vmid}`, { 19 | headers: getHeader(vmid), 20 | }); 21 | } 22 | 23 | /** 24 | * 预约 25 | * 75077 重复参加活动! 26 | */ 27 | export function reserve(sid: number, vmid?: number | string) { 28 | return biliApi.post>( 29 | `x/space/reserve`, 30 | { sid, csrf: TaskConfig.BILIJCT, jsonp: 'jsonp' }, 31 | { headers: getHeader(vmid || TaskConfig.USERID) }, 32 | ); 33 | } 34 | (async () => { 35 | console.log(JSON.stringify(await reservation(36081646))); 36 | console.log(JSON.stringify(await reserve(1199852))); 37 | })(); 38 | 39 | // http://api.bilibili.com/x/space/reservation?vmid=36081646 40 | // { 41 | // "code": 0, 42 | // "message": "0", 43 | // "ttl": 1, 44 | // "data": [ 45 | // { 46 | // "sid": 1199852, 47 | // "name": "预告:【洛天依】用阿卡贝拉的方式打开经典《茉莉花》", 48 | // "total": 3810, 49 | // "stime": 1672060198, 50 | // "etime": 2147454847, 51 | // "is_follow": 0, 52 | // "state": 120, 53 | // "oid": "904264736", 54 | // "type": 1, 55 | // "up_mid": 36081646, 56 | // "reserve_record_ctime": 0, 57 | // "live_plan_start_time": 1672131600, 58 | // "show_total": true, 59 | // "subtitle": "", 60 | // "attached_badge_text": "" 61 | // } 62 | // ] 63 | // } 64 | 65 | /** 66 | * 预约2/取消预约 67 | * @param cur_btn_status 1 预约 2 取消预约 68 | * @default 1 69 | */ 70 | export function reserveAttachCardButton( 71 | reserve_id: number, 72 | reserve_total: number, 73 | cur_btn_status: 1 | 2 = 1, 74 | ) { 75 | return vcApi.post( 76 | `dynamic_mix/v1/dynamic_mix/reserve_attach_card_button`, 77 | { csrf: TaskConfig.BILIJCT, cur_btn_status, reserve_id, reserve_total }, 78 | { 79 | headers: { 80 | origin: OriginURLs.www, 81 | }, 82 | }, 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/net/session.request.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { OriginURLs } from '@/constant/biliUri'; 3 | import type { SessionDto, SessionHistoryDto } from '@/dto/session.dto'; 4 | import { vcApi } from './api'; 5 | 6 | /** 7 | * 获取会话 8 | */ 9 | export function getSession({ session_type } = { session_type: 1 }) { 10 | return vcApi.get( 11 | `session_svr/v1/session_svr/get_sessions?session_type=${session_type}&group_fold=1&unfollow_fold=1&sort_rule=2&build=0&mobi_app=web`, 12 | { 13 | headers: { 14 | Origin: OriginURLs.message, 15 | }, 16 | }, 17 | ); 18 | } 19 | 20 | /** 21 | * 读取会话 22 | */ 23 | export function readSession({ 24 | talker_id, 25 | session_type, 26 | ack_seqno, 27 | }: { 28 | talker_id: number; 29 | session_type: number; 30 | ack_seqno: number; 31 | }) { 32 | return vcApi.post( 33 | 'session_svr/v1/session_svr/update_ack', 34 | { 35 | talker_id, 36 | session_type, 37 | ack_seqno, 38 | build: 0, 39 | mobi_app: 'web', 40 | csrf_token: TaskConfig.BILIJCT, 41 | csrf: TaskConfig.BILIJCT, 42 | }, 43 | { 44 | headers: { 45 | origin: OriginURLs.message, 46 | }, 47 | }, 48 | ); 49 | } 50 | 51 | /** 52 | * 删除会话 53 | */ 54 | export function deleteSession({ 55 | talker_id, 56 | session_type, 57 | }: { 58 | talker_id: number; 59 | session_type: number; 60 | }) { 61 | return vcApi.post( 62 | 'session_svr/v1/session_svr/remove_session', 63 | { 64 | talker_id, 65 | session_type, 66 | build: 0, 67 | mobi_app: 'web', 68 | csrf_token: TaskConfig.BILIJCT, 69 | csrf: TaskConfig.BILIJCT, 70 | }, 71 | { 72 | headers: { 73 | Origin: OriginURLs.message, 74 | }, 75 | }, 76 | ); 77 | } 78 | 79 | /** 80 | * 获取某个会话的历史消息 81 | */ 82 | export function getSessionHistory(talker_id = 17561219, session_type = 1) { 83 | return vcApi.get( 84 | `svr_sync/v1/svr_sync/fetch_session_msgs?sender_device_id=1&talker_id=${talker_id}&session_type=${session_type}&size=20&build=0&mobi_app=web`, 85 | { 86 | headers: { 87 | Origin: OriginURLs.message, 88 | }, 89 | }, 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/net/sup-group.request.ts: -------------------------------------------------------------------------------- 1 | import type { SupGroupsDto, SupGroupsSignDto } from '../dto/sup-group.dto'; 2 | import { vcApi } from './api'; 3 | 4 | /** 5 | * 获取自己的应援团 6 | */ 7 | export function getMyGroupsApi(): Promise { 8 | return vcApi.get('/link_group/v1/member/my_groups'); 9 | } 10 | 11 | /** 12 | * 应援团签到 13 | * @param group_id 应援团id 14 | * @param owner_id 爱豆ID(狗头) 15 | */ 16 | export function groupSignApi(group_id: number, owner_id: number): Promise { 17 | return vcApi.get('/link_setting/v1/link_setting/sign_in', { 18 | params: { 19 | group_id, 20 | owner_id, 21 | }, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/net/video.request.ts: -------------------------------------------------------------------------------- 1 | import type { ApiBaseProp } from '@/dto/bili-base-prop'; 2 | import type { 3 | ShareAddDto, 4 | RegionRankingVideosDto, 5 | DonatedCoinsForVideoDto, 6 | HeartbeatDto, 7 | RecommendVideoDto, 8 | BangumiFollowDto, 9 | } from '../dto/video.dto'; 10 | import { biliApi } from './api'; 11 | import { TaskConfig } from '../config/globalVar'; 12 | 13 | /** 14 | * 分享视频 15 | * @param aid 分享的视频av号 16 | */ 17 | export function addShare(aid: number | string): Promise { 18 | const reqData = { 19 | csrf: TaskConfig.BILIJCT, 20 | aid, 21 | }; 22 | return biliApi.post('/x/web-interface/share/add', reqData); 23 | } 24 | 25 | /** 26 | * 获取视频分区列表 27 | * @param rid 分区编号 28 | * @param day 排行方式 29 | */ 30 | export function getRegionRankingVideos(rid = 1, day = 3): Promise { 31 | return biliApi.get('/x/web-interface/ranking/region', { 32 | params: { 33 | rid, 34 | day, 35 | }, 36 | }); 37 | } 38 | 39 | /** 40 | * 获取推荐视频列表 41 | */ 42 | export function getRecommendVideos(ps = 2) { 43 | return biliApi.get( 44 | `x/web-interface/index/top/rcmd?fresh_type=12&version=1&ps=${ps}`, 45 | ); 46 | } 47 | 48 | /** 49 | * 给该视频投币数量 50 | * @param aid 视频av号 51 | */ 52 | export function donatedCoinsForVideo(aid: number): Promise { 53 | return biliApi.get('/x/web-interface/archive/coins', { 54 | params: { aid }, 55 | }); 56 | } 57 | 58 | /** 59 | * 上传视频已经观看时间 60 | * @param aid av号 61 | * @param playedTime 观看时间 62 | */ 63 | export function uploadVideoHeartbeat( 64 | aid: number | string, 65 | playedTime: number, 66 | ): Promise { 67 | return biliApi.post('/x/click-interface/web/heartbeat', { 68 | aid, 69 | played_time: playedTime, 70 | }); 71 | } 72 | 73 | interface HeartbeatParams { 74 | start_ts: number; 75 | aid?: number; 76 | cid?: number; 77 | type?: number; 78 | sub_type?: number; 79 | dt?: number; 80 | play_type?: number; 81 | realtime?: number; 82 | played_time?: number; 83 | real_played_time?: number; 84 | refer_url?: string; 85 | sid?: number; 86 | epid?: number; 87 | } 88 | 89 | /** 90 | * 视频观看心跳 91 | */ 92 | export function videoHeartbeat({ 93 | start_ts, 94 | aid = 243332804, 95 | cid = 209599683, 96 | type = 4, 97 | sub_type = 5, 98 | dt = 2, 99 | play_type = 0, 100 | realtime = 0, 101 | played_time = 10, 102 | real_played_time = 0, 103 | refer_url = '', 104 | sid = 33622, 105 | epid = 327107, 106 | }: HeartbeatParams) { 107 | return biliApi.post>('x/click-interface/web/heartbeat', { 108 | start_ts, 109 | mid: TaskConfig.USERID, 110 | aid, 111 | cid, 112 | type, 113 | sub_type, 114 | dt, 115 | play_type, 116 | realtime, 117 | played_time, 118 | real_played_time, 119 | refer_url, 120 | bsource: '', 121 | sid, 122 | epid, 123 | spmid: '666.25', 124 | from_spmid: '..0.0', 125 | csrf: TaskConfig.BILIJCT, 126 | }); 127 | } 128 | 129 | /** 130 | * 追剧 131 | */ 132 | export function addBangumi(season_id: number | string) { 133 | return biliApi.post('pgc/web/follow/add', { 134 | season_id, 135 | csrf: TaskConfig.BILIJCT, 136 | }); 137 | } 138 | 139 | /**Promise('pgc/web/follow/del', { 144 | season_id, 145 | csrf: TaskConfig.BILIJCT, 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /src/net/vip.request.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ChargingDto, 3 | ChargingMessageDto, 4 | ReceiveVipMyDto, 5 | ReceiveVipPrivilegeDto, 6 | } from '../dto/vip-privilege.dto'; 7 | import { TaskConfig } from '../config/globalVar'; 8 | import { biliApi } from './api'; 9 | import { OriginURLs, RefererURLs } from '@/constant/biliUri'; 10 | 11 | /** 12 | * 领取年度大会员权益 13 | * @param type 1.大会员B币券;2.大会员福利 14 | */ 15 | export function receiveVipPrivilege(type = 1): Promise { 16 | return biliApi.post( 17 | '/x/vip/privilege/receive', 18 | { 19 | csrf: TaskConfig.BILIJCT, 20 | type, 21 | }, 22 | { 23 | headers: { 24 | origin: OriginURLs.www, 25 | referer: RefererURLs.www, 26 | }, 27 | }, 28 | ); 29 | } 30 | 31 | /** 32 | * 查看大会员权益领取状态 33 | */ 34 | export function receiveVipMy() { 35 | return biliApi.get('/x/vip/privilege/my', { 36 | headers: { 37 | origin: OriginURLs.account, 38 | referer: RefererURLs.www, 39 | }, 40 | }); 41 | } 42 | 43 | /** 44 | * 为 up 充电 45 | * @param bp_num 充电 b 币数量 46 | * @param is_bp_remains_prior B币充电请选择true 47 | * @param up_mid 充电对象用户UID 48 | * @param otype 充电来源 空间充电/视频充电 49 | * @param oid 充电来源代码(UID 或者 稿件 avID) 50 | */ 51 | export function chargingForUp( 52 | bp_num = 50, 53 | is_bp_remains_prior = true, 54 | up_mid: number = TaskConfig.USERID, 55 | otype: 'up' | 'archive' = 'up', 56 | oid: number = up_mid, 57 | ): Promise { 58 | return biliApi.post('/x/ugcpay/web/v2/trade/elec/pay/quick', { 59 | csrf: TaskConfig.BILIJCT, 60 | bp_num, 61 | is_bp_remains_prior, 62 | up_mid, 63 | otype, 64 | oid, 65 | }); 66 | } 67 | 68 | /** 69 | * 充电后留言 70 | * @param orderId 留言token 71 | * @param message 留言内容 72 | */ 73 | export function chargingCommentsForUp( 74 | orderId: string, 75 | message = '支持大佬一波', 76 | ): Promise { 77 | return biliApi.post('/x/ugcpay/trade/elec/message', { 78 | csrf: TaskConfig.BILIJCT, 79 | message, 80 | order_id: orderId, 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { defHttp } from '@/utils/http/def'; 2 | import { defLogger } from '@/utils/log/def'; 3 | import { appSignString } from '@/utils/bili'; 4 | import { CookieJar } from '@/utils/cookie'; 5 | import { RequestError } from 'got'; 6 | 7 | interface AcgTvLoginResponse { 8 | code: number; 9 | status: boolean; 10 | ts: number; 11 | data: { 12 | api_host: string; 13 | has_login: number; 14 | direct_login: number; 15 | user_info: { mid: string; uname: string; face: string }; 16 | confirm_uri: string; 17 | }; 18 | } 19 | 20 | export async function accessKey2Cookie(access_key: string) { 21 | const cookieJar = new CookieJar(); 22 | await defHttp.get( 23 | `https://passport.bilibili.com/api/login/sso?${appSignString({ 24 | access_key, 25 | gourl: 'https://account.bilibili.com/account/home', 26 | })}`, 27 | { 28 | cookieJar, 29 | requestOptions: { withCredentials: true }, 30 | }, 31 | ); 32 | return cookieJar.getCookieString(); 33 | } 34 | 35 | async function getAcgTvLogin(cookieJar: CookieJar) { 36 | try { 37 | const { data } = await defHttp.get( 38 | 'https://passport.bilibili.com/login/app/third?appkey=27eb53fc9058f8c3&api=http://link.acg.tv/forum.php&sign=67ec798004373253d60114caaad89a8c', 39 | { 40 | cookieJar, 41 | }, 42 | ); 43 | return data?.confirm_uri; 44 | } catch (error) { 45 | defLogger.error(error); 46 | } 47 | } 48 | 49 | export async function cookie2AccessKey(cookie: string) { 50 | const cookieJar = new CookieJar(cookie); 51 | const confirm_uri = await getAcgTvLogin(cookieJar); 52 | if (!confirm_uri) { 53 | return; 54 | } 55 | try { 56 | await defHttp.get(confirm_uri, { cookieJar }); 57 | } catch (error) { 58 | if (error instanceof RequestError) { 59 | const url = error.request?.requestUrl; 60 | if (!url) { 61 | return; 62 | } 63 | const usp = new URLSearchParams(url.split('?')?.[1]); 64 | return usp.get('access_key'); 65 | } 66 | } 67 | } 68 | 69 | export async function getNewCookie(cookie: string) { 70 | const access_key = await cookie2AccessKey(cookie); 71 | if (!access_key) { 72 | defLogger.error('获取 access_key 失败!'); 73 | return; 74 | } 75 | return await accessKey2Cookie(access_key); 76 | } 77 | -------------------------------------------------------------------------------- /src/service/balance.service.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig, TaskModule } from '@/config/globalVar'; 2 | import { defaultComments } from '../constant'; 3 | import { isArray } from '@/utils/is'; 4 | import { logger } from '@/utils/log'; 5 | import { getMonthHasDays, getPRCDate, random } from '@/utils/pure'; 6 | import { chargingCommentsForUp, chargingForUp } from '@/net/vip.request'; 7 | import { updateNav } from './nav.service'; 8 | import { apiDelay } from '@/utils/effect'; 9 | import { exchangeBattery } from '@/net/live.request'; 10 | 11 | async function init() { 12 | // 根据时间确定是否执行 13 | const nowTime = getPRCDate(), 14 | today = nowTime.getDate(), 15 | monthHasDays = getMonthHasDays(nowTime); 16 | 17 | let presetTime: number[] = TaskConfig.couponBalance.presetTime; 18 | if (!isArray(presetTime)) { 19 | presetTime = [presetTime]; 20 | } 21 | 22 | const isInPresetTime = presetTime.some(time => time === today) || presetTime.length === 0; 23 | const isLastDay = monthHasDays === today; 24 | // 判断是否在指定时间内 25 | if (!isInPresetTime && !isLastDay) { 26 | logger.info(`不在预设时间,不符合条件`); 27 | return false; 28 | } 29 | 30 | // 充电前获取下 nav 31 | await updateNav(); 32 | await apiDelay(); 33 | 34 | // 判断余额是否足够 35 | const useType = TaskConfig.couponBalance.use, 36 | bp_num = TaskModule.couponBalance; 37 | if (useType === '充电' && bp_num < 2) { 38 | logger.info(`剩余券为${bp_num},不足2跳过充电`); 39 | return false; 40 | } 41 | if (bp_num < 1) { 42 | logger.info(`剩余券为${bp_num},跳过兑换`); 43 | return false; 44 | } 45 | 46 | logger.info(`b 币券余额${bp_num}`); 47 | return true; 48 | } 49 | 50 | /** 51 | * 留言只是不作处理 52 | */ 53 | export async function chargeComments() { 54 | try { 55 | if (!TaskModule.chargeOrderNo) { 56 | return false; 57 | } 58 | const comment = defaultComments[random(0, defaultComments.length - 1)]; 59 | const { code } = await chargingCommentsForUp(TaskModule.chargeOrderNo, comment); 60 | if (code === 0) { 61 | logger.info('留言成功!'); 62 | } 63 | } catch (error) { 64 | logger.warn(error); 65 | } 66 | } 67 | 68 | enum ChargeStatus { 69 | '成功' = 4, 70 | '低于20电池' = -2, 71 | 'B币不足' = -4, 72 | } 73 | 74 | export async function chargingService() { 75 | if (!(await init())) { 76 | return; 77 | } 78 | 79 | try { 80 | const bp_num = TaskModule.couponBalance || 0; 81 | let errorCount = 0; 82 | const up_mid = TaskConfig.couponBalance.mid; 83 | // 固定为 up 模式 84 | const run = async () => { 85 | const { code, message, data } = await chargingForUp(bp_num, true, up_mid); 86 | if (code !== 0) { 87 | logger.warn(`充电失败:${code} ${message}`); 88 | return; 89 | } 90 | logger.info(`目标【${up_mid}】${ChargeStatus[data.status]}`); 91 | 92 | if (data.status === ChargeStatus['成功']) { 93 | TaskModule.chargeOrderNo = data.order_no; 94 | await apiDelay(); 95 | await chargeComments(); 96 | } 97 | }; 98 | TaskModule.chargeOrderNo = ''; 99 | while (!TaskModule.chargeOrderNo) { 100 | await run(); 101 | await apiDelay(); 102 | // 尝试 4 次 103 | if (errorCount++ > 2) { 104 | break; 105 | } 106 | } 107 | } catch (error) { 108 | logger.error(`充电出现异常:${error.message}`); 109 | logger.error(error); 110 | } 111 | } 112 | 113 | /** 114 | * 兑换电池 115 | */ 116 | export async function exchangeBatteryService() { 117 | if (!(await init())) { 118 | return; 119 | } 120 | try { 121 | const bp_num = TaskModule.couponBalance || 0; 122 | if (bp_num < 1) { 123 | logger.info(`剩余券为${bp_num},跳过兑换`); 124 | return; 125 | } 126 | const { code, message } = await exchangeBattery(bp_num); 127 | if (code === 0) { 128 | logger.info(`兑换电池${bp_num * 10}成功!`); 129 | return; 130 | } 131 | logger.warn(`兑换电池失败:${code} ${message}`); 132 | } catch (error) { 133 | logger.error(`兑换电池出现异常:${error.message}`); 134 | logger.error(error); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/service/bangumi.service.ts: -------------------------------------------------------------------------------- 1 | import type { BangumiFollowDto } from '@/dto/video.dto'; 2 | import { addBangumi, cancelBangumi } from '@/net/video.request'; 3 | import { apiDelay, logger } from '@/utils'; 4 | 5 | async function followBangumi( 6 | reqFunc: (season_id: number | string) => Promise, 7 | season_id: number | string, 8 | name: string, 9 | ) { 10 | try { 11 | const { code, message, result } = await reqFunc(season_id); 12 | if (code !== 0) { 13 | logger.warn(`${name}失败:${code} ${message}`); 14 | return; 15 | } 16 | return result; 17 | } catch (error) { 18 | logger.error(error); 19 | } 20 | } 21 | 22 | /** 23 | * 追番 24 | */ 25 | export function addBangumiFollow(season_id: number | string) { 26 | return followBangumi(addBangumi, season_id, '追番'); 27 | } 28 | 29 | /** 30 | * 取消追番 31 | */ 32 | export function delBangumiFollow(season_id: number | string) { 33 | return followBangumi(cancelBangumi, season_id, '取消追番'); 34 | } 35 | 36 | /** 37 | * 追番然后取消追番 38 | */ 39 | export async function addAndDelBangumiFollow(season_id: number | string) { 40 | await addBangumiFollow(season_id); 41 | await apiDelay(); 42 | await delBangumiFollow(season_id); 43 | } 44 | -------------------------------------------------------------------------------- /src/service/daily-battery.service.ts: -------------------------------------------------------------------------------- 1 | import * as net from '@/net/daily-battery.request'; 2 | import { apiDelay, getRandomItem, logger } from '@/utils'; 3 | import { sendDmMessage } from './dm.service'; 4 | 5 | /** 6 | * 获取任务进度 7 | */ 8 | async function getTaskStatus() { 9 | try { 10 | const { code, message, data } = await net.getUserTaskProgress(); 11 | if (code !== 0) { 12 | logger.warn(`获取任务进度失败:${code}-${message}`); 13 | return -1; 14 | } 15 | if (data.is_surplus === -1 || data.target === 0) { 16 | logger.info('账号无法完成该任务,故跳过'); 17 | return -2; 18 | } 19 | const { status, progress } = data; 20 | if (status === 0 || status === 1) { 21 | logger.debug(`任务进度:${progress}`); 22 | return progress + 10; 23 | } 24 | return status; 25 | } catch (error) { 26 | logger.error('获取任务进度异常', error); 27 | return -1; 28 | } 29 | } 30 | 31 | /** 32 | * 领取任务奖励 33 | */ 34 | async function receiveTaskReward() { 35 | try { 36 | const { code, message } = await net.receiveTaskReward(); 37 | if (code !== 0) { 38 | logger.warn(`领取任务奖励失败:${code}-${message}`); 39 | return false; 40 | } 41 | logger.info('领取任务奖励成功'); 42 | return true; 43 | } catch (error) { 44 | logger.error('领取任务奖励异常', error); 45 | return false; 46 | } 47 | } 48 | 49 | /** 50 | * 每日任务 51 | */ 52 | async function dailyBattery() { 53 | const status = await getTaskStatus(); 54 | switch (status) { 55 | case -2: { 56 | return true; 57 | } 58 | case -1: { 59 | return false; 60 | } 61 | case 2: { 62 | // 领取任务奖励 63 | return await receiveTaskReward(); 64 | } 65 | case 3: { 66 | logger.info('任务已完成'); 67 | return true; 68 | } 69 | default: { 70 | // lol 的直播间发一条弹幕 71 | const rooms = [21144080, 7734200, 46936]; 72 | logger.debug(`发送弹幕 ${status - 10}`); 73 | for (let index = 0; index < 15 - status; index++) { 74 | await sendDmMessage(getRandomItem(rooms), 'bili官方'); 75 | await apiDelay(10000, 15000); 76 | } 77 | return false; 78 | } 79 | } 80 | } 81 | 82 | export async function dailyBatteryService() { 83 | for (let index = 0; index < 3; index++) { 84 | const result = await dailyBattery(); 85 | if (result) return; 86 | await apiDelay(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/service/dm.service.ts: -------------------------------------------------------------------------------- 1 | import { kaomoji } from '@/constant'; 2 | import { SeedMessageResult } from '@/enums/intimacy.emum'; 3 | import { logger, random } from '@/utils'; 4 | import * as liveRequest from '../net/live.request'; 5 | 6 | const messageArray = kaomoji.concat('1', '2', '3', '4', '5', '6', '7', '8', '9', '签到', '哈哈'); 7 | 8 | export async function sendDmMessage(roomid: number, nickName: string) { 9 | const msg = messageArray[random(messageArray.length - 1)]; 10 | try { 11 | const { code, message } = await liveRequest.sendMessage(roomid, msg); 12 | 13 | if (code === SeedMessageResult.Success) { 14 | return 0; 15 | } 16 | if (code === SeedMessageResult.Unresistant) { 17 | logger.warn(`【${nickName}】${roomid}-可能未开启评论`); 18 | return SeedMessageResult.Unresistant; 19 | } 20 | logger.warn(`【${nickName}】${roomid}-发送失败 ${code} ${message}`); 21 | return code; 22 | } catch (error) { 23 | logger.verbose(`发送弹幕异常 ${error.message}`); 24 | } 25 | return SeedMessageResult.Unknown; 26 | } 27 | -------------------------------------------------------------------------------- /src/service/live.service.ts: -------------------------------------------------------------------------------- 1 | import type { LiveRoomList } from '@/dto/live.dto'; 2 | import { PendentID } from '@/enums/live-lottery.enum'; 3 | import { getArea, getLiveRoom } from '@/net/live.request'; 4 | import { sleep, logger } from '@/utils'; 5 | 6 | interface LiveAreaType { 7 | areaId: string; 8 | parentId: string; 9 | } 10 | 11 | /** 12 | * 分类检测 13 | */ 14 | function pendentLottery(list: LiveRoomList[]) { 15 | const lotteryTime: LiveRoomList[] = [], 16 | lotteryPacket: LiveRoomList[] = []; 17 | list.forEach(item => { 18 | const num2 = item.pendant_info['2']; 19 | if (!num2) { 20 | return; 21 | } 22 | if (num2.pendent_id === PendentID.Time) { 23 | lotteryTime.push(item); 24 | } else if (num2.pendent_id === PendentID.RedPacket) { 25 | lotteryPacket.push(item); 26 | } 27 | }); 28 | return { lotteryTime, lotteryPacket }; 29 | } 30 | 31 | /** 32 | * 获取直播分区 33 | * @description 之所以是二维数组,是为了方便后面的递归,如果全部数据整合到一个数组中,会导致数据量过大,天选超时了可能都没请求完 34 | */ 35 | export async function getLiveArea(): Promise { 36 | try { 37 | const { data, code, message } = await getArea(); 38 | if (code !== 0) { 39 | logger.info(`获取直播分区失败: ${code}-${message}`); 40 | } 41 | return data.data 42 | .map(item => item.list) 43 | .map(item => item.map(area => ({ areaId: area.id, parentId: area.parent_id }))); 44 | } catch (error) { 45 | logger.error(`获取直播分区异常:`, error); 46 | throw error; 47 | } 48 | } 49 | 50 | /** 51 | * 获取直播间列表 52 | * @param areaId 53 | * @param parentId 54 | * @param page 55 | */ 56 | export async function getLotteryRoomList( 57 | areaId: string, 58 | parentId: string, 59 | page = 1, 60 | lotType: 'lottery' | 'redPack' = 'lottery', 61 | ): Promise { 62 | try { 63 | await sleep(100); 64 | const { data, code, message } = await getLiveRoom(parentId, areaId, page); 65 | if (code !== 0) { 66 | logger.warn(`获取直播间列表失败: ${code} ${message}`); 67 | throw new Error(`获取直播间列表失败: ${code} ${message}`); 68 | } 69 | return pendentLottery(data.list)[lotType === 'lottery' ? 'lotteryTime' : 'lotteryPacket']; 70 | } catch (error) { 71 | logger.error(`获取直播间列表异常:`, error); 72 | throw error; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/service/nav.service.ts: -------------------------------------------------------------------------------- 1 | import type { UserInfoNavDto } from '../dto/user-info.dto'; 2 | import { TaskModule } from '../config/globalVar'; 3 | import { loginByCookie } from '../net/user-info.request'; 4 | import { logger } from '../utils/log'; 5 | 6 | type UserNav = UserInfoNavDto['data']; 7 | 8 | function getBCoinBalance(data: UserNav) { 9 | TaskModule.couponBalance = data.wallet?.coupon_balance || 0; 10 | } 11 | 12 | export async function updateNav() { 13 | try { 14 | const { data, message, code } = await loginByCookie(); 15 | if (code !== 0) { 16 | logger.warn(`获取用户信息失败:${code} ${message}`); 17 | return; 18 | } 19 | getBCoinBalance(data); 20 | } catch (error) { 21 | logger.error(`获取用户信息异常:${error.message}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/service/reservation.service.ts: -------------------------------------------------------------------------------- 1 | import type { Reservation } from '@/dto/reservation.dto'; 2 | import { reservation, reserveAttachCardButton } from '@/net/reservation.request'; 3 | import { getUnixTime, logger, sleep } from '@/utils'; 4 | 5 | /** 6 | * 请求指定用户的预约列表 7 | */ 8 | async function fetchReservation(vmid: string) { 9 | const { code, message, data } = await reservation(vmid); 10 | if (code !== 0) { 11 | logger.warn(`获取预约列表失败:${code} ${message}`); 12 | return; 13 | } 14 | return data?.filter(res => filterLottery(res)); 15 | } 16 | 17 | /** 18 | * 过滤出符合要求的预约 19 | */ 20 | function filterLottery(data: Reservation) { 21 | // 参与过了? 22 | if (data.reserve_record_ctime) { 23 | return false; 24 | } 25 | // 已经开奖了 26 | if (getUnixTime() > data.live_plan_start_time) { 27 | return false; 28 | } 29 | // 其它 30 | if (Reflect.has(data, 'lottery_prize_info') && Reflect.has(data, 'lottery_type')) { 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | /** 37 | * 进行一个预约 38 | */ 39 | async function reserveLive(res: Reservation) { 40 | logger.debug(`预约直播:${res.name}(${res.sid}/${res.up_mid})`); 41 | logger.debug(`奖励列表:${res.lottery_prize_info.text}`); 42 | logger.debug(`活动链接:${res.lottery_prize_info.jump_url}`); 43 | logger.debug(`开奖时间:${new Date(res.live_plan_start_time * 1000).toLocaleString('zh-CN')}`); 44 | 45 | const { code, message, data } = await reserveAttachCardButton(res.sid, res.total, 1); 46 | if (code !== 0) { 47 | logger.warn(`预约直播${res.sid}失败:${code} ${message}`); 48 | return false; 49 | } 50 | logger.debug(`预约成功:${data.toast},${data.desc_update}`); 51 | return true; 52 | } 53 | 54 | export async function reservationService() { 55 | // TODO: demo 56 | const demo = []; 57 | for (const d of demo) { 58 | const list = await fetchReservation(d); 59 | await sleep(1000); 60 | if (!list || list.length < 1) { 61 | continue; 62 | } 63 | // 一个人不至于有几个直播预约吧 64 | await Promise.all(list.map(res => reserveLive(res))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/service/reward.service.ts: -------------------------------------------------------------------------------- 1 | import { getDailyTaskRewardInfo } from '../net/user-info.request'; 2 | import { TaskConfig, TaskModule } from '../config/globalVar'; 3 | import { logger } from '../utils/log'; 4 | import { getTodayCoinNum } from '@/service/coin.service'; 5 | 6 | /** 7 | * 投币检测 8 | */ 9 | export async function checkCoin() { 10 | const coinNum = await getTodayCoinNum(); 11 | /** 剩余硬币数量 */ 12 | const targetCoinsDiff = TaskModule.money - TaskConfig.coin.stayCoins; 13 | let coins = 0; 14 | if (TaskModule.coinsTask === 0) { 15 | // 根据经验设置的目标 16 | logger.info(`今日投币数量:${coinNum},还需投币0颗,经验够了,不想投了`); 17 | } else if (targetCoinsDiff <= 0) { 18 | // 剩余硬币比需要保留的少 19 | logger.info(`今日投币数量:${coinNum},还需投币0颗,硬币不够了,不投币了`); 20 | } else if (targetCoinsDiff < TaskModule.coinsTask) { 21 | coins = targetCoinsDiff; 22 | logger.info( 23 | `投币数量: ${coinNum},还能投币数量: ${targetCoinsDiff}颗;(目标${TaskModule.coinsTask}颗,忽略部分投币)`, 24 | ); 25 | } else { 26 | coins = TaskModule.coinsTask - coinNum; 27 | logger.info(`投币数量:${coinNum},还需投币数量: ${coins}颗;(目标${TaskModule.coinsTask}颗)`); 28 | } 29 | TaskModule.coinsTask = coins; 30 | } 31 | 32 | /** 33 | * 播放和分享检测 34 | */ 35 | export async function checkShareAndWatch() { 36 | try { 37 | const { data, message, code } = await getDailyTaskRewardInfo(); 38 | if (code != 0) { 39 | logger.warn(`状态获取失败: ${code} ${message}`); 40 | return; 41 | } 42 | const { share, watch } = data; 43 | share && logger.info(`分享任务已完成`); 44 | watch && logger.info(`观看任务已完成`); 45 | 46 | TaskModule.share = share; 47 | TaskModule.watch = watch; 48 | } catch (error) { 49 | logger.error(`每日分享/播放检测出现异常: ${error.message}`); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/store/red-packet.ts: -------------------------------------------------------------------------------- 1 | export const noWinRef = { 2 | value: 0, 3 | }; 4 | 5 | export const realRisk = { 6 | value: false, 7 | }; 8 | -------------------------------------------------------------------------------- /src/task/LiveReservation.ts: -------------------------------------------------------------------------------- 1 | import { reservationService } from '@/service/reservation.service'; 2 | import { logger } from '@/utils/log'; 3 | 4 | export default async function LiveReservation() { 5 | logger.info('----【直播预约】----'); 6 | try { 7 | await reservationService(); 8 | } catch (error) { 9 | logger.error(`【直播预约异常】`, error); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/task/activityLottery.ts: -------------------------------------------------------------------------------- 1 | import { activityLotteryService } from '@/service/activity-lottery.service'; 2 | import { logger } from '@/utils'; 3 | 4 | export default async function activityLottery() { 5 | logger.info('----【转盘抽奖】----'); 6 | try { 7 | await activityLotteryService(); 8 | logger.info('转盘抽奖完成'); 9 | } catch (error) { 10 | logger.error(`转盘抽奖: ${error.message}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/task/batchUnfollow.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { unFollowTag } from '@/service/tags.service'; 3 | import { logger } from '@/utils'; 4 | 5 | export default async function batchUnfollow() { 6 | logger.info('----【批量取关】----'); 7 | const { tags, totalNum } = TaskConfig.unFollow; 8 | let restNum = totalNum; 9 | try { 10 | for (const tag of tags) { 11 | restNum = await unFollowTag(tag, restNum); 12 | } 13 | logger.info('批量取关完成'); 14 | } catch (error) { 15 | logger.error(`批量取关: ${error.message}`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/task/beforeTask.ts: -------------------------------------------------------------------------------- 1 | import getNewCookie from './getNewCookie'; 2 | 3 | export default async function beforeTask() { 4 | await getNewCookie(); 5 | } 6 | -------------------------------------------------------------------------------- /src/task/bigPoint.ts: -------------------------------------------------------------------------------- 1 | import { TaskModule } from '@/config/globalVar'; 2 | import { bigPointService } from '@/service/big-point.service'; 3 | import { logger } from '@/utils/log'; 4 | 5 | export default async function bigPoint() { 6 | logger.info('----【大会员积分】----'); 7 | try { 8 | if (TaskModule.vipType === 0 || TaskModule.vipStatus === 0) { 9 | logger.info('当前不是大会员,跳过任务'); 10 | return; 11 | } 12 | await bigPointService(); 13 | } catch (error) { 14 | logger.error(`大会员积分任务异常: ${error.message}`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/task/dailyBattery.ts: -------------------------------------------------------------------------------- 1 | import { dailyBatteryService } from '@/service/daily-battery.service'; 2 | import { logger } from '@/utils/log'; 3 | 4 | export default async function dailyBattery() { 5 | logger.info('----【获取每日电池】----'); 6 | try { 7 | await dailyBatteryService(); 8 | } catch (error) { 9 | logger.error(`获取每日电池出现异常`, error); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/task/dailyTask.ts: -------------------------------------------------------------------------------- 1 | import noLoginTask from './noLoginTask'; 2 | 3 | export async function dailyTasks( 4 | cb?: (...arg: T[]) => Promise, 5 | ...cbArg: T[] 6 | ) { 7 | const { getBiliTasks } = await import('./index'); 8 | const { apiDelay, logger, Logger } = await import('../utils'); 9 | const { getWaitRuningFunc } = await import('../config/configOffFun'); 10 | const { printVersion } = await import('@/utils/version'); 11 | await Logger.init(); 12 | await printVersion(); 13 | try { 14 | const { beforeTask, loginTask } = await getBiliTasks(['beforeTask', 'loginTask']); 15 | await beforeTask(); 16 | await loginTask(); 17 | } catch (error) { 18 | logger.error(`登录失败:`, error); 19 | await noLoginTask(); 20 | await Logger.push('【登录失败】'); 21 | return '未完成'; 22 | } 23 | const biliArr = getWaitRuningFunc(); 24 | // 我是吐了,为了解决一个问题引用了黑魔法,导致处处黑魔法 25 | for await (const asyncFun of biliArr) { 26 | try { 27 | await asyncFun(); 28 | } catch (error) { 29 | logger.error(`${asyncFun.name}失败: ${error}`); 30 | } finally { 31 | await apiDelay(); 32 | } 33 | } 34 | 35 | cb && (await cb(...cbArg)); 36 | 37 | await Logger.push('每日完成'); 38 | return '完成'; 39 | } 40 | -------------------------------------------------------------------------------- /src/task/exchangeCoupon.ts: -------------------------------------------------------------------------------- 1 | import { exchangeCouponService } from '@/service/manga.service'; 2 | import { logger } from '../utils/log'; 3 | 4 | export default async function exchangeCoupon() { 5 | logger.info('----【漫画兑换任务】----'); 6 | try { 7 | // 兑换漫读券 8 | await exchangeCouponService(); 9 | } catch (error) { 10 | logger.error(`漫画兑换任务异常: ${error}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/task/getNewCookie.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { MS2DATE } from '@/constant'; 3 | import * as auth from '@/service/auth.service'; 4 | import { logger } from '@/utils'; 5 | import { readJsonFile, writeJsonFile } from '@/utils/file'; 6 | import { dirname, resolve } from 'path'; 7 | 8 | export default async function getNewCookie() { 9 | const day = TaskConfig.createCookieDay; 10 | if (!day || day < 1) { 11 | return false; 12 | } 13 | const btJonPath = getBtJonPath(); 14 | if (!btJonPath) { 15 | return false; 16 | } 17 | const btJob = readJsonFile(btJonPath); 18 | if (!isNeedCreateCookie(btJob?.lastNewCookie, day)) { 19 | return; 20 | } 21 | const newCookie = await auth.getNewCookie(TaskConfig.cookie); 22 | if (!newCookie) { 23 | return; 24 | } 25 | TaskConfig.cookie = newCookie; 26 | logger.debug('cookie 使用新 cookie'); 27 | writeJsonFile(btJonPath, { 28 | lastNewCookie: Date.now(), 29 | }); 30 | } 31 | 32 | /** 33 | * 获取 bt_job.json 路径 34 | */ 35 | function getBtJonPath() { 36 | if (!process.env.__BT_CONFIG_PATH__) { 37 | return undefined; 38 | } 39 | const configDir = dirname(process.env.__BT_CONFIG_PATH__); 40 | return resolve(configDir, 'bt_jobs.json'); 41 | } 42 | 43 | /** 44 | * 判断是否需要创建新 cookie 45 | * @param timestamp 上次运行时间 46 | * @param day 间隔天数 47 | */ 48 | function isNeedCreateCookie(timestamp: number, day: number) { 49 | if (!timestamp) { 50 | return true; 51 | } 52 | const dayDiff = (Date.now() - timestamp) / MS2DATE; 53 | return dayDiff > day; 54 | } 55 | -------------------------------------------------------------------------------- /src/task/getVipPrivilege.ts: -------------------------------------------------------------------------------- 1 | import { receiveVipMy, receiveVipPrivilege } from '../net/vip.request'; 2 | import { TaskModule } from '../config/globalVar'; 3 | import { logger } from '../utils/log'; 4 | import { apiDelay } from '@/utils'; 5 | 6 | /** 7 | * 获取当前领取状态 8 | */ 9 | async function getPrivilegeStatus() { 10 | try { 11 | const { data, code, message } = await receiveVipMy(); 12 | if (code !== 0) { 13 | logger.info(`获取领取状态失败:${code} ${message}`); 14 | return; 15 | } 16 | const { list } = data; 17 | const stateList = list.filter(item => item.state === 0 && [1, 3, 5].includes(item.type)); 18 | if (stateList.length === 0) { 19 | return; 20 | } 21 | return stateList; 22 | // 查找未领取的权益 23 | } catch (error) { 24 | logger.error(`获取领取状态出现异常:${error.message}`); 25 | } 26 | } 27 | 28 | function getPrivilegeName(type: number): string { 29 | if (type > 5 || type < 1) return `未知权益 ${type}`; 30 | return ['B 币券', '会员购优惠券', '漫画福利券', '会员购包邮券', '漫画商城优惠券'][type - 1]; 31 | } 32 | 33 | async function getOnePrivilege(type: number): Promise { 34 | try { 35 | const name = getPrivilegeName(type); 36 | const { code, message } = await receiveVipPrivilege(type); 37 | 38 | if (code === 73319) { 39 | logger.error(`${name}领取失败,需要手机验证(可能异地登陆),跳过`); 40 | return true; 41 | } 42 | 43 | if (code !== 0) { 44 | logger.info(`领取${name}失败:${code} ${message}`); 45 | } 46 | logger.info(`领取${name}成功!`); 47 | return true; 48 | } catch (error) { 49 | logger.error(`领取权益出现异常:`, error); 50 | } 51 | return false; 52 | } 53 | 54 | async function getPrivilege(type: number) { 55 | let errCount = 0, 56 | suc = false; 57 | 58 | while (!suc) { 59 | suc = await getOnePrivilege(type); 60 | if (errCount > 2) { 61 | break; 62 | } 63 | errCount++; 64 | } 65 | 66 | return suc; 67 | } 68 | 69 | export default async function getVipPrivilege() { 70 | try { 71 | logger.info('----【领取大会员权益】----'); 72 | if (TaskModule.vipStatus === 0 || TaskModule.vipType === 0) { 73 | logger.info('您还不是大会员,无法领取权益'); 74 | return; 75 | } 76 | 77 | const privilegeList = await getPrivilegeStatus(); 78 | 79 | if (!privilegeList || privilegeList.length === 0) { 80 | logger.info('暂无可领取权益(除保留)'); 81 | return; 82 | } 83 | 84 | for (let index = 0; index < privilegeList.length; index++) { 85 | await apiDelay(100); 86 | const privilege = privilegeList[index]; 87 | await getPrivilege(privilege.type); 88 | } 89 | } catch (error) { 90 | logger.error(`领取大会员权益出现异常:`, error); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/task/index.ts: -------------------------------------------------------------------------------- 1 | export const biliTaskArray = [ 2 | ['beforeTask', () => import('./beforeTask')], 3 | ['loginTask', () => import('./loginTask')], 4 | ['exchangeCoupon', () => import('./exchangeCoupon')], 5 | ['liveSignTask', () => import('./liveSignTask')], 6 | ['addCoins', () => import('./addCoins')], 7 | ['bigPoint', () => import('./bigPoint')], 8 | ['shareAndWatch', () => import('./shareAndWatch')], 9 | ['silver2Coin', () => import('./silver2Coin')], 10 | ['mangaTask', () => import('./mangaTask')], 11 | ['supGroupSign', () => import('./supGroupSign')], 12 | ['getVipPrivilege', () => import('./getVipPrivilege')], 13 | ['useCouponBp', () => import('./useCouponBp')], 14 | ['matchGame', () => import('./matchGame')], 15 | ['giveGift', () => import('./giveGift')], 16 | ['liveIntimacy', () => import('./liveIntimacy')], 17 | ['batchUnfollow', () => import('./batchUnfollow')], 18 | ['liveLottery', () => import('./liveLottery')], 19 | ['liveRedPack', () => import('./liveRedPack')], 20 | ['dailyBattery', () => import('./dailyBattery')], 21 | ['activityLottery', () => import('./activityLottery')], 22 | ['LiveReservation', () => import('./LiveReservation')], 23 | ['judgement', () => import('./judgement')], 24 | ] as const; 25 | 26 | export type BiliTaskName = typeof biliTaskArray[number][0]; 27 | export type TaskFunc = () => Promise<{ default: () => Promise }>; 28 | 29 | export const biliTasks = new Map(biliTaskArray); 30 | 31 | export default biliTasks; 32 | 33 | export async function getBiliTask(funcName: BiliTaskName) { 34 | const biliTask = biliTasks.get(funcName); 35 | if (!biliTask) { 36 | return () => Promise.resolve(0); 37 | } 38 | return (await biliTask()).default; 39 | } 40 | 41 | export async function getBiliTasks( 42 | funcNames: T[], 43 | ): Promise> { 44 | const tasks: Record = {} as any; 45 | for (const funcName of funcNames) { 46 | const biliTask = biliTasks.get(funcName); 47 | if (!biliTask) continue; 48 | tasks[funcName] = (await biliTask()).default; 49 | } 50 | return tasks; 51 | } 52 | 53 | /** 54 | * 获取用户输入的任务 55 | */ 56 | export function getInputBiliTask(taskNameStr: string) { 57 | const taskNameArr = taskNameStr.split(','); 58 | const taskArr: Defined>[] = [biliTaskArray[0][1]]; 59 | taskNameArr.forEach(name => { 60 | const task = biliTasks.get(name); 61 | if (task) { 62 | taskArr.push(task); 63 | } 64 | }); 65 | return taskArr.map(async func => (await func()).default); 66 | } 67 | 68 | /** 69 | * 运行用户输入的任务 70 | */ 71 | export async function runInputBiliTask(taskNameStr: string) { 72 | const { logger, Logger, clearLogs } = await import('../utils/log'); 73 | await Logger.init(); 74 | logger.info(`开始执行自定义任务!`); 75 | const taskArr = getInputBiliTask(taskNameStr); 76 | for await (const task of taskArr) { 77 | await task(); 78 | } 79 | // 在任务完成后再加载版本 80 | logger.info(`----【版本信息】----`); 81 | const { printVersion } = await import('../utils/version'); 82 | await printVersion(); 83 | if (taskNameStr.includes('noPush')) { 84 | logger.info(`已设置不推送通知`); 85 | } else { 86 | await Logger.push('自定义任务完成'); 87 | } 88 | clearLogs(); 89 | } 90 | -------------------------------------------------------------------------------- /src/task/judgement.ts: -------------------------------------------------------------------------------- 1 | import { juryService } from '@/service/jury.service'; 2 | import { logger } from '@/utils'; 3 | 4 | export default async function judgement() { 5 | logger.info('----【风纪任务】----'); 6 | try { 7 | await juryService(); 8 | } catch (error) { 9 | logger.error(`风纪任务未完成 ×: `, error); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/task/liveIntimacy.ts: -------------------------------------------------------------------------------- 1 | import { liveIntimacyService } from '@/service/intimacy.service'; 2 | import { logger } from '@/utils'; 3 | 4 | export default async function liveIntimacy() { 5 | logger.info('----【直播亲密度】----'); 6 | try { 7 | await liveIntimacyService(); 8 | } catch (error) { 9 | logger.error(`直播亲密度: ${error.message}`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/task/liveLottery.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/utils'; 2 | import { liveFollowLotteryService, liveLotteryService } from '@/service/live-lottery.service'; 3 | import { getLastFollow, handleFollowUps } from '@/service/tags.service'; 4 | import { TaskConfig } from '@/config/globalVar'; 5 | import { printLiveUserSession } from '@/service/session.service'; 6 | 7 | export default async function liveLottery() { 8 | logger.info('----【天选时刻】----'); 9 | const isGo = await liveFollowLotteryService(); 10 | if (!isGo) return isGo; 11 | try { 12 | const { moveTag, actFollowMsg, mayBeWinMsg } = TaskConfig.lottery; 13 | // 获取最后一个关注的UP 14 | const lastFollow = await getLastFollow(); 15 | logger.verbose(`最后一个关注的UP: ${lastFollow?.uname}`); 16 | const newFollowUps = await liveLotteryService(); 17 | logger.verbose('扫描完成'); 18 | await handleFollowUps(newFollowUps, lastFollow, moveTag, actFollowMsg); 19 | // 打印会话 20 | if (mayBeWinMsg) { 21 | await printLiveUserSession(); 22 | } 23 | } catch (error) { 24 | logger.warn(`天选时刻异常: ${error.message}`); 25 | logger.debug(error); 26 | } 27 | logger.info('结束天选时刻'); 28 | } 29 | 30 | export { liveLottery }; 31 | -------------------------------------------------------------------------------- /src/task/liveRedPack.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/utils'; 2 | import { liveRedPackService } from '@/service/red-pack.service'; 3 | import { getLastFollow, handleFollowUps } from '@/service/tags.service'; 4 | import { TaskConfig } from '@/config/globalVar'; 5 | 6 | export default async function liveRedPack() { 7 | logger.info('----【天选红包】----'); 8 | try { 9 | const { moveTag } = TaskConfig.redPack; 10 | // 获取最后一个关注的UP 11 | const lastFollow = await getLastFollow(); 12 | lastFollow && logger.verbose(`最后一个关注的UP: ${lastFollow.uname}`); 13 | const newFollowUps = await liveRedPackService(); 14 | await handleFollowUps(newFollowUps, lastFollow, moveTag); 15 | } catch (error) { 16 | logger.warn(`天选时刻异常: ${error.message}`); 17 | logger.debug(error); 18 | } 19 | logger.info('结束天选红包'); 20 | } 21 | 22 | export { liveRedPack }; 23 | -------------------------------------------------------------------------------- /src/task/liveSignTask.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/log'; 2 | import { doLiveSign, webGetSignInfo } from '../net/live.request'; 3 | 4 | export default async function liveSignTask() { 5 | logger.info('----【直播签到】----'); 6 | try { 7 | const { data } = await webGetSignInfo(); 8 | if (data.status === 1) { 9 | logger.info('已签到,跳过签到'); 10 | logger.info(`已经签到${data.hadSignDays}天,${data.specialText}`); 11 | return; 12 | } 13 | } catch (error) { 14 | logger.debug(`直播签到,${error.message}`); 15 | } 16 | try { 17 | const { code, data, message } = await doLiveSign(); 18 | if (code === 0) { 19 | logger.info( 20 | `直播签到成功: ${data.text},特别信息: ${data.specialText},本月签到天数: ${data.hadSignDays}天;`, 21 | ); 22 | } else { 23 | logger.warn(`直播签到失败: ${code} ${message}`); 24 | } 25 | } catch (error) { 26 | logger.error(`直播签到异常: ${error.message}`); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/task/loginTask.ts: -------------------------------------------------------------------------------- 1 | import { loginByCookie, getCoinBalance } from '../net/user-info.request'; 2 | import { TaskConfig, TaskModule } from '../config/globalVar'; 3 | import { apiDelay } from '../utils'; 4 | import { UserInfoNavDto } from '../dto/user-info.dto'; 5 | import { logger } from '../utils/log'; 6 | import { request } from '@/utils/request'; 7 | 8 | type UserNavData = UserInfoNavDto['data']; 9 | 10 | function estimatedDays(upLevelExp: number): number { 11 | const { targetCoins } = TaskConfig.coin; 12 | if (targetCoins <= 0) return upLevelExp / 15; 13 | const dailyExp = targetCoins * 10 + 15; 14 | const idealDays = upLevelExp / dailyExp; 15 | const coinSupportDays = TaskModule.money / (targetCoins - 1); 16 | if (idealDays < coinSupportDays) return Math.floor(idealDays); 17 | const needExp = upLevelExp - coinSupportDays * dailyExp; 18 | return needExp / 25 + coinSupportDays; 19 | } 20 | 21 | function setLevelInfo(data: UserNavData) { 22 | /** 等级相关信息 */ 23 | const levelInfo = data.level_info; 24 | const currentLevel = levelInfo.current_level; 25 | // 判断当前等级是否还需要投币 26 | if (currentLevel >= TaskConfig.coin.targetLevel && TaskConfig.limit.level6) { 27 | TaskModule.coinsTask = 0; 28 | } 29 | logger.info(`当前等级: ${levelInfo.current_level}`); 30 | if (currentLevel < 6) { 31 | const upLevelExp = levelInfo.next_exp - levelInfo.current_exp; 32 | // 实际天数肯定会少一些 33 | logger.info(`距升级还需 ${upLevelExp} 经验,预计 ${estimatedDays(upLevelExp).toFixed(2)} 天`); 34 | return; 35 | } 36 | if (TaskConfig.limit.level6) { 37 | logger.info('已经满级(关闭部分功能)'); 38 | const funcs = TaskConfig.function; 39 | funcs.shareAndWatch = false; 40 | funcs.addCoins = false; 41 | } else { 42 | logger.info('已经满级,但要求继续(投币,分享等)'); 43 | } 44 | } 45 | 46 | export function setVipStatus(data: { vipType: number; vipStatus: number }) { 47 | /** 大会员信息 */ 48 | let vipTypeMsg = ''; 49 | 50 | TaskModule.vipType = data.vipType; 51 | TaskModule.vipStatus = data.vipStatus; 52 | 53 | switch (data.vipType) { 54 | case 0: 55 | vipTypeMsg = '无大会员'; 56 | break; 57 | case 1: 58 | vipTypeMsg = '月度大会员'; 59 | break; 60 | case 2: 61 | vipTypeMsg = '年度大会员'; 62 | break; 63 | default: 64 | break; 65 | } 66 | 67 | // 判断是否过期,因为即使大会员过期,下面也会显示 68 | if (data.vipStatus === 0) { 69 | vipTypeMsg = vipTypeMsg === '无大会员' ? vipTypeMsg : vipTypeMsg + '[已过期]'; 70 | } 71 | 72 | logger.info(`大会员状态: ${vipTypeMsg}`); 73 | } 74 | 75 | async function setUserInfo(data: UserNavData) { 76 | try { 77 | const { money } = await request(getCoinBalance); // 获取更精准的硬币数量 78 | 79 | logger.info(`登录成功: ${data.uname}`); 80 | logger.info(`硬币余额: ${money || 0}`); 81 | TaskModule.nickname = data.uname; 82 | TaskModule.money = money || 0; 83 | TaskModule.userLevel = data.level_info.current_level; 84 | TaskModule.couponBalance = data.wallet?.coupon_balance || 0; 85 | 86 | setLevelInfo(data); 87 | setVipStatus(data); 88 | } catch (error) { 89 | logger.error(`获取硬币信息异常: ${error.message}`); 90 | logger.debug(error); 91 | } 92 | } 93 | 94 | export default async function loginTask() { 95 | logger.info('----【登录】----'); 96 | const { data, message, code } = await loginByCookie(); 97 | if (code !== 0) { 98 | logger.error(`[${TaskConfig.USERID}]登录错误 ${code} ${message}`); 99 | throw new Error(message); 100 | } 101 | if (!data.isLogin) { 102 | throw new Error('接口返回为未登录'); 103 | } 104 | await apiDelay(); 105 | await setUserInfo(data); 106 | } 107 | -------------------------------------------------------------------------------- /src/task/mangaTask.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mangaSign, 3 | buyMangaService, 4 | takeSeasonGift, 5 | shareComicService, 6 | readMangaService, 7 | guessGameService, 8 | } from '@/service/manga.service'; 9 | import { logger } from '../utils/log'; 10 | 11 | export default async function mangaTask() { 12 | logger.info('----【漫画任务】----'); 13 | try { 14 | // 漫画签到 15 | await mangaSign(); 16 | // 每日分享 17 | await shareComicService(); 18 | // 每日阅读 19 | await readMangaService(); 20 | // 每日游戏 21 | await guessGameService(); 22 | // 领取任务奖励 23 | await takeSeasonGift(); 24 | // 购买漫画 25 | await buyMangaService(); 26 | } catch (error) { 27 | logger.error(`漫画任务异常:`, error); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/task/matchGame.ts: -------------------------------------------------------------------------------- 1 | import type { GuessCollectionDto, GuessDetails } from '../dto/match-game.dto'; 2 | import { TaskConfig, TaskModule } from '../config/globalVar'; 3 | import { getGuessCollection, guessAdd } from '../net/match-game.request'; 4 | import { apiDelay } from '../utils'; 5 | import { logger } from '../utils/log'; 6 | 7 | export default async function matchGame() { 8 | logger.info('----【赛事硬币竞猜】----'); 9 | const { match } = TaskConfig; 10 | 11 | if (match.coins <= 0) { 12 | logger.info('硬币数量不能小于 0'); 13 | return; 14 | } 15 | 16 | if (isLackOfCoin()) { 17 | return; 18 | } 19 | 20 | const list = await getOneGuessCollection(); 21 | await apiDelay(); 22 | 23 | if (!list) { 24 | return; 25 | } 26 | 27 | const count = await guessOne(filterList(list, match.diff)); 28 | logger.info(`【竞猜结束】一共参与${count}次预测`); 29 | } 30 | 31 | /** 32 | * 过滤掉差值小于 n 的赛事 33 | */ 34 | function filterList(list: GuessCollectionDto['data']['list'], n: number) { 35 | return list.filter(item => { 36 | const { questions } = item; 37 | const [{ details, is_guess }] = questions; 38 | if (is_guess) { 39 | return false; 40 | } 41 | const [team1, team2] = details; 42 | const diff = Math.abs(team1.odds - team2.odds); 43 | return diff >= n; 44 | }); 45 | } 46 | 47 | async function getOneGuessCollection() { 48 | try { 49 | const { 50 | code, 51 | message, 52 | data: { list, page }, 53 | } = await getGuessCollection(); 54 | 55 | if (code !== 0) { 56 | logger.warn(`获取赛事错误 ${code} ${message}`); 57 | return; 58 | } 59 | 60 | if (page.total === 0) { 61 | logger.info('今日已经无法获取赛事'); 62 | return null; 63 | } 64 | 65 | return list; 66 | } catch (error) { 67 | logger.error(`赛事竞猜异常:`, error); 68 | } 69 | } 70 | 71 | async function guessOne(list: GuessCollectionDto['data']['list']) { 72 | let count = 0; 73 | try { 74 | for (const { contest, questions } of list) { 75 | const [{ id: questionsId, title, details, is_guess }] = questions; 76 | 77 | if (isLackOfCoin()) return count; 78 | 79 | if (is_guess) continue; 80 | 81 | await apiDelay(); 82 | const coins = TaskConfig.match.coins; 83 | const { code, message } = await guessAdd( 84 | contest.id, 85 | questionsId, 86 | selectOdd(title, details), 87 | coins, 88 | ); 89 | if (code !== 0) { 90 | logger.warn(`预测失败:${code} ${message}`); 91 | continue; 92 | } 93 | count++; 94 | TaskModule.money -= coins; 95 | } 96 | } catch (error) { 97 | logger.error('赛事硬币竞猜出错了', error); 98 | } 99 | return count; 100 | } 101 | 102 | function selectOdd(title: string, [team1, team2]: GuessDetails[]) { 103 | logger.debug(`${title} <=> ${team1.odds}:${team2.odds}`); 104 | 105 | const oddResult = team1.odds > team2.odds, 106 | { match } = TaskConfig; 107 | let teamSelect: typeof team1; 108 | // 正选,赔率越小越选 109 | if (match.selection > 0) { 110 | teamSelect = oddResult ? team2 : team1; 111 | } else { 112 | teamSelect = oddResult ? team1 : team2; 113 | } 114 | 115 | logger.verbose(`预测[ ${teamSelect.option} ] ${match.coins} 颗硬币`); 116 | return teamSelect.detail_id; 117 | } 118 | 119 | function isLackOfCoin() { 120 | const { coin, match } = TaskConfig; 121 | if (TaskModule.money - match.coins < coin.stayCoins) { 122 | logger.verbose(`需要保留${coin.stayCoins}个硬币,任务结束`); 123 | return true; 124 | } 125 | return false; 126 | } 127 | -------------------------------------------------------------------------------- /src/task/noLoginTask.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { readMangaService } from '@/service/manga.service'; 3 | import { logger } from '@/utils'; 4 | 5 | export default async function noLoginTask() { 6 | logger.info('账号未登录,将仅执行无需登录的任务'); 7 | // 漫画阅读任务无需登录 8 | if (TaskConfig.function.mangaTask) { 9 | logger.info('----【漫画阅读】----'); 10 | await readMangaService(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/task/shareAndWatch.ts: -------------------------------------------------------------------------------- 1 | import { apiDelay, random } from '../utils'; 2 | import { addShare, uploadVideoHeartbeat } from '../net/video.request'; 3 | import { getAidByByPriority, getAidByRecommend } from '../service/coin.service'; 4 | import { TaskModule } from '../config/globalVar'; 5 | import { logger } from '../utils/log'; 6 | import { request } from '@/utils/request'; 7 | import { checkShareAndWatch } from '@/service/reward.service'; 8 | 9 | /** 10 | * 每日分享/播放视频 11 | */ 12 | export default async function shareAndWatch() { 13 | logger.info('----【分享/播放视频】----'); 14 | await checkShareAndWatch(); 15 | if (TaskModule.share && TaskModule.watch) { 16 | logger.info('已完成,跳过分享/播放'); 17 | return; 18 | } 19 | const aid = TaskModule.videoAid || (await getVideoAid()); 20 | if (!aid) { 21 | return; 22 | } 23 | 24 | //分享 25 | if (!TaskModule.share) { 26 | await apiDelay(); 27 | await request(addShare, { name: '分享视频', okMsg: '分享视频成功!' }, aid); 28 | } 29 | 30 | //播放视频 31 | if (!TaskModule.watch) { 32 | await apiDelay(); 33 | //随机上传4s到60s 34 | await request( 35 | uploadVideoHeartbeat, 36 | { name: '播放视频', okMsg: '播放视频成功!' }, 37 | aid, 38 | random(4, 60), 39 | ); 40 | } 41 | } 42 | 43 | /** 44 | * 获取视频 45 | */ 46 | export async function getVideo() { 47 | for (let errCount = 5; errCount > 0; errCount--) { 48 | const biliav = await getAidByByPriority(['视频']); 49 | if (biliav.code !== 0) { 50 | return await getAidByRecommend(); 51 | } 52 | if (biliav && biliav.data?.coinType === '视频') { 53 | return biliav; 54 | } 55 | } 56 | return await getAidByRecommend(); 57 | } 58 | 59 | async function getVideoAid() { 60 | // 获取aid 61 | try { 62 | const biliav = await getVideo(); 63 | if (biliav.code === 0) { 64 | const { id, author, title } = biliav.data || {}; 65 | logger.debug(`获取视频: ${title} --up【${author}】`); 66 | return id; 67 | } else { 68 | logger.warn(`获取视频失败 ${biliav.msg}`); 69 | return; 70 | } 71 | } catch (error) { 72 | logger.error(`获取视频出现异常:`, error); 73 | return; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/task/silver2Coin.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/log'; 2 | import { exchangeSilver2Coin, exchangeStatus, getMyWallet } from '../net/live.request'; 3 | 4 | /** 5 | * 银瓜子兑换硬币 6 | */ 7 | export default async function silver2Coin() { 8 | logger.info('----【银瓜子兑换硬币】----'); 9 | try { 10 | const { data, code, message } = await exchangeStatus(); 11 | if (code != 0) { 12 | logger.info(`获取瓜子详情失败 ${message}`); 13 | } 14 | if (data.silver_2_coin_left === 0) { 15 | logger.info('今日已兑换一次'); 16 | // 700 是一次的价格 17 | } else if (data.silver < 700) { 18 | logger.info('兑换失败,你瓜子不够了'); 19 | } else { 20 | const { message } = await exchangeSilver2Coin(); 21 | logger.info(`兑换硬币: ${message}`); 22 | await getMyWallet(); 23 | } 24 | } catch (error) { 25 | logger.error(`操作异常 ${error.message}`); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/task/supGroupSign.ts: -------------------------------------------------------------------------------- 1 | import { SupGroupsDto } from '../dto/sup-group.dto'; 2 | import { apiDelay } from '../utils'; 3 | import { getMyGroupsApi, groupSignApi } from '../net/sup-group.request'; 4 | import { logger } from '../utils/log'; 5 | 6 | type Group = SupGroupsDto['data']['list']; 7 | 8 | /** 9 | * 获取应援团信息 10 | */ 11 | async function getMyGroups() { 12 | try { 13 | const { data, code, message } = await getMyGroupsApi(); 14 | if (code === 0) { 15 | return data?.list || []; 16 | } 17 | logger.warn(`获取自己的应援团异常失败: ${message}`); 18 | return []; 19 | } catch (error) { 20 | logger.error(`获取自己的应援团异常: ${error}`); 21 | } 22 | } 23 | 24 | /** 25 | * 应援团我的签到 26 | */ 27 | export default async function supGroupSign() { 28 | logger.info('----【应援团签到】----'); 29 | 30 | const myGroups = await getMyGroups(); 31 | if (!myGroups) return; 32 | await apiDelay(); 33 | const countRef = { value: 0 }; 34 | for (let i = 0; i < myGroups.length; i++) { 35 | const group = myGroups[i]; 36 | await groupSign(group, countRef); 37 | } 38 | logger.info(`签到结束,成功${countRef.value}/${myGroups.length}`); 39 | } 40 | 41 | async function groupSign(group: Group[0], countRef: Ref) { 42 | try { 43 | // logger.info(`应援团${group.group_name}签到开始`); 44 | const { data, code, message } = await groupSignApi(group.group_id, group.owner_uid); 45 | if (code !== 0) { 46 | logger.warn(`[${group.group_name}]签到失败 ${message}`); 47 | return; 48 | } 49 | if (data.status === 0) { 50 | countRef.value++; 51 | // logger.info(`签到成功: ${message}`); 52 | } else { 53 | logger.verbose(`签到失败: ${data.status} ${message}`); 54 | } 55 | } catch (error) { 56 | logger.error(`签到异常 ${error.message}`); 57 | } finally { 58 | await apiDelay(400, 1000); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/task/useCouponBp.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '@/config/globalVar'; 2 | import { chargingService, exchangeBatteryService } from '@/service/balance.service'; 3 | import { logger } from '@/utils/log'; 4 | 5 | export default async function useCouponBp() { 6 | logger.info('----【使用b币券】----'); 7 | try { 8 | // 使用方式 9 | const useType = TaskConfig.couponBalance.use; 10 | if (useType === '充电' || useType === 'charge') { 11 | await chargingService(); 12 | return; 13 | } 14 | await exchangeBatteryService(); 15 | } catch (error) { 16 | logger.error(`使用b币券出现异常:${error.message}`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/types/@alicloud__fc2.d.ts: -------------------------------------------------------------------------------- 1 | // https://doxmate.cool/aliyun/fc-nodejs-sdk/api.html 2 | declare module '@alicloud/fc2' { 3 | interface FCClientProp { 4 | accessKeyID; 5 | accessKeySecret; 6 | region: string; 7 | timeout?: string; // Request timeout in milliseconds, default is 10s 8 | } 9 | 10 | interface TriggerConfig { 11 | payload?: string; 12 | cronExpression?: string; 13 | enable?: boolean; 14 | } 15 | 16 | interface TriggerOptions { 17 | invocationRole?: unknown; 18 | sourceArn?: string; 19 | triggerType?: string; 20 | triggerName?: string; 21 | triggerConfig?: TriggerConfig; 22 | } 23 | 24 | interface TriggerResponse { 25 | headers: { 26 | 'content-type': string; 27 | date: string; 28 | 'content-length': string; 29 | [key: string]: string; 30 | }; 31 | data: { 32 | triggerName: string; 33 | description: string; 34 | triggerType: string; 35 | triggerId: string; 36 | sourceArn: string; 37 | invocationRole: null; 38 | qualifier: string; 39 | urlInternet: null; 40 | urlIntranet: null; 41 | triggerConfig: TriggerConfig; 42 | createdTime: string; 43 | lastModifiedTime: string; 44 | }; 45 | } 46 | 47 | export default class { 48 | constructor(accountId: string, props: FCClientProp); 49 | 50 | createTrigger(serviceName: string, functionName: string, options: TriggerOptions); 51 | 52 | getTrigger( 53 | serviceName: string, 54 | functionName: string, 55 | triggerName: string, 56 | ): Promise; 57 | 58 | updateTrigger( 59 | serviceName: string, 60 | functionName: string, 61 | triggerName: string, 62 | options: TriggerOptions, 63 | ): Promise; 64 | 65 | deleteTrigger( 66 | serviceName: string, 67 | functionName: string, 68 | triggerName: string, 69 | ): Promise; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/types/LiveHeart.ts: -------------------------------------------------------------------------------- 1 | export type LiveHeartRuleId = [number, number, number, number]; 2 | export type DeviceType = [string, string]; 3 | 4 | export interface LiveHeartERequest { 5 | id: string; 6 | device: string; 7 | ts: number; 8 | is_patch: number; 9 | heart_beat: string; 10 | ua: string; 11 | visit_id: string; 12 | csrf: string; 13 | csrf_token: string; 14 | } 15 | 16 | export interface HmacsData { 17 | id: string; 18 | device: string; 19 | ets: number; 20 | benchmark: string; 21 | time: number; 22 | ts: number; 23 | ua: string; 24 | } 25 | 26 | export interface LiveHeartXRequest extends HmacsData { 27 | csrf: string; 28 | csrf_token: string; 29 | visit_id: string; 30 | s: string; 31 | } 32 | 33 | export interface HeartBaseDateType { 34 | ua: string; 35 | id: LiveHeartRuleId; 36 | csrf_token: string; 37 | csrf: string; 38 | device: DeviceType; 39 | uname: string; 40 | } 41 | 42 | export interface HeartSLSDateType { 43 | /** 压缩的数据 */ 44 | d: string; 45 | /** 心跳数 */ 46 | hn: { 47 | v: number; 48 | }; 49 | /** 数据长度 */ 50 | l: number; 51 | } 52 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | import type { TheConfig } from '@/config/config'; 2 | 3 | type MessageType = { 4 | message: { 5 | email: { 6 | pass: string; 7 | from: string; 8 | port: number; 9 | host: string; 10 | to: string; 11 | }; 12 | }; 13 | }; 14 | 15 | // 必选项 16 | type RequiredConfig = { 17 | cookie: string; 18 | }; 19 | 20 | type CommonBase = { 21 | __common__: boolean; 22 | }; 23 | 24 | export type CommonConfig = Omit & CommonBase; 25 | 26 | export type Config = Required; 27 | 28 | export type UserConfig = RecursivePartial & RequiredConfig; 29 | 30 | export type MabEmptyConfig = UserConfig | undefined; 31 | export type ConfigArray = MabEmptyConfig[]; 32 | 33 | export type CouponBalanceUseType = '充电' | '电池' | 'charge' | 'battery'; 34 | -------------------------------------------------------------------------------- /src/types/fc.ts: -------------------------------------------------------------------------------- 1 | // https://help.aliyun.com/document_detail/426947.html 2 | export interface FCContext { 3 | requestId: string; 4 | credentials: { 5 | accessKeyId?: string; 6 | accessKeySecret?: string; 7 | securityToken?: string; 8 | }; 9 | function: { name: string; handler: string; memory: number; timeout: number }; 10 | service: { 11 | name: string; 12 | qualifier: string; 13 | logProject?: string; 14 | logStore?: string; 15 | versionId?: unknown; 16 | }; 17 | /* 区域 */ 18 | region: string; 19 | accountId: string; 20 | logger: { 21 | // 与 requestId 相同 22 | requestId: string; 23 | logLevel: string; 24 | }; 25 | retryCount: number; 26 | tracing: { 27 | openTracingSpanBaggages: Record; 28 | openTracingSpanContext?: unknown; 29 | jaegerEndpoint?: unknown; 30 | }; 31 | waitsForEmptyEventLoopBeforeCallback: boolean; 32 | } 33 | 34 | export interface FCEvent { 35 | /** eg: '2022-05-03T11:10:00Z' */ 36 | triggerTime: string; 37 | triggerName: string; 38 | payload: string; 39 | [key: string]: unknown; 40 | } 41 | 42 | export type FCCallback = (err?: Error | null, message?: string) => void; 43 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | import type { Config } from './config'; 3 | declare global { 4 | var __BT_context__: { 5 | event: any; 6 | context: any; 7 | resolve: (value: unknown) => void; 8 | reject: (reason?: any) => void; 9 | }; 10 | var BILITOOLS_CONFIG: Config; 11 | 12 | type RecursivePartial = { 13 | [p in keyof S]+?: S[p] extends object ? RecursivePartial : S[p]; 14 | }; 15 | 16 | type RecursiveRequired = { 17 | [p in keyof S]-?: S[p] extends object ? RecursiveRequired : S[p]; 18 | }; 19 | 20 | type Ref = { 21 | value: T; 22 | }; 23 | 24 | type UnPromisify = T extends Promise ? U : T; 25 | 26 | // 一定已经定义了 27 | type Defined = T extends undefined ? never : T; 28 | 29 | // 将值作为类型 30 | type ValueOf = T[keyof T]; 31 | } 32 | 33 | export {}; 34 | -------------------------------------------------------------------------------- /src/types/got.ts: -------------------------------------------------------------------------------- 1 | import type { OptionsOfUnknownResponseBody } from 'got'; 2 | import type { RequestOptions } from './request'; 3 | 4 | export interface VGotOptions extends OptionsOfUnknownResponseBody { 5 | params?: OptionsOfUnknownResponseBody['searchParams']; 6 | data?: any; 7 | baseURL?: string; 8 | requestOptions?: RequestOptions; 9 | httpsAgent?: any; 10 | httpAgent?: any; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './LiveHeart'; 3 | export * from './scf'; 4 | export * from './fc'; 5 | 6 | export type IdType = number | string; 7 | export type SLSType = 'scf' | 'fc' | 'cfc' | 'agc' | 'hg'; 8 | export interface CronDateType { 9 | hours: number; 10 | minutes: number; 11 | seconds?: number; 12 | } 13 | 14 | export interface ActivityLotteryIdType { 15 | sid: string; 16 | title?: string; 17 | bangumis?: number[]; 18 | followBangumi?: boolean; 19 | follows?: number[]; 20 | followed?: boolean; 21 | } 22 | 23 | export type SessionHandleType = 'read' | 'del' | 'delete' | 'none' | undefined; 24 | -------------------------------------------------------------------------------- /src/types/log.ts: -------------------------------------------------------------------------------- 1 | export type MessageType = string | number | boolean | undefined | null; 2 | export type LevelType = 'error' | 'warn' | 'info' | 'verbose' | 'debug'; 3 | export interface SimpleLoggerOptions { 4 | console?: LevelType | boolean; 5 | file?: LevelType | boolean; 6 | push?: LevelType | boolean; 7 | name?: string; 8 | payload?: string | number; 9 | } 10 | export interface LoggerOptions extends SimpleLoggerOptions { 11 | /** 日志文件分割方式,按天 */ 12 | fileSplit?: 'day' | 'month'; 13 | } 14 | export interface LogOptions { 15 | level?: LevelType; 16 | } 17 | export type LoggerInitOptions = { br?: string; useEmoji?: boolean }; 18 | -------------------------------------------------------------------------------- /src/types/request.ts: -------------------------------------------------------------------------------- 1 | export interface RequestOptions { 2 | // 需要对返回数据进行处理 3 | isTransformResponse?: boolean; 4 | // 是否返回原生响应头 比如:需要获取响应头时使用该属性 5 | isReturnNativeResponse?: boolean; 6 | // 默认请求地址 7 | apiUrl?: string; 8 | // 忽略重复请求 9 | ignoreCancelToken?: boolean; 10 | // 是 JSONP 11 | isJsonp?: boolean; 12 | // 重试次数 13 | retry?: number; 14 | // 当前重试次数 15 | __retryCount?: number; 16 | // 重试延迟 17 | retryDelay?: number; 18 | // 是否携带 cookie 19 | withCredentials?: boolean; 20 | // 是否携带 bili cookie 21 | withBiliCookie?: boolean; 22 | } 23 | 24 | export interface Result { 25 | code: number; 26 | data?: T; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/scf.ts: -------------------------------------------------------------------------------- 1 | export type SCFEvent = { 2 | key1?: string; // 手动执行时会有 3 | key2?: string; // 手动执行时会有 4 | /** 附带信息 */ 5 | Message?: string; 6 | Time?: string; 7 | TriggerName?: string; 8 | /** 触发器类型 */ 9 | Type?: string; 10 | }; 11 | 12 | export type SCFContext = { 13 | callbackWaitsForEmptyEventLoop: unknown; 14 | getRemainingTimeInMillis: unknown; 15 | memory_limit_in_mb: number; 16 | time_limit_in_ms: number; 17 | request_id: string; 18 | environment: string; 19 | environ: string; 20 | function_version: string; 21 | function_name: string; 22 | namespace: string; 23 | tencentcloud_region: string; 24 | tencentcloud_appid: string; 25 | tencentcloud_uin: string; 26 | }; 27 | -------------------------------------------------------------------------------- /src/types/sls.d.ts: -------------------------------------------------------------------------------- 1 | export type SlSOptions = { 2 | customArg?: Record; 3 | triggerDesc?: { value: string; string: string }; 4 | triggerName?: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/type.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type { RequestOptions } from './request'; 3 | 4 | declare module '@catlair/node-got' { 5 | export interface RequestOptions { 6 | withBiliCookie?: boolean; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/args.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取参数 3 | * 以下四种代码等效 4 | * --arg value 5 | * -a value 6 | * --arg=value 7 | * -a=value 8 | */ 9 | export function getArg(arg: string, shortStr?: string | false) { 10 | const args = process.argv.slice(2); 11 | 12 | // 判断 --arg value 13 | const argIndex = args.indexOf(`--${arg}`); 14 | if (argIndex !== -1) { 15 | return args[argIndex + 1]; 16 | } 17 | // 判断 --arg=value 18 | const thisArg = args.find(str => str.startsWith(`--${arg}=`)); 19 | if (thisArg) { 20 | return thisArg.split('=')[1]; 21 | } 22 | if (shortStr === false) return; 23 | if (!shortStr) { 24 | shortStr = arg.at(0); 25 | } 26 | // 判断 -a value 27 | const shortIndex = args.indexOf(`-${shortStr}`); 28 | if (shortIndex !== -1) { 29 | return args[shortIndex + 1]; 30 | } 31 | // 判断 -a=value 32 | const thisShort = args.find(str => str.startsWith(`-${shortStr}=`)); 33 | if (thisShort) { 34 | return thisShort.split('=')[1]; 35 | } 36 | } 37 | 38 | /** 39 | * 是否存在参数(有的参数并不需要值) 40 | */ 41 | export function isArg(arg: string, shortStr?: string | false) { 42 | const args = process.argv.slice(2); 43 | const longArg = Boolean( 44 | args.indexOf(`--${arg}`) !== -1 || args.find(str => str.startsWith(`--${arg}=`)), 45 | ); 46 | if (shortStr === false) return; 47 | shortStr = shortStr || arg.at(0); 48 | const shortArg = Boolean( 49 | args.indexOf(`-${shortStr}`) !== -1 || args.find(str => str.startsWith(`-${shortStr}=`)), 50 | ); 51 | return longArg || shortArg; 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/bili.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isNumber, isObject } from './is'; 2 | import { getUnixTime, md5, stringify } from './pure'; 3 | import * as crypto from 'crypto'; 4 | 5 | type Params = Record>; 6 | 7 | /** 8 | * API 接口签名 9 | * @param params 10 | * @param appkey 11 | * @param appsec 12 | */ 13 | export function appSignString(params: Params = {}, appkey?: string, appsec?: string) { 14 | return getAppSign(params, appkey, appsec).query; 15 | } 16 | 17 | export function appSign(params: Params, appkey?: string, appsec?: string) { 18 | return getAppSign(params, appkey, appsec).sign; 19 | } 20 | 21 | function sortParams(params: Params) { 22 | const keys = Object.keys(params).sort(); 23 | return keys.map(key => [key, params[key]]); 24 | } 25 | 26 | export function getSign(params: Params, appsec: string, noSign = false) { 27 | const query = stringify(sortParams(params)); 28 | if (noSign) { 29 | return { query, sign: '' }; 30 | } 31 | const sign = md5(query + appsec); 32 | return { 33 | query: query + '&sign=' + sign, 34 | sign, 35 | }; 36 | } 37 | 38 | function getAppSign( 39 | params: Params, 40 | appkey = '1d8b6e7d45233436', 41 | appsec = '560c52ccd288fed045859ed18bffd973', 42 | ) { 43 | params = { 44 | ...params, 45 | platform: 'android', 46 | mobi_app: 'android', 47 | disable_rcmd: 0, 48 | build: 6780300, 49 | c_locale: 'zh_CN', 50 | s_locale: 'zh_CN', 51 | ts: getUnixTime(), 52 | }; 53 | // 某些情况下不需要也不能从配置文件中读取 54 | params.access_key = params.access_key || require('../config/globalVar')?.TaskConfig.access_key; 55 | if (!params.access_key) { 56 | delete params.access_key; 57 | return getSign(params, appsec, true); 58 | } 59 | delete params.csrf; 60 | delete params.csrf_token; 61 | params = { 62 | ...params, 63 | actionKey: 'appkey', 64 | appkey, 65 | }; 66 | return getSign(params, appsec); 67 | } 68 | 69 | /** 70 | * 对象值转换为字符串 71 | */ 72 | function objectValueToString(params: Record) { 73 | Object.keys(params).forEach(key => { 74 | if (isNumber(params[key])) { 75 | params[key] = params[key].toString(); 76 | return; 77 | } 78 | if (isObject(params[key])) { 79 | objectValueToString(params[key]); 80 | return; 81 | } 82 | if (isArray(params[key])) { 83 | params[key] = params[key].map(item => 84 | isObject(item) ? objectValueToString(item) : item.toString(), 85 | ); 86 | } 87 | }); 88 | return params; 89 | } 90 | 91 | export function clientSign(params: Params) { 92 | let data = JSON.stringify(objectValueToString(params)); 93 | const alg = ['SHA512', 'SHA3-512', 'SHA384', 'SHA3-384', 'BLAKE2b512']; 94 | for (const a of alg) { 95 | data = crypto.createHash(a).update(data).digest('hex'); 96 | } 97 | return data; 98 | } 99 | 100 | /** 101 | * 给昵称添加 ** (目的是变简短) 102 | */ 103 | export function conciseNickname(nickname = '') { 104 | const length = nickname.length; 105 | if (length <= 3) { 106 | return nickname; 107 | } 108 | const firstWord = nickname[0]; 109 | const lastWord = nickname[length - 1]; 110 | return `${firstWord}**${lastWord}`; 111 | } 112 | -------------------------------------------------------------------------------- /src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | import { isString } from './is'; 2 | 3 | export function getCookieJSON(cookie: string | undefined): Record { 4 | if (!cookie) return {}; 5 | // 使用正则表达式获取 cookie 键值对,并转换为对象 6 | const matchArray = cookie.match(/([^;=]+)(?:=([^;]*))?/g); 7 | if (!matchArray) return {}; 8 | return matchArray.reduce((pre, cur) => { 9 | const [key, value] = cur.trim().split('='); 10 | pre[key] = encodeCookieValue(value); 11 | return pre; 12 | }, {}); 13 | } 14 | 15 | /** 16 | * 处理 set-cookie 17 | * @param setCookieArray 18 | */ 19 | function getSetCookieValue(setCookieArray: string[]) { 20 | let cookieStr = ''; 21 | setCookieArray.forEach(setCookie => (cookieStr += setCookie.split('; ')[0] + '; ')); 22 | if (cookieStr.endsWith('; ')) { 23 | cookieStr = cookieStr.substring(0, cookieStr.length - 2 || 0); 24 | } 25 | return cookieStr; 26 | } 27 | 28 | function encodeCookieValue(val: string) { 29 | return encodeURIComponent(val) 30 | .replaceAll('%7C', '|') 31 | .replaceAll('%2B', '+') 32 | .replaceAll('%25', '%') 33 | .replaceAll('*', '%2A'); 34 | } 35 | 36 | export function encodeCookie(cookie: string) { 37 | return getCookieString(getCookieJSON(cookie)); 38 | } 39 | 40 | function getCookieString(obj: object) { 41 | const string = Object.keys(obj).reduce((pre, cur) => pre + `${cur}=${obj[cur]}; `, ''); 42 | return string.substring(0, string.length - 2 || 0); 43 | } 44 | 45 | export default function getCookie(cookie = '', setCookie: string[] | string) { 46 | if (isString(setCookie)) setCookie = [setCookie]; 47 | if (!setCookie || setCookie.length === 0) return cookie; 48 | 49 | return getCookieString({ 50 | ...getCookieJSON(cookie), 51 | ...getCookieJSON(getSetCookieValue(setCookie)), 52 | }); 53 | } 54 | export { getCookie }; 55 | 56 | export function getCookieItem(cookie: string | undefined, key: string) { 57 | if (!cookie) return null; 58 | const reg = `(?:^| )${key}=([^;]*)(?:;|$)`; 59 | const r = cookie.match(reg); 60 | return r ? r[1] : null; 61 | } 62 | 63 | export function getUserId(cookie: string): number { 64 | return Number(getCookieItem(cookie, 'DedeUserID')) || 0; 65 | } 66 | 67 | export function getBiliJct(cookie: string): string { 68 | return getCookieItem(cookie, 'bili_jct') || ''; 69 | } 70 | 71 | export function getLIVE_BUVID(cookie: string): string { 72 | return getCookieItem(cookie, 'LIVE_BUVID') || ''; 73 | } 74 | 75 | export class CookieJar { 76 | private cookie: string; 77 | 78 | constructor(cookie?: string) { 79 | if (cookie) { 80 | this.cookie = cookie; 81 | } 82 | } 83 | 84 | getCookieString() { 85 | return this.cookie; 86 | } 87 | 88 | setCookie(rawCookie: string) { 89 | this.cookie = getCookie(this.cookie, rawCookie); 90 | } 91 | 92 | toJSON() { 93 | return getCookieJSON(this.cookie); 94 | } 95 | 96 | getCookieItem(key: string) { 97 | return getCookieItem(this.cookie, key); 98 | } 99 | } 100 | 101 | export function isBiliCookie(cookie: string) { 102 | return Boolean( 103 | cookie && 104 | cookie.length > 90 && 105 | ['bili_jct', 'SESSDATA', 'DedeUserID'].every(str => cookie.includes(str)), 106 | ); 107 | } 108 | 109 | (async () => { 110 | const a = new CookieJar(); 111 | [ 112 | 'SESSDATA=e860d3ee%2C1686573108%2C5b7a7%2Ac1; Path=/; Domain=bilibili.com; Expires=Mon, 12 Jun 2023 12:31:48 GMT; HttpOnly; Secure', 113 | 'bili_jct=4b403a186c5cde6f9e52be10c2cd9695; Path=/; Domain=bilibili.com; Expires=Mon, 12 Jun 2023 12:31:48 GMT', 114 | 'DedeUserID=357123798; Path=/; Domain=bilibili.com; Expires=Mon, 12 Jun 2023 12:31:48 GMT', 115 | 'DedeUserID__ckMd5=8aa3b2eff1fa70ed; Path=/; Domain=bilibili.com; Expires=Mon, 12 Jun 2023 12:31:48 GMT', 116 | 'sid=h1j3dijd; Path=/; Domain=bilibili.com; Expires=Mon, 12 Jun 2023 12:31:48 GMT', 117 | ].forEach(setCookie => a.setCookie(setCookie)); 118 | })(); 119 | -------------------------------------------------------------------------------- /src/utils/effect.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig } from '../config/globalVar'; 2 | import { Sleep, random } from './pure'; 3 | 4 | /** 5 | * 异步延迟函数 6 | * @param delayTime 延迟时间(ms) 7 | * @param delayTime2 延迟时间2(ms) 8 | */ 9 | export function apiDelay(delayTime?: number, delayTime2?: number) { 10 | return Sleep.wait(getDelay(delayTime, delayTime2)); 11 | } 12 | 13 | export function apiDelaySync(delayTime?: number, delayTime2?: number) { 14 | Sleep.waitSync(getDelay(delayTime, delayTime2)); 15 | } 16 | 17 | export const sleep = apiDelay; 18 | export const sleepSync = apiDelaySync; 19 | 20 | function getDelay(delayTime?: number, delayTime2?: number) { 21 | if (delayTime && delayTime2) { 22 | return random(delayTime, delayTime2); 23 | } 24 | if (delayTime) { 25 | return delayTime; 26 | } 27 | const API_DELAY = TaskConfig.apiDelay; 28 | if (API_DELAY.length === 1) { 29 | return API_DELAY[0] * 1000; 30 | } 31 | return random(API_DELAY[0] || 2, API_DELAY[1] || 6) * 1000; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import type { SLSType } from '@/types'; 2 | 3 | type ENVType = { 4 | qinglong: boolean; 5 | fc: boolean; 6 | scf: boolean; 7 | cfc: boolean; 8 | agc: boolean; 9 | serverless: boolean; 10 | type: SLSType | 'local'; 11 | }; 12 | 13 | const ENV_BASE = { 14 | qinglong: isQingLongPanel(), 15 | fc: isFC(), 16 | scf: isSCF(), 17 | cfc: isCFC(), 18 | agc: isAGC(), 19 | }; 20 | 21 | export const ENV: ENVType = { 22 | ...ENV_BASE, 23 | serverless: isServerless(), 24 | type: getEnvType(), 25 | }; 26 | 27 | /** 28 | * 是否是青龙面板 29 | */ 30 | export function isQingLongPanel() { 31 | // @ts-ignore 32 | return Boolean(process.env.IS_QING_LONG || '__IS_QINGLONG__' === 'true' || process.env.QL_BRANCH); 33 | } 34 | 35 | /** 36 | * 是否是 CFC 37 | */ 38 | export function isCFC() { 39 | // @ts-ignore 40 | return global.IS_CFC || '__IS_CFC__' === 'true'; 41 | } 42 | 43 | /** 44 | * 是否是 AGC 45 | */ 46 | export function isAGC() { 47 | // @ts-ignore 48 | // @TODO: IS_CFC 是因为,代码使用的同一套 49 | return global.IS_CFC || '__IS_AGC__' === 'true'; 50 | } 51 | 52 | export function setConfigFileName() { 53 | const defaultConfigFileName = ENV_BASE.qinglong ? 'cat_bili_config' : 'config', 54 | ext = '.json'; 55 | 56 | const { BILITOOLS_FILE_NAME } = process.env; 57 | 58 | if (BILITOOLS_FILE_NAME) { 59 | if (BILITOOLS_FILE_NAME.endsWith(ext)) { 60 | return BILITOOLS_FILE_NAME; 61 | } 62 | return `${BILITOOLS_FILE_NAME}${ext}`; 63 | } 64 | 65 | return defaultConfigFileName + ext; 66 | } 67 | 68 | /** 69 | * 判断是否是 FC 70 | */ 71 | export function isFC() { 72 | const keys = Object.keys(process.env); 73 | const tags = [ 74 | 'securityToken', 75 | 'accessKeyID', 76 | 'accessKeySecret', 77 | 'FC_FUNCTION_MEMORY_SIZE', 78 | 'FC_FUNC_CODE_PATH', 79 | 'FC_RUNTIME_VERSION', 80 | ]; 81 | // @ts-ignore 82 | return keys.filter(key => tags.includes(key)).length >= 3 || '__IS_FC__' === 'true'; 83 | } 84 | 85 | /** 86 | * 判断是否是 SCF 87 | */ 88 | export function isSCF() { 89 | const keys = Object.keys(process.env); 90 | const isSCF = keys.filter(key => key.startsWith('SCF_')).length >= 10; 91 | const isTENCENTCLOUD = keys.filter(key => key.startsWith('TENCENTCLOUD_')).length >= 3; 92 | // @ts-ignore 93 | return (isSCF && isTENCENTCLOUD) || '__IS_SCF__' === 'true'; 94 | } 95 | 96 | export function isServerless() { 97 | return ENV_BASE.fc || ENV_BASE.scf || ENV_BASE.cfc || ENV_BASE.agc; 98 | } 99 | 100 | export function getEnvType(): SLSType | 'local' { 101 | if (ENV_BASE.scf) { 102 | return 'scf'; 103 | } 104 | if (ENV_BASE.fc) { 105 | return 'fc'; 106 | } 107 | if (ENV_BASE.agc) { 108 | return 'agc'; 109 | } 110 | if (ENV_BASE.cfc) { 111 | return 'cfc'; 112 | } 113 | return 'local'; 114 | } 115 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from 'fs'; 2 | import { dirname } from 'path'; 3 | import { isString } from './is'; 4 | import { JSON5 } from './json5'; 5 | import { defLogger } from './log/def'; 6 | 7 | /** 8 | * 读取 json 文件 9 | * @param filePath 文件路径 10 | */ 11 | export function readJsonFile(filePath: string): T | undefined { 12 | try { 13 | let content: string; 14 | if (existsSync(filePath)) { 15 | content = readFileSync(filePath, 'utf-8'); 16 | } 17 | if (content!) { 18 | return JSON5.parse(content); 19 | } 20 | } catch (error) { 21 | defLogger.debug(`加载路径为 ${filePath} 的文件失败`); 22 | defLogger.error(error); 23 | jsonErrorHandle(error.message); 24 | } 25 | } 26 | 27 | export function jsonErrorHandle(message: string) { 28 | if (!isString(message)) { 29 | return; 30 | } 31 | if (message.includes('SyntaxError: JSON5')) { 32 | throw new SyntaxError('配置文件存在,但是无法解析!可能 JSON5 格式不正确!'); 33 | } 34 | throw new Error(message); 35 | } 36 | 37 | /** 38 | * 通过用户 id 匹配并替换 cookie 39 | */ 40 | export function replaceAllCookie(filePath: string, userId: number | string, newCookie: string) { 41 | if (!filePath) return false; 42 | try { 43 | const content = readFileSync(filePath, 'utf-8'); 44 | const DedeUserID = `DedeUserID=${userId}`; 45 | const reg = new RegExp(`['"]?cookie['"]?:\\s?['"](.*${DedeUserID}.*)['"]`, 'g'); 46 | const newJson5 = content.replaceAll(reg, substring => { 47 | let quote = substring.at(0) || ''; 48 | /['"]/.test(quote) || (quote = ''); 49 | // 避免使用转义符 50 | const quote2 = newCookie.includes("'") 51 | ? '"' 52 | : substring.match(/^['"]?cookie['"]?:\s?(['"])/)?.[1] || '"'; 53 | return `${quote}cookie${quote}: ${quote2}${newCookie}${quote2}`; 54 | }); 55 | if (content === newJson5) return false; 56 | writeFileSync(filePath, newJson5); 57 | return true; 58 | } catch (error) { 59 | defLogger.error(error); 60 | } 61 | return false; 62 | } 63 | 64 | /** 65 | * 重写配置文件 66 | * @param filepath 67 | * @param obj 68 | */ 69 | export function writeJsonFile(filepath: string, obj: Record) { 70 | try { 71 | const oldObj = readJsonFile(filepath); 72 | writeFileSync( 73 | filepath, 74 | JSON.stringify({ 75 | ...oldObj, 76 | ...obj, 77 | }), 78 | ); 79 | } catch (err) { 80 | defLogger.debug(err); 81 | } 82 | } 83 | 84 | /** 85 | * 获取 config 路径 86 | */ 87 | export function getConfigPath() { 88 | const path = process.env.__BT_CONFIG_PATH__; 89 | if (!path) { 90 | return undefined; 91 | } 92 | return { 93 | path, 94 | dir: dirname(path), 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/got/BiliGot.ts: -------------------------------------------------------------------------------- 1 | import type { VGotOptions } from '@catlair/node-got'; 2 | import { BiliCookieJar } from '@/config/globalVar'; 3 | import { VGot } from '@catlair/node-got'; 4 | import { CookieJar } from '../cookie'; 5 | 6 | export class BiliGot extends VGot { 7 | constructor(options: VGotOptions) { 8 | // 处理 cookie 9 | const { withBiliCookie, withCredentials } = options.requestOptions || {}; 10 | if (withBiliCookie) { 11 | options.cookieJar = new BiliCookieJar(); 12 | } else if (withCredentials) { 13 | options.cookieJar = new CookieJar(); 14 | } 15 | super(options); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/got/bili.ts: -------------------------------------------------------------------------------- 1 | import type { VGotOptions } from '@catlair/node-got'; 2 | import { deepMergeObject } from '../pure'; 3 | import { getOptions } from './config'; 4 | import { BiliGot } from './BiliGot'; 5 | 6 | export function createRequest(opt: Partial = {}) { 7 | return new BiliGot(deepMergeObject(getOptions(), opt)); 8 | } 9 | 10 | export const biliHttp = createRequest(); 11 | -------------------------------------------------------------------------------- /src/utils/got/config.ts: -------------------------------------------------------------------------------- 1 | import type { VGotOptions } from '@catlair/node-got'; 2 | import { defaultHeaders } from '@/constant/biliUri'; 3 | 4 | export function getOptions(): VGotOptions { 5 | return { 6 | timeout: 10000, 7 | headers: { 8 | 'content-type': defaultHeaders['content-type'], 9 | 'user-agent': defaultHeaders['user-agent'], 10 | 'accept-language': defaultHeaders['accept-language'], 11 | 'accept-encoding': defaultHeaders['accept-encoding'], 12 | }, 13 | // 配置项,下面的选项都可以在独立的接口请求中覆盖 14 | requestOptions: { 15 | // 需要对返回数据进行处理 16 | isTransformResponse: true, 17 | // 忽略重复请求 18 | ignoreCancelToken: true, 19 | // 是否携带 bili cookie 20 | withBiliCookie: true, 21 | }, 22 | throwHttpErrors: false, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/got/index.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from '@catlair/node-got'; 2 | import { getOptions } from './config'; 3 | 4 | export { createRequest }; 5 | 6 | export const defHttp = createRequest(getOptions()); 7 | -------------------------------------------------------------------------------- /src/utils/gzip.ts: -------------------------------------------------------------------------------- 1 | import { gzipSync, unzipSync } from 'zlib'; 2 | 3 | // 与百度对比 4 | // 百度使用 escape/unescape,而我使用 encodeURIComponent/decodeURIComponent 5 | // 存在不兼容的情况: 6 | // 从百度压缩,此处解压是必须完成的,所以使用了 unicode2str 7 | // 从此处压缩,百度解压不必要,就 encodeURIComponent 即可(无法在百度解压成功) 8 | 9 | /** 10 | * gzip 11 | * @param str 12 | * @returns {*} 13 | */ 14 | export function gzipEncode(str: string): string { 15 | try { 16 | return gzipSync(encodeURIComponent(str)).toString('base64'); 17 | } catch (e) { 18 | return 'Error: 当前字符串不能被Gzip压缩'; 19 | } 20 | } 21 | 22 | /** 23 | * ungzip 24 | * @param str 25 | * @returns {string} 26 | */ 27 | export function gzipDecode(str: string): string { 28 | try { 29 | const result = unzipSync(Buffer.from(str, 'base64')).toString(); 30 | try { 31 | return decodeURIComponent(unicode2str(result)); 32 | } catch (error) { 33 | // 兼容百度的在线压缩 34 | return unescape(result); 35 | } 36 | } catch (e) { 37 | throw new Error('Error: 当前字符串不能被Gzip解压'); 38 | } 39 | } 40 | 41 | /** 42 | * unicode 转字符串 43 | */ 44 | export function unicode2str(str: string): string { 45 | return str.replace(/\\u([\d\w]{4})/gi, (_match, grp) => String.fromCodePoint(parseInt(grp, 16))); 46 | } 47 | 48 | export default { gzipDecode, gzipEncode }; 49 | -------------------------------------------------------------------------------- /src/utils/http/def.ts: -------------------------------------------------------------------------------- 1 | export * from '../got'; 2 | -------------------------------------------------------------------------------- /src/utils/http/index.ts: -------------------------------------------------------------------------------- 1 | import { defHttp } from '../got'; 2 | export { defHttp }; 3 | 4 | export * from '../got/bili'; 5 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gzip'; 2 | export * from './effect'; 3 | export * from './cookie'; 4 | export * from './env'; 5 | export * from './log'; 6 | export * from './pure'; 7 | export * from './is'; 8 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | export function is(val: unknown, type: string) { 4 | return toString.call(val) === `[object ${type}]`; 5 | } 6 | 7 | export function isDef(val?: T): val is T { 8 | return typeof val !== 'undefined'; 9 | } 10 | 11 | export function isUnDef(val?: T): val is undefined { 12 | return !isDef(val); 13 | } 14 | 15 | export function isObject(val: any): val is Record { 16 | return val !== null && is(val, 'Object'); 17 | } 18 | 19 | export function isEmpty(val: T): val is T { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0; 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0; 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function isDate(val: unknown): val is Date { 36 | return is(val, 'Date'); 37 | } 38 | 39 | export function isNull(val: unknown): val is null { 40 | return val === null; 41 | } 42 | 43 | export function isNullAndUnDef(val: unknown): val is null | undefined { 44 | return isUnDef(val) && isNull(val); 45 | } 46 | 47 | export function isNullOrUnDef(val: unknown): val is null | undefined { 48 | return isUnDef(val) || isNull(val); 49 | } 50 | 51 | export function isNumber(val: unknown): val is number { 52 | return is(val, 'Number'); 53 | } 54 | 55 | export function isPromise(val: unknown): val is Promise { 56 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); 57 | } 58 | 59 | export function isString(val: unknown): val is string { 60 | return is(val, 'String'); 61 | } 62 | 63 | // eslint-disable-next-line @typescript-eslint/ban-types 64 | export function isFunction(val: unknown): val is Function { 65 | return typeof val === 'function'; 66 | } 67 | 68 | export function isBoolean(val: unknown): val is boolean { 69 | return is(val, 'Boolean'); 70 | } 71 | 72 | export function isRegExp(val: unknown): val is RegExp { 73 | return is(val, 'RegExp'); 74 | } 75 | 76 | export function isArray(val: any): val is Array { 77 | return val && Array.isArray(val); 78 | } 79 | 80 | export function isMap(val: unknown): val is Map { 81 | return is(val, 'Map'); 82 | } 83 | 84 | export function isUrl(path: string): boolean { 85 | const reg = 86 | // eslint-disable-next-line no-useless-escape 87 | /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 88 | return reg.test(path); 89 | } 90 | 91 | export function isEmail(val: string): boolean { 92 | return /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(val); 93 | } 94 | 95 | export function isHexString(val: string): boolean { 96 | return /^[01]+$/.test(val); 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/json5.ts: -------------------------------------------------------------------------------- 1 | interface JSON5Type { 2 | parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | null): T; 3 | stringify( 4 | value: any, 5 | replacer?: ((this: any, key: string, value: any) => any) | null, 6 | space?: string | number | null, 7 | ): string; 8 | } 9 | 10 | function getJSON5() { 11 | try { 12 | const json5 = require('json5'); 13 | // 解决cjs mjs环境导致的路径问题 14 | // @ts-ignore 15 | return json5.default ? json5.default : json5; 16 | } catch {} 17 | } 18 | 19 | export const JSON5: JSON5Type = getJSON5(); 20 | -------------------------------------------------------------------------------- /src/utils/log/file.ts: -------------------------------------------------------------------------------- 1 | import { resolvePath } from '../path'; 2 | import * as fs from 'fs'; 3 | import { isServerless } from '@/utils/env'; 4 | import { MS2DATE } from '@/constant'; 5 | 6 | export function clearLogs() { 7 | // 如果是云函数则直接退出 8 | if (isServerless()) return; 9 | const day = Number(process.env.BILIOUTILS_LOG_CLEAR_DAY || 15); 10 | deleteLogLineByDay(day); 11 | deleteLogFile(day); 12 | } 13 | 14 | /** 15 | * 删除多久之前的日志文件 16 | */ 17 | export function deleteLogFile(day = 15) { 18 | try { 19 | const filePath = resolvePath(`./logs`); 20 | const allLogFiles = fs 21 | .readdirSync(filePath) 22 | .map(file => file.match(/^bt_(?:combined|error)-\d{4}-\d{1,2}(?:-\d{1,2})?\.log$/)?.[0] || '') 23 | .filter(Boolean); 24 | 25 | allLogFiles.forEach(file => { 26 | const timeStr = file 27 | .match(/\d{4}-\d{1,2}(?:-\d{1,2})?/)?.[0] 28 | .split('-') 29 | .map(i => i.padStart(2, '0')) 30 | .join('-') 31 | .padEnd(10, '-28'); 32 | 33 | if (!timeStr) return; 34 | 35 | const time = new Date(timeStr), 36 | now = new Date(); 37 | // now > time 15 天吗 38 | if (now.getTime() - time.getTime() > day * MS2DATE) { 39 | fs.unlinkSync(`${filePath}/${file}`); 40 | } 41 | }); 42 | } catch {} 43 | } 44 | 45 | /** 46 | * 删除文件中n天之前的日志 47 | * 找到满足条件的最后一行,然后删除之前的日志 48 | */ 49 | export function deleteLogLineByDay(day = 15) { 50 | try { 51 | const filePath = resolvePath(`./logs/bt_combined-def.log`); 52 | const file = fs.readFileSync(filePath, 'utf-8'); 53 | const br = getBrChar(filePath); 54 | const lines = file.split(br); 55 | const index = lines.findIndex(line => { 56 | const linePrefix = line.match(/\[\w+\s(\d{4}\/\d{1,2}\/\d{1,2}\s\d{2}:\d{2}:\d{2})]/); 57 | if (!linePrefix) return false; 58 | return new Date().getTime() - new Date(linePrefix[1]).getTime() < day * MS2DATE; 59 | }); 60 | // index 是从0开始的,所以刚好是要删除的行数 61 | fs.writeFileSync(filePath, lines.slice(index).join(br)); 62 | } catch {} 63 | } 64 | 65 | /** 66 | * 获取文件的换行符 67 | */ 68 | export function getBrChar(file: string) { 69 | const fileContent = fs.readFileSync(file, 'utf-8'); 70 | return fileContent.match(/\r?\n/)?.[0] || `\r\n`; 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/log/index.ts: -------------------------------------------------------------------------------- 1 | import type { LoggerInitOptions, LoggerOptions, MessageType } from '@/types/log'; 2 | import { TaskConfig, TaskModule } from '@/config/globalVar'; 3 | import { defLogger, EmptyLogger, SimpleLogger } from './def'; 4 | import { clearLogs } from '@/utils/log/file'; 5 | import { resolvePath } from '../path'; 6 | import { getPRCDate } from '../pure'; 7 | 8 | export { defLogger, clearLogs }; 9 | export const emptyLogger = new EmptyLogger() as unknown as Logger; 10 | 11 | export class Logger extends SimpleLogger { 12 | constructor(protected options: LoggerOptions = {}, public name = 'default') { 13 | super(options); 14 | this.mergeOptions({ ...options, fileSplit: 'day' } as LoggerOptions); 15 | const thisTime = getPRCDate(), 16 | thisFullYear = thisTime.getFullYear(), 17 | thisMonth = thisTime.getMonth() + 1; 18 | if (options.fileSplit === 'day') { 19 | this.setFilename(`${thisFullYear}-${thisMonth}-${thisTime.getDate()}`); 20 | } else { 21 | this.setFilename(`${thisFullYear}-${thisMonth}`); 22 | } 23 | } 24 | 25 | protected setFilename(file: string) { 26 | this.errorFile = resolvePath(`./logs/bt_error-${file}.log`); 27 | this.logFile = resolvePath(`./logs/bt_combined-${file}.log`); 28 | } 29 | 30 | public error(message: MessageType | Error, error?: Error) { 31 | super.error(message, error); 32 | TaskModule.hasError = true; 33 | } 34 | 35 | static setEmoji(useEmoji = true) { 36 | if (!useEmoji) { 37 | return; 38 | } 39 | SimpleLogger.emojis = { 40 | error: '❓', 41 | warn: '❔', 42 | info: '👻', 43 | verbose: '💬', 44 | debug: '🐛', 45 | }; 46 | } 47 | 48 | static async init({ br, useEmoji }: LoggerInitOptions = {}) { 49 | this.setEmoji(useEmoji || TaskConfig.log.useEmoji); 50 | SimpleLogger.pushValue = ''; 51 | SimpleLogger.brChar = br || TaskConfig.message.br || '\n'; 52 | } 53 | 54 | static async push(title = '日志推送') { 55 | const { sendMessage } = await import('@/utils/sendNotify'); 56 | return sendMessage(title, this.pushValue); 57 | } 58 | } 59 | 60 | export const logger = new Logger({ 61 | console: TaskConfig.log.consoleLevel, 62 | file: TaskConfig.log.fileLevel, 63 | push: TaskConfig.log.pushLevel, 64 | payload: process.env.BILITOOLS_IS_ASYNC && TaskConfig.USERID, 65 | }); 66 | 67 | export const _logger = new Logger({ 68 | console: 'debug', 69 | file: false, 70 | push: false, 71 | payload: 'cat', 72 | }); 73 | 74 | export function notPush() { 75 | return TaskConfig.message.onlyError && !TaskModule.hasError && TaskModule.pushTitle.length === 0; 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/log/std.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from '../env'; 2 | 3 | /** 4 | * 写入 stdout 5 | */ 6 | export function writeOut(message: string) { 7 | process.stdout.write(message); 8 | } 9 | 10 | /** 11 | * 写入 stderr 12 | */ 13 | export function writeError(message: string) { 14 | // fc 不支持输出 stderr 15 | if (ENV.fc) { 16 | process.stdout.write(message); 17 | return; 18 | } 19 | process.stderr.write(message); 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/login.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 扫码登录 3 | */ 4 | export async function scanLogin(filepath?: string) { 5 | const { defLogger } = await import('@/utils/log/def'); 6 | try { 7 | const { pcLogin } = await import('@catlair/blogin'); 8 | const loginInfo = await pcLogin(); 9 | if (!loginInfo) { 10 | defLogger.error('登录失败/取消登录'); 11 | return; 12 | } 13 | defLogger.info('登录成功'); 14 | defLogger.info(JSON.stringify(loginInfo, null, 2)); 15 | // 使用 readConfig 不是为了读取配置,而是为了获取配置文件路径 16 | if (!filepath) { 17 | const { readConfig } = await import('@/config/setConfig'); 18 | readConfig(); 19 | filepath = process.env.__BT_CONFIG_PATH__; 20 | } 21 | if (!filepath) { 22 | defLogger.debug('未找到配置文件,请手动配置'); 23 | // 输出 cookie 24 | defLogger.info(`cookie: ${loginInfo.cookie}`); 25 | return; 26 | } 27 | const { replaceAllCookie } = await import('@/utils/file'); 28 | // 是否替换成功 29 | if (replaceAllCookie(filepath, loginInfo.mid, loginInfo.cookie)) { 30 | defLogger.info('已经自动更新配置文件。'); 31 | return; 32 | } 33 | defLogger.warn('这是一个新的账号,但我们暂时无法修改 json5 文件,故请手动配置'); 34 | defLogger.info(`cookie: ${loginInfo.cookie}`); 35 | defLogger.info(`请将以上信息添加到 ${filepath} 中`); 36 | } catch (error) { 37 | if (error?.message?.includes('Cannot find module')) { 38 | defLogger.error('请先运行 yarn add @catlair/blogin'); 39 | return; 40 | } 41 | defLogger.error(error); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export function resolvePath(str: string) { 4 | return path.resolve(process.cwd(), str); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { AnyProp } from '@/dto/bili-base-prop'; 2 | import { isBoolean } from './is'; 3 | import { emptyLogger, Logger, logger } from './log'; 4 | 5 | export type RequestOptions = { 6 | logger?: Logger | boolean; 7 | name?: string; 8 | successCode?: number; 9 | transform?: boolean; 10 | okMsg?: string; 11 | }; 12 | 13 | /** 14 | * 请求处理 15 | */ 16 | export async function request< 17 | T extends (...args: any[]) => Promise>, 18 | R extends UnPromisify>, 19 | O extends RequestOptions, 20 | >( 21 | reqFunc: T, 22 | options: O = { transform: true } as O, 23 | ...args: Parameters 24 | ): Promise : R['data']> { 25 | const thatlogger = getLogger(options.logger); 26 | const { name, successCode = 0, transform, okMsg } = options; 27 | try { 28 | const resp = await reqFunc(...args); 29 | const { code, message, msg, data } = resp || {}; 30 | if (code !== successCode) { 31 | thatlogger.warn(`${name || reqFunc.name}请求失败:${code} ${message || msg}`); 32 | } 33 | if (okMsg) { 34 | thatlogger.info(okMsg); 35 | } 36 | if (transform === false) { 37 | return resp; 38 | } 39 | return data as any; 40 | } catch (error) { 41 | thatlogger.error(`${name || reqFunc.name}请求出现异常`, error); 42 | } 43 | return {} as Record; 44 | } 45 | 46 | function getLogger(loggerOption: RequestOptions['logger']) { 47 | if (loggerOption === undefined) { 48 | return logger; 49 | } else if (isBoolean(loggerOption)) { 50 | return loggerOption ? logger : emptyLogger; 51 | } else { 52 | return loggerOption; 53 | } 54 | } 55 | 56 | export function getRequestNameWrapper(options: RequestOptions = {}) { 57 | return < 58 | T extends (...args: any[]) => Promise>, 59 | R extends UnPromisify>, 60 | >( 61 | reqFunc: T, 62 | name: string, 63 | ...args: Parameters 64 | ) => request(reqFunc, { ...options, name }, ...args); 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/serverless/index.ts: -------------------------------------------------------------------------------- 1 | import type { FCContext, FCEvent } from '@/types/fc'; 2 | import type { SCFContext, SCFEvent, SLSType } from '@/types'; 3 | import type { SlSOptions } from '@/types/sls'; 4 | import { getPRCDate } from '../pure'; 5 | import { dailyTasks } from '@/task/dailyTask'; 6 | import { JSON5 } from '../json5'; 7 | 8 | interface Params { 9 | event: FCEvent | SCFEvent; 10 | context: FCContext | SCFContext; 11 | slsType: SLSType; 12 | } 13 | 14 | function getPayload(slsType: SLSType, event: Params['event']) { 15 | return slsType === 'scf' ? (event as SCFEvent).Message : (event as FCEvent).payload; 16 | } 17 | 18 | export async function getUpdateTrigger( 19 | slsType: SLSType, 20 | event: any, 21 | ): Promise<(slsOptions?: SlSOptions) => Promise> { 22 | const caller = 23 | slsType === 'scf' 24 | ? (await import('./updateScfTrigger')).default 25 | : (await import('./updateFcTrigger')).default; 26 | return slsOptions => caller(event, slsOptions); 27 | } 28 | 29 | export async function getClinet(slsType: T) { 30 | if (slsType === 'scf') { 31 | return (await import('./updateScfTrigger')).scfClient; 32 | } 33 | return (await import('./updateFcTrigger')).fcClient; 34 | } 35 | 36 | async function initClient(slsType: SLSType, context: SCFContext | FCContext) { 37 | const client = await getClinet(slsType); 38 | if (!client) { 39 | return false; 40 | } 41 | return client.init(context as any); 42 | } 43 | 44 | class DailyHandler { 45 | event: FCEvent | SCFEvent; 46 | context: SCFContext | FCContext; 47 | slsType: SLSType; 48 | payload?: string; 49 | 50 | init({ event, context, slsType }: Params) { 51 | this.context = context; 52 | this.event = event; 53 | this.slsType = slsType; 54 | this.payload = getPayload(slsType, event); 55 | 56 | initClient(slsType, context); 57 | 58 | return this; 59 | } 60 | 61 | async run() { 62 | let message: { lastTime: string }; 63 | try { 64 | if (this.payload) { 65 | message = JSON5.parse(this.payload); 66 | } 67 | } catch {} 68 | 69 | if (message! && message.lastTime === getPRCDate().getDate().toString()) { 70 | return '今日重复执行'; 71 | } 72 | 73 | const updateTrigger = await getUpdateTrigger(this.slsType, this.event); 74 | return await dailyTasks(updateTrigger); 75 | } 76 | } 77 | 78 | export const dailyHandler = new DailyHandler(); 79 | -------------------------------------------------------------------------------- /src/utils/version.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { defHttp } from './http'; 4 | 5 | /** 6 | * 获取最新版本 7 | */ 8 | async function getLatestVersion() { 9 | const options = { 10 | timeout: 6000, 11 | }; 12 | try { 13 | const data = await Promise.any([ 14 | defHttp.get('https://api.github.com/repos/KudouRan/BiliTools/releases/latest', options), 15 | defHttp.get('https://gitee.com/api/v5/repos/KudouRan/BiliTools/releases/latest', options), 16 | ]); 17 | return data.tag_name; 18 | } catch (error) { 19 | return; 20 | } 21 | } 22 | 23 | /** 24 | * 打印版本 25 | */ 26 | export async function printVersion() { 27 | const { logger } = await import('./log'); 28 | let version = '__BILI_VERSION__'; 29 | // 如果 version 被替换,则直接打印 30 | if (version.includes('.')) { 31 | logger.info(`当前版本【__BILI_VERSION__】`); 32 | } else { 33 | version = ''; 34 | } 35 | try { 36 | // 如果没有获取到版本,则尝试获取 37 | if (!version) { 38 | version = 'v' + (getVersionByPkg() || getVersionByFile()); 39 | logger.info(`当前版本【${version}】`); 40 | } 41 | if (!version) { 42 | return; 43 | } 44 | const latestTag = await getLatestVersion(); 45 | if (latestTag && checkVersion(version, latestTag)) { 46 | logger.info(`可更新:最新版本【${latestTag}】`); 47 | } 48 | } catch {} 49 | } 50 | 51 | function getVersionByPkg() { 52 | try { 53 | return require('../../package.json').version; 54 | } catch {} 55 | } 56 | 57 | function getVersionByFile() { 58 | try { 59 | return fs.readFileSync(path.resolve(__dirname, '../version.txt'), 'utf8').trim(); 60 | } catch {} 61 | } 62 | 63 | /** 64 | * 检查版本是否可更新 65 | * @param version 当前版本 66 | * @param latestTag 最新版本 67 | */ 68 | export function checkVersion(version: string, latestTag: string) { 69 | if (version.startsWith('v')) { 70 | version = version.substring(1); 71 | } 72 | if (latestTag.startsWith('v')) { 73 | latestTag = latestTag.substring(1); 74 | } 75 | if (version === latestTag) { 76 | return false; 77 | } 78 | const versionArr = version.split('.').slice(0, 3), 79 | latestTagArr = latestTag.split('.').slice(0, 3); 80 | for (let i = 0; i < versionArr.length; i++) { 81 | const versionNum = parseInt(versionArr[i]), 82 | latestTagNum = parseInt(latestTagArr[i]); 83 | if (isNaN(versionNum) || isNaN(latestTagNum)) { 84 | return true; 85 | } 86 | if (versionNum < latestTagNum) { 87 | return true; 88 | } 89 | if (versionNum > latestTagNum) { 90 | return false; 91 | } 92 | } 93 | return false; 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/ws/index.ts: -------------------------------------------------------------------------------- 1 | import { unzipSync } from 'zlib'; 2 | import { OpType } from '@/enums/packet.enum'; 3 | import { TextDecoder, TextEncoder } from 'util'; 4 | 5 | export interface PacketBody { 6 | cmd: string; 7 | data: T; 8 | } 9 | 10 | export interface Packet { 11 | packetLen: number; 12 | headerLen: number; 13 | ver: number; 14 | op: number; 15 | seq: number; 16 | body: (number | PacketBody)[]; 17 | } 18 | 19 | function str2bytes(str: string) { 20 | return Array.from(new TextEncoder().encode(str)); 21 | } 22 | 23 | function readInt(buffer: Record, start: number, len: number) { 24 | let result = 0; 25 | for (let i = len - 1; i >= 0; i--) { 26 | result += Math.pow(256, len - i - 1) * buffer[start + i]; 27 | } 28 | return result; 29 | } 30 | 31 | export function decode(buffer: Uint8Array) { 32 | const textDecoder = new TextDecoder('utf-8'); 33 | const result: Packet = { 34 | packetLen: readInt(buffer, 0, 4), 35 | headerLen: readInt(buffer, 4, 2), 36 | ver: readInt(buffer, 6, 2), 37 | op: readInt(buffer, 8, 4), 38 | seq: readInt(buffer, 12, 4), 39 | body: [], 40 | }; 41 | 42 | if (result.op === 5) { 43 | result.body = []; 44 | let offset = 0; 45 | while (offset < buffer.length) { 46 | const packetLen = readInt(buffer, offset, 4); 47 | const dataSlice = buffer.slice(offset + 16, offset + packetLen); 48 | let body: string; 49 | if (result.ver === 2) { 50 | body = textDecoder.decode(unzipSync(dataSlice)); 51 | } else { 52 | body = textDecoder.decode(dataSlice); 53 | } 54 | // eslint-disable-next-line no-control-regex 55 | body?.split(/[\x00-\x1f]+/)?.forEach(item => { 56 | try { 57 | result.body.push(JSON.parse(item)); 58 | } catch {} 59 | }); 60 | offset += packetLen; 61 | } 62 | } 63 | return result; 64 | } 65 | 66 | export function getCertification(json: string) { 67 | const bytes = str2bytes(json); // 字符串转bytes 68 | return formatDataView( 69 | { 70 | byteOffset: bytes.length, 71 | op: OpType.认证, 72 | }, 73 | bytes, 74 | ); 75 | } 76 | 77 | interface DataViewOptions { 78 | // 偏移量 79 | byteOffset?: number; 80 | // 协议版本: 0 - 3 (1 心跳及认证包正文不使用压缩) 81 | packVer?: number; 82 | // 操作码(封包类型) 2 心跳包 83 | op?: number; 84 | // sequence 次序 85 | seq?: number; 86 | } 87 | 88 | export function formatDataView( 89 | { byteOffset, packVer = 1, op = OpType.心跳, seq = 1 }: DataViewOptions = {}, 90 | body?: number[], 91 | ) { 92 | let totalSize = 16; 93 | if (body && !byteOffset) { 94 | totalSize += body.length; 95 | } else if (byteOffset) { 96 | totalSize += byteOffset; 97 | } 98 | const dv = new DataView(new ArrayBuffer(totalSize)); 99 | dv.setUint32(0, totalSize); // 封包总大小(头部大小 + 正文大小) 100 | dv.setUint16(4, 16); // 头部大小(一般为0x0010,16字节) 101 | dv.setUint16(6, packVer); // 协议版本: 0 - 3 (1 心跳及认证包正文不使用压缩) 102 | dv.setUint32(8, op); // 操作码(封包类型) 2 心跳包 103 | dv.setUint32(12, seq); // sequence,每次发包时向上递增 104 | body && body.forEach((d, i) => dv.setUint8(16 + i, d)); 105 | return dv.buffer; 106 | } 107 | -------------------------------------------------------------------------------- /tools/bilitools_login.js: -------------------------------------------------------------------------------- 1 | require('@catlair/bilitools').sacnLogin(); 2 | -------------------------------------------------------------------------------- /tools/bilitools_npm.js: -------------------------------------------------------------------------------- 1 | require('@catlair/bilitools').ql(); 2 | -------------------------------------------------------------------------------- /tools/download.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import { dir, files, githubUrl, proxyUrl, tag } from './giteeConfig'; 4 | 5 | async function download(url: string, file: string): Promise { 6 | return new Promise((resolve, reject) => { 7 | const fileStream = fs.createWriteStream(file); 8 | https 9 | .get(url, response => { 10 | response.pipe(fileStream); 11 | fileStream.on('finish', () => { 12 | resolve(); 13 | }); 14 | }) 15 | .on('error', error => { 16 | reject(error); 17 | }); 18 | }); 19 | } 20 | 21 | (async function () { 22 | const requests = files.map(file => { 23 | return download(`${proxyUrl}${githubUrl}${tag}/${file}`, `./${dir}/${file}`); 24 | }); 25 | await Promise.all(requests); 26 | console.log('download success'); 27 | })(); 28 | -------------------------------------------------------------------------------- /tools/giteeConfig.ts: -------------------------------------------------------------------------------- 1 | // 加载 .env 文件 2 | require('dotenv').config(); 3 | 4 | export const files = [ 5 | 'tencent_scf.zip', 6 | 'huawei_agc.zip', 7 | 'baidu_cfc.zip', 8 | 'aliyun_fc.zip', 9 | 'index.js', 10 | 'cat_bili_ql.js', 11 | ]; 12 | 13 | export const dir = 'temp'; 14 | 15 | export const proxyUrl = 'https://ghproxy.com/'; 16 | export const githubUrl = 'https://github.com/KudouRan/BiliTools/releases/download/'; 17 | 18 | export const tag = process.env.TAG; 19 | 20 | export const input = { 21 | username: process.env.GITEE_USERNAME || '', 22 | password: process.env.GITEE_PASSWORD || '', 23 | tag: '', 24 | repo: 'catlair/BiliTools', 25 | files, 26 | prerelease: false, 27 | title: '来自 Github BiliTools 的备份', 28 | description: '来自 Github BiliTools 的备份', 29 | }; 30 | -------------------------------------------------------------------------------- /tools/pkgclean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 用于清理 node_modules 中 package.json 中不必要的字段 3 | * 由于并不清楚第三方包访问了其 package.json 的什么内容(除常见的 version),所以只能大概清理一些 4 | * 测试表明该方式减少的大小微乎其微,完全可以忽略不记 5 | * 本次测试 3.50 降到了 3.42 6 | */ 7 | 8 | const { resolve } = require('path'); 9 | const { readFileSync, readdirSync, statSync, writeFileSync, rmdirSync } = require('fs'); 10 | 11 | const nodeModulesPath = resolve(__dirname, '../node_modules'); 12 | packageClean(nodeModulesPath); 13 | console.log('package.json 清理完成'); 14 | 15 | function writePackageFile(path) { 16 | try { 17 | /** 18 | * 暂时保留这些 19 | */ 20 | const { name, version, author, main } = JSON.parse(readFileSync(path).toString()); 21 | const newPKG = { name, version, author, main }; 22 | writeFileSync(path, JSON.stringify(newPKG)); 23 | } catch {} 24 | } 25 | 26 | function packageClean(rootPath) { 27 | // 找出路径下的所有文件和文件夹 28 | const allPath = readdirSync(rootPath).map(p => resolve(rootPath, p)); 29 | 30 | const dirPath = [], 31 | filePath = []; 32 | allPath.forEach(p => { 33 | // 只处理 package.json 文件 34 | try { 35 | if (statSync(p).isDirectory()) { 36 | !delEmptyDir(p) && dirPath.push(p); 37 | return; 38 | } 39 | if (p.endsWith('package.json')) { 40 | filePath.push(p); 41 | } 42 | } catch (error) { 43 | console.warn(error); 44 | } 45 | }); 46 | 47 | // 处理所有文件 48 | filePath.forEach(file => writePackageFile(file)); 49 | // 递归处理目录 50 | dirPath.forEach(dir => packageClean(dir)); 51 | } 52 | 53 | /** 54 | * 删除空文件夹 55 | * @param {*} dirPath 文件夹路径 56 | */ 57 | function delEmptyDir(dirPath) { 58 | // 判断是否为空文件夹 59 | if (readdirSync(dirPath).length === 0) { 60 | // 删除空文件夹 61 | try { 62 | rmdirSync(dirPath); 63 | return true; 64 | } catch (error) { 65 | console.warn('删除空文件夹失败', error); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tools/processConfig.js: -------------------------------------------------------------------------------- 1 | const { unzipSync } = require('zlib'); 2 | const { writeFileSync, readFileSync, existsSync } = require('fs'); 3 | const { resolve } = require('path'); 4 | 5 | function gzipDecode(str) { 6 | try { 7 | const result = unzipSync(Buffer.from(str, 'base64')).toString(); 8 | try { 9 | return decodeURIComponent(unicode2str(result)); 10 | } catch (error) { 11 | // 兼容百度的在线压缩 12 | return unescape(result); 13 | } 14 | } catch (e) { 15 | process.stderr.write(e); 16 | throw new Error('Error: 当前字符串不能被Gzip解压'); 17 | } 18 | } 19 | 20 | function unicode2str(str) { 21 | return str.replace(/\\u([\d\w]{4})/gi, (_match, grp) => String.fromCodePoint(parseInt(grp, 16))); 22 | } 23 | 24 | /** 25 | * 26 | * @param {String} targetPath 27 | */ 28 | function resolveRootPath(targetPath) { 29 | const rootPath = process.cwd(); 30 | return resolve(rootPath, targetPath); 31 | } 32 | 33 | function getConfig() { 34 | const { BILI_CONFIG, BILITOOLS_CONFIG } = process.env; 35 | if (BILITOOLS_CONFIG || BILI_CONFIG) { 36 | process.stdout.write('使用环境变量 BILITOOLS_CONFIG\n'); 37 | return (BILITOOLS_CONFIG || BILI_CONFIG)?.trim(); 38 | } 39 | const tempTxtPath = resolveRootPath('config/config.txt'); 40 | if (existsSync(tempTxtPath)) { 41 | process.stdout.write('使用本地文件 config.txt\n'); 42 | return readFileSync(tempTxtPath, 'utf-8'); 43 | } 44 | return ''; 45 | } 46 | 47 | function setConfig(gzipString) { 48 | writeFileSync(resolveRootPath('config/config.json'), gzipDecode(gzipString)); 49 | } 50 | 51 | const gzipString = getConfig(); 52 | if (gzipString) { 53 | setConfig(gzipString); 54 | process.stdout.write('配置转化完成\n\n'); 55 | } else { 56 | process.stdout.write('没有可以转化的内容\n\n'); 57 | } 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "files": true, 4 | "compilerOptions": { 5 | "module": "commonjs" 6 | } 7 | }, 8 | "compilerOptions": { 9 | "module": "esnext", 10 | "removeComments": true, 11 | "target": "esnext", 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "paths": { 15 | "@/*": ["./src/*"], 16 | "#/*": ["./src/types/*"] 17 | }, 18 | "lib": ["esnext"], 19 | "skipLibCheck": true, 20 | "moduleResolution": "node", 21 | "esModuleInterop": true, 22 | "strictNullChecks": true 23 | }, 24 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 25 | } 26 | --------------------------------------------------------------------------------