[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"\\U0001F41E Bug report\"\ndescription: Create a report to help us improve\nbody:\n  - type: input\n    id: version\n    attributes:\n      label: Qinglong version\n    validations:\n      required: true\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: Steps to reproduce\n      description: |\n        What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.\n      placeholder: Steps to reproduce\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: What is expected?\n    validations:\n      required: true\n  - type: textarea\n    id: actually-happening\n    attributes:\n      label: What is actually happening?\n    validations:\n      required: true\n  - type: textarea\n    id: system-info\n    attributes:\n      label: System Info\n      description: Output of `npx envinfo --system --binaries --browsers`\n      render: shell\n      placeholder: System, Binaries, Browsers\n  - type: textarea\n    id: additional-comments\n    attributes:\n      label: Any additional comments?\n      description: e.g. some background/context of how you ran into this bug.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Telegram Chat\n    url: https://t.me/jiao_long\n    about: Ask questions and discuss with other Qinglong users in real time.\n  - name: Questions & Discussions\n    url: https://github.com/whyour/qinglong/discussions/new?category=q-a\n    about: Use GitHub discussions for message-board style questions and discussions.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: \"\\U0001F680 New feature proposal\"\ndescription: Suggest an idea for this project\nlabels: [\":sparkles: feature request\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in the project and taking the time to fill out this feature report!\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Clear and concise description of the problem\n      description: \"Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?\"\n    validations:\n      required: true\n  - type: textarea\n    id: suggested-solution\n    attributes:\n      label: Suggested solution\n      description: \"In module [xy] we could provide following implementation...\"\n    validations:\n      required: true\n  - type: textarea\n    id: alternative\n    attributes:\n      label: Alternative\n      description: Clear and concise description of any alternative solutions or features you've considered.\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional context\n      description: Any other context or screenshots about the feature request here.\n  - type: checkboxes\n    id: checkboxes\n    attributes:\n      label: Validations\n      description: Before submitting the issue, please make sure you do the following\n      options:\n        - label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.\n          required: true"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## PR Type\nWhat kind of change does this PR introduce?\n\n<!-- Please check the one that applies to this PR using \"x\". -->\n\n- [ ] Bugfix\n- [ ] Feature\n- [ ] Code style update (formatting, local variables)\n- [ ] Refactoring (no functional changes, no api changes)\n- [ ] Other... Please describe:\n\n\n## What is the current behavior?\n<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->\n\nIssue Number: N/A\n\n\n## What is the new behavior?\n\n\n## Does this PR introduce a breaking change?\n\n- [ ] Yes\n- [ ] No\n\n\n<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->\n\n\n## Other information"
  },
  {
    "path": ".github/agents/ql.agent.md",
    "content": "---\nname: Bug Fixer\ndescription: Fix this issue following our error handling pattern.\n---\n"
  },
  {
    "path": ".github/config.yml",
    "content": "# Comment to be posted to on PRs from first time contributors in your repository\nnewPRWelcomeComment: |\n  💖 Thanks for opening this pull request! 💖\n  Please be patient and we will get back to you as soon as we can.\n# Comment to be posted to on pull requests merged by a first time user\nfirstPRMergeComment: >\n  Congrats on merging your first pull request! 🎉🎉🎉"
  },
  {
    "path": ".github/workflows/build-docker-image.yml",
    "content": "name: Build And Push Docker Image\n\non:\n  push:\n    paths-ignore:\n      - \"*.md\"\n    branches:\n      - \"master\"\n      - \"develop\"\n    tags:\n      - \"v*\"\n  workflow_dispatch:\n\njobs:\n  code_gitlab:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - uses: Yikun/hub-mirror-action@master\n        with:\n          src: github/whyour\n          dst: gitlab/whyour\n          dst_key: ${{ secrets.GITLAB_SSH_PK }}\n          dst_token: ${{ secrets.GITLAB_TOKEN }}\n          static_list: \"qinglong\"\n          force_update: true\n\n  code_gitee:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - uses: Yikun/hub-mirror-action@master\n        with:\n          src: github/whyour\n          dst: gitee/whyour\n          dst_key: ${{ secrets.GITLAB_SSH_PK }}\n          dst_token: ${{ secrets.GITEE_TOKEN }}\n          static_list: \"qinglong\"\n          force_update: true\n\n  build-static:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: pnpm/action-setup@v4\n        with:\n          version: \"8.3.1\"\n\n      - uses: actions/setup-node@v6\n        with:\n          cache: \"pnpm\"\n\n      - name: build front and back\n        run: |\n          pnpm install --frozen-lockfile\n          pnpm build:front\n          pnpm build:back\n\n      - name: copy to static repo\n        env:\n          GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static\n          GITHUB_BRANCH: ${{ github.ref_name }}\n          REPO_GITEE: git@gitee.com:whyour/qinglong-static.git\n          REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git\n          PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}\n        run: |\n          mkdir -p tmp\n          cd ./tmp\n          cp -rf ../static/* ./\n          git init -b ${GITHUB_BRANCH} && git add .\n          git config --local user.name 'github-actions[bot]'\n          git config --local user.email 'github-actions[bot]@users.noreply.github.com'\n          git commit --allow-empty -m \"copy static at $(date +'%Y-%m-%d %H:%M:%S')\"\n          git push --force --quiet \"https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git\" ${GITHUB_BRANCH}:${GITHUB_BRANCH}\n\n  static_gitlab:\n    needs: build-static\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - uses: Yikun/hub-mirror-action@master\n        with:\n          src: github/whyour\n          dst: gitlab/whyour\n          dst_key: ${{ secrets.GITLAB_SSH_PK }}\n          dst_token: ${{ secrets.GITLAB_TOKEN }}\n          static_list: \"qinglong-static\"\n          force_update: true\n\n  static_gitee:\n    needs: build-static\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - uses: Yikun/hub-mirror-action@master\n        with:\n          src: github/whyour\n          dst: gitee/whyour\n          dst_key: ${{ secrets.GITLAB_SSH_PK }}\n          dst_token: ${{ secrets.GITEE_TOKEN }}\n          static_list: \"qinglong-static\"\n          force_update: true\n\n  build:\n    if: ${{ !startsWith(github.ref, 'refs/tags/') }}\n    needs: build-static\n\n    runs-on: ubuntu-22.04\n\n    permissions:\n      packages: write\n      contents: read\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: pnpm/action-setup@v4\n        with:\n          version: \"8.3.1\"\n      - uses: actions/setup-node@v6\n        with:\n          cache: \"pnpm\"\n\n      - name: Read version from version.yaml\n        id: version\n        run: |\n          VERSION=$(grep '^version:' version.yaml | awk '{print $2}')\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Version: $VERSION\"\n\n      - name: Setup timezone\n        uses: szenius/set-timezone@v2.0\n        with:\n          timezoneLinux: Asia/Shanghai\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Login to GHCR\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ${{ github.repository }}\n            ghcr.io/${{ github.repository }}\n          flavor: |\n            latest=false\n          tags: |\n            type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}\n            type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}\n            type=raw,value=${{ steps.version.outputs.version }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }}\n            type=semver,pattern={{version}}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v6\n        with:\n          build-args: |\n            MAINTAINER=${{ github.repository_owner }}\n            QL_BRANCH=${{ github.ref_name }}\n            SOURCE_COMMIT=${{ github.sha }}\n          network: host\n          # linux/s390x npm 暂不可用\n          platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386\n          context: .\n          file: ./docker/Dockerfile\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=registry,ref=whyour/qinglong:cache\n          cache-to: type=registry,ref=whyour/qinglong:cache,mode=max\n\n      - name: Image digest\n        run: |\n          echo ${{ steps.docker_build.outputs.digest }}\n\n  build310:\n    if: ${{ github.ref_name == 'master' }}\n    needs: build-static\n\n    runs-on: ubuntu-22.04\n\n    permissions:\n      packages: write\n      contents: read\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: pnpm/action-setup@v4\n        with:\n          version: \"8.3.1\"\n      - uses: actions/setup-node@v6\n        with:\n          cache: \"pnpm\"\n\n      - name: Read version from version.yaml\n        id: version\n        run: |\n          VERSION=$(grep '^version:' version.yaml | awk '{print $2}')\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Version: $VERSION\"\n\n      - name: Setup timezone\n        uses: szenius/set-timezone@v2.0\n        with:\n          timezoneLinux: Asia/Shanghai\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Login to GHCR\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push python3.10\n        id: docker_build_310\n        uses: docker/build-push-action@v6\n        with:\n          build-args: |\n            MAINTAINER=${{ github.repository_owner }}\n            QL_BRANCH=${{ github.ref_name }}\n            SOURCE_COMMIT=${{ github.sha }}\n          network: host\n          # linux/s390x npm 暂不可用\n          platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386\n          context: .\n          file: ./docker/310.Dockerfile\n          push: true\n          tags: |\n            whyour/qinglong:python3.10\n            whyour/qinglong:${{ steps.version.outputs.version }}-python3.10\n          cache-from: type=registry,ref=whyour/qinglong:cache-python3.10\n          cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max\n\n      - name: Image digest\n        run: |\n          echo ${{ steps.docker_build_310.outputs.digest }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/npm-debug.log*\n/yarn-error.log\n/yarn.lock\n/package-lock.json\n\n# production\n/static\n/data\n\n# misc\n.DS_Store\n\n# umi\n/src/.umi\n/src/.umi-production\n/src/.umi-test\n/.env.local\n.env\n.history\n.version.ts\n/.tmp\n__pycache__\n/shell/preload/env.*\n/shell/preload/notify.*\n/shell/preload/*-notify.json\n/shell/preload/__ql_notify__.*\n"
  },
  {
    "path": ".npmrc",
    "content": "strict-peer-dependencies=false"
  },
  {
    "path": ".prettierignore",
    "content": "**/*.md\n**/*.svg\n**/*.ejs\n**/*.html\n/.umi\n/.umi-production\n/.umi-test\n/.history\n/.tmp\n/node_modules\nnpm-debug.log*\nyarn-error.log\nyarn.lock\npackage-lock.json\n/static\n/data\nDS_Store\n/src/.umi\n/src/.umi-production\n/src/.umi-test\n.env.local\n.env\nversion.ts\n/.tmp"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 80,\n  \"overrides\": [\n    {\n      \"files\": \".prettierrc\",\n      \"options\": { \"parser\": \"json\" }\n    }\n  ]\n}\n"
  },
  {
    "path": ".umirc.ts",
    "content": "import { defineConfig } from '@umijs/max';\nconst CompressionPlugin = require('compression-webpack-plugin');\n\nconst baseUrl = process.env.QlBaseUrl || '/';\nexport default defineConfig({\n  hash: true,\n  jsMinifier: 'terser',\n  antd: {},\n  locale: {\n    antd: true,\n    title: true,\n    baseNavigator: true,\n  },\n  outputPath: 'static/dist',\n  fastRefresh: true,\n  favicons: [`https://qn.whyour.cn/favicon.svg`],\n  publicPath: process.env.NODE_ENV === 'production' ? './' : '/',\n  proxy: {\n    [`${baseUrl}api`]: {\n      target: 'http://127.0.0.1:5700/',\n      changeOrigin: true,\n      ws: true,\n      pathRewrite: { [`^${baseUrl}api`]: '/api' },\n    },\n  },\n  chainWebpack: ((config: any) => {\n    config.plugin('compression-webpack-plugin').use(\n      new CompressionPlugin({\n        algorithm: 'gzip',\n        test: new RegExp('\\\\.(js|css)$'),\n        threshold: 10240,\n        minRatio: 0.6,\n      }),\n    );\n  }) as any,\n  headScripts: [`./api/env.js`],\n  copy: [\n    {\n      from: 'node_modules/monaco-editor/min/vs',\n      to: 'static/dist/monaco-editor/min/vs',\n    },\n  ],\n  npmClient: 'pnpm',\n});\n"
  },
  {
    "path": "LICENSE",
    "content": "                              Apache License\n                        Version 2.0, January 2004\n                    http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n  \"License\" shall mean the terms and conditions for use, reproduction,\n  and distribution as defined by Sections 1 through 9 of this document.\n\n  \"Licensor\" shall mean the copyright owner or entity authorized by\n  the copyright owner that is granting the License.\n\n  \"Legal Entity\" shall mean the union of the acting entity and all\n  other entities that control, are controlled by, or are under common\n  control with that entity. For the purposes of this definition,\n  \"control\" means (i) the power, direct or indirect, to cause the\n  direction or management of such entity, whether by contract or\n  otherwise, or (ii) ownership of fifty percent (50%) or more of the\n  outstanding shares, or (iii) beneficial ownership of such entity.\n\n  \"You\" (or \"Your\") shall mean an individual or Legal Entity\n  exercising permissions granted by this License.\n\n  \"Source\" form shall mean the preferred form for making modifications,\n  including but not limited to software source code, documentation\n  source, and configuration files.\n\n  \"Object\" form shall mean any form resulting from mechanical\n  transformation or translation of a Source form, including but\n  not limited to compiled object code, generated documentation,\n  and conversions to other media types.\n\n  \"Work\" shall mean the work of authorship, whether in Source or\n  Object form, made available under the License, as indicated by a\n  copyright notice that is included in or attached to the work\n  (an example is provided in the Appendix below).\n\n  \"Derivative Works\" shall mean any work, whether in Source or Object\n  form, that is based on (or derived from) the Work and for which the\n  editorial revisions, annotations, elaborations, or other modifications\n  represent, as a whole, an original work of authorship. For the purposes\n  of this License, Derivative Works shall not include works that remain\n  separable from, or merely link (or bind by name) to the interfaces of,\n  the Work and Derivative Works thereof.\n\n  \"Contribution\" shall mean any work of authorship, including\n  the original version of the Work and any modifications or additions\n  to that Work or Derivative Works thereof, that is intentionally\n  submitted to Licensor for inclusion in the Work by the copyright owner\n  or by an individual or Legal Entity authorized to submit on behalf of\n  the copyright owner. For the purposes of this definition, \"submitted\"\n  means any form of electronic, verbal, or written communication sent\n  to the Licensor or its representatives, including but not limited to\n  communication on electronic mailing lists, source code control systems,\n  and issue tracking systems that are managed by, or on behalf of, the\n  Licensor for the purpose of discussing and improving the Work, but\n  excluding communication that is conspicuously marked or otherwise\n  designated in writing by the copyright owner as \"Not a Contribution.\"\n\n  \"Contributor\" shall mean Licensor and any individual or Legal Entity\n  on behalf of whom a Contribution has been received by Licensor and\n  subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n  this License, each Contributor hereby grants to You a perpetual,\n  worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n  copyright license to reproduce, prepare Derivative Works of,\n  publicly display, publicly perform, sublicense, and distribute the\n  Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n  this License, each Contributor hereby grants to You a perpetual,\n  worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n  (except as stated in this section) patent license to make, have made,\n  use, offer to sell, sell, import, and otherwise transfer the Work,\n  where such license applies only to those patent claims licensable\n  by such Contributor that are necessarily infringed by their\n  Contribution(s) alone or by combination of their Contribution(s)\n  with the Work to which such Contribution(s) was submitted. If You\n  institute patent litigation against any entity (including a\n  cross-claim or counterclaim in a lawsuit) alleging that the Work\n  or a Contribution incorporated within the Work constitutes direct\n  or contributory patent infringement, then any patent licenses\n  granted to You under this License for that Work shall terminate\n  as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n  Work or Derivative Works thereof in any medium, with or without\n  modifications, and in Source or Object form, provided that You\n  meet the following conditions:\n\n  (a) You must give any other recipients of the Work or\n      Derivative Works a copy of this License; and\n\n  (b) You must cause any modified files to carry prominent notices\n      stating that You changed the files; and\n\n  (c) You must retain, in the Source form of any Derivative Works\n      that You distribute, all copyright, patent, trademark, and\n      attribution notices from the Source form of the Work,\n      excluding those notices that do not pertain to any part of\n      the Derivative Works; and\n\n  (d) If the Work includes a \"NOTICE\" text file as part of its\n      distribution, then any Derivative Works that You distribute must\n      include a readable copy of the attribution notices contained\n      within such NOTICE file, excluding those notices that do not\n      pertain to any part of the Derivative Works, in at least one\n      of the following places: within a NOTICE text file distributed\n      as part of the Derivative Works; within the Source form or\n      documentation, if provided along with the Derivative Works; or,\n      within a display generated by the Derivative Works, if and\n      wherever such third-party notices normally appear. The contents\n      of the NOTICE file are for informational purposes only and\n      do not modify the License. You may add Your own attribution\n      notices within Derivative Works that You distribute, alongside\n      or as an addendum to the NOTICE text from the Work, provided\n      that such additional attribution notices cannot be construed\n      as modifying the License.\n\n  You may add Your own copyright statement to Your modifications and\n  may provide additional or different license terms and conditions\n  for use, reproduction, or distribution of Your modifications, or\n  for any such Derivative Works as a whole, provided Your use,\n  reproduction, and distribution of the Work otherwise complies with\n  the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n  any Contribution intentionally submitted for inclusion in the Work\n  by You to the Licensor shall be under the terms and conditions of\n  this License, without any additional terms or conditions.\n  Notwithstanding the above, nothing herein shall supersede or modify\n  the terms of any separate license agreement you may have executed\n  with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n  names, trademarks, service marks, or product names of the Licensor,\n  except as required for reasonable and customary use in describing the\n  origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n  agreed to in writing, Licensor provides the Work (and each\n  Contributor provides its Contributions) on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n  implied, including, without limitation, any warranties or conditions\n  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n  PARTICULAR PURPOSE. You are solely responsible for determining the\n  appropriateness of using or redistributing the Work and assume any\n  risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n  whether in tort (including negligence), contract, or otherwise,\n  unless required by applicable law (such as deliberate and grossly\n  negligent acts) or agreed to in writing, shall any Contributor be\n  liable to You for damages, including any direct, indirect, special,\n  incidental, or consequential damages of any character arising as a\n  result of this License or out of the use or inability to use the\n  Work (including but not limited to damages for loss of goodwill,\n  work stoppage, computer failure or malfunction, or any and all\n  other commercial damages or losses), even if such Contributor\n  has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n  the Work or Derivative Works thereof, You may choose to offer,\n  and charge a fee for, acceptance of support, warranty, indemnity,\n  or other liability obligations and/or rights consistent with this\n  License. However, in accepting such obligations, You may act only\n  on Your own behalf and on Your sole responsibility, not on behalf\n  of any other Contributor, and only if You agree to indemnify,\n  defend, and hold each Contributor harmless for any liability\n  incurred by, or claims asserted against, such Contributor by reason\n  of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n  To apply the Apache License to your work, attach the following\n  boilerplate notice, with the fields enclosed by brackets \"{}\"\n  replaced with your own identifying information. (Don't include\n  the brackets!)  The text should be enclosed in the appropriate\n  comment syntax for the file format. We also recommend that a\n  file or class name and description of purpose be included on the\n  same \"printed page\" as the copyright notice for easier\n  identification within third-party archives.\n\nCopyright 2021 WHYOUR <https://github.com/whyour>.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "README-en.md",
    "content": "<div align=\"center\">\n<img width=\"100\" src=\"https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png\">\n\n<h1 align=\"center\">Qinglong</h1>\n\n[简体中文](./README.md) | English\n\nTimed task management platform supporting Python3, JavaScript, Shell, Typescript\n\n[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]\n\n[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat\n[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme\n[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat\n[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong\n[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat\n[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong\n[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat\n[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong\n\n[Demo](http://demo.qinglong.online:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)\n\n[演示](http://demo.qinglong.online:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)\n</div>\n\n![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)\n\n## Features\n\n- Support for multiple scripting languages (python3, javaScript, shell, typescript)\n- Support online management of scripts, environment variables, configuration files\n- Support online view task log\n- Support second-level task setting\n- Support system level notification\n- Support dark mode\n- Support cell phone operation\n\n## Version\n\n### docker\n\nThe `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.\n\n**⚠️ Important**: If you need to run Docker as a **non-root user**, please use the `debian` image. Alpine's `crond` requires root privileges.\n\n```bash\ndocker pull whyour/qinglong:latest\ndocker pull whyour/qinglong:debian\n```\n\n### npm\n\nThe npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed.\n\n```bash\nnpm i @whyour/qinglong\n```\n\n## Deployment\n\n[View Documentation](https://qinglong.online/guide/getting-started/installation-guide)\n\n## Built-in API\n\n[View Documentation](https://qinglong.online/guide/user-guide/built-in-api)\n\n## Built-in commands\n\n[View Documentation](https://qinglong.online/guide/user-guide/basic-explanation)\n\n## Development\n\n```bash\ngit clone https://github.com/whyour/qinglong.git\ncd qinglong\ncp .env.example .env\n# Recommended use pnpm https://pnpm.io/zh/installation\nnpm install -g pnpm@8.3.1\npnpm install\npnpm start\n```\n\nOpen your browser and visit <http://127.0.0.1:5700>\n\n## Links\n\n- [nevinee](https://gitee.com/evine)\n- [crontab-ui](https://github.com/alseambusher/crontab-ui)\n- [Ant Design](https://ant.design)\n- [Ant Design Pro](https://pro.ant.design/)\n- [Umijs](https://umijs.org)\n- [darkreader](https://github.com/darkreader/darkreader)\n- [admin-server](https://github.com/sunpu007/admin-server)\n\n## Name Origin\n\nThe Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), \"a true dragon is one that has complementary wings\".\n\nIn the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: \"The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)\n\nAmong the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as \"Mengzhang\" and in different Taoist scriptures as \"Dijun\", \"Shengjian\", \"Shenjian\" and He is also known in different Daoist scriptures as \"Dijun\", \"Shengjun\", \"Shenjun\" and \"Ghost Catcher\"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<img width=\"100\" src=\"https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png\">\n\n<h1 align=\"center\">青龙</h1>\n\n简体中文 | [English](./README-en.md)\n\n支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台\n\nTimed task management platform supporting Python3, JavaScript, Shell, Typescript\n\n[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]\n\n[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat\n[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme\n[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat\n[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong\n[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat\n[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong\n[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat\n[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong\n\n[Demo](http://demo.qinglong.online:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)\n\n[演示](http://demo.qinglong.online:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)\n</div>\n\n![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)\n\n## 功能\n\n- 支持多种脚本语言（python3、javaScript、shell、typescript）\n- 支持在线管理脚本、环境变量、配置文件\n- 支持在线查看任务日志\n- 支持秒级任务设置\n- 支持系统级通知\n- 支持暗黑模式\n- 支持手机端操作\n\n## 版本\n\n### docker\n\n`latest` 镜像是基于 `alpine` 构建，`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖，建议使用 `debian` 镜像\n\n**⚠️ 重要提示**: 如果您需要以**非 root 用户**运行 Docker，请使用 `debian` 镜像。Alpine 的 `crond` 需要 root 权限。\n\n```bash\ndocker pull whyour/qinglong:latest\ndocker pull whyour/qinglong:debian\n```\n\n### npm\n\nnpm 版本支持 `debian/ubuntu/alpine` 系统，需要自行安装 `node/npm/python3/pip3/pnpm`\n\n```bash\nnpm i @whyour/qinglong\n```\n\n## 部署\n\n[查看文档](https://qinglong.online/guide/getting-started/installation-guide)\n\n## 内置 API\n\n[查看文档](https://qinglong.online/guide/user-guide/built-in-api)\n\n## 内置命令\n\n[查看文档](https://qinglong.online/guide/user-guide/basic-explanation)\n\n## 开发\n\n```bash\ngit clone https://github.com/whyour/qinglong.git\ncd qinglong\ncp .env.example .env\n# 推荐使用 pnpm https://pnpm.io/zh/installation\nnpm install -g pnpm@8.3.1\npnpm install\npnpm start\n```\n\n打开你的浏览器，访问 <http://127.0.0.1:5700>\n\n## 链接\n\n- [nevinee](https://gitee.com/evine)\n- [crontab-ui](https://github.com/alseambusher/crontab-ui)\n- [Ant Design](https://ant.design)\n- [Ant Design Pro](https://pro.ant.design/)\n- [Umijs](https://umijs.org)\n- [darkreader](https://github.com/darkreader/darkreader)\n- [admin-server](https://github.com/sunpu007/admin-server)\n\n## 名称来源\n\n青龙，又名苍龙，在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一，根据五行学说，它是代表东方的灵兽，为青色的龙，五行属木，代表的季节是春季，八卦主震。苍龙与应龙一样，都是身具羽翼。《张果星宗》称“又有辅翼，方为真龙”。\n\n《后汉书·律历志下》记载：日周于天，一寒一暑，四时备成，万物毕改，摄提迁次，青龙移辰，谓之岁。\n\n在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中，青龙是东方七宿（角、亢、氐、房、心、尾、箕）的总称。 在早期星宿信仰中，祂是最尊贵的天神。 但被道教信仰吸纳入其神系后，神格大跌，道教将其称为“孟章”，在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼，与白虎监兵神君一起，是道教的护卫天神。\n"
  },
  {
    "path": "SECURITY.md",
    "content": "## Reporting a Vulnerability\n\nTo report a vulnerability, please open a private vulnerability report at <https://github.com/whyour/qinglong/security>.\n\nWhile the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Qinglong to ensure your application remains as secure as possible.\n"
  },
  {
    "path": "back/api/config.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport config from '../config';\nimport * as fs from 'fs/promises';\nimport { celebrate, Joi } from 'celebrate';\nimport { join } from 'path';\nimport { SAMPLE_FILES } from '../config/const';\nimport ConfigService from '../services/config';\nimport { writeFileWithLock } from '../shared/utils';\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/configs', route);\n\n  route.get(\n    '/sample',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        res.send({\n          code: 200,\n          data: SAMPLE_FILES,\n        });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/files',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const fileList = await fs.readdir(config.configPath, 'utf-8');\n        res.send({\n          code: 200,\n          data: fileList\n            .filter((x) => !config.blackFileList.includes(x))\n            .map((x) => {\n              return { title: x, value: x };\n            }),\n        });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/detail',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const configService = Container.get(ConfigService);\n        await configService.getFile(req.query.path as string, res);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/save',\n    celebrate({\n      body: Joi.object({\n        name: Joi.string().required(),\n        content: Joi.string().allow('').optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const { name, content } = req.body;\n        if (config.blackFileList.includes(name)) {\n          res.send({ code: 403, message: '文件无法访问' });\n        }\n        let path = join(config.configPath, name);\n        if (name.startsWith('data/scripts/')) {\n          path = join(config.rootPath, name);\n        }\n        await writeFileWithLock(path, content);\n        res.send({ code: 200, message: '保存成功' });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:file',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const configService = Container.get(ConfigService);\n        await configService.getFile(req.params.file, res);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/cron.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport CronService from '../services/cron';\nimport CronViewService from '../services/cronView';\nimport { celebrate, Joi } from 'celebrate';\nimport { commonCronSchema } from '../validation/schedule';\n\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/crons', route);\n\n  route.get(\n    '/views',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const cronViewService = Container.get(CronViewService);\n        const data = await cronViewService.list();\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/views',\n    celebrate({\n      body: Joi.object({\n        name: Joi.string().required(),\n        sorts: Joi.array().optional().allow(null),\n        filters: Joi.array().optional(),\n        filterRelation: Joi.string().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const cronViewService = Container.get(CronViewService);\n        const data = await cronViewService.create(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/views',\n    celebrate({\n      body: Joi.object({\n        name: Joi.string().required(),\n        id: Joi.number().required(),\n        sorts: Joi.array().optional().allow(null),\n        filters: Joi.array().optional(),\n        filterRelation: Joi.string().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const cronViewService = Container.get(CronViewService);\n        if (req.body.type === 1) {\n          return res.send({ code: 400, message: '参数错误' });\n        } else {\n          const data = await cronViewService.update(req.body);\n          return res.send({ code: 200, data });\n        }\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/views',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const cronViewService = Container.get(CronViewService);\n        const data = await cronViewService.remove(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/views/move',\n    celebrate({\n      body: Joi.object({\n        fromIndex: Joi.number().required(),\n        toIndex: Joi.number().required(),\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      try {\n        const cronViewService = Container.get(CronViewService);\n        const data = await cronViewService.move(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/views/disable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronViewService = Container.get(CronViewService);\n        const data = await cronViewService.disabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/views/enable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronViewService = Container.get(CronViewService);\n        const data = await cronViewService.enabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get('/', async (req: Request, res: Response, next: NextFunction) => {\n    const logger: Logger = Container.get('logger');\n    try {\n      const cronService = Container.get(CronService);\n      const data = await cronService.crontabs(req.query as any);\n      return res.send({ code: 200, data });\n    } catch (e) {\n      logger.error('🔥 error: %o', e);\n      return next(e);\n    }\n  });\n\n  route.get(\n    '/detail',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.find(req.query as any);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        logger.error('🔥 error: %o', e);\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/',\n    celebrate({\n      body: Joi.object(commonCronSchema),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.create(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/run',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.run(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/stop',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.stop(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/labels',\n    celebrate({\n      body: Joi.object({\n        ids: Joi.array().items(Joi.number().required()),\n        labels: Joi.array().items(Joi.string().required()),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.removeLabels(\n          req.body.ids,\n          req.body.labels,\n        );\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/labels',\n    celebrate({\n      body: Joi.object({\n        ids: Joi.array().items(Joi.number().required()),\n        labels: Joi.array().items(Joi.string().required()),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.addLabels(req.body.ids, req.body.labels);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/disable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.disabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/enable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.enabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id/log',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.log(req.params.id);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/',\n    celebrate({\n      body: Joi.object({\n        ...commonCronSchema,\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.update(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.remove(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/pin',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.pin(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/unpin',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.unPin(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/import',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.importCrontab();\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.getDb({ id: req.params.id });\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/status',\n    celebrate({\n      body: Joi.object({\n        ids: Joi.array().items(Joi.number().required()),\n        status: Joi.string().required(),\n        pid: Joi.string().optional().allow(null),\n        log_path: Joi.string().optional().allow(null),\n        last_running_time: Joi.number().optional().allow(null),\n        last_execution_time: Joi.number().optional().allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.status({\n          ...req.body,\n          status: req.body.status ? parseInt(req.body.status) : undefined,\n          pid: req.body.pid ? parseInt(req.body.pid) : undefined,\n        });\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id/logs',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const cronService = Container.get(CronService);\n        const data = await cronService.logs(req.params.id);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/dependence.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport DependenceService from '../services/dependence';\nimport { Logger } from 'winston';\nimport { celebrate, Joi } from 'celebrate';\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/dependencies', route);\n\n  route.get(\n    '/',\n    celebrate({\n      query: \n        Joi.object({\n          searchValue: Joi.string().optional().allow(''),\n          type: Joi.string().optional().allow(''),\n          status: Joi.string().optional().allow(''),\n        }).unknown(true),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.dependencies(req.query as any);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        logger.error('🔥 error: %o', e);\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/',\n    celebrate({\n      body: Joi.array().items(\n        Joi.object({\n          name: Joi.string().required(),\n          type: Joi.number().required(),\n          remark: Joi.string().optional().allow(''),\n        }),\n      ),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.create(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/',\n    celebrate({\n      body: Joi.object({\n        name: Joi.string().required(),\n        id: Joi.number().required(),\n        type: Joi.number().required(),\n        remark: Joi.string().optional().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.update(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.remove(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/force',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.remove(req.body, true);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.getDb({ id: req.params.id });\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/reinstall',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        const data = await dependenceService.reInstall(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/cancel',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const dependenceService = Container.get(DependenceService);\n        await dependenceService.cancel(req.body);\n        return res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/env.ts",
    "content": "import { Joi, celebrate } from 'celebrate';\nimport { NextFunction, Request, Response, Router } from 'express';\nimport fs from 'fs';\nimport multer from 'multer';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport config from '../config';\nimport { safeJSONParse } from '../config/util';\nimport EnvService from '../services/env';\nconst route = Router();\n\nconst storage = multer.diskStorage({\n  destination: function (req, file, cb) {\n    cb(null, config.scriptPath);\n  },\n  filename: function (req, file, cb) {\n    cb(null, file.originalname);\n  },\n});\nconst upload = multer({ storage: storage });\n\nexport default (app: Router) => {\n  app.use('/envs', route);\n\n  route.get('/', async (req: Request, res: Response, next: NextFunction) => {\n    const logger: Logger = Container.get('logger');\n    try {\n      const envService = Container.get(EnvService);\n      const data = await envService.envs(req.query.searchValue as string);\n      return res.send({ code: 200, data });\n    } catch (e) {\n      logger.error('🔥 error: %o', e);\n      return next(e);\n    }\n  });\n\n  route.post(\n    '/',\n    celebrate({\n      body: Joi.array().items(\n        Joi.object({\n          value: Joi.string().required(),\n          name: Joi.string()\n            .required()\n            .pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),\n          remarks: Joi.string().optional().allow(''),\n        }),\n      ),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        if (!req.body?.length) {\n          return res.send({ code: 400, message: '参数不正确' });\n        }\n        const data = await envService.create(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/',\n    celebrate({\n      body: Joi.object({\n        value: Joi.string().required(),\n        name: Joi.string().required(),\n        remarks: Joi.string().optional().allow('').allow(null),\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.update(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.remove(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/:id/move',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n      body: Joi.object({\n        fromIndex: Joi.number().required(),\n        toIndex: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.move(req.params.id, req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/disable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.disabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/enable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.enabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/name',\n    celebrate({\n      body: Joi.object({\n        ids: Joi.array().items(Joi.number().required()),\n        name: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.updateNames(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.getDb({ id: req.params.id });\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/pin',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.pin(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/unpin',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const data = await envService.unPin(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/upload',\n    upload.single('env'),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const envService = Container.get(EnvService);\n        const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');\n        const parseContent = safeJSONParse(fileContent);\n        const data = Array.isArray(parseContent)\n          ? parseContent\n          : [parseContent];\n        if (data.every((x) => x.name && x.value)) {\n          const result = await envService.create(\n            data.map((x) => ({\n              name: x.name,\n              value: x.value,\n              remarks: x.remarks,\n            })),\n          );\n          return res.send({ code: 200, data: result });\n        } else {\n          return res.send({\n            code: 400,\n            message: '每条数据 name 或者 value 字段不能为空，参考导出文件格式',\n          });\n        }\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/health.ts",
    "content": "import { Router } from 'express';\nimport Logger from '../loaders/logger';\nimport { HealthService } from '../services/health';\nimport Container from 'typedi';\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/', route);\n\n  route.get('/health', async (req, res) => {\n    try {\n      const healthService = Container.get(HealthService);\n      const health = await healthService.check();\n      res.status(200).send({\n        code: 200,\n        data: health,\n      });\n    } catch (err: any) {\n      Logger.error('Health check failed:', err);\n      res.status(500).send({\n        code: 500,\n        message: 'Health check failed',\n        error: err.message,\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "back/api/index.ts",
    "content": "import { Router } from 'express';\nimport user from './user';\nimport env from './env';\nimport config from './config';\nimport log from './log';\nimport cron from './cron';\nimport script from './script';\nimport open from './open';\nimport dependence from './dependence';\nimport system from './system';\nimport subscription from './subscription';\nimport update from './update';\nimport health from './health';\n\nexport default () => {\n  const app = Router();\n  user(app);\n  env(app);\n  config(app);\n  log(app);\n  cron(app);\n  script(app);\n  open(app);\n  dependence(app);\n  system(app);\n  subscription(app);\n  update(app);\n  health(app);\n\n  return app;\n};\n"
  },
  {
    "path": "back/api/log.ts",
    "content": "import { celebrate, Joi } from 'celebrate';\nimport { NextFunction, Request, Response, Router } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport config from '../config';\nimport {\n  getFileContentByName,\n  readDirs,\n  removeAnsi,\n  rmPath,\n} from '../config/util';\nimport LogService from '../services/log';\nconst route = Router();\nconst blacklist = ['.tmp'];\n\nexport default (app: Router) => {\n  app.use('/logs', route);\n\n  route.get('/', async (req: Request, res: Response, next: NextFunction) => {\n    const logger: Logger = Container.get('logger');\n    try {\n      const result = await readDirs(config.logPath, config.logPath, blacklist);\n      res.send({\n        code: 200,\n        data: result,\n      });\n    } catch (e) {\n      logger.error('🔥 error: %o', e);\n      return next(e);\n    }\n  });\n\n  route.get(\n    '/detail',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const logService = Container.get(LogService);\n        const finalPath = logService.checkFilePath(\n          (req.query.path as string) || '',\n          (req.query.file as string) || '',\n        );\n        if (!finalPath || blacklist.includes(req.query.path as string)) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        const content = await getFileContentByName(finalPath);\n        res.send({ code: 200, data: removeAnsi(content) });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:file',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const logService = Container.get(LogService);\n        const finalPath = logService.checkFilePath(\n          (req.query.path as string) || '',\n          (req.params.file as string) || '',\n        );\n        if (!finalPath || blacklist.includes(req.query.path as string)) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        const content = await getFileContentByName(finalPath);\n        res.send({ code: 200, data: content });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().allow(''),\n        type: Joi.string().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path } = req.body as {\n          filename: string;\n          path: string;\n        };\n        const logService = Container.get(LogService);\n        const finalPath = logService.checkFilePath(path, filename);\n        if (!finalPath || blacklist.includes(path)) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        await rmPath(finalPath);\n        res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/download',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path } = req.body as {\n          filename: string;\n          path: string;\n        };\n        const logService = Container.get(LogService);\n        const filePath = logService.checkFilePath(path, filename);\n        if (!filePath) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        return res.download(filePath, filename, (err) => {\n          if (err) {\n            return next(err);\n          }\n        });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/open.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport OpenService from '../services/open';\nimport { Logger } from 'winston';\nimport { celebrate, Joi } from 'celebrate';\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/', route);\n  route.get(\n    '/apps',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const openService = Container.get(OpenService);\n        const data = await openService.list();\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/apps',\n    celebrate({\n      body: Joi.object({\n        name: Joi.string().optional().allow('').disallow('system'),\n        scopes: Joi.array().items(Joi.string().required()),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const openService = Container.get(OpenService);\n        const data = await openService.create(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/apps',\n    celebrate({\n      body: Joi.object({\n        name: Joi.string().optional().allow(''),\n        scopes: Joi.array().items(Joi.string()),\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const openService = Container.get(OpenService);\n        const data = await openService.update(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/apps',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const openService = Container.get(OpenService);\n        const data = await openService.remove(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/apps/:id/reset-secret',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const openService = Container.get(OpenService);\n        const data = await openService.resetSecret(req.params.id);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/auth/token',\n    celebrate({\n      query: {\n        client_id: Joi.string().required(),\n        client_secret: Joi.string().required(),\n      },\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const openService = Container.get(OpenService);\n        const result = await openService.authToken(req.query as any);\n        return res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/script.ts",
    "content": "import { fileExist, readDirs, readDir, rmPath, IFile } from '../config/util';\nimport { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport config from '../config';\nimport * as fs from 'fs/promises';\nimport { celebrate, Joi } from 'celebrate';\nimport path, { join, parse } from 'path';\nimport ScriptService from '../services/script';\nimport multer from 'multer';\nimport { writeFileWithLock } from '../shared/utils';\nconst route = Router();\n\nconst storage = multer.diskStorage({\n  destination: function (req, file, cb) {\n    cb(null, config.scriptPath);\n  },\n  filename: function (req, file, cb) {\n    cb(null, file.originalname);\n  },\n});\nconst upload = multer({ storage: storage });\n\nexport default (app: Router) => {\n  app.use('/scripts', route);\n\n  route.get(\n    '/',\n    celebrate({\n      query: Joi.object({\n        path: Joi.string().optional().allow(''),\n      }).unknown(true),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        let result: IFile[] = [];\n        const blacklist = [\n          'node_modules',\n          '.git',\n          '.pnpm',\n          'pnpm-lock.yaml',\n          'yarn.lock',\n          'package-lock.json',\n        ];\n        if (req.query.path) {\n          result = await readDir(\n            req.query.path as string,\n            config.scriptPath,\n            blacklist,\n          );\n        } else {\n          result = await readDirs(\n            config.scriptPath,\n            config.scriptPath,\n            blacklist,\n            (a, b) => {\n              if (a.type === b.type) {\n                return a.title.localeCompare(b.title);\n              } else {\n                return a.type === 'directory' ? -1 : 1;\n              }\n            },\n          );\n        }\n        res.send({\n          code: 200,\n          data: result,\n        });\n      } catch (e) {\n        logger.error('🔥 error: %o', e);\n        return next(e);\n      }\n    });\n\n  route.get(\n    '/detail',\n    celebrate({\n      query: Joi.object({\n        path: Joi.string().optional().allow(''),\n        file: Joi.string().required(),\n      }).unknown(true),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const scriptService = Container.get(ScriptService);\n        const content = await scriptService.getFile(\n          req.query?.path as string || '',\n          req.query.file as string,\n        );\n        res.send({ code: 200, data: content });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:file',\n    celebrate({\n      params: Joi.object({\n        file: Joi.string().required(),\n      }),\n      query: Joi.object({\n        path: Joi.string().optional().allow(''),\n      }).unknown(true),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const scriptService = Container.get(ScriptService);\n        const content = await scriptService.getFile(\n          req.query?.path as string || '',\n          req.params.file,\n        );\n        res.send({ code: 200, data: content });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/',\n    upload.single('file'),\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().optional().allow(''),\n        content: Joi.string().optional().allow(''),\n        originFilename: Joi.string().optional().allow(''),\n        directory: Joi.string().optional().allow(''),\n        file: Joi.string().optional().allow(''),\n      }).unknown(true),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path, content, originFilename, directory } =\n          req.body as {\n            filename: string;\n            path: string;\n            content: string;\n            originFilename: string;\n            directory: string;\n          };\n\n        if (!path) {\n          path = config.scriptPath;\n        }\n        if (!path.endsWith('/')) {\n          path += '/';\n        }\n        if (!path.startsWith('/')) {\n          path = join(config.scriptPath, path);\n        }\n        if (config.writePathList.every((x) => !path.startsWith(x))) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n\n        if (req.file) {\n          await fs.rename(req.file.path, join(path, filename));\n          return res.send({ code: 200 });\n        }\n\n        if (directory) {\n          await fs.mkdir(join(path, directory), { recursive: true });\n          return res.send({ code: 200 });\n        }\n\n        if (!originFilename) {\n          originFilename = filename;\n        }\n        const originFilePath = join(\n          path,\n          `${originFilename.replace(/\\//g, '')}`,\n        );\n        await fs.mkdir(path, { recursive: true });\n        const filePath = join(path, `${filename.replace(/\\//g, '')}`);\n        const fileExists = await fileExist(filePath);\n        if (fileExists) {\n          await fs.copyFile(\n            originFilePath,\n            join(config.bakPath, originFilename.replace(/\\//g, '')),\n          );\n          if (filename !== originFilename) {\n            await rmPath(originFilePath);\n          }\n        }\n        await writeFileWithLock(filePath, content);\n        return res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().optional().allow(''),\n        content: Joi.string().required().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, content, path } = req.body as {\n          filename: string;\n          content: string;\n          path: string;\n        };\n        const scriptService = Container.get(ScriptService);\n        const filePath = scriptService.checkFilePath(path, filename);\n        if (!filePath) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        await writeFileWithLock(filePath, content);\n        return res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().optional().allow(''),\n        type: Joi.string().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path } = req.body as {\n          filename: string;\n          path: string;\n        };\n        if (!path) {\n          path = '';\n        }\n        const scriptService = Container.get(ScriptService);\n        const filePath = scriptService.checkFilePath(path, filename);\n        if (!filePath) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        await rmPath(filePath);\n        res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/download',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().optional().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path } = req.body as {\n          filename: string;\n          path: string;\n        };\n        if (!path) {\n          path = '';\n        }\n        const scriptService = Container.get(ScriptService);\n        const filePath = scriptService.checkFilePath(path, filename);\n        if (!filePath) {\n          return res.send({\n            code: 403,\n            message: '暂无权限',\n          });\n        }\n        return res.download(filePath, filename, (err) => {\n          if (err) {\n            return next(err);\n          }\n        });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/run',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        content: Joi.string().optional().allow(''),\n        path: Joi.string().optional().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        let { filename, content, path } = req.body;\n        if (!path) {\n          path = '';\n        }\n        const { name, ext } = parse(filename);\n        const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);\n        await writeFileWithLock(filePath, content || '');\n\n        const scriptService = Container.get(ScriptService);\n        const result = await scriptService.runScript(filePath);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/stop',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().optional().allow(''),\n        pid: Joi.number().optional().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path, pid } = req.body;\n        if (!path) {\n          path = '';\n        }\n        const { name, ext } = parse(filename);\n        const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);\n        const logPath = join(config.logPath, path, `${name}.swap`);\n\n        const scriptService = Container.get(ScriptService);\n        const result = await scriptService.stopScript(filePath, pid);\n        setTimeout(() => {\n          rmPath(logPath);\n        }, 3000);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/rename',\n    celebrate({\n      body: Joi.object({\n        filename: Joi.string().required(),\n        path: Joi.string().allow(''),\n        newFilename: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        let { filename, path, newFilename } = req.body as {\n          filename: string;\n          path: string;\n          newFilename: string;\n        };\n        if (!path) {\n          path = '';\n        }\n        const filePath = join(config.scriptPath, path, filename);\n        const newPath = join(config.scriptPath, path, newFilename);\n        await fs.rename(filePath, newPath);\n        res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/subscription.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport SubscriptionService from '../services/subscription';\nimport { celebrate, Joi } from 'celebrate';\nimport CronExpressionParser from 'cron-parser';\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/subscriptions', route);\n\n  route.get('/', async (req: Request, res: Response, next: NextFunction) => {\n    const logger: Logger = Container.get('logger');\n    try {\n      const subscriptionService = Container.get(SubscriptionService);\n      const data = await subscriptionService.list(\n        req.query.searchValue as string,\n        req.query.ids as string,\n      );\n      return res.send({ code: 200, data });\n    } catch (e) {\n      logger.error('🔥 error: %o', e);\n      return next(e);\n    }\n  });\n\n  route.post(\n    '/',\n    celebrate({\n      body: Joi.object({\n        type: Joi.string().required(),\n        schedule: Joi.string().optional().allow('').allow(null),\n        interval_schedule: Joi.object({\n          type: Joi.string().required(),\n          value: Joi.number().min(1).required(),\n        })\n          .optional()\n          .allow('')\n          .allow(null),\n        name: Joi.string().optional().allow('').allow(null),\n        url: Joi.string().required(),\n        whitelist: Joi.string().optional().allow('').allow(null),\n        blacklist: Joi.string().optional().allow('').allow(null),\n        branch: Joi.string().optional().allow('').allow(null),\n        dependences: Joi.string().optional().allow('').allow(null),\n        pull_type: Joi.string().optional().allow('').allow(null),\n        pull_option: Joi.object().optional().allow('').allow(null),\n        extensions: Joi.string().optional().allow('').allow(null),\n        sub_before: Joi.string().optional().allow('').allow(null),\n        sub_after: Joi.string().optional().allow('').allow(null),\n        schedule_type: Joi.string().required(),\n        alias: Joi.string().required(),\n        proxy: Joi.string().optional().allow('').allow(null),\n        autoAddCron: Joi.boolean().optional().allow('').allow(null),\n        autoDelCron: Joi.boolean().optional().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        if (\n          !req.body.schedule ||\n          CronExpressionParser.parse(req.body.schedule).hasNext()\n        ) {\n          const subscriptionService = Container.get(SubscriptionService);\n          const data = await subscriptionService.create(req.body);\n          return res.send({ code: 200, data });\n        } else {\n          return res.send({ code: 400, message: 'param schedule error' });\n        }\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/run',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.run(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/stop',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.stop(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/disable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.disabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/enable',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.enabled(req.body);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id/log',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.log(req.params.id);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/',\n    celebrate({\n      body: Joi.object({\n        type: Joi.string().required(),\n        schedule: Joi.string().optional().allow('').allow(null),\n        interval_schedule: Joi.object().optional().allow('').allow(null),\n        name: Joi.string().optional().allow('').allow(null),\n        url: Joi.string().required(),\n        whitelist: Joi.string().optional().allow('').allow(null),\n        blacklist: Joi.string().optional().allow('').allow(null),\n        branch: Joi.string().optional().allow('').allow(null),\n        dependences: Joi.string().optional().allow('').allow(null),\n        pull_type: Joi.string().optional().allow('').allow(null),\n        pull_option: Joi.object().optional().allow('').allow(null),\n        schedule_type: Joi.string().optional().allow('').allow(null),\n        extensions: Joi.string().optional().allow('').allow(null),\n        sub_before: Joi.string().optional().allow('').allow(null),\n        sub_after: Joi.string().optional().allow('').allow(null),\n        alias: Joi.string().required(),\n        proxy: Joi.string().optional().allow('').allow(null),\n        autoAddCron: Joi.boolean().optional().allow('').allow(null),\n        autoDelCron: Joi.boolean().optional().allow('').allow(null),\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        if (\n          !req.body.schedule ||\n          typeof req.body.schedule === 'object' ||\n          CronExpressionParser.parse(req.body.schedule).hasNext()\n        ) {\n          const subscriptionService = Container.get(SubscriptionService);\n          const data = await subscriptionService.update(req.body);\n          return res.send({ code: 200, data });\n        } else {\n          return res.send({ code: 400, message: 'param schedule error' });\n        }\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/',\n    celebrate({\n      body: Joi.array().items(Joi.number().required()),\n      query: Joi.object({\n        force: Joi.boolean().optional(),\n        t: Joi.number(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.remove(req.body, req.query);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.getDb({ id: req.params.id });\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/status',\n    celebrate({\n      body: Joi.object({\n        ids: Joi.array().items(Joi.number().required()),\n        status: Joi.string().required(),\n        pid: Joi.string().optional(),\n        log_path: Joi.string().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.status({\n          ...req.body,\n          status: parseInt(req.body.status),\n          pid: parseInt(req.body.pid) || '',\n        });\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/:id/logs',\n    celebrate({\n      params: Joi.object({\n        id: Joi.number().required(),\n      }),\n    }),\n    async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const subscriptionService = Container.get(SubscriptionService);\n        const data = await subscriptionService.logs(req.params.id);\n        return res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/system.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport * as fs from 'fs/promises';\nimport config from '../config';\nimport SystemService from '../services/system';\nimport { celebrate, Joi } from 'celebrate';\nimport UserService from '../services/user';\nimport {\n  getUniqPath,\n  handleLogPath,\n  parseVersion,\n  promiseExec,\n} from '../config/util';\nimport dayjs from 'dayjs';\nimport multer from 'multer';\nimport { logStreamManager } from '../shared/logStreamManager';\n\nconst route = Router();\nconst storage = multer.diskStorage({\n  destination: function (req, file, cb) {\n    cb(null, config.tmpPath);\n  },\n  filename: function (req, file, cb) {\n    cb(null, 'data.tgz');\n  },\n});\nconst upload = multer({ storage: storage });\n\nexport default (app: Router) => {\n  app.use('/system', route);\n\n  route.get('/', async (req: Request, res: Response, next: NextFunction) => {\n    const logger: Logger = Container.get('logger');\n    try {\n      const userService = Container.get(UserService);\n      const authInfo = await userService.getAuthInfo();\n      const { version, changeLog, changeLogLink, publishTime } =\n        await parseVersion(config.versionFile);\n\n      let isInitialized = true;\n      if (\n        Object.keys(authInfo).length === 2 &&\n        authInfo.username === 'admin' &&\n        authInfo.password === 'admin'\n      ) {\n        isInitialized = false;\n      }\n      res.send({\n        code: 200,\n        data: {\n          isInitialized,\n          version,\n          publishTime: dayjs(publishTime).unix(),\n          branch: process.env.QL_BRANCH || 'master',\n          changeLog,\n          changeLogLink,\n        },\n      });\n    } catch (e) {\n      logger.error('🔥 error: %o', e);\n      return next(e);\n    }\n  });\n\n  route.get(\n    '/config',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const systemService = Container.get(SystemService);\n        const data = await systemService.getSystemConfig();\n        res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/log-remove-frequency',\n    celebrate({\n      body: Joi.object({\n        logRemoveFrequency: Joi.number().allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updateLogRemoveFrequency(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/cron-concurrency',\n    celebrate({\n      body: Joi.object({\n        cronConcurrency: Joi.number().allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updateCronConcurrency(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/dependence-proxy',\n    celebrate({\n      body: Joi.object({\n        dependenceProxy: Joi.string().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updateDependenceProxy(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/node-mirror',\n    celebrate({\n      body: Joi.object({\n        nodeMirror: Joi.string().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        res.setHeader('Content-type', 'application/octet-stream');\n        await systemService.updateNodeMirror(req.body, res);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/python-mirror',\n    celebrate({\n      body: Joi.object({\n        pythonMirror: Joi.string().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updatePythonMirror(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/linux-mirror',\n    celebrate({\n      body: Joi.object({\n        linuxMirror: Joi.string().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        res.setHeader('Content-type', 'application/octet-stream');\n        await systemService.updateLinuxMirror(req.body, res);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/update-check',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.checkUpdate();\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/update',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updateSystem();\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/reload',\n    celebrate({\n      body: Joi.object({\n        type: Joi.string().optional().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.reloadSystem(req.body.type);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/notify',\n    celebrate({\n      body: Joi.object({\n        title: Joi.string().required(),\n        content: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.notify(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/command-run',\n    celebrate({\n      body: Joi.object({\n        command: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const command = req.body.command;\n        const idStr = `cat ${config.crontabFile} | grep -E \"${command}\" | perl -pe \"s|.*ID=(.*) ${command}.*|\\\\1|\" | head -1 | awk -F \" \" '{print $1}' | xargs echo -n`;\n        let id = await promiseExec(idStr);\n        const uniqPath = await getUniqPath(command, id);\n        const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');\n        const logPath = `${uniqPath}/${logTime}.log`;\n        res.setHeader('Content-type', 'application/octet-stream');\n        await systemService.run(\n          { ...req.body, logPath },\n          {\n            onStart: async (cp, startTime) => {\n              res.setHeader('QL-Task-Pid', `${cp.pid}`);\n              res.setHeader('QL-Task-Log', `${logPath}`);\n            },\n            onEnd: async (cp, endTime, diff) => {\n              // Close the stream after task completion\n              await logStreamManager.closeStream(await handleLogPath(logPath));\n              res.end();\n            },\n            onError: async (message: string) => {\n              res.write(message);\n              const absolutePath = await handleLogPath(logPath);\n              await logStreamManager.write(absolutePath, message);\n            },\n            onLog: async (message: string) => {\n              res.write(message);\n              const absolutePath = await handleLogPath(logPath);\n              await logStreamManager.write(absolutePath, message);\n            },\n          },\n        );\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/command-stop',\n    celebrate({\n      body: Joi.object({\n        command: Joi.string().optional(),\n        pid: Joi.number().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.stop(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/data/export',\n    celebrate({\n      body: Joi.object({\n        type: Joi.array().items(Joi.string()).optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        await systemService.exportData(res, req.body.type);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/data/import',\n    upload.single('data'),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.importData();\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/log',\n    celebrate({\n      query: {\n        startTime: Joi.string().allow('').optional(),\n        endTime: Joi.string().allow('').optional(),\n        t: Joi.string().optional(),\n      },\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        await systemService.getSystemLog(\n          res,\n          req.query as {\n            startTime?: string;\n            endTime?: string;\n          },\n        );\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.delete(\n    '/log',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        await systemService.deleteSystemLog();\n        res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/auth/reset',\n    celebrate({\n      body: Joi.object({\n        retries: Joi.number().optional(),\n        twoFactorActivated: Joi.boolean().optional(),\n        password: Joi.string().optional(),\n        username: Joi.string().optional(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const userService = Container.get(UserService);\n        await userService.resetAuthInfo(req.body);\n        res.send({ code: 200, message: '更新成功' });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/timezone',\n    celebrate({\n      body: Joi.object({\n        timezone: Joi.string().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updateTimezone(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/global-ssh-key',\n    celebrate({\n      body: Joi.object({\n        globalSshKey: Joi.string().allow('').allow(null),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.updateGlobalSshKey(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/config/dependence-clean',\n    celebrate({\n      body: Joi.object({\n        type: Joi.string().allow(''),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.cleanDependence(req.body.type);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/update.ts",
    "content": "import { NextFunction, Request, Response, Router } from 'express';\nimport Container from 'typedi';\nimport Logger from '../loaders/logger';\nimport SystemService from '../services/system';\nconst route = Router();\n\nexport default (app: Router) => {\n  app.use('/update', route);\n\n  route.put(\n    '/reload',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.reloadSystem();\n        res.send(result);\n      } catch (e) {\n        Logger.error('🔥 error: %o', e);\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/system',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.reloadSystem('system');\n        res.send(result);\n      } catch (e) {\n        Logger.error('🔥 error: %o', e);\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/data',\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        const systemService = Container.get(SystemService);\n        const result = await systemService.reloadSystem('data');\n        res.send(result);\n      } catch (e) {\n        Logger.error('🔥 error: %o', e);\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/api/user.ts",
    "content": "import { Router, Request, Response, NextFunction } from 'express';\nimport { Container } from 'typedi';\nimport { Logger } from 'winston';\nimport UserService from '../services/user';\nimport { celebrate, Joi } from 'celebrate';\nimport multer from 'multer';\nimport path from 'path';\nimport { v4 as uuidV4 } from 'uuid';\nimport rateLimit from 'express-rate-limit';\nimport config from '../config';\nimport { isDemoEnv, getToken } from '../config/util';\nconst route = Router();\n\nconst storage = multer.diskStorage({\n  destination: function (req, file, cb) {\n    cb(null, config.uploadPath);\n  },\n  filename: function (req, file, cb) {\n    const ext = path.parse(file.originalname).ext;\n    const key = uuidV4();\n    cb(null, key + ext);\n  },\n});\nconst upload = multer({ storage: storage });\n\nexport default (app: Router) => {\n  app.use('/user', route);\n\n  route.post(\n    '/login',\n    rateLimit({\n      windowMs: 15 * 60 * 1000,\n      max: 100,\n    }),\n    celebrate({\n      body: Joi.object({\n        username: Joi.string().required(),\n        password: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.login({ ...req.body }, req);\n        return res.send(data);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.post(\n    '/logout',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const token = getToken(req);\n        await userService.logout(req.platform, token);\n        res.send({ code: 200 });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/',\n    celebrate({\n      body: Joi.object({\n        username: Joi.string().required(),\n        password: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      try {\n        if (isDemoEnv()) {\n          return res.send({ code: 450, message: '未知错误' });\n        }\n        const userService = Container.get(UserService);\n        await userService.updateUsernameAndPassword(req.body);\n        res.send({ code: 200, message: '更新成功' });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get('/', async (req: Request, res: Response, next: NextFunction) => {\n    const logger: Logger = Container.get('logger');\n    try {\n      const userService = Container.get(UserService);\n      const authInfo = await userService.getAuthInfo();\n      res.send({\n        code: 200,\n        data: {\n          username: authInfo.username,\n          avatar: authInfo.avatar,\n          twoFactorActivated: authInfo.twoFactorActivated,\n        },\n      });\n    } catch (e) {\n      logger.error('🔥 error: %o', e);\n      return next(e);\n    }\n  });\n\n  route.get(\n    '/two-factor/init',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.initTwoFactor();\n        res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/two-factor/active',\n    celebrate({\n      body: Joi.object({\n        code: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.activeTwoFactor(req.body.code);\n        res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/two-factor/deactive',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.deactiveTwoFactor();\n        res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/two-factor/login',\n    celebrate({\n      body: Joi.object({\n        code: Joi.string().required(),\n        username: Joi.string().required(),\n        password: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.twoFactorLogin(req.body, req);\n        res.send(data);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/login-log',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.getLoginLog();\n        res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.get(\n    '/notification',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const data = await userService.getNotificationMode();\n        res.send({ code: 200, data });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/notification',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const result = await userService.updateNotificationMode(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/init',\n    celebrate({\n      body: Joi.object({\n        username: Joi.string().required(),\n        password: Joi.string().required(),\n      }),\n    }),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        await userService.updateUsernameAndPassword(req.body);\n        res.send({ code: 200, message: '更新成功' });\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/notification/init',\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const result = await userService.updateNotificationMode(req.body);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n\n  route.put(\n    '/avatar',\n    upload.single('avatar'),\n    async (req: Request, res: Response, next: NextFunction) => {\n      const logger: Logger = Container.get('logger');\n      try {\n        const userService = Container.get(UserService);\n        const result = await userService.updateAvatar(req.file!.filename);\n        res.send(result);\n      } catch (e) {\n        return next(e);\n      }\n    },\n  );\n};\n"
  },
  {
    "path": "back/app.ts",
    "content": "import 'reflect-metadata';\nimport cluster, { type Worker } from 'cluster';\nimport compression from 'compression';\nimport cors from 'cors';\nimport express from 'express';\nimport helmet from 'helmet';\nimport { Container } from 'typedi';\nimport config from './config';\nimport Logger from './loaders/logger';\nimport { monitoringMiddleware } from './middlewares/monitoring';\nimport { type GrpcServerService } from './services/grpc';\nimport { type HttpServerService } from './services/http';\n\ninterface WorkerMetadata {\n  id: number;\n  pid: number;\n  serviceType: string;\n  startTime: Date;\n}\n\nclass Application {\n  private app: express.Application;\n  private httpServerService?: HttpServerService;\n  private grpcServerService?: GrpcServerService;\n  private isShuttingDown = false;\n  private workerMetadataMap = new Map<number, WorkerMetadata>();\n  private httpWorker?: Worker;\n\n  constructor() {\n    this.app = express();\n    // 创建一个全局中间件，删除查询参数中的t\n    this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {\n      if (req.query.t) {\n        delete req.query.t;\n      }\n      next();\n    });\n  }\n\n  async start() {\n    try {\n      if (cluster.isPrimary) {\n        await this.initializeDatabase();\n      }\n      if (cluster.isPrimary) {\n        this.startMasterProcess();\n      } else {\n        await this.startWorkerProcess();\n      }\n    } catch (error) {\n      Logger.error('Failed to start application:', error);\n      process.exit(1);\n    }\n  }\n\n  private startMasterProcess() {\n    // Fork gRPC worker first and wait for it to be ready\n    const grpcWorker = this.forkWorker('grpc');\n    \n    // Wait for gRPC worker to signal it's ready before starting HTTP worker\n    this.waitForWorkerReady(grpcWorker, 30000)\n      .then(() => {\n        Logger.info('✌️ gRPC worker is ready, starting HTTP worker');\n        this.httpWorker = this.forkWorker('http');\n      })\n      .catch((error) => {\n        Logger.error('✌️ Failed to wait for gRPC worker:', error);\n        process.exit(1);\n      });\n\n    cluster.on('exit', (worker, code, signal) => {\n      const metadata = this.workerMetadataMap.get(worker.id);\n      if (metadata) {\n        if (!this.isShuttingDown) {\n          Logger.error(\n            `✌️ ${metadata.serviceType} worker ${worker.process.pid} died (${signal || code\n            }). Restarting...`,\n          );\n          // If gRPC worker died, restart it and wait for it to be ready\n          if (metadata.serviceType === 'grpc') {\n            const newGrpcWorker = this.forkWorker('grpc');\n            this.waitForWorkerReady(newGrpcWorker, 30000)\n              .then(() => {\n                Logger.info('✌️ gRPC worker restarted and ready');\n                // Re-register cron jobs by notifying the HTTP worker\n                if (this.httpWorker) {\n                  try {\n                    this.httpWorker.send('reregister-crons');\n                    Logger.info('✌️ Sent reregister-crons message to HTTP worker');\n                  } catch (error) {\n                    Logger.error('✌️ Failed to send reregister-crons message:', error);\n                  }\n                }\n              })\n              .catch((error) => {\n                Logger.error('✌️ Failed to restart gRPC worker:', error);\n                process.exit(1);\n              });\n          } else {\n            // For HTTP worker, just restart it\n            const newWorker = this.forkWorker(metadata.serviceType);\n            this.httpWorker = newWorker;\n            Logger.info(`✌️ Restarted ${metadata.serviceType} worker (PID: ${newWorker.process.pid})`);\n          }\n        }\n\n        this.workerMetadataMap.delete(worker.id);\n      }\n    });\n\n    this.setupMasterShutdown();\n  }\n\n  private waitForWorkerReady(worker: Worker, timeoutMs: number): Promise<void> {\n    return new Promise<void>((resolve, reject) => {\n      const messageHandler = (msg: any) => {\n        if (msg === 'ready') {\n          worker.removeListener('message', messageHandler);\n          clearTimeout(timeoutId);\n          resolve();\n        }\n      };\n      worker.on('message', messageHandler);\n      \n      // Timeout after specified milliseconds\n      const timeoutId = setTimeout(() => {\n        worker.removeListener('message', messageHandler);\n        reject(new Error(`Worker failed to start within ${timeoutMs / 1000} seconds`));\n      }, timeoutMs);\n    });\n  }\n\n  private forkWorker(serviceType: string): Worker {\n    const worker = cluster.fork({ SERVICE_TYPE: serviceType });\n\n    this.workerMetadataMap.set(worker.id, {\n      id: worker.id,\n      pid: worker.process.pid!,\n      serviceType,\n      startTime: new Date(),\n    });\n\n    return worker;\n  }\n\n  private async initializeDatabase() {\n    const dbLoader = await import('./loaders/db');\n    await dbLoader.default();\n  }\n\n  private setupMiddlewares() {\n    this.app.use(helmet({\n      contentSecurityPolicy: false,\n    }));\n    this.app.use(cors(config.cors));\n    this.app.use(compression());\n    this.app.use(monitoringMiddleware);\n  }\n\n  private setupMasterShutdown() {\n    const shutdown = async () => {\n      if (this.isShuttingDown) return;\n      this.isShuttingDown = true;\n\n      const workers = Object.values(cluster.workers || {});\n      const workerPromises: Promise<void>[] = [];\n\n      workers.forEach((worker) => {\n        if (worker) {\n          const exitPromise = new Promise<void>((resolve) => {\n            worker.once('exit', () => {\n              Logger.info(`✌️ Worker ${worker.process.pid} exited`);\n              resolve();\n            });\n\n            try {\n              worker.send('shutdown');\n            } catch (error) {\n              Logger.warn(\n                `✌️ Failed to send shutdown to worker ${worker.process.pid}:`,\n                error,\n              );\n            }\n          });\n\n          workerPromises.push(exitPromise);\n        }\n      });\n\n      try {\n        await Promise.race([\n          Promise.all(workerPromises),\n          new Promise<void>((resolve) => {\n            setTimeout(() => {\n              Logger.warn('✌️ Worker shutdown timeout reached');\n              resolve();\n            }, 10000);\n          }),\n        ]);\n        process.exit(0);\n      } catch (error) {\n        Logger.error('✌️ Error during worker shutdown:', error);\n        process.exit(1);\n      }\n    };\n\n    process.on('SIGTERM', shutdown);\n    process.on('SIGINT', shutdown);\n  }\n\n  private async startWorkerProcess() {\n    const serviceType = process.env.SERVICE_TYPE;\n    if (!serviceType || !['http', 'grpc'].includes(serviceType)) {\n      Logger.error('✌️ Invalid SERVICE_TYPE:', serviceType);\n      process.exit(1);\n    }\n\n    Logger.info(`✌️ ${serviceType} worker started (PID: ${process.pid})`);\n\n    try {\n      if (serviceType === 'http') {\n        await this.startHttpService();\n      } else {\n        await this.startGrpcService();\n      }\n\n      process.send?.('ready');\n    } catch (error) {\n      Logger.error(`✌️ ${serviceType} worker failed:`, error);\n      process.exit(1);\n    }\n  }\n\n  private async startHttpService() {\n    this.setupMiddlewares();\n\n    const { HttpServerService } = await import('./services/http');\n    this.httpServerService = Container.get(HttpServerService);\n\n    const appLoader = await import('./loaders/app');\n    await appLoader.default({ app: this.app });\n\n    const server = await this.httpServerService.initialize(\n      this.app,\n      config.port,\n    );\n\n    const serverLoader = await import('./loaders/server');\n    await (serverLoader.default as any)({ server });\n    this.setupWorkerShutdown('http');\n  }\n\n  private async startGrpcService() {\n    const { GrpcServerService } = await import('./services/grpc');\n    this.grpcServerService = Container.get(GrpcServerService);\n\n    await this.grpcServerService.initialize();\n    this.setupWorkerShutdown('grpc');\n  }\n\n  private setupWorkerShutdown(serviceType: string) {\n    process.on('message', async (msg) => {\n      if (msg === 'shutdown') {\n        this.gracefulShutdown(serviceType);\n      } else if (msg === 'reregister-crons' && serviceType === 'http') {\n        // Re-register cron jobs when gRPC worker restarts\n        try {\n          Logger.info('✌️ Received reregister-crons message, re-registering cron jobs...');\n          const CronService = (await import('./services/cron')).default;\n          const cronService = Container.get(CronService);\n          await cronService.autosave_crontab();\n          Logger.info('✌️ Cron jobs re-registered successfully');\n        } catch (error) {\n          Logger.error('✌️ Failed to re-register cron jobs:', error);\n        }\n      }\n    });\n\n    const shutdown = () => this.gracefulShutdown(serviceType);\n    process.on('SIGTERM', shutdown);\n    process.on('SIGINT', shutdown);\n  }\n\n  private async gracefulShutdown(serviceType: string) {\n    if (this.isShuttingDown) return;\n    this.isShuttingDown = true;\n\n    try {\n      if (serviceType === 'http') {\n        await this.httpServerService?.shutdown();\n      } else {\n        await this.grpcServerService?.shutdown();\n      }\n      process.exit(0);\n    } catch (error) {\n      Logger.error(`✌️ [${serviceType}] Error during shutdown:`, error);\n      process.exit(1);\n    }\n  }\n}\n\nconst app = new Application();\napp.start().catch((error) => {\n  Logger.error('🙅‍♀️ Application failed to start:', error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "back/config/const.ts",
    "content": "export const LOG_END_SYMBOL = '　　　　　';\n\nexport const TASK_COMMAND = 'task';\nexport const QL_COMMAND = 'ql';\n\nexport const TASK_PREFIX = `${TASK_COMMAND} `;\nexport const QL_PREFIX = `${QL_COMMAND} `;\n\nexport const SAMPLE_FILES = [\n  {\n    title: 'config.sample.sh',\n    value: 'sample/config.sample.sh',\n    target: 'config.sh',\n  },\n  {\n    title: 'notify.js',\n    value: 'sample/notify.js',\n    target: 'data/scripts/sendNotify.js',\n  },\n  {\n    title: 'notify.py',\n    value: 'sample/notify.py',\n    target: 'data/scripts/notify.py',\n  },\n];\n\nexport const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;\n\nexport const NotificationModeStringMap = {\n  0: 'gotify',\n  1: 'goCqHttpBot',\n  2: 'serverChan',\n  3: 'pushDeer',\n  4: 'bark',\n  5: 'chat',\n  6: 'telegramBot',\n  7: 'dingtalkBot',\n  8: 'weWorkBot',\n  9: 'weWorkApp',\n  10: 'aibotk',\n  11: 'iGot',\n  12: 'pushPlus',\n  13: 'wePlusBot',\n  14: 'email',\n  15: 'pushMe',\n  16: 'feishu',\n  17: 'webhook',\n  18: 'chronocat',\n  19: 'ntfy',\n  20: 'wxPusherBot',\n} as const;\n"
  },
  {
    "path": "back/config/http.ts",
    "content": "import { request as undiciRequest, Dispatcher } from 'undici';\n\ntype RequestBaseOptions = {\n  dispatcher?: Dispatcher;\n  json?: Record<string, any>;\n  form?: string;\n  headers?: Record<string, string>;\n} & Omit<Dispatcher.RequestOptions<null>, 'origin' | 'path' | 'method'>;\n\ntype RequestOptionsWithOptions = RequestBaseOptions &\n  Partial<Pick<Dispatcher.RequestOptions, 'method'>>;\n\ntype ResponseTypeMap = {\n  json: Record<string, any>;\n  text: string;\n};\n\ntype ResponseTypeKey = keyof ResponseTypeMap;\n\nasync function request(\n  url: string,\n  options?: RequestOptionsWithOptions,\n): Promise<Dispatcher.ResponseData<null>> {\n  const { json, form, body, headers = {}, ...rest } = options || {};\n  const finalHeaders = { ...headers } as Record<string, string>;\n  let finalBody = body;\n\n  if (json) {\n    finalHeaders['content-type'] = 'application/json';\n    finalBody = JSON.stringify(json);\n  } else if (form) {\n    finalBody = form;\n    delete finalHeaders['content-type'];\n  }\n\n  const res = await undiciRequest(url, {\n    method: 'POST',\n    headers: finalHeaders,\n    body: finalBody,\n    ...rest,\n  });\n\n  return res;\n}\n\nasync function post<T extends ResponseTypeKey = 'json'>(\n  url: string,\n  options?: RequestBaseOptions & { responseType?: T },\n): Promise<ResponseTypeMap[T]> {\n  const resp = await request(url, { ...options, method: 'POST' });\n\n  const rawText = await resp.body.text();\n\n  if (options?.responseType === 'text') {\n    return rawText as ResponseTypeMap[T];\n  }\n\n  try {\n    return JSON.parse(rawText) as ResponseTypeMap[T];\n  } catch {\n    return rawText as ResponseTypeMap[T];\n  }\n}\n\nexport const httpClient = {\n  post,\n  request,\n};\n"
  },
  {
    "path": "back/config/index.ts",
    "content": "import dotenv from 'dotenv';\nimport path from 'path';\nimport { createRandomString } from './share';\n\ndotenv.config({\n  path: path.join(__dirname, '../../.env'),\n});\n\ninterface Config {\n  port: number;\n  grpcPort: number;\n  nodeEnv: string;\n  isDevelopment: boolean;\n  isProduction: boolean;\n  jwt: {\n    secret: string;\n    expiresIn?: string;\n  };\n  cors: {\n    origin: string[];\n    methods: string[];\n  };\n  logs: {\n    level: string;\n  };\n  api: {\n    prefix: string;\n  };\n}\n\nconst config: Config = {\n  port: parseInt(process.env.BACK_PORT || '5700', 10),\n  grpcPort: parseInt(process.env.GRPC_PORT || '5500', 10),\n  nodeEnv: process.env.NODE_ENV || 'development',\n  isDevelopment: process.env.NODE_ENV === 'development',\n  isProduction: process.env.NODE_ENV === 'production',\n  logs: {\n    level: process.env.LOG_LEVEL || 'silly',\n  },\n  api: {\n    prefix: '/api',\n  },\n  jwt: {\n    secret: process.env.JWT_SECRET || 'whyour-secret',\n    expiresIn: process.env.JWT_EXPIRES_IN,\n  },\n  cors: {\n    origin: process.env.CORS_ORIGIN\n      ? process.env.CORS_ORIGIN.split(',')\n      : ['*'],\n    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n  },\n};\n\nprocess.env.NODE_ENV = process.env.NODE_ENV || 'development';\n\nif (!process.env.QL_DIR) {\n  let qlHomePath = path.join(__dirname, '../../');\n  if (qlHomePath.endsWith('/static/')) {\n    qlHomePath = path.join(qlHomePath, '../');\n  }\n  process.env.QL_DIR = qlHomePath.replace(/\\/$/g, '');\n}\n\nconst lastVersionFile = `https://qn.whyour.cn/version.yaml`;\n\n// Get and normalize QlBaseUrl\nlet baseUrl = process.env.QlBaseUrl || '';\nif (baseUrl) {\n  // Ensure it starts with /\n  if (!baseUrl.startsWith('/')) {\n    baseUrl = `/${baseUrl}`;\n  }\n  // Remove trailing slash for consistency in route definitions\n  if (baseUrl.endsWith('/')) {\n    baseUrl = baseUrl.slice(0, -1);\n  }\n}\n\nconst rootPath = process.env.QL_DIR as string;\nconst envFound = dotenv.config({ path: path.join(rootPath, '.env') });\n\nlet dataPath = path.join(rootPath, 'data/');\n\nif (process.env.QL_DATA_DIR) {\n  dataPath = process.env.QL_DATA_DIR.replace(/\\/$/g, '');\n}\n\nconst shellPath = path.join(rootPath, 'shell/');\nconst preloadPath = path.join(shellPath, 'preload/');\nconst tmpPath = path.join(rootPath, '.tmp/');\nconst samplePath = path.join(rootPath, 'sample/');\nconst configPath = path.join(dataPath, 'config/');\nconst scriptPath = path.join(dataPath, 'scripts/');\nconst repoPath = path.join(dataPath, 'repo/');\nconst bakPath = path.join(dataPath, 'bak/');\nconst logPath = path.join(dataPath, 'log/');\nconst dbPath = path.join(dataPath, 'db/');\nconst uploadPath = path.join(dataPath, 'upload/');\nconst sshdPath = path.join(dataPath, 'ssh.d/');\nconst systemLogPath = path.join(dataPath, 'syslog/');\nconst dependenceCachePath = path.join(dataPath, 'dep_cache/');\n\nconst envFile = path.join(preloadPath, 'env.sh');\nconst jsEnvFile = path.join(preloadPath, 'env.js');\nconst pyEnvFile = path.join(preloadPath, 'env.py');\nconst jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');\nconst pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');\nconst confFile = path.join(configPath, 'config.sh');\nconst crontabFile = path.join(configPath, 'crontab.list');\nconst authConfigFile = path.join(configPath, 'auth.json');\nconst extraFile = path.join(configPath, 'extra.sh');\nconst confBakDir = path.join(dataPath, 'config/bak/');\nconst sampleFile = path.join(samplePath, 'config.sample.sh');\nconst sqliteFile = path.join(samplePath, 'database.sqlite');\n\nconst authError = '错误的用户名密码，请重试';\nconst loginFaild = '请先登录!';\nconst configString = 'config sample crontab shareCode diy';\nconst versionFile = path.join(rootPath, 'version.yaml');\nconst dataTgzFile = path.join(tmpPath, 'data.tgz');\nconst shareShellFile = path.join(shellPath, 'share.sh');\nconst dependenceProxyFile = path.join(configPath, 'dependence-proxy.sh');\n\nif (envFound.error) {\n  throw new Error(\"⚠️  Couldn't find .env file  ⚠️\");\n}\n\nexport default {\n  ...config,\n  jwt: config.jwt,\n  baseUrl,\n  rootPath,\n  tmpPath,\n  dataPath,\n  dataTgzFile,\n  shareShellFile,\n  dependenceProxyFile,\n  configString,\n  loginFaild,\n  authError,\n  logPath,\n  extraFile,\n  authConfigFile,\n  confBakDir,\n  crontabFile,\n  sampleFile,\n  confFile,\n  envFile,\n  jsEnvFile,\n  pyEnvFile,\n  jsNotifyFile,\n  pyNotifyFile,\n  dbPath,\n  uploadPath,\n  configPath,\n  scriptPath,\n  repoPath,\n  samplePath,\n  blackFileList: [\n    'auth.json',\n    'config.sh.sample',\n    'cookie.sh',\n    'crontab.list',\n    'dependence-proxy.sh',\n    'env.sh',\n    'env.js',\n    'env.py',\n    'token.json',\n  ],\n  writePathList: [configPath, scriptPath],\n  bakPath,\n  apiWhiteList: [\n    '/api/user/login',\n    '/api/health',\n    '/open/auth/token',\n    '/api/user/two-factor/login',\n    '/api/system',\n    '/api/user/init',\n    '/api/user/notification/init',\n    '/open/user/login',\n    '/open/user/two-factor/login',\n    '/open/system',\n    '/open/user/init',\n    '/open/user/notification/init',\n  ],\n  versionFile,\n  lastVersionFile,\n  sqliteFile,\n  sshdPath,\n  systemLogPath,\n  dependenceCachePath,\n  maxTokensPerPlatform: 10, // Maximum number of concurrent sessions per platform\n};\n"
  },
  {
    "path": "back/config/serverEnv.ts",
    "content": "import { Request, Response } from 'express';\nimport pick from 'lodash/pick';\n\nlet pickedEnv: Record<string, string>;\n\nfunction getPickedEnv() {\n  if (pickedEnv) return pickedEnv;\n  const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv', 'QL_DIR']);\n  if (picked.QlBaseUrl) {\n    if (!picked.QlBaseUrl.startsWith('/')) {\n      picked.QlBaseUrl = `/${picked.QlBaseUrl}`;\n    }\n    if (!picked.QlBaseUrl.endsWith('/')) {\n      picked.QlBaseUrl = `${picked.QlBaseUrl}/`;\n    }\n  }\n  pickedEnv = picked as Record<string, string>;\n  return picked;\n}\n\nexport function serveEnv(_req: Request, res: Response) {\n  res.type('.js');\n  res.send(\n    Object.entries(getPickedEnv())\n      .map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)\n      .join('\\n'),\n  );\n}\n"
  },
  {
    "path": "back/config/share.ts",
    "content": "export function createRandomString(min: number, max: number): string {\n  const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];\n  const english = [\n    'a',\n    'b',\n    'c',\n    'd',\n    'e',\n    'f',\n    'g',\n    'h',\n    'i',\n    'j',\n    'k',\n    'l',\n    'm',\n    'n',\n    'o',\n    'p',\n    'q',\n    'r',\n    's',\n    't',\n    'u',\n    'v',\n    'w',\n    'x',\n    'y',\n    'z',\n  ];\n  const ENGLISH = [\n    'A',\n    'B',\n    'C',\n    'D',\n    'E',\n    'F',\n    'G',\n    'H',\n    'I',\n    'J',\n    'K',\n    'L',\n    'M',\n    'N',\n    'O',\n    'P',\n    'Q',\n    'R',\n    'S',\n    'T',\n    'U',\n    'V',\n    'W',\n    'X',\n    'Y',\n    'Z',\n  ];\n  const special = ['-', '_'];\n  const config = num.concat(english).concat(ENGLISH).concat(special);\n\n  const arr = [];\n  arr.push(getOne(num));\n  arr.push(getOne(english));\n  arr.push(getOne(ENGLISH));\n  arr.push(getOne(special));\n\n  const len = min + Math.floor(Math.random() * (max - min + 1));\n\n  for (let i = 4; i < len; i++) {\n    arr.push(config[Math.floor(Math.random() * config.length)]);\n  }\n\n  const newArr = [];\n  for (let j = 0; j < len; j++) {\n    newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);\n  }\n\n  function getOne(arr: any[]) {\n    return arr[Math.floor(Math.random() * arr.length)];\n  }\n\n  return newArr.join('');\n}"
  },
  {
    "path": "back/config/subscription.ts",
    "content": "import { Subscription } from '../data/subscription';\nimport isNil from 'lodash/isNil';\n\nexport function formatUrl(doc: Subscription) {\n  let url = doc.url;\n  let host = '';\n  if (doc.type === 'private-repo') {\n    if (doc.pull_type === 'ssh-key') {\n      host = doc.url!.replace(/.*\\@([^\\:]+)\\:.*/, '$1');\n      url = doc.url!.replace(host, doc.alias);\n    } else {\n      host = doc.url!.replace(/.*\\:\\/\\/([^\\/]+)\\/.*/, '$1');\n      const { username, password } = doc.pull_option as any;\n      url = doc.url!.replace(host, `${username}:${password}@${host}`);\n    }\n  }\n  return { url, host };\n}\n\nexport function formatCommand(doc: Subscription, url?: string) {\n  let command = `SUB_ID=${doc.id} ql `;\n  let _url = url || formatUrl(doc).url;\n  const {\n    type,\n    whitelist,\n    blacklist,\n    dependences,\n    branch,\n    extensions,\n    proxy,\n    autoAddCron,\n    autoDelCron,\n  } = doc;\n  if (type === 'file') {\n    command += `raw \"${_url}\" \"${proxy || ''}\" \"${\n      isNil(autoAddCron) ? true : Boolean(autoAddCron)\n    }\" \"${isNil(autoDelCron) ? true : Boolean(autoDelCron)}\"`;\n  } else {\n    command += `repo \"${_url}\" \"${whitelist || ''}\" \"${blacklist || ''}\" \"${\n      dependences || ''\n    }\" \"${branch || ''}\" \"${extensions || ''}\" \"${proxy || ''}\" \"${\n      isNil(autoAddCron) ? true : Boolean(autoAddCron)\n    }\" \"${isNil(autoDelCron) ? true : Boolean(autoDelCron)}\"`;\n  }\n  return command;\n}\n"
  },
  {
    "path": "back/config/util.ts",
    "content": "import * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { exec } from 'child_process';\nimport psTreeFun from 'ps-tree';\nimport { promisify } from 'util';\nimport { load } from 'js-yaml';\nimport config from './index';\nimport { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';\nimport Logger from '../loaders/logger';\nimport { writeFileWithLock } from '../shared/utils';\nimport { DependenceTypes } from '../data/dependence';\nimport { FormData } from 'undici';\n\nexport * from './share';\n\nexport async function getFileContentByName(fileName: string) {\n  const _exsit = await fileExist(fileName);\n  if (_exsit) {\n    return await fs.readFile(fileName, 'utf8');\n  }\n  return '';\n}\n\nexport function removeAnsi(text: string) {\n  return text.replace(/\\x1b\\[\\d+m/g, '');\n}\n\nexport async function getLastModifyFilePath(dir: string) {\n  let filePath = '';\n\n  const _exsit = await fileExist(dir);\n  if (_exsit) {\n    const arr = await fs.readdir(dir);\n\n    arr.forEach(async (item) => {\n      const fullpath = path.join(dir, item);\n      const stats = await fs.lstat(fullpath);\n      if (stats.isFile()) {\n        if (stats.mtimeMs >= 0) {\n          filePath = fullpath;\n        }\n      }\n    });\n  }\n  return filePath;\n}\n\nexport function getToken(req: any) {\n  const { authorization = '' } = req.headers;\n  if (authorization && authorization.split(' ')[0] === 'Bearer') {\n    return (authorization as string)\n      .replace('Bearer ', '')\n      .replace('mobile-', '')\n      .replace('desktop-', '');\n  }\n  return '';\n}\n\nexport function getPlatform(userAgent: string): 'mobile' | 'desktop' {\n  const ua = userAgent.toLowerCase();\n  const testUa = (regexp: RegExp) => regexp.test(ua);\n  const testVs = (regexp: RegExp) =>\n    (ua.match(regexp) || [])\n      .toString()\n      .replace(/[^0-9|_.]/g, '')\n      .replace(/_/g, '.');\n\n  // 系统\n  let system = 'unknow';\n  if (testUa(/windows|win32|win64|wow32|wow64/g)) {\n    system = 'windows'; // windows系统\n  } else if (testUa(/macintosh|macintel/g)) {\n    system = 'macos'; // macos系统\n  } else if (testUa(/x11/g)) {\n    system = 'linux'; // linux系统\n  } else if (testUa(/android|adr/g)) {\n    system = 'android'; // android系统\n  } else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {\n    system = 'ios'; // ios系统\n  } else if (testUa(/openharmony/g)) {\n    system = 'openharmony'; // openharmony系统\n  }\n\n  let platform = 'desktop';\n  if (system === 'windows' || system === 'macos' || system === 'linux') {\n    platform = 'desktop';\n  } else if (\n    system === 'android' ||\n    system === 'ios' ||\n    system === 'openharmony' ||\n    testUa(/mobile/g)\n  ) {\n    platform = 'mobile';\n  }\n\n  return platform as 'mobile' | 'desktop';\n}\n\nexport async function fileExist(file: any) {\n  try {\n    await fs.access(file);\n    return true;\n  } catch (error) {\n    return false;\n  }\n}\n\nexport async function createFile(file: string, data: string = '') {\n  await fs.mkdir(path.dirname(file), { recursive: true });\n  await writeFileWithLock(file, data);\n}\n\nexport async function handleLogPath(\n  logPath: string,\n  data: string = '',\n): Promise<string> {\n  const absolutePath = path.resolve(config.logPath, logPath);\n  const logFileExist = await fileExist(absolutePath);\n  if (!logFileExist) {\n    await createFile(absolutePath, data);\n  }\n  return absolutePath;\n}\n\nexport async function concurrentRun(\n  fnList: Array<() => Promise<any>> = [],\n  max = 5,\n) {\n  if (!fnList.length) return;\n\n  const replyList: any[] = []; // 收集任务执行结果\n  const startTime = new Date().getTime(); // 记录任务执行开始时间\n\n  // 任务执行程序\n  const schedule = async (index: number) => {\n    return new Promise(async (resolve) => {\n      const fn = fnList[index];\n      if (!fn) return resolve(null);\n\n      // 执行当前异步任务\n      const reply = await fn();\n      replyList[index] = reply;\n\n      // 执行完当前任务后，继续执行任务池的剩余任务\n      await schedule(index + max);\n      resolve(null);\n    });\n  };\n\n  // 任务池执行程序\n  const scheduleList = new Array(max)\n    .fill(0)\n    .map((_, index) => schedule(index));\n\n  // 使用 Promise.all 批量执行\n  const r = await Promise.all(scheduleList);\n  const cost = (new Date().getTime() - startTime) / 1000;\n\n  return replyList;\n}\n\nenum FileType {\n  'directory',\n  'file',\n}\n\nexport interface IFile {\n  title: string;\n  key: string;\n  type: 'directory' | 'file';\n  parent: string;\n  createTime: number;\n  size?: number;\n  children?: IFile[];\n}\n\nexport function dirSort(a: IFile, b: IFile): number {\n  if (a.type === 'file' && b.type === 'file') {\n    return b.createTime - a.createTime;\n  } else if (a.type === 'directory' && b.type === 'directory') {\n    return a.title.localeCompare(b.title);\n  } else {\n    return a.type === 'directory' ? -1 : 1;\n  }\n}\n\nexport async function readDirs(\n  dir: string,\n  baseDir: string = '',\n  blacklist: string[] = [],\n  sort: (a: IFile, b: IFile) => number = dirSort,\n): Promise<IFile[]> {\n  const relativePath = path.relative(baseDir, dir);\n  const files = await fs.readdir(dir);\n  const result: IFile[] = [];\n\n  for (const file of files) {\n    const subPath = path.join(dir, file);\n    const stats = await fs.lstat(subPath);\n    const key = path.join(relativePath, file);\n\n    if (blacklist.includes(file) || stats.isSymbolicLink()) {\n      continue;\n    }\n\n    if (stats.isDirectory()) {\n      const children = await readDirs(subPath, baseDir, blacklist, sort);\n      result.push({\n        title: file,\n        key,\n        type: 'directory',\n        parent: relativePath,\n        createTime: stats.birthtime.getTime(),\n        children: children.sort(sort),\n      });\n    } else {\n      result.push({\n        title: file,\n        type: 'file',\n        key,\n        parent: relativePath,\n        size: stats.size,\n        createTime: stats.birthtime.getTime(),\n      });\n    }\n  }\n\n  return result.sort(sort);\n}\n\nexport async function readDir(\n  dir: string,\n  baseDir: string = '',\n  blacklist: string[] = [],\n): Promise<IFile[]> {\n  const absoluteDir = path.join(baseDir, dir);\n  const relativePath = path.relative(baseDir, absoluteDir);\n\n  try {\n    const files = await fs.readdir(absoluteDir);\n    const result: IFile[] = [];\n\n    for (const file of files) {\n      const subPath = path.join(absoluteDir, file);\n      const stats = await fs.lstat(subPath);\n      const key = path.join(relativePath, file);\n\n      if (blacklist.includes(file) || stats.isSymbolicLink()) {\n        continue;\n      }\n\n      if (stats.isDirectory()) {\n        result.push({\n          title: file,\n          type: 'directory',\n          key,\n          parent: relativePath,\n          createTime: stats.birthtime.getTime(),\n          children: [],\n        });\n      } else {\n        result.push({\n          title: file,\n          type: 'file',\n          key,\n          parent: relativePath,\n          size: stats.size,\n          createTime: stats.birthtime.getTime(),\n        });\n      }\n    }\n\n    return result;\n  } catch (error: any) {\n    if (error.code === 'ENOENT') {\n      return [];\n    }\n    throw error;\n  }\n}\n\nexport async function promiseExec(command: string): Promise<string> {\n  try {\n    const { stderr, stdout } = await promisify(exec)(command, {\n      maxBuffer: 200 * 1024 * 1024,\n      encoding: 'utf8',\n    });\n    return stdout || stderr;\n  } catch (error) {\n    return JSON.stringify(error);\n  }\n}\n\nexport async function promiseExecSuccess(command: string): Promise<string> {\n  try {\n    const { stdout } = await promisify(exec)(command, {\n      maxBuffer: 200 * 1024 * 1024,\n      encoding: 'utf8',\n    });\n    return stdout || '';\n  } catch (error) {\n    return '';\n  }\n}\n\nexport function parseHeaders(headers: string) {\n  if (!headers) return {};\n\n  const parsed: any = {};\n  let key: string;\n  let val: string;\n  let i: number;\n\n  headers &&\n    headers.split('\\n').forEach(function parser(line) {\n      i = line.indexOf(':');\n      key = line.substring(0, i).trim().toLowerCase();\n      val = line.substring(i + 1).trim();\n\n      if (!key) {\n        return;\n      }\n\n      parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;\n    });\n\n  return parsed;\n}\n\nfunction parseString(\n  input: string,\n  valueFormatFn?: (v: string) => string,\n): Record<string, string> {\n  const regex = /(\\w+):\\s*((?:(?!\\n\\w+:).)*)/g;\n  const matches: Record<string, string> = {};\n\n  let match;\n  while ((match = regex.exec(input)) !== null) {\n    const [, key, value] = match;\n    const _key = key.trim();\n    if (!_key || matches[_key]) {\n      continue;\n    }\n\n    let _value = value.trim();\n\n    try {\n      _value = valueFormatFn ? valueFormatFn(_value) : _value;\n      const jsonValue = JSON.parse(_value);\n      matches[_key] = jsonValue;\n    } catch (error) {\n      matches[_key] = _value;\n    }\n  }\n\n  return matches;\n}\n\nexport function parseBody(\n  body: string,\n  contentType:\n    | 'application/json'\n    | 'multipart/form-data'\n    | 'application/x-www-form-urlencoded'\n    | 'text/plain',\n  valueFormatFn?: (v: string) => string,\n) {\n  if (contentType === 'text/plain' || !body) {\n    return valueFormatFn && body ? valueFormatFn(body) : body;\n  }\n\n  const parsed = parseString(body, valueFormatFn);\n\n  switch (contentType) {\n    case 'multipart/form-data':\n      return Object.keys(parsed).reduce((p, c) => {\n        p.append(c, parsed[c]);\n        return p;\n      }, new FormData());\n    case 'application/x-www-form-urlencoded':\n      return Object.keys(parsed).reduce((p, c) => {\n        return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;\n      });\n  }\n\n  return parsed;\n}\n\nexport function psTree(pid: number): Promise<number[]> {\n  return new Promise((resolve, reject) => {\n    psTreeFun(pid, (err: any, children) => {\n      if (err) {\n        reject(err);\n      }\n      resolve(children.map((x) => Number(x.PID)).filter((x) => !isNaN(x)));\n    });\n  });\n}\n\nexport async function killTask(pid: number) {\n  const pids = await psTree(pid);\n\n  if (pids.length) {\n    try {\n      [pid, ...pids].reverse().forEach((x) => {\n        process.kill(x, 15);\n      });\n    } catch (error) {}\n  } else {\n    process.kill(pid, 2);\n  }\n}\n\nexport async function getPid(cmd: string) {\n  const taskCommand = `ps -eo pid,command | grep \"${cmd}\" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;\n  const pid = await promiseExec(taskCommand);\n  return pid ? Number(pid) : undefined;\n}\n\nexport async function getAllPids(cmd: string): Promise<number[]> {\n  const taskCommand = `ps -eo pid,command | grep \"${cmd}\" | grep -v grep | awk '{print $1}'`;\n  const pidsStr = await promiseExec(taskCommand);\n  if (!pidsStr) return [];\n  return pidsStr\n    .split('\\n')\n    .map((p) => Number(p.trim()))\n    .filter((p) => !isNaN(p) && p > 0);\n}\n\nexport async function killAllTasks(cmd: string): Promise<void> {\n  const pids = await getAllPids(cmd);\n  for (const pid of pids) {\n    try {\n      await killTask(pid);\n    } catch (error) {\n      // Ignore errors if process already terminated\n    }\n  }\n}\n\ninterface IVersion {\n  version: string;\n  changeLogLink: string;\n  changeLog: string;\n  publishTime: string;\n}\n\nexport async function parseVersion(path: string): Promise<IVersion> {\n  return load(await fs.readFile(path, 'utf8')) as IVersion;\n}\n\nexport function parseContentVersion(content: string): IVersion {\n  return load(content) as IVersion;\n}\n\nexport async function getUniqPath(\n  command: string,\n  id: string,\n): Promise<string> {\n  let suffix = '';\n  if (/^\\d+$/.test(id)) {\n    suffix = `_${id}`;\n  }\n\n  let items = command.split(/ +/);\n\n  const maxTimeCommandIndex = items.findIndex((x) => x === '-m');\n  if (maxTimeCommandIndex !== -1) {\n    items = items.slice(maxTimeCommandIndex + 2);\n  }\n\n  let str = items[0];\n  if (items[0] === TASK_COMMAND) {\n    str = items[1];\n  }\n\n  const dotIndex = str.lastIndexOf('.');\n\n  if (dotIndex !== -1) {\n    str = str.slice(0, dotIndex);\n  }\n\n  const slashIndex = str.lastIndexOf('/');\n\n  let tempStr = '';\n  if (slashIndex !== -1) {\n    tempStr = str.slice(0, slashIndex);\n    const _slashIndex = tempStr.lastIndexOf('/');\n    if (_slashIndex !== -1) {\n      tempStr = tempStr.slice(_slashIndex + 1);\n    }\n    str = `${tempStr}_${str.slice(slashIndex + 1)}`;\n  }\n\n  return `${str}${suffix}`;\n}\n\nexport function safeJSONParse(value?: string) {\n  if (!value) {\n    return {};\n  }\n\n  try {\n    return JSON.parse(value);\n  } catch (error) {\n    Logger.error('[safeJSONParse失败]', error);\n    return {};\n  }\n}\n\nexport async function rmPath(path: string) {\n  try {\n    const _exsit = await fileExist(path);\n    if (_exsit) {\n      await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });\n    }\n  } catch (error) {\n    Logger.error('[rmPath失败]', error);\n  }\n}\n\nexport async function setSystemTimezone(timezone: string): Promise<boolean> {\n  try {\n    if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) {\n      throw new Error('Invalid timezone');\n    }\n\n    await promiseExec(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`);\n    await promiseExec(`echo \"${timezone}\" > /etc/timezone`);\n\n    return true;\n  } catch (error) {\n    Logger.error('[setSystemTimezone失败]', error);\n    return false;\n  }\n}\n\nexport function getGetCommand(type: DependenceTypes, name: string): string {\n  const baseCommands = {\n    [DependenceTypes.nodejs]: `pnpm ls -g  | grep \"${name}\" | head -1`,\n    [DependenceTypes.python3]: `\n    python3 -c \"exec('''\nname='${name}'\ntry:\n    from importlib.metadata import version\n    print(version(name))\nexcept:\n    import importlib.util as u\n    import importlib.metadata as m\n    spec=u.find_spec(name)\n    print(name if spec else '')\n''')\"`,\n    [DependenceTypes.linux]: `apk info -es ${name}`,\n  };\n\n  return baseCommands[type];\n}\n\nexport function getInstallCommand(type: DependenceTypes, name: string): string {\n  const baseCommands = {\n    [DependenceTypes.nodejs]: 'pnpm add -g',\n    [DependenceTypes.python3]:\n      'pip3 install --disable-pip-version-check --root-user-action=ignore',\n    [DependenceTypes.linux]: 'apk add --no-check-certificate',\n  };\n\n  let command = baseCommands[type];\n\n  if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {\n    command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;\n  }\n\n  return `${command} ${name.trim()}`;\n}\n\nexport function getUninstallCommand(\n  type: DependenceTypes,\n  name: string,\n): string {\n  const baseCommands = {\n    [DependenceTypes.nodejs]: 'pnpm remove -g',\n    [DependenceTypes.python3]:\n      'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',\n    [DependenceTypes.linux]: 'apk del',\n  };\n\n  return `${baseCommands[type]} ${name.trim()}`;\n}\n\nexport function isDemoEnv() {\n  return process.env.DeployEnv === 'demo';\n}\n"
  },
  {
    "path": "back/data/cron.ts",
    "content": "import { sequelize } from '.';\nimport { DataTypes, Model, ModelDefined } from 'sequelize';\n\nexport class Crontab {\n  name?: string;\n  command: string;\n  schedule?: string;\n  timestamp?: string;\n  saved?: boolean;\n  id?: number;\n  status?: CrontabStatus;\n  isSystem?: 1 | 0;\n  pid?: number;\n  isDisabled?: 1 | 0;\n  log_path?: string;\n  isPinned?: 1 | 0;\n  labels?: string[];\n  last_running_time?: number;\n  last_execution_time?: number;\n  sub_id?: number;\n  extra_schedules?: Array<{ schedule: string }>;\n  task_before?: string;\n  task_after?: string;\n  log_name?: string;\n  allow_multiple_instances?: 1 | 0;\n\n  constructor(options: Crontab) {\n    this.name = options.name;\n    this.command = options.command.trim();\n    this.schedule = options.schedule;\n    this.saved = options.saved;\n    this.id = options.id;\n    this.status =\n      typeof options.status === 'number' && CrontabStatus[options.status]\n        ? options.status\n        : CrontabStatus.idle;\n    this.timestamp = new Date().toString();\n    this.isSystem = options.isSystem || 0;\n    this.pid = options.pid;\n    this.isDisabled = options.isDisabled || 0;\n    this.log_path = options.log_path || '';\n    this.isPinned = options.isPinned || 0;\n    this.labels = options.labels || [];\n    this.last_running_time = options.last_running_time || 0;\n    this.last_execution_time = options.last_execution_time || 0;\n    this.sub_id = options.sub_id;\n    this.extra_schedules = options.extra_schedules;\n    this.task_before = options.task_before;\n    this.task_after = options.task_after;\n    this.log_name = options.log_name;\n    this.allow_multiple_instances = options.allow_multiple_instances || 0;\n  }\n}\n\nexport enum CrontabStatus {\n  'running' = 0,\n  'queued' = 0.5,\n  'idle' = 1,\n  'disabled',\n}\n\nexport interface CronInstance extends Model<Crontab, Crontab>, Crontab {}\nexport const CrontabModel = sequelize.define<CronInstance>('Crontab', {\n  name: {\n    unique: 'compositeIndex',\n    type: DataTypes.STRING,\n  },\n  command: {\n    unique: 'compositeIndex',\n    type: DataTypes.STRING,\n  },\n  schedule: {\n    unique: 'compositeIndex',\n    type: DataTypes.STRING,\n  },\n  timestamp: DataTypes.STRING,\n  saved: DataTypes.BOOLEAN,\n  status: DataTypes.NUMBER,\n  isSystem: DataTypes.NUMBER,\n  pid: DataTypes.NUMBER,\n  isDisabled: DataTypes.NUMBER,\n  isPinned: DataTypes.NUMBER,\n  log_path: DataTypes.STRING,\n  labels: DataTypes.JSON,\n  last_running_time: DataTypes.NUMBER,\n  last_execution_time: DataTypes.NUMBER,\n  sub_id: { type: DataTypes.NUMBER, allowNull: true },\n  extra_schedules: DataTypes.JSON,\n  task_before: DataTypes.STRING,\n  task_after: DataTypes.STRING,\n  log_name: DataTypes.STRING,\n  allow_multiple_instances: DataTypes.NUMBER,\n});\n"
  },
  {
    "path": "back/data/cronView.ts",
    "content": "import { sequelize } from '.';\nimport { DataTypes, Model } from 'sequelize';\n\nexport enum CronViewType {\n  '系统' = 1,\n  '个人',\n}\n\ninterface SortType {\n  type: 'ASC' | 'DESC';\n  value: string;\n}\n\ninterface FilterType {\n  property: string;\n  operation: string;\n  value: string;\n}\n\nexport class CrontabView {\n  name?: string;\n  id?: number;\n  position?: number;\n  isDisabled?: 1 | 0;\n  filters?: FilterType[];\n  sorts?: SortType[];\n  filterRelation?: 'and' | 'or';\n  type?: CronViewType;\n\n  constructor(options: CrontabView) {\n    this.name = options.name;\n    this.id = options.id;\n    this.position = options.position;\n    this.isDisabled = options.isDisabled || 0;\n    this.filters = options.filters;\n    this.sorts = options.sorts;\n    this.filterRelation = options.filterRelation;\n    this.type = options.type || CronViewType.个人;\n  }\n}\n\nexport interface CronViewInstance\n  extends Model<CrontabView, CrontabView>,\n    CrontabView {}\nexport const CrontabViewModel = sequelize.define<CronViewInstance>(\n  'CrontabView',\n  {\n    name: {\n      unique: 'name',\n      type: DataTypes.STRING,\n    },\n    position: DataTypes.NUMBER,\n    isDisabled: DataTypes.NUMBER,\n    filters: DataTypes.JSON,\n    sorts: DataTypes.JSON,\n    filterRelation: {\n      type: DataTypes.STRING,\n      allowNull: true,\n    },\n    type: DataTypes.NUMBER,\n  },\n);\n"
  },
  {
    "path": "back/data/dependence.ts",
    "content": "import { sequelize } from '.';\nimport { DataTypes, Model, ModelDefined } from 'sequelize';\n\nexport class Dependence {\n  timestamp?: string;\n  id?: number;\n  status: DependenceStatus;\n  type: DependenceTypes;\n  name: string;\n  log?: string[];\n  remark?: string;\n\n  constructor(options: Dependence) {\n    this.id = options.id;\n    this.status =\n      typeof options.status === 'number' && DependenceStatus[options.status]\n        ? options.status\n        : DependenceStatus.queued;\n    this.type = options.type || DependenceTypes.nodejs;\n    this.timestamp = new Date().toString();\n    this.name = options.name.trim();\n    this.log = options.log || [];\n    this.remark = options.remark || '';\n  }\n}\n\nexport enum DependenceStatus {\n  'installing',\n  'installed',\n  'installFailed',\n  'removing',\n  'removed',\n  'removeFailed',\n  'queued',\n  'cancelled',\n}\n\nexport enum DependenceTypes {\n  'nodejs',\n  'python3',\n  'linux',\n}\n\nexport enum versionDependenceCommandTypes {\n  '@',\n  '==',\n  '=',\n}\n\nexport interface DependenceInstance\n  extends Model<Dependence, Dependence>,\n    Dependence {}\nexport const DependenceModel = sequelize.define<DependenceInstance>(\n  'Dependence',\n  {\n    name: DataTypes.STRING,\n    type: DataTypes.NUMBER,\n    timestamp: DataTypes.STRING,\n    status: DataTypes.NUMBER,\n    log: DataTypes.JSON,\n    remark: DataTypes.STRING,\n  },\n);\n"
  },
  {
    "path": "back/data/env.ts",
    "content": "import { DataTypes, Model } from 'sequelize';\nimport { sequelize } from '.';\n\nexport class Env {\n  value?: string;\n  timestamp?: string;\n  id?: number;\n  status?: EnvStatus;\n  position?: number;\n  name?: string;\n  remarks?: string;\n  isPinned?: 1 | 0;\n\n  constructor(options: Env) {\n    this.value = options.value;\n    this.id = options.id;\n    this.status =\n      typeof options.status === 'number' && EnvStatus[options.status]\n        ? options.status\n        : EnvStatus.normal;\n    this.timestamp = new Date().toString();\n    this.position = options.position;\n    this.name = options.name;\n    this.remarks = options.remarks || '';\n    this.isPinned = options.isPinned || 0;\n  }\n}\n\nexport enum EnvStatus {\n  'normal',\n  'disabled',\n}\n\nexport const maxPosition = 9000000000000000;\nexport const initPosition = 4500000000000000;\nexport const stepPosition = 10000000000;\nexport const minPosition = 100;\n\nexport interface EnvInstance extends Model<Env, Env>, Env {}\nexport const EnvModel = sequelize.define<EnvInstance>('Env', {\n  value: { type: DataTypes.STRING, unique: 'compositeIndex' },\n  timestamp: DataTypes.STRING,\n  status: DataTypes.NUMBER,\n  position: DataTypes.NUMBER,\n  name: { type: DataTypes.STRING, unique: 'compositeIndex' },\n  remarks: DataTypes.STRING,\n  isPinned: DataTypes.NUMBER,\n});\n"
  },
  {
    "path": "back/data/index.ts",
    "content": "import { Sequelize, Transaction } from 'sequelize';\nimport config from '../config/index';\nimport { join } from 'path';\n\nexport const sequelize = new Sequelize({\n  dialect: 'sqlite',\n  storage: join(config.dbPath, 'database.sqlite'),\n  logging: false,\n  retry: {\n    max: 10,\n    match: ['SQLITE_BUSY: database is locked'],\n  },\n  pool: {\n    max: 5,\n    min: 2,\n    idle: 30000,\n    acquire: 30000,\n    evict: 10000,\n  },\n  transactionType: Transaction.TYPES.IMMEDIATE,\n});\n\nexport type ResponseType<T> = { code: number; data?: T; message?: string };\n"
  },
  {
    "path": "back/data/notify.ts",
    "content": "export enum NotificationMode {\n  'gotify' = 'gotify',\n  'goCqHttpBot' = 'goCqHttpBot',\n  'serverChan' = 'serverChan',\n  'pushDeer' = 'pushDeer',\n  'bark' = 'bark',\n  'chat' = 'chat',\n  'telegramBot' = 'telegramBot',\n  'dingtalkBot' = 'dingtalkBot',\n  'weWorkBot' = 'weWorkBot',\n  'weWorkApp' = 'weWorkApp',\n  'aibotk' = 'aibotk',\n  'iGot' = 'iGot',\n  'pushPlus' = 'pushPlus',\n  'wePlusBot' = 'wePlusBot',\n  'email' = 'email',\n  'pushMe' = 'pushMe',\n  'feishu' = 'feishu',\n  'webhook' = 'webhook',\n  'chronocat' = 'Chronocat',\n  'ntfy' = 'ntfy',\n  'wxPusherBot' = 'wxPusherBot',\n}\n\nabstract class NotificationBaseInfo {\n  public type!: NotificationMode;\n}\n\nexport class GotifyNotification extends NotificationBaseInfo {\n  public gotifyUrl = '';\n  public gotifyToken = '';\n  public gotifyPriority = 0;\n}\n\nexport class GoCqHttpBotNotification extends NotificationBaseInfo {\n  public goCqHttpBotUrl = '';\n  public goCqHttpBotToken = '';\n  public goCqHttpBotQq = '';\n}\n\nexport class ServerChanNotification extends NotificationBaseInfo {\n  public serverChanKey = '';\n}\n\nexport class PushDeerNotification extends NotificationBaseInfo {\n  public pushDeerKey = '';\n  public pushDeerUrl = '';\n}\n\nexport class synologyChatNotification extends NotificationBaseInfo {\n  public synologyChatUrl = '';\n}\n\nexport class BarkNotification extends NotificationBaseInfo {\n  public barkPush = '';\n  public barkIcon = 'https://qn.whyour.cn/logo.png';\n  public barkSound = '';\n  public barkGroup = 'qinglong';\n  public barkLevel = 'active';\n  public barkUrl = '';\n  public barkArchive = '';\n}\n\nexport class TelegramBotNotification extends NotificationBaseInfo {\n  public telegramBotToken = '';\n  public telegramBotUserId = '';\n  public telegramBotProxyHost = '';\n  public telegramBotProxyPort = '';\n  public telegramBotProxyAuth = '';\n  public telegramBotApiHost = 'https://api.telegram.org';\n}\n\nexport class DingtalkBotNotification extends NotificationBaseInfo {\n  public dingtalkBotToken = '';\n  public dingtalkBotSecret = '';\n}\n\nexport class WeWorkBotNotification extends NotificationBaseInfo {\n  public weWorkBotKey = '';\n  public weWorkOrigin = '';\n}\n\nexport class WeWorkAppNotification extends NotificationBaseInfo {\n  public weWorkAppKey = '';\n  public weWorkOrigin = '';\n}\n\nexport class AibotkNotification extends NotificationBaseInfo {\n  public aibotkKey: string = '';\n  public aibotkType: 'room' | 'contact' = 'room';\n  public aibotkName: string = '';\n}\n\nexport class IGotNotification extends NotificationBaseInfo {\n  public iGotPushKey = '';\n}\n\nexport class PushPlusNotification extends NotificationBaseInfo {\n  public pushPlusToken = '';\n  public pushPlusUser = '';\n  public pushPlusTemplate = '';\n  public pushplusChannel = '';\n  public pushplusWebhook = '';\n  public pushplusCallbackUrl = '';\n  public pushplusTo = '';\n}\n\nexport class WePlusBotNotification extends NotificationBaseInfo {\n  public wePlusBotToken = '';\n  public wePlusBotReceiver = '';\n  public wePlusBotVersion = '';\n}\n\nexport class EmailNotification extends NotificationBaseInfo {\n  public emailService: string = '';\n  public emailUser: string = '';\n  public emailPass: string = '';\n  public emailTo: string = '';\n}\n\nexport class PushMeNotification extends NotificationBaseInfo {\n  public pushMeKey: string = '';\n  public pushMeUrl: string = '';\n}\n\nexport class ChronocatNotification extends NotificationBaseInfo {\n  public chronocatURL: string = '';\n  public chronocatQQ: string = '';\n  public chronocatToken: string = '';\n}\n\nexport class WebhookNotification extends NotificationBaseInfo {\n  public webhookHeaders: string = '';\n  public webhookBody: string = '';\n  public webhookUrl: string = '';\n  public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';\n  public webhookContentType:\n    | 'application/json'\n    | 'multipart/form-data'\n    | 'application/x-www-form-urlencoded' = 'application/json';\n}\n\nexport class LarkNotification extends NotificationBaseInfo {\n  public larkKey = '';\n  public larkSecret = '';\n}\n\nexport class NtfyNotification extends NotificationBaseInfo {\n  public ntfyUrl = '';\n  public ntfyTopic = '';\n  public ntfyPriority = '';\n  public ntfyToken = '';\n  public ntfyUsername = '';\n  public ntfyPassword = '';\n  public ntfyActions = '';\n}\n\nexport class WxPusherBotNotification extends NotificationBaseInfo {\n  public wxPusherBotAppToken = '';\n  public wxPusherBotTopicIds = '';\n  public wxPusherBotUids = '';\n}\n\nexport interface NotificationInfo\n  extends GoCqHttpBotNotification,\n    GotifyNotification,\n    ServerChanNotification,\n    PushDeerNotification,\n    synologyChatNotification,\n    BarkNotification,\n    TelegramBotNotification,\n    DingtalkBotNotification,\n    WeWorkBotNotification,\n    WeWorkAppNotification,\n    AibotkNotification,\n    IGotNotification,\n    PushPlusNotification,\n    WePlusBotNotification,\n    EmailNotification,\n    PushMeNotification,\n    WebhookNotification,\n    ChronocatNotification,\n    LarkNotification,\n    NtfyNotification,\n    WxPusherBotNotification {}\n"
  },
  {
    "path": "back/data/open.ts",
    "content": "import { sequelize } from '.';\nimport { DataTypes, Model, ModelDefined } from 'sequelize';\n\nexport class App {\n  name: string;\n  scopes: AppScope[];\n  client_id: string;\n  client_secret: string;\n  tokens?: AppToken[];\n  id?: number;\n\n  constructor(options: App) {\n    this.name = options.name;\n    this.scopes = options.scopes;\n    this.client_id = options.client_id;\n    this.client_secret = options.client_secret;\n    this.id = options.id;\n  }\n}\n\nexport interface AppToken {\n  value: string;\n  type?: 'Bearer';\n  expiration: number;\n}\n\nexport type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system';\n\nexport interface AppInstance extends Model<App, App>, App {}\nexport const AppModel = sequelize.define<AppInstance>('App', {\n  name: { type: DataTypes.STRING, unique: 'name' },\n  scopes: DataTypes.JSON,\n  client_id: DataTypes.STRING,\n  client_secret: DataTypes.STRING,\n  tokens: DataTypes.JSON,\n});\n"
  },
  {
    "path": "back/data/sock.ts",
    "content": "export class SockMessage {\n  message?: string;\n  type?: SockMessageType;\n  references?: number[];\n\n  constructor(options: SockMessage) {\n    this.type = options.type;\n    this.message = options.message;\n    this.references = options.references;\n  }\n}\n\nexport type SockMessageType =\n  | 'ping'\n  | 'installDependence'\n  | 'uninstallDependence'\n  | 'updateSystemVersion'\n  | 'manuallyRunScript'\n  | 'runSubscriptionEnd'\n  | 'reloadSystem'\n  | 'updateNodeMirror'\n  | 'updateLinuxMirror';\n"
  },
  {
    "path": "back/data/subscription.ts",
    "content": "import { sequelize } from '.';\nimport { DataTypes, Model, ModelDefined } from 'sequelize';\nimport { SimpleIntervalSchedule } from 'toad-scheduler';\n\ntype SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;\nexport class Subscription {\n  id?: number;\n  name?: string;\n  type?: 'public-repo' | 'private-repo' | 'file';\n  schedule_type?: 'crontab' | 'interval';\n  schedule?: string;\n  interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };\n  url?: string;\n  whitelist?: string;\n  blacklist?: string;\n  dependences?: string;\n  branch?: string;\n  status?: SubscriptionStatus;\n  pull_type?: 'ssh-key' | 'user-pwd';\n  pull_option?:\n    | { private_key: string }\n    | { username: string; password: string };\n  pid?: number;\n  is_disabled?: 1 | 0;\n  log_path?: string;\n  alias: string;\n  command?: string;\n  extensions?: string;\n  sub_before?: string;\n  sub_after?: string;\n  proxy?: string;\n  autoAddCron?: 1 | 0;\n  autoDelCron?: 1 | 0;\n\n  constructor(options: Subscription) {\n    this.id = options.id;\n    this.name = options.name || options.alias;\n    this.type = options.type;\n    this.schedule = options.schedule;\n    this.status = this.status =\n      typeof options.status === 'number' && SubscriptionStatus[options.status]\n        ? options.status\n        : SubscriptionStatus.idle;\n    this.url = options.url;\n    this.whitelist = options.whitelist;\n    this.blacklist = options.blacklist;\n    this.dependences = options.dependences;\n    this.branch = options.branch;\n    this.pull_type = options.pull_type;\n    this.pull_option = options.pull_option;\n    this.pid = options.pid;\n    this.is_disabled = options.is_disabled;\n    this.log_path = options.log_path;\n    this.schedule_type = options.schedule_type;\n    this.alias = options.alias;\n    this.interval_schedule = options.interval_schedule;\n    this.extensions = options.extensions;\n    this.sub_before = options.sub_before;\n    this.sub_after = options.sub_after;\n    this.proxy = options.proxy;\n    this.autoAddCron = options.autoAddCron ? 1 : 0;\n    this.autoDelCron = options.autoDelCron ? 1 : 0;\n  }\n}\n\nexport enum SubscriptionStatus {\n  'running',\n  'idle',\n  'disabled',\n  'queued',\n}\n\nexport interface SubscriptionInstance\n  extends Model<Subscription, Subscription>,\n    Subscription {}\nexport const SubscriptionModel = sequelize.define<SubscriptionInstance>(\n  'Subscription',\n  {\n    name: {\n      unique: 'compositeIndex',\n      type: DataTypes.STRING,\n    },\n    url: {\n      unique: 'compositeIndex',\n      type: DataTypes.STRING,\n    },\n    schedule: {\n      unique: 'compositeIndex',\n      type: DataTypes.STRING,\n    },\n    interval_schedule: {\n      unique: 'compositeIndex',\n      type: DataTypes.JSON,\n    },\n    type: DataTypes.STRING,\n    whitelist: DataTypes.STRING,\n    blacklist: DataTypes.STRING,\n    status: DataTypes.NUMBER,\n    dependences: DataTypes.STRING,\n    extensions: DataTypes.STRING,\n    sub_before: DataTypes.STRING,\n    sub_after: DataTypes.STRING,\n    branch: DataTypes.STRING,\n    pull_type: DataTypes.STRING,\n    pull_option: DataTypes.JSON,\n    pid: DataTypes.NUMBER,\n    is_disabled: DataTypes.NUMBER,\n    log_path: DataTypes.STRING,\n    schedule_type: DataTypes.STRING,\n    alias: { type: DataTypes.STRING, unique: 'alias' },\n    proxy: { type: DataTypes.STRING, allowNull: true },\n    autoAddCron: { type: DataTypes.NUMBER, allowNull: true },\n    autoDelCron: { type: DataTypes.NUMBER, allowNull: true },\n  },\n);\n"
  },
  {
    "path": "back/data/system.ts",
    "content": "import { sequelize } from '.';\nimport { DataTypes, Model, ModelDefined } from 'sequelize';\nimport { NotificationInfo } from './notify';\n\nexport class SystemInfo {\n  ip?: string;\n  type: AuthDataType;\n  info?: SystemModelInfo;\n  id?: number;\n\n  constructor(options: SystemInfo) {\n    this.ip = options.ip;\n    this.info = options.info;\n    this.type = options.type;\n    this.id = options.id;\n  }\n}\n\nexport enum LoginStatus {\n  'success',\n  'fail',\n}\n\nexport enum AuthDataType {\n  'loginLog' = 'loginLog',\n  'authToken' = 'authToken',\n  'notification' = 'notification',\n  'removeLogFrequency' = 'removeLogFrequency',\n  'systemConfig' = 'systemConfig',\n  'authConfig' = 'authConfig',\n}\n\nexport interface SystemConfigInfo {\n  logRemoveFrequency?: number;\n  cronConcurrency?: number;\n  dependenceProxy?: string;\n  nodeMirror?: string;\n  pythonMirror?: string;\n  linuxMirror?: string;\n  timezone?: string;\n  globalSshKey?: string;\n}\n\nexport interface LoginLogInfo {\n  timestamp?: number;\n  address?: string;\n  ip?: string;\n  platform?: string;\n  status?: LoginStatus;\n}\n\nexport interface TokenInfo {\n  value: string;\n  timestamp: number;\n  ip: string;\n  address: string;\n  platform: string;\n  /**\n   * Token expiration time in seconds since Unix epoch.\n   * If undefined, the token uses JWT's built-in expiration.\n   */\n  expiration?: number;\n}\n\nexport interface AuthInfo {\n  username: string;\n  password: string;\n  retries: number;\n  lastlogon: number;\n  lastip: string;\n  lastaddr: string;\n  platform: string;\n  isTwoFactorChecking: boolean;\n  token: string;\n  tokens: Record<string, string | TokenInfo[]>;\n  twoFactorActivated: boolean;\n  twoFactorSecret: string;\n  avatar: string;\n}\n\nexport type SystemModelInfo = SystemConfigInfo &\n  Partial<NotificationInfo> &\n  LoginLogInfo &\n  Partial<AuthInfo>;\n\nexport interface SystemInstance\n  extends Model<SystemInfo, SystemInfo>,\n    SystemInfo {}\nexport const SystemModel = sequelize.define<SystemInstance>('Auth', {\n  ip: DataTypes.STRING,\n  type: DataTypes.STRING,\n  info: {\n    type: DataTypes.JSON,\n    allowNull: true,\n  },\n});\n"
  },
  {
    "path": "back/interface/schedule.ts",
    "content": "export enum ScheduleType {\n  BOOT = '@boot',\n  ONCE = '@once',\n}\n\nexport type ScheduleValidator = (schedule?: string) => boolean;\nexport type CronSchedulerPayload = {\n  name: string;\n  id: string;\n  schedule: string;\n  command: string;\n  extra_schedules: Array<{ schedule: string }>;\n};\n"
  },
  {
    "path": "back/loaders/app.ts",
    "content": "import expressLoader from './express';\nimport depInjectorLoader from './depInjector';\nimport Logger from './logger';\nimport initData from './initData';\nimport { Application } from 'express';\nimport linkDeps from './deps';\nimport initTask from './initTask';\nimport initFile from './initFile';\n\nexport default async ({ app }: { app: Application }) => {\n  depInjectorLoader();\n  Logger.info('✌️ Dependency loaded');\n\n  await linkDeps();\n  Logger.info('✌️ Link deps loaded');\n\n  initFile();\n  Logger.info('✌️ Init file loaded');\n\n  await initData();\n  Logger.info('✌️ Init data loaded');\n\n  initTask();\n  Logger.info('✌️ Init task loaded');\n\n  expressLoader({ app });\n  Logger.info('✌️ Express loaded');\n};\n"
  },
  {
    "path": "back/loaders/bootAfter.ts",
    "content": "import Container from 'typedi';\nimport CronService from '../services/cron';\n\nexport default async () => {\n  const cronService = Container.get(CronService);\n\n  await cronService.bootTask();\n};\n"
  },
  {
    "path": "back/loaders/db.ts",
    "content": "import Logger from './logger';\nimport { EnvModel } from '../data/env';\nimport { CrontabModel } from '../data/cron';\nimport { DependenceModel } from '../data/dependence';\nimport { AppModel } from '../data/open';\nimport { SystemModel } from '../data/system';\nimport { SubscriptionModel } from '../data/subscription';\nimport { CrontabViewModel } from '../data/cronView';\nimport { sequelize } from '../data';\n\nexport default async () => {\n  try {\n    await CrontabModel.sync();\n    await DependenceModel.sync();\n    await AppModel.sync();\n    await SystemModel.sync();\n    await EnvModel.sync();\n    await SubscriptionModel.sync();\n    await CrontabViewModel.sync();\n\n    // 初始化新增字段\n    const migrations = [\n      {\n        table: 'CrontabViews',\n        column: 'filterRelation',\n        type: 'VARCHAR(255)',\n      },\n      { table: 'Subscriptions', column: 'proxy', type: 'VARCHAR(255)' },\n      { table: 'CrontabViews', column: 'type', type: 'NUMBER' },\n      { table: 'Subscriptions', column: 'autoAddCron', type: 'NUMBER' },\n      { table: 'Subscriptions', column: 'autoDelCron', type: 'NUMBER' },\n      { table: 'Crontabs', column: 'sub_id', type: 'NUMBER' },\n      { table: 'Crontabs', column: 'extra_schedules', type: 'JSON' },\n      { table: 'Crontabs', column: 'task_before', type: 'TEXT' },\n      { table: 'Crontabs', column: 'task_after', type: 'TEXT' },\n      { table: 'Crontabs', column: 'log_name', type: 'VARCHAR(255)' },\n      {\n        table: 'Crontabs',\n        column: 'allow_multiple_instances',\n        type: 'NUMBER',\n      },\n      { table: 'Envs', column: 'isPinned', type: 'NUMBER' },\n    ];\n\n    for (const migration of migrations) {\n      try {\n        await sequelize.query(\n          `alter table ${migration.table} add column ${migration.column} ${migration.type}`,\n        );\n      } catch (error) {\n        // Column already exists or other error, continue\n      }\n    }\n\n    Logger.info('✌️ DB loaded');\n  } catch (error) {\n    Logger.error('✌️ DB load failed', error);\n  }\n};\n"
  },
  {
    "path": "back/loaders/depInjector.ts",
    "content": "import { Container } from 'typedi';\nimport LoggerInstance from './logger';\n\nexport default () => {\n  try {\n    Container.set('logger', LoggerInstance);\n  } catch (e) {\n    LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);\n    throw e;\n  }\n};\n"
  },
  {
    "path": "back/loaders/deps.ts",
    "content": "import path from 'path';\nimport fs from 'fs/promises';\nimport os from 'os';\nimport chokidar from 'chokidar';\nimport config from '../config/index';\nimport Logger from './logger';\n\nasync function linkToNodeModule(src: string, dst?: string) {\n  const target = path.join(config.rootPath, 'node_modules', dst || src);\n  const source = path.join(config.rootPath, src);\n\n  try {\n    const stats = await fs.lstat(target);\n    if (!stats) {\n      await fs.symlink(source, target, 'dir');\n    }\n  } catch (error) { }\n}\n\nasync function linkCommand() {\n  const homeDir = os.homedir();\n  let userBinDir = path.join(homeDir, 'bin');\n\n  try {\n    await fs.mkdir(userBinDir, { recursive: true });\n    await linkCommandToDir(userBinDir);\n  } catch (error) {\n    Logger.error('Linking command failed:', error);\n  }\n}\n\nasync function linkCommandToDir(commandDir: string) {\n  const linkShell = [\n    {\n      src: 'update.sh',\n      dest: 'ql',\n      tmp: 'ql_tmp',\n    },\n    {\n      src: 'task.sh',\n      dest: 'task',\n      tmp: 'task_tmp',\n    },\n  ];\n\n  for (const link of linkShell) {\n    const source = path.join(config.rootPath, 'shell', link.src);\n    const target = path.join(commandDir, link.dest);\n    const tmpTarget = path.join(commandDir, link.tmp);\n    try {\n      const stats = await fs.lstat(tmpTarget);\n      if (stats) {\n        await fs.unlink(tmpTarget);\n      }\n    } catch (error) { }\n\n    await fs.symlink(source, tmpTarget);\n    await fs.rename(tmpTarget, target);\n  }\n}\n\nexport default async (src: string = 'deps') => {\n  await linkCommand();\n  await linkToNodeModule(src);\n\n  const source = path.join(config.rootPath, src);\n  const watcher = chokidar.watch(source, {\n    ignored: /(^|[\\/\\\\])\\../, // ignore dotfiles\n    persistent: true,\n  });\n\n  watcher\n    .on('add', () => linkToNodeModule(src))\n    .on('change', () => linkToNodeModule(src));\n};\n"
  },
  {
    "path": "back/loaders/express.ts",
    "content": "import express, { Request, Response, NextFunction, Application } from 'express';\nimport bodyParser from 'body-parser';\nimport cors from 'cors';\nimport routes from '../api';\nimport config from '../config';\nimport { UnauthorizedError, expressjwt } from 'express-jwt';\nimport { getPlatform, getToken } from '../config/util';\nimport rewrite from 'express-urlrewrite';\nimport { errors } from 'celebrate';\nimport { serveEnv } from '../config/serverEnv';\nimport { IKeyvStore, shareStore } from '../shared/store';\nimport { isValidToken } from '../shared/auth';\nimport path from 'path';\n\nexport default ({ app }: { app: Application }) => {\n  // Security: Enable strict routing to prevent case-insensitive path bypass\n  app.set('case sensitive routing', true);\n  app.set('strict routing', true);\n  app.set('trust proxy', 'loopback');\n  app.use(cors());\n  \n  // Security: Path normalization middleware to prevent case variation attacks\n  app.use((req, res, next) => {\n    const originalPath = req.path;\n    const normalizedPath = originalPath.toLowerCase();\n    \n    // Block requests with case variations on protected paths\n    if (originalPath !== normalizedPath && \n        (normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) {\n      return res.status(400).json({\n        code: 400,\n        message: 'Invalid path format'\n      });\n    }\n    \n    next();\n  });\n  \n  // Rewrite URLs to strip baseUrl prefix if configured\n  // This allows the rest of the app to work without baseUrl awareness\n  if (config.baseUrl) {\n    app.use(rewrite(`${config.baseUrl}/*`, '/$1'));\n  }\n  \n  app.get(`${config.api.prefix}/env.js`, serveEnv);\n  app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));\n\n  app.use(bodyParser.json({ limit: '50mb' }));\n  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));\n\n  const frontendPath = path.join(config.rootPath, 'static/dist');\n  app.use(express.static(frontendPath));\n\n  app.use(\n    expressjwt({\n      secret: config.jwt.secret,\n      algorithms: ['HS384'],\n    }).unless({\n      path: [...config.apiWhiteList, /^(\\/(?!api\\/).*)$/i],\n    }),\n  );\n\n  app.use((req: Request, res, next) => {\n    if (!req.headers) {\n      req.platform = 'desktop';\n    } else {\n      const platform = getPlatform(req.headers['user-agent'] || '');\n      req.platform = platform;\n    }\n    return next();\n  });\n\n  app.use(async (req: Request, res, next) => {\n    const pathLower = req.path.toLowerCase();\n    if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) {\n      return next();\n    }\n\n    const headerToken = getToken(req);\n    if (pathLower.startsWith('/open/')) {\n      const apps = await shareStore.getApps();\n      const doc = apps?.filter((x) =>\n        x.tokens?.find((y) => y.value === headerToken),\n      )?.[0];\n      if (doc && doc.tokens && doc.tokens.length > 0) {\n        const currentToken = doc.tokens.find((x) => x.value === headerToken);\n        const keyMatch = pathLower.match(/\\/open\\/([a-z]+)\\/*/);\n        const key = keyMatch && keyMatch[1];\n        if (\n          doc.scopes.includes(key as any) &&\n          currentToken &&\n          currentToken.expiration >= Math.round(Date.now() / 1000)\n        ) {\n          return next();\n        }\n      }\n    }\n\n    const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;\n    if (\n      !headerToken &&\n      originPath &&\n      config.apiWhiteList.includes(originPath)\n    ) {\n      return next();\n    }\n\n    const authInfo = await shareStore.getAuthInfo();\n    if (isValidToken(authInfo, headerToken, req.platform)) {\n      return next();\n    }\n\n    const errorCode = headerToken ? 'invalid_token' : 'credentials_required';\n    const errorMessage = headerToken\n      ? 'jwt malformed'\n      : 'No authorization token was found';\n    const err = new UnauthorizedError(errorCode, { message: errorMessage });\n    next(err);\n  });\n\n  app.use(async (req, res, next) => {\n    const pathLower = req.path.toLowerCase();\n    if (\n      ![\n        '/api/user/init',\n        '/api/user/notification/init',\n        '/open/user/init',\n        '/open/user/notification/init',\n      ].includes(req.path)\n    ) {\n      return next();\n    }\n    const authInfo =\n      (await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);\n\n    let isInitialized = true;\n    if (\n      Object.keys(authInfo).length === 2 &&\n      authInfo.username === 'admin' &&\n      authInfo.password === 'admin'\n    ) {\n      isInitialized = false;\n    }\n\n    if (isInitialized) {\n      return res.send({ code: 450, message: '未知错误' });\n    } else {\n      return next();\n    }\n  });\n\n  app.use(rewrite('/open/*', '/api/$1'));\n  app.use(config.api.prefix, routes());\n\n  app.get('*', (_, res, next) => {\n    const indexPath = path.join(frontendPath, 'index.html');\n    res.sendFile(indexPath, (err) => {\n      if (err) {\n        const err: any = new Error('Not Found');\n        err['status'] = 404;\n        next(err);\n      }\n    });\n  });\n\n  app.use(errors());\n\n  app.use(\n    (\n      err: Error & { status: number },\n      req: Request,\n      res: Response,\n      next: NextFunction,\n    ) => {\n      if (err.name === 'UnauthorizedError') {\n        return res\n          .status(err.status)\n          .send({ code: 401, message: err.message })\n          .end();\n      }\n      return next(err);\n    },\n  );\n\n  app.use(\n    (\n      err: Error & { errors: any[] },\n      req: Request,\n      res: Response,\n      next: NextFunction,\n    ) => {\n      if (err.name.includes('Sequelize')) {\n        return res\n          .status(500)\n          .send({\n            code: 400,\n            message: `${err.message}`,\n            errors: err.errors,\n          })\n          .end();\n      }\n      return next(err);\n    },\n  );\n\n  app.use(\n    (\n      err: Error & { status: number },\n      req: Request,\n      res: Response,\n      next: NextFunction,\n    ) => {\n      res.status(err.status || 500);\n      res.json({\n        code: err.status || 500,\n        message: err.message,\n      });\n    },\n  );\n};\n"
  },
  {
    "path": "back/loaders/initData.ts",
    "content": "import DependenceService from '../services/dependence';\nimport { exec } from 'child_process';\nimport { Container } from 'typedi';\nimport { Crontab, CrontabModel, CrontabStatus } from '../data/cron';\nimport CronService from '../services/cron';\nimport EnvService from '../services/env';\nimport { DependenceModel, DependenceStatus } from '../data/dependence';\nimport { Op } from 'sequelize';\nimport config from '../config';\nimport { CrontabViewModel, CronViewType } from '../data/cronView';\nimport { initPosition } from '../data/env';\nimport { AuthDataType, SystemModel } from '../data/system';\nimport SystemService from '../services/system';\nimport UserService from '../services/user';\nimport { writeFile, readFile } from 'fs/promises';\nimport { createRandomString, fileExist, isDemoEnv, safeJSONParse } from '../config/util';\nimport OpenService from '../services/open';\nimport { shareStore } from '../shared/store';\nimport Logger from './logger';\nimport { AppModel } from '../data/open';\n\nexport default async () => {\n  const cronService = Container.get(CronService);\n  const envService = Container.get(EnvService);\n  const dependenceService = Container.get(DependenceService);\n  const systemService = Container.get(SystemService);\n  const userService = Container.get(UserService);\n  const openService = Container.get(OpenService);\n\n  // 初始化增加系统配置\n  let systemApp = (\n    await AppModel.findOne({\n      where: { name: 'system' },\n    })\n  )?.get({ plain: true });\n  if (!systemApp) {\n    systemApp = await AppModel.create({\n      name: 'system',\n      scopes: ['crons', 'system'],\n      client_id: createRandomString(12, 12),\n      client_secret: createRandomString(24, 24),\n    });\n  }\n  const [systemConfig] = await SystemModel.findOrCreate({\n    where: { type: AuthDataType.systemConfig },\n  });\n  await SystemModel.findOrCreate({\n    where: { type: AuthDataType.notification },\n  });\n  const [authConfig] = await SystemModel.findOrCreate({\n    where: { type: AuthDataType.authConfig },\n  });\n  if (!authConfig?.info || isDemoEnv()) {\n    let authInfo = {\n      username: 'admin',\n      password: 'admin',\n    };\n    try {\n      const authFileExist = await fileExist(config.authConfigFile);\n      if (authFileExist) {\n        const content = await readFile(config.authConfigFile, 'utf8');\n        authInfo = safeJSONParse(content);\n      }\n    } catch (error) {\n      Logger.warn('Failed to read auth config file, using default credentials');\n    }\n    await SystemModel.upsert({\n      id: authConfig?.id,\n      info: authInfo,\n      type: AuthDataType.authConfig,\n    });\n  }\n\n  const installDependencies = async () => {\n    const docs = await DependenceModel.findAll({\n      where: {},\n      order: [\n        ['type', 'DESC'],\n        ['createdAt', 'DESC'],\n      ],\n      raw: true,\n    });\n\n    await DependenceModel.update(\n      { status: DependenceStatus.queued, log: [] },\n      { where: { id: docs.map((x) => x.id!) } },\n    );\n\n    setTimeout(async () => {\n      await dependenceService.installDependenceOneByOne(docs);\n\n      const bootAfterLoader = await import('./bootAfter');\n      bootAfterLoader.default();\n    }, 5000);\n  };\n\n  // 初始化更新 linux/python/nodejs 镜像源配置\n  if (systemConfig.info?.pythonMirror) {\n    systemService.updatePythonMirror({\n      pythonMirror: systemConfig.info?.pythonMirror,\n    });\n  }\n  if (systemConfig.info?.linuxMirror) {\n    systemService.updateLinuxMirror(\n      {\n        linuxMirror: systemConfig.info?.linuxMirror,\n      },\n      undefined,\n      () => installDependencies(),\n    );\n  } else {\n    installDependencies();\n  }\n  if (systemConfig.info?.nodeMirror) {\n    systemService.updateNodeMirror({\n      nodeMirror: systemConfig.info?.nodeMirror,\n    });\n  }\n\n  // 初始化新增默认全部任务视图\n  CrontabViewModel.findAll({\n    where: { type: CronViewType.系统, name: '全部任务' },\n    raw: true,\n  }).then((docs) => {\n    if (docs.length === 0) {\n      CrontabViewModel.create({\n        name: '全部任务',\n        type: CronViewType.系统,\n        position: initPosition / 2,\n      });\n    }\n  });\n\n  // 初始化更新所有任务状态为空闲\n  await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });\n\n  // 初始化时执行一次所有的 ql repo 任务\n  CrontabModel.findAll({\n    where: {\n      isDisabled: { [Op.ne]: 1 },\n      command: {\n        [Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],\n      },\n    },\n  }).then((docs) => {\n    for (let i = 0; i < docs.length; i++) {\n      const doc = docs[i];\n      if (doc) {\n        exec(doc.command);\n      }\n    }\n  });\n\n  // 更新2.11.3以前的脚本路径\n  CrontabModel.findAll({\n    where: {\n      command: {\n        [Op.or]: [\n          { [Op.like]: `%\\/${config.rootPath}\\/scripts\\/%` },\n          { [Op.like]: `%\\/${config.rootPath}\\/config\\/%` },\n          { [Op.like]: `%\\/${config.rootPath}\\/log\\/%` },\n          { [Op.like]: `%\\/${config.rootPath}\\/db\\/%` },\n        ],\n      },\n    },\n  }).then(async (docs) => {\n    for (let i = 0; i < docs.length; i++) {\n      const doc = docs[i];\n      if (doc) {\n        if (doc.command.includes(`${config.rootPath}/scripts/`)) {\n          await CrontabModel.update(\n            { command: doc.command.replace(`${config.rootPath}/scripts/`, '') },\n            { where: { id: doc.id } },\n          );\n        }\n        if (doc.command.includes(`${config.rootPath}/log/`)) {\n          await CrontabModel.update(\n            {\n              command: `${config.dataPath}/log/${doc.command.replace(\n                `${config.rootPath}/log/`,\n                '',\n              )}`,\n            },\n            { where: { id: doc.id } },\n          );\n        }\n        if (doc.command.includes(`${config.rootPath}/config/`)) {\n          await CrontabModel.update(\n            {\n              command: `${config.dataPath}/config/${doc.command.replace(\n                `${config.rootPath}/config/`,\n                '',\n              )}`,\n            },\n            { where: { id: doc.id } },\n          );\n        }\n        if (doc.command.includes(`${config.rootPath}/db/`)) {\n          await CrontabModel.update(\n            {\n              command: `${config.dataPath}/db/${doc.command.replace(\n                `${config.rootPath}/db/`,\n                '',\n              )}`,\n            },\n            { where: { id: doc.id } },\n          );\n        }\n      }\n    }\n  });\n\n  // 初始化保存一次ck和定时任务数据\n  await cronService.autosave_crontab();\n  await envService.set_envs();\n\n  const authInfo = await userService.getAuthInfo();\n  const apps = await openService.findApps();\n  await shareStore.updateAuthInfo(authInfo);\n  if (apps?.length) {\n    await shareStore.updateApps(apps);\n  }\n};\n"
  },
  {
    "path": "back/loaders/initFile.ts",
    "content": "import fs from 'fs/promises';\nimport path from 'path';\nimport os from 'os';\nimport Logger from './logger';\nimport { fileExist } from '../config/util';\nimport { writeFileWithLock } from '../shared/utils';\n\nconst rootPath = process.env.QL_DIR as string;\nlet dataPath = path.join(rootPath, 'data/');\n\nif (process.env.QL_DATA_DIR) {\n  dataPath = process.env.QL_DATA_DIR.replace(/\\/$/g, '');\n}\n\nconst preloadPath = path.join(rootPath, 'shell/preload/');\nconst configPath = path.join(dataPath, 'config/');\nconst scriptPath = path.join(dataPath, 'scripts/');\nconst logPath = path.join(dataPath, 'log/');\nconst uploadPath = path.join(dataPath, 'upload/');\nconst bakPath = path.join(dataPath, 'bak/');\nconst samplePath = path.join(rootPath, 'sample/');\nconst tmpPath = path.join(logPath, '.tmp/');\nconst confFile = path.join(configPath, 'config.sh');\nconst sampleConfigFile = path.join(samplePath, 'config.sample.sh');\nconst sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');\nconst sampleNotifyJsFile = path.join(samplePath, 'notify.js');\nconst sampleNotifyPyFile = path.join(samplePath, 'notify.py');\nconst scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');\nconst scriptNotifyPyFile = path.join(scriptPath, 'notify.py');\nconst jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');\nconst pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');\nconst TaskBeforeFile = path.join(configPath, 'task_before.sh');\nconst TaskBeforeJsFile = path.join(configPath, 'task_before.js');\nconst TaskBeforePyFile = path.join(configPath, 'task_before.py');\nconst TaskAfterFile = path.join(configPath, 'task_after.sh');\nconst homedir = os.homedir();\nconst sshPath = path.resolve(homedir, '.ssh');\nconst sshdPath = path.join(dataPath, 'ssh.d');\nconst systemLogPath = path.join(dataPath, 'syslog');\n\nconst directories = [\n  configPath,\n  scriptPath,\n  preloadPath,\n  logPath,\n  tmpPath,\n  uploadPath,\n  sshPath,\n  bakPath,\n  sshdPath,\n  systemLogPath,\n];\n\nconst files = [\n  {\n    target: confFile,\n    source: sampleConfigFile,\n    checkExistence: true,\n  },\n  {\n    target: jsNotifyFile,\n    source: sampleNotifyJsFile,\n    checkExistence: false,\n  },\n  {\n    target: pyNotifyFile,\n    source: sampleNotifyPyFile,\n    checkExistence: false,\n  },\n  {\n    target: scriptNotifyJsFile,\n    source: sampleNotifyJsFile,\n    checkExistence: true,\n  },\n  {\n    target: scriptNotifyPyFile,\n    source: sampleNotifyPyFile,\n    checkExistence: true,\n  },\n  {\n    target: TaskBeforeFile,\n    source: sampleTaskShellFile,\n    checkExistence: true,\n  },\n  {\n    target: TaskBeforeJsFile,\n    content:\n      '// The JavaScript code that executes before the JavaScript task execution will execute.',\n    checkExistence: true,\n  },\n  {\n    target: TaskBeforePyFile,\n    content:\n      '# The Python code that executes before the Python task execution will execute.',\n    checkExistence: true,\n  },\n  {\n    target: TaskAfterFile,\n    source: sampleTaskShellFile,\n    checkExistence: true,\n  },\n];\n\nexport default async () => {\n  for (const dirPath of directories) {\n    if (!(await fileExist(dirPath))) {\n      await fs.mkdir(dirPath);\n    }\n  }\n\n  for (const item of files) {\n    const exists = await fileExist(item.target);\n    if (!item.checkExistence || !exists) {\n      if (!item.content && !item.source) {\n        throw new Error(\n          `Neither content nor source specified for ${item.target}`,\n        );\n      }\n      const content =\n        item.content ||\n        (await fs.readFile(item.source!, { encoding: 'utf-8' }));\n      await writeFileWithLock(item.target, content);\n    }\n  }\n\n  Logger.info('✌️ Init file down');\n};\n"
  },
  {
    "path": "back/loaders/initTask.ts",
    "content": "import { Container } from 'typedi';\nimport SystemService from '../services/system';\nimport ScheduleService, { ScheduleTaskType } from '../services/schedule';\nimport SubscriptionService from '../services/subscription';\nimport SshKeyService from '../services/sshKey';\nimport config from '../config';\nimport { fileExist } from '../config/util';\nimport { join } from 'path';\n\nexport default async () => {\n  const systemService = Container.get(SystemService);\n  const scheduleService = Container.get(ScheduleService);\n  const subscriptionService = Container.get(SubscriptionService);\n  const sshKeyService = Container.get(SshKeyService);\n\n  // 生成内置token\n  let tokenCommand = `ts-node-transpile-only ${join(\n    config.rootPath,\n    'back/token.ts',\n  )}`;\n  const tokenFile = join(config.rootPath, 'static/build/token.js');\n\n  if (await fileExist(tokenFile)) {\n    tokenCommand = `node ${tokenFile}`;\n  }\n  const cron = {\n    id: NaN,\n    name: '生成token',\n    command: tokenCommand,\n    runOrigin: 'system',\n  } as ScheduleTaskType;\n  await scheduleService.cancelIntervalTask(cron);\n  scheduleService.createIntervalTask(\n    cron,\n    {\n      days: 28,\n    },\n    true,\n  );\n\n  // 运行删除日志任务\n  const data = await systemService.getSystemConfig();\n  if (data && data.info) {\n    if (data.info.logRemoveFrequency) {\n      const rmlogCron = {\n        id: data.id as number,\n        name: '删除日志',\n        command: `ql rmlog ${data.info.logRemoveFrequency}`,\n        runOrigin: 'system' as const,\n      };\n      await scheduleService.cancelIntervalTask(rmlogCron);\n      scheduleService.createIntervalTask(\n        rmlogCron,\n        {\n          days: data.info.logRemoveFrequency,\n        },\n        true,\n      );\n    }\n\n    systemService.updateTimezone(data.info);\n    \n    // Apply global SSH key if configured\n    if (data.info.globalSshKey) {\n      await sshKeyService.addGlobalSSHKey(data.info.globalSshKey, 'global');\n    }\n  }\n\n  await subscriptionService.setSshConfig();\n  const subs = await subscriptionService.list();\n  for (const sub of subs) {\n    subscriptionService.handleTask(sub.get({ plain: true }), !sub.is_disabled);\n  }\n};\n"
  },
  {
    "path": "back/loaders/logger.ts",
    "content": "import winston from 'winston';\nimport 'winston-daily-rotate-file';\nimport config from '../config';\nimport path from 'path';\n\nconst levelMap: Record<string, string> = {\n  info: 'ℹ️', // info图标\n  warn: '⚠️', // 警告图标\n  error: '❌', // 错误图标\n  debug: '🐛', // debug调试图标\n};\n\nconst baseFormat = [\n  winston.format.splat(),\n  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n  winston.format.align(),\n];\n\nconst consoleFormat = winston.format.combine(\n  winston.format.colorize({ level: true }),\n  ...baseFormat,\n  winston.format.printf((info) => {\n    return `[${info.level} ${info.timestamp}]:${info.message}`;\n  }),\n);\n\nconst plainFormat = winston.format.combine(\n  winston.format.uncolorize(),\n  ...baseFormat,\n  winston.format.printf((info) => {\n    return `[${levelMap[info.level] || ''}${info.level} ${info.timestamp}]:${\n      info.message\n    }`;\n  }),\n);\n\nconst consoleTransport = new winston.transports.Console({\n  format: consoleFormat,\n  level: 'debug',\n});\n\nconst fileTransport = new winston.transports.DailyRotateFile({\n  filename: path.join(config.systemLogPath, '%DATE%.log'),\n  datePattern: 'YYYY-MM-DD',\n  maxSize: '20m',\n  maxFiles: '7d',\n  format: plainFormat,\n  level: config.logs.level || 'info',\n});\n\nconst LoggerInstance = winston.createLogger({\n  level: 'debug',\n  levels: winston.config.npm.levels,\n  transports: [consoleTransport, fileTransport],\n  exceptionHandlers: [consoleTransport, fileTransport],\n  rejectionHandlers: [consoleTransport, fileTransport],\n});\n\nLoggerInstance.on('error', (error) => {\n  console.error('Logger error:', error);\n});\n\nexport default LoggerInstance;\n"
  },
  {
    "path": "back/loaders/server.ts",
    "content": "import { Server } from 'http';\nimport Logger from './logger';\nimport Sock from './sock';\n\nexport default async ({ server }: { server: Server }) => {\n  await Sock({ server });\n  Logger.info('✌️ Sock loaded');\n\n  process.on('uncaughtException', (error) => {\n    Logger.error('Uncaught exception:', error);\n  });\n\n  process.on('unhandledRejection', (reason, promise) => {\n    Logger.error('Unhandled rejection:', reason, promise);\n  });\n};\n"
  },
  {
    "path": "back/loaders/sock.ts",
    "content": "import sockJs from 'sockjs';\nimport { Server } from 'http';\nimport { Container } from 'typedi';\nimport SockService from '../services/sock';\nimport { getPlatform } from '../config/util';\nimport { shareStore } from '../shared/store';\nimport { isValidToken } from '../shared/auth';\nimport config from '../config';\n\nexport default async ({ server }: { server: Server }) => {\n  const echo = sockJs.createServer({ prefix: `${config.baseUrl}/api/ws`, log: () => { } });\n  const sockService = Container.get(SockService);\n\n  echo.on('connection', async (conn) => {\n    if (!conn.headers || !conn.url || !conn.pathname) {\n      conn.close('404');\n    }\n\n    const authInfo = await shareStore.getAuthInfo();\n    const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';\n    const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');\n\n    if (isValidToken(authInfo, headerToken, platform)) {\n      sockService.addClient(conn);\n\n      conn.on('data', (message) => {\n        conn.write(message);\n      });\n\n      conn.on('close', function () {\n        sockService.removeClient(conn);\n      });\n\n      return;\n    }\n\n    conn.close('404');\n  });\n\n  echo.installHandlers(server);\n};\n"
  },
  {
    "path": "back/middlewares/monitoring.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport Logger from '../loaders/logger';\nimport { performance } from 'perf_hooks';\nimport { metricsService } from '../services/metrics';\n\ninterface RequestMetrics {\n  method: string;\n  path: string;\n  duration: number;\n  statusCode: number;\n  timestamp: number;\n  platform?: string;\n}\n\nconst requestMetrics: RequestMetrics[] = [];\n\nexport const monitoringMiddleware = (\n  req: Request,\n  res: Response,\n  next: NextFunction,\n) => {\n  const start = performance.now();\n  const originalEnd = res.end;\n\n  res.end = function (chunk?: any, encoding?: any, cb?: any) {\n    const duration = performance.now() - start;\n    const metric: RequestMetrics = {\n      method: req.method,\n      path: req.path,\n      duration,\n      statusCode: res.statusCode,\n      timestamp: Date.now(),\n      platform: req.platform,\n    };\n\n    requestMetrics.push(metric);\n    metricsService.record('http_request', duration, {\n      method: req.method,\n      path: req.path,\n      statusCode: res.statusCode.toString(),\n      ...(req.platform && { platform: req.platform }),\n    });\n\n    if (requestMetrics.length > 1000) {\n      requestMetrics.shift();\n    }\n\n    if (duration > 1000) {\n      Logger.warn(\n        `Slow request detected: ${req.method} ${\n          req.path\n        } took ${duration.toFixed(2)}ms`,\n      );\n    }\n\n    return originalEnd.call(this, chunk, encoding, cb);\n  };\n\n  next();\n};\n\nexport const getMetrics = () => {\n  return {\n    totalRequests: requestMetrics.length,\n    averageDuration:\n      requestMetrics.reduce((acc, curr) => acc + curr.duration, 0) /\n      requestMetrics.length,\n    requestsByMethod: requestMetrics.reduce((acc, curr) => {\n      acc[curr.method] = (acc[curr.method] || 0) + 1;\n      return acc;\n    }, {} as Record<string, number>),\n    requestsByPlatform: requestMetrics.reduce((acc, curr) => {\n      if (curr.platform) {\n        acc[curr.platform] = (acc[curr.platform] || 0) + 1;\n      }\n      return acc;\n    }, {} as Record<string, number>),\n    recentRequests: requestMetrics.slice(-10),\n  };\n};\n"
  },
  {
    "path": "back/protos/api.proto",
    "content": "syntax = \"proto3\";\n\npackage com.ql.api;\n\nmessage EnvItem {\n  optional int32 id = 1;\n  optional string name = 2;\n  optional string value = 3;\n  optional string remarks = 4;\n  optional int32 status = 5;\n  optional int64 position = 6;\n}\n\nmessage GetEnvsRequest { string searchValue = 1; }\n\nmessage CreateEnvRequest { repeated EnvItem envs = 1; }\n\nmessage UpdateEnvRequest { EnvItem env = 1; }\n\nmessage DeleteEnvsRequest { repeated int32 ids = 1; }\n\nmessage MoveEnvRequest {\n  int32 id = 1;\n  int32 fromIndex = 2;\n  int32 toIndex = 3;\n}\n\nmessage DisableEnvsRequest { repeated int32 ids = 1; }\n\nmessage EnableEnvsRequest { repeated int32 ids = 1; }\n\nmessage UpdateEnvNamesRequest {\n  repeated int32 ids = 1;\n  string name = 2;\n}\n\nmessage GetEnvByIdRequest { int32 id = 1; }\n\nmessage EnvsResponse {\n  int32 code = 1;\n  repeated EnvItem data = 2;\n  optional string message = 3;\n}\n\nmessage EnvResponse {\n  int32 code = 1;\n  EnvItem data = 2;\n  optional string message = 3;\n}\n\nmessage Response {\n  int32 code = 1;\n  optional string message = 2;\n}\n\nmessage ExtraScheduleItem { string schedule = 1; }\n\nmessage CronItem {\n  optional int32 id = 1;\n  optional string command = 2;\n  optional string schedule = 3;\n  optional string name = 4;\n  repeated string labels = 5;\n  optional int32 sub_id = 6;\n  repeated ExtraScheduleItem extra_schedules = 7;\n  optional string task_before = 8;\n  optional string task_after = 9;\n  optional int32 status = 10;\n  optional string log_path = 11;\n  optional int32 pid = 12;\n  optional int64 last_running_time = 13;\n  optional int64 last_execution_time = 14;\n}\n\nmessage CreateCronRequest {\n  string command = 1;\n  string schedule = 2;\n  optional string name = 3;\n  repeated string labels = 4;\n  optional int32 sub_id = 5;\n  repeated ExtraScheduleItem extra_schedules = 6;\n  optional string task_before = 7;\n  optional string task_after = 8;\n}\n\nmessage UpdateCronRequest {\n  int32 id = 1;\n  optional string command = 2;\n  optional string schedule = 3;\n  optional string name = 4;\n  repeated string labels = 5;\n  optional int32 sub_id = 6;\n  repeated ExtraScheduleItem extra_schedules = 7;\n  optional string task_before = 8;\n  optional string task_after = 9;\n}\n\nmessage DeleteCronsRequest { repeated int32 ids = 1; }\n\nmessage GetCronsRequest {\n  optional string searchValue = 1;\n}\n\nmessage GetCronByIdRequest { int32 id = 1; }\n\nmessage EnableCronsRequest { repeated int32 ids = 1; }\n\nmessage DisableCronsRequest { repeated int32 ids = 1; }\n\nmessage RunCronsRequest { repeated int32 ids = 1; }\n\nmessage CronsResponse {\n  int32 code = 1;\n  repeated CronItem data = 2;\n  optional string message = 3;\n}\n\nmessage CronResponse {\n  int32 code = 1;\n  CronItem data = 2;\n  optional string message = 3;\n}\n\nmessage CronDetailRequest { string log_path = 1; }\n\nmessage CronDetailResponse {\n  int32 code = 1;\n  CronItem data = 2;\n  optional string message = 3;\n}\n\nenum NotificationMode {\n  gotify = 0;\n  goCqHttpBot = 1;\n  serverChan = 2;\n  pushDeer = 3;\n  bark = 4;\n  chat = 5;\n  telegramBot = 6;\n  dingtalkBot = 7;\n  weWorkBot = 8;\n  weWorkApp = 9;\n  aibotk = 10;\n  iGot = 11;\n  pushPlus = 12;\n  wePlusBot = 13;\n  email = 14;\n  pushMe = 15;\n  feishu = 16;\n  webhook = 17;\n  chronocat = 18;\n  ntfy = 19;\n  wxPusherBot = 20;\n}\n\nmessage NotificationInfo {\n  NotificationMode type = 1;\n\n  optional string gotifyUrl = 2;\n  optional string gotifyToken = 3;\n  optional int32 gotifyPriority = 4;\n\n  optional string goCqHttpBotUrl = 5;\n  optional string goCqHttpBotToken = 6;\n  optional string goCqHttpBotQq = 7;\n\n  optional string serverChanKey = 8;\n\n  optional string pushDeerKey = 9;\n  optional string pushDeerUrl = 10;\n\n  optional string synologyChatUrl = 11;\n\n  optional string barkPush = 12;\n  optional string barkIcon = 13;\n  optional string barkSound = 14;\n  optional string barkGroup = 15;\n  optional string barkLevel = 16;\n  optional string barkUrl = 17;\n  optional string barkArchive = 18;\n\n  optional string telegramBotToken = 19;\n  optional string telegramBotUserId = 20;\n  optional string telegramBotProxyHost = 21;\n  optional string telegramBotProxyPort = 22;\n  optional string telegramBotProxyAuth = 23;\n  optional string telegramBotApiHost = 24;\n\n  optional string dingtalkBotToken = 25;\n  optional string dingtalkBotSecret = 26;\n\n  optional string weWorkBotKey = 27;\n  optional string weWorkOrigin = 28;\n\n  optional string weWorkAppKey = 29;\n\n  optional string aibotkKey = 30;\n  optional string aibotkType = 31;\n  optional string aibotkName = 32;\n\n  optional string iGotPushKey = 33;\n\n  optional string pushPlusToken = 34;\n  optional string pushPlusUser = 35;\n  optional string pushPlusTemplate = 36;\n  optional string pushplusChannel = 37;\n  optional string pushplusWebhook = 38;\n  optional string pushplusCallbackUrl = 39;\n  optional string pushplusTo = 40;\n\n  optional string wePlusBotToken = 41;\n  optional string wePlusBotReceiver = 42;\n  optional string wePlusBotVersion = 43;\n\n  optional string emailService = 44;\n  optional string emailUser = 45;\n  optional string emailPass = 46;\n  optional string emailTo = 47;\n\n  optional string pushMeKey = 48;\n  optional string pushMeUrl = 49;\n\n  optional string chronocatURL = 50;\n  optional string chronocatQQ = 51;\n  optional string chronocatToken = 52;\n\n  optional string webhookHeaders = 53;\n  optional string webhookBody = 54;\n  optional string webhookUrl = 55;\n  optional string webhookMethod = 56;\n  optional string webhookContentType = 57;\n\n  optional string larkKey = 58;\n  optional string larkSecret = 69;\n\n  optional string ntfyUrl = 59;\n  optional string ntfyTopic = 60;\n  optional string ntfyPriority = 61;\n  optional string ntfyToken = 62;\n  optional string ntfyUsername = 63;\n  optional string ntfyPassword = 64;\n  optional string ntfyActions = 65;\n\n  optional string wxPusherBotAppToken = 66;\n  optional string wxPusherBotTopicIds = 67;\n  optional string wxPusherBotUids = 68;\n}\n\nmessage SystemNotifyRequest {\n  string title = 1;\n  string content = 2;\n  optional NotificationInfo notificationInfo = 3;\n}\n\nservice Api {\n  rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}\n  rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}\n  rpc UpdateEnv(UpdateEnvRequest) returns (EnvResponse) {}\n  rpc DeleteEnvs(DeleteEnvsRequest) returns (Response) {}\n  rpc MoveEnv(MoveEnvRequest) returns (EnvResponse) {}\n  rpc DisableEnvs(DisableEnvsRequest) returns (Response) {}\n  rpc EnableEnvs(EnableEnvsRequest) returns (Response) {}\n  rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}\n  rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}\n  rpc SystemNotify(SystemNotifyRequest) returns (Response) {}\n  rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}\n  rpc CreateCron(CreateCronRequest) returns (CronResponse) {}\n  rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}\n  rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}\n  rpc GetCrons(GetCronsRequest) returns (CronsResponse) {}\n  rpc GetCronById(GetCronByIdRequest) returns (CronResponse) {}\n  rpc EnableCrons(EnableCronsRequest) returns (Response) {}\n  rpc DisableCrons(DisableCronsRequest) returns (Response) {}\n  rpc RunCrons(RunCronsRequest) returns (Response) {}\n}"
  },
  {
    "path": "back/protos/api.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.6.1\n//   protoc               v3.21.12\n// source: back/protos/api.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\nimport {\n  type CallOptions,\n  ChannelCredentials,\n  Client,\n  type ClientOptions,\n  type ClientUnaryCall,\n  type handleUnaryCall,\n  makeGenericClientConstructor,\n  Metadata,\n  type ServiceError,\n  type UntypedServiceImplementation,\n} from \"@grpc/grpc-js\";\n\nexport const protobufPackage = \"com.ql.api\";\n\nexport enum NotificationMode {\n  gotify = 0,\n  goCqHttpBot = 1,\n  serverChan = 2,\n  pushDeer = 3,\n  bark = 4,\n  chat = 5,\n  telegramBot = 6,\n  dingtalkBot = 7,\n  weWorkBot = 8,\n  weWorkApp = 9,\n  aibotk = 10,\n  iGot = 11,\n  pushPlus = 12,\n  wePlusBot = 13,\n  email = 14,\n  pushMe = 15,\n  feishu = 16,\n  webhook = 17,\n  chronocat = 18,\n  ntfy = 19,\n  wxPusherBot = 20,\n  UNRECOGNIZED = -1,\n}\n\nexport function notificationModeFromJSON(object: any): NotificationMode {\n  switch (object) {\n    case 0:\n    case \"gotify\":\n      return NotificationMode.gotify;\n    case 1:\n    case \"goCqHttpBot\":\n      return NotificationMode.goCqHttpBot;\n    case 2:\n    case \"serverChan\":\n      return NotificationMode.serverChan;\n    case 3:\n    case \"pushDeer\":\n      return NotificationMode.pushDeer;\n    case 4:\n    case \"bark\":\n      return NotificationMode.bark;\n    case 5:\n    case \"chat\":\n      return NotificationMode.chat;\n    case 6:\n    case \"telegramBot\":\n      return NotificationMode.telegramBot;\n    case 7:\n    case \"dingtalkBot\":\n      return NotificationMode.dingtalkBot;\n    case 8:\n    case \"weWorkBot\":\n      return NotificationMode.weWorkBot;\n    case 9:\n    case \"weWorkApp\":\n      return NotificationMode.weWorkApp;\n    case 10:\n    case \"aibotk\":\n      return NotificationMode.aibotk;\n    case 11:\n    case \"iGot\":\n      return NotificationMode.iGot;\n    case 12:\n    case \"pushPlus\":\n      return NotificationMode.pushPlus;\n    case 13:\n    case \"wePlusBot\":\n      return NotificationMode.wePlusBot;\n    case 14:\n    case \"email\":\n      return NotificationMode.email;\n    case 15:\n    case \"pushMe\":\n      return NotificationMode.pushMe;\n    case 16:\n    case \"feishu\":\n      return NotificationMode.feishu;\n    case 17:\n    case \"webhook\":\n      return NotificationMode.webhook;\n    case 18:\n    case \"chronocat\":\n      return NotificationMode.chronocat;\n    case 19:\n    case \"ntfy\":\n      return NotificationMode.ntfy;\n    case 20:\n    case \"wxPusherBot\":\n      return NotificationMode.wxPusherBot;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return NotificationMode.UNRECOGNIZED;\n  }\n}\n\nexport function notificationModeToJSON(object: NotificationMode): string {\n  switch (object) {\n    case NotificationMode.gotify:\n      return \"gotify\";\n    case NotificationMode.goCqHttpBot:\n      return \"goCqHttpBot\";\n    case NotificationMode.serverChan:\n      return \"serverChan\";\n    case NotificationMode.pushDeer:\n      return \"pushDeer\";\n    case NotificationMode.bark:\n      return \"bark\";\n    case NotificationMode.chat:\n      return \"chat\";\n    case NotificationMode.telegramBot:\n      return \"telegramBot\";\n    case NotificationMode.dingtalkBot:\n      return \"dingtalkBot\";\n    case NotificationMode.weWorkBot:\n      return \"weWorkBot\";\n    case NotificationMode.weWorkApp:\n      return \"weWorkApp\";\n    case NotificationMode.aibotk:\n      return \"aibotk\";\n    case NotificationMode.iGot:\n      return \"iGot\";\n    case NotificationMode.pushPlus:\n      return \"pushPlus\";\n    case NotificationMode.wePlusBot:\n      return \"wePlusBot\";\n    case NotificationMode.email:\n      return \"email\";\n    case NotificationMode.pushMe:\n      return \"pushMe\";\n    case NotificationMode.feishu:\n      return \"feishu\";\n    case NotificationMode.webhook:\n      return \"webhook\";\n    case NotificationMode.chronocat:\n      return \"chronocat\";\n    case NotificationMode.ntfy:\n      return \"ntfy\";\n    case NotificationMode.wxPusherBot:\n      return \"wxPusherBot\";\n    case NotificationMode.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface EnvItem {\n  id?: number | undefined;\n  name?: string | undefined;\n  value?: string | undefined;\n  remarks?: string | undefined;\n  status?: number | undefined;\n  position?: number | undefined;\n}\n\nexport interface GetEnvsRequest {\n  searchValue: string;\n}\n\nexport interface CreateEnvRequest {\n  envs: EnvItem[];\n}\n\nexport interface UpdateEnvRequest {\n  env: EnvItem | undefined;\n}\n\nexport interface DeleteEnvsRequest {\n  ids: number[];\n}\n\nexport interface MoveEnvRequest {\n  id: number;\n  fromIndex: number;\n  toIndex: number;\n}\n\nexport interface DisableEnvsRequest {\n  ids: number[];\n}\n\nexport interface EnableEnvsRequest {\n  ids: number[];\n}\n\nexport interface UpdateEnvNamesRequest {\n  ids: number[];\n  name: string;\n}\n\nexport interface GetEnvByIdRequest {\n  id: number;\n}\n\nexport interface EnvsResponse {\n  code: number;\n  data: EnvItem[];\n  message?: string | undefined;\n}\n\nexport interface EnvResponse {\n  code: number;\n  data: EnvItem | undefined;\n  message?: string | undefined;\n}\n\nexport interface Response {\n  code: number;\n  message?: string | undefined;\n}\n\nexport interface ExtraScheduleItem {\n  schedule: string;\n}\n\nexport interface CronItem {\n  id?: number | undefined;\n  command?: string | undefined;\n  schedule?: string | undefined;\n  name?: string | undefined;\n  labels: string[];\n  sub_id?: number | undefined;\n  extra_schedules: ExtraScheduleItem[];\n  task_before?: string | undefined;\n  task_after?: string | undefined;\n  status?: number | undefined;\n  log_path?: string | undefined;\n  pid?: number | undefined;\n  last_running_time?: number | undefined;\n  last_execution_time?: number | undefined;\n}\n\nexport interface CreateCronRequest {\n  command: string;\n  schedule: string;\n  name?: string | undefined;\n  labels: string[];\n  sub_id?: number | undefined;\n  extra_schedules: ExtraScheduleItem[];\n  task_before?: string | undefined;\n  task_after?: string | undefined;\n}\n\nexport interface UpdateCronRequest {\n  id: number;\n  command?: string | undefined;\n  schedule?: string | undefined;\n  name?: string | undefined;\n  labels: string[];\n  sub_id?: number | undefined;\n  extra_schedules: ExtraScheduleItem[];\n  task_before?: string | undefined;\n  task_after?: string | undefined;\n}\n\nexport interface DeleteCronsRequest {\n  ids: number[];\n}\n\nexport interface GetCronsRequest {\n  searchValue?: string | undefined;\n}\n\nexport interface GetCronByIdRequest {\n  id: number;\n}\n\nexport interface EnableCronsRequest {\n  ids: number[];\n}\n\nexport interface DisableCronsRequest {\n  ids: number[];\n}\n\nexport interface RunCronsRequest {\n  ids: number[];\n}\n\nexport interface CronsResponse {\n  code: number;\n  data: CronItem[];\n  message?: string | undefined;\n}\n\nexport interface CronResponse {\n  code: number;\n  data: CronItem | undefined;\n  message?: string | undefined;\n}\n\nexport interface CronDetailRequest {\n  log_path: string;\n}\n\nexport interface CronDetailResponse {\n  code: number;\n  data: CronItem | undefined;\n  message?: string | undefined;\n}\n\nexport interface NotificationInfo {\n  type: NotificationMode;\n  gotifyUrl?: string | undefined;\n  gotifyToken?: string | undefined;\n  gotifyPriority?: number | undefined;\n  goCqHttpBotUrl?: string | undefined;\n  goCqHttpBotToken?: string | undefined;\n  goCqHttpBotQq?: string | undefined;\n  serverChanKey?: string | undefined;\n  pushDeerKey?: string | undefined;\n  pushDeerUrl?: string | undefined;\n  synologyChatUrl?: string | undefined;\n  barkPush?: string | undefined;\n  barkIcon?: string | undefined;\n  barkSound?: string | undefined;\n  barkGroup?: string | undefined;\n  barkLevel?: string | undefined;\n  barkUrl?: string | undefined;\n  barkArchive?: string | undefined;\n  telegramBotToken?: string | undefined;\n  telegramBotUserId?: string | undefined;\n  telegramBotProxyHost?: string | undefined;\n  telegramBotProxyPort?: string | undefined;\n  telegramBotProxyAuth?: string | undefined;\n  telegramBotApiHost?: string | undefined;\n  dingtalkBotToken?: string | undefined;\n  dingtalkBotSecret?: string | undefined;\n  weWorkBotKey?: string | undefined;\n  weWorkOrigin?: string | undefined;\n  weWorkAppKey?: string | undefined;\n  aibotkKey?: string | undefined;\n  aibotkType?: string | undefined;\n  aibotkName?: string | undefined;\n  iGotPushKey?: string | undefined;\n  pushPlusToken?: string | undefined;\n  pushPlusUser?: string | undefined;\n  pushPlusTemplate?: string | undefined;\n  pushplusChannel?: string | undefined;\n  pushplusWebhook?: string | undefined;\n  pushplusCallbackUrl?: string | undefined;\n  pushplusTo?: string | undefined;\n  wePlusBotToken?: string | undefined;\n  wePlusBotReceiver?: string | undefined;\n  wePlusBotVersion?: string | undefined;\n  emailService?: string | undefined;\n  emailUser?: string | undefined;\n  emailPass?: string | undefined;\n  emailTo?: string | undefined;\n  pushMeKey?: string | undefined;\n  pushMeUrl?: string | undefined;\n  chronocatURL?: string | undefined;\n  chronocatQQ?: string | undefined;\n  chronocatToken?: string | undefined;\n  webhookHeaders?: string | undefined;\n  webhookBody?: string | undefined;\n  webhookUrl?: string | undefined;\n  webhookMethod?: string | undefined;\n  webhookContentType?: string | undefined;\n  larkKey?: string | undefined;\n  larkSecret?: string | undefined;\n  ntfyUrl?: string | undefined;\n  ntfyTopic?: string | undefined;\n  ntfyPriority?: string | undefined;\n  ntfyToken?: string | undefined;\n  ntfyUsername?: string | undefined;\n  ntfyPassword?: string | undefined;\n  ntfyActions?: string | undefined;\n  wxPusherBotAppToken?: string | undefined;\n  wxPusherBotTopicIds?: string | undefined;\n  wxPusherBotUids?: string | undefined;\n}\n\nexport interface SystemNotifyRequest {\n  title: string;\n  content: string;\n  notificationInfo?: NotificationInfo | undefined;\n}\n\nfunction createBaseEnvItem(): EnvItem {\n  return {\n    id: undefined,\n    name: undefined,\n    value: undefined,\n    remarks: undefined,\n    status: undefined,\n    position: undefined,\n  };\n}\n\nexport const EnvItem: MessageFns<EnvItem> = {\n  encode(message: EnvItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== undefined) {\n      writer.uint32(8).int32(message.id);\n    }\n    if (message.name !== undefined) {\n      writer.uint32(18).string(message.name);\n    }\n    if (message.value !== undefined) {\n      writer.uint32(26).string(message.value);\n    }\n    if (message.remarks !== undefined) {\n      writer.uint32(34).string(message.remarks);\n    }\n    if (message.status !== undefined) {\n      writer.uint32(40).int32(message.status);\n    }\n    if (message.position !== undefined) {\n      writer.uint32(48).int64(message.position);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnvItem {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnvItem();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.value = reader.string();\n          continue;\n        }\n        case 4: {\n          if (tag !== 34) {\n            break;\n          }\n\n          message.remarks = reader.string();\n          continue;\n        }\n        case 5: {\n          if (tag !== 40) {\n            break;\n          }\n\n          message.status = reader.int32();\n          continue;\n        }\n        case 6: {\n          if (tag !== 48) {\n            break;\n          }\n\n          message.position = longToNumber(reader.int64());\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnvItem {\n    return {\n      id: isSet(object.id) ? globalThis.Number(object.id) : undefined,\n      name: isSet(object.name) ? globalThis.String(object.name) : undefined,\n      value: isSet(object.value) ? globalThis.String(object.value) : undefined,\n      remarks: isSet(object.remarks) ? globalThis.String(object.remarks) : undefined,\n      status: isSet(object.status) ? globalThis.Number(object.status) : undefined,\n      position: isSet(object.position) ? globalThis.Number(object.position) : undefined,\n    };\n  },\n\n  toJSON(message: EnvItem): unknown {\n    const obj: any = {};\n    if (message.id !== undefined) {\n      obj.id = Math.round(message.id);\n    }\n    if (message.name !== undefined) {\n      obj.name = message.name;\n    }\n    if (message.value !== undefined) {\n      obj.value = message.value;\n    }\n    if (message.remarks !== undefined) {\n      obj.remarks = message.remarks;\n    }\n    if (message.status !== undefined) {\n      obj.status = Math.round(message.status);\n    }\n    if (message.position !== undefined) {\n      obj.position = Math.round(message.position);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnvItem>, I>>(base?: I): EnvItem {\n    return EnvItem.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnvItem>, I>>(object: I): EnvItem {\n    const message = createBaseEnvItem();\n    message.id = object.id ?? undefined;\n    message.name = object.name ?? undefined;\n    message.value = object.value ?? undefined;\n    message.remarks = object.remarks ?? undefined;\n    message.status = object.status ?? undefined;\n    message.position = object.position ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseGetEnvsRequest(): GetEnvsRequest {\n  return { searchValue: \"\" };\n}\n\nexport const GetEnvsRequest: MessageFns<GetEnvsRequest> = {\n  encode(message: GetEnvsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.searchValue !== \"\") {\n      writer.uint32(10).string(message.searchValue);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GetEnvsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGetEnvsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.searchValue = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GetEnvsRequest {\n    return { searchValue: isSet(object.searchValue) ? globalThis.String(object.searchValue) : \"\" };\n  },\n\n  toJSON(message: GetEnvsRequest): unknown {\n    const obj: any = {};\n    if (message.searchValue !== \"\") {\n      obj.searchValue = message.searchValue;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GetEnvsRequest>, I>>(base?: I): GetEnvsRequest {\n    return GetEnvsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GetEnvsRequest>, I>>(object: I): GetEnvsRequest {\n    const message = createBaseGetEnvsRequest();\n    message.searchValue = object.searchValue ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseCreateEnvRequest(): CreateEnvRequest {\n  return { envs: [] };\n}\n\nexport const CreateEnvRequest: MessageFns<CreateEnvRequest> = {\n  encode(message: CreateEnvRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.envs) {\n      EnvItem.encode(v!, writer.uint32(10).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CreateEnvRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCreateEnvRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.envs.push(EnvItem.decode(reader, reader.uint32()));\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CreateEnvRequest {\n    return { envs: globalThis.Array.isArray(object?.envs) ? object.envs.map((e: any) => EnvItem.fromJSON(e)) : [] };\n  },\n\n  toJSON(message: CreateEnvRequest): unknown {\n    const obj: any = {};\n    if (message.envs?.length) {\n      obj.envs = message.envs.map((e) => EnvItem.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CreateEnvRequest>, I>>(base?: I): CreateEnvRequest {\n    return CreateEnvRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CreateEnvRequest>, I>>(object: I): CreateEnvRequest {\n    const message = createBaseCreateEnvRequest();\n    message.envs = object.envs?.map((e) => EnvItem.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseUpdateEnvRequest(): UpdateEnvRequest {\n  return { env: undefined };\n}\n\nexport const UpdateEnvRequest: MessageFns<UpdateEnvRequest> = {\n  encode(message: UpdateEnvRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.env !== undefined) {\n      EnvItem.encode(message.env, writer.uint32(10).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): UpdateEnvRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseUpdateEnvRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.env = EnvItem.decode(reader, reader.uint32());\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): UpdateEnvRequest {\n    return { env: isSet(object.env) ? EnvItem.fromJSON(object.env) : undefined };\n  },\n\n  toJSON(message: UpdateEnvRequest): unknown {\n    const obj: any = {};\n    if (message.env !== undefined) {\n      obj.env = EnvItem.toJSON(message.env);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<UpdateEnvRequest>, I>>(base?: I): UpdateEnvRequest {\n    return UpdateEnvRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<UpdateEnvRequest>, I>>(object: I): UpdateEnvRequest {\n    const message = createBaseUpdateEnvRequest();\n    message.env = (object.env !== undefined && object.env !== null) ? EnvItem.fromPartial(object.env) : undefined;\n    return message;\n  },\n};\n\nfunction createBaseDeleteEnvsRequest(): DeleteEnvsRequest {\n  return { ids: [] };\n}\n\nexport const DeleteEnvsRequest: MessageFns<DeleteEnvsRequest> = {\n  encode(message: DeleteEnvsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DeleteEnvsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDeleteEnvsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DeleteEnvsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: DeleteEnvsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DeleteEnvsRequest>, I>>(base?: I): DeleteEnvsRequest {\n    return DeleteEnvsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DeleteEnvsRequest>, I>>(object: I): DeleteEnvsRequest {\n    const message = createBaseDeleteEnvsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseMoveEnvRequest(): MoveEnvRequest {\n  return { id: 0, fromIndex: 0, toIndex: 0 };\n}\n\nexport const MoveEnvRequest: MessageFns<MoveEnvRequest> = {\n  encode(message: MoveEnvRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int32(message.id);\n    }\n    if (message.fromIndex !== 0) {\n      writer.uint32(16).int32(message.fromIndex);\n    }\n    if (message.toIndex !== 0) {\n      writer.uint32(24).int32(message.toIndex);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): MoveEnvRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseMoveEnvRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 16) {\n            break;\n          }\n\n          message.fromIndex = reader.int32();\n          continue;\n        }\n        case 3: {\n          if (tag !== 24) {\n            break;\n          }\n\n          message.toIndex = reader.int32();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): MoveEnvRequest {\n    return {\n      id: isSet(object.id) ? globalThis.Number(object.id) : 0,\n      fromIndex: isSet(object.fromIndex) ? globalThis.Number(object.fromIndex) : 0,\n      toIndex: isSet(object.toIndex) ? globalThis.Number(object.toIndex) : 0,\n    };\n  },\n\n  toJSON(message: MoveEnvRequest): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    if (message.fromIndex !== 0) {\n      obj.fromIndex = Math.round(message.fromIndex);\n    }\n    if (message.toIndex !== 0) {\n      obj.toIndex = Math.round(message.toIndex);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<MoveEnvRequest>, I>>(base?: I): MoveEnvRequest {\n    return MoveEnvRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<MoveEnvRequest>, I>>(object: I): MoveEnvRequest {\n    const message = createBaseMoveEnvRequest();\n    message.id = object.id ?? 0;\n    message.fromIndex = object.fromIndex ?? 0;\n    message.toIndex = object.toIndex ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseDisableEnvsRequest(): DisableEnvsRequest {\n  return { ids: [] };\n}\n\nexport const DisableEnvsRequest: MessageFns<DisableEnvsRequest> = {\n  encode(message: DisableEnvsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DisableEnvsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDisableEnvsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DisableEnvsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: DisableEnvsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DisableEnvsRequest>, I>>(base?: I): DisableEnvsRequest {\n    return DisableEnvsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DisableEnvsRequest>, I>>(object: I): DisableEnvsRequest {\n    const message = createBaseDisableEnvsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseEnableEnvsRequest(): EnableEnvsRequest {\n  return { ids: [] };\n}\n\nexport const EnableEnvsRequest: MessageFns<EnableEnvsRequest> = {\n  encode(message: EnableEnvsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnableEnvsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnableEnvsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnableEnvsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: EnableEnvsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnableEnvsRequest>, I>>(base?: I): EnableEnvsRequest {\n    return EnableEnvsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnableEnvsRequest>, I>>(object: I): EnableEnvsRequest {\n    const message = createBaseEnableEnvsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseUpdateEnvNamesRequest(): UpdateEnvNamesRequest {\n  return { ids: [], name: \"\" };\n}\n\nexport const UpdateEnvNamesRequest: MessageFns<UpdateEnvNamesRequest> = {\n  encode(message: UpdateEnvNamesRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    if (message.name !== \"\") {\n      writer.uint32(18).string(message.name);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): UpdateEnvNamesRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseUpdateEnvNamesRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): UpdateEnvNamesRequest {\n    return {\n      ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [],\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n    };\n  },\n\n  toJSON(message: UpdateEnvNamesRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    if (message.name !== \"\") {\n      obj.name = message.name;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<UpdateEnvNamesRequest>, I>>(base?: I): UpdateEnvNamesRequest {\n    return UpdateEnvNamesRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<UpdateEnvNamesRequest>, I>>(object: I): UpdateEnvNamesRequest {\n    const message = createBaseUpdateEnvNamesRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    message.name = object.name ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseGetEnvByIdRequest(): GetEnvByIdRequest {\n  return { id: 0 };\n}\n\nexport const GetEnvByIdRequest: MessageFns<GetEnvByIdRequest> = {\n  encode(message: GetEnvByIdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int32(message.id);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GetEnvByIdRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGetEnvByIdRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GetEnvByIdRequest {\n    return { id: isSet(object.id) ? globalThis.Number(object.id) : 0 };\n  },\n\n  toJSON(message: GetEnvByIdRequest): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GetEnvByIdRequest>, I>>(base?: I): GetEnvByIdRequest {\n    return GetEnvByIdRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GetEnvByIdRequest>, I>>(object: I): GetEnvByIdRequest {\n    const message = createBaseGetEnvByIdRequest();\n    message.id = object.id ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseEnvsResponse(): EnvsResponse {\n  return { code: 0, data: [], message: undefined };\n}\n\nexport const EnvsResponse: MessageFns<EnvsResponse> = {\n  encode(message: EnvsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.code !== 0) {\n      writer.uint32(8).int32(message.code);\n    }\n    for (const v of message.data) {\n      EnvItem.encode(v!, writer.uint32(18).fork()).join();\n    }\n    if (message.message !== undefined) {\n      writer.uint32(26).string(message.message);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnvsResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnvsResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.code = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.data.push(EnvItem.decode(reader, reader.uint32()));\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.message = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnvsResponse {\n    return {\n      code: isSet(object.code) ? globalThis.Number(object.code) : 0,\n      data: globalThis.Array.isArray(object?.data) ? object.data.map((e: any) => EnvItem.fromJSON(e)) : [],\n      message: isSet(object.message) ? globalThis.String(object.message) : undefined,\n    };\n  },\n\n  toJSON(message: EnvsResponse): unknown {\n    const obj: any = {};\n    if (message.code !== 0) {\n      obj.code = Math.round(message.code);\n    }\n    if (message.data?.length) {\n      obj.data = message.data.map((e) => EnvItem.toJSON(e));\n    }\n    if (message.message !== undefined) {\n      obj.message = message.message;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnvsResponse>, I>>(base?: I): EnvsResponse {\n    return EnvsResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnvsResponse>, I>>(object: I): EnvsResponse {\n    const message = createBaseEnvsResponse();\n    message.code = object.code ?? 0;\n    message.data = object.data?.map((e) => EnvItem.fromPartial(e)) || [];\n    message.message = object.message ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseEnvResponse(): EnvResponse {\n  return { code: 0, data: undefined, message: undefined };\n}\n\nexport const EnvResponse: MessageFns<EnvResponse> = {\n  encode(message: EnvResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.code !== 0) {\n      writer.uint32(8).int32(message.code);\n    }\n    if (message.data !== undefined) {\n      EnvItem.encode(message.data, writer.uint32(18).fork()).join();\n    }\n    if (message.message !== undefined) {\n      writer.uint32(26).string(message.message);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnvResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnvResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.code = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.data = EnvItem.decode(reader, reader.uint32());\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.message = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnvResponse {\n    return {\n      code: isSet(object.code) ? globalThis.Number(object.code) : 0,\n      data: isSet(object.data) ? EnvItem.fromJSON(object.data) : undefined,\n      message: isSet(object.message) ? globalThis.String(object.message) : undefined,\n    };\n  },\n\n  toJSON(message: EnvResponse): unknown {\n    const obj: any = {};\n    if (message.code !== 0) {\n      obj.code = Math.round(message.code);\n    }\n    if (message.data !== undefined) {\n      obj.data = EnvItem.toJSON(message.data);\n    }\n    if (message.message !== undefined) {\n      obj.message = message.message;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnvResponse>, I>>(base?: I): EnvResponse {\n    return EnvResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnvResponse>, I>>(object: I): EnvResponse {\n    const message = createBaseEnvResponse();\n    message.code = object.code ?? 0;\n    message.data = (object.data !== undefined && object.data !== null) ? EnvItem.fromPartial(object.data) : undefined;\n    message.message = object.message ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseResponse(): Response {\n  return { code: 0, message: undefined };\n}\n\nexport const Response: MessageFns<Response> = {\n  encode(message: Response, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.code !== 0) {\n      writer.uint32(8).int32(message.code);\n    }\n    if (message.message !== undefined) {\n      writer.uint32(18).string(message.message);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): Response {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.code = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.message = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): Response {\n    return {\n      code: isSet(object.code) ? globalThis.Number(object.code) : 0,\n      message: isSet(object.message) ? globalThis.String(object.message) : undefined,\n    };\n  },\n\n  toJSON(message: Response): unknown {\n    const obj: any = {};\n    if (message.code !== 0) {\n      obj.code = Math.round(message.code);\n    }\n    if (message.message !== undefined) {\n      obj.message = message.message;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<Response>, I>>(base?: I): Response {\n    return Response.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<Response>, I>>(object: I): Response {\n    const message = createBaseResponse();\n    message.code = object.code ?? 0;\n    message.message = object.message ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseExtraScheduleItem(): ExtraScheduleItem {\n  return { schedule: \"\" };\n}\n\nexport const ExtraScheduleItem: MessageFns<ExtraScheduleItem> = {\n  encode(message: ExtraScheduleItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.schedule !== \"\") {\n      writer.uint32(10).string(message.schedule);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ExtraScheduleItem {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseExtraScheduleItem();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.schedule = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ExtraScheduleItem {\n    return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : \"\" };\n  },\n\n  toJSON(message: ExtraScheduleItem): unknown {\n    const obj: any = {};\n    if (message.schedule !== \"\") {\n      obj.schedule = message.schedule;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ExtraScheduleItem>, I>>(base?: I): ExtraScheduleItem {\n    return ExtraScheduleItem.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ExtraScheduleItem>, I>>(object: I): ExtraScheduleItem {\n    const message = createBaseExtraScheduleItem();\n    message.schedule = object.schedule ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseCronItem(): CronItem {\n  return {\n    id: undefined,\n    command: undefined,\n    schedule: undefined,\n    name: undefined,\n    labels: [],\n    sub_id: undefined,\n    extra_schedules: [],\n    task_before: undefined,\n    task_after: undefined,\n    status: undefined,\n    log_path: undefined,\n    pid: undefined,\n    last_running_time: undefined,\n    last_execution_time: undefined,\n  };\n}\n\nexport const CronItem: MessageFns<CronItem> = {\n  encode(message: CronItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== undefined) {\n      writer.uint32(8).int32(message.id);\n    }\n    if (message.command !== undefined) {\n      writer.uint32(18).string(message.command);\n    }\n    if (message.schedule !== undefined) {\n      writer.uint32(26).string(message.schedule);\n    }\n    if (message.name !== undefined) {\n      writer.uint32(34).string(message.name);\n    }\n    for (const v of message.labels) {\n      writer.uint32(42).string(v!);\n    }\n    if (message.sub_id !== undefined) {\n      writer.uint32(48).int32(message.sub_id);\n    }\n    for (const v of message.extra_schedules) {\n      ExtraScheduleItem.encode(v!, writer.uint32(58).fork()).join();\n    }\n    if (message.task_before !== undefined) {\n      writer.uint32(66).string(message.task_before);\n    }\n    if (message.task_after !== undefined) {\n      writer.uint32(74).string(message.task_after);\n    }\n    if (message.status !== undefined) {\n      writer.uint32(80).int32(message.status);\n    }\n    if (message.log_path !== undefined) {\n      writer.uint32(90).string(message.log_path);\n    }\n    if (message.pid !== undefined) {\n      writer.uint32(96).int32(message.pid);\n    }\n    if (message.last_running_time !== undefined) {\n      writer.uint32(104).int64(message.last_running_time);\n    }\n    if (message.last_execution_time !== undefined) {\n      writer.uint32(112).int64(message.last_execution_time);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CronItem {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCronItem();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.command = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.schedule = reader.string();\n          continue;\n        }\n        case 4: {\n          if (tag !== 34) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        }\n        case 5: {\n          if (tag !== 42) {\n            break;\n          }\n\n          message.labels.push(reader.string());\n          continue;\n        }\n        case 6: {\n          if (tag !== 48) {\n            break;\n          }\n\n          message.sub_id = reader.int32();\n          continue;\n        }\n        case 7: {\n          if (tag !== 58) {\n            break;\n          }\n\n          message.extra_schedules.push(ExtraScheduleItem.decode(reader, reader.uint32()));\n          continue;\n        }\n        case 8: {\n          if (tag !== 66) {\n            break;\n          }\n\n          message.task_before = reader.string();\n          continue;\n        }\n        case 9: {\n          if (tag !== 74) {\n            break;\n          }\n\n          message.task_after = reader.string();\n          continue;\n        }\n        case 10: {\n          if (tag !== 80) {\n            break;\n          }\n\n          message.status = reader.int32();\n          continue;\n        }\n        case 11: {\n          if (tag !== 90) {\n            break;\n          }\n\n          message.log_path = reader.string();\n          continue;\n        }\n        case 12: {\n          if (tag !== 96) {\n            break;\n          }\n\n          message.pid = reader.int32();\n          continue;\n        }\n        case 13: {\n          if (tag !== 104) {\n            break;\n          }\n\n          message.last_running_time = longToNumber(reader.int64());\n          continue;\n        }\n        case 14: {\n          if (tag !== 112) {\n            break;\n          }\n\n          message.last_execution_time = longToNumber(reader.int64());\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CronItem {\n    return {\n      id: isSet(object.id) ? globalThis.Number(object.id) : undefined,\n      command: isSet(object.command) ? globalThis.String(object.command) : undefined,\n      schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : undefined,\n      name: isSet(object.name) ? globalThis.String(object.name) : undefined,\n      labels: globalThis.Array.isArray(object?.labels) ? object.labels.map((e: any) => globalThis.String(e)) : [],\n      sub_id: isSet(object.sub_id) ? globalThis.Number(object.sub_id) : undefined,\n      extra_schedules: globalThis.Array.isArray(object?.extra_schedules)\n        ? object.extra_schedules.map((e: any) => ExtraScheduleItem.fromJSON(e))\n        : [],\n      task_before: isSet(object.task_before) ? globalThis.String(object.task_before) : undefined,\n      task_after: isSet(object.task_after) ? globalThis.String(object.task_after) : undefined,\n      status: isSet(object.status) ? globalThis.Number(object.status) : undefined,\n      log_path: isSet(object.log_path) ? globalThis.String(object.log_path) : undefined,\n      pid: isSet(object.pid) ? globalThis.Number(object.pid) : undefined,\n      last_running_time: isSet(object.last_running_time) ? globalThis.Number(object.last_running_time) : undefined,\n      last_execution_time: isSet(object.last_execution_time)\n        ? globalThis.Number(object.last_execution_time)\n        : undefined,\n    };\n  },\n\n  toJSON(message: CronItem): unknown {\n    const obj: any = {};\n    if (message.id !== undefined) {\n      obj.id = Math.round(message.id);\n    }\n    if (message.command !== undefined) {\n      obj.command = message.command;\n    }\n    if (message.schedule !== undefined) {\n      obj.schedule = message.schedule;\n    }\n    if (message.name !== undefined) {\n      obj.name = message.name;\n    }\n    if (message.labels?.length) {\n      obj.labels = message.labels;\n    }\n    if (message.sub_id !== undefined) {\n      obj.sub_id = Math.round(message.sub_id);\n    }\n    if (message.extra_schedules?.length) {\n      obj.extra_schedules = message.extra_schedules.map((e) => ExtraScheduleItem.toJSON(e));\n    }\n    if (message.task_before !== undefined) {\n      obj.task_before = message.task_before;\n    }\n    if (message.task_after !== undefined) {\n      obj.task_after = message.task_after;\n    }\n    if (message.status !== undefined) {\n      obj.status = Math.round(message.status);\n    }\n    if (message.log_path !== undefined) {\n      obj.log_path = message.log_path;\n    }\n    if (message.pid !== undefined) {\n      obj.pid = Math.round(message.pid);\n    }\n    if (message.last_running_time !== undefined) {\n      obj.last_running_time = Math.round(message.last_running_time);\n    }\n    if (message.last_execution_time !== undefined) {\n      obj.last_execution_time = Math.round(message.last_execution_time);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CronItem>, I>>(base?: I): CronItem {\n    return CronItem.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CronItem>, I>>(object: I): CronItem {\n    const message = createBaseCronItem();\n    message.id = object.id ?? undefined;\n    message.command = object.command ?? undefined;\n    message.schedule = object.schedule ?? undefined;\n    message.name = object.name ?? undefined;\n    message.labels = object.labels?.map((e) => e) || [];\n    message.sub_id = object.sub_id ?? undefined;\n    message.extra_schedules = object.extra_schedules?.map((e) => ExtraScheduleItem.fromPartial(e)) || [];\n    message.task_before = object.task_before ?? undefined;\n    message.task_after = object.task_after ?? undefined;\n    message.status = object.status ?? undefined;\n    message.log_path = object.log_path ?? undefined;\n    message.pid = object.pid ?? undefined;\n    message.last_running_time = object.last_running_time ?? undefined;\n    message.last_execution_time = object.last_execution_time ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseCreateCronRequest(): CreateCronRequest {\n  return {\n    command: \"\",\n    schedule: \"\",\n    name: undefined,\n    labels: [],\n    sub_id: undefined,\n    extra_schedules: [],\n    task_before: undefined,\n    task_after: undefined,\n  };\n}\n\nexport const CreateCronRequest: MessageFns<CreateCronRequest> = {\n  encode(message: CreateCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.command !== \"\") {\n      writer.uint32(10).string(message.command);\n    }\n    if (message.schedule !== \"\") {\n      writer.uint32(18).string(message.schedule);\n    }\n    if (message.name !== undefined) {\n      writer.uint32(26).string(message.name);\n    }\n    for (const v of message.labels) {\n      writer.uint32(34).string(v!);\n    }\n    if (message.sub_id !== undefined) {\n      writer.uint32(40).int32(message.sub_id);\n    }\n    for (const v of message.extra_schedules) {\n      ExtraScheduleItem.encode(v!, writer.uint32(50).fork()).join();\n    }\n    if (message.task_before !== undefined) {\n      writer.uint32(58).string(message.task_before);\n    }\n    if (message.task_after !== undefined) {\n      writer.uint32(66).string(message.task_after);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CreateCronRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCreateCronRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.command = reader.string();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.schedule = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        }\n        case 4: {\n          if (tag !== 34) {\n            break;\n          }\n\n          message.labels.push(reader.string());\n          continue;\n        }\n        case 5: {\n          if (tag !== 40) {\n            break;\n          }\n\n          message.sub_id = reader.int32();\n          continue;\n        }\n        case 6: {\n          if (tag !== 50) {\n            break;\n          }\n\n          message.extra_schedules.push(ExtraScheduleItem.decode(reader, reader.uint32()));\n          continue;\n        }\n        case 7: {\n          if (tag !== 58) {\n            break;\n          }\n\n          message.task_before = reader.string();\n          continue;\n        }\n        case 8: {\n          if (tag !== 66) {\n            break;\n          }\n\n          message.task_after = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CreateCronRequest {\n    return {\n      command: isSet(object.command) ? globalThis.String(object.command) : \"\",\n      schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : \"\",\n      name: isSet(object.name) ? globalThis.String(object.name) : undefined,\n      labels: globalThis.Array.isArray(object?.labels) ? object.labels.map((e: any) => globalThis.String(e)) : [],\n      sub_id: isSet(object.sub_id) ? globalThis.Number(object.sub_id) : undefined,\n      extra_schedules: globalThis.Array.isArray(object?.extra_schedules)\n        ? object.extra_schedules.map((e: any) => ExtraScheduleItem.fromJSON(e))\n        : [],\n      task_before: isSet(object.task_before) ? globalThis.String(object.task_before) : undefined,\n      task_after: isSet(object.task_after) ? globalThis.String(object.task_after) : undefined,\n    };\n  },\n\n  toJSON(message: CreateCronRequest): unknown {\n    const obj: any = {};\n    if (message.command !== \"\") {\n      obj.command = message.command;\n    }\n    if (message.schedule !== \"\") {\n      obj.schedule = message.schedule;\n    }\n    if (message.name !== undefined) {\n      obj.name = message.name;\n    }\n    if (message.labels?.length) {\n      obj.labels = message.labels;\n    }\n    if (message.sub_id !== undefined) {\n      obj.sub_id = Math.round(message.sub_id);\n    }\n    if (message.extra_schedules?.length) {\n      obj.extra_schedules = message.extra_schedules.map((e) => ExtraScheduleItem.toJSON(e));\n    }\n    if (message.task_before !== undefined) {\n      obj.task_before = message.task_before;\n    }\n    if (message.task_after !== undefined) {\n      obj.task_after = message.task_after;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CreateCronRequest>, I>>(base?: I): CreateCronRequest {\n    return CreateCronRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CreateCronRequest>, I>>(object: I): CreateCronRequest {\n    const message = createBaseCreateCronRequest();\n    message.command = object.command ?? \"\";\n    message.schedule = object.schedule ?? \"\";\n    message.name = object.name ?? undefined;\n    message.labels = object.labels?.map((e) => e) || [];\n    message.sub_id = object.sub_id ?? undefined;\n    message.extra_schedules = object.extra_schedules?.map((e) => ExtraScheduleItem.fromPartial(e)) || [];\n    message.task_before = object.task_before ?? undefined;\n    message.task_after = object.task_after ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseUpdateCronRequest(): UpdateCronRequest {\n  return {\n    id: 0,\n    command: undefined,\n    schedule: undefined,\n    name: undefined,\n    labels: [],\n    sub_id: undefined,\n    extra_schedules: [],\n    task_before: undefined,\n    task_after: undefined,\n  };\n}\n\nexport const UpdateCronRequest: MessageFns<UpdateCronRequest> = {\n  encode(message: UpdateCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int32(message.id);\n    }\n    if (message.command !== undefined) {\n      writer.uint32(18).string(message.command);\n    }\n    if (message.schedule !== undefined) {\n      writer.uint32(26).string(message.schedule);\n    }\n    if (message.name !== undefined) {\n      writer.uint32(34).string(message.name);\n    }\n    for (const v of message.labels) {\n      writer.uint32(42).string(v!);\n    }\n    if (message.sub_id !== undefined) {\n      writer.uint32(48).int32(message.sub_id);\n    }\n    for (const v of message.extra_schedules) {\n      ExtraScheduleItem.encode(v!, writer.uint32(58).fork()).join();\n    }\n    if (message.task_before !== undefined) {\n      writer.uint32(66).string(message.task_before);\n    }\n    if (message.task_after !== undefined) {\n      writer.uint32(74).string(message.task_after);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): UpdateCronRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseUpdateCronRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.command = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.schedule = reader.string();\n          continue;\n        }\n        case 4: {\n          if (tag !== 34) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        }\n        case 5: {\n          if (tag !== 42) {\n            break;\n          }\n\n          message.labels.push(reader.string());\n          continue;\n        }\n        case 6: {\n          if (tag !== 48) {\n            break;\n          }\n\n          message.sub_id = reader.int32();\n          continue;\n        }\n        case 7: {\n          if (tag !== 58) {\n            break;\n          }\n\n          message.extra_schedules.push(ExtraScheduleItem.decode(reader, reader.uint32()));\n          continue;\n        }\n        case 8: {\n          if (tag !== 66) {\n            break;\n          }\n\n          message.task_before = reader.string();\n          continue;\n        }\n        case 9: {\n          if (tag !== 74) {\n            break;\n          }\n\n          message.task_after = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): UpdateCronRequest {\n    return {\n      id: isSet(object.id) ? globalThis.Number(object.id) : 0,\n      command: isSet(object.command) ? globalThis.String(object.command) : undefined,\n      schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : undefined,\n      name: isSet(object.name) ? globalThis.String(object.name) : undefined,\n      labels: globalThis.Array.isArray(object?.labels) ? object.labels.map((e: any) => globalThis.String(e)) : [],\n      sub_id: isSet(object.sub_id) ? globalThis.Number(object.sub_id) : undefined,\n      extra_schedules: globalThis.Array.isArray(object?.extra_schedules)\n        ? object.extra_schedules.map((e: any) => ExtraScheduleItem.fromJSON(e))\n        : [],\n      task_before: isSet(object.task_before) ? globalThis.String(object.task_before) : undefined,\n      task_after: isSet(object.task_after) ? globalThis.String(object.task_after) : undefined,\n    };\n  },\n\n  toJSON(message: UpdateCronRequest): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    if (message.command !== undefined) {\n      obj.command = message.command;\n    }\n    if (message.schedule !== undefined) {\n      obj.schedule = message.schedule;\n    }\n    if (message.name !== undefined) {\n      obj.name = message.name;\n    }\n    if (message.labels?.length) {\n      obj.labels = message.labels;\n    }\n    if (message.sub_id !== undefined) {\n      obj.sub_id = Math.round(message.sub_id);\n    }\n    if (message.extra_schedules?.length) {\n      obj.extra_schedules = message.extra_schedules.map((e) => ExtraScheduleItem.toJSON(e));\n    }\n    if (message.task_before !== undefined) {\n      obj.task_before = message.task_before;\n    }\n    if (message.task_after !== undefined) {\n      obj.task_after = message.task_after;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<UpdateCronRequest>, I>>(base?: I): UpdateCronRequest {\n    return UpdateCronRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<UpdateCronRequest>, I>>(object: I): UpdateCronRequest {\n    const message = createBaseUpdateCronRequest();\n    message.id = object.id ?? 0;\n    message.command = object.command ?? undefined;\n    message.schedule = object.schedule ?? undefined;\n    message.name = object.name ?? undefined;\n    message.labels = object.labels?.map((e) => e) || [];\n    message.sub_id = object.sub_id ?? undefined;\n    message.extra_schedules = object.extra_schedules?.map((e) => ExtraScheduleItem.fromPartial(e)) || [];\n    message.task_before = object.task_before ?? undefined;\n    message.task_after = object.task_after ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseDeleteCronsRequest(): DeleteCronsRequest {\n  return { ids: [] };\n}\n\nexport const DeleteCronsRequest: MessageFns<DeleteCronsRequest> = {\n  encode(message: DeleteCronsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDeleteCronsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DeleteCronsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: DeleteCronsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DeleteCronsRequest>, I>>(base?: I): DeleteCronsRequest {\n    return DeleteCronsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DeleteCronsRequest>, I>>(object: I): DeleteCronsRequest {\n    const message = createBaseDeleteCronsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseGetCronsRequest(): GetCronsRequest {\n  return { searchValue: undefined };\n}\n\nexport const GetCronsRequest: MessageFns<GetCronsRequest> = {\n  encode(message: GetCronsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.searchValue !== undefined) {\n      writer.uint32(10).string(message.searchValue);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GetCronsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGetCronsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.searchValue = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GetCronsRequest {\n    return { searchValue: isSet(object.searchValue) ? globalThis.String(object.searchValue) : undefined };\n  },\n\n  toJSON(message: GetCronsRequest): unknown {\n    const obj: any = {};\n    if (message.searchValue !== undefined) {\n      obj.searchValue = message.searchValue;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GetCronsRequest>, I>>(base?: I): GetCronsRequest {\n    return GetCronsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GetCronsRequest>, I>>(object: I): GetCronsRequest {\n    const message = createBaseGetCronsRequest();\n    message.searchValue = object.searchValue ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseGetCronByIdRequest(): GetCronByIdRequest {\n  return { id: 0 };\n}\n\nexport const GetCronByIdRequest: MessageFns<GetCronByIdRequest> = {\n  encode(message: GetCronByIdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int32(message.id);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GetCronByIdRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGetCronByIdRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GetCronByIdRequest {\n    return { id: isSet(object.id) ? globalThis.Number(object.id) : 0 };\n  },\n\n  toJSON(message: GetCronByIdRequest): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GetCronByIdRequest>, I>>(base?: I): GetCronByIdRequest {\n    return GetCronByIdRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GetCronByIdRequest>, I>>(object: I): GetCronByIdRequest {\n    const message = createBaseGetCronByIdRequest();\n    message.id = object.id ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseEnableCronsRequest(): EnableCronsRequest {\n  return { ids: [] };\n}\n\nexport const EnableCronsRequest: MessageFns<EnableCronsRequest> = {\n  encode(message: EnableCronsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnableCronsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnableCronsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnableCronsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: EnableCronsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnableCronsRequest>, I>>(base?: I): EnableCronsRequest {\n    return EnableCronsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnableCronsRequest>, I>>(object: I): EnableCronsRequest {\n    const message = createBaseEnableCronsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseDisableCronsRequest(): DisableCronsRequest {\n  return { ids: [] };\n}\n\nexport const DisableCronsRequest: MessageFns<DisableCronsRequest> = {\n  encode(message: DisableCronsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DisableCronsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDisableCronsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DisableCronsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: DisableCronsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DisableCronsRequest>, I>>(base?: I): DisableCronsRequest {\n    return DisableCronsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DisableCronsRequest>, I>>(object: I): DisableCronsRequest {\n    const message = createBaseDisableCronsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseRunCronsRequest(): RunCronsRequest {\n  return { ids: [] };\n}\n\nexport const RunCronsRequest: MessageFns<RunCronsRequest> = {\n  encode(message: RunCronsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.ids) {\n      writer.int32(v);\n    }\n    writer.join();\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): RunCronsRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseRunCronsRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag === 8) {\n            message.ids.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.ids.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): RunCronsRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.Number(e)) : [] };\n  },\n\n  toJSON(message: RunCronsRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids.map((e) => Math.round(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<RunCronsRequest>, I>>(base?: I): RunCronsRequest {\n    return RunCronsRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<RunCronsRequest>, I>>(object: I): RunCronsRequest {\n    const message = createBaseRunCronsRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseCronsResponse(): CronsResponse {\n  return { code: 0, data: [], message: undefined };\n}\n\nexport const CronsResponse: MessageFns<CronsResponse> = {\n  encode(message: CronsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.code !== 0) {\n      writer.uint32(8).int32(message.code);\n    }\n    for (const v of message.data) {\n      CronItem.encode(v!, writer.uint32(18).fork()).join();\n    }\n    if (message.message !== undefined) {\n      writer.uint32(26).string(message.message);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CronsResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCronsResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.code = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.data.push(CronItem.decode(reader, reader.uint32()));\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.message = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CronsResponse {\n    return {\n      code: isSet(object.code) ? globalThis.Number(object.code) : 0,\n      data: globalThis.Array.isArray(object?.data) ? object.data.map((e: any) => CronItem.fromJSON(e)) : [],\n      message: isSet(object.message) ? globalThis.String(object.message) : undefined,\n    };\n  },\n\n  toJSON(message: CronsResponse): unknown {\n    const obj: any = {};\n    if (message.code !== 0) {\n      obj.code = Math.round(message.code);\n    }\n    if (message.data?.length) {\n      obj.data = message.data.map((e) => CronItem.toJSON(e));\n    }\n    if (message.message !== undefined) {\n      obj.message = message.message;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CronsResponse>, I>>(base?: I): CronsResponse {\n    return CronsResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CronsResponse>, I>>(object: I): CronsResponse {\n    const message = createBaseCronsResponse();\n    message.code = object.code ?? 0;\n    message.data = object.data?.map((e) => CronItem.fromPartial(e)) || [];\n    message.message = object.message ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseCronResponse(): CronResponse {\n  return { code: 0, data: undefined, message: undefined };\n}\n\nexport const CronResponse: MessageFns<CronResponse> = {\n  encode(message: CronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.code !== 0) {\n      writer.uint32(8).int32(message.code);\n    }\n    if (message.data !== undefined) {\n      CronItem.encode(message.data, writer.uint32(18).fork()).join();\n    }\n    if (message.message !== undefined) {\n      writer.uint32(26).string(message.message);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CronResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCronResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.code = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.data = CronItem.decode(reader, reader.uint32());\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.message = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CronResponse {\n    return {\n      code: isSet(object.code) ? globalThis.Number(object.code) : 0,\n      data: isSet(object.data) ? CronItem.fromJSON(object.data) : undefined,\n      message: isSet(object.message) ? globalThis.String(object.message) : undefined,\n    };\n  },\n\n  toJSON(message: CronResponse): unknown {\n    const obj: any = {};\n    if (message.code !== 0) {\n      obj.code = Math.round(message.code);\n    }\n    if (message.data !== undefined) {\n      obj.data = CronItem.toJSON(message.data);\n    }\n    if (message.message !== undefined) {\n      obj.message = message.message;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CronResponse>, I>>(base?: I): CronResponse {\n    return CronResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CronResponse>, I>>(object: I): CronResponse {\n    const message = createBaseCronResponse();\n    message.code = object.code ?? 0;\n    message.data = (object.data !== undefined && object.data !== null) ? CronItem.fromPartial(object.data) : undefined;\n    message.message = object.message ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseCronDetailRequest(): CronDetailRequest {\n  return { log_path: \"\" };\n}\n\nexport const CronDetailRequest: MessageFns<CronDetailRequest> = {\n  encode(message: CronDetailRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.log_path !== \"\") {\n      writer.uint32(10).string(message.log_path);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CronDetailRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCronDetailRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.log_path = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CronDetailRequest {\n    return { log_path: isSet(object.log_path) ? globalThis.String(object.log_path) : \"\" };\n  },\n\n  toJSON(message: CronDetailRequest): unknown {\n    const obj: any = {};\n    if (message.log_path !== \"\") {\n      obj.log_path = message.log_path;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CronDetailRequest>, I>>(base?: I): CronDetailRequest {\n    return CronDetailRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CronDetailRequest>, I>>(object: I): CronDetailRequest {\n    const message = createBaseCronDetailRequest();\n    message.log_path = object.log_path ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseCronDetailResponse(): CronDetailResponse {\n  return { code: 0, data: undefined, message: undefined };\n}\n\nexport const CronDetailResponse: MessageFns<CronDetailResponse> = {\n  encode(message: CronDetailResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.code !== 0) {\n      writer.uint32(8).int32(message.code);\n    }\n    if (message.data !== undefined) {\n      CronItem.encode(message.data, writer.uint32(18).fork()).join();\n    }\n    if (message.message !== undefined) {\n      writer.uint32(26).string(message.message);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): CronDetailResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCronDetailResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.code = reader.int32();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.data = CronItem.decode(reader, reader.uint32());\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.message = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): CronDetailResponse {\n    return {\n      code: isSet(object.code) ? globalThis.Number(object.code) : 0,\n      data: isSet(object.data) ? CronItem.fromJSON(object.data) : undefined,\n      message: isSet(object.message) ? globalThis.String(object.message) : undefined,\n    };\n  },\n\n  toJSON(message: CronDetailResponse): unknown {\n    const obj: any = {};\n    if (message.code !== 0) {\n      obj.code = Math.round(message.code);\n    }\n    if (message.data !== undefined) {\n      obj.data = CronItem.toJSON(message.data);\n    }\n    if (message.message !== undefined) {\n      obj.message = message.message;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<CronDetailResponse>, I>>(base?: I): CronDetailResponse {\n    return CronDetailResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<CronDetailResponse>, I>>(object: I): CronDetailResponse {\n    const message = createBaseCronDetailResponse();\n    message.code = object.code ?? 0;\n    message.data = (object.data !== undefined && object.data !== null) ? CronItem.fromPartial(object.data) : undefined;\n    message.message = object.message ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseNotificationInfo(): NotificationInfo {\n  return {\n    type: 0,\n    gotifyUrl: undefined,\n    gotifyToken: undefined,\n    gotifyPriority: undefined,\n    goCqHttpBotUrl: undefined,\n    goCqHttpBotToken: undefined,\n    goCqHttpBotQq: undefined,\n    serverChanKey: undefined,\n    pushDeerKey: undefined,\n    pushDeerUrl: undefined,\n    synologyChatUrl: undefined,\n    barkPush: undefined,\n    barkIcon: undefined,\n    barkSound: undefined,\n    barkGroup: undefined,\n    barkLevel: undefined,\n    barkUrl: undefined,\n    barkArchive: undefined,\n    telegramBotToken: undefined,\n    telegramBotUserId: undefined,\n    telegramBotProxyHost: undefined,\n    telegramBotProxyPort: undefined,\n    telegramBotProxyAuth: undefined,\n    telegramBotApiHost: undefined,\n    dingtalkBotToken: undefined,\n    dingtalkBotSecret: undefined,\n    weWorkBotKey: undefined,\n    weWorkOrigin: undefined,\n    weWorkAppKey: undefined,\n    aibotkKey: undefined,\n    aibotkType: undefined,\n    aibotkName: undefined,\n    iGotPushKey: undefined,\n    pushPlusToken: undefined,\n    pushPlusUser: undefined,\n    pushPlusTemplate: undefined,\n    pushplusChannel: undefined,\n    pushplusWebhook: undefined,\n    pushplusCallbackUrl: undefined,\n    pushplusTo: undefined,\n    wePlusBotToken: undefined,\n    wePlusBotReceiver: undefined,\n    wePlusBotVersion: undefined,\n    emailService: undefined,\n    emailUser: undefined,\n    emailPass: undefined,\n    emailTo: undefined,\n    pushMeKey: undefined,\n    pushMeUrl: undefined,\n    chronocatURL: undefined,\n    chronocatQQ: undefined,\n    chronocatToken: undefined,\n    webhookHeaders: undefined,\n    webhookBody: undefined,\n    webhookUrl: undefined,\n    webhookMethod: undefined,\n    webhookContentType: undefined,\n    larkKey: undefined,\n    larkSecret: undefined,\n    ntfyUrl: undefined,\n    ntfyTopic: undefined,\n    ntfyPriority: undefined,\n    ntfyToken: undefined,\n    ntfyUsername: undefined,\n    ntfyPassword: undefined,\n    ntfyActions: undefined,\n    wxPusherBotAppToken: undefined,\n    wxPusherBotTopicIds: undefined,\n    wxPusherBotUids: undefined,\n  };\n}\n\nexport const NotificationInfo: MessageFns<NotificationInfo> = {\n  encode(message: NotificationInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.type !== 0) {\n      writer.uint32(8).int32(message.type);\n    }\n    if (message.gotifyUrl !== undefined) {\n      writer.uint32(18).string(message.gotifyUrl);\n    }\n    if (message.gotifyToken !== undefined) {\n      writer.uint32(26).string(message.gotifyToken);\n    }\n    if (message.gotifyPriority !== undefined) {\n      writer.uint32(32).int32(message.gotifyPriority);\n    }\n    if (message.goCqHttpBotUrl !== undefined) {\n      writer.uint32(42).string(message.goCqHttpBotUrl);\n    }\n    if (message.goCqHttpBotToken !== undefined) {\n      writer.uint32(50).string(message.goCqHttpBotToken);\n    }\n    if (message.goCqHttpBotQq !== undefined) {\n      writer.uint32(58).string(message.goCqHttpBotQq);\n    }\n    if (message.serverChanKey !== undefined) {\n      writer.uint32(66).string(message.serverChanKey);\n    }\n    if (message.pushDeerKey !== undefined) {\n      writer.uint32(74).string(message.pushDeerKey);\n    }\n    if (message.pushDeerUrl !== undefined) {\n      writer.uint32(82).string(message.pushDeerUrl);\n    }\n    if (message.synologyChatUrl !== undefined) {\n      writer.uint32(90).string(message.synologyChatUrl);\n    }\n    if (message.barkPush !== undefined) {\n      writer.uint32(98).string(message.barkPush);\n    }\n    if (message.barkIcon !== undefined) {\n      writer.uint32(106).string(message.barkIcon);\n    }\n    if (message.barkSound !== undefined) {\n      writer.uint32(114).string(message.barkSound);\n    }\n    if (message.barkGroup !== undefined) {\n      writer.uint32(122).string(message.barkGroup);\n    }\n    if (message.barkLevel !== undefined) {\n      writer.uint32(130).string(message.barkLevel);\n    }\n    if (message.barkUrl !== undefined) {\n      writer.uint32(138).string(message.barkUrl);\n    }\n    if (message.barkArchive !== undefined) {\n      writer.uint32(146).string(message.barkArchive);\n    }\n    if (message.telegramBotToken !== undefined) {\n      writer.uint32(154).string(message.telegramBotToken);\n    }\n    if (message.telegramBotUserId !== undefined) {\n      writer.uint32(162).string(message.telegramBotUserId);\n    }\n    if (message.telegramBotProxyHost !== undefined) {\n      writer.uint32(170).string(message.telegramBotProxyHost);\n    }\n    if (message.telegramBotProxyPort !== undefined) {\n      writer.uint32(178).string(message.telegramBotProxyPort);\n    }\n    if (message.telegramBotProxyAuth !== undefined) {\n      writer.uint32(186).string(message.telegramBotProxyAuth);\n    }\n    if (message.telegramBotApiHost !== undefined) {\n      writer.uint32(194).string(message.telegramBotApiHost);\n    }\n    if (message.dingtalkBotToken !== undefined) {\n      writer.uint32(202).string(message.dingtalkBotToken);\n    }\n    if (message.dingtalkBotSecret !== undefined) {\n      writer.uint32(210).string(message.dingtalkBotSecret);\n    }\n    if (message.weWorkBotKey !== undefined) {\n      writer.uint32(218).string(message.weWorkBotKey);\n    }\n    if (message.weWorkOrigin !== undefined) {\n      writer.uint32(226).string(message.weWorkOrigin);\n    }\n    if (message.weWorkAppKey !== undefined) {\n      writer.uint32(234).string(message.weWorkAppKey);\n    }\n    if (message.aibotkKey !== undefined) {\n      writer.uint32(242).string(message.aibotkKey);\n    }\n    if (message.aibotkType !== undefined) {\n      writer.uint32(250).string(message.aibotkType);\n    }\n    if (message.aibotkName !== undefined) {\n      writer.uint32(258).string(message.aibotkName);\n    }\n    if (message.iGotPushKey !== undefined) {\n      writer.uint32(266).string(message.iGotPushKey);\n    }\n    if (message.pushPlusToken !== undefined) {\n      writer.uint32(274).string(message.pushPlusToken);\n    }\n    if (message.pushPlusUser !== undefined) {\n      writer.uint32(282).string(message.pushPlusUser);\n    }\n    if (message.pushPlusTemplate !== undefined) {\n      writer.uint32(290).string(message.pushPlusTemplate);\n    }\n    if (message.pushplusChannel !== undefined) {\n      writer.uint32(298).string(message.pushplusChannel);\n    }\n    if (message.pushplusWebhook !== undefined) {\n      writer.uint32(306).string(message.pushplusWebhook);\n    }\n    if (message.pushplusCallbackUrl !== undefined) {\n      writer.uint32(314).string(message.pushplusCallbackUrl);\n    }\n    if (message.pushplusTo !== undefined) {\n      writer.uint32(322).string(message.pushplusTo);\n    }\n    if (message.wePlusBotToken !== undefined) {\n      writer.uint32(330).string(message.wePlusBotToken);\n    }\n    if (message.wePlusBotReceiver !== undefined) {\n      writer.uint32(338).string(message.wePlusBotReceiver);\n    }\n    if (message.wePlusBotVersion !== undefined) {\n      writer.uint32(346).string(message.wePlusBotVersion);\n    }\n    if (message.emailService !== undefined) {\n      writer.uint32(354).string(message.emailService);\n    }\n    if (message.emailUser !== undefined) {\n      writer.uint32(362).string(message.emailUser);\n    }\n    if (message.emailPass !== undefined) {\n      writer.uint32(370).string(message.emailPass);\n    }\n    if (message.emailTo !== undefined) {\n      writer.uint32(378).string(message.emailTo);\n    }\n    if (message.pushMeKey !== undefined) {\n      writer.uint32(386).string(message.pushMeKey);\n    }\n    if (message.pushMeUrl !== undefined) {\n      writer.uint32(394).string(message.pushMeUrl);\n    }\n    if (message.chronocatURL !== undefined) {\n      writer.uint32(402).string(message.chronocatURL);\n    }\n    if (message.chronocatQQ !== undefined) {\n      writer.uint32(410).string(message.chronocatQQ);\n    }\n    if (message.chronocatToken !== undefined) {\n      writer.uint32(418).string(message.chronocatToken);\n    }\n    if (message.webhookHeaders !== undefined) {\n      writer.uint32(426).string(message.webhookHeaders);\n    }\n    if (message.webhookBody !== undefined) {\n      writer.uint32(434).string(message.webhookBody);\n    }\n    if (message.webhookUrl !== undefined) {\n      writer.uint32(442).string(message.webhookUrl);\n    }\n    if (message.webhookMethod !== undefined) {\n      writer.uint32(450).string(message.webhookMethod);\n    }\n    if (message.webhookContentType !== undefined) {\n      writer.uint32(458).string(message.webhookContentType);\n    }\n    if (message.larkKey !== undefined) {\n      writer.uint32(466).string(message.larkKey);\n    }\n    if (message.larkSecret !== undefined) {\n      writer.uint32(554).string(message.larkSecret);\n    }\n    if (message.ntfyUrl !== undefined) {\n      writer.uint32(474).string(message.ntfyUrl);\n    }\n    if (message.ntfyTopic !== undefined) {\n      writer.uint32(482).string(message.ntfyTopic);\n    }\n    if (message.ntfyPriority !== undefined) {\n      writer.uint32(490).string(message.ntfyPriority);\n    }\n    if (message.ntfyToken !== undefined) {\n      writer.uint32(498).string(message.ntfyToken);\n    }\n    if (message.ntfyUsername !== undefined) {\n      writer.uint32(506).string(message.ntfyUsername);\n    }\n    if (message.ntfyPassword !== undefined) {\n      writer.uint32(514).string(message.ntfyPassword);\n    }\n    if (message.ntfyActions !== undefined) {\n      writer.uint32(522).string(message.ntfyActions);\n    }\n    if (message.wxPusherBotAppToken !== undefined) {\n      writer.uint32(530).string(message.wxPusherBotAppToken);\n    }\n    if (message.wxPusherBotTopicIds !== undefined) {\n      writer.uint32(538).string(message.wxPusherBotTopicIds);\n    }\n    if (message.wxPusherBotUids !== undefined) {\n      writer.uint32(546).string(message.wxPusherBotUids);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): NotificationInfo {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseNotificationInfo();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.type = reader.int32() as any;\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.gotifyUrl = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.gotifyToken = reader.string();\n          continue;\n        }\n        case 4: {\n          if (tag !== 32) {\n            break;\n          }\n\n          message.gotifyPriority = reader.int32();\n          continue;\n        }\n        case 5: {\n          if (tag !== 42) {\n            break;\n          }\n\n          message.goCqHttpBotUrl = reader.string();\n          continue;\n        }\n        case 6: {\n          if (tag !== 50) {\n            break;\n          }\n\n          message.goCqHttpBotToken = reader.string();\n          continue;\n        }\n        case 7: {\n          if (tag !== 58) {\n            break;\n          }\n\n          message.goCqHttpBotQq = reader.string();\n          continue;\n        }\n        case 8: {\n          if (tag !== 66) {\n            break;\n          }\n\n          message.serverChanKey = reader.string();\n          continue;\n        }\n        case 9: {\n          if (tag !== 74) {\n            break;\n          }\n\n          message.pushDeerKey = reader.string();\n          continue;\n        }\n        case 10: {\n          if (tag !== 82) {\n            break;\n          }\n\n          message.pushDeerUrl = reader.string();\n          continue;\n        }\n        case 11: {\n          if (tag !== 90) {\n            break;\n          }\n\n          message.synologyChatUrl = reader.string();\n          continue;\n        }\n        case 12: {\n          if (tag !== 98) {\n            break;\n          }\n\n          message.barkPush = reader.string();\n          continue;\n        }\n        case 13: {\n          if (tag !== 106) {\n            break;\n          }\n\n          message.barkIcon = reader.string();\n          continue;\n        }\n        case 14: {\n          if (tag !== 114) {\n            break;\n          }\n\n          message.barkSound = reader.string();\n          continue;\n        }\n        case 15: {\n          if (tag !== 122) {\n            break;\n          }\n\n          message.barkGroup = reader.string();\n          continue;\n        }\n        case 16: {\n          if (tag !== 130) {\n            break;\n          }\n\n          message.barkLevel = reader.string();\n          continue;\n        }\n        case 17: {\n          if (tag !== 138) {\n            break;\n          }\n\n          message.barkUrl = reader.string();\n          continue;\n        }\n        case 18: {\n          if (tag !== 146) {\n            break;\n          }\n\n          message.barkArchive = reader.string();\n          continue;\n        }\n        case 19: {\n          if (tag !== 154) {\n            break;\n          }\n\n          message.telegramBotToken = reader.string();\n          continue;\n        }\n        case 20: {\n          if (tag !== 162) {\n            break;\n          }\n\n          message.telegramBotUserId = reader.string();\n          continue;\n        }\n        case 21: {\n          if (tag !== 170) {\n            break;\n          }\n\n          message.telegramBotProxyHost = reader.string();\n          continue;\n        }\n        case 22: {\n          if (tag !== 178) {\n            break;\n          }\n\n          message.telegramBotProxyPort = reader.string();\n          continue;\n        }\n        case 23: {\n          if (tag !== 186) {\n            break;\n          }\n\n          message.telegramBotProxyAuth = reader.string();\n          continue;\n        }\n        case 24: {\n          if (tag !== 194) {\n            break;\n          }\n\n          message.telegramBotApiHost = reader.string();\n          continue;\n        }\n        case 25: {\n          if (tag !== 202) {\n            break;\n          }\n\n          message.dingtalkBotToken = reader.string();\n          continue;\n        }\n        case 26: {\n          if (tag !== 210) {\n            break;\n          }\n\n          message.dingtalkBotSecret = reader.string();\n          continue;\n        }\n        case 27: {\n          if (tag !== 218) {\n            break;\n          }\n\n          message.weWorkBotKey = reader.string();\n          continue;\n        }\n        case 28: {\n          if (tag !== 226) {\n            break;\n          }\n\n          message.weWorkOrigin = reader.string();\n          continue;\n        }\n        case 29: {\n          if (tag !== 234) {\n            break;\n          }\n\n          message.weWorkAppKey = reader.string();\n          continue;\n        }\n        case 30: {\n          if (tag !== 242) {\n            break;\n          }\n\n          message.aibotkKey = reader.string();\n          continue;\n        }\n        case 31: {\n          if (tag !== 250) {\n            break;\n          }\n\n          message.aibotkType = reader.string();\n          continue;\n        }\n        case 32: {\n          if (tag !== 258) {\n            break;\n          }\n\n          message.aibotkName = reader.string();\n          continue;\n        }\n        case 33: {\n          if (tag !== 266) {\n            break;\n          }\n\n          message.iGotPushKey = reader.string();\n          continue;\n        }\n        case 34: {\n          if (tag !== 274) {\n            break;\n          }\n\n          message.pushPlusToken = reader.string();\n          continue;\n        }\n        case 35: {\n          if (tag !== 282) {\n            break;\n          }\n\n          message.pushPlusUser = reader.string();\n          continue;\n        }\n        case 36: {\n          if (tag !== 290) {\n            break;\n          }\n\n          message.pushPlusTemplate = reader.string();\n          continue;\n        }\n        case 37: {\n          if (tag !== 298) {\n            break;\n          }\n\n          message.pushplusChannel = reader.string();\n          continue;\n        }\n        case 38: {\n          if (tag !== 306) {\n            break;\n          }\n\n          message.pushplusWebhook = reader.string();\n          continue;\n        }\n        case 39: {\n          if (tag !== 314) {\n            break;\n          }\n\n          message.pushplusCallbackUrl = reader.string();\n          continue;\n        }\n        case 40: {\n          if (tag !== 322) {\n            break;\n          }\n\n          message.pushplusTo = reader.string();\n          continue;\n        }\n        case 41: {\n          if (tag !== 330) {\n            break;\n          }\n\n          message.wePlusBotToken = reader.string();\n          continue;\n        }\n        case 42: {\n          if (tag !== 338) {\n            break;\n          }\n\n          message.wePlusBotReceiver = reader.string();\n          continue;\n        }\n        case 43: {\n          if (tag !== 346) {\n            break;\n          }\n\n          message.wePlusBotVersion = reader.string();\n          continue;\n        }\n        case 44: {\n          if (tag !== 354) {\n            break;\n          }\n\n          message.emailService = reader.string();\n          continue;\n        }\n        case 45: {\n          if (tag !== 362) {\n            break;\n          }\n\n          message.emailUser = reader.string();\n          continue;\n        }\n        case 46: {\n          if (tag !== 370) {\n            break;\n          }\n\n          message.emailPass = reader.string();\n          continue;\n        }\n        case 47: {\n          if (tag !== 378) {\n            break;\n          }\n\n          message.emailTo = reader.string();\n          continue;\n        }\n        case 48: {\n          if (tag !== 386) {\n            break;\n          }\n\n          message.pushMeKey = reader.string();\n          continue;\n        }\n        case 49: {\n          if (tag !== 394) {\n            break;\n          }\n\n          message.pushMeUrl = reader.string();\n          continue;\n        }\n        case 50: {\n          if (tag !== 402) {\n            break;\n          }\n\n          message.chronocatURL = reader.string();\n          continue;\n        }\n        case 51: {\n          if (tag !== 410) {\n            break;\n          }\n\n          message.chronocatQQ = reader.string();\n          continue;\n        }\n        case 52: {\n          if (tag !== 418) {\n            break;\n          }\n\n          message.chronocatToken = reader.string();\n          continue;\n        }\n        case 53: {\n          if (tag !== 426) {\n            break;\n          }\n\n          message.webhookHeaders = reader.string();\n          continue;\n        }\n        case 54: {\n          if (tag !== 434) {\n            break;\n          }\n\n          message.webhookBody = reader.string();\n          continue;\n        }\n        case 55: {\n          if (tag !== 442) {\n            break;\n          }\n\n          message.webhookUrl = reader.string();\n          continue;\n        }\n        case 56: {\n          if (tag !== 450) {\n            break;\n          }\n\n          message.webhookMethod = reader.string();\n          continue;\n        }\n        case 57: {\n          if (tag !== 458) {\n            break;\n          }\n\n          message.webhookContentType = reader.string();\n          continue;\n        }\n        case 58: {\n          if (tag !== 466) {\n            break;\n          }\n\n          message.larkKey = reader.string();\n          continue;\n        }\n        case 69: {\n          if (tag !== 554) {\n            break;\n          }\n\n          message.larkSecret = reader.string();\n          continue;\n        }\n        case 59: {\n          if (tag !== 474) {\n            break;\n          }\n\n          message.ntfyUrl = reader.string();\n          continue;\n        }\n        case 60: {\n          if (tag !== 482) {\n            break;\n          }\n\n          message.ntfyTopic = reader.string();\n          continue;\n        }\n        case 61: {\n          if (tag !== 490) {\n            break;\n          }\n\n          message.ntfyPriority = reader.string();\n          continue;\n        }\n        case 62: {\n          if (tag !== 498) {\n            break;\n          }\n\n          message.ntfyToken = reader.string();\n          continue;\n        }\n        case 63: {\n          if (tag !== 506) {\n            break;\n          }\n\n          message.ntfyUsername = reader.string();\n          continue;\n        }\n        case 64: {\n          if (tag !== 514) {\n            break;\n          }\n\n          message.ntfyPassword = reader.string();\n          continue;\n        }\n        case 65: {\n          if (tag !== 522) {\n            break;\n          }\n\n          message.ntfyActions = reader.string();\n          continue;\n        }\n        case 66: {\n          if (tag !== 530) {\n            break;\n          }\n\n          message.wxPusherBotAppToken = reader.string();\n          continue;\n        }\n        case 67: {\n          if (tag !== 538) {\n            break;\n          }\n\n          message.wxPusherBotTopicIds = reader.string();\n          continue;\n        }\n        case 68: {\n          if (tag !== 546) {\n            break;\n          }\n\n          message.wxPusherBotUids = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): NotificationInfo {\n    return {\n      type: isSet(object.type) ? notificationModeFromJSON(object.type) : 0,\n      gotifyUrl: isSet(object.gotifyUrl) ? globalThis.String(object.gotifyUrl) : undefined,\n      gotifyToken: isSet(object.gotifyToken) ? globalThis.String(object.gotifyToken) : undefined,\n      gotifyPriority: isSet(object.gotifyPriority) ? globalThis.Number(object.gotifyPriority) : undefined,\n      goCqHttpBotUrl: isSet(object.goCqHttpBotUrl) ? globalThis.String(object.goCqHttpBotUrl) : undefined,\n      goCqHttpBotToken: isSet(object.goCqHttpBotToken) ? globalThis.String(object.goCqHttpBotToken) : undefined,\n      goCqHttpBotQq: isSet(object.goCqHttpBotQq) ? globalThis.String(object.goCqHttpBotQq) : undefined,\n      serverChanKey: isSet(object.serverChanKey) ? globalThis.String(object.serverChanKey) : undefined,\n      pushDeerKey: isSet(object.pushDeerKey) ? globalThis.String(object.pushDeerKey) : undefined,\n      pushDeerUrl: isSet(object.pushDeerUrl) ? globalThis.String(object.pushDeerUrl) : undefined,\n      synologyChatUrl: isSet(object.synologyChatUrl) ? globalThis.String(object.synologyChatUrl) : undefined,\n      barkPush: isSet(object.barkPush) ? globalThis.String(object.barkPush) : undefined,\n      barkIcon: isSet(object.barkIcon) ? globalThis.String(object.barkIcon) : undefined,\n      barkSound: isSet(object.barkSound) ? globalThis.String(object.barkSound) : undefined,\n      barkGroup: isSet(object.barkGroup) ? globalThis.String(object.barkGroup) : undefined,\n      barkLevel: isSet(object.barkLevel) ? globalThis.String(object.barkLevel) : undefined,\n      barkUrl: isSet(object.barkUrl) ? globalThis.String(object.barkUrl) : undefined,\n      barkArchive: isSet(object.barkArchive) ? globalThis.String(object.barkArchive) : undefined,\n      telegramBotToken: isSet(object.telegramBotToken) ? globalThis.String(object.telegramBotToken) : undefined,\n      telegramBotUserId: isSet(object.telegramBotUserId) ? globalThis.String(object.telegramBotUserId) : undefined,\n      telegramBotProxyHost: isSet(object.telegramBotProxyHost)\n        ? globalThis.String(object.telegramBotProxyHost)\n        : undefined,\n      telegramBotProxyPort: isSet(object.telegramBotProxyPort)\n        ? globalThis.String(object.telegramBotProxyPort)\n        : undefined,\n      telegramBotProxyAuth: isSet(object.telegramBotProxyAuth)\n        ? globalThis.String(object.telegramBotProxyAuth)\n        : undefined,\n      telegramBotApiHost: isSet(object.telegramBotApiHost) ? globalThis.String(object.telegramBotApiHost) : undefined,\n      dingtalkBotToken: isSet(object.dingtalkBotToken) ? globalThis.String(object.dingtalkBotToken) : undefined,\n      dingtalkBotSecret: isSet(object.dingtalkBotSecret) ? globalThis.String(object.dingtalkBotSecret) : undefined,\n      weWorkBotKey: isSet(object.weWorkBotKey) ? globalThis.String(object.weWorkBotKey) : undefined,\n      weWorkOrigin: isSet(object.weWorkOrigin) ? globalThis.String(object.weWorkOrigin) : undefined,\n      weWorkAppKey: isSet(object.weWorkAppKey) ? globalThis.String(object.weWorkAppKey) : undefined,\n      aibotkKey: isSet(object.aibotkKey) ? globalThis.String(object.aibotkKey) : undefined,\n      aibotkType: isSet(object.aibotkType) ? globalThis.String(object.aibotkType) : undefined,\n      aibotkName: isSet(object.aibotkName) ? globalThis.String(object.aibotkName) : undefined,\n      iGotPushKey: isSet(object.iGotPushKey) ? globalThis.String(object.iGotPushKey) : undefined,\n      pushPlusToken: isSet(object.pushPlusToken) ? globalThis.String(object.pushPlusToken) : undefined,\n      pushPlusUser: isSet(object.pushPlusUser) ? globalThis.String(object.pushPlusUser) : undefined,\n      pushPlusTemplate: isSet(object.pushPlusTemplate) ? globalThis.String(object.pushPlusTemplate) : undefined,\n      pushplusChannel: isSet(object.pushplusChannel) ? globalThis.String(object.pushplusChannel) : undefined,\n      pushplusWebhook: isSet(object.pushplusWebhook) ? globalThis.String(object.pushplusWebhook) : undefined,\n      pushplusCallbackUrl: isSet(object.pushplusCallbackUrl)\n        ? globalThis.String(object.pushplusCallbackUrl)\n        : undefined,\n      pushplusTo: isSet(object.pushplusTo) ? globalThis.String(object.pushplusTo) : undefined,\n      wePlusBotToken: isSet(object.wePlusBotToken) ? globalThis.String(object.wePlusBotToken) : undefined,\n      wePlusBotReceiver: isSet(object.wePlusBotReceiver) ? globalThis.String(object.wePlusBotReceiver) : undefined,\n      wePlusBotVersion: isSet(object.wePlusBotVersion) ? globalThis.String(object.wePlusBotVersion) : undefined,\n      emailService: isSet(object.emailService) ? globalThis.String(object.emailService) : undefined,\n      emailUser: isSet(object.emailUser) ? globalThis.String(object.emailUser) : undefined,\n      emailPass: isSet(object.emailPass) ? globalThis.String(object.emailPass) : undefined,\n      emailTo: isSet(object.emailTo) ? globalThis.String(object.emailTo) : undefined,\n      pushMeKey: isSet(object.pushMeKey) ? globalThis.String(object.pushMeKey) : undefined,\n      pushMeUrl: isSet(object.pushMeUrl) ? globalThis.String(object.pushMeUrl) : undefined,\n      chronocatURL: isSet(object.chronocatURL) ? globalThis.String(object.chronocatURL) : undefined,\n      chronocatQQ: isSet(object.chronocatQQ) ? globalThis.String(object.chronocatQQ) : undefined,\n      chronocatToken: isSet(object.chronocatToken) ? globalThis.String(object.chronocatToken) : undefined,\n      webhookHeaders: isSet(object.webhookHeaders) ? globalThis.String(object.webhookHeaders) : undefined,\n      webhookBody: isSet(object.webhookBody) ? globalThis.String(object.webhookBody) : undefined,\n      webhookUrl: isSet(object.webhookUrl) ? globalThis.String(object.webhookUrl) : undefined,\n      webhookMethod: isSet(object.webhookMethod) ? globalThis.String(object.webhookMethod) : undefined,\n      webhookContentType: isSet(object.webhookContentType) ? globalThis.String(object.webhookContentType) : undefined,\n      larkKey: isSet(object.larkKey) ? globalThis.String(object.larkKey) : undefined,\n      larkSecret: isSet(object.larkSecret) ? globalThis.String(object.larkSecret) : undefined,\n      ntfyUrl: isSet(object.ntfyUrl) ? globalThis.String(object.ntfyUrl) : undefined,\n      ntfyTopic: isSet(object.ntfyTopic) ? globalThis.String(object.ntfyTopic) : undefined,\n      ntfyPriority: isSet(object.ntfyPriority) ? globalThis.String(object.ntfyPriority) : undefined,\n      ntfyToken: isSet(object.ntfyToken) ? globalThis.String(object.ntfyToken) : undefined,\n      ntfyUsername: isSet(object.ntfyUsername) ? globalThis.String(object.ntfyUsername) : undefined,\n      ntfyPassword: isSet(object.ntfyPassword) ? globalThis.String(object.ntfyPassword) : undefined,\n      ntfyActions: isSet(object.ntfyActions) ? globalThis.String(object.ntfyActions) : undefined,\n      wxPusherBotAppToken: isSet(object.wxPusherBotAppToken)\n        ? globalThis.String(object.wxPusherBotAppToken)\n        : undefined,\n      wxPusherBotTopicIds: isSet(object.wxPusherBotTopicIds)\n        ? globalThis.String(object.wxPusherBotTopicIds)\n        : undefined,\n      wxPusherBotUids: isSet(object.wxPusherBotUids) ? globalThis.String(object.wxPusherBotUids) : undefined,\n    };\n  },\n\n  toJSON(message: NotificationInfo): unknown {\n    const obj: any = {};\n    if (message.type !== 0) {\n      obj.type = notificationModeToJSON(message.type);\n    }\n    if (message.gotifyUrl !== undefined) {\n      obj.gotifyUrl = message.gotifyUrl;\n    }\n    if (message.gotifyToken !== undefined) {\n      obj.gotifyToken = message.gotifyToken;\n    }\n    if (message.gotifyPriority !== undefined) {\n      obj.gotifyPriority = Math.round(message.gotifyPriority);\n    }\n    if (message.goCqHttpBotUrl !== undefined) {\n      obj.goCqHttpBotUrl = message.goCqHttpBotUrl;\n    }\n    if (message.goCqHttpBotToken !== undefined) {\n      obj.goCqHttpBotToken = message.goCqHttpBotToken;\n    }\n    if (message.goCqHttpBotQq !== undefined) {\n      obj.goCqHttpBotQq = message.goCqHttpBotQq;\n    }\n    if (message.serverChanKey !== undefined) {\n      obj.serverChanKey = message.serverChanKey;\n    }\n    if (message.pushDeerKey !== undefined) {\n      obj.pushDeerKey = message.pushDeerKey;\n    }\n    if (message.pushDeerUrl !== undefined) {\n      obj.pushDeerUrl = message.pushDeerUrl;\n    }\n    if (message.synologyChatUrl !== undefined) {\n      obj.synologyChatUrl = message.synologyChatUrl;\n    }\n    if (message.barkPush !== undefined) {\n      obj.barkPush = message.barkPush;\n    }\n    if (message.barkIcon !== undefined) {\n      obj.barkIcon = message.barkIcon;\n    }\n    if (message.barkSound !== undefined) {\n      obj.barkSound = message.barkSound;\n    }\n    if (message.barkGroup !== undefined) {\n      obj.barkGroup = message.barkGroup;\n    }\n    if (message.barkLevel !== undefined) {\n      obj.barkLevel = message.barkLevel;\n    }\n    if (message.barkUrl !== undefined) {\n      obj.barkUrl = message.barkUrl;\n    }\n    if (message.barkArchive !== undefined) {\n      obj.barkArchive = message.barkArchive;\n    }\n    if (message.telegramBotToken !== undefined) {\n      obj.telegramBotToken = message.telegramBotToken;\n    }\n    if (message.telegramBotUserId !== undefined) {\n      obj.telegramBotUserId = message.telegramBotUserId;\n    }\n    if (message.telegramBotProxyHost !== undefined) {\n      obj.telegramBotProxyHost = message.telegramBotProxyHost;\n    }\n    if (message.telegramBotProxyPort !== undefined) {\n      obj.telegramBotProxyPort = message.telegramBotProxyPort;\n    }\n    if (message.telegramBotProxyAuth !== undefined) {\n      obj.telegramBotProxyAuth = message.telegramBotProxyAuth;\n    }\n    if (message.telegramBotApiHost !== undefined) {\n      obj.telegramBotApiHost = message.telegramBotApiHost;\n    }\n    if (message.dingtalkBotToken !== undefined) {\n      obj.dingtalkBotToken = message.dingtalkBotToken;\n    }\n    if (message.dingtalkBotSecret !== undefined) {\n      obj.dingtalkBotSecret = message.dingtalkBotSecret;\n    }\n    if (message.weWorkBotKey !== undefined) {\n      obj.weWorkBotKey = message.weWorkBotKey;\n    }\n    if (message.weWorkOrigin !== undefined) {\n      obj.weWorkOrigin = message.weWorkOrigin;\n    }\n    if (message.weWorkAppKey !== undefined) {\n      obj.weWorkAppKey = message.weWorkAppKey;\n    }\n    if (message.aibotkKey !== undefined) {\n      obj.aibotkKey = message.aibotkKey;\n    }\n    if (message.aibotkType !== undefined) {\n      obj.aibotkType = message.aibotkType;\n    }\n    if (message.aibotkName !== undefined) {\n      obj.aibotkName = message.aibotkName;\n    }\n    if (message.iGotPushKey !== undefined) {\n      obj.iGotPushKey = message.iGotPushKey;\n    }\n    if (message.pushPlusToken !== undefined) {\n      obj.pushPlusToken = message.pushPlusToken;\n    }\n    if (message.pushPlusUser !== undefined) {\n      obj.pushPlusUser = message.pushPlusUser;\n    }\n    if (message.pushPlusTemplate !== undefined) {\n      obj.pushPlusTemplate = message.pushPlusTemplate;\n    }\n    if (message.pushplusChannel !== undefined) {\n      obj.pushplusChannel = message.pushplusChannel;\n    }\n    if (message.pushplusWebhook !== undefined) {\n      obj.pushplusWebhook = message.pushplusWebhook;\n    }\n    if (message.pushplusCallbackUrl !== undefined) {\n      obj.pushplusCallbackUrl = message.pushplusCallbackUrl;\n    }\n    if (message.pushplusTo !== undefined) {\n      obj.pushplusTo = message.pushplusTo;\n    }\n    if (message.wePlusBotToken !== undefined) {\n      obj.wePlusBotToken = message.wePlusBotToken;\n    }\n    if (message.wePlusBotReceiver !== undefined) {\n      obj.wePlusBotReceiver = message.wePlusBotReceiver;\n    }\n    if (message.wePlusBotVersion !== undefined) {\n      obj.wePlusBotVersion = message.wePlusBotVersion;\n    }\n    if (message.emailService !== undefined) {\n      obj.emailService = message.emailService;\n    }\n    if (message.emailUser !== undefined) {\n      obj.emailUser = message.emailUser;\n    }\n    if (message.emailPass !== undefined) {\n      obj.emailPass = message.emailPass;\n    }\n    if (message.emailTo !== undefined) {\n      obj.emailTo = message.emailTo;\n    }\n    if (message.pushMeKey !== undefined) {\n      obj.pushMeKey = message.pushMeKey;\n    }\n    if (message.pushMeUrl !== undefined) {\n      obj.pushMeUrl = message.pushMeUrl;\n    }\n    if (message.chronocatURL !== undefined) {\n      obj.chronocatURL = message.chronocatURL;\n    }\n    if (message.chronocatQQ !== undefined) {\n      obj.chronocatQQ = message.chronocatQQ;\n    }\n    if (message.chronocatToken !== undefined) {\n      obj.chronocatToken = message.chronocatToken;\n    }\n    if (message.webhookHeaders !== undefined) {\n      obj.webhookHeaders = message.webhookHeaders;\n    }\n    if (message.webhookBody !== undefined) {\n      obj.webhookBody = message.webhookBody;\n    }\n    if (message.webhookUrl !== undefined) {\n      obj.webhookUrl = message.webhookUrl;\n    }\n    if (message.webhookMethod !== undefined) {\n      obj.webhookMethod = message.webhookMethod;\n    }\n    if (message.webhookContentType !== undefined) {\n      obj.webhookContentType = message.webhookContentType;\n    }\n    if (message.larkKey !== undefined) {\n      obj.larkKey = message.larkKey;\n    }\n    if (message.larkSecret !== undefined) {\n      obj.larkSecret = message.larkSecret;\n    }\n    if (message.ntfyUrl !== undefined) {\n      obj.ntfyUrl = message.ntfyUrl;\n    }\n    if (message.ntfyTopic !== undefined) {\n      obj.ntfyTopic = message.ntfyTopic;\n    }\n    if (message.ntfyPriority !== undefined) {\n      obj.ntfyPriority = message.ntfyPriority;\n    }\n    if (message.ntfyToken !== undefined) {\n      obj.ntfyToken = message.ntfyToken;\n    }\n    if (message.ntfyUsername !== undefined) {\n      obj.ntfyUsername = message.ntfyUsername;\n    }\n    if (message.ntfyPassword !== undefined) {\n      obj.ntfyPassword = message.ntfyPassword;\n    }\n    if (message.ntfyActions !== undefined) {\n      obj.ntfyActions = message.ntfyActions;\n    }\n    if (message.wxPusherBotAppToken !== undefined) {\n      obj.wxPusherBotAppToken = message.wxPusherBotAppToken;\n    }\n    if (message.wxPusherBotTopicIds !== undefined) {\n      obj.wxPusherBotTopicIds = message.wxPusherBotTopicIds;\n    }\n    if (message.wxPusherBotUids !== undefined) {\n      obj.wxPusherBotUids = message.wxPusherBotUids;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<NotificationInfo>, I>>(base?: I): NotificationInfo {\n    return NotificationInfo.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<NotificationInfo>, I>>(object: I): NotificationInfo {\n    const message = createBaseNotificationInfo();\n    message.type = object.type ?? 0;\n    message.gotifyUrl = object.gotifyUrl ?? undefined;\n    message.gotifyToken = object.gotifyToken ?? undefined;\n    message.gotifyPriority = object.gotifyPriority ?? undefined;\n    message.goCqHttpBotUrl = object.goCqHttpBotUrl ?? undefined;\n    message.goCqHttpBotToken = object.goCqHttpBotToken ?? undefined;\n    message.goCqHttpBotQq = object.goCqHttpBotQq ?? undefined;\n    message.serverChanKey = object.serverChanKey ?? undefined;\n    message.pushDeerKey = object.pushDeerKey ?? undefined;\n    message.pushDeerUrl = object.pushDeerUrl ?? undefined;\n    message.synologyChatUrl = object.synologyChatUrl ?? undefined;\n    message.barkPush = object.barkPush ?? undefined;\n    message.barkIcon = object.barkIcon ?? undefined;\n    message.barkSound = object.barkSound ?? undefined;\n    message.barkGroup = object.barkGroup ?? undefined;\n    message.barkLevel = object.barkLevel ?? undefined;\n    message.barkUrl = object.barkUrl ?? undefined;\n    message.barkArchive = object.barkArchive ?? undefined;\n    message.telegramBotToken = object.telegramBotToken ?? undefined;\n    message.telegramBotUserId = object.telegramBotUserId ?? undefined;\n    message.telegramBotProxyHost = object.telegramBotProxyHost ?? undefined;\n    message.telegramBotProxyPort = object.telegramBotProxyPort ?? undefined;\n    message.telegramBotProxyAuth = object.telegramBotProxyAuth ?? undefined;\n    message.telegramBotApiHost = object.telegramBotApiHost ?? undefined;\n    message.dingtalkBotToken = object.dingtalkBotToken ?? undefined;\n    message.dingtalkBotSecret = object.dingtalkBotSecret ?? undefined;\n    message.weWorkBotKey = object.weWorkBotKey ?? undefined;\n    message.weWorkOrigin = object.weWorkOrigin ?? undefined;\n    message.weWorkAppKey = object.weWorkAppKey ?? undefined;\n    message.aibotkKey = object.aibotkKey ?? undefined;\n    message.aibotkType = object.aibotkType ?? undefined;\n    message.aibotkName = object.aibotkName ?? undefined;\n    message.iGotPushKey = object.iGotPushKey ?? undefined;\n    message.pushPlusToken = object.pushPlusToken ?? undefined;\n    message.pushPlusUser = object.pushPlusUser ?? undefined;\n    message.pushPlusTemplate = object.pushPlusTemplate ?? undefined;\n    message.pushplusChannel = object.pushplusChannel ?? undefined;\n    message.pushplusWebhook = object.pushplusWebhook ?? undefined;\n    message.pushplusCallbackUrl = object.pushplusCallbackUrl ?? undefined;\n    message.pushplusTo = object.pushplusTo ?? undefined;\n    message.wePlusBotToken = object.wePlusBotToken ?? undefined;\n    message.wePlusBotReceiver = object.wePlusBotReceiver ?? undefined;\n    message.wePlusBotVersion = object.wePlusBotVersion ?? undefined;\n    message.emailService = object.emailService ?? undefined;\n    message.emailUser = object.emailUser ?? undefined;\n    message.emailPass = object.emailPass ?? undefined;\n    message.emailTo = object.emailTo ?? undefined;\n    message.pushMeKey = object.pushMeKey ?? undefined;\n    message.pushMeUrl = object.pushMeUrl ?? undefined;\n    message.chronocatURL = object.chronocatURL ?? undefined;\n    message.chronocatQQ = object.chronocatQQ ?? undefined;\n    message.chronocatToken = object.chronocatToken ?? undefined;\n    message.webhookHeaders = object.webhookHeaders ?? undefined;\n    message.webhookBody = object.webhookBody ?? undefined;\n    message.webhookUrl = object.webhookUrl ?? undefined;\n    message.webhookMethod = object.webhookMethod ?? undefined;\n    message.webhookContentType = object.webhookContentType ?? undefined;\n    message.larkKey = object.larkKey ?? undefined;\n    message.larkSecret = object.larkSecret ?? undefined;\n    message.ntfyUrl = object.ntfyUrl ?? undefined;\n    message.ntfyTopic = object.ntfyTopic ?? undefined;\n    message.ntfyPriority = object.ntfyPriority ?? undefined;\n    message.ntfyToken = object.ntfyToken ?? undefined;\n    message.ntfyUsername = object.ntfyUsername ?? undefined;\n    message.ntfyPassword = object.ntfyPassword ?? undefined;\n    message.ntfyActions = object.ntfyActions ?? undefined;\n    message.wxPusherBotAppToken = object.wxPusherBotAppToken ?? undefined;\n    message.wxPusherBotTopicIds = object.wxPusherBotTopicIds ?? undefined;\n    message.wxPusherBotUids = object.wxPusherBotUids ?? undefined;\n    return message;\n  },\n};\n\nfunction createBaseSystemNotifyRequest(): SystemNotifyRequest {\n  return { title: \"\", content: \"\", notificationInfo: undefined };\n}\n\nexport const SystemNotifyRequest: MessageFns<SystemNotifyRequest> = {\n  encode(message: SystemNotifyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.title !== \"\") {\n      writer.uint32(10).string(message.title);\n    }\n    if (message.content !== \"\") {\n      writer.uint32(18).string(message.content);\n    }\n    if (message.notificationInfo !== undefined) {\n      NotificationInfo.encode(message.notificationInfo, writer.uint32(26).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): SystemNotifyRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseSystemNotifyRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.title = reader.string();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.content = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.notificationInfo = NotificationInfo.decode(reader, reader.uint32());\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): SystemNotifyRequest {\n    return {\n      title: isSet(object.title) ? globalThis.String(object.title) : \"\",\n      content: isSet(object.content) ? globalThis.String(object.content) : \"\",\n      notificationInfo: isSet(object.notificationInfo) ? NotificationInfo.fromJSON(object.notificationInfo) : undefined,\n    };\n  },\n\n  toJSON(message: SystemNotifyRequest): unknown {\n    const obj: any = {};\n    if (message.title !== \"\") {\n      obj.title = message.title;\n    }\n    if (message.content !== \"\") {\n      obj.content = message.content;\n    }\n    if (message.notificationInfo !== undefined) {\n      obj.notificationInfo = NotificationInfo.toJSON(message.notificationInfo);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<SystemNotifyRequest>, I>>(base?: I): SystemNotifyRequest {\n    return SystemNotifyRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<SystemNotifyRequest>, I>>(object: I): SystemNotifyRequest {\n    const message = createBaseSystemNotifyRequest();\n    message.title = object.title ?? \"\";\n    message.content = object.content ?? \"\";\n    message.notificationInfo = (object.notificationInfo !== undefined && object.notificationInfo !== null)\n      ? NotificationInfo.fromPartial(object.notificationInfo)\n      : undefined;\n    return message;\n  },\n};\n\nexport type ApiService = typeof ApiService;\nexport const ApiService = {\n  getEnvs: {\n    path: \"/com.ql.api.Api/GetEnvs\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: GetEnvsRequest) => Buffer.from(GetEnvsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => GetEnvsRequest.decode(value),\n    responseSerialize: (value: EnvsResponse) => Buffer.from(EnvsResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => EnvsResponse.decode(value),\n  },\n  createEnv: {\n    path: \"/com.ql.api.Api/CreateEnv\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: CreateEnvRequest) => Buffer.from(CreateEnvRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => CreateEnvRequest.decode(value),\n    responseSerialize: (value: EnvsResponse) => Buffer.from(EnvsResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => EnvsResponse.decode(value),\n  },\n  updateEnv: {\n    path: \"/com.ql.api.Api/UpdateEnv\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: UpdateEnvRequest) => Buffer.from(UpdateEnvRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => UpdateEnvRequest.decode(value),\n    responseSerialize: (value: EnvResponse) => Buffer.from(EnvResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => EnvResponse.decode(value),\n  },\n  deleteEnvs: {\n    path: \"/com.ql.api.Api/DeleteEnvs\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: DeleteEnvsRequest) => Buffer.from(DeleteEnvsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => DeleteEnvsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  moveEnv: {\n    path: \"/com.ql.api.Api/MoveEnv\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: MoveEnvRequest) => Buffer.from(MoveEnvRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => MoveEnvRequest.decode(value),\n    responseSerialize: (value: EnvResponse) => Buffer.from(EnvResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => EnvResponse.decode(value),\n  },\n  disableEnvs: {\n    path: \"/com.ql.api.Api/DisableEnvs\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: DisableEnvsRequest) => Buffer.from(DisableEnvsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => DisableEnvsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  enableEnvs: {\n    path: \"/com.ql.api.Api/EnableEnvs\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: EnableEnvsRequest) => Buffer.from(EnableEnvsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => EnableEnvsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  updateEnvNames: {\n    path: \"/com.ql.api.Api/UpdateEnvNames\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: UpdateEnvNamesRequest) => Buffer.from(UpdateEnvNamesRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => UpdateEnvNamesRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  getEnvById: {\n    path: \"/com.ql.api.Api/GetEnvById\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: GetEnvByIdRequest) => Buffer.from(GetEnvByIdRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => GetEnvByIdRequest.decode(value),\n    responseSerialize: (value: EnvResponse) => Buffer.from(EnvResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => EnvResponse.decode(value),\n  },\n  systemNotify: {\n    path: \"/com.ql.api.Api/SystemNotify\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: SystemNotifyRequest) => Buffer.from(SystemNotifyRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => SystemNotifyRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  getCronDetail: {\n    path: \"/com.ql.api.Api/GetCronDetail\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: CronDetailRequest) => Buffer.from(CronDetailRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => CronDetailRequest.decode(value),\n    responseSerialize: (value: CronDetailResponse) => Buffer.from(CronDetailResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => CronDetailResponse.decode(value),\n  },\n  createCron: {\n    path: \"/com.ql.api.Api/CreateCron\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: CreateCronRequest) => Buffer.from(CreateCronRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => CreateCronRequest.decode(value),\n    responseSerialize: (value: CronResponse) => Buffer.from(CronResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => CronResponse.decode(value),\n  },\n  updateCron: {\n    path: \"/com.ql.api.Api/UpdateCron\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: UpdateCronRequest) => Buffer.from(UpdateCronRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => UpdateCronRequest.decode(value),\n    responseSerialize: (value: CronResponse) => Buffer.from(CronResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => CronResponse.decode(value),\n  },\n  deleteCrons: {\n    path: \"/com.ql.api.Api/DeleteCrons\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: DeleteCronsRequest) => Buffer.from(DeleteCronsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => DeleteCronsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  getCrons: {\n    path: \"/com.ql.api.Api/GetCrons\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: GetCronsRequest) => Buffer.from(GetCronsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => GetCronsRequest.decode(value),\n    responseSerialize: (value: CronsResponse) => Buffer.from(CronsResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => CronsResponse.decode(value),\n  },\n  getCronById: {\n    path: \"/com.ql.api.Api/GetCronById\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: GetCronByIdRequest) => Buffer.from(GetCronByIdRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => GetCronByIdRequest.decode(value),\n    responseSerialize: (value: CronResponse) => Buffer.from(CronResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => CronResponse.decode(value),\n  },\n  enableCrons: {\n    path: \"/com.ql.api.Api/EnableCrons\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: EnableCronsRequest) => Buffer.from(EnableCronsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => EnableCronsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  disableCrons: {\n    path: \"/com.ql.api.Api/DisableCrons\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: DisableCronsRequest) => Buffer.from(DisableCronsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => DisableCronsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n  runCrons: {\n    path: \"/com.ql.api.Api/RunCrons\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: RunCronsRequest) => Buffer.from(RunCronsRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => RunCronsRequest.decode(value),\n    responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => Response.decode(value),\n  },\n} as const;\n\nexport interface ApiServer extends UntypedServiceImplementation {\n  getEnvs: handleUnaryCall<GetEnvsRequest, EnvsResponse>;\n  createEnv: handleUnaryCall<CreateEnvRequest, EnvsResponse>;\n  updateEnv: handleUnaryCall<UpdateEnvRequest, EnvResponse>;\n  deleteEnvs: handleUnaryCall<DeleteEnvsRequest, Response>;\n  moveEnv: handleUnaryCall<MoveEnvRequest, EnvResponse>;\n  disableEnvs: handleUnaryCall<DisableEnvsRequest, Response>;\n  enableEnvs: handleUnaryCall<EnableEnvsRequest, Response>;\n  updateEnvNames: handleUnaryCall<UpdateEnvNamesRequest, Response>;\n  getEnvById: handleUnaryCall<GetEnvByIdRequest, EnvResponse>;\n  systemNotify: handleUnaryCall<SystemNotifyRequest, Response>;\n  getCronDetail: handleUnaryCall<CronDetailRequest, CronDetailResponse>;\n  createCron: handleUnaryCall<CreateCronRequest, CronResponse>;\n  updateCron: handleUnaryCall<UpdateCronRequest, CronResponse>;\n  deleteCrons: handleUnaryCall<DeleteCronsRequest, Response>;\n  getCrons: handleUnaryCall<GetCronsRequest, CronsResponse>;\n  getCronById: handleUnaryCall<GetCronByIdRequest, CronResponse>;\n  enableCrons: handleUnaryCall<EnableCronsRequest, Response>;\n  disableCrons: handleUnaryCall<DisableCronsRequest, Response>;\n  runCrons: handleUnaryCall<RunCronsRequest, Response>;\n}\n\nexport interface ApiClient extends Client {\n  getEnvs(\n    request: GetEnvsRequest,\n    callback: (error: ServiceError | null, response: EnvsResponse) => void,\n  ): ClientUnaryCall;\n  getEnvs(\n    request: GetEnvsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: EnvsResponse) => void,\n  ): ClientUnaryCall;\n  getEnvs(\n    request: GetEnvsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: EnvsResponse) => void,\n  ): ClientUnaryCall;\n  createEnv(\n    request: CreateEnvRequest,\n    callback: (error: ServiceError | null, response: EnvsResponse) => void,\n  ): ClientUnaryCall;\n  createEnv(\n    request: CreateEnvRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: EnvsResponse) => void,\n  ): ClientUnaryCall;\n  createEnv(\n    request: CreateEnvRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: EnvsResponse) => void,\n  ): ClientUnaryCall;\n  updateEnv(\n    request: UpdateEnvRequest,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  updateEnv(\n    request: UpdateEnvRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  updateEnv(\n    request: UpdateEnvRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  deleteEnvs(\n    request: DeleteEnvsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  deleteEnvs(\n    request: DeleteEnvsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  deleteEnvs(\n    request: DeleteEnvsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  moveEnv(\n    request: MoveEnvRequest,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  moveEnv(\n    request: MoveEnvRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  moveEnv(\n    request: MoveEnvRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  disableEnvs(\n    request: DisableEnvsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  disableEnvs(\n    request: DisableEnvsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  disableEnvs(\n    request: DisableEnvsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  enableEnvs(\n    request: EnableEnvsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  enableEnvs(\n    request: EnableEnvsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  enableEnvs(\n    request: EnableEnvsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  updateEnvNames(\n    request: UpdateEnvNamesRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  updateEnvNames(\n    request: UpdateEnvNamesRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  updateEnvNames(\n    request: UpdateEnvNamesRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  getEnvById(\n    request: GetEnvByIdRequest,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  getEnvById(\n    request: GetEnvByIdRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  getEnvById(\n    request: GetEnvByIdRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: EnvResponse) => void,\n  ): ClientUnaryCall;\n  systemNotify(\n    request: SystemNotifyRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  systemNotify(\n    request: SystemNotifyRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  systemNotify(\n    request: SystemNotifyRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  getCronDetail(\n    request: CronDetailRequest,\n    callback: (error: ServiceError | null, response: CronDetailResponse) => void,\n  ): ClientUnaryCall;\n  getCronDetail(\n    request: CronDetailRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: CronDetailResponse) => void,\n  ): ClientUnaryCall;\n  getCronDetail(\n    request: CronDetailRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: CronDetailResponse) => void,\n  ): ClientUnaryCall;\n  createCron(\n    request: CreateCronRequest,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  createCron(\n    request: CreateCronRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  createCron(\n    request: CreateCronRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  updateCron(\n    request: UpdateCronRequest,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  updateCron(\n    request: UpdateCronRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  updateCron(\n    request: UpdateCronRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  deleteCrons(\n    request: DeleteCronsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  deleteCrons(\n    request: DeleteCronsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  deleteCrons(\n    request: DeleteCronsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  getCrons(\n    request: GetCronsRequest,\n    callback: (error: ServiceError | null, response: CronsResponse) => void,\n  ): ClientUnaryCall;\n  getCrons(\n    request: GetCronsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: CronsResponse) => void,\n  ): ClientUnaryCall;\n  getCrons(\n    request: GetCronsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: CronsResponse) => void,\n  ): ClientUnaryCall;\n  getCronById(\n    request: GetCronByIdRequest,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  getCronById(\n    request: GetCronByIdRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  getCronById(\n    request: GetCronByIdRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: CronResponse) => void,\n  ): ClientUnaryCall;\n  enableCrons(\n    request: EnableCronsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  enableCrons(\n    request: EnableCronsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  enableCrons(\n    request: EnableCronsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  disableCrons(\n    request: DisableCronsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  disableCrons(\n    request: DisableCronsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  disableCrons(\n    request: DisableCronsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  runCrons(\n    request: RunCronsRequest,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  runCrons(\n    request: RunCronsRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n  runCrons(\n    request: RunCronsRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: Response) => void,\n  ): ClientUnaryCall;\n}\n\nexport const ApiClient = makeGenericClientConstructor(ApiService, \"com.ql.api.Api\") as unknown as {\n  new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): ApiClient;\n  service: typeof ApiService;\n  serviceName: string;\n};\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction longToNumber(int64: { toString(): string }): number {\n  const num = globalThis.Number(int64.toString());\n  if (num > globalThis.Number.MAX_SAFE_INTEGER) {\n    throw new globalThis.Error(\"Value is larger than Number.MAX_SAFE_INTEGER\");\n  }\n  if (num < globalThis.Number.MIN_SAFE_INTEGER) {\n    throw new globalThis.Error(\"Value is smaller than Number.MIN_SAFE_INTEGER\");\n  }\n  return num;\n}\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "back/protos/cron.proto",
    "content": "syntax = \"proto3\";\n\npackage com.ql.cron;\n\nservice Cron {\n  rpc addCron(AddCronRequest) returns (AddCronResponse);\n  rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);\n}\n\nmessage ISchedule { string schedule = 1; }\n\nmessage ICron {\n  string id = 1;\n  string schedule = 2;\n  string command = 3;\n  repeated ISchedule extra_schedules = 4;\n  string name = 5;\n}\n\nmessage AddCronRequest { repeated ICron crons = 1; }\n\nmessage AddCronResponse {}\n\nmessage DeleteCronRequest { repeated string ids = 1; }\n\nmessage DeleteCronResponse {}\n"
  },
  {
    "path": "back/protos/cron.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.6.1\n//   protoc               v3.21.12\n// source: back/protos/cron.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\nimport {\n  type CallOptions,\n  ChannelCredentials,\n  Client,\n  type ClientOptions,\n  type ClientUnaryCall,\n  type handleUnaryCall,\n  makeGenericClientConstructor,\n  Metadata,\n  type ServiceError,\n  type UntypedServiceImplementation,\n} from \"@grpc/grpc-js\";\n\nexport const protobufPackage = \"com.ql.cron\";\n\nexport interface ISchedule {\n  schedule: string;\n}\n\nexport interface ICron {\n  id: string;\n  schedule: string;\n  command: string;\n  extra_schedules: ISchedule[];\n  name: string;\n}\n\nexport interface AddCronRequest {\n  crons: ICron[];\n}\n\nexport interface AddCronResponse {\n}\n\nexport interface DeleteCronRequest {\n  ids: string[];\n}\n\nexport interface DeleteCronResponse {\n}\n\nfunction createBaseISchedule(): ISchedule {\n  return { schedule: \"\" };\n}\n\nexport const ISchedule: MessageFns<ISchedule> = {\n  encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.schedule !== \"\") {\n      writer.uint32(10).string(message.schedule);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseISchedule();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.schedule = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ISchedule {\n    return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : \"\" };\n  },\n\n  toJSON(message: ISchedule): unknown {\n    const obj: any = {};\n    if (message.schedule !== \"\") {\n      obj.schedule = message.schedule;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {\n    return ISchedule.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {\n    const message = createBaseISchedule();\n    message.schedule = object.schedule ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseICron(): ICron {\n  return { id: \"\", schedule: \"\", command: \"\", extra_schedules: [], name: \"\" };\n}\n\nexport const ICron: MessageFns<ICron> = {\n  encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== \"\") {\n      writer.uint32(10).string(message.id);\n    }\n    if (message.schedule !== \"\") {\n      writer.uint32(18).string(message.schedule);\n    }\n    if (message.command !== \"\") {\n      writer.uint32(26).string(message.command);\n    }\n    for (const v of message.extra_schedules) {\n      ISchedule.encode(v!, writer.uint32(34).fork()).join();\n    }\n    if (message.name !== \"\") {\n      writer.uint32(42).string(message.name);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ICron {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseICron();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.id = reader.string();\n          continue;\n        }\n        case 2: {\n          if (tag !== 18) {\n            break;\n          }\n\n          message.schedule = reader.string();\n          continue;\n        }\n        case 3: {\n          if (tag !== 26) {\n            break;\n          }\n\n          message.command = reader.string();\n          continue;\n        }\n        case 4: {\n          if (tag !== 34) {\n            break;\n          }\n\n          message.extra_schedules.push(ISchedule.decode(reader, reader.uint32()));\n          continue;\n        }\n        case 5: {\n          if (tag !== 42) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ICron {\n    return {\n      id: isSet(object.id) ? globalThis.String(object.id) : \"\",\n      schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : \"\",\n      command: isSet(object.command) ? globalThis.String(object.command) : \"\",\n      extra_schedules: globalThis.Array.isArray(object?.extra_schedules)\n        ? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))\n        : [],\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n    };\n  },\n\n  toJSON(message: ICron): unknown {\n    const obj: any = {};\n    if (message.id !== \"\") {\n      obj.id = message.id;\n    }\n    if (message.schedule !== \"\") {\n      obj.schedule = message.schedule;\n    }\n    if (message.command !== \"\") {\n      obj.command = message.command;\n    }\n    if (message.extra_schedules?.length) {\n      obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));\n    }\n    if (message.name !== \"\") {\n      obj.name = message.name;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {\n    return ICron.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {\n    const message = createBaseICron();\n    message.id = object.id ?? \"\";\n    message.schedule = object.schedule ?? \"\";\n    message.command = object.command ?? \"\";\n    message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];\n    message.name = object.name ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseAddCronRequest(): AddCronRequest {\n  return { crons: [] };\n}\n\nexport const AddCronRequest: MessageFns<AddCronRequest> = {\n  encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.crons) {\n      ICron.encode(v!, writer.uint32(10).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseAddCronRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.crons.push(ICron.decode(reader, reader.uint32()));\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): AddCronRequest {\n    return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };\n  },\n\n  toJSON(message: AddCronRequest): unknown {\n    const obj: any = {};\n    if (message.crons?.length) {\n      obj.crons = message.crons.map((e) => ICron.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {\n    return AddCronRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {\n    const message = createBaseAddCronRequest();\n    message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseAddCronResponse(): AddCronResponse {\n  return {};\n}\n\nexport const AddCronResponse: MessageFns<AddCronResponse> = {\n  encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseAddCronResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(_: any): AddCronResponse {\n    return {};\n  },\n\n  toJSON(_: AddCronResponse): unknown {\n    const obj: any = {};\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {\n    return AddCronResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {\n    const message = createBaseAddCronResponse();\n    return message;\n  },\n};\n\nfunction createBaseDeleteCronRequest(): DeleteCronRequest {\n  return { ids: [] };\n}\n\nexport const DeleteCronRequest: MessageFns<DeleteCronRequest> = {\n  encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.ids) {\n      writer.uint32(10).string(v!);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDeleteCronRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.ids.push(reader.string());\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DeleteCronRequest {\n    return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] };\n  },\n\n  toJSON(message: DeleteCronRequest): unknown {\n    const obj: any = {};\n    if (message.ids?.length) {\n      obj.ids = message.ids;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {\n    return DeleteCronRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {\n    const message = createBaseDeleteCronRequest();\n    message.ids = object.ids?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseDeleteCronResponse(): DeleteCronResponse {\n  return {};\n}\n\nexport const DeleteCronResponse: MessageFns<DeleteCronResponse> = {\n  encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDeleteCronResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(_: any): DeleteCronResponse {\n    return {};\n  },\n\n  toJSON(_: DeleteCronResponse): unknown {\n    const obj: any = {};\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {\n    return DeleteCronResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {\n    const message = createBaseDeleteCronResponse();\n    return message;\n  },\n};\n\nexport type CronService = typeof CronService;\nexport const CronService = {\n  addCron: {\n    path: \"/com.ql.cron.Cron/addCron\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),\n    responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),\n  },\n  delCron: {\n    path: \"/com.ql.cron.Cron/delCron\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),\n    responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),\n  },\n} as const;\n\nexport interface CronServer extends UntypedServiceImplementation {\n  addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;\n  delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;\n}\n\nexport interface CronClient extends Client {\n  addCron(\n    request: AddCronRequest,\n    callback: (error: ServiceError | null, response: AddCronResponse) => void,\n  ): ClientUnaryCall;\n  addCron(\n    request: AddCronRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: AddCronResponse) => void,\n  ): ClientUnaryCall;\n  addCron(\n    request: AddCronRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: AddCronResponse) => void,\n  ): ClientUnaryCall;\n  delCron(\n    request: DeleteCronRequest,\n    callback: (error: ServiceError | null, response: DeleteCronResponse) => void,\n  ): ClientUnaryCall;\n  delCron(\n    request: DeleteCronRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: DeleteCronResponse) => void,\n  ): ClientUnaryCall;\n  delCron(\n    request: DeleteCronRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: DeleteCronResponse) => void,\n  ): ClientUnaryCall;\n}\n\nexport const CronClient = makeGenericClientConstructor(CronService, \"com.ql.cron.Cron\") as unknown as {\n  new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;\n  service: typeof CronService;\n  serviceName: string;\n};\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "back/protos/health.proto",
    "content": "syntax = \"proto3\";\n\npackage com.ql.health;\n\nmessage HealthCheckRequest {\n  string service = 1;\n}\n\nmessage HealthCheckResponse {\n  enum ServingStatus {\n    UNKNOWN = 0;\n    SERVING = 1;\n    NOT_SERVING = 2;\n    SERVICE_UNKNOWN = 3;\n  }\n  ServingStatus status = 1;\n}\n\nservice Health {\n  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);\n  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);\n}\n"
  },
  {
    "path": "back/protos/health.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.6.1\n//   protoc               v3.21.12\n// source: back/protos/health.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\nimport {\n  type CallOptions,\n  ChannelCredentials,\n  Client,\n  type ClientOptions,\n  type ClientReadableStream,\n  type ClientUnaryCall,\n  type handleServerStreamingCall,\n  type handleUnaryCall,\n  makeGenericClientConstructor,\n  Metadata,\n  type ServiceError,\n  type UntypedServiceImplementation,\n} from \"@grpc/grpc-js\";\n\nexport const protobufPackage = \"com.ql.health\";\n\nexport interface HealthCheckRequest {\n  service: string;\n}\n\nexport interface HealthCheckResponse {\n  status: HealthCheckResponse_ServingStatus;\n}\n\nexport enum HealthCheckResponse_ServingStatus {\n  UNKNOWN = 0,\n  SERVING = 1,\n  NOT_SERVING = 2,\n  SERVICE_UNKNOWN = 3,\n  UNRECOGNIZED = -1,\n}\n\nexport function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {\n  switch (object) {\n    case 0:\n    case \"UNKNOWN\":\n      return HealthCheckResponse_ServingStatus.UNKNOWN;\n    case 1:\n    case \"SERVING\":\n      return HealthCheckResponse_ServingStatus.SERVING;\n    case 2:\n    case \"NOT_SERVING\":\n      return HealthCheckResponse_ServingStatus.NOT_SERVING;\n    case 3:\n    case \"SERVICE_UNKNOWN\":\n      return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return HealthCheckResponse_ServingStatus.UNRECOGNIZED;\n  }\n}\n\nexport function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {\n  switch (object) {\n    case HealthCheckResponse_ServingStatus.UNKNOWN:\n      return \"UNKNOWN\";\n    case HealthCheckResponse_ServingStatus.SERVING:\n      return \"SERVING\";\n    case HealthCheckResponse_ServingStatus.NOT_SERVING:\n      return \"NOT_SERVING\";\n    case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:\n      return \"SERVICE_UNKNOWN\";\n    case HealthCheckResponse_ServingStatus.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nfunction createBaseHealthCheckRequest(): HealthCheckRequest {\n  return { service: \"\" };\n}\n\nexport const HealthCheckRequest: MessageFns<HealthCheckRequest> = {\n  encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.service !== \"\") {\n      writer.uint32(10).string(message.service);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseHealthCheckRequest();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 10) {\n            break;\n          }\n\n          message.service = reader.string();\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): HealthCheckRequest {\n    return { service: isSet(object.service) ? globalThis.String(object.service) : \"\" };\n  },\n\n  toJSON(message: HealthCheckRequest): unknown {\n    const obj: any = {};\n    if (message.service !== \"\") {\n      obj.service = message.service;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {\n    return HealthCheckRequest.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {\n    const message = createBaseHealthCheckRequest();\n    message.service = object.service ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseHealthCheckResponse(): HealthCheckResponse {\n  return { status: 0 };\n}\n\nexport const HealthCheckResponse: MessageFns<HealthCheckResponse> = {\n  encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.status !== 0) {\n      writer.uint32(8).int32(message.status);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseHealthCheckResponse();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1: {\n          if (tag !== 8) {\n            break;\n          }\n\n          message.status = reader.int32() as any;\n          continue;\n        }\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): HealthCheckResponse {\n    return { status: isSet(object.status) ? healthCheckResponse_ServingStatusFromJSON(object.status) : 0 };\n  },\n\n  toJSON(message: HealthCheckResponse): unknown {\n    const obj: any = {};\n    if (message.status !== 0) {\n      obj.status = healthCheckResponse_ServingStatusToJSON(message.status);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {\n    return HealthCheckResponse.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {\n    const message = createBaseHealthCheckResponse();\n    message.status = object.status ?? 0;\n    return message;\n  },\n};\n\nexport type HealthService = typeof HealthService;\nexport const HealthService = {\n  check: {\n    path: \"/com.ql.health.Health/Check\",\n    requestStream: false,\n    responseStream: false,\n    requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),\n    responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),\n  },\n  watch: {\n    path: \"/com.ql.health.Health/Watch\",\n    requestStream: false,\n    responseStream: true,\n    requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),\n    requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),\n    responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),\n    responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),\n  },\n} as const;\n\nexport interface HealthServer extends UntypedServiceImplementation {\n  check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;\n  watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;\n}\n\nexport interface HealthClient extends Client {\n  check(\n    request: HealthCheckRequest,\n    callback: (error: ServiceError | null, response: HealthCheckResponse) => void,\n  ): ClientUnaryCall;\n  check(\n    request: HealthCheckRequest,\n    metadata: Metadata,\n    callback: (error: ServiceError | null, response: HealthCheckResponse) => void,\n  ): ClientUnaryCall;\n  check(\n    request: HealthCheckRequest,\n    metadata: Metadata,\n    options: Partial<CallOptions>,\n    callback: (error: ServiceError | null, response: HealthCheckResponse) => void,\n  ): ClientUnaryCall;\n  watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;\n  watch(\n    request: HealthCheckRequest,\n    metadata?: Metadata,\n    options?: Partial<CallOptions>,\n  ): ClientReadableStream<HealthCheckResponse>;\n}\n\nexport const HealthClient = makeGenericClientConstructor(HealthService, \"com.ql.health.Health\") as unknown as {\n  new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;\n  service: typeof HealthService;\n  serviceName: string;\n};\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "back/schedule/addCron.ts",
    "content": "import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';\nimport { AddCronRequest, AddCronResponse } from '../protos/cron';\nimport nodeSchedule from 'node-schedule';\nimport { scheduleStacks } from './data';\nimport { runCron } from '../shared/runCron';\nimport Logger from '../loaders/logger';\n\nconst addCron = (\n  call: ServerUnaryCall<AddCronRequest, AddCronResponse>,\n  callback: sendUnaryData<AddCronResponse>,\n) => {\n  for (const item of call.request.crons) {\n    const { id, schedule, command, extra_schedules, name } = item;\n    if (scheduleStacks.has(id)) {\n      scheduleStacks.get(id)?.forEach((x) => x.cancel());\n    }\n\n    Logger.info(\n      '[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',\n      id,\n      name,\n      schedule,\n      command,\n    );\n\n    if (extra_schedules?.length) {\n      extra_schedules.forEach((x) => {\n        Logger.info(\n          '[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',\n          id,\n          name,\n          x.schedule,\n          command,\n        );\n      });\n    }\n\n    scheduleStacks.set(id, [\n      nodeSchedule.scheduleJob(id, schedule, async () => {\n        Logger.info(`[schedule][准备运行任务] 命令: ${command}`);\n        runCron(command, item);\n      }),\n      ...(extra_schedules?.length\n        ? extra_schedules.map((x) =>\n            nodeSchedule.scheduleJob(id, x.schedule, async () => {\n              Logger.info(`[schedule][准备运行任务] 命令: ${command}`);\n              runCron(command, item);\n            }),\n          )\n        : []),\n    ]);\n  }\n\n  callback(null, null);\n};\n\nexport { addCron };\n"
  },
  {
    "path": "back/schedule/api.ts",
    "content": "import 'reflect-metadata';\nimport { Container } from 'typedi';\nimport EnvService from '../services/env';\nimport { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';\nimport {\n  CreateEnvRequest,\n  CronItem,\n  DeleteEnvsRequest,\n  DisableEnvsRequest,\n  EnableEnvsRequest,\n  EnvItem,\n  EnvResponse,\n  EnvsResponse,\n  GetEnvByIdRequest,\n  GetEnvsRequest,\n  MoveEnvRequest,\n  Response,\n  SystemNotifyRequest,\n  UpdateEnvNamesRequest,\n  UpdateEnvRequest,\n} from '../protos/api';\nimport LoggerInstance from '../loaders/logger';\nimport pick from 'lodash/pick';\nimport SystemService from '../services/system';\nimport CronService from '../services/cron';\nimport {\n  CronDetailRequest,\n  CronDetailResponse,\n  CreateCronRequest,\n  UpdateCronRequest,\n  DeleteCronsRequest,\n  CronResponse,\n  GetCronsRequest,\n  CronsResponse,\n  GetCronByIdRequest,\n  EnableCronsRequest,\n  DisableCronsRequest,\n  RunCronsRequest,\n} from '../protos/api';\nimport { NotificationInfo } from '../data/notify';\n\nContainer.set('logger', LoggerInstance);\n\nexport const getEnvs = async (\n  call: ServerUnaryCall<GetEnvsRequest, EnvsResponse>,\n  callback: sendUnaryData<EnvsResponse>,\n) => {\n  try {\n    const envService = Container.get(EnvService);\n    const data = await envService.envs(call.request.searchValue);\n    callback(null, {\n      code: 200,\n      data: data.map((x) => ({ ...x, remarks: x.remarks || '' })),\n    });\n  } catch (e: any) {\n    callback(null, {\n      code: 500,\n      data: [],\n      message: e.message,\n    });\n  }\n};\n\nexport const createEnv = async (\n  call: ServerUnaryCall<CreateEnvRequest, EnvsResponse>,\n  callback: sendUnaryData<EnvsResponse>,\n) => {\n  try {\n    const envService = Container.get(EnvService);\n    const data = await envService.create(call.request.envs);\n    callback(null, { code: 200, data });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const updateEnv = async (\n  call: ServerUnaryCall<UpdateEnvRequest, EnvResponse>,\n  callback: sendUnaryData<EnvResponse>,\n) => {\n  try {\n    if (!call.request.env?.id) {\n      return callback(null, {\n        code: 400,\n        data: undefined,\n        message: 'id parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    const data = await envService.update(\n      pick(call.request.env, ['id', 'name', 'value', 'remarks']) as EnvItem,\n    );\n    callback(null, { code: 200, data });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const deleteEnvs = async (\n  call: ServerUnaryCall<DeleteEnvsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    await envService.remove(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const moveEnv = async (\n  call: ServerUnaryCall<MoveEnvRequest, EnvResponse>,\n  callback: sendUnaryData<EnvResponse>,\n) => {\n  try {\n    if (!call.request.id) {\n      return callback(null, {\n        code: 400,\n        data: undefined,\n        message: 'id parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    const data = await envService.move(call.request.id, {\n      fromIndex: call.request.fromIndex,\n      toIndex: call.request.toIndex,\n    });\n    callback(null, { code: 200, data });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const disableEnvs = async (\n  call: ServerUnaryCall<DisableEnvsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    await envService.disabled(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const enableEnvs = async (\n  call: ServerUnaryCall<EnableEnvsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    await envService.enabled(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const updateEnvNames = async (\n  call: ServerUnaryCall<UpdateEnvNamesRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    await envService.updateNames({\n      ids: call.request.ids,\n      name: call.request.name,\n    });\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const getEnvById = async (\n  call: ServerUnaryCall<GetEnvByIdRequest, EnvResponse>,\n  callback: sendUnaryData<EnvResponse>,\n) => {\n  try {\n    if (!call.request.id) {\n      return callback(null, {\n        code: 400,\n        data: undefined,\n        message: 'id parameter is required',\n      });\n    }\n\n    const envService = Container.get(EnvService);\n    const data = await envService.getDb({ id: call.request.id });\n    callback(null, {\n      code: 200,\n      data: { ...data, remarks: data.remarks || '' },\n    });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const systemNotify = async (\n  call: ServerUnaryCall<SystemNotifyRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    const systemService = Container.get(SystemService);\n    const data = await systemService.notify({\n      title: call.request.title,\n      content: call.request.content,\n      notificationInfo: call.request.notificationInfo as unknown as NotificationInfo,\n    });\n    callback(null, data);\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nconst normalizeCronData = (data: CronItem | null): CronItem | undefined => {\n  if (!data) return undefined;\n  return {\n    ...data,\n    sub_id: data.sub_id ?? undefined,\n    extra_schedules: data.extra_schedules ?? undefined,\n    pid: data.pid ?? undefined,\n    task_before: data.task_before ?? undefined,\n    task_after: data.task_after ?? undefined,\n  };\n};\n\nexport const getCronDetail = async (\n  call: ServerUnaryCall<CronDetailRequest, CronDetailResponse>,\n  callback: sendUnaryData<CronDetailResponse>,\n) => {\n  try {\n    if (!call.request.log_path) {\n      return callback(null, {\n        code: 400,\n        data: undefined,\n        message: 'log_path is required',\n      });\n    }\n    const cronService = Container.get(CronService);\n    const data = (await cronService.find({\n      log_path: call.request.log_path,\n    })) as CronItem;\n    callback(null, { code: 200, data: normalizeCronData(data) });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const createCron = async (\n  call: ServerUnaryCall<CreateCronRequest, CronResponse>,\n  callback: sendUnaryData<CronResponse>,\n) => {\n  try {\n    const cronService = Container.get(CronService);\n    const data = (await cronService.create(call.request)) as CronItem;\n    callback(null, { code: 200, data: normalizeCronData(data) });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const updateCron = async (\n  call: ServerUnaryCall<UpdateCronRequest, CronResponse>,\n  callback: sendUnaryData<CronResponse>,\n) => {\n  try {\n    const cronService = Container.get(CronService);\n    const { id, ...fields } = call.request;\n\n    const updateRequest = {\n      id,\n      ...Object.entries(fields).reduce((acc: any, [key, value]) => {\n        if (value !== undefined) {\n          acc[key] = value;\n        }\n        return acc;\n      }, {}),\n    } as UpdateCronRequest;\n\n    const data = (await cronService.update(updateRequest)) as CronItem;\n    callback(null, { code: 200, data: normalizeCronData(data) });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const deleteCrons = async (\n  call: ServerUnaryCall<DeleteCronsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    const cronService = Container.get(CronService);\n    await cronService.remove(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const getCrons = async (\n  call: ServerUnaryCall<GetCronsRequest, CronsResponse>,\n  callback: sendUnaryData<CronsResponse>,\n) => {\n  try {\n    const cronService = Container.get(CronService);\n    const result = await cronService.crontabs({\n      searchValue: call.request.searchValue || '',\n      page: '0',\n      size: '0',\n      sorter: '',\n      filters: '',\n      queryString: '',\n    });\n    const data = result.data.map((x) => normalizeCronData(x as CronItem));\n    callback(null, {\n      code: 200,\n      data: data.filter((x): x is CronItem => x !== undefined),\n    });\n  } catch (e: any) {\n    callback(null, {\n      code: 500,\n      data: [],\n      message: e.message,\n    });\n  }\n};\n\nexport const getCronById = async (\n  call: ServerUnaryCall<GetCronByIdRequest, CronResponse>,\n  callback: sendUnaryData<CronResponse>,\n) => {\n  try {\n    if (!call.request.id) {\n      return callback(null, {\n        code: 400,\n        data: undefined,\n        message: 'id parameter is required',\n      });\n    }\n\n    const cronService = Container.get(CronService);\n    const data = (await cronService.getDb({ id: call.request.id })) as CronItem;\n    callback(null, { code: 200, data: normalizeCronData(data) });\n  } catch (e: any) {\n    callback(null, {\n      code: 404,\n      data: undefined,\n      message: e.message,\n    });\n  }\n};\n\nexport const enableCrons = async (\n  call: ServerUnaryCall<EnableCronsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const cronService = Container.get(CronService);\n    await cronService.enabled(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const disableCrons = async (\n  call: ServerUnaryCall<DisableCronsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const cronService = Container.get(CronService);\n    await cronService.disabled(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n\nexport const runCrons = async (\n  call: ServerUnaryCall<RunCronsRequest, Response>,\n  callback: sendUnaryData<Response>,\n) => {\n  try {\n    if (!call.request.ids || call.request.ids.length === 0) {\n      return callback(null, {\n        code: 400,\n        message: 'ids parameter is required',\n      });\n    }\n\n    const cronService = Container.get(CronService);\n    await cronService.run(call.request.ids);\n    callback(null, { code: 200 });\n  } catch (e: any) {\n    callback(e);\n  }\n};\n"
  },
  {
    "path": "back/schedule/client.ts",
    "content": "import { credentials } from '@grpc/grpc-js';\nimport {\n  AddCronRequest,\n  AddCronResponse,\n  CronClient,\n  DeleteCronRequest,\n  DeleteCronResponse,\n} from '../protos/cron';\nimport config from '../config';\n\nclass Client {\n  private client = new CronClient(\n    `0.0.0.0:${config.grpcPort}`,\n    credentials.createInsecure(),\n    { 'grpc.enable_http_proxy': 0 },\n  );\n\n  addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {\n    return new Promise((resolve, reject) => {\n      this.client.addCron({ crons: request }, (err, res) => {\n        if (err) {\n          reject(err);\n        }\n        resolve(res);\n      });\n    });\n  }\n\n  delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {\n    return new Promise((resolve, reject) => {\n      this.client.delCron({ ids: request }, (err, res) => {\n        if (err) {\n          reject(err);\n        }\n        resolve(res);\n      });\n    });\n  }\n}\n\nexport default new Client();\n"
  },
  {
    "path": "back/schedule/data.ts",
    "content": "import nodeSchedule from 'node-schedule';\nimport { ToadScheduler } from 'toad-scheduler';\n\nexport const scheduleStacks = new Map<string, nodeSchedule.Job[]>();\n\nexport const intervalSchedule = new ToadScheduler();\n"
  },
  {
    "path": "back/schedule/delCron.ts",
    "content": "import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';\nimport { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';\nimport { scheduleStacks } from './data';\nimport Logger from '../loaders/logger';\n\nconst delCron = (\n  call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,\n  callback: sendUnaryData<DeleteCronResponse>,\n) => {\n  for (const id of call.request.ids) {\n    if (scheduleStacks.has(id)) {\n      Logger.info(\n        '[schedule][取消定时任务] 任务ID: %s',\n        id,\n      );\n      scheduleStacks.get(id)?.forEach(x => x.cancel());\n      scheduleStacks.delete(id);\n    }\n  }\n\n  callback(null, null);\n};\n\nexport { delCron };\n"
  },
  {
    "path": "back/schedule/health.ts",
    "content": "import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';\nimport { HealthCheckRequest, HealthCheckResponse } from '../protos/health';\nimport config from '../config';\nimport { promiseExec } from '../config/util';\n\nconst check = async (\n  call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,\n  callback: sendUnaryData<HealthCheckResponse>,\n) => {\n  switch (call.request.service) {\n    case 'cron':\n      const res = await promiseExec(\n        `curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`,\n      );\n\n      if (res.includes('200')) {\n        return callback(null, { status: 1 });\n      }\n\n      const qinglongErrLog = await promiseExec(\n        `tail -n 300 ~/.pm2/logs/qinglong-error.log`,\n      );\n      return callback(\n        new Error(`${qinglongErrLog || ''}\\n${res}`.trim()),\n      );\n\n    default:\n      return callback(null, { status: 1 });\n  }\n};\n\nexport { check };\n"
  },
  {
    "path": "back/services/config.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport path, { join } from 'path';\nimport config from '../config';\nimport { getFileContentByName } from '../config/util';\nimport { Response } from 'express';\nimport { request } from 'undici';\n\n@Service()\nexport default class ConfigService {\n  constructor() {}\n\n  public async getFile(filePath: string, res: Response) {\n    let content = '';\n    const avaliablePath = [config.rootPath, config.configPath].map((x) =>\n      path.resolve(x, filePath),\n    );\n\n    if (\n      config.blackFileList.includes(filePath) ||\n      avaliablePath.every(\n        (x) =>\n          !x.startsWith(config.scriptPath) && !x.startsWith(config.configPath),\n      ) ||\n      !filePath\n    ) {\n      return res.send({ code: 403, message: '文件无法访问' });\n    }\n\n    if (filePath.startsWith('sample/')) {\n      const res = await request(\n        `https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,\n      );\n      content = await res.body.text();\n    } else if (filePath.startsWith('data/scripts/')) {\n      content = await getFileContentByName(join(config.rootPath, filePath));\n    } else {\n      content = await getFileContentByName(join(config.configPath, filePath));\n    }\n\n    res.send({ code: 200, data: content });\n  }\n}\n"
  },
  {
    "path": "back/services/cron.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport config from '../config';\nimport { Crontab, CrontabModel, CrontabStatus } from '../data/cron';\nimport { exec, execSync } from 'child_process';\nimport fs from 'fs/promises';\nimport CronExpressionParser from 'cron-parser';\nimport {\n  getFileContentByName,\n  fileExist,\n  killTask,\n  killAllTasks,\n  getUniqPath,\n  safeJSONParse,\n  isDemoEnv,\n} from '../config/util';\nimport { Op, where, col as colFn, FindOptions, fn, Order } from 'sequelize';\nimport path from 'path';\nimport { TASK_PREFIX, QL_PREFIX } from '../config/const';\nimport cronClient from '../schedule/client';\nimport taskLimit from '../shared/pLimit';\nimport { spawn } from 'cross-spawn';\nimport dayjs from 'dayjs';\nimport pickBy from 'lodash/pickBy';\nimport omit from 'lodash/omit';\nimport { writeFileWithLock } from '../shared/utils';\nimport { ScheduleType } from '../interface/schedule';\nimport { logStreamManager } from '../shared/logStreamManager';\n\n@Service()\nexport default class CronService {\n  constructor(@Inject('logger') private logger: winston.Logger) { }\n\n  private isNodeCron(cron: Crontab) {\n    const { schedule, extra_schedules } = cron;\n    if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {\n      return true;\n    }\n    return false;\n  }\n\n  private isOnceSchedule(schedule?: string) {\n    return schedule?.startsWith(ScheduleType.ONCE);\n  }\n\n  private isBootSchedule(schedule?: string) {\n    return schedule?.startsWith(ScheduleType.BOOT);\n  }\n\n  private isSpecialSchedule(schedule?: string) {\n    return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);\n  }\n\n  private async getLogName(cron: Crontab) {\n    const { log_name, command, id } = cron;\n    if (log_name === '/dev/null') {\n      return log_name;\n    }\n    let uniqPath = await getUniqPath(command, `${id}`);\n    if (log_name) {\n      const normalizedLogName = log_name.startsWith('/')\n        ? log_name\n        : path.join(config.logPath, log_name);\n      if (normalizedLogName.startsWith(config.logPath)) {\n        uniqPath = log_name;\n      }\n    }\n    const logDirPath = path.resolve(config.logPath, `${uniqPath}`);\n    await fs.mkdir(logDirPath, { recursive: true });\n    return uniqPath;\n  }\n\n  public async create(payload: Crontab): Promise<Crontab> {\n    const tab = new Crontab(payload);\n    tab.saved = false;\n    tab.log_name = await this.getLogName(tab);\n    const doc = await this.insert(tab);\n\n    if (isDemoEnv()) {\n      return doc;\n    }\n\n    if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {\n      await cronClient.addCron([\n        {\n          name: doc.name || '',\n          id: String(doc.id),\n          schedule: doc.schedule!,\n          command: this.makeCommand(doc),\n          extra_schedules: doc.extra_schedules || [],\n        },\n      ]);\n    }\n\n    await this.setCrontab();\n    return doc;\n  }\n\n  public async insert(payload: Crontab): Promise<Crontab> {\n    return await CrontabModel.create(payload, { returning: true });\n  }\n\n  public async update(payload: Partial<Crontab>): Promise<Crontab> {\n    const doc = await this.getDb({ id: payload.id });\n    const tab = new Crontab({ ...doc, ...payload });\n    tab.saved = false;\n    tab.log_name = await this.getLogName(tab);\n    const newDoc = await this.updateDb(tab);\n\n    if (doc.isDisabled === 1 || isDemoEnv()) {\n      return newDoc;\n    }\n\n    if (this.isNodeCron(doc)) {\n      await cronClient.delCron([String(doc.id)]);\n    }\n\n    if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {\n      await cronClient.addCron([\n        {\n          name: doc.name || '',\n          id: String(newDoc.id),\n          schedule: newDoc.schedule!,\n          command: this.makeCommand(newDoc),\n          extra_schedules: newDoc.extra_schedules || [],\n        },\n      ]);\n    }\n\n    await this.setCrontab();\n    return newDoc;\n  }\n\n  public async updateDb(payload: Crontab): Promise<Crontab> {\n    await CrontabModel.update(payload, { where: { id: payload.id } });\n    return await this.getDb({ id: payload.id });\n  }\n\n  public async status({\n    ids,\n    status,\n    pid,\n    log_path,\n    last_running_time = 0,\n    last_execution_time = 0,\n  }: {\n    ids: number[];\n    status: CrontabStatus;\n    pid: number;\n    log_path: string;\n    last_running_time: number;\n    last_execution_time: number;\n  }) {\n    let options: any = {\n      status,\n      pid,\n      log_path,\n      last_execution_time,\n    };\n    if (last_running_time > 0) {\n      options.last_running_time = last_running_time;\n    }\n\n    for (const id of ids) {\n      let cron;\n      try {\n        cron = await this.getDb({ id });\n      } catch (err) { }\n      if (!cron) {\n        continue;\n      }\n      if (status === CrontabStatus.idle && log_path !== cron.log_path) {\n        options = omit(options, ['status', 'log_path', 'pid']);\n      }\n      await CrontabModel.update(\n        { ...pickBy(options, (v) => v === 0 || !!v) },\n        { where: { id } },\n      );\n    }\n  }\n\n  public async remove(ids: number[]) {\n    await CrontabModel.destroy({ where: { id: ids } });\n    await cronClient.delCron(ids.map(String));\n    await this.setCrontab();\n  }\n\n  public async pin(ids: number[]) {\n    await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });\n  }\n\n  public async unPin(ids: number[]) {\n    await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });\n  }\n\n  public async addLabels(ids: string[], labels: string[]) {\n    const docs = await CrontabModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      await CrontabModel.update(\n        {\n          labels: Array.from(new Set((doc.labels || []).concat(labels))),\n        },\n        { where: { id: doc.id } },\n      );\n    }\n  }\n\n  public async removeLabels(ids: string[], labels: string[]) {\n    const docs = await CrontabModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      await CrontabModel.update(\n        {\n          labels: (doc.labels || []).filter((label) => !labels.includes(label)),\n        },\n        { where: { id: doc.id } },\n      );\n    }\n  }\n\n  private formatViewQuery(query: any, viewQuery: any) {\n    if (viewQuery.filters && viewQuery.filters.length > 0) {\n      const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;\n      if (!query[primaryOperate]) {\n        query[primaryOperate] = [];\n      }\n      for (const col of viewQuery.filters) {\n        const { property, value, operation } = col;\n        let q: any = {};\n        let operate2: any = null;\n        let operate: any = null;\n        switch (operation) {\n          case 'Reg':\n            operate = Op.like;\n            operate2 = Op.or;\n            break;\n          case 'NotReg':\n            operate = Op.notLike;\n            operate2 = Op.and;\n            break;\n          case 'In':\n            if (\n              property === 'status' &&\n              !value.includes(CrontabStatus.disabled)\n            ) {\n              q[Op.and] = [\n                { [property]: Array.isArray(value) ? value : [value] },\n                { isDisabled: 0 },\n              ];\n            } else {\n              q[Op.or] = [\n                {\n                  [property]: Array.isArray(value) ? value : [value],\n                },\n                property === 'status' && value.includes(CrontabStatus.disabled)\n                  ? { isDisabled: 1 }\n                  : {},\n              ];\n            }\n            break;\n          case 'Nin':\n            q[Op.and] = [\n              {\n                [Op.or]: [\n                  {\n                    [property]: {\n                      [Op.notIn]: Array.isArray(value) ? value : [value],\n                    },\n                  },\n                  {\n                    [property]: { [Op.is]: null },\n                  },\n                ],\n              },\n              property === 'status' && value.includes(2)\n                ? { isDisabled: { [Op.ne]: 1 } }\n                : {},\n            ];\n            break;\n          default:\n            break;\n        }\n        if (operate && operate2) {\n          q[property] = {\n            [Op.or]: [\n              {\n                [operate2]: [\n                  { [operate]: `%${value}%` },\n                  { [operate]: `%${encodeURI(value)}%` },\n                ],\n              },\n              {\n                [operate2]: [\n                  where(colFn(property), operate, `%${value}%`),\n                  where(colFn(property), operate, `%${encodeURI(value)}%`),\n                ],\n              },\n            ],\n          };\n        }\n        query[primaryOperate].push(q);\n      }\n    }\n  }\n\n  private formatSearchText(query: any, searchText: string | undefined) {\n    if (searchText) {\n      if (!query[Op.and]) {\n        query[Op.and] = [];\n      }\n      let q: any = {};\n      const textArray = searchText.split(':');\n      switch (textArray[0]) {\n        case 'name':\n        case 'command':\n        case 'schedule':\n        case 'label':\n          const column = textArray[0] === 'label' ? 'labels' : textArray[0];\n          q[column] = {\n            [Op.or]: [\n              { [Op.like]: `%${textArray[1]}%` },\n              { [Op.like]: `%${encodeURI(textArray[1])}%` },\n            ],\n          };\n          break;\n        default:\n          const reg = {\n            [Op.or]: [\n              { [Op.like]: `%${searchText}%` },\n              { [Op.like]: `%${encodeURI(searchText)}%` },\n            ],\n          };\n          q[Op.or] = [\n            {\n              name: reg,\n            },\n            {\n              command: reg,\n            },\n            {\n              schedule: reg,\n            },\n            {\n              labels: reg,\n            },\n          ];\n          break;\n      }\n      query[Op.and].push(q);\n    }\n  }\n\n  private formatFilterQuery(query: any, filterQuery: any) {\n    if (filterQuery) {\n      if (!query[Op.and]) {\n        query[Op.and] = [];\n      }\n      const filterKeys: any = Object.keys(filterQuery);\n      for (const key of filterKeys) {\n        let q: any = {};\n        if (!filterQuery[key]) continue;\n        if (key === 'status') {\n          if (filterQuery[key].includes(CrontabStatus.disabled)) {\n            q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };\n          } else {\n            q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };\n          }\n        } else {\n          q[key] = filterQuery[key];\n        }\n        query[Op.and].push(q);\n      }\n    }\n  }\n\n  private formatViewSort(order: string[][], viewQuery: any) {\n    if (viewQuery.sorts && viewQuery.sorts.length > 0) {\n      for (const { property, type } of viewQuery.sorts) {\n        order.unshift([property, type]);\n      }\n    }\n  }\n\n  public async find({\n    log_path,\n  }: {\n    log_path: string;\n  }): Promise<Crontab | undefined> {\n    try {\n      const result = await CrontabModel.findOne({ where: { log_path } });\n      return result?.get({ plain: true });\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  public async crontabs(params?: {\n    searchValue: string;\n    page: string;\n    size: string;\n    sorter: string;\n    filters: string;\n    queryString: string;\n  }): Promise<{ data: Crontab[]; total: number }> {\n    const searchText = params?.searchValue;\n    const page = Number(params?.page || '0');\n    const size = Number(params?.size || '0');\n    const viewQuery = safeJSONParse(params?.queryString);\n    const filterQuery = safeJSONParse(params?.filters);\n    const sorterQuery = safeJSONParse(params?.sorter);\n\n    let query: any = {};\n    let order = [\n      ['isPinned', 'DESC'],\n      ['isDisabled', 'ASC'],\n      ['status', 'ASC'],\n      ['createdAt', 'DESC'],\n    ];\n\n    this.formatViewQuery(query, viewQuery);\n    this.formatSearchText(query, searchText);\n    this.formatFilterQuery(query, filterQuery);\n    this.formatViewSort(order, viewQuery);\n\n    if (sorterQuery) {\n      const { field, type } = sorterQuery;\n      if (field && type) {\n        order.unshift([field, type]);\n      }\n    }\n    let condition: FindOptions<Crontab> = {\n      where: query,\n      order: order as Order,\n    };\n    if (page && size) {\n      condition.offset = (page - 1) * size;\n      condition.limit = size;\n    }\n    try {\n      const result = await CrontabModel.findAll(condition);\n      const count = await CrontabModel.count({ where: query });\n      return { data: result.map((x) => x.get({ plain: true })), total: count };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {\n    const doc: any = await CrontabModel.findOne({ where: { ...query } });\n    if (!doc) {\n      throw new Error(`Cron ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async run(ids: number[]) {\n    await CrontabModel.update(\n      { status: CrontabStatus.queued },\n      { where: { id: ids } },\n    );\n    ids.forEach((id) => {\n      this.runSingle(id);\n    });\n  }\n\n  public async stop(ids: number[]) {\n    const docs = await CrontabModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      // Kill all running instances of this task\n      try {\n        if (doc.pid) {\n          await killTask(doc.pid);\n        }\n        const command = doc.command.replace(/\\s+/g, ' ').trim();\n        await killAllTasks(command);\n        this.logger.info(\n          `[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,\n        );\n      } catch (error) {\n        this.logger.error(\n          `[panel][停止任务失败] 任务ID: ${doc.id}, 错误: ${error}`,\n        );\n      }\n    }\n\n    await CrontabModel.update(\n      { status: CrontabStatus.idle, pid: undefined },\n      { where: { id: ids } },\n    );\n  }\n\n  private async runSingle(cronId: number): Promise<number | void> {\n    return taskLimit.manualRunWithCronLimit(() => {\n      return new Promise(async (resolve: any) => {\n        const cron = await this.getDb({ id: cronId });\n        const params = {\n          name: cron.name,\n          command: cron.command,\n          schedule: cron.schedule,\n          extra_schedules: cron.extra_schedules,\n        };\n        if (cron.status !== CrontabStatus.queued) {\n          resolve(params);\n          return;\n        }\n\n        this.logger.info(\n          `[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,\n        );\n\n        let { id, command, log_name } = cron;\n\n        const uniqPath =\n          log_name === '/dev/null' || !log_name\n            ? await getUniqPath(command, `${id}`)\n            : log_name;\n        const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');\n        const logDirPath = path.resolve(config.logPath, `${uniqPath}`);\n        await fs.mkdir(logDirPath, { recursive: true });\n        const logPath = `${uniqPath}/${logTime}.log`;\n        const absolutePath = path.resolve(config.logPath, `${logPath}`);\n        const cp = spawn(\n          `real_log_path=${logPath} no_delay=true ${this.makeCommand(\n            cron,\n            true,\n          )}`,\n          { shell: '/bin/bash' },\n        );\n\n        await CrontabModel.update(\n          { status: CrontabStatus.running, pid: cp.pid, log_path: logPath },\n          { where: { id } },\n        );\n        cp.stdout.on('data', async (data) => {\n          await logStreamManager.write(absolutePath, data.toString());\n        });\n        cp.stderr.on('data', async (data) => {\n          this.logger.info(\n            '[panel][执行任务失败] 命令: %s, 错误信息: %j',\n            command,\n            data.toString(),\n          );\n          await logStreamManager.write(absolutePath, data.toString());\n        });\n        cp.on('error', async (err) => {\n          this.logger.error(\n            '[panel][创建任务失败] 命令: %s, 错误信息: %j',\n            command,\n            err,\n          );\n          await logStreamManager.write(absolutePath, JSON.stringify(err));\n        });\n\n        cp.on('exit', async (code) => {\n          this.logger.info(\n            '[panel][执行任务结束] 参数: %s, 退出码: %j',\n            JSON.stringify(params),\n            code,\n          );\n          // Close the stream after task completion\n          await logStreamManager.closeStream(absolutePath);\n          await CrontabModel.update(\n            { status: CrontabStatus.idle, pid: undefined },\n            { where: { id } },\n          );\n          resolve({ ...params, pid: cp.pid, code });\n        });\n      });\n    });\n  }\n\n  public async disabled(ids: number[]) {\n    await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });\n    await cronClient.delCron(ids.map(String));\n    await this.setCrontab();\n  }\n\n  public async enabled(ids: number[]) {\n    await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });\n    const docs = await CrontabModel.findAll({ where: { id: ids } });\n    const sixCron = docs\n      .filter((x) => this.isNodeCron(x) && !this.isSpecialSchedule(x.schedule))\n      .map((doc) => ({\n        name: doc.name || '',\n        id: String(doc.id),\n        schedule: doc.schedule!,\n        command: this.makeCommand(doc),\n        extra_schedules: doc.extra_schedules || [],\n      }));\n\n    if (isDemoEnv()) {\n      return;\n    }\n    await cronClient.addCron(sixCron);\n    await this.setCrontab();\n  }\n\n  public async log(id: number) {\n    const doc = await this.getDb({ id });\n    if (!doc) {\n      return '';\n    }\n    if (doc.log_name === '/dev/null') {\n      return '日志设置为忽略';\n    }\n    const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);\n    const logFileExist = doc.log_path && (await fileExist(absolutePath));\n    if (logFileExist) {\n      return await getFileContentByName(`${absolutePath}`);\n    } else {\n      return typeof doc.status === 'number' &&\n        [CrontabStatus.queued, CrontabStatus.running].includes(doc.status)\n        ? '运行中...'\n        : '日志不存在...';\n    }\n  }\n\n  public async logs(id: number) {\n    const doc = await this.getDb({ id });\n    if (!doc || !doc.log_path) {\n      return [];\n    }\n\n    const relativeDir = path.dirname(`${doc.log_path}`);\n    const dir = path.resolve(config.logPath, relativeDir);\n    const dirExist = await fileExist(dir);\n    if (dirExist) {\n      let files = await fs.readdir(dir);\n      return (\n        await Promise.all(\n          files.map(async (x) => ({\n            filename: x,\n            directory: relativeDir.replace(config.logPath, ''),\n            time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,\n          })),\n        )\n      ).sort((a, b) => b.time - a.time);\n    } else {\n      return [];\n    }\n  }\n\n  private makeCommand(tab: Crontab, realTime?: boolean) {\n    let command = tab.command.trim();\n    if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {\n      command = `${TASK_PREFIX}${tab.command}`;\n    }\n    let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${tab.id} `;\n    // Only include log_name if it has a truthy value to avoid passing null/undefined to shell\n    if (tab.log_name) {\n      commandVariable += `log_name=${tab.log_name} `;\n    }\n    if (tab.task_before) {\n      commandVariable += `task_before='${tab.task_before\n        .replace(/'/g, \"'\\\\''\")\n        .replace(/;? *\\n/g, ';')\n        .trim()}' `;\n    }\n    if (tab.task_after) {\n      commandVariable += `task_after='${tab.task_after\n        .replace(/'/g, \"'\\\\''\")\n        .replace(/;? *\\n/g, ';')\n        .trim()}' `;\n    }\n\n    const crontab_job_string = `${commandVariable}${command}`;\n    return crontab_job_string;\n  }\n\n  private async setCrontab(data?: { data: Crontab[]; total: number }) {\n    const tabs = data ?? (await this.crontabs());\n    var crontab_string = '';\n    tabs.data.forEach((tab) => {\n      if (\n        tab.isDisabled === 1 ||\n        this.isNodeCron(tab) ||\n        this.isSpecialSchedule(tab.schedule)\n      ) {\n        crontab_string += '# ';\n        crontab_string += tab.schedule;\n        crontab_string += ' ';\n        crontab_string += this.makeCommand(tab);\n        crontab_string += '\\n';\n      } else {\n        crontab_string += tab.schedule;\n        crontab_string += ' ';\n        crontab_string += this.makeCommand(tab);\n        crontab_string += '\\n';\n      }\n    });\n\n    await writeFileWithLock(config.crontabFile, crontab_string);\n\n    try {\n      execSync(`crontab ${config.crontabFile}`);\n    } catch (error: any) {\n      const errorMsg = error.message || String(error);\n      this.logger.error('[crontab] Failed to update system crontab:', errorMsg);\n    }\n\n    await CrontabModel.update({ saved: true }, { where: {} });\n  }\n\n  public importCrontab() {\n    exec('crontab -l', (error, stdout) => {\n      if (error) {\n        const errorMsg = error.message || String(error);\n        this.logger.error('[crontab] Failed to read system crontab:', errorMsg);\n      }\n\n      const lines = stdout.split('\\n');\n      const namePrefix = new Date().getTime();\n\n      lines.reverse().forEach(async (line, index) => {\n        line = line.replace(/\\t+/g, ' ');\n        const regex =\n          /^((\\@[a-zA-Z]+\\s+)|(([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)\\s+))/;\n        const command = line.replace(regex, '').trim();\n        const schedule = line.replace(command, '').trim();\n\n        if (\n          command &&\n          schedule &&\n          CronExpressionParser.parse(schedule).hasNext()\n        ) {\n          const name = namePrefix + '_' + index;\n\n          const _crontab = await CrontabModel.findOne({\n            where: { command, schedule },\n          });\n          if (!_crontab) {\n            await this.create({ name, command, schedule });\n          } else {\n            _crontab.command = command;\n            _crontab.schedule = schedule;\n            await this.update(_crontab);\n          }\n        }\n      });\n    });\n  }\n\n  public async autosave_crontab() {\n    const tabs = await this.crontabs();\n    const regularCrons = tabs.data\n      .filter(\n        (x) =>\n          x.isDisabled !== 1 &&\n          this.isNodeCron(x) &&\n          !this.isSpecialSchedule(x.schedule),\n      )\n      .map((doc) => ({\n        name: doc.name || '',\n        id: String(doc.id),\n        schedule: doc.schedule!,\n        command: this.makeCommand(doc),\n        extra_schedules: doc.extra_schedules || [],\n      }));\n\n    if (isDemoEnv()) {\n      await writeFileWithLock(config.crontabFile, '');\n      return;\n    }\n    await cronClient.addCron(regularCrons);\n    this.setCrontab(tabs);\n  }\n\n  public async bootTask() {\n    const tabs = await this.crontabs();\n    const bootTasks = tabs.data.filter(\n      (x) => !x.isDisabled && this.isBootSchedule(x.schedule),\n    );\n    if (bootTasks.length > 0) {\n      await CrontabModel.update(\n        { status: CrontabStatus.queued },\n        { where: { id: bootTasks.map((t) => t.id!) } },\n      );\n      for (const task of bootTasks) {\n        await this.runSingle(task.id!);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "back/services/cronView.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport { CrontabView, CrontabViewModel } from '../data/cronView';\nimport {\n  initPosition,\n  maxPosition,\n  minPosition,\n  stepPosition,\n} from '../data/env';\nimport { FindOptions } from 'sequelize';\n\n@Service()\nexport default class CronViewService {\n  constructor(@Inject('logger') private logger: winston.Logger) {}\n\n  public async create(payload: CrontabView): Promise<CrontabView> {\n    let position = initPosition;\n    const views = await this.list();\n    if (views && views.length > 0 && views[views.length - 1].position) {\n      position = views[views.length - 1].position as number;\n    }\n    position = position / 2;\n    const tab = new CrontabView({ ...payload, position });\n    const doc = await this.insert(tab);\n\n    await this.checkPosition(tab.position!);\n    return doc;\n  }\n\n  public async insert(payload: CrontabView): Promise<CrontabView> {\n    return await CrontabViewModel.create(payload, { returning: true });\n  }\n\n  public async update(payload: CrontabView): Promise<CrontabView> {\n    const doc = await this.getDb({ id: payload.id });\n    const tab = new CrontabView({ ...doc, ...payload });\n    const newDoc = await this.updateDb(tab);\n    return newDoc;\n  }\n\n  public async updateDb(payload: CrontabView): Promise<CrontabView> {\n    await CrontabViewModel.update(payload, { where: { id: payload.id } });\n    return await this.getDb({ id: payload.id });\n  }\n\n  public async remove(ids: number[]) {\n    await CrontabViewModel.destroy({ where: { id: ids } });\n  }\n\n  public async list(): Promise<CrontabView[]> {\n    try {\n      const result = await CrontabViewModel.findAll({\n        where: {},\n        order: [['position', 'DESC']],\n      });\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  public async getDb(\n    query: FindOptions<CrontabView>['where'],\n  ): Promise<CrontabView> {\n    const doc: any = await CrontabViewModel.findOne({ where: { ...query } });\n    if (!doc) {\n      throw new Error(`CronView ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async disabled(ids: number[]) {\n    await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });\n  }\n\n  public async enabled(ids: number[]) {\n    await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });\n  }\n\n  private async checkPosition(position: number) {\n    const precisionPosition = parseFloat(position.toPrecision(16));\n    if (precisionPosition < minPosition || precisionPosition > maxPosition) {\n      const envs = await this.list();\n      let position = initPosition;\n      for (const env of envs) {\n        position = position - stepPosition;\n        await this.updateDb({ id: env.id, position });\n      }\n    }\n  }\n\n  private getPrecisionPosition(position: number): number {\n    return parseFloat(position.toPrecision(16));\n  }\n\n  public async move({\n    id,\n    fromIndex,\n    toIndex,\n  }: {\n    fromIndex: number;\n    toIndex: number;\n    id: number;\n  }): Promise<CrontabView> {\n    let targetPosition: number;\n    const isUpward = fromIndex > toIndex;\n    const views = await this.list();\n    if (toIndex === 0 || toIndex === views.length - 1) {\n      targetPosition = isUpward\n        ? views[0].position! * 2\n        : views[toIndex].position! / 2;\n    } else {\n      targetPosition = isUpward\n        ? (views[toIndex].position! + views[toIndex - 1].position!) / 2\n        : (views[toIndex].position! + views[toIndex + 1].position!) / 2;\n    }\n    const newDoc = await this.update({\n      id,\n      position: this.getPrecisionPosition(targetPosition),\n    });\n\n    await this.checkPosition(targetPosition);\n    return newDoc;\n  }\n}\n"
  },
  {
    "path": "back/services/dependence.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport config from '../config';\nimport {\n  Dependence,\n  DependenceStatus,\n  DependenceTypes,\n  DependenceModel,\n  versionDependenceCommandTypes,\n} from '../data/dependence';\nimport { spawn } from 'cross-spawn';\nimport SockService from './sock';\nimport { FindOptions, Op } from 'sequelize';\nimport {\n  fileExist,\n  getPid,\n  killTask,\n  promiseExecSuccess,\n  getInstallCommand,\n  getUninstallCommand,\n  getGetCommand,\n} from '../config/util';\nimport dayjs from 'dayjs';\nimport taskLimit from '../shared/pLimit';\n\n@Service()\nexport default class DependenceService {\n  constructor(\n    @Inject('logger') private logger: winston.Logger,\n    private sockService: SockService,\n  ) { }\n\n  public async create(payloads: Dependence[]): Promise<Dependence[]> {\n    const tabs = payloads.map((x) => {\n      const tab = new Dependence({ ...x, status: DependenceStatus.queued });\n      return tab;\n    });\n    const docs = await this.insert(tabs);\n    this.installDependenceOneByOne(docs);\n    return docs;\n  }\n\n  public async insert(payloads: Dependence[]): Promise<Dependence[]> {\n    const docs = await DependenceModel.bulkCreate(payloads);\n    return docs;\n  }\n\n  public async update(\n    payload: Dependence & { id: string },\n  ): Promise<Dependence> {\n    const { id, ...other } = payload;\n    const doc = await this.getDb({ id });\n    const tab = new Dependence({\n      ...doc,\n      ...other,\n      status: DependenceStatus.queued,\n    });\n    const newDoc = await this.updateDb(tab);\n    this.installDependenceOneByOne([newDoc]);\n    return newDoc;\n  }\n\n  private async updateDb(payload: Dependence): Promise<Dependence> {\n    await DependenceModel.update(payload, { where: { id: payload.id } });\n    return await this.getDb({ id: payload.id });\n  }\n\n  public async remove(ids: number[], force = false): Promise<Dependence[]> {\n    const docs = await DependenceModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      taskLimit.removeQueuedDependency(doc);\n    }\n    const unInstalledDeps = docs.filter(\n      (x) => x.status !== DependenceStatus.installed,\n    );\n    const installedDeps = docs.filter(\n      (x) => x.status === DependenceStatus.installed,\n    );\n    await this.removeDb(unInstalledDeps.map((x) => x.id!));\n\n    if (installedDeps.length) {\n      await DependenceModel.update(\n        { status: DependenceStatus.queued, log: [] },\n        { where: { id: ids } },\n      );\n\n      this.installDependenceOneByOne(docs, false, force);\n    }\n    return docs;\n  }\n\n  public async removeDb(ids: number[]) {\n    await DependenceModel.destroy({ where: { id: ids } });\n  }\n\n  public async dependencies(\n    {\n      searchValue,\n      type,\n      status,\n    }: {\n      searchValue: string;\n      type: keyof typeof DependenceTypes;\n      status: string;\n    },\n    sort: any = [],\n    query: any = {},\n  ): Promise<Dependence[]> {\n    let condition = query;\n    if (type && DependenceTypes[type] !== undefined) {\n      condition.type = DependenceTypes[type];\n    }\n    if (status) {\n      condition.status = status.split(',').map(Number);\n    }\n    if (searchValue) {\n      const encodeText = encodeURI(searchValue);\n      condition.name = {\n        [Op.or]: [\n          { [Op.like]: `%${searchValue}%` },\n          { [Op.like]: `%${encodeText}%` },\n        ],\n      };\n    }\n    try {\n      return await this.find(condition, sort);\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  public installDependenceOneByOne(\n    docs: Dependence[],\n    isInstall: boolean = true,\n    force: boolean = false,\n  ): Promise<void> {\n    docs.forEach((dep) => {\n      this.installOrUninstallDependency(dep, isInstall, force);\n    });\n\n    return taskLimit.waitDependencyQueueDone();\n  }\n\n  public async reInstall(ids: number[]): Promise<Dependence[]> {\n    await DependenceModel.update(\n      { status: DependenceStatus.queued, log: [] },\n      { where: { id: ids } },\n    );\n\n    const docs = await DependenceModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      taskLimit.removeQueuedDependency(doc);\n    }\n    this.installDependenceOneByOne(docs, true, true);\n    return docs;\n  }\n\n  public async cancel(ids: number[]) {\n    const docs = await DependenceModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      taskLimit.removeQueuedDependency(doc);\n      const depInstallCommand = getInstallCommand(doc.type, doc.name);\n      const depUnInstallCommand = getUninstallCommand(doc.type, doc.name);\n      const pids = await Promise.all([\n        getPid(depInstallCommand),\n        getPid(depUnInstallCommand),\n      ]);\n      for (const pid of pids) {\n        pid && (await killTask(pid));\n      }\n    }\n    await DependenceModel.update(\n      { status: DependenceStatus.cancelled },\n      { where: { id: ids } },\n    );\n  }\n\n  private async find(query: any, sort: any = []): Promise<Dependence[]> {\n    const docs = await DependenceModel.findAll({\n      where: { ...query },\n      order: [...sort, ['createdAt', 'DESC']],\n    });\n    return docs;\n  }\n\n  public async getDb(\n    query: FindOptions<Dependence>['where'],\n  ): Promise<Dependence> {\n    const doc: any = await DependenceModel.findOne({ where: { ...query } });\n    if (!doc) {\n      throw new Error(`Dependency ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  private async updateLog(ids: number[], log: string): Promise<void> {\n    taskLimit.updateDepLog(async () => {\n      const docs = await DependenceModel.findAll({ where: { id: ids } });\n      for (const doc of docs) {\n        const newLog = doc?.log ? [...doc.log, log] : [log];\n        await DependenceModel.update(\n          { log: newLog },\n          { where: { id: doc.id } },\n        );\n      }\n      return null;\n    });\n  }\n\n  public installOrUninstallDependency(\n    dependency: Dependence,\n    isInstall: boolean = true,\n    force: boolean = false,\n  ) {\n    return taskLimit.runDependeny(dependency, () => {\n      return new Promise(async (resolve) => {\n        if (taskLimit.firstDependencyId !== dependency.id) {\n          return resolve(null);\n        }\n\n        taskLimit.removeQueuedDependency(dependency);\n\n        const depIds = [dependency.id!];\n        const status = isInstall\n          ? DependenceStatus.installing\n          : DependenceStatus.removing;\n        await DependenceModel.update({ status }, { where: { id: depIds } });\n\n        const socketMessageType = isInstall\n          ? 'installDependence'\n          : 'uninstallDependence';\n        let depName = dependency.name.trim();\n        const command = isInstall\n          ? getInstallCommand(dependency.type, depName)\n          : getUninstallCommand(dependency.type, depName);\n        const actionText = isInstall ? '安装' : '删除';\n        const startTime = dayjs();\n\n        const message = `开始${actionText}依赖 ${depName}，开始时间 ${startTime.format(\n          'YYYY-MM-DD HH:mm:ss',\n        )}\\n\\n`;\n        this.sockService.sendMessage({\n          type: socketMessageType,\n          message,\n          references: depIds,\n        });\n        this.updateLog(depIds, message);\n\n        // 判断是否已经安装过依赖\n        if (isInstall && !force) {\n          const getCommand = getGetCommand(dependency.type, depName);\n          const depVersionStr = versionDependenceCommandTypes[dependency.type];\n          let depVersion = '';\n          if (depName.includes(depVersionStr)) {\n            const symbolRegx = new RegExp(\n              `(.*)${depVersionStr}([0-9\\\\.\\\\-\\\\+a-zA-Z]*)`,\n            );\n            const [, _depName, _depVersion] = depName.match(symbolRegx) || [];\n            if (_depVersion && _depName) {\n              depName = _depName;\n              depVersion = _depVersion;\n            }\n          }\n          const isNodeDependence = dependency.type === DependenceTypes.nodejs;\n          const isLinuxDependence = dependency.type === DependenceTypes.linux;\n          const isPythonDependence =\n            dependency.type === DependenceTypes.python3;\n          const depInfo = (await promiseExecSuccess(getCommand))\n            .replace(/\\s{2,}/, ' ')\n            .replace(/\\s+$/, '');\n\n          if (\n            depInfo &&\n            ((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||\n              (isLinuxDependence &&\n                depInfo.toLocaleLowerCase().includes('installed')) ||\n              isPythonDependence) &&\n            (!depVersion || depInfo.includes(depVersion))\n          ) {\n            const endTime = dayjs();\n            const _message = `检测到已经安装 ${depName}\\n\\n${depInfo}\\n\\n跳过安装\\n\\n依赖${actionText}成功，结束时间 ${endTime.format(\n              'YYYY-MM-DD HH:mm:ss',\n            )}，耗时 ${endTime.diff(startTime, 'second')} 秒`;\n            this.sockService.sendMessage({\n              type: socketMessageType,\n              message: _message,\n              references: depIds,\n            });\n            this.updateLog(depIds, _message);\n            await DependenceModel.update(\n              { status: DependenceStatus.installed },\n              { where: { id: depIds } },\n            );\n            return resolve(null);\n          }\n        }\n        const dependenceProxyFileExist = await fileExist(\n          config.dependenceProxyFile,\n        );\n        const proxyStr = dependenceProxyFileExist\n          ? `source ${config.dependenceProxyFile} &&`\n          : '';\n        const cp = spawn(`${proxyStr} ${command}`, {\n          shell: '/bin/bash',\n        });\n\n        cp.stdout.on('data', async (data) => {\n          this.sockService.sendMessage({\n            type: socketMessageType,\n            message: data.toString(),\n            references: depIds,\n          });\n          this.updateLog(depIds, data.toString());\n        });\n\n        cp.stderr.on('data', async (data) => {\n          this.sockService.sendMessage({\n            type: socketMessageType,\n            message: data.toString(),\n            references: depIds,\n          });\n          this.updateLog(depIds, data.toString());\n        });\n\n        cp.on('error', async (err) => {\n          this.sockService.sendMessage({\n            type: socketMessageType,\n            message: JSON.stringify(err),\n            references: depIds,\n          });\n          this.updateLog(depIds, JSON.stringify(err));\n        });\n\n        cp.on('exit', async (code) => {\n          const endTime = dayjs();\n          const isSucceed = code === 0;\n          const resultText = isSucceed ? '成功' : '失败';\n\n          const message = `\\n依赖${actionText}${resultText}，结束时间 ${endTime.format(\n            'YYYY-MM-DD HH:mm:ss',\n          )}，耗时 ${endTime.diff(startTime, 'second')} 秒`;\n          this.sockService.sendMessage({\n            type: socketMessageType,\n            message,\n            references: depIds,\n          });\n          this.updateLog(depIds, message);\n\n          let status: number;\n          if (isSucceed) {\n            status = isInstall\n              ? DependenceStatus.installed\n              : DependenceStatus.removed;\n          } else {\n            status = isInstall\n              ? DependenceStatus.installFailed\n              : DependenceStatus.removeFailed;\n          }\n          const docs = await DependenceModel.findAll({ where: { id: depIds } });\n          const _docIds = docs\n            .filter((x) => x.status !== DependenceStatus.cancelled)\n            .map((x) => x.id!);\n\n          if (_docIds.length > 0) {\n            await DependenceModel.update(\n              { status },\n              { where: { id: _docIds } },\n            );\n          }\n\n          // 如果删除依赖成功或者强制删除\n          if ((isSucceed || force) && !isInstall) {\n            this.removeDb(depIds);\n          }\n\n          resolve(null);\n        });\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "back/services/env.ts",
    "content": "import groupBy from 'lodash/groupBy';\nimport { FindOptions, Op } from 'sequelize';\nimport { Inject, Service } from 'typedi';\nimport winston from 'winston';\nimport config from '../config';\nimport {\n  Env,\n  EnvModel,\n  EnvStatus,\n  initPosition,\n  maxPosition,\n  minPosition,\n  stepPosition,\n} from '../data/env';\nimport { writeFileWithLock } from '../shared/utils';\nimport { sequelize } from '../data';\n\n@Service()\nexport default class EnvService {\n  constructor(@Inject('logger') private logger: winston.Logger) { }\n\n  public async create(payloads: Env[]): Promise<Env[]> {\n    const envs = await this.envs();\n    let position = initPosition;\n    if (\n      envs &&\n      envs.length > 0 &&\n      typeof envs[envs.length - 1].position === 'number'\n    ) {\n      position = envs[envs.length - 1].position!;\n    }\n    const tabs = payloads.map((x) => {\n      position = position - stepPosition;\n      const tab = new Env({ ...x, position });\n      return tab;\n    });\n    const docs = await this.insert(tabs);\n    await this.set_envs();\n    await this.checkPosition(tabs[tabs.length - 1].position!);\n    return docs;\n  }\n\n  public async insert(payloads: Env[]): Promise<Env[]> {\n    const result: Env[] = [];\n    for (const env of payloads) {\n      const doc = await EnvModel.create(env, { returning: true });\n      result.push(doc);\n    }\n    return result;\n  }\n\n  public async update(payload: Env): Promise<Env> {\n    const doc = await this.getDb({ id: payload.id });\n    const tab = new Env({ ...doc, ...payload });\n    const newDoc = await this.updateDb(tab);\n    await this.set_envs();\n    return newDoc;\n  }\n\n  private async updateDb(payload: Env): Promise<Env> {\n    await EnvModel.update({ ...payload }, { where: { id: payload.id } });\n    return await this.getDb({ id: payload.id });\n  }\n\n  public async remove(ids: number[]) {\n    await EnvModel.destroy({ where: { id: ids } });\n    await this.set_envs();\n  }\n\n  public async move(\n    id: number,\n    {\n      fromIndex,\n      toIndex,\n    }: {\n      fromIndex: number;\n      toIndex: number;\n    },\n  ): Promise<Env> {\n    let targetPosition: number;\n    const isUpward = fromIndex > toIndex;\n    const envs = await this.envs();\n    if (toIndex === 0 || toIndex === envs.length - 1) {\n      targetPosition = isUpward\n        ? envs[0].position! + stepPosition\n        : envs[toIndex].position! - stepPosition;\n    } else {\n      targetPosition = isUpward\n        ? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2\n        : (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;\n    }\n\n    const newDoc = await this.update({\n      id,\n      position: this.getPrecisionPosition(targetPosition),\n    });\n\n    await this.checkPosition(targetPosition, envs[toIndex].position!);\n    return newDoc;\n  }\n\n  private async checkPosition(position: number, edge: number = 0) {\n    const precisionPosition = parseFloat(position.toPrecision(16));\n    if (\n      precisionPosition < minPosition ||\n      precisionPosition > maxPosition ||\n      Math.abs(precisionPosition - edge) < minPosition\n    ) {\n      const envs = await this.envs();\n      let position = initPosition;\n      for (const env of envs) {\n        position = position - stepPosition;\n        await this.updateDb({ id: env.id, position });\n      }\n    }\n  }\n\n  private getPrecisionPosition(position: number): number {\n    return parseFloat(position.toPrecision(16));\n  }\n\n  public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {\n    let condition = { ...query };\n    if (searchText) {\n      const encodeText = encodeURI(searchText);\n      const reg = {\n        [Op.or]: [\n          { [Op.like]: `%${searchText}%` },\n          { [Op.like]: `%${encodeText}%` },\n        ],\n      };\n\n      condition = {\n        ...condition,\n        [Op.or]: [\n          {\n            name: reg,\n          },\n          {\n            value: reg,\n          },\n          {\n            remarks: reg,\n          },\n        ],\n      };\n    }\n    try {\n      const result = await this.find(condition, [\n        [sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],\n        ['position', 'DESC'],\n        ['createdAt', 'ASC'],\n      ]);\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  private async find(query: any, sort: any = []): Promise<Env[]> {\n    const docs = await EnvModel.findAll({\n      where: { ...query },\n      order: [...sort],\n    });\n    return docs.map((x) => x.get({ plain: true }));\n  }\n\n  public async getDb(query: FindOptions<Env>['where']): Promise<Env> {\n    const doc: any = await EnvModel.findOne({ where: { ...query } });\n    if (!doc) {\n      throw new Error(`Env ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async disabled(ids: number[]) {\n    await EnvModel.update(\n      { status: EnvStatus.disabled },\n      { where: { id: ids } },\n    );\n    await this.set_envs();\n  }\n\n  public async enabled(ids: number[]) {\n    await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });\n    await this.set_envs();\n  }\n\n  public async updateNames({ ids, name }: { ids: number[]; name: string }) {\n    await EnvModel.update({ name }, { where: { id: ids } });\n    await this.set_envs();\n  }\n\n  public async pin(ids: number[]) {\n    await EnvModel.update({ isPinned: 1 }, { where: { id: ids } });\n  }\n\n  public async unPin(ids: number[]) {\n    await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });\n  }\n\n  public async set_envs() {\n    const envs = await this.envs('', {\n      name: { [Op.not]: null },\n      status: EnvStatus.normal,\n    });\n    const groups = groupBy(envs, 'name');\n    let env_string = '';\n    let js_env_string = '';\n    let py_env_string = 'import os\\n';\n    for (const key in groups) {\n      if (Object.prototype.hasOwnProperty.call(groups, key)) {\n        const group = groups[key];\n\n        // 忽略不符合bash要求的环境变量名称\n        if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {\n          let value = group\n            .map((x) => x.value)\n            .join('&')\n            .replace(/'/g, \"'\\\\''\")\n            .trim();\n          env_string += `export ${key}='${value}'\\n`;\n          const _env_value = `${group\n            .map((x) => x.value)\n            .join('&')\n            .replace(/\\\\/g, '\\\\\\\\')}`;\n          js_env_string += `process.env.${key}=\\`${_env_value.replace(\n            /\\`/g,\n            '\\\\`',\n          )}\\`;\\n`;\n          py_env_string += `os.environ['${key}']='''${_env_value.replace(\n            /\\'/g,\n            \"\\\\'\",\n          )}'''\\n`;\n        }\n      }\n    }\n    await writeFileWithLock(config.envFile, env_string);\n    await writeFileWithLock(config.jsEnvFile, js_env_string);\n    await writeFileWithLock(config.pyEnvFile, py_env_string);\n  }\n}\n"
  },
  {
    "path": "back/services/grpc.ts",
    "content": "import { Server, ServerCredentials } from '@grpc/grpc-js';\nimport { CronService } from '../protos/cron';\nimport { HealthService } from '../protos/health';\nimport { ApiService } from '../protos/api';\nimport { addCron } from '../schedule/addCron';\nimport { delCron } from '../schedule/delCron';\nimport { check } from '../schedule/health';\nimport * as Api from '../schedule/api';\nimport Logger from '../loaders/logger';\nimport { promisify } from 'util';\nimport config from '../config';\nimport { metricsService } from './metrics';\nimport { Service } from 'typedi';\n\n@Service()\nexport class GrpcServerService {\n  private server: Server = new Server({ 'grpc.enable_http_proxy': 0 });\n\n  async initialize() {\n    try {\n      this.server.addService(HealthService, { check });\n      this.server.addService(CronService, { addCron, delCron });\n      this.server.addService(ApiService, Api);\n\n      const grpcPort = config.grpcPort;\n      const bindAsync = promisify(this.server.bindAsync).bind(this.server);\n      await bindAsync(\n        `0.0.0.0:${grpcPort}`,\n        ServerCredentials.createInsecure(),\n      );\n      Logger.debug(`✌️ gRPC service started successfully`);\n\n      metricsService.record('grpc_service_start', 1, {\n        port: grpcPort.toString(),\n      });\n\n      return grpcPort;\n    } catch (err) {\n      Logger.error('Failed to start gRPC service:', err);\n      throw err;\n    }\n  }\n\n  async shutdown() {\n    try {\n      if (this.server) {\n        await new Promise((resolve) => {\n          this.server.tryShutdown(() => {\n            Logger.debug('gRPC service stopped');\n            metricsService.record('grpc_service_stop', 1);\n            resolve(null);\n          });\n        });\n      }\n    } catch (err) {\n      Logger.error('Error while shutting down gRPC service:', err);\n      throw err;\n    }\n  }\n\n  getServer() {\n    return this.server;\n  }\n}\n"
  },
  {
    "path": "back/services/health.ts",
    "content": "import { Service } from 'typedi';\nimport Logger from '../loaders/logger';\nimport { GrpcServerService } from './grpc';\nimport { HttpServerService } from './http';\n\ninterface HealthStatus {\n  status: 'ok' | 'error';\n  services: {\n    http: boolean;\n    grpc: boolean;\n  };\n  metrics: {\n    uptime: number;\n    memory: {\n      used: number;\n      total: number;\n    };\n  };\n}\n\n@Service()\nexport class HealthService {\n  private startTime = Date.now();\n\n  constructor(\n    private grpcServerService: GrpcServerService,\n    private httpServerService: HttpServerService,\n  ) {}\n\n  async check(): Promise<HealthStatus> {\n    const status: HealthStatus = {\n      status: 'ok',\n      services: {\n        http: true,\n        grpc: true,\n      },\n      metrics: {\n        uptime: Math.floor((Date.now() - this.startTime) / 1000),\n        memory: {\n          used: process.memoryUsage().heapUsed,\n          total: process.memoryUsage().heapTotal,\n        },\n      },\n    };\n\n    try {\n      const httpServer = this.httpServerService.getServer();\n      if (!httpServer) {\n        status.services.http = false;\n        status.status = 'error';\n      }\n    } catch (err) {\n      status.services.http = false;\n      status.status = 'error';\n      Logger.error('HTTP server check failed:', err);\n    }\n\n    try {\n      const grpcServer = this.grpcServerService.getServer();\n      if (!grpcServer) {\n        status.services.grpc = false;\n        status.status = 'error';\n      }\n    } catch (err) {\n      status.services.grpc = false;\n      status.status = 'error';\n      Logger.error('gRPC server check failed:', err);\n    }\n\n    return status;\n  }\n}\n"
  },
  {
    "path": "back/services/http.ts",
    "content": "import express from 'express';\nimport Logger from '../loaders/logger';\nimport { metricsService } from './metrics';\nimport { Service } from 'typedi';\nimport { Server } from 'http';\n\n@Service()\nexport class HttpServerService {\n  private server?: Server = undefined;\n\n  async initialize(expressApp: express.Application, port: number) {\n    try {\n      return new Promise((resolve, reject) => {\n        this.server = expressApp.listen(port, '0.0.0.0', () => {\n          Logger.debug(`✌️ HTTP service started successfully`);\n          metricsService.record('http_service_start', 1, {\n            port: port.toString(),\n          });\n          resolve(this.server);\n        });\n\n        this.server?.on('error', (err: Error) => {\n          Logger.error('Failed to start HTTP service:', err);\n          reject(err);\n        });\n      });\n    } catch (err) {\n      Logger.error('Failed to start HTTP service:', err);\n      throw err;\n    }\n  }\n\n  async shutdown() {\n    try {\n      if (this.server) {\n        await new Promise((resolve) => {\n          this.server?.close(() => {\n            Logger.debug('HTTP service stopped');\n            metricsService.record('http_service_stop', 1);\n            resolve(null);\n          });\n        });\n      }\n    } catch (err) {\n      Logger.error('Error while shutting down HTTP service:', err);\n      throw err;\n    }\n  }\n\n  getServer() {\n    return this.server;\n  }\n}\n"
  },
  {
    "path": "back/services/log.ts",
    "content": "import path from 'path';\nimport { Inject, Service } from 'typedi';\nimport winston from 'winston';\nimport config from '../config';\n\n@Service()\nexport default class LogService {\n  constructor(@Inject('logger') private logger: winston.Logger) {}\n\n  public checkFilePath(filePath: string, fileName: string) {\n    const finalPath = path.resolve(config.logPath, filePath, fileName);\n    return finalPath.startsWith(config.logPath) ? finalPath : '';\n  }\n}\n"
  },
  {
    "path": "back/services/metrics.ts",
    "content": "import { performance } from 'perf_hooks';\nimport Logger from '../loaders/logger';\n\ninterface Metric {\n  name: string;\n  value: number;\n  timestamp: number;\n  tags?: Record<string, string>;\n}\n\nclass MetricsService {\n  private metrics: Metric[] = [];\n  private static instance: MetricsService;\n\n  private constructor() {\n    // 定期清理旧数据\n    setInterval(() => {\n      const oneHourAgo = Date.now() - 3600000;\n      this.metrics = this.metrics.filter(m => m.timestamp > oneHourAgo);\n    }, 60000);\n  }\n\n  static getInstance(): MetricsService {\n    if (!MetricsService.instance) {\n      MetricsService.instance = new MetricsService();\n    }\n    return MetricsService.instance;\n  }\n\n  record(name: string, value: number, tags?: Record<string, string>) {\n    this.metrics.push({\n      name,\n      value,\n      timestamp: Date.now(),\n      tags,\n    });\n  }\n\n  measure(name: string, fn: () => void, tags?: Record<string, string>) {\n    const start = performance.now();\n    try {\n      fn();\n    } finally {\n      const duration = performance.now() - start;\n      this.record(name, duration, tags);\n    }\n  }\n\n  async measureAsync(name: string, fn: () => Promise<void>, tags?: Record<string, string>) {\n    const start = performance.now();\n    try {\n      await fn();\n    } finally {\n      const duration = performance.now() - start;\n      this.record(name, duration, tags);\n    }\n  }\n\n  getMetrics(name?: string, tags?: Record<string, string>) {\n    let filtered = this.metrics;\n    \n    if (name) {\n      filtered = filtered.filter(m => m.name === name);\n    }\n    \n    if (tags) {\n      filtered = filtered.filter(m => {\n        if (!m.tags) return false;\n        return Object.entries(tags).every(([key, value]) => m.tags![key] === value);\n      });\n    }\n\n    return {\n      count: filtered.length,\n      average: filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,\n      min: Math.min(...filtered.map(m => m.value)),\n      max: Math.max(...filtered.map(m => m.value)),\n      metrics: filtered,\n    };\n  }\n\n  report() {\n    const report = {\n      timestamp: Date.now(),\n      metrics: this.getMetrics(),\n    };\n    Logger.info('性能指标报告:', report);\n    return report;\n  }\n}\n\nexport const metricsService = MetricsService.getInstance(); "
  },
  {
    "path": "back/services/notify.ts",
    "content": "import crypto from 'crypto';\nimport nodemailer from 'nodemailer';\nimport { Inject, Service } from 'typedi';\nimport { parseBody, parseHeaders } from '../config/util';\nimport { NotificationInfo } from '../data/notify';\nimport UserService from './user';\nimport { httpClient } from '../config/http';\nimport { ProxyAgent } from 'undici';\n\n@Service()\nexport default class NotificationService {\n  @Inject((type) => UserService)\n  private userService!: UserService;\n\n  private modeMap = new Map([\n    ['gotify', this.gotify],\n    ['goCqHttpBot', this.goCqHttpBot],\n    ['serverChan', this.serverChan],\n    ['pushDeer', this.pushDeer],\n    ['chat', this.chat],\n    ['bark', this.bark],\n    ['telegramBot', this.telegramBot],\n    ['dingtalkBot', this.dingtalkBot],\n    ['weWorkBot', this.weWorkBot],\n    ['weWorkApp', this.weWorkApp],\n    ['aibotk', this.aibotk],\n    ['iGot', this.iGot],\n    ['pushPlus', this.pushPlus],\n    ['wePlusBot', this.wePlusBot],\n    ['email', this.email],\n    ['pushMe', this.pushMe],\n    ['webhook', this.webhook],\n    ['lark', this.lark],\n    ['chronocat', this.chronocat],\n    ['ntfy', this.ntfy],\n    ['wxPusherBot', this.wxPusherBot],\n  ]);\n\n  private title = '';\n  private content = '';\n  private params!: Omit<NotificationInfo, 'type'>;\n  private gotOption = {\n    timeout: 10000,\n    retry: 1,\n  };\n\n  constructor() {}\n\n  public async notify(\n    title: string,\n    content: string,\n    notificationInfo?: NotificationInfo,\n  ): Promise<boolean | undefined> {\n    let { type, ...rest } = await this.userService.getNotificationMode();\n    if (notificationInfo?.type) {\n      type = notificationInfo?.type;\n    }\n    if (type) {\n      this.title = title;\n      this.content = content;\n      let params = rest;\n      if (notificationInfo) {\n        const { type: _, ...others } = notificationInfo;\n        params = { ...rest, ...others };\n      }\n      this.params = params;\n      const notificationModeAction = this.modeMap.get(type);\n      try {\n        return await notificationModeAction?.call(this);\n      } catch (error: any) {\n        console.error(error);\n      }\n    }\n    return false;\n  }\n\n  public async testNotify(\n    info: NotificationInfo,\n    title: string,\n    content: string,\n  ) {\n    const { type, ...rest } = info;\n    if (type) {\n      this.title = title;\n      this.content = content;\n      this.params = rest;\n      const notificationModeAction = this.modeMap.get(type);\n      return await notificationModeAction?.call(this);\n    }\n    return true;\n  }\n\n  private async gotify() {\n    const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;\n    try {\n      const res = await httpClient.post(\n        `${gotifyUrl}/message?token=${gotifyToken}`,\n        {\n          ...this.gotOption,\n          body: `title=${encodeURIComponent(\n            this.title,\n          )}&message=${encodeURIComponent(\n            this.content,\n          )}&priority=${gotifyPriority}`,\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n          },\n        },\n      );\n      if (typeof res.id === 'number') {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async goCqHttpBot() {\n    const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;\n    try {\n      const res = await httpClient.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {\n        ...this.gotOption,\n        json: { message: `${this.title}\\n${this.content}` },\n        headers: { Authorization: 'Bearer ' + goCqHttpBotToken },\n      });\n      if (res.retcode === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async serverChan() {\n    const { serverChanKey } = this.params;\n    const matchResult = serverChanKey.match(/^sctp(\\d+)t/i);\n    const url =\n      matchResult && matchResult[1]\n        ? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`\n        : `https://sctapi.ftqq.com/${serverChanKey}.send`;\n\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        body: `title=${encodeURIComponent(\n          this.title,\n        )}&desp=${encodeURIComponent(this.content)}`,\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      });\n      if (res.errno === 0 || res.data.errno === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async pushDeer() {\n    const { pushDeerKey, pushDeerUrl } = this.params;\n    const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(\n          this.title,\n        )}&desp=${encodeURIComponent(this.content)}&type=markdown`,\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      });\n      if (\n        res.content.result.length !== undefined &&\n        res.content.result.length > 0\n      ) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async chat() {\n    const { synologyChatUrl } = this.params;\n    try {\n      const res = await httpClient.post(synologyChatUrl, {\n        ...this.gotOption,\n        body: `payload={\"text\":\"${this.title}\\n${this.content}\"}`,\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      });\n      if (res.success) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async bark() {\n    let {\n      barkPush,\n      barkIcon = '',\n      barkSound = '',\n      barkGroup = '',\n      barkLevel = '',\n      barkUrl = '',\n      barkArchive = '',\n    } = this.params;\n    if (!barkPush.startsWith('http')) {\n      barkPush = `https://api.day.app/${barkPush}`;\n    }\n    const url = `${barkPush}`;\n    const body = {\n      title: this.title,\n      body: this.content,\n      icon: barkIcon,\n      sound: barkSound,\n      group: barkGroup,\n      isArchive: barkArchive,\n      level: barkLevel,\n      url: barkUrl,\n    };\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        json: body,\n        headers: { 'Content-Type': 'application/json' },\n      });\n      if (res.code === 200) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async telegramBot() {\n    const {\n      telegramBotApiHost,\n      telegramBotProxyAuth,\n      telegramBotProxyHost,\n      telegramBotProxyPort,\n      telegramBotToken,\n      telegramBotUserId,\n    } = this.params;\n    const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';\n    const url = `${\n      telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'\n    }/bot${telegramBotToken}/sendMessage`;\n    let agent;\n    if (telegramBotProxyHost && telegramBotProxyPort) {\n      agent = new ProxyAgent({\n        uri: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,\n      });\n    }\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        body: `chat_id=${telegramBotUserId}&text=${this.title}\\n\\n${this.content}&disable_web_page_preview=true`,\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n        dispatcher: agent,\n      });\n      if (res.ok) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async dingtalkBot() {\n    const { dingtalkBotSecret, dingtalkBotToken } = this.params;\n    let secretParam = '';\n    if (dingtalkBotSecret) {\n      const dateNow = Date.now();\n      const hmac = crypto.createHmac('sha256', dingtalkBotSecret);\n      hmac.update(`${dateNow}\\n${dingtalkBotSecret}`);\n      const result = encodeURIComponent(hmac.digest('base64'));\n      secretParam = `&timestamp=${dateNow}&sign=${result}`;\n    }\n    const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        json: {\n          msgtype: 'text',\n          text: {\n            content: ` ${this.title}\\n\\n${this.content}`,\n          },\n        },\n      });\n      if (res.errcode === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async weWorkBot() {\n    const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =\n      this.params;\n    const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        json: {\n          msgtype: 'text',\n          text: {\n            content: ` ${this.title}\\n\\n${this.content}`,\n          },\n        },\n      });\n      if (res.errcode === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async weWorkApp() {\n    const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =\n      this.params;\n    const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =\n      weWorkAppKey.split(',');\n    const url = `${weWorkOrigin}/cgi-bin/gettoken`;\n    const tokenRes = await httpClient.post(url, {\n      ...this.gotOption,\n      json: {\n        corpid,\n        corpsecret,\n      },\n    });\n\n    let options: any = {\n      msgtype: 'mpnews',\n      mpnews: {\n        articles: [\n          {\n            title: `${this.title}`,\n            thumb_media_id,\n            author: `智能助手`,\n            content_source_url: ``,\n            content: `${this.content.replace(/\\n/g, '<br/>')}`,\n            digest: `${this.content}`,\n          },\n        ],\n      },\n    };\n    switch (thumb_media_id) {\n      case '0':\n        options = {\n          msgtype: 'textcard',\n          textcard: {\n            title: `${this.title}`,\n            description: `${this.content}`,\n            url: 'https://github.com/whyour/qinglong',\n            btntxt: '更多',\n          },\n        };\n        break;\n\n      case '1':\n        options = {\n          msgtype: 'text',\n          text: {\n            content: `${this.title}\\n\\n${this.content}`,\n          },\n        };\n        break;\n    }\n\n    try {\n      const res = await httpClient.post(\n        `${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,\n        {\n          ...this.gotOption,\n          json: {\n            touser,\n            agentid,\n            safe: '0',\n            ...options,\n          },\n        },\n      );\n\n      if (res.errcode === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async aibotk() {\n    const { aibotkKey, aibotkType, aibotkName } = this.params;\n    let url = '';\n    let json = {};\n    switch (aibotkType) {\n      case 'room':\n        url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';\n        json = {\n          apiKey: `${aibotkKey}`,\n          roomName: `${aibotkName}`,\n          message: {\n            type: 1,\n            content: `【青龙快讯】\\n\\n${this.title}\\n${this.content}`,\n          },\n        };\n        break;\n      case 'contact':\n        url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';\n        json = {\n          apiKey: `${aibotkKey}`,\n          name: `${aibotkName}`,\n          message: {\n            type: 1,\n            content: `【青龙快讯】\\n\\n${this.title}\\n${this.content}`,\n          },\n        };\n        break;\n    }\n\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        json: {\n          ...json,\n        },\n      });\n      if (res.code === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async iGot() {\n    const { iGotPushKey } = this.params;\n    const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        body: `title=${this.title}&content=${this.content}`,\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      });\n\n      if (res.ret === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async pushPlus() {\n    const {\n      pushPlusToken,\n      pushPlusUser,\n      pushplusWebhook,\n      pushPlusTemplate,\n      pushplusChannel,\n      pushplusCallbackUrl,\n      pushplusTo,\n    } = this.params;\n    const url = `https://www.pushplus.plus/send`;\n    try {\n      let body = {\n        ...this.gotOption,\n        json: {\n          token: `${pushPlusToken}`,\n          title: `${this.title}`,\n          content: `${this.content.replace(/[\\n\\r]/g, '<br>')}`,\n          topic: `${pushPlusUser || ''}`,\n          template: `${pushPlusTemplate || 'html'}`,\n          channel: `${pushplusChannel || 'wechat'}`,\n          webhook: `${pushplusWebhook || ''}`,\n          callbackUrl: `${pushplusCallbackUrl || ''}`,\n          to: `${pushplusTo || ''}`,\n        },\n      };\n\n      const res = await httpClient.post(url, body);\n\n      if (res.code === 200) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async wePlusBot() {\n    const { wePlusBotToken, wePlusBotReceiver, wePlusBotVersion } = this.params;\n\n    let content = this.content;\n    let template = 'txt';\n    if (this.content.length > 800) {\n      template = 'html';\n      content = content.replace(/[\\n\\r]/g, '<br>');\n    }\n\n    const url = `https://www.weplusbot.com/send`;\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        json: {\n          token: `${wePlusBotToken}`,\n          title: `${this.title}`,\n          template: `${template}`,\n          content: `${content}`,\n          receiver: `${wePlusBotReceiver || ''}`,\n          version: `${wePlusBotVersion || 'pro'}`,\n        },\n      });\n\n      if (res.code === 200) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async lark() {\n    let { larkKey, larkSecret } = this.params;\n\n    if (!larkKey.startsWith('http')) {\n      larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;\n    }\n\n    const body: Record<string, any> = {\n      msg_type: 'text',\n      content: { text: `${this.title}\\n\\n${this.content}` },\n    };\n\n    // Add signature if secret is provided\n    // Note: Feishu's signature algorithm uses timestamp+\"\\n\"+secret as the HMAC key\n    // and signs an empty message, which differs from typical HMAC usage\n    if (larkSecret) {\n      const timestamp = Math.floor(Date.now() / 1000).toString();\n      const stringToSign = `${timestamp}\\n${larkSecret}`;\n      const hmac = crypto.createHmac('sha256', stringToSign);\n      const sign = hmac.digest('base64');\n      body.timestamp = timestamp;\n      body.sign = sign;\n    }\n\n    try {\n      const res = await httpClient.post(larkKey, {\n        ...this.gotOption,\n        json: body,\n        headers: { 'Content-Type': 'application/json' },\n      });\n      if (res.StatusCode === 0 || res.code === 0) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async email() {\n    const { emailPass, emailService, emailUser, emailTo } = this.params;\n\n    try {\n      const transporter = nodemailer.createTransport({\n        service: emailService,\n        auth: {\n          user: emailUser,\n          pass: emailPass,\n        },\n      });\n\n      const info = await transporter.sendMail({\n        from: `\"青龙快讯\" <${emailUser}>`,\n        to: emailTo ? emailTo.split(';') : emailUser,\n        subject: `${this.title}`,\n        html: `${this.content.replace(/\\n/g, '<br/>')}`,\n      });\n\n      transporter.close();\n\n      if (info.messageId) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(info));\n      }\n    } catch (error: any) {\n      throw error;\n    }\n  }\n\n  private async pushMe() {\n    const { pushMeKey, pushMeUrl } = this.params;\n    try {\n      const res = await httpClient.post<'text'>(\n        pushMeUrl || 'https://push.i-i.me/',\n        {\n          ...this.gotOption,\n          json: {\n            push_key: pushMeKey,\n            title: this.title,\n            content: this.content,\n          },\n          headers: { 'Content-Type': 'application/json' },\n        },\n      );\n      if (res === 'success') {\n        return true;\n      } else {\n        throw new Error(res);\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async ntfy() {\n    const {\n      ntfyUrl,\n      ntfyTopic,\n      ntfyPriority,\n      ntfyToken,\n      ntfyUsername,\n      ntfyPassword,\n      ntfyActions,\n    } = this.params;\n    // 编码函数\n    const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {\n      const encodedText = Buffer.from(text).toString('base64');\n      return `=?${charset}?B?${encodedText}?=`;\n    };\n    try {\n      const headers: Record<string, string> = {\n        Title: encodeRfc2047(this.title),\n        Priority: `${ntfyPriority || '3'}`,\n        Icon: 'https://qn.whyour.cn/logo.png',\n      };\n      if (ntfyToken) {\n        headers['Authorization'] = `Bearer ${ntfyToken}`;\n      } else if (ntfyUsername && ntfyPassword) {\n        headers['Authorization'] = `Basic ${Buffer.from(\n          `${ntfyUsername}:${ntfyPassword}`,\n        ).toString('base64')}`;\n      }\n      if (ntfyActions) {\n        headers['Actions'] = encodeRfc2047(ntfyActions);\n      }\n      const res = await httpClient.request(\n        `${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,\n        {\n          ...this.gotOption,\n          body: `${this.content}`,\n          headers: headers,\n          method: 'POST',\n        },\n      );\n      if (res.statusCode === 200) {\n        return true;\n      } else {\n        throw new Error(await res.body.text());\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async wxPusherBot() {\n    const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =\n      this.params;\n    // 处理 topicIds，将分号分隔的字符串转为数组\n    const topicIds = wxPusherBotTopicIds\n      ? wxPusherBotTopicIds\n          .split(';')\n          .map((id) => id.trim())\n          .filter((id) => id)\n          .map((id) => parseInt(id))\n      : [];\n\n    // 处理 uids，将分号分隔的字符串转为数组\n    const uids = wxPusherBotUids\n      ? wxPusherBotUids\n          .split(';')\n          .map((uid) => uid.trim())\n          .filter((uid) => uid)\n      : [];\n\n    // topic_ids 和 uids 至少要有一个\n    if (!topicIds.length && !uids.length) {\n      throw new Error('wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行');\n    }\n\n    const url = `https://wxpusher.zjiecode.com/api/send/message`;\n    try {\n      const res = await httpClient.post(url, {\n        ...this.gotOption,\n        json: {\n          appToken: wxPusherBotAppToken,\n          content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,\n          summary: this.title,\n          contentType: 2,\n          topicIds: topicIds,\n          uids: uids,\n          verifyPayType: 0,\n        },\n      });\n\n      if (res.code === 1000) {\n        return true;\n      } else {\n        throw new Error(JSON.stringify(res));\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async chronocat() {\n    const { chronocatURL, chronocatQQ, chronocatToken } = this.params;\n    try {\n      const user_ids = chronocatQQ\n        .match(/user_id=(\\d+)/g)\n        ?.map((match: any) => match.split('=')[1]);\n      const group_ids = chronocatQQ\n        .match(/group_id=(\\d+)/g)\n        ?.map((match: any) => match.split('=')[1]);\n\n      const url = `${chronocatURL}/api/message/send`;\n      const headers = {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${chronocatToken}`,\n      };\n\n      for (const [chat_type, ids] of [\n        [1, user_ids],\n        [2, group_ids],\n      ]) {\n        if (!ids) {\n          continue;\n        }\n        let _ids: any = ids;\n        for (const chat_id of _ids) {\n          const data = {\n            peer: {\n              chatType: chat_type,\n              peerUin: chat_id,\n            },\n            elements: [\n              {\n                elementType: 1,\n                textElement: {\n                  content: `${this.title}\\n\\n${this.content}`,\n                },\n              },\n            ],\n          };\n          const res = await httpClient.request(url, {\n            ...this.gotOption,\n            json: data,\n            headers,\n            method: 'POST',\n          });\n          if (res.statusCode === 200) {\n            return true;\n          } else {\n            throw new Error(await res.body.text());\n          }\n        }\n      }\n      return false;\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private async webhook() {\n    const {\n      webhookUrl,\n      webhookBody,\n      webhookHeaders,\n      webhookMethod,\n      webhookContentType,\n    } = this.params;\n\n    if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) {\n      throw new Error('Url 或者 Body 中必须包含 $title');\n    }\n\n    const headers = parseHeaders(webhookHeaders);\n    const body = parseBody(webhookBody, webhookContentType, (v) =>\n      v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),\n    );\n    const bodyParam = this.formatBody(webhookContentType, body);\n    const options = {\n      method: webhookMethod,\n      headers,\n      ...this.gotOption,\n      allowGetBody: true,\n      ...bodyParam,\n    };\n\n    try {\n      const formatUrl = webhookUrl\n        ?.replaceAll('$title', encodeURIComponent(this.title))\n        ?.replaceAll('$content', encodeURIComponent(this.content));\n      const res = await httpClient.request(formatUrl, options);\n      const text = await res.body.text();\n      if (String(res.statusCode).startsWith('20')) {\n        return true;\n      } else {\n        throw new Error(await res.body.text());\n      }\n    } catch (error: any) {\n      throw new Error(error.response ? error.response.body : error);\n    }\n  }\n\n  private formatBody(contentType: string, body: any): object {\n    if (!body) return {};\n    switch (contentType) {\n      case 'application/json':\n        return { json: body };\n      case 'multipart/form-data':\n        return { form: body };\n      case 'application/x-www-form-urlencoded':\n      case 'text/plain':\n        return { body };\n    }\n    return {};\n  }\n}\n"
  },
  {
    "path": "back/services/open.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport { createRandomString } from '../config/util';\nimport { App, AppModel } from '../data/open';\nimport { v4 as uuidV4 } from 'uuid';\nimport sequelize, { Op } from 'sequelize';\nimport { shareStore } from '../shared/store';\n\n@Service()\nexport default class OpenService {\n  constructor(@Inject('logger') private logger: winston.Logger) {}\n\n  public async findApps(): Promise<App[] | null> {\n    const docs = await this.find({});\n    return docs;\n  }\n\n  public async create(payload: App): Promise<App> {\n    const tab = { ...payload };\n    tab.client_id = createRandomString(12, 12);\n    tab.client_secret = createRandomString(24, 24);\n    const doc = await this.insert(tab);\n    const apps = await this.find({});\n    await shareStore.updateApps(apps);\n    return { ...doc, tokens: [] };\n  }\n\n  public async insert(payload: App): Promise<App> {\n    const doc = await AppModel.create(payload, { returning: true });\n    return doc.get({ plain: true });\n  }\n\n  public async update(payload: App): Promise<App> {\n    const newDoc = await this.updateDb({\n      name: payload.name,\n      scopes: payload.scopes,\n      id: payload.id,\n    } as App);\n    return { ...newDoc, tokens: [] };\n  }\n\n  private async updateDb(payload: Partial<App>): Promise<App> {\n    await AppModel.update(payload, { where: { id: payload.id } });\n    const apps = await this.find({});\n    await shareStore.updateApps(apps);\n    return apps?.find((x) => x.id === payload.id) as App;\n  }\n\n  public async getDb(query: Record<string, any>): Promise<App> {\n    const doc = await AppModel.findOne({ where: query });\n    if (!doc) {\n      throw new Error(`App ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async remove(ids: number[]) {\n    await AppModel.destroy({ where: { id: ids } });\n    const apps = await this.find({});\n    await shareStore.updateApps(apps);\n  }\n\n  public async resetSecret(id: number): Promise<App> {\n    const tab: Partial<App> = {\n      client_secret: createRandomString(24, 24),\n      tokens: [],\n      id,\n    };\n    // const doc = await this.get(id);\n    // const tab = new App({ ...doc });\n    // tab.client_secret = createRandomString(24, 24);\n    // tab.tokens = [];\n    // const newDoc = await this.updateDb(tab);\n    // return newDoc;\n    const newDoc = await this.updateDb(tab);\n    return newDoc;\n  }\n\n  public async list(\n    searchText: string = '',\n    sort: any = {},\n    query: Record<string, any> = {},\n  ): Promise<App[]> {\n    let condition = { ...query };\n    if (searchText) {\n      const encodeText = encodeURI(searchText);\n      const reg = {\n        [Op.or]: [\n          { [Op.like]: `%${searchText}%` },\n          { [Op.like]: `%${encodeText}%` },\n        ],\n      };\n\n      condition = {\n        ...condition,\n        name: reg,\n      };\n    }\n    try {\n      const result = await this.find(condition);\n      return result\n        .filter((x) => x.name !== 'system')\n        .map((x) => ({ ...x, tokens: [] }));\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  private async find(query: Record<string, any>, sort?: any): Promise<App[]> {\n    const docs = await AppModel.findAll({ where: { ...query } });\n    return docs.map((x) => x.get({ plain: true }));\n  }\n\n  public async authToken({\n    client_id,\n    client_secret,\n  }: {\n    client_id: string;\n    client_secret: string;\n  }): Promise<any> {\n    let token = uuidV4();\n    const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天\n    const doc = await AppModel.findOne({ where: { client_id, client_secret } });\n    if (doc) {\n      const timestamp = Math.round(Date.now() / 1000);\n      const invalidTokens = (doc.tokens || []).filter(\n        (x) => x.expiration >= timestamp,\n      );\n      let tokens = invalidTokens;\n      if (invalidTokens.length >= 5) {\n        tokens = [\n          ...invalidTokens.slice(0, 4),\n          { ...invalidTokens[4], expiration },\n        ];\n        token = invalidTokens[4].value;\n      } else {\n        tokens = [...invalidTokens, { value: token, expiration }];\n      }\n      await AppModel.update(\n        { tokens },\n        { where: { client_id, client_secret } },\n      );\n      const apps = await this.find({});\n      await shareStore.updateApps(apps);\n      return {\n        code: 200,\n        data: {\n          token,\n          token_type: 'Bearer',\n          expiration,\n        },\n      };\n    } else {\n      return { code: 400, message: 'client_id 或 client_seret 有误' };\n    }\n  }\n\n  public async generateSystemToken(): Promise<{\n    value: string;\n    expiration: number;\n  }> {\n    const apps = await shareStore.getApps();\n    const systemApp = apps?.find((x) => x.name === 'system');\n    if (!systemApp) {\n      throw new Error('system app not found');\n    }\n    const now = Math.round(Date.now() / 1000);\n    const currentToken = systemApp.tokens?.find((x) => x.expiration > now);\n    if (currentToken) {\n      return currentToken;\n    }\n    const { data } = await this.authToken({\n      client_id: systemApp.client_id,\n      client_secret: systemApp.client_secret,\n    });\n    return {\n      ...data,\n      value: data.token,\n    };\n  }\n}\n"
  },
  {
    "path": "back/services/schedule.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport nodeSchedule from 'node-schedule';\nimport { ChildProcessWithoutNullStreams } from 'child_process';\nimport {\n  ToadScheduler,\n  LongIntervalJob,\n  SimpleIntervalSchedule,\n  Task,\n} from 'toad-scheduler';\nimport dayjs from 'dayjs';\nimport taskLimit from '../shared/pLimit';\nimport { spawn } from 'cross-spawn';\n\nexport interface ScheduleTaskType {\n  id?: number;\n  command: string;\n  name?: string;\n  schedule?: string;\n  runOrigin: 'subscription' | 'system' | 'script';\n}\n\nexport interface TaskCallbacks {\n  onBefore?: (startTime: dayjs.Dayjs) => Promise<void>;\n  onStart?: (\n    cp: ChildProcessWithoutNullStreams,\n    startTime: dayjs.Dayjs,\n  ) => Promise<void>;\n  onEnd?: (\n    cp: ChildProcessWithoutNullStreams,\n    endTime: dayjs.Dayjs,\n    diff: number,\n  ) => Promise<void>;\n  onLog?: (message: string) => Promise<void>;\n  onError?: (message: string) => Promise<void>;\n}\n\n@Service()\nexport default class ScheduleService {\n  private scheduleStacks = new Map<string, nodeSchedule.Job>();\n\n  private intervalSchedule = new ToadScheduler();\n\n  private taskLimitMap = {\n    system: 'runWithSystemLimit' as const,\n    script: 'runWithScriptLimit' as const,\n    subscription: 'runWithSubscriptionLimit' as const,\n  };\n\n  constructor(@Inject('logger') private logger: winston.Logger) {}\n\n  async runTask(\n    command: string,\n    callbacks: TaskCallbacks = {},\n    params: {\n      schedule?: string;\n      name?: string;\n      command?: string;\n      id: string;\n      runOrigin: 'subscription' | 'system' | 'script';\n    },\n    completionTime: 'start' | 'end' = 'end',\n  ) {\n    const { runOrigin, ...others } = params;\n\n    return taskLimit[this.taskLimitMap[runOrigin]](others, () => {\n      return new Promise(async (resolve, reject) => {\n        this.logger.info(\n          `[panel][开始执行任务] 参数: ${JSON.stringify({\n            ...others,\n            command,\n          })}`,\n        );\n\n        try {\n          const startTime = dayjs();\n          await callbacks.onBefore?.(startTime);\n\n          const cp = spawn(command, { shell: '/bin/bash' });\n\n          callbacks.onStart?.(cp, startTime);\n          completionTime === 'start' && resolve(cp.pid);\n\n          cp.stdout.on('data', async (data) => {\n            await callbacks.onLog?.(data.toString());\n          });\n\n          cp.stderr.on('data', async (data) => {\n            this.logger.info(\n              '[panel][执行任务失败] 命令: %s, 错误信息: %j',\n              command,\n              data.toString(),\n            );\n            await callbacks.onError?.(data.toString());\n          });\n\n          cp.on('error', async (err) => {\n            this.logger.error(\n              '[panel][创建任务失败] 命令: %s, 错误信息: %j',\n              command,\n              err,\n            );\n            await callbacks.onError?.(JSON.stringify(err));\n          });\n\n          cp.on('exit', async (code) => {\n            this.logger.info(\n              '[panel][执行任务结束] 参数: %s, 退出码: %j',\n              JSON.stringify({\n                ...others,\n                command,\n              }),\n              code,\n            );\n            const endTime = dayjs();\n            await callbacks.onEnd?.(\n              cp,\n              endTime,\n              endTime.diff(startTime, 'seconds'),\n            );\n            resolve({ ...others, pid: cp.pid, code });\n          });\n        } catch (error) {\n          this.logger.error(\n            '[panel][执行任务失败] 命令: %s, 错误信息: %j',\n            command,\n            error,\n          );\n          await callbacks.onError?.(JSON.stringify(error));\n        }\n      });\n    });\n  }\n\n  async createCronTask(\n    { id = 0, command, name, schedule = '', runOrigin }: ScheduleTaskType,\n    callbacks?: TaskCallbacks,\n    runImmediately = false,\n  ) {\n    const _id = this.formatId(id);\n    this.logger.info(\n      '[panel][创建cron任务] 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',\n      _id,\n      schedule,\n      name,\n      command,\n    );\n\n    this.scheduleStacks.set(\n      _id,\n      nodeSchedule.scheduleJob(_id, schedule, async () => {\n        this.runTask(command, callbacks, {\n          name,\n          schedule,\n          command,\n          id: _id,\n          runOrigin,\n        });\n      }),\n    );\n\n    if (runImmediately) {\n      this.runTask(command, callbacks, {\n        name,\n        schedule,\n        command,\n        id: _id,\n        runOrigin,\n      });\n    }\n  }\n\n  async cancelCronTask({ id = 0, name }: ScheduleTaskType) {\n    const _id = this.formatId(id);\n    this.logger.info('[panel][取消定时任务] 任务名: %s', name);\n    if (this.scheduleStacks.has(_id)) {\n      this.scheduleStacks.get(_id)?.cancel();\n      this.scheduleStacks.delete(_id);\n    }\n  }\n\n  async createIntervalTask(\n    { id = 0, command, name = '', runOrigin }: ScheduleTaskType,\n    schedule: SimpleIntervalSchedule,\n    runImmediately = true,\n    callbacks?: TaskCallbacks,\n  ) {\n    const _id = this.formatId(id);\n    this.logger.info(\n      '[panel][创建interval任务] 任务ID: %s, 任务名: %s, 执行命令: %s',\n      _id,\n      name,\n      command,\n    );\n    const task = new Task(\n      name,\n      () => {\n        this.runTask(command, callbacks, {\n          name,\n          command,\n          id: _id,\n          runOrigin,\n        });\n      },\n      (err) => {\n        this.logger.error(\n          '[panel][执行任务失败] 命令: %s, 错误信息: %j',\n          command,\n          err,\n        );\n      },\n    );\n\n    const job = new LongIntervalJob(\n      { runImmediately: false, ...schedule },\n      task,\n      { id: _id },\n    );\n\n    this.intervalSchedule.addIntervalJob(job);\n\n    if (runImmediately) {\n      this.runTask(command, callbacks, {\n        name,\n        command,\n        id: _id,\n        runOrigin,\n      });\n    }\n  }\n\n  async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {\n    const _id = this.formatId(id);\n    this.logger.info(\n      '[panel][取消interval任务] 任务ID: %s, 任务名: %s',\n      _id,\n      name,\n    );\n    this.intervalSchedule.removeById(_id);\n  }\n\n  private formatId(id: number): string {\n    return String(id);\n  }\n}\n"
  },
  {
    "path": "back/services/script.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport path, { join } from 'path';\nimport SockService from './sock';\nimport CronService from './cron';\nimport ScheduleService, { TaskCallbacks } from './schedule';\nimport config from '../config';\nimport { TASK_COMMAND } from '../config/const';\nimport { getFileContentByName, getPid, killTask, rmPath } from '../config/util';\nimport taskLimit from '../shared/pLimit';\n\n@Service()\nexport default class ScriptService {\n  constructor(\n    @Inject('logger') private logger: winston.Logger,\n    private sockService: SockService,\n    private cronService: CronService,\n    private scheduleService: ScheduleService,\n  ) {}\n\n  private taskCallbacks(filePath: string): TaskCallbacks {\n    return {\n      onEnd: async (cp, endTime, diff) => {\n        await rmPath(filePath);\n      },\n      onError: async (message: string) => {\n        this.sockService.sendMessage({\n          type: 'manuallyRunScript',\n          message,\n        });\n      },\n      onLog: async (message: string) => {\n        this.sockService.sendMessage({\n          type: 'manuallyRunScript',\n          message,\n        });\n      },\n    };\n  }\n\n  public async runScript(filePath: string) {\n    const relativePath = path.relative(config.scriptPath, filePath);\n    const command = `${TASK_COMMAND} ${relativePath} now`;\n    const pid = await this.scheduleService.runTask(\n      `real_time=true ${command}`,\n      this.taskCallbacks(filePath),\n      { command, id: relativePath.replace(/ /g, '-'), runOrigin: 'script' },\n      'start',\n    );\n\n    return { code: 200, data: pid };\n  }\n\n  public async stopScript(filePath: string, pid: number) {\n    if (!pid) {\n      const relativePath = path.relative(config.scriptPath, filePath);\n      taskLimit.removeQueuedCron(relativePath.replace(/ /g, '-'));\n      pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;\n    }\n    try {\n      await killTask(pid);\n    } catch (error) {}\n\n    return { code: 200 };\n  }\n\n  public checkFilePath(filePath: string, fileName: string) {\n    const finalPath = path.resolve(config.scriptPath, filePath, fileName);\n    return finalPath.startsWith(config.scriptPath) ? finalPath : '';\n  }\n\n  public async getFile(filePath: string, fileName: string) {\n    const finalPath = this.checkFilePath(filePath, fileName);\n\n    if (!finalPath) {\n      return '';\n    }\n\n    const content = await getFileContentByName(finalPath);\n    return content;\n  }\n}\n"
  },
  {
    "path": "back/services/sock.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport { Connection } from 'sockjs';\nimport { SockMessage } from '../data/sock';\n\n@Service()\nexport default class SockService {\n  private clients: Connection[] = [];\n\n  constructor(@Inject('logger') private logger: winston.Logger) { }\n\n  public getClients() {\n    return this.clients;\n  }\n\n  public addClient(conn: Connection) {\n    if (this.clients.indexOf(conn) === -1) {\n      this.clients.push(conn);\n    }\n  }\n\n  public removeClient(conn: Connection) {\n    const index = this.clients.indexOf(conn);\n    if (index !== -1) {\n      this.clients.splice(index, 1);\n    }\n  }\n\n  public sendMessage(msg: SockMessage) {\n    this.clients.forEach((x) => {\n      x.write(JSON.stringify(msg));\n    });\n  }\n}\n"
  },
  {
    "path": "back/services/sshKey.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport fs from 'fs/promises';\nimport os from 'os';\nimport path from 'path';\nimport { Subscription } from '../data/subscription';\nimport { formatUrl } from '../config/subscription';\nimport config from '../config';\nimport { fileExist, rmPath } from '../config/util';\nimport { writeFileWithLock } from '../shared/utils';\n\n@Service()\nexport default class SshKeyService {\n  private homedir = os.homedir();\n  private sshPath = config.sshdPath;\n  private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');\n  private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;\n\n  constructor(@Inject('logger') private logger: winston.Logger) {\n    this.initSshConfigFile();\n  }\n\n  private async initSshConfigFile() {\n    let config = '';\n    const _exist = await fileExist(this.sshConfigFilePath);\n    if (_exist) {\n      config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });\n    } else {\n      await writeFileWithLock(this.sshConfigFilePath, '', { mode: '600' });\n    }\n    if (!config.includes(this.sshConfigHeader)) {\n      await writeFileWithLock(\n        this.sshConfigFilePath,\n        `${this.sshConfigHeader}\\n\\n${config}`,\n        { mode: '600' },\n      );\n    }\n  }\n\n  private async generatePrivateKeyFile(\n    alias: string,\n    key: string,\n  ): Promise<void> {\n    try {\n      await writeFileWithLock(\n        path.join(this.sshPath, alias),\n        `${key}${os.EOL}`,\n        {\n          mode: '400',\n        },\n      );\n    } catch (error) {\n      this.logger.error('生成私钥文件失败', error);\n    }\n  }\n\n  private async removePrivateKeyFile(alias: string): Promise<void> {\n    try {\n      const filePath = path.join(this.sshPath, alias);\n      await rmPath(filePath);\n    } catch (error) {\n      this.logger.error('删除私钥文件失败', error);\n    }\n  }\n\n  private async generateSingleSshConfig(\n    alias: string,\n    host: string,\n    proxy?: string,\n  ) {\n    if (host === 'github.com') {\n      host = `ssh.github.com\\n    Port 443\\n    HostkeyAlgorithms +ssh-rsa`;\n    }\n    const proxyStr = proxy\n      ? `    ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\\n`\n      : '';\n    const config = `Host ${alias}\\n    Hostname ${host}\\n    IdentityFile ${path.join(\n      this.sshPath,\n      alias,\n    )}\\n    StrictHostKeyChecking no\\n${proxyStr}`;\n    await writeFileWithLock(\n      `${path.join(this.sshPath, `${alias}.config`)}`,\n      config,\n      {\n        encoding: 'utf8',\n        mode: '600',\n      },\n    );\n  }\n\n  private async removeSshConfig(alias: string) {\n    try {\n      const filePath = path.join(this.sshPath, `${alias}.config`);\n      await rmPath(filePath);\n    } catch (error) {\n      this.logger.error(`删除ssh配置文件${alias}失败`, error);\n    }\n  }\n\n  public async addSSHKey(\n    key: string,\n    alias: string,\n    host: string,\n    proxy?: string,\n  ): Promise<void> {\n    await this.generatePrivateKeyFile(alias, key);\n    await this.generateSingleSshConfig(alias, host, proxy);\n  }\n\n  public async removeSSHKey(\n    alias: string,\n    host: string,\n    proxy?: string,\n  ): Promise<void> {\n    await this.removePrivateKeyFile(alias);\n    await this.removeSshConfig(alias);\n  }\n\n  public async setSshConfig(docs: Subscription[]) {\n    for (const doc of docs) {\n      if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {\n        const { alias, proxy } = doc;\n        const { host } = formatUrl(doc);\n        await this.removePrivateKeyFile(alias);\n        await this.removeSshConfig(alias);\n        await this.generatePrivateKeyFile(\n          alias,\n          (doc.pull_option as any).private_key,\n        );\n        await this.generateSingleSshConfig(alias, host, proxy);\n      }\n    }\n  }\n\n  public async addGlobalSSHKey(key: string, alias: string): Promise<void> {\n    await this.generatePrivateKeyFile(`~global_${alias}`, key);\n    // Create a global SSH config entry that matches all hosts\n    // This allows the key to be used for any Git repository\n    await this.generateGlobalSshConfig(`~global_${alias}`);\n  }\n\n  public async removeGlobalSSHKey(alias: string): Promise<void> {\n    await this.removePrivateKeyFile(`~global_${alias}`);\n    await this.removeSshConfig(`~global_${alias}`);\n  }\n\n  private async generateGlobalSshConfig(alias: string) {\n    // Create a config that matches all hosts, making this key globally available\n    const config = `Host *\\n    IdentityFile ${path.join(\n      this.sshPath,\n      alias,\n    )}\\n    StrictHostKeyChecking no\\n`;\n    await writeFileWithLock(\n      `${path.join(this.sshPath, `${alias}.config`)}`,\n      config,\n      {\n        encoding: 'utf8',\n        mode: '600',\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "back/services/subscription.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport config from '../config';\nimport {\n  Subscription,\n  SubscriptionInstance,\n  SubscriptionModel,\n  SubscriptionStatus,\n} from '../data/subscription';\nimport { ChildProcessWithoutNullStreams } from 'child_process';\nimport {\n  getFileContentByName,\n  concurrentRun,\n  fileExist,\n  createFile,\n  killTask,\n  handleLogPath,\n  promiseExec,\n  rmPath,\n} from '../config/util';\nimport fs from 'fs/promises';\nimport { FindOptions, Op } from 'sequelize';\nimport path, { join } from 'path';\nimport ScheduleService, { TaskCallbacks } from './schedule';\nimport { SimpleIntervalSchedule } from 'toad-scheduler';\nimport SockService from './sock';\nimport SshKeyService from './sshKey';\nimport dayjs from 'dayjs';\nimport { LOG_END_SYMBOL } from '../config/const';\nimport { formatCommand, formatUrl } from '../config/subscription';\nimport { CrontabModel } from '../data/cron';\nimport CrontabService from './cron';\nimport taskLimit from '../shared/pLimit';\nimport { logStreamManager } from '../shared/logStreamManager';\n\n@Service()\nexport default class SubscriptionService {\n  constructor(\n    @Inject('logger') private logger: winston.Logger,\n    private scheduleService: ScheduleService,\n    private sockService: SockService,\n    private sshKeyService: SshKeyService,\n    private crontabService: CrontabService,\n  ) {}\n\n  public async list(\n    searchText?: string,\n    ids?: string,\n  ): Promise<SubscriptionInstance[]> {\n    let query = {};\n    const subIds = JSON.parse(ids || '[]');\n    if (searchText) {\n      const reg = {\n        [Op.or]: [\n          { [Op.like]: `%${searchText}%` },\n          { [Op.like]: `%${encodeURI(searchText)}%` },\n        ],\n      };\n      query = {\n        [Op.or]: [\n          {\n            name: reg,\n          },\n          {\n            url: reg,\n          },\n        ],\n      };\n    }\n    try {\n      const result = await SubscriptionModel.findAll({\n        where: { ...query, ...(ids ? { id: subIds } : undefined) },\n        order: [\n          ['is_disabled', 'ASC'],\n          ['createdAt', 'DESC'],\n        ],\n      });\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  public async handleTask(\n    doc: Subscription,\n    needCreate = true,\n    runImmediately = false,\n  ) {\n    const { url } = formatUrl(doc);\n\n    doc.command = formatCommand(doc, url as string);\n\n    if (doc.schedule_type === 'crontab') {\n      this.scheduleService.cancelCronTask(doc as any);\n      needCreate &&\n        (await this.scheduleService.createCronTask(\n          { ...doc, runOrigin: 'subscription' } as any,\n          this.taskCallbacks(doc),\n          runImmediately,\n        ));\n    } else if (doc.interval_schedule) {\n      this.scheduleService.cancelIntervalTask(doc as any);\n      const { type, value } = doc.interval_schedule;\n      needCreate &&\n        (await this.scheduleService.createIntervalTask(\n          { ...doc, runOrigin: 'subscription' } as any,\n          { [type]: value } as SimpleIntervalSchedule,\n          runImmediately,\n          this.taskCallbacks(doc),\n        ));\n    }\n  }\n\n  public async setSshConfig() {\n    const docs = await SubscriptionModel.findAll();\n    await this.sshKeyService.setSshConfig(docs);\n  }\n\n  private taskCallbacks(doc: Subscription): TaskCallbacks {\n    return {\n      onBefore: async (startTime) => {\n        const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');\n        const logPath = `${doc.alias}/${logTime}.log`;\n        await SubscriptionModel.update(\n          {\n            status: SubscriptionStatus.running,\n            log_path: logPath,\n          },\n          { where: { id: doc.id } },\n        );\n        const absolutePath = await handleLogPath(\n          logPath as string,\n          `## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\\n`,\n        );\n\n        // 执行sub_before\n        let beforeStr = '';\n        try {\n          if (doc.sub_before) {\n            await logStreamManager.write(absolutePath, `\\n## 执行before命令...\\n\\n`);\n            beforeStr = await promiseExec(doc.sub_before);\n          }\n        } catch (error: any) {\n          beforeStr =\n            (error.stderr && error.stderr.toString()) || JSON.stringify(error);\n        }\n        if (beforeStr) {\n          await logStreamManager.write(absolutePath, `${beforeStr}\\n`);\n        }\n      },\n      onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {\n        await SubscriptionModel.update(\n          {\n            pid: cp.pid,\n          },\n          { where: { id: doc.id } },\n        );\n      },\n      onEnd: async (cp, endTime, diff) => {\n        const sub = await this.getDb({ id: doc.id });\n        const absolutePath = await handleLogPath(sub.log_path as string);\n\n        // 执行 sub_after\n        let afterStr = '';\n        try {\n          if (sub.sub_after) {\n            await logStreamManager.write(absolutePath, `\\n\\n## 执行after命令...\\n\\n`);\n            afterStr = await promiseExec(sub.sub_after);\n          }\n        } catch (error: any) {\n          afterStr =\n            (error.stderr && error.stderr.toString()) || JSON.stringify(error);\n        }\n        if (afterStr) {\n          await logStreamManager.write(absolutePath, `${afterStr}\\n`);\n        }\n\n        await logStreamManager.write(\n          absolutePath,\n          `\\n## 执行结束... ${endTime.format(\n            'YYYY-MM-DD HH:mm:ss',\n          )}  耗时 ${diff} 秒${LOG_END_SYMBOL}`,\n        );\n\n        // Close the stream after task completion\n        await logStreamManager.closeStream(absolutePath);\n\n        await SubscriptionModel.update(\n          { status: SubscriptionStatus.idle, pid: undefined },\n          { where: { id: sub.id } },\n        );\n\n        this.sockService.sendMessage({\n          type: 'runSubscriptionEnd',\n          message: '订阅执行完成',\n          references: [doc.id as number],\n        });\n      },\n      onError: async (message: string) => {\n        const sub = await this.getDb({ id: doc.id });\n        const absolutePath = await handleLogPath(sub.log_path as string);\n        await logStreamManager.write(absolutePath, `\\n${message}`);\n      },\n      onLog: async (message: string) => {\n        const sub = await this.getDb({ id: doc.id });\n        const absolutePath = await handleLogPath(sub.log_path as string);\n        await logStreamManager.write(absolutePath, `\\n${message}`);\n      },\n    };\n  }\n\n  public async create(payload: Subscription): Promise<Subscription> {\n    const tab = new Subscription(payload);\n    const doc = await this.insert(tab);\n    await this.handleTask(doc.get({ plain: true }));\n    await this.setSshConfig();\n    return doc;\n  }\n\n  public async insert(payload: Subscription): Promise<SubscriptionInstance> {\n    return await SubscriptionModel.create(payload, { returning: true });\n  }\n\n  public async update(payload: Subscription): Promise<Subscription> {\n    const doc = await this.getDb({ id: payload.id });\n    const tab = new Subscription({ ...doc, ...payload });\n    const newDoc = await this.updateDb(tab);\n    await this.handleTask(newDoc, !newDoc.is_disabled);\n    await this.setSshConfig();\n    return newDoc;\n  }\n\n  public async updateDb(payload: Subscription): Promise<Subscription> {\n    await SubscriptionModel.update(payload, { where: { id: payload.id } });\n    return await this.getDb({ id: payload.id });\n  }\n\n  public async status({\n    ids,\n    status,\n    pid,\n    log_path,\n    last_running_time = 0,\n    last_execution_time = 0,\n  }: {\n    ids: number[];\n    status: SubscriptionStatus;\n    pid: number;\n    log_path: string;\n    last_running_time: number;\n    last_execution_time: number;\n  }) {\n    const options: any = {\n      status,\n      pid,\n      log_path,\n      last_execution_time,\n    };\n    if (last_running_time > 0) {\n      options.last_running_time = last_running_time;\n    }\n\n    return await SubscriptionModel.update(\n      { ...options },\n      { where: { id: ids } },\n    );\n  }\n\n  public async remove(ids: number[], query: { force?: boolean }) {\n    const docs = await SubscriptionModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      await this.handleTask(doc.get({ plain: true }), false);\n    }\n    await SubscriptionModel.destroy({ where: { id: ids } });\n    await this.setSshConfig();\n\n    if (query?.force === true) {\n      const crons = await CrontabModel.findAll({ where: { sub_id: ids } });\n      if (crons?.length) {\n        await this.crontabService.remove(crons.map((x) => x.id!));\n      }\n      for (const doc of docs) {\n        const filePath = join(config.scriptPath, doc.alias);\n        const repoPath = join(config.repoPath, doc.alias);\n        await rmPath(filePath);\n        await rmPath(repoPath);\n      }\n    }\n  }\n\n  public async getDb(\n    query: FindOptions<Subscription>['where'],\n  ): Promise<Subscription> {\n    const doc = await SubscriptionModel.findOne({ where: { ...query } });\n    if (!doc) {\n      throw new Error(`Subscription ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async run(ids: number[]) {\n    await SubscriptionModel.update(\n      { status: SubscriptionStatus.queued },\n      { where: { id: ids } },\n    );\n    ids.forEach((id) => {\n      this.runSingle(id);\n    });\n  }\n\n  public async stop(ids: number[]) {\n    const docs = await SubscriptionModel.findAll({ where: { id: ids } });\n    for (const doc of docs) {\n      if (doc.pid) {\n        try {\n          await killTask(doc.pid);\n        } catch (error) {\n          this.logger.error(error);\n        }\n      }\n    }\n\n    await SubscriptionModel.update(\n      { status: SubscriptionStatus.idle, pid: undefined },\n      { where: { id: ids } },\n    );\n  }\n\n  private async runSingle(subscriptionId: number) {\n    const subscription = await this.getDb({ id: subscriptionId });\n    if (subscription.status !== SubscriptionStatus.queued) {\n      return;\n    }\n\n    const command = formatCommand(subscription);\n\n    this.scheduleService.runTask(command, this.taskCallbacks(subscription), {\n      name: subscription.name,\n      schedule: subscription.schedule,\n      command,\n      id: String(subscription.id),\n      runOrigin: 'subscription',\n    });\n  }\n\n  public async disabled(ids: number[]) {\n    await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });\n    const docs = await SubscriptionModel.findAll({ where: { id: ids } });\n    await this.setSshConfig();\n    for (const doc of docs) {\n      await this.handleTask(doc.get({ plain: true }), false);\n    }\n  }\n\n  public async enabled(ids: number[]) {\n    await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });\n    const docs = await SubscriptionModel.findAll({ where: { id: ids } });\n    await this.setSshConfig();\n    for (const doc of docs) {\n      await this.handleTask(doc.get({ plain: true }));\n    }\n  }\n\n  public async log(id: number) {\n    const doc = await this.getDb({ id });\n    if (!doc || !doc.log_path) {\n      return '';\n    }\n\n    const absolutePath = await handleLogPath(doc.log_path as string);\n    return await getFileContentByName(absolutePath);\n  }\n\n  public async logs(id: number) {\n    const doc = await this.getDb({ id });\n    if (!doc) {\n      return [];\n    }\n\n    if (doc.log_path) {\n      const relativeDir = path.dirname(`${doc.log_path}`);\n      const dir = path.resolve(config.logPath, relativeDir);\n      const _exist = await fileExist(dir);\n      if (_exist) {\n        let files = await fs.readdir(dir);\n        return (\n          await Promise.all(\n            files.map(async (x) => ({\n              filename: x,\n              directory: relativeDir.replace(config.logPath, ''),\n              time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,\n            })),\n          )\n        ).sort((a, b) => b.time - a.time);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "back/services/system.ts",
    "content": "import { spawn } from 'cross-spawn';\nimport { Response } from 'express';\nimport fs from 'fs';\nimport { Agent, request } from 'undici';\nimport sum from 'lodash/sum';\nimport path from 'path';\nimport { Inject, Service } from 'typedi';\nimport winston from 'winston';\nimport config from '../config';\nimport { NotificationModeStringMap, TASK_COMMAND } from '../config/const';\nimport {\n  getPid,\n  killTask,\n  parseContentVersion,\n  parseVersion,\n  promiseExec,\n  readDirs,\n  rmPath,\n  setSystemTimezone,\n} from '../config/util';\nimport {\n  DependenceModel,\n  DependenceStatus,\n  DependenceTypes,\n} from '../data/dependence';\nimport { NotificationInfo } from '../data/notify';\nimport {\n  AuthDataType,\n  SystemInfo,\n  SystemInstance,\n  SystemModel,\n  SystemModelInfo,\n} from '../data/system';\nimport taskLimit from '../shared/pLimit';\nimport NotificationService from './notify';\nimport ScheduleService, { TaskCallbacks } from './schedule';\nimport SockService from './sock';\nimport os from 'os';\nimport dayjs from 'dayjs';\n\n@Service()\nexport default class SystemService {\n  @Inject((type) => NotificationService)\n  private notificationService!: NotificationService;\n\n  constructor(\n    @Inject('logger') private logger: winston.Logger,\n    private scheduleService: ScheduleService,\n    private sockService: SockService,\n  ) { }\n\n  public async getSystemConfig() {\n    const doc = await this.getDb({ type: AuthDataType.systemConfig });\n    return {\n      ...doc,\n      info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },\n    };\n  }\n\n  private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {\n    const { id, ...others } = payload;\n    await SystemModel.update(others, { where: { id } });\n    const doc = await this.getDb({ id });\n    return doc;\n  }\n\n  public async getDb(query: any): Promise<SystemInfo> {\n    const doc = await SystemModel.findOne({ where: query });\n    if (!doc) {\n      throw new Error(`System ${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async updateNotificationMode(notificationInfo: NotificationInfo) {\n    const code = Math.random().toString().slice(-6);\n    const isSuccess = await this.notificationService.testNotify(\n      notificationInfo,\n      '青龙',\n      `【蛟龙】测试通知 https://t.me/jiao_long`,\n    );\n    if (isSuccess) {\n      const result = await this.updateAuthDb({\n        type: AuthDataType.notification,\n        info: { ...notificationInfo },\n      });\n      return { code: 200, data: { ...result, code } };\n    } else {\n      return { code: 400, message: '通知发送失败，请检查参数' };\n    }\n  }\n\n  public async updateLogRemoveFrequency(info: SystemModelInfo) {\n    const oDoc = await this.getSystemConfig();\n    const result = await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    const cron = {\n      id: result.id as number,\n      name: '删除日志',\n      command: `ql rmlog ${info.logRemoveFrequency}`,\n      runOrigin: 'system' as const,\n    };\n    if (oDoc.info?.logRemoveFrequency) {\n      await this.scheduleService.cancelIntervalTask(cron);\n    }\n    if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {\n      this.scheduleService.createIntervalTask(\n        cron,\n        {\n          days: info.logRemoveFrequency,\n        },\n        true,\n      );\n    }\n    return { code: 200, data: info };\n  }\n\n  public async updateCronConcurrency(info: SystemModelInfo) {\n    const oDoc = await this.getSystemConfig();\n    await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    if (info.cronConcurrency) {\n      await taskLimit.setCustomLimit(info.cronConcurrency);\n    }\n    return { code: 200, data: info };\n  }\n\n  public async updateDependenceProxy(info: SystemModelInfo) {\n    const oDoc = await this.getSystemConfig();\n    await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    if (info.dependenceProxy) {\n      await fs.promises.writeFile(\n        config.dependenceProxyFile,\n        `export http_proxy=\"${info.dependenceProxy}\"\\nexport https_proxy=\"${info.dependenceProxy}\"`,\n      );\n    } else {\n      await fs.promises.rm(config.dependenceProxyFile);\n    }\n    return { code: 200, data: info };\n  }\n\n  public async updateNodeMirror(info: SystemModelInfo, res?: Response) {\n    const oDoc = await this.getSystemConfig();\n    await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    let cmd = 'pnpm config delete registry';\n    if (info.nodeMirror) {\n      cmd = `pnpm config set registry ${info.nodeMirror}`;\n    }\n    let command = `cd && ${cmd}`;\n    const docs = await DependenceModel.findAll({\n      where: {\n        type: DependenceTypes.nodejs,\n        status: DependenceStatus.installed,\n      },\n    });\n    if (docs.length > 0) {\n      command += ` && pnpm i -g`;\n    }\n    this.scheduleService.runTask(\n      command,\n      {\n        onStart: async (cp) => {\n          res?.setHeader('QL-Task-Pid', `${cp.pid}`);\n          res?.end();\n        },\n        onEnd: async () => {\n          this.sockService.sendMessage({\n            type: 'updateNodeMirror',\n            message: 'update node mirror end',\n          });\n        },\n        onError: async (message: string) => {\n          this.sockService.sendMessage({ type: 'updateNodeMirror', message });\n        },\n        onLog: async (message: string) => {\n          this.sockService.sendMessage({ type: 'updateNodeMirror', message });\n        },\n      },\n      {\n        command,\n        id: 'update-node-mirror',\n        runOrigin: 'system',\n      },\n    );\n  }\n\n  public async updatePythonMirror(info: SystemModelInfo) {\n    const oDoc = await this.getSystemConfig();\n    await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    let cmd = 'pip config unset global.index-url';\n    if (info.pythonMirror) {\n      cmd = `pip3 config set global.index-url ${info.pythonMirror}`;\n    }\n    await promiseExec(cmd);\n    return { code: 200, data: info };\n  }\n\n  public async updateLinuxMirror(\n    info: SystemModelInfo,\n    res?: Response,\n    onEnd?: () => void,\n  ) {\n    const oDoc = await this.getSystemConfig();\n    await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    let defaultDomain = 'https://dl-cdn.alpinelinux.org';\n    let targetDomain = 'https://dl-cdn.alpinelinux.org';\n    if (os.platform() !== 'linux') {\n      return;\n    }\n    const content = await fs.promises.readFile('/etc/apk/repositories', {\n      encoding: 'utf-8',\n    });\n    const domainMatch = content.match(/(http.*)\\/alpine\\/.*/);\n    if (domainMatch) {\n      defaultDomain = domainMatch[1];\n    }\n    if (info.linuxMirror) {\n      targetDomain = info.linuxMirror;\n    }\n    const command = `sed -i 's/${defaultDomain.replace(\n      /\\//g,\n      '\\\\/',\n    )}/${targetDomain.replace(\n      /\\//g,\n      '\\\\/',\n    )}/g' /etc/apk/repositories && apk update -f`;\n\n    this.scheduleService.runTask(\n      command,\n      {\n        onStart: async (cp) => {\n          res?.setHeader('QL-Task-Pid', `${cp.pid}`);\n          res?.end();\n        },\n        onEnd: async () => {\n          this.sockService.sendMessage({\n            type: 'updateLinuxMirror',\n            message: 'update linux mirror end',\n          });\n          onEnd?.();\n        },\n        onError: async (message: string) => {\n          this.sockService.sendMessage({ type: 'updateLinuxMirror', message });\n        },\n        onLog: async (message: string) => {\n          this.sockService.sendMessage({ type: 'updateLinuxMirror', message });\n        },\n      },\n      {\n        command,\n        id: 'update-linux-mirror',\n        runOrigin: 'system',\n      },\n    );\n  }\n\n  public async checkUpdate() {\n    try {\n      const currentVersionContent = await parseVersion(config.versionFile);\n\n      let lastVersionContent;\n      try {\n        const { body } = await request(\n          `${config.lastVersionFile}?t=${Date.now()}`,\n          {\n            dispatcher: new Agent({\n              keepAliveTimeout: 30000,\n              keepAliveMaxTimeout: 30000,\n            }),\n          },\n        );\n        const text = await body.text();\n        lastVersionContent = parseContentVersion(text);\n      } catch (error) { }\n\n      if (!lastVersionContent) {\n        lastVersionContent = currentVersionContent;\n      }\n\n      return {\n        code: 200,\n        data: {\n          hasNewVersion: this.checkHasNewVersion(\n            currentVersionContent.version,\n            lastVersionContent.version,\n          ),\n          lastVersion: lastVersionContent.version,\n          lastLog: lastVersionContent.changeLog,\n          lastLogLink: lastVersionContent.changeLogLink,\n        },\n      };\n    } catch (error: any) {\n      return {\n        code: 400,\n        message: error.message,\n      };\n    }\n  }\n\n  private checkHasNewVersion(curVersion: string, lastVersion: string) {\n    const curArr = curVersion.split('.').map((x) => parseInt(x, 10));\n    const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));\n    if (curArr[0] < lastArr[0]) {\n      return true;\n    }\n    if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {\n      return true;\n    }\n    if (\n      curArr[0] === lastArr[0] &&\n      curArr[1] === lastArr[1] &&\n      curArr[2] < lastArr[2]\n    ) {\n      return true;\n    }\n    return false;\n  }\n\n  public async updateSystem() {\n    const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });\n\n    cp.stdout.on('data', (data) => {\n      this.sockService.sendMessage({\n        type: 'updateSystemVersion',\n        message: data.toString(),\n      });\n    });\n\n    cp.stderr.on('data', (data) => {\n      this.sockService.sendMessage({\n        type: 'updateSystemVersion',\n        message: data.toString(),\n      });\n    });\n\n    cp.on('error', (err) => {\n      this.sockService.sendMessage({\n        type: 'updateSystemVersion',\n        message: JSON.stringify(err),\n      });\n    });\n\n    return { code: 200 };\n  }\n\n  public async reloadSystem(target?: 'system' | 'data') {\n    const cmd = `real_time=true ql reload ${target || ''}`;\n    const cp = spawn(cmd, {\n      shell: '/bin/bash',\n      detached: true,\n      stdio: 'ignore',\n    });\n    cp.unref();\n    setTimeout(() => {\n      process.exit(0);\n    });\n    return { code: 200 };\n  }\n\n  public async notify({\n    title,\n    content,\n    notificationInfo,\n  }: {\n    title: string;\n    content: string;\n    notificationInfo?: NotificationInfo;\n  }) {\n    const typeString =\n      typeof notificationInfo?.type === 'number'\n        ? NotificationModeStringMap[notificationInfo.type]\n        : undefined;\n    if (notificationInfo && typeString) {\n      notificationInfo.type = typeString;\n    }\n    const isSuccess = await this.notificationService.notify(\n      title,\n      content,\n      notificationInfo,\n    );\n    if (isSuccess) {\n      return { code: 200, message: '通知发送成功' };\n    } else {\n      return { code: 400, message: '通知发送失败，请检查系统设置/通知配置' };\n    }\n  }\n\n  public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {\n    if (!command.startsWith(TASK_COMMAND)) {\n      command = `${TASK_COMMAND} ${command}`;\n    }\n    const logPathPrefix = logPath ? `real_log_path=${logPath}` : ''\n    this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, {\n      command,\n      id: command.replace(/ /g, '-'),\n      runOrigin: 'system',\n    });\n  }\n\n  public async stop({ command, pid }: { command: string; pid: number }) {\n    if (!pid && !command) {\n      return { code: 400, message: '参数错误' };\n    }\n\n    if (pid) {\n      await killTask(pid);\n      return { code: 200 };\n    }\n\n    if (!command.startsWith(TASK_COMMAND)) {\n      command = `${TASK_COMMAND} ${command}`;\n    }\n    const _pid = await getPid(command);\n    if (_pid) {\n      await killTask(_pid);\n      return { code: 200 };\n    } else {\n      return { code: 400, message: '任务未找到' };\n    }\n  }\n\n  public async exportData(res: Response, type?: string[]) {\n    try {\n      let dataDirs = ['db', 'upload'];\n      if (type && type.length) {\n        dataDirs = dataDirs.concat(type.filter((x) => x !== 'base'));\n      }\n      const dataPaths = dataDirs.map((dir) => `data/${dir}`);\n      await promiseExec(\n        `cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile\n        } ${dataPaths.join(' ')}`,\n      );\n      res.download(config.dataTgzFile);\n    } catch (error: any) {\n      return res.send({ code: 400, message: error.message });\n    }\n  }\n\n  public async importData() {\n    try {\n      await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);\n      const res = await promiseExec(\n        `cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`,\n      );\n      return { code: 200, data: res };\n    } catch (error: any) {\n      return { code: 400, message: error.message };\n    }\n  }\n\n  public async getSystemLog(\n    res: Response,\n    query: {\n      startTime?: string;\n      endTime?: string;\n    },\n  ) {\n    const startTime = dayjs(query.startTime || undefined)\n      .startOf('d')\n      .valueOf();\n    const endTime = dayjs(query.endTime || undefined)\n      .endOf('d')\n      .valueOf();\n    const result = await readDirs(config.systemLogPath, config.systemLogPath);\n    const logs = result\n      .reverse()\n      .filter((x) => x.title.endsWith('.log'))\n      .filter((x) => x.createTime >= startTime && x.createTime <= endTime);\n\n    res.set({\n      'Content-Length': sum(logs.map((x) => x.size)),\n    });\n    (function sendFiles(res, fileNames) {\n      if (fileNames.length === 0) {\n        res.end();\n        return;\n      }\n\n      const currentLog = fileNames.shift();\n      if (currentLog) {\n        const currentFileStream = fs.createReadStream(\n          path.join(config.systemLogPath, currentLog.title),\n        );\n        currentFileStream.on('end', () => {\n          sendFiles(res, fileNames);\n        });\n        currentFileStream.pipe(res, { end: false });\n      }\n    })(res, logs);\n  }\n\n  public async deleteSystemLog() {\n    const result = await readDirs(config.systemLogPath, config.systemLogPath);\n    const logs = result.reverse().filter((x) => x.title.endsWith('.log'));\n    for (const log of logs) {\n      await rmPath(path.join(config.systemLogPath, log.title));\n    }\n  }\n\n  public async updateTimezone(info: SystemModelInfo) {\n    if (!info.timezone) {\n      info.timezone = 'Asia/Shanghai';\n    }\n    const oDoc = await this.getSystemConfig();\n    await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    const success = await setSystemTimezone(info.timezone);\n    if (success) {\n      return { code: 200, data: info };\n    } else {\n      return { code: 400, message: '设置时区失败' };\n    }\n  }\n\n  public async updateGlobalSshKey(info: SystemModelInfo) {\n    const oDoc = await this.getSystemConfig();\n    const result = await this.updateAuthDb({\n      ...oDoc,\n      info: { ...oDoc.info, ...info },\n    });\n    \n    // Apply the global SSH key\n    const SshKeyService = require('./sshKey').default;\n    const Container = require('typedi').Container;\n    const sshKeyService = Container.get(SshKeyService);\n    \n    if (info.globalSshKey) {\n      await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');\n    } else {\n      await sshKeyService.removeGlobalSSHKey('global');\n    }\n    \n    return { code: 200, data: result };\n  }\n\n  public async cleanDependence(type: 'node' | 'python3') {\n    if (!type || !['node', 'python3'].includes(type)) {\n      return { code: 400, message: '参数错误' };\n    }\n    try {\n      const finalPath = path.join(config.dependenceCachePath, type);\n      await fs.promises.rm(finalPath, { recursive: true });\n    } catch (error) { }\n    return { code: 200 };\n  }\n}\n"
  },
  {
    "path": "back/services/user.ts",
    "content": "import { Service, Inject } from 'typedi';\nimport winston from 'winston';\nimport { createRandomString } from '../config/util';\nimport config from '../config';\nimport jwt from 'jsonwebtoken';\nimport { authenticator } from '@otplib/preset-default';\nimport {\n  AuthDataType,\n  SystemInfo,\n  SystemModel,\n  SystemModelInfo,\n  LoginStatus,\n  AuthInfo,\n  TokenInfo,\n} from '../data/system';\nimport { NotificationInfo } from '../data/notify';\nimport NotificationService from './notify';\nimport { Request } from 'express';\nimport ScheduleService from './schedule';\nimport SockService from './sock';\nimport dayjs from 'dayjs';\nimport IP2Region from 'ip2region';\nimport requestIp from 'request-ip';\nimport uniq from 'lodash/uniq';\nimport pickBy from 'lodash/pickBy';\nimport isNil from 'lodash/isNil';\nimport { shareStore } from '../shared/store';\n\n@Service()\nexport default class UserService {\n  @Inject((type) => NotificationService)\n  private notificationService!: NotificationService;\n\n  constructor(\n    @Inject('logger') private logger: winston.Logger,\n    private scheduleService: ScheduleService,\n    private sockService: SockService,\n  ) {}\n\n  public async login(\n    payloads: {\n      username: string;\n      password: string;\n    },\n    req: Request,\n    needTwoFactor = true,\n  ): Promise<any> {\n    let { username, password } = payloads;\n    const content = await this.getAuthInfo();\n    const timestamp = Date.now();\n    let {\n      username: cUsername,\n      password: cPassword,\n      retries = 0,\n      lastlogon,\n      lastip,\n      lastaddr,\n      twoFactorActivated,\n      tokens = {},\n      platform,\n    } = content;\n    const retriesTime = Math.pow(3, retries) * 1000;\n    if (retries > 2 && timestamp - lastlogon < retriesTime) {\n      const waitTime = Math.ceil(\n        (retriesTime - (timestamp - lastlogon)) / 1000,\n      );\n      return {\n        code: 410,\n        message: `失败次数过多，请${waitTime}秒后重试`,\n        data: waitTime,\n      };\n    }\n\n    if (\n      username === cUsername &&\n      password === cPassword &&\n      twoFactorActivated &&\n      needTwoFactor\n    ) {\n      await this.updateAuthInfo(content, {\n        isTwoFactorChecking: true,\n      });\n      return {\n        code: 420,\n        message: '',\n      };\n    }\n\n    const ip = requestIp.getClientIp(req) || '';\n    const query = new IP2Region();\n    const ipAddress = query.search(ip);\n    let address = '';\n    if (ipAddress) {\n      const { country, province, city, isp } = ipAddress;\n      address = uniq([country, province, city, isp]).filter(Boolean).join(' ');\n    }\n    if (username === cUsername && password === cPassword) {\n      const data = createRandomString(50, 100);\n      const expiration = twoFactorActivated ? '60d' : '20d';\n      let token = jwt.sign({ data }, config.jwt.secret, {\n        expiresIn: config.jwt.expiresIn || expiration,\n        algorithm: 'HS384',\n      });\n\n      const tokenInfo: TokenInfo = {\n        value: token,\n        timestamp,\n        ip,\n        address,\n        platform: req.platform,\n      };\n\n      const updatedTokens = this.addTokenToList(\n        tokens,\n        req.platform,\n        tokenInfo,\n      );\n\n      await this.updateAuthInfo(content, {\n        token,\n        tokens: updatedTokens,\n        lastlogon: timestamp,\n        retries: 0,\n        lastip: ip,\n        lastaddr: address,\n        platform: req.platform,\n        isTwoFactorChecking: false,\n      });\n      this.notificationService.notify(\n        '登录通知',\n        `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${\n          req.platform\n        }端 登录成功，ip地址 ${ip}`,\n      );\n      await this.insertDb({\n        type: AuthDataType.loginLog,\n        info: {\n          timestamp,\n          address,\n          ip,\n          platform: req.platform,\n          status: LoginStatus.success,\n        },\n      });\n      this.getLoginLog();\n      return {\n        code: 200,\n        data: {\n          token,\n          lastip,\n          lastaddr,\n          lastlogon,\n          retries,\n          platform,\n        },\n      };\n    } else {\n      await this.updateAuthInfo(content, {\n        retries: retries + 1,\n        lastlogon: timestamp,\n        lastip: ip,\n        lastaddr: address,\n        platform: req.platform,\n      });\n      this.notificationService.notify(\n        '登录通知',\n        `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${\n          req.platform\n        }端 登录失败，ip地址 ${ip}`,\n      );\n      await this.insertDb({\n        type: AuthDataType.loginLog,\n        info: {\n          timestamp,\n          address,\n          ip,\n          platform: req.platform,\n          status: LoginStatus.fail,\n        },\n      });\n      this.getLoginLog();\n      if (retries > 2) {\n        const waitTime = Math.round(Math.pow(3, retries + 1));\n        return {\n          code: 410,\n          message: `失败次数过多，请${waitTime}秒后重试`,\n          data: waitTime,\n        };\n      } else {\n        return { code: 400, message: config.authError };\n      }\n    }\n  }\n\n  public async logout(platform: string, tokenValue: string): Promise<any> {\n    if (!platform || !tokenValue) {\n      this.logger.warn('Invalid logout parameters - empty platform or token');\n      return;\n    }\n\n    const authInfo = await this.getAuthInfo();\n\n    // Verify the token exists before attempting to remove it\n    const tokenExists = this.findTokenInList(\n      authInfo.tokens,\n      platform,\n      tokenValue,\n    );\n    if (!tokenExists && authInfo.token !== tokenValue) {\n      // Token not found, but don't throw error - user may have already logged out\n      this.logger.info(\n        `Logout attempted for non-existent token on platform: ${platform}`,\n      );\n      return;\n    }\n\n    const updatedTokens = this.removeTokenFromList(\n      authInfo.tokens,\n      platform,\n      tokenValue,\n    );\n\n    await this.updateAuthInfo(authInfo, {\n      token: authInfo.token === tokenValue ? '' : authInfo.token,\n      tokens: updatedTokens,\n    });\n  }\n\n  public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {\n    const docs = await SystemModel.findAll({\n      where: { type: AuthDataType.loginLog },\n    });\n    if (docs && docs.length > 0) {\n      const result = docs.sort(\n        (a, b) => b.info!.timestamp! - a.info!.timestamp!,\n      );\n      if (result.length > 100) {\n        const ids = result.slice(100).map((x) => x.id!);\n        await SystemModel.destroy({\n          where: { id: ids },\n        });\n      }\n      return result.map((x) => x.info);\n    }\n    return [];\n  }\n\n  private async insertDb(payload: SystemInfo): Promise<SystemInfo> {\n    const doc = await SystemModel.create({ ...payload }, { returning: true });\n    return doc;\n  }\n\n  public async updateUsernameAndPassword({\n    username,\n    password,\n  }: {\n    username: string;\n    password: string;\n  }) {\n    if (password === 'admin') {\n      return { code: 400, message: '密码不能设置为admin' };\n    }\n    const authInfo = await this.getAuthInfo();\n    await this.updateAuthInfo(authInfo, { username, password });\n    return { code: 200, message: '更新成功' };\n  }\n\n  public async updateAvatar(avatar: string) {\n    const authInfo = await this.getAuthInfo();\n    await this.updateAuthInfo(authInfo, { avatar });\n    return { code: 200, data: avatar, message: '更新成功' };\n  }\n\n  public async initTwoFactor() {\n    const secret = authenticator.generateSecret();\n    const authInfo = await this.getAuthInfo();\n    const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);\n    await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });\n    return { secret, url: otpauth };\n  }\n\n  public async activeTwoFactor(code: string) {\n    const authInfo = await this.getAuthInfo();\n    const isValid = authenticator.verify({\n      token: code,\n      secret: authInfo.twoFactorSecret,\n    });\n    if (isValid) {\n      await this.updateAuthInfo(authInfo, { twoFactorActivated: true });\n    }\n    return isValid;\n  }\n\n  public async twoFactorLogin(\n    {\n      username,\n      password,\n      code,\n    }: { username: string; password: string; code: string },\n    req: any,\n  ) {\n    const authInfo = await this.getAuthInfo();\n    const { isTwoFactorChecking, twoFactorSecret } = authInfo;\n    if (!isTwoFactorChecking) {\n      return { code: 450, message: '未知错误' };\n    }\n    const isValid = authenticator.verify({\n      token: code,\n      secret: twoFactorSecret,\n    });\n    if (isValid) {\n      return this.login({ username, password }, req, false);\n    } else {\n      const ip = requestIp.getClientIp(req) || '';\n      const query = new IP2Region();\n      const ipAddress = query.search(ip);\n      let address = '';\n      if (ipAddress) {\n        const { country, province, city, isp } = ipAddress;\n        address = uniq([country, province, city, isp])\n          .filter(Boolean)\n          .join(' ');\n      }\n      await this.updateAuthInfo(authInfo, {\n        lastip: ip,\n        lastaddr: address,\n        platform: req.platform,\n      });\n      return { code: 430, message: '验证失败' };\n    }\n  }\n\n  public async deactiveTwoFactor() {\n    const authInfo = await this.getAuthInfo();\n    await this.updateAuthInfo(authInfo, {\n      twoFactorActivated: false,\n      twoFactorSecret: '',\n    });\n    return true;\n  }\n\n  public async getAuthInfo() {\n    const authInfo = await shareStore.getAuthInfo();\n    if (authInfo) {\n      return authInfo;\n    }\n    const doc = await this.getDb({ type: AuthDataType.authConfig });\n    return (doc.info || {}) as AuthInfo;\n  }\n\n  private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) {\n    const result = { ...authInfo, ...info };\n    await shareStore.updateAuthInfo(result);\n    await this.updateAuthDb({\n      type: AuthDataType.authConfig,\n      info: result,\n    });\n  }\n\n  public async getNotificationMode(): Promise<NotificationInfo> {\n    const doc = await this.getDb({ type: AuthDataType.notification });\n    return (doc.info || {}) as NotificationInfo;\n  }\n\n  private async updateAuthDb(payload: SystemInfo): Promise<any> {\n    let doc = await SystemModel.findOne({ where: { type: payload.type } });\n    if (doc) {\n      const updateResult = await SystemModel.update(payload, {\n        where: { id: doc.id },\n        returning: true,\n      });\n      doc = updateResult[1][0];\n    } else {\n      doc = await SystemModel.create(payload, { returning: true });\n    }\n    return doc;\n  }\n\n  public async getDb(query: any): Promise<SystemInfo> {\n    const doc = await SystemModel.findOne({ where: { ...query } });\n    if (!doc) {\n      throw new Error(`${JSON.stringify(query)} not found`);\n    }\n    return doc.get({ plain: true });\n  }\n\n  public async updateNotificationMode(notificationInfo: NotificationInfo) {\n    const code = Math.random().toString().slice(-6);\n    const isSuccess = await this.notificationService.testNotify(\n      notificationInfo,\n      '青龙',\n      `【蛟龙】测试通知 https://t.me/jiao_long`,\n    );\n    if (isSuccess) {\n      const result = await this.updateAuthDb({\n        type: AuthDataType.notification,\n        info: { ...notificationInfo },\n      });\n      return { code: 200, data: { ...result, code } };\n    } else {\n      return { code: 400, message: '通知发送失败，请检查参数' };\n    }\n  }\n\n  private normalizeTokens(\n    tokens: Record<string, string | TokenInfo[]>,\n  ): Record<string, TokenInfo[]> {\n    const normalized: Record<string, TokenInfo[]> = {};\n\n    for (const [platform, value] of Object.entries(tokens)) {\n      if (typeof value === 'string') {\n        // Legacy format: convert string token to TokenInfo array\n        if (value) {\n          normalized[platform] = [\n            {\n              value,\n              timestamp: Date.now(),\n              ip: '',\n              address: '',\n              platform,\n            },\n          ];\n        } else {\n          normalized[platform] = [];\n        }\n      } else {\n        // Already in new format\n        normalized[platform] = value || [];\n      }\n    }\n\n    return normalized;\n  }\n\n  private addTokenToList(\n    tokens: Record<string, string | TokenInfo[]>,\n    platform: string,\n    tokenInfo: TokenInfo,\n    maxTokensPerPlatform: number = config.maxTokensPerPlatform,\n  ): Record<string, TokenInfo[]> {\n    // Validate maxTokensPerPlatform parameter\n    if (!Number.isInteger(maxTokensPerPlatform) || maxTokensPerPlatform < 1) {\n      this.logger.warn(\n        `Invalid maxTokensPerPlatform value: ${maxTokensPerPlatform}, using default`,\n      );\n      maxTokensPerPlatform = config.maxTokensPerPlatform;\n    }\n\n    const normalized = this.normalizeTokens(tokens);\n\n    if (!normalized[platform]) {\n      normalized[platform] = [];\n    }\n\n    // Add new token\n    normalized[platform].unshift(tokenInfo);\n\n    // Limit the number of active tokens per platform\n    if (normalized[platform].length > maxTokensPerPlatform) {\n      normalized[platform] = normalized[platform].slice(\n        0,\n        maxTokensPerPlatform,\n      );\n    }\n\n    return normalized;\n  }\n\n  private removeTokenFromList(\n    tokens: Record<string, string | TokenInfo[]>,\n    platform: string,\n    tokenValue: string,\n  ): Record<string, TokenInfo[]> {\n    const normalized = this.normalizeTokens(tokens);\n\n    if (normalized[platform]) {\n      normalized[platform] = normalized[platform].filter(\n        (t) => t.value !== tokenValue,\n      );\n    }\n\n    return normalized;\n  }\n\n  private findTokenInList(\n    tokens: Record<string, string | TokenInfo[]>,\n    platform: string,\n    tokenValue: string,\n  ): TokenInfo | undefined {\n    const normalized = this.normalizeTokens(tokens);\n\n    if (normalized[platform]) {\n      return normalized[platform].find((t) => t.value === tokenValue);\n    }\n\n    return undefined;\n  }\n\n  public async resetAuthInfo(info: Partial<AuthInfo>) {\n    const { retries, twoFactorActivated, password, username } = info;\n    const authInfo = await this.getAuthInfo();\n    const payload = pickBy(\n      {\n        retries,\n        twoFactorActivated,\n        password,\n        username,\n      },\n      (x) => !isNil(x),\n    );\n\n    await this.updateAuthInfo(authInfo, payload);\n  }\n}\n"
  },
  {
    "path": "back/shared/auth.ts",
    "content": "import { AuthInfo, TokenInfo } from '../data/system';\n\n/**\n * Validates if a token exists in the authentication info.\n * Supports both legacy string tokens and new TokenInfo array format.\n *\n * @param authInfo - The authentication information\n * @param headerToken - The token to validate\n * @param platform - The platform (desktop, mobile)\n * @returns true if the token is valid, false otherwise\n */\nexport function isValidToken(\n  authInfo: AuthInfo | null | undefined,\n  headerToken: string,\n  platform: string,\n): boolean {\n  if (!authInfo || !headerToken) {\n    return false;\n  }\n\n  const { token = '', tokens = {} } = authInfo;\n\n  // Check legacy token field\n  if (headerToken === token) {\n    return true;\n  }\n\n  // Check platform-specific tokens (support both legacy string and new TokenInfo[] format)\n  const platformTokens = tokens[platform];\n\n  // Handle null/undefined platformTokens\n  if (platformTokens === null || platformTokens === undefined) {\n    return false;\n  }\n\n  if (typeof platformTokens === 'string') {\n    // Legacy format: single string token\n    return headerToken === platformTokens;\n  } else if (Array.isArray(platformTokens)) {\n    // New format: array of TokenInfo objects\n    return platformTokens.some((t: TokenInfo) => t && t.value === headerToken);\n  }\n\n  // Unexpected type - log warning and reject\n  return false;\n}\n"
  },
  {
    "path": "back/shared/interface.ts",
    "content": "import { Dependence } from '../data/dependence';\nimport { ICron } from '../protos/cron';\n\nexport type Override<\n  T,\n  K extends Partial<{ [P in keyof T]: any }> | string,\n> = K extends string\n  ? Omit<T, K> & { [P in keyof T]: T[P] | unknown }\n  : Omit<T, keyof K> & K;\n\nexport type TCron = Override<Partial<ICron>, { id: string }>;\n\nexport interface IDependencyFn<T> {\n  (): Promise<T>;\n  dependency?: Dependence;\n}\n\nexport interface ICronFn<T> {\n  (): Promise<T>;\n  cron?: TCron;\n}\n\nexport interface ISchedule {\n  schedule?: string;\n  name?: string;\n  command?: string;\n  id: string;\n}\n\nexport interface IScheduleFn<T> {\n  (): Promise<T>;\n  schedule?: ISchedule;\n}\n"
  },
  {
    "path": "back/shared/logStreamManager.ts",
    "content": "import { createWriteStream, WriteStream } from 'fs';\nimport { EventEmitter } from 'events';\n\n/**\n * Manages write streams for log files to improve performance by avoiding repeated file opens\n */\nexport class LogStreamManager extends EventEmitter {\n  private streams: Map<string, WriteStream> = new Map();\n  private pendingWrites: Map<string, Promise<void>> = new Map();\n\n  /**\n   * Write data to a log file using a managed stream\n   * @param filePath - Absolute path to the log file\n   * @param data - Data to write to the log file\n   */\n  async write(filePath: string, data: string): Promise<void> {\n    // Wait for any pending writes to this file to complete\n    const pending = this.pendingWrites.get(filePath);\n    if (pending) {\n      await pending;\n    }\n\n    // Create a new promise for this write operation\n    const writePromise = new Promise<void>((resolve, reject) => {\n      let stream = this.streams.get(filePath);\n\n      if (!stream) {\n        // Create a new write stream if one doesn't exist\n        stream = createWriteStream(filePath, { flags: 'a' });\n        this.streams.set(filePath, stream);\n\n        // Handle stream errors\n        stream.on('error', (error) => {\n          this.emit('error', { filePath, error });\n          // Remove the stream from the map on error\n          this.streams.delete(filePath);\n          reject(error);\n        });\n      }\n\n      // Write the data\n      const canContinue = stream.write(data, 'utf8', (error) => {\n        if (error) {\n          reject(error);\n        } else {\n          resolve();\n        }\n      });\n\n      // Handle backpressure\n      if (!canContinue) {\n        stream.once('drain', () => {\n          // Stream is ready for more data\n        });\n      }\n    });\n\n    this.pendingWrites.set(filePath, writePromise);\n\n    try {\n      await writePromise;\n    } finally {\n      this.pendingWrites.delete(filePath);\n    }\n  }\n\n  /**\n   * Close the stream for a specific file path\n   * @param filePath - Absolute path to the log file\n   */\n  async closeStream(filePath: string): Promise<void> {\n    // Wait for any pending writes to complete\n    const pending = this.pendingWrites.get(filePath);\n    if (pending) {\n      await pending.catch(() => {\n        // Ignore errors on pending writes during close\n      });\n    }\n\n    const stream = this.streams.get(filePath);\n    if (stream) {\n      return new Promise<void>((resolve) => {\n        stream.end(() => {\n          this.streams.delete(filePath);\n          resolve();\n        });\n      });\n    }\n  }\n\n  /**\n   * Close all open streams\n   */\n  async closeAll(): Promise<void> {\n    const closePromises = Array.from(this.streams.keys()).map((filePath) =>\n      this.closeStream(filePath),\n    );\n    await Promise.all(closePromises);\n  }\n\n  /**\n   * Get the number of open streams\n   */\n  getOpenStreamCount(): number {\n    return this.streams.size;\n  }\n}\n\n// Export a singleton instance for shared use\nexport const logStreamManager = new LogStreamManager();\n"
  },
  {
    "path": "back/shared/pLimit.ts",
    "content": "import PQueue, { QueueAddOptions } from 'p-queue-cjs';\nimport os from 'os';\nimport { AuthDataType, SystemModel } from '../data/system';\nimport Logger from '../loaders/logger';\nimport { Dependence } from '../data/dependence';\nimport NotificationService from '../services/notify';\nimport {\n  ICronFn,\n  IDependencyFn,\n  ISchedule,\n  IScheduleFn,\n  TCron,\n} from './interface';\nimport config from '../config';\nimport { credentials } from '@grpc/grpc-js';\nimport { ApiClient } from '../protos/api';\n\nclass TaskLimit {\n  private dependenyLimit = new PQueue({ concurrency: 1 });\n  private queuedDependencyIds = new Set<number>([]);\n  private queuedCrons = new Map<string, ICronFn<any>[]>();\n  private repeatCronNotifyMap = new Map<string, number>();\n  private updateLogLimit = new PQueue({ concurrency: 1 });\n  private cronLimit = new PQueue({\n    concurrency: Math.max(os.cpus().length, 4),\n  });\n  private manualCronoLimit = new PQueue({\n    concurrency: Math.max(os.cpus().length, 4),\n  });\n  private subscriptionLimit = new PQueue({\n    concurrency: Math.max(os.cpus().length, 4),\n  });\n  private scriptLimit = new PQueue({\n    concurrency: Math.max(os.cpus().length, 4),\n  });\n  private systemLimit = new PQueue({\n    concurrency: Math.max(os.cpus().length, 4),\n  });\n  private client = new ApiClient(\n    `0.0.0.0:${config.grpcPort}`,\n    credentials.createInsecure(),\n    { 'grpc.enable_http_proxy': 0 },\n  );\n\n  get cronLimitActiveCount() {\n    return this.cronLimit.pending;\n  }\n\n  get cronLimitPendingCount() {\n    return this.cronLimit.size;\n  }\n\n  get firstDependencyId() {\n    return [...this.queuedDependencyIds.values()][0];\n  }\n\n  private notificationService: NotificationService = new NotificationService();\n\n  constructor() {\n    this.setCustomLimit();\n    this.handleEvents();\n  }\n\n  private handleEvents() {\n    this.cronLimit.on('add', () => {\n      Logger.info(\n        `[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,\n      );\n    });\n    this.cronLimit.on('active', () => {\n      Logger.info(\n        `[schedule][开始处理任务] 运行中任务数: ${\n          this.cronLimitActiveCount + 1\n        }, 等待中任务数: ${this.cronLimitPendingCount}`,\n      );\n    });\n    this.cronLimit.on('completed', (param) => {\n      Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`);\n    });\n    this.cronLimit.on('error', (error) => {\n      Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`);\n    });\n    this.cronLimit.on('next', () => {\n      Logger.info(\n        `[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,\n      );\n    });\n    this.cronLimit.on('idle', () => {\n      Logger.info(`[schedule][任务队列] 空闲中...`);\n    });\n  }\n\n  public removeQueuedDependency(dependency: Dependence) {\n    if (this.queuedDependencyIds.has(dependency.id!)) {\n      this.queuedDependencyIds.delete(dependency.id!);\n    }\n  }\n\n  public removeQueuedCron(id: string) {\n    if (this.queuedCrons.has(id)) {\n      const runs = this.queuedCrons.get(id);\n      if (runs && runs.length > 0) {\n        runs.pop();\n        this.queuedCrons.set(id, runs);\n      }\n    }\n  }\n\n  public async setCustomLimit(limit?: number) {\n    if (limit) {\n      this.cronLimit.concurrency = limit;\n      this.manualCronoLimit.concurrency = limit;\n      return;\n    }\n    await SystemModel.sync();\n    const doc = await SystemModel.findOne({\n      where: { type: AuthDataType.systemConfig },\n    });\n    if (doc?.info?.cronConcurrency) {\n      this.cronLimit.concurrency = doc.info.cronConcurrency;\n      this.manualCronoLimit.concurrency = doc.info.cronConcurrency;\n    }\n  }\n\n  public async runWithCronLimit<T>(\n    cron: TCron,\n    fn: ICronFn<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    fn.cron = cron;\n    let runs = this.queuedCrons.get(cron.id);\n    const result = runs?.length ? [...runs, fn] : [fn];\n    const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;\n    if (result?.length > 5) {\n      if (repeatTimes < 3) {\n        this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);\n        this.client.systemNotify(\n          {\n            title: '任务重复运行',\n            content: `任务：${cron.name}，命令：${cron.command}，定时：${cron.schedule}，处于运行中的超过 5 个，请检查定时设置`,\n          },\n          (err, res) => {\n            if (err) {\n              Logger.error(\n                `[schedule][任务重复运行] 通知失败 ${JSON.stringify(err)}`,\n              );\n            }\n          },\n        );\n      }\n      Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`);\n      return;\n    }\n    this.queuedCrons.set(cron.id, result);\n    return this.cronLimit.add(fn, options);\n  }\n\n  public async manualRunWithCronLimit<T>(\n    fn: () => Promise<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    return this.manualCronoLimit.add(fn, options);\n  }\n\n  public async runWithSubscriptionLimit<T>(\n    schedule: TCron,\n    fn: IScheduleFn<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    fn.schedule = schedule;\n    return this.subscriptionLimit.add(fn, options);\n  }\n\n  public async runWithSystemLimit<T>(\n    schedule: TCron,\n    fn: IScheduleFn<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    fn.schedule = schedule;\n    return this.systemLimit.add(fn, options);\n  }\n\n  public async runWithScriptLimit<T>(\n    schedule: ISchedule,\n    fn: IScheduleFn<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    fn.schedule = schedule;\n    return this.scriptLimit.add(fn, options);\n  }\n\n  public async waitDependencyQueueDone(): Promise<void> {\n    if (this.dependenyLimit.size === 0 && this.dependenyLimit.pending === 0) {\n      return;\n    }\n    return new Promise((resolve) => {\n      const onIdle = () => {\n        this.dependenyLimit.removeListener('idle', onIdle);\n        resolve();\n      };\n      this.dependenyLimit.on('idle', onIdle);\n    });\n  }\n\n  public runDependeny<T>(\n    dependency: Dependence,\n    fn: IDependencyFn<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    this.queuedDependencyIds.add(dependency.id!);\n    fn.dependency = dependency;\n    return this.dependenyLimit.add(fn, options);\n  }\n\n  public updateDepLog<T>(\n    fn: () => Promise<T>,\n    options?: Partial<QueueAddOptions>,\n  ): Promise<T | void> {\n    return this.updateLogLimit.add(fn, options);\n  }\n}\n\nexport default new TaskLimit();\n"
  },
  {
    "path": "back/shared/runCron.ts",
    "content": "import { spawn } from 'cross-spawn';\nimport taskLimit from './pLimit';\nimport Logger from '../loaders/logger';\nimport { ICron } from '../protos/cron';\nimport { CrontabModel, CrontabStatus } from '../data/cron';\nimport { killTask } from '../config/util';\n\nexport function runCron(cmd: string, cron: ICron): Promise<number | void> {\n  return taskLimit.runWithCronLimit(cron, () => {\n    return new Promise(async (resolve: any) => {\n      // Check if the cron is already running and stop it (only if multiple instances are not allowed)\n      try {\n        const existingCron = await CrontabModel.findOne({\n          where: { id: Number(cron.id) },\n        });\n\n        // Default to single instance mode (0) for backward compatibility\n        const allowSingleInstances =\n          existingCron?.allow_multiple_instances === 0;\n\n        if (\n          allowSingleInstances &&\n          existingCron &&\n          existingCron.pid &&\n          (existingCron.status === CrontabStatus.running ||\n            existingCron.status === CrontabStatus.queued)\n        ) {\n          Logger.info(\n            `[schedule][停止已运行任务] 任务ID: ${cron.id}, PID: ${existingCron.pid}`,\n          );\n          await killTask(existingCron.pid);\n          // Update the status to idle after killing\n          await CrontabModel.update(\n            { status: CrontabStatus.idle, pid: undefined },\n            { where: { id: Number(cron.id) } },\n          );\n        }\n      } catch (error) {\n        Logger.error(\n          `[schedule][检查已运行任务失败] 任务ID: ${cron.id}, 错误: ${error}`,\n        );\n      }\n\n      Logger.info(\n        `[schedule][开始执行任务] 参数 ${JSON.stringify({\n          ...cron,\n          command: cmd,\n        })}`,\n      );\n      const cp = spawn(cmd, { shell: '/bin/bash' });\n\n      cp.stderr.on('data', (data) => {\n        Logger.info(\n          '[schedule][执行任务失败] 命令: %s, 错误信息: %j',\n          cmd,\n          data.toString(),\n        );\n      });\n      cp.on('error', (err) => {\n        Logger.error(\n          '[schedule][创建任务失败] 命令: %s, 错误信息: %j',\n          cmd,\n          err,\n        );\n      });\n\n      cp.on('exit', async (code) => {\n        taskLimit.removeQueuedCron(cron.id);\n        Logger.info(\n          '[schedule][执行任务结束] 参数: %s, 退出码: %j',\n          JSON.stringify({\n            ...cron,\n            command: cmd,\n          }),\n          code,\n        );\n        resolve({ ...cron, command: cmd, pid: cp.pid, code });\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "back/shared/store.ts",
    "content": "import { AuthInfo } from '../data/system';\nimport { App } from '../data/open';\nimport Keyv from 'keyv';\nimport KeyvSqlite from '@keyv/sqlite';\nimport config from '../config';\nimport path from 'path';\n\nexport enum EKeyv {\n  'apps' = 'apps',\n  'authInfo' = 'authInfo',\n}\n\nexport interface IKeyvStore {\n  apps: App[];\n  authInfo: AuthInfo;\n}\n\nconst keyvSqlite = new KeyvSqlite(path.join(config.dbPath, 'keyv.sqlite'));\nexport const keyvStore = new Keyv<IKeyvStore>({ store: keyvSqlite });\n\nexport const shareStore = {\n  getAuthInfo() {\n    return keyvStore.get<IKeyvStore['authInfo']>(EKeyv.authInfo);\n  },\n  updateAuthInfo(value: IKeyvStore['authInfo']) {\n    return keyvStore.set<IKeyvStore['authInfo']>(EKeyv.authInfo, value);\n  },\n  getApps() {\n    return keyvStore.get<IKeyvStore['apps']>(EKeyv.apps);\n  },\n  updateApps(apps: App[]) {\n    return keyvStore.set<IKeyvStore['apps']>(EKeyv.apps, apps);\n  },\n};\n"
  },
  {
    "path": "back/shared/utils.ts",
    "content": "import { lock } from 'proper-lockfile';\nimport os from 'os';\nimport path from 'path';\nimport { writeFile, open, chmod } from 'fs/promises';\nimport { fileExist } from '../config/util';\n\nfunction getUniqueLockPath(filePath: string) {\n  const sanitizedPath = filePath\n    .replace(/[<>:\"/\\\\|?*]/g, '_')\n    .replace(/^_/, '');\n  return path.join(os.tmpdir(), `${sanitizedPath}.ql_lock`);\n}\n\nexport async function writeFileWithLock(\n  filePath: string,\n  content: string,\n  options: Parameters<typeof writeFile>[2] = {},\n) {\n  if (typeof options === 'string') {\n    options = { encoding: options };\n  }\n  if (!(await fileExist(filePath))) {\n    const fileHandle = await open(filePath, 'w');\n    fileHandle.close();\n  }\n  const lockfilePath = getUniqueLockPath(filePath);\n\n  const release = await lock(filePath, {\n    retries: {\n      retries: 10,\n      factor: 2,\n      minTimeout: 100,\n      maxTimeout: 3000,\n    },\n    lockfilePath,\n  });\n  await writeFile(filePath, content, { encoding: 'utf8', ...options });\n  if (options?.mode) {\n    await chmod(filePath, options.mode);\n  }\n  await release();\n}\n"
  },
  {
    "path": "back/token.ts",
    "content": "import 'reflect-metadata';\nimport OpenService from './services/open';\nimport { Container } from 'typedi';\nimport LoggerInstance from './loaders/logger';\nimport config from './config';\nimport path from 'path';\nimport os from 'os';\nimport { writeFileWithLock } from './shared/utils';\n\nconst tokenFile = path.join(config.configPath, 'token.json');\n\nasync function getToken() {\n  try {\n    Container.set('logger', LoggerInstance);\n    const openService = Container.get(OpenService);\n    const appToken = await openService.generateSystemToken();\n    console.log(appToken.value);\n    await writeFile({\n      value: appToken.value,\n      expiration: appToken.expiration,\n    });\n  } catch (error) {\n    console.log(error);\n  }\n}\n\nasync function writeFile(data: any) {\n  await writeFileWithLock(tokenFile, `${JSON.stringify(data)}${os.EOL}`);\n}\n\ngetToken();\n"
  },
  {
    "path": "back/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"ESNext\"],\n    \"typeRoots\": [\n      \"./types\",\n      \"../node_modules/celebrate/lib\",\n      \"../node_modules/@types\"\n    ],\n    \"allowSyntheticDefaultImports\": true,\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"node\",\n    \"module\": \"commonjs\",\n    \"pretty\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"../static/build\",\n    \"allowJs\": true,\n    \"noEmit\": false,\n    \"esModuleInterop\": true\n  },\n  \"include\": [\"./**/*\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "back/types/express.d.ts",
    "content": "/// <reference types=\"express\" />\n\nexport {};\n\ndeclare global {\n  namespace Express {\n    interface Request {\n      platform: 'desktop' | 'mobile';\n    }\n  }\n} "
  },
  {
    "path": "back/validation/schedule.ts",
    "content": "import { Joi } from 'celebrate';\nimport CronExpressionParser from 'cron-parser';\nimport { ScheduleType } from '../interface/schedule';\nimport path from 'path';\nimport config from '../config';\n\nconst validateSchedule = (value: string, helpers: any) => {\n  if (\n    value.startsWith(ScheduleType.ONCE) ||\n    value.startsWith(ScheduleType.BOOT)\n  ) {\n    return value;\n  }\n\n  try {\n    if (CronExpressionParser.parse(value).hasNext()) {\n      return value;\n    }\n  } catch (e) {\n    return helpers.error('any.invalid');\n  }\n  return helpers.error('any.invalid');\n};\n\nexport const scheduleSchema = Joi.string()\n  .required()\n  .custom(validateSchedule)\n  .messages({\n    'any.invalid': '无效的定时规则',\n    'string.empty': '定时规则不能为空',\n  });\n\nexport const commonCronSchema = {\n  name: Joi.string().optional(),\n  command: Joi.string().required(),\n  schedule: scheduleSchema,\n  labels: Joi.array().optional(),\n  sub_id: Joi.number().optional().allow(null),\n  extra_schedules: Joi.array().optional().allow(null),\n  task_before: Joi.string().optional().allow('').allow(null),\n  task_after: Joi.string().optional().allow('').allow(null),\n  log_name: Joi.string()\n    .optional()\n    .allow('')\n    .allow(null)\n    .custom((value, helpers) => {\n      if (!value) return value;\n\n      // Check if it's an absolute path\n      if (value.startsWith('/')) {\n        // Allow /dev/null as special case\n        if (value === '/dev/null') {\n          return value;\n        }\n\n        // For other absolute paths, ensure they are within the safe log directory\n        const normalizedValue = path.normalize(value);\n        const normalizedLogPath = path.normalize(config.logPath);\n\n        if (!normalizedValue.startsWith(normalizedLogPath)) {\n          return helpers.error('string.unsafePath');\n        }\n\n        return value;\n      }\n\n      if (\n        !/^(?!.*(?:^|\\/)\\.{1,2}(?:\\/|$))(?:\\/)?(?:[\\w.-]+\\/)*[\\w.-]+\\/?$/.test(\n          value,\n        )\n      ) {\n        return helpers.error('string.pattern.base');\n      }\n      if (value.length > 100) {\n        return helpers.error('string.max');\n      }\n      return value;\n    })\n    .messages({\n      'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',\n      'string.max': '日志名称不能超过100个字符',\n      'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',\n    }),\n  allow_multiple_instances: Joi.number().optional().valid(0, 1).allow(null),\n};\n"
  },
  {
    "path": "docker/310.Dockerfile",
    "content": "FROM python:3.10-alpine3.18 AS builder\nCOPY package.json .npmrc pnpm-lock.yaml /tmp/build/\nRUN set -x \\\n  && apk update \\\n  && apk add nodejs npm git \\\n  && npm i -g pnpm@8.3.1 pm2 ts-node \\\n  && cd /tmp/build \\\n  && pnpm install --prod\n\nFROM python:3.10-alpine\n\nARG QL_MAINTAINER=\"whyour\"\nLABEL maintainer=\"${QL_MAINTAINER}\"\nARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git\nARG QL_BRANCH=develop\nARG PYTHON_SHORT_VERSION=3.10\n\nENV QL_DIR=/ql \\\n  QL_BRANCH=${QL_BRANCH} \\\n  LANG=C.UTF-8 \\\n  SHELL=/bin/bash \\\n  PS1=\"\\u@\\h:\\w \\$ \"\n\nVOLUME /ql/data\n  \nEXPOSE 5700\n\nCOPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/\nCOPY --from=builder /usr/local/bin/. /usr/local/bin/\n\nRUN set -x \\\n  && apk update -f \\\n  && apk upgrade \\\n  && apk --no-cache add -f bash \\\n  coreutils \\\n  git \\\n  curl \\\n  wget \\\n  tzdata \\\n  perl \\\n  openssl \\\n  nodejs \\\n  jq \\\n  openssh \\\n  procps \\\n  netcat-openbsd \\\n  unzip \\\n  npm \\\n  && rm -rf /var/cache/apk/* \\\n  && apk update \\\n  && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\\n  && echo \"Asia/Shanghai\" > /etc/timezone \\\n  && git config --global user.email \"qinglong@users.noreply.github.com\" \\\n  && git config --global user.name \"qinglong\" \\\n  && git config --global http.postBuffer 524288000 \\\n  && rm -rf /root/.cache \\\n  && ulimit -c 0\n\nARG SOURCE_COMMIT\nRUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \\\n  && cd ${QL_DIR} \\\n  && cp -f .env.example .env \\\n  && chmod 777 ${QL_DIR}/shell/*.sh \\\n  && chmod 777 ${QL_DIR}/docker/*.sh \\\n  && git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \\\n  && mkdir -p ${QL_DIR}/static \\\n  && cp -rf /static/* ${QL_DIR}/static \\\n  && rm -rf /static\n\nENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \\\n  PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \\\n  PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \\\n  HOME=/root\n\nENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \\\n  NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \\\n  PIP_CACHE_DIR=${PYTHON_HOME}/pip \\\n  PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages\n\nRUN pip3 install --prefix ${PYTHON_HOME} requests\n\nCOPY --from=builder /tmp/build/node_modules/. /ql/node_modules/\n\nWORKDIR ${QL_DIR}\n\nHEALTHCHECK --interval=5s --timeout=2s --retries=20 \\\n  CMD curl -sf --noproxy '*' http://127.0.0.1:${QlPort:-5700}/api/health || exit 1\n\nENTRYPOINT [\"./docker/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM python:3.11-alpine3.18 AS builder\nCOPY package.json .npmrc pnpm-lock.yaml /tmp/build/\nRUN set -x \\\n  && apk update \\\n  && apk add nodejs npm git \\\n  && npm i -g pnpm@8.3.1 pm2 ts-node \\\n  && cd /tmp/build \\\n  && pnpm install --prod\n\nFROM python:3.11-alpine\n\nARG QL_MAINTAINER=\"whyour\"\nLABEL maintainer=\"${QL_MAINTAINER}\"\nARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git\nARG QL_BRANCH=develop\nARG PYTHON_SHORT_VERSION=3.11\n\nENV QL_DIR=/ql \\\n  QL_BRANCH=${QL_BRANCH} \\\n  LANG=C.UTF-8 \\\n  SHELL=/bin/bash \\\n  PS1=\"\\u@\\h:\\w \\$ \"\n\nVOLUME /ql/data\n  \nEXPOSE 5700\n\nCOPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/\nCOPY --from=builder /usr/local/bin/. /usr/local/bin/\n\nRUN set -x \\\n  && apk update -f \\\n  && apk upgrade \\\n  && apk --no-cache add -f bash \\\n  coreutils \\\n  git \\\n  curl \\\n  wget \\\n  tzdata \\\n  perl \\\n  openssl \\\n  nodejs \\\n  jq \\\n  openssh \\\n  procps \\\n  netcat-openbsd \\\n  unzip \\\n  npm \\\n  && rm -rf /var/cache/apk/* \\\n  && apk update \\\n  && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\\n  && echo \"Asia/Shanghai\" > /etc/timezone \\\n  && git config --global user.email \"qinglong@users.noreply.github.com\" \\\n  && git config --global user.name \"qinglong\" \\\n  && git config --global http.postBuffer 524288000 \\\n  && rm -rf /root/.cache \\\n  && ulimit -c 0\n\nARG SOURCE_COMMIT\nRUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \\\n  && cd ${QL_DIR} \\\n  && cp -f .env.example .env \\\n  && chmod 777 ${QL_DIR}/shell/*.sh \\\n  && chmod 777 ${QL_DIR}/docker/*.sh \\\n  && git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \\\n  && mkdir -p ${QL_DIR}/static \\\n  && cp -rf /static/* ${QL_DIR}/static \\\n  && rm -rf /static\n\nENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \\\n  PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \\\n  PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \\\n  HOME=/root\n\nENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \\\n  NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \\\n  PIP_CACHE_DIR=${PYTHON_HOME}/pip \\\n  PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages\n\nRUN pip3 install --prefix ${PYTHON_HOME} requests\n\nCOPY --from=builder /tmp/build/node_modules/. /ql/node_modules/\n\nWORKDIR ${QL_DIR}\n\nHEALTHCHECK --interval=5s --timeout=2s --retries=20 \\\n  CMD curl -sf --noproxy '*' http://127.0.0.1:${QlPort:-5700}/api/health || exit 1\n\nENTRYPOINT [\"./docker/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "services:\n  web:\n    image: whyour/qinglong:latest # 基于 Debian 的版本：whyour/qinglong:debian  \n    volumes:\n      - ./data:/ql/data\n    ports:\n      - \"5700:5700\"\n    environment:\n      QlBaseUrl: '/' # 部署路径非必须，以斜杠开头和结尾，比如 /test/\n    restart: unless-stopped\n"
  },
  {
    "path": "docker/docker-entrypoint.sh",
    "content": "#!/bin/bash\n\ndir_shell=/ql/shell\n. $dir_shell/share.sh\n\nexport_ql_envs() {\n  export BACK_PORT=\"${ql_port}\"\n  export GRPC_PORT=\"${ql_grpc_port}\"\n}\n\nlog_with_style() {\n  local level=\"$1\"\n  local message=\"$2\"\n  local timestamp=$(date '+%Y-%m-%d %H:%M:%S')\n  printf \"\\n[%s] [%7s]  %s\\n\" \"${timestamp}\" \"${level}\" \"${message}\"\n}\n\n# Fix DNS resolution issues in Alpine Linux\n# Alpine uses musl libc which has known DNS resolver issues with certain domains\n# Adding ndots:0 prevents unnecessary search domain appending\nif [ -f /etc/alpine-release ]; then\n  if ! grep -q \"^options ndots:0\" /etc/resolv.conf 2>/dev/null; then\n    echo \"options ndots:0\" >> /etc/resolv.conf\n    log_with_style \"INFO\" \"🔧  0. 已配置 DNS 解析优化 (ndots:0)\"\n  fi\nfi\n\nlog_with_style \"INFO\" \"🚀  1. 检测配置文件...\"\nload_ql_envs\nexport_ql_envs\n. $dir_shell/env.sh\nimport_config \"$@\"\nfix_config\n\n# Try to initialize PM2, but don't fail if it doesn't work\npm2 l &>/dev/null || log_with_style \"WARN\" \"PM2 初始化可能失败，将在启动时尝试使用备用方案\"\n\nlog_with_style \"INFO\" \"⚙️  2. 启动 pm2 服务...\"\nreload_pm2\n\nif [[ $AutoStartBot == true ]]; then\n  log_with_style \"INFO\" \"🤖  3. 启动 bot...\"\n  nohup ql bot >$dir_log/bot.log 2>&1 &\nfi\n\nif [[ $EnableExtraShell == true ]]; then\n  log_with_style \"INFO\" \"🛠️  4. 执行自定义脚本...\"\n  nohup ql extra >$dir_log/extra.log 2>&1 &\nfi\n\nlog_with_style \"SUCCESS\" \"🎉  容器启动成功!\"\n\ncrond -f >/dev/null\n\nexec \"$@\"\n"
  },
  {
    "path": "ecosystem.config.js",
    "content": "module.exports = {\n  apps: [\n    {\n      name: 'qinglong',\n      max_restarts: 5,\n      kill_timeout: 1000,\n      wait_ready: true,\n      listen_timeout: 5000,\n      source_map_support: true,\n      time: true,\n      script: 'static/build/app.js',\n      env: {\n        http_proxy: '',\n        https_proxy: '',\n        HTTP_PROXY: '',\n        HTTPS_PROXY: '',\n        all_proxy: '',\n        ALL_PROXY: '',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "nodemon.json",
    "content": "{\n  \"watch\": [\n    \"back\",\n    \".env\"\n  ],\n  \"ext\": \"js,ts,json\",\n  \"env\": {\n    \"NODE_ENV\": \"development\",\n    \"TS_NODE_PROJECT\": \"./back/tsconfig.json\"\n  },\n  \"verbose\": true,\n  \"execMap\": {\n    \"ts\": \"node --require ts-node/register\"\n  }\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"packageManager\": \"pnpm@8.3.1\",\n  \"scripts\": {\n    \"start\": \"concurrently -n w: npm:start:*\",\n    \"start:back\": \"nodemon ./back/app.ts\",\n    \"start:front\": \"max dev\",\n    \"build:front\": \"max build\",\n    \"build:back\": \"tsc -p back/tsconfig.json\",\n    \"panel\": \"npm run build:back && node static/build/app.js\",\n    \"gen:proto\": \"protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false\",\n    \"prettier\": \"prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'\",\n    \"postinstall\": \"max setup 2>/dev/null || true\",\n    \"test\": \"umi-test\",\n    \"test:coverage\": \"umi-test --coverage\"\n  },\n  \"gitHooks\": {\n    \"pre-commit\": \"lint-staged\"\n  },\n  \"lint-staged\": {\n    \"*.{js,jsx,less,md,json}\": [\n      \"prettier --write\"\n    ],\n    \"*.ts?(x)\": [\n      \"prettier --parser=typescript --write\"\n    ]\n  },\n  \"pnpm\": {\n    \"peerDependencyRules\": {\n      \"ignoreMissing\": [\n        \"react\",\n        \"react-dom\",\n        \"antd\",\n        \"dva\",\n        \"postcss\",\n        \"webpack\",\n        \"eslint\",\n        \"stylelint\",\n        \"redux\",\n        \"@babel/core\",\n        \"monaco-editor\",\n        \"rc-field-form\",\n        \"@types/lodash.merge\",\n        \"rollup\",\n        \"styled-components\"\n      ],\n      \"allowedVersions\": {\n        \"react\": \"18\",\n        \"react-dom\": \"18\",\n        \"dva-core\": \"2\"\n      }\n    },\n    \"overrides\": {\n      \"sqlite3\": \"git+https://github.com/whyour/node-sqlite3.git#v1.0.3\"\n    }\n  },\n  \"dependencies\": {\n    \"@grpc/grpc-js\": \"^1.14.0\",\n    \"@grpc/proto-loader\": \"^0.8.0\",\n    \"@otplib/preset-default\": \"^12.0.1\",\n    \"body-parser\": \"^1.20.3\",\n    \"celebrate\": \"^15.0.3\",\n    \"chokidar\": \"^4.0.1\",\n    \"cors\": \"^2.8.5\",\n    \"cron-parser\": \"^5.4.0\",\n    \"cross-spawn\": \"^7.0.6\",\n    \"dayjs\": \"^1.11.13\",\n    \"dotenv\": \"^16.4.6\",\n    \"express\": \"^4.21.2\",\n    \"express-jwt\": \"^8.4.1\",\n    \"express-rate-limit\": \"^7.4.1\",\n    \"express-urlrewrite\": \"^2.0.3\",\n    \"undici\": \"^7.9.0\",\n    \"hpagent\": \"^1.2.0\",\n    \"http-proxy-middleware\": \"^3.0.3\",\n    \"iconv-lite\": \"^0.6.3\",\n    \"js-yaml\": \"^4.1.0\",\n    \"jsonwebtoken\": \"^9.0.2\",\n    \"lodash\": \"^4.17.21\",\n    \"multer\": \"2.1.1\",\n    \"node-schedule\": \"^2.1.0\",\n    \"nodemailer\": \"^8.0.1\",\n    \"p-queue-cjs\": \"7.3.4\",\n    \"@bufbuild/protobuf\": \"^2.10.0\",\n    \"ps-tree\": \"^1.2.0\",\n    \"reflect-metadata\": \"^0.2.2\",\n    \"sequelize\": \"^6.37.5\",\n    \"sockjs\": \"^0.3.24\",\n    \"sqlite3\": \"git+https://github.com/whyour/node-sqlite3.git#v1.0.3\",\n    \"toad-scheduler\": \"^3.0.1\",\n    \"typedi\": \"^0.10.0\",\n    \"uuid\": \"^11.0.3\",\n    \"winston\": \"^3.17.0\",\n    \"winston-daily-rotate-file\": \"^5.0.0\",\n    \"request-ip\": \"3.3.0\",\n    \"ip2region\": \"2.3.0\",\n    \"keyv\": \"^5.2.3\",\n    \"@keyv/sqlite\": \"^4.0.1\",\n    \"proper-lockfile\": \"^4.1.2\",\n    \"compression\": \"^1.7.4\",\n    \"helmet\": \"^8.1.0\"\n  },\n  \"devDependencies\": {\n    \"moment\": \"2.30.1\",\n    \"@ant-design/icons\": \"^5.0.1\",\n    \"@ant-design/pro-layout\": \"6.38.22\",\n    \"@codemirror/view\": \"^6.34.1\",\n    \"@codemirror/state\": \"^6.4.1\",\n    \"@monaco-editor/react\": \"4.2.1\",\n    \"@react-hook/resize-observer\": \"^2.0.2\",\n    \"react-router-dom\": \"6.26.1\",\n    \"@types/body-parser\": \"^1.19.2\",\n    \"@types/cors\": \"^2.8.12\",\n    \"@types/cross-spawn\": \"^6.0.2\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-jwt\": \"^6.0.4\",\n    \"@types/file-saver\": \"2.0.2\",\n    \"@types/js-yaml\": \"^4.0.5\",\n    \"@types/jsonwebtoken\": \"^8.5.8\",\n    \"@types/lodash\": \"^4.14.185\",\n    \"@types/multer\": \"^1.4.7\",\n    \"@types/node\": \"^17.0.21\",\n    \"@types/node-schedule\": \"^1.3.2\",\n    \"@types/nodemailer\": \"^6.4.4\",\n    \"@types/qrcode.react\": \"^1.0.2\",\n    \"@types/react\": \"^18.0.20\",\n    \"@types/react-copy-to-clipboard\": \"^5.0.4\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"@types/serve-handler\": \"^6.1.1\",\n    \"@types/sockjs\": \"^0.3.33\",\n    \"@types/sockjs-client\": \"^1.5.1\",\n    \"@types/uuid\": \"^8.3.4\",\n    \"@types/request-ip\": \"0.0.41\",\n    \"@types/proper-lockfile\": \"^4.1.4\",\n    \"@types/ps-tree\": \"^1.1.6\",\n    \"@uiw/codemirror-extensions-langs\": \"^4.21.9\",\n    \"@uiw/react-codemirror\": \"^4.21.9\",\n    \"@umijs/max\": \"^4.4.4\",\n    \"@umijs/ssr-darkreader\": \"^4.9.45\",\n    \"ahooks\": \"^3.7.8\",\n    \"ansi-to-react\": \"^6.1.6\",\n    \"antd\": \"^4.24.16\",\n    \"antd-img-crop\": \"^4.23.0\",\n    \"axios\": \"^1.4.0\",\n    \"compression-webpack-plugin\": \"9.2.0\",\n    \"concurrently\": \"^7.0.0\",\n    \"react-hotkeys-hook\": \"^4.6.1\",\n    \"file-saver\": \"2.0.2\",\n    \"lint-staged\": \"^13.0.3\",\n    \"monaco-editor\": \"0.33.0\",\n    \"nodemon\": \"^3.0.1\",\n    \"prettier\": \"^2.5.1\",\n    \"pretty-bytes\": \"6.1.1\",\n    \"qiniu\": \"^7.4.0\",\n    \"qrcode.react\": \"^1.0.1\",\n    \"query-string\": \"^7.1.1\",\n    \"rc-tween-one\": \"^3.0.6\",\n    \"rc-virtual-list\": \"3.15.0\",\n    \"react\": \"18.3.1\",\n    \"react-copy-to-clipboard\": \"^5.1.0\",\n    \"react-diff-viewer\": \"^3.1.1\",\n    \"react-dnd\": \"^16.0.1\",\n    \"react-dnd-html5-backend\": \"^16.0.1\",\n    \"react-dom\": \"18.3.1\",\n    \"react-intl-universal\": \"^2.12.0\",\n    \"react-split-pane\": \"^0.1.92\",\n    \"sockjs-client\": \"^1.6.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"ts-proto\": \"^2.6.1\",\n    \"tslib\": \"^2.4.0\",\n    \"typescript\": \"5.2.2\",\n    \"vh-check\": \"^2.0.5\",\n    \"virtualizedtableforantd4\": \"1.3.0\",\n    \"@types/compression\": \"^1.7.2\",\n    \"@types/helmet\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "sample/auth.sample.json",
    "content": "{ \"username\": \"admin\", \"password\": \"admin\" }\n"
  },
  {
    "path": "sample/config.sample.sh",
    "content": "## 在运行 ql repo 命令时，是否自动删除失效的脚本与定时任务\nAutoDelCron=\"true\"\n\n## 在运行 ql repo 命令时，是否自动增加新的本地定时任务\nAutoAddCron=\"true\"\n\n## 拉取脚本时默认的定时规则，当匹配不到定时规则时使用，例如: 0 9 * * *\nDefaultCronRule=\"\"\n\n## ql repo命令拉取脚本时需要拉取的文件后缀，直接写文件后缀名即可\nRepoFileExtensions=\"js mjs py pyc\"\n\n## 代理地址，支持HTTP/SOCK5，例如 http://127.0.0.1:7890\nProxyUrl=\"\"\n\n## 资源告警阙值，默认CPU 80%、内存80%、磁盘90%\nCpuWarn=80\nMemoryWarn=80\nDiskWarn=90\n\n## 设置定时任务执行的超时时间，例如1h，后缀\"s\"代表秒(默认值), \"m\"代表分, \"h\"代表小时, \"d\"代表天\nCommandTimeoutTime=\"\"\n\n## 在运行 task 命令时，随机延迟启动任务的最大延迟时间，如 RandomDelay=\"300\" ，表示任务将在 1-300 秒内随机延迟一个秒数，然后再运行，取消延迟赋值为空\nRandomDelay=\"\"\n\n## 需要随机延迟运行任务的文件后缀，直接写后缀名即可，多个后缀用空格分开，例如: js py ts\n## 默认仅给javascript任务加随机延迟，其它任务按定时规则准点运行。全部任务随机延迟赋值为空\nRandomDelayFileExtensions=\"\"\n\n## 每小时的第几分钟准点运行任务，当在这些时间运行任务时将忽略 RandomDelay 配置，不会被随机延迟\n## 默认是第0分钟和第30分钟，例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空\nRandomDelayIgnoredMinutes=\"\"\n\n## 如果你自己会写shell脚本，并且希望在每次容器启动时，额外运行你的 shell 脚本，请赋值为 \"true\"\nEnableExtraShell=\"\"\n\n## 是否自动启动bot，默认不启动，设置为true时自动启动，目前需要自行克隆bot仓库所需代码，存到ql/repo目录下，文件夹命名为dockerbot\nAutoStartBot=\"\"\n\n## 是否使用第三方bot，默认不使用，使用时填入仓库地址，存到ql/repo目录下，文件夹命名为diybot\nBotRepoUrl=\"\"\n\n## 通知环境变量\n## 1. Server酱\n## https://sct.ftqq.com/r/13363\n## 下方填写 SCHKEY 值或 SendKey 值\nexport PUSH_KEY=\"\"\n\n## 2. BARK\n## 下方填写app提供的设备码，例如：https://api.day.app/123 那么此处的设备码就是123\nexport BARK_PUSH=\"\"\n## 下方填写推送图标设置，自定义推送图标(需iOS15或以上)\nexport BARK_ICON=\"https://qn.whyour.cn/logo.png\"\n## 下方填写推送声音设置，例如choo，具体值请在bark-推送铃声-查看所有铃声\nexport BARK_SOUND=\"\"\n## 下方填写推送消息分组，默认为\"QingLong\"\nexport BARK_GROUP=\"QingLong\"\n## bark 推送时效性\nexport BARK_LEVEL=\"active\"\n## bark 推送是否存档\nexport BARK_ARCHIVE=\"\"\n## bark 推送跳转 URL\nexport BARK_URL=\"\"\n\n## 3. Telegram\n## 下方填写自己申请@BotFather的Token，如10xxx4:AAFcqxxxxgER5uw\nexport TG_BOT_TOKEN=\"\"\n## 下方填写 @getuseridbot 中获取到的纯数字ID\nexport TG_USER_ID=\"\"\n## Telegram 代理IP（选填）\n## 下方填写代理IP地址，代理类型为 http，比如您代理是 http://127.0.0.1:1080，则填写 \"127.0.0.1\"\n## 如需使用，请自行解除下一行的注释\nexport TG_PROXY_HOST=\"\"\n## Telegram 代理端口（选填）\n## 下方填写代理端口号，代理类型为 http，比如您代理是 http://127.0.0.1:1080，则填写 \"1080\"\n## 如需使用，请自行解除下一行的注释\nexport TG_PROXY_PORT=\"\"\n## Telegram 代理的认证参数（选填）\nexport TG_PROXY_AUTH=\"\"\n## Telegram api自建反向代理地址（选填）\n## 教程：https://www.hostloc.com/thread-805441-1-1.html\n## 如反向代理地址 http://aaa.bbb.ccc 则填写 aaa.bbb.ccc\n## 如需使用，请赋值代理地址链接，并自行解除下一行的注释\nexport TG_API_HOST=\"\"\n\n## 4. 钉钉\n## 官方文档：https://developers.dingtalk.com/document/app/custom-robot-access\n## 下方填写token后面的内容，只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可\nexport DD_BOT_TOKEN=\"\"\nexport DD_BOT_SECRET=\"\"\n\n## 企业微信反向代理地址\n## (环境变量名 QYWX_ORIGIN)\nexport QYWX_ORIGIN=\"\"\n\n## 5. 企业微信机器人\n## 官方说明文档：https://work.weixin.qq.com/api/doc/90000/90136/91770\n## 下方填写密钥，企业微信推送 webhook 后面的 key\nexport QYWX_KEY=\"\"\n\n## 6. 企业微信应用\n## 参考文档：http://note.youdao.com/s/HMiudGkb\n## 下方填写素材库图片id（corpid,corpsecret,touser,agentid），素材库图片填0为图文消息, 填1为纯文本消息\nexport QYWX_AM=\"\"\n\n## 7. iGot聚合\n## 参考文档：https://wahao.github.io/Bark-MP-helper\n## 下方填写iGot的推送key，支持多方式推送，确保消息可达\nexport IGOT_PUSH_KEY=\"\"\n\n## 8. Push Plus\n## 官方网站：http://www.pushplus.plus\n## 下方填写您的Token，微信扫码登录后一对一推送或一对多推送下面的token，只填 PUSH_PLUS_TOKEN 默认为一对一推送\nexport PUSH_PLUS_TOKEN=\"\"\n## 一对一多推送（选填）\n## 下方填写您的一对多推送的 \"群组编码\" ，（一对多推送下面->您的群组(如无则新建)->群组编码）\n## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人，也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送\nexport PUSH_PLUS_USER=\"\"\n## 发送模板，支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay\nexport PUSH_PLUS_TEMPLATE=\"html\"\n## 发送渠道，支持wechat,webhook,cp,mail,sms\nexport PUSH_PLUS_CHANNEL=\"wechat\"\n## webhook编码，可在pushplus公众号上扩展配置出更多渠道\nexport PUSH_PLUS_WEBHOOK=\"\"\n## 发送结果回调地址，会把推送最终结果通知到这个地址上\nexport PUSH_PLUS_CALLBACKURL=\"\"\n## 好友令牌，微信公众号渠道填写好友令牌，企业微信渠道填写企业微信用户id\nexport PUSH_PLUS_TO=\"\"\n\n## 9. 微加机器人\n## 官方网站：http://www.weplusbot.com\n## 下方填写您的Token；微信扫描登录后在\"我的\"->\"设置\"->\"令牌\"中获取\nexport WE_PLUS_BOT_TOKEN=\"\"\n## 消息接收人；\n## 个人版填写接收消息的群编码，不填发送给自己的微信号\n## 专业版不填默认发给机器人自己，发送给好友填写wxid，发送给微信群填写群编码\nexport WE_PLUS_BOT_RECEIVER=\"\"\n## 调用版本；分为专业版和个人版，专业版填写pro，个人版填写personal\nexport WE_PLUS_BOT_VERSION=\"pro\"\n\n## 10. go-cqhttp\n## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg  群：http://127.0.0.1/send_group_msg\n## gobot_token 填写在go-cqhttp文件设置的访问密钥\n## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群\n## go-cqhttp相关API https://docs.go-cqhttp.org/api\nexport GOBOT_URL=\"\"\nexport GOBOT_TOKEN=\"\"\nexport GOBOT_QQ=\"\"\n\n## 11. gotify\n## gotify_url 填写gotify地址,如https://push.example.de:8080\n## gotify_token 填写gotify的消息应用token\n## gotify_priority 填写推送消息优先级,默认为0\nexport GOTIFY_URL=\"\"\nexport GOTIFY_TOKEN=\"\"\nexport GOTIFY_PRIORITY=0\n\n## 12. PushDeer\n## deer_key 填写PushDeer的key\nexport DEER_KEY=\"\"\n\n## 13. Chat\n## chat_url 填写synology chat地址，http://IP:PORT/webapi/***token=\n## chat_token 填写后面的token\nexport CHAT_URL=\"\"\nexport CHAT_TOKEN=\"\"\n\n## 14. aibotk\n## 官方说明文档：http://wechat.aibotk.com/oapi/oapi?from=ql\n## aibotk_key (必填)填写智能微秘书个人中心的apikey\nexport AIBOTK_KEY=\"\"\n## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效\nexport AIBOTK_TYPE=\"\"\n## aibotk_name (必填)填写群名或用户昵称，和上面的type类型要对应\nexport AIBOTK_NAME=\"\"\n\n## 15. CHRONOCAT\n## CHRONOCAT_URL 推送 http://127.0.0.1:16530\n## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥\n## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如：user_id=xxx;group_id=xxxx;group_id=xxxxx\n## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/\nexport CHRONOCAT_URL=\"\"\nexport CHRONOCAT_QQ=\"\"\nexport CHRONOCAT_TOKEN=\"\"\n\n## 16. SMTP\n## JavaScript 参数\n## 邮箱服务名称，比如126、163、Gmail、QQ等，支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json\nexport SMTP_SERVICE=\"\"\n\n## Python 参数\n## SMTP 发送邮件服务器，形如 smtp.exmail.qq.com:465\nexport SMTP_SERVER=\"\"\n## SMTP 发送邮件服务器是否使用 SSL，填写 true 或 false\nexport SMTP_SSL=\"\"\n\n## smtp_email 填写 SMTP 收发件邮箱，通知将会由自己发给自己\nexport SMTP_EMAIL=\"\"\n## smtp_password 填写 SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\nexport SMTP_PASSWORD=\"\"\n## smtp_name 填写 SMTP 收发件人姓名，可随意填写\nexport SMTP_NAME=\"\"\n\n## 17. PushMe\n## 官方说明文档：https://push.i-i.me/\n## PUSHME_KEY (必填)填写PushMe APP上获取的push_key\n## PUSHME_URL (选填)填写自建的PushMeServer消息服务接口地址，例如：http://127.0.0.1:3010，不填则使用官方接口服务\nexport PUSHME_KEY=\"\"\nexport PUSHME_URL=\"\"\n\n## 18. 飞书机器人\n## 官方文档：https://www.feishu.cn/hc/zh-CN/articles/360024984973\n## FSKEY 飞书机器人的 FSKEY\nexport FSKEY=\"\"\n\n## 19. Qmsg酱\n## 官方文档：https://qmsg.zendee.cn/docs/api/\n## qmsg 酱的 QMSG_KEY\n## qmsg 酱的 QMSG_TYPE send 为私聊，group 为群聊\nexport QMSG_KEY=\"\"\nexport QMSG_TYPE=\"\"\n\n## 20.Ntfy\n## 官方文档: https://docs.ntfy.sh\n## ntfy_url 填写ntfy地址,如https://ntfy.sh\n## ntfy_topic 填写ntfy的消息应用topic\n## ntfy_priority 填写推送消息优先级,默认为3\n## ntfy_token 填写推送token,可选\n## ntfy_username 填写推送用户名称,可选\n## ntfy_password 填写推送用户密码,可选\n## ntfy_actions 填写推送用户动作,可选\nexport NTFY_URL=\"\"\nexport NTFY_TOPIC=\"\"\nexport NTFY_PRIORITY=\"3\"\nexport NTFY_TOKEN=\"\"\nexport NTFY_USERNAME=\"\"\nexport NTFY_PASSWORD=\"\"\nexport NTFY_ACTIONS=\"\"\n\n## 21. wxPusher\n## 官方文档: https://wxpusher.zjiecode.com/docs/\n## 管理后台: https://wxpusher.zjiecode.com/admin/\n## wxPusher 的 appToken\nexport WXPUSHER_APP_TOKEN=\"\"\n## wxPusher 的 topicIds，多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行\nexport WXPUSHER_TOPIC_IDS=\"\"\n## wxPusher 的 用户ID，多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行\nexport WXPUSHER_UIDS=\"\"\n\n## 22. 自定义通知\n## 自定义通知 接收回调的URL\nexport WEBHOOK_URL=\"\"\n## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时，直接换行或者使用 $'\\n' 连接多行字符串，比如 export dd=\"line 1\"$'\\n'\"line 2\"\nexport WEBHOOK_BODY=\"\"\nexport WEBHOOK_HEADERS=\"\"\n## 支持 GET/POST/PUT\nexport WEBHOOK_METHOD=\"\"\n## 支持 text/plain、application/json、multipart/form-data、application/x-www-form-urlencoded\nexport WEBHOOK_CONTENT_TYPE=\"\"\n\n## 其他需要的变量，脚本中需要的变量使用 export 变量名= 声明即可\n"
  },
  {
    "path": "sample/extra.sample.sh",
    "content": "#!/usr/bin/env bash\n\n## 添加你需要重启自动执行的任意命令，比如 ql repo\n## 安装node依赖使用 pnpm add -g xxx xxx\n## 安装python依赖使用 pip3 install xxx\n"
  },
  {
    "path": "sample/notify.js",
    "content": "const querystring = require('node:querystring');\nconst { request: undiciRequest, ProxyAgent, FormData } = require('undici');\nconst timeout = 15000;\n\nasync function request(url, options = {}) {\n  const { json, form, body, headers = {}, ...rest } = options;\n\n  const finalHeaders = { ...headers };\n  let finalBody = body;\n\n  if (json) {\n    finalHeaders['content-type'] = 'application/json';\n    finalBody = JSON.stringify(json);\n  } else if (form) {\n    finalBody = form;\n    delete finalHeaders['content-type'];\n  }\n\n  return undiciRequest(url, {\n    headers: finalHeaders,\n    body: finalBody,\n    ...rest,\n  });\n}\n\nfunction post(url, options = {}) {\n  return request(url, { ...options, method: 'POST' });\n}\n\nfunction get(url, options = {}) {\n  return request(url, { ...options, method: 'GET' });\n}\n\nconst httpClient = {\n  request,\n  post,\n  get,\n};\n\nconst push_config = {\n  HITOKOTO: true, // 启用一言（随机句子）\n\n  BARK_PUSH: '', // bark IP 或设备码，例：https://api.day.app/DxHcxxxxxRxxxxxxcm/\n  BARK_ARCHIVE: '', // bark 推送是否存档\n  BARK_GROUP: '', // bark 推送分组\n  BARK_SOUND: '', // bark 推送声音\n  BARK_ICON: '', // bark 推送图标\n  BARK_LEVEL: '', // bark 推送时效性\n  BARK_URL: '', // bark 推送跳转URL\n\n  DD_BOT_SECRET: '', // 钉钉机器人的 DD_BOT_SECRET\n  DD_BOT_TOKEN: '', // 钉钉机器人的 DD_BOT_TOKEN\n\n  FSKEY: '', // 飞书机器人的 FSKEY\n  FSSECRET: '', // 飞书机器人的 FSSECRET，对应安全设置里的签名校验密钥\n\n  // 推送到个人QQ：http://127.0.0.1/send_private_msg\n  // 群：http://127.0.0.1/send_group_msg\n  GOBOT_URL: '', // go-cqhttp\n  // 推送到个人QQ 填入 user_id=个人QQ\n  // 群 填入 group_id=QQ群\n  GOBOT_QQ: '', // go-cqhttp 的推送群或用户\n  GOBOT_TOKEN: '', // go-cqhttp 的 access_token\n\n  GOTIFY_URL: '', // gotify地址,如https://push.example.de:8080\n  GOTIFY_TOKEN: '', // gotify的消息应用token\n  GOTIFY_PRIORITY: 0, // 推送消息优先级,默认为0\n\n  IGOT_PUSH_KEY: '', // iGot 聚合推送的 IGOT_PUSH_KEY，例如：https://push.hellyw.com/XXXXXXXX\n\n  PUSH_KEY: '', // server 酱的 PUSH_KEY，兼容旧版与 Turbo 版\n\n  DEER_KEY: '', // PushDeer 的 PUSHDEER_KEY\n  DEER_URL: '', // PushDeer 的 PUSHDEER_URL\n\n  CHAT_URL: '', // synology chat url\n  CHAT_TOKEN: '', // synology chat token\n\n  // 官方文档：https://www.pushplus.plus/\n  PUSH_PLUS_TOKEN: '', // pushplus 推送的用户令牌\n  PUSH_PLUS_USER: '', // pushplus 推送的群组编码\n  PUSH_PLUS_TEMPLATE: 'html', // pushplus 发送模板，支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay\n  PUSH_PLUS_CHANNEL: 'wechat', // pushplus 发送渠道，支持wechat,webhook,cp,mail,sms\n  PUSH_PLUS_WEBHOOK: '', // pushplus webhook编码，可在pushplus公众号上扩展配置出更多渠道\n  PUSH_PLUS_CALLBACKURL: '', // pushplus 发送结果回调地址，会把推送最终结果通知到这个地址上\n  PUSH_PLUS_TO: '', // pushplus 好友令牌，微信公众号渠道填写好友令牌，企业微信渠道填写企业微信用户id\n\n  // 微加机器人，官方网站：https://www.weplusbot.com/\n  WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌\n  WE_PLUS_BOT_RECEIVER: '', // 微加机器人的消息接收人\n  WE_PLUS_BOT_VERSION: 'pro', //微加机器人调用版本，pro和personal；为空默认使用pro(专业版)，个人版填写：personal\n\n  QMSG_KEY: '', // qmsg 酱的 QMSG_KEY\n  QMSG_TYPE: '', // qmsg 酱的 QMSG_TYPE\n\n  QYWX_ORIGIN: 'https://qyapi.weixin.qq.com', // 企业微信代理地址\n\n  /*\n    此处填你企业微信应用消息的值(详见文档 https://work.weixin.qq.com/api/doc/90000/90135/90236)\n    环境变量名 QYWX_AM依次填入 corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型)\n    注意用,号隔开(英文输入法的逗号)，例如：wwcff56746d9adwers,B-791548lnzXBE6_BWfxdf3kSTMJr9vFEPKAbh6WERQ,mingcheng,1000001,2COXgjH2UIfERF2zxrtUOKgQ9XklUqMdGSWLBoW_lSDAdafat\n    可选推送消息类型(推荐使用图文消息（mpnews）):\n    - 文本卡片消息: 0 (数字零)\n    - 文本消息: 1 (数字一)\n    - 图文消息（mpnews）: 素材库图片id, 可查看此教程(http://note.youdao.com/s/HMiudGkb)或者(https://note.youdao.com/ynoteshare1/index.html?id=1a0c8aff284ad28cbd011b29b3ad0191&type=note)\n  */\n  QYWX_AM: '', // 企业微信应用\n\n  QYWX_KEY: '', // 企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)，例如：693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa\n\n  TG_BOT_TOKEN: '', // tg 机器人的 TG_BOT_TOKEN，例：1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ\n  TG_USER_ID: '', // tg 机器人的 TG_USER_ID，例：1434078534\n  TG_API_HOST: 'https://api.telegram.org', // tg 代理 api\n  TG_PROXY_AUTH: '', // tg 代理认证参数\n  TG_PROXY_HOST: '', // tg 机器人的 TG_PROXY_HOST\n  TG_PROXY_PORT: '', // tg 机器人的 TG_PROXY_PORT\n\n  AIBOTK_KEY: '', // 智能微秘书 个人中心的apikey 文档地址：http://wechat.aibotk.com/docs/about\n  AIBOTK_TYPE: '', // 智能微秘书 发送目标 room 或 contact\n  AIBOTK_NAME: '', // 智能微秘书  发送群名 或者好友昵称和type要对应好\n\n  SMTP_SERVICE: '', // 邮箱服务名称，比如 126、163、Gmail、QQ 等，支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json\n  SMTP_EMAIL: '', // SMTP 发件邮箱\n  SMTP_TO: '', // SMTP 收件邮箱，默认通知将会发给发件邮箱\n  SMTP_PASSWORD: '', // SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\n  SMTP_NAME: '', // SMTP 收发件人姓名，可随意填写\n\n  PUSHME_KEY: '', // 官方文档：https://push.i-i.me，PushMe 酱的 PUSHME_KEY\n\n  // CHRONOCAT API https://chronocat.vercel.app/install/docker/official/\n  CHRONOCAT_QQ: '', // 个人: user_id=个人QQ 群则填入 group_id=QQ群 多个用英文;隔开同时支持个人和群\n  CHRONOCAT_TOKEN: '', // 填写在CHRONOCAT文件生成的访问密钥\n  CHRONOCAT_URL: '', // Red 协议连接地址 例： http://127.0.0.1:16530\n\n  WEBHOOK_URL: '', // 自定义通知 请求地址\n  WEBHOOK_BODY: '', // 自定义通知 请求体\n  WEBHOOK_HEADERS: '', // 自定义通知 请求头\n  WEBHOOK_METHOD: '', // 自定义通知 请求方法\n  WEBHOOK_CONTENT_TYPE: '', // 自定义通知 content-type\n\n  NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh\n  NTFY_TOPIC: '', // ntfy的消息应用topic\n  NTFY_PRIORITY: '3', // 推送消息优先级,默认为3\n  NTFY_TOKEN: '', // 推送token,可选\n  NTFY_USERNAME: '', // 推送用户名称,可选\n  NTFY_PASSWORD: '', // 推送用户密码,可选\n  NTFY_ACTIONS: '', // 推送用户动作,可选\n\n  // 官方文档: https://wxpusher.zjiecode.com/docs/\n  // 管理后台: https://wxpusher.zjiecode.com/admin/\n  WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken\n  WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID，多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行\n  WXPUSHER_UIDS: '', // wxpusher 的 用户ID，多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行\n};\n\nfor (const key in push_config) {\n  const v = process.env[key];\n  if (v) {\n    push_config[key] = v;\n  }\n}\n\nconst $ = {\n  post: (params, callback) => {\n    const { url, ...others } = params;\n    httpClient.post(url, others).then(\n      async (res) => {\n        let body = await res.body.text();\n        try {\n          body = JSON.parse(body);\n        } catch (error) {}\n        callback(null, res, body);\n      },\n      (err) => {\n        callback(err?.response?.body || err);\n      },\n    );\n  },\n  get: (params, callback) => {\n    const { url, ...others } = params;\n    httpClient.get(url, others).then(\n      async (res) => {\n        let body = await res.body.text();\n        try {\n          body = JSON.parse(body);\n        } catch (error) {}\n        callback(null, res, body);\n      },\n      (err) => {\n        callback(err?.response?.body || err);\n      },\n    );\n  },\n  logErr: console.log,\n};\n\nasync function one() {\n  const url = 'https://v1.hitokoto.cn/';\n  const res = await httpClient.request(url);\n  const body = await res.body.json();\n  return `${body.hitokoto}    ----${body.from}`;\n}\n\nfunction gotifyNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { GOTIFY_URL, GOTIFY_TOKEN, GOTIFY_PRIORITY } = push_config;\n    if (GOTIFY_URL && GOTIFY_TOKEN) {\n      const options = {\n        url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,\n        body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(\n          desp,\n        )}&priority=${GOTIFY_PRIORITY}`,\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded',\n        },\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Gotify 发送通知调用API失败😞\\n', err);\n          } else {\n            if (data.id) {\n              console.log('Gotify 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`Gotify 发送通知调用API失败😞 ${data.message}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve();\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction gobotNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { GOBOT_URL, GOBOT_TOKEN, GOBOT_QQ } = push_config;\n    if (GOBOT_URL) {\n      const options = {\n        url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,\n        json: { message: `${text}\\n${desp}` },\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Go-cqhttp 通知调用API失败😞\\n', err);\n          } else {\n            if (data.retcode === 0) {\n              console.log('Go-cqhttp 发送通知消息成功🎉\\n');\n            } else if (data.retcode === 100) {\n              console.log(`Go-cqhttp 发送通知消息异常 ${data.errmsg}\\n`);\n            } else {\n              console.log(`Go-cqhttp 发送通知消息异常 ${JSON.stringify(data)}`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction serverNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { PUSH_KEY } = push_config;\n    if (PUSH_KEY) {\n      // 微信server酱推送通知一个\\n不会换行，需要两个\\n才能换行，故做此替换\n      desp = desp.replace(/[\\n\\r]/g, '\\n\\n');\n\n      const matchResult = PUSH_KEY.match(/^sctp(\\d+)t/i);\n      const options = {\n        url:\n          matchResult && matchResult[1]\n            ? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send`\n            : `https://sctapi.ftqq.com/${PUSH_KEY}.send`,\n        body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(\n          desp,\n        )}`,\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Server 酱发送通知调用API失败😞\\n', err);\n          } else {\n            // server酱和Server酱·Turbo版的返回json格式不太一样\n            if (data.errno === 0 || data.data.errno === 0) {\n              console.log('Server 酱发送通知消息成功🎉\\n');\n            } else if (data.errno === 1024) {\n              // 一分钟内发送相同的内容会触发\n              console.log(`Server 酱发送通知消息异常 ${data.errmsg}\\n`);\n            } else {\n              console.log(`Server 酱发送通知消息异常 ${JSON.stringify(data)}`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction pushDeerNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { DEER_KEY, DEER_URL } = push_config;\n    if (DEER_KEY) {\n      // PushDeer 建议对消息内容进行 urlencode\n      desp = encodeURI(desp);\n      const options = {\n        url: DEER_URL || `https://api2.pushdeer.com/message/push`,\n        body: `pushkey=${DEER_KEY}&text=${text}&desp=${desp}&type=markdown`,\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('PushDeer 通知调用API失败😞\\n', err);\n          } else {\n            // 通过返回的result的长度来判断是否成功\n            if (\n              data.content.result.length !== undefined &&\n              data.content.result.length > 0\n            ) {\n              console.log('PushDeer 发送通知消息成功🎉\\n');\n            } else {\n              console.log(\n                `PushDeer 发送通知消息异常😞 ${JSON.stringify(data)}`,\n              );\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction chatNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { CHAT_URL, CHAT_TOKEN } = push_config;\n    if (CHAT_URL && CHAT_TOKEN) {\n      // 对消息内容进行 urlencode\n      desp = encodeURI(desp);\n      const options = {\n        url: `${CHAT_URL}${CHAT_TOKEN}`,\n        body: `payload={\"text\":\"${text}\\n${desp}\"}`,\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded',\n        },\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Chat 发送通知调用API失败😞\\n', err);\n          } else {\n            if (data.success) {\n              console.log('Chat 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`Chat 发送通知消息异常 ${JSON.stringify(data)}`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction barkNotify(text, desp, params = {}) {\n  return new Promise((resolve) => {\n    let {\n      BARK_PUSH,\n      BARK_ICON,\n      BARK_SOUND,\n      BARK_GROUP,\n      BARK_LEVEL,\n      BARK_ARCHIVE,\n      BARK_URL,\n    } = push_config;\n    if (BARK_PUSH) {\n      // 兼容BARK本地用户只填写设备码的情况\n      if (!BARK_PUSH.startsWith('http')) {\n        BARK_PUSH = `https://api.day.app/${BARK_PUSH}`;\n      }\n      const options = {\n        url: `${BARK_PUSH}`,\n        json: {\n          title: text,\n          body: desp,\n          icon: BARK_ICON,\n          sound: BARK_SOUND,\n          group: BARK_GROUP,\n          isArchive: BARK_ARCHIVE,\n          level: BARK_LEVEL,\n          url: BARK_URL,\n          ...params,\n        },\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Bark APP 发送通知调用API失败😞\\n', err);\n          } else {\n            if (data.code === 200) {\n              console.log('Bark APP 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`Bark APP 发送通知消息异常 ${data.message}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve();\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction tgBotNotify(text, desp) {\n  return new Promise((resolve) => {\n    const {\n      TG_BOT_TOKEN,\n      TG_USER_ID,\n      TG_PROXY_HOST,\n      TG_PROXY_PORT,\n      TG_API_HOST,\n      TG_PROXY_AUTH,\n    } = push_config;\n    if (TG_BOT_TOKEN && TG_USER_ID) {\n      let options = {\n        url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,\n        json: {\n          chat_id: `${TG_USER_ID}`,\n          text: `${text}\\n\\n${desp}`,\n          disable_web_page_preview: true,\n        },\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      if (TG_PROXY_HOST && TG_PROXY_PORT) {\n        let proxyHost = TG_PROXY_HOST;\n        if (TG_PROXY_AUTH && !TG_PROXY_HOST.includes('@')) {\n          proxyHost = `${TG_PROXY_AUTH}@${TG_PROXY_HOST}`;\n        }\n        let agent;\n        agent = new ProxyAgent({\n          uri: `http://${proxyHost}:${TG_PROXY_PORT}`,\n        });\n        options.dispatcher = agent;\n      }\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Telegram 发送通知消息失败😞\\n', err);\n          } else {\n            if (data.ok) {\n              console.log('Telegram 发送通知消息成功🎉。\\n');\n            } else if (data.error_code === 400) {\n              console.log(\n                '请主动给bot发送一条消息并检查接收用户ID是否正确。\\n',\n              );\n            } else if (data.error_code === 401) {\n              console.log('Telegram bot token 填写错误。\\n');\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\nfunction ddBotNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { DD_BOT_TOKEN, DD_BOT_SECRET } = push_config;\n    const options = {\n      url: `https://oapi.dingtalk.com/robot/send?access_token=${DD_BOT_TOKEN}`,\n      json: {\n        msgtype: 'text',\n        text: {\n          content: `${text}\\n\\n${desp}`,\n        },\n      },\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      timeout,\n    };\n    if (DD_BOT_TOKEN && DD_BOT_SECRET) {\n      const crypto = require('crypto');\n      const dateNow = Date.now();\n      const hmac = crypto.createHmac('sha256', DD_BOT_SECRET);\n      hmac.update(`${dateNow}\\n${DD_BOT_SECRET}`);\n      const result = encodeURIComponent(hmac.digest('base64'));\n      options.url = `${options.url}&timestamp=${dateNow}&sign=${result}`;\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('钉钉发送通知消息失败😞\\n', err);\n          } else {\n            if (data.errcode === 0) {\n              console.log('钉钉发送通知消息成功🎉\\n');\n            } else {\n              console.log(`钉钉发送通知消息异常 ${data.errmsg}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else if (DD_BOT_TOKEN) {\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('钉钉发送通知消息失败😞\\n', err);\n          } else {\n            if (data.errcode === 0) {\n              console.log('钉钉发送通知消息成功🎉\\n');\n            } else {\n              console.log(`钉钉发送通知消息异常 ${data.errmsg}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction qywxBotNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { QYWX_ORIGIN, QYWX_KEY } = push_config;\n    const options = {\n      url: `${QYWX_ORIGIN}/cgi-bin/webhook/send?key=${QYWX_KEY}`,\n      json: {\n        msgtype: 'text',\n        text: {\n          content: `${text}\\n\\n${desp}`,\n        },\n      },\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      timeout,\n    };\n    if (QYWX_KEY) {\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('企业微信发送通知消息失败😞\\n', err);\n          } else {\n            if (data.errcode === 0) {\n              console.log('企业微信发送通知消息成功🎉。\\n');\n            } else {\n              console.log(`企业微信发送通知消息异常 ${data.errmsg}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction ChangeUserId(desp) {\n  const { QYWX_AM } = push_config;\n  const QYWX_AM_AY = QYWX_AM.split(',');\n  if (QYWX_AM_AY[2]) {\n    const userIdTmp = QYWX_AM_AY[2].split('|');\n    let userId = '';\n    for (let i = 0; i < userIdTmp.length; i++) {\n      const count = '账号' + (i + 1);\n      const count2 = '签到号 ' + (i + 1);\n      if (desp.match(count2)) {\n        userId = userIdTmp[i];\n      }\n    }\n    if (!userId) userId = QYWX_AM_AY[2];\n    return userId;\n  } else {\n    return '@all';\n  }\n}\n\nasync function qywxamNotify(text, desp) {\n  const MAX_LENGTH = 900;\n  if (desp.length > MAX_LENGTH) {\n    let d = desp.substr(0, MAX_LENGTH) + '\\n==More==';\n    await do_qywxamNotify(text, d);\n    await qywxamNotify(text, desp.substr(MAX_LENGTH));\n  } else {\n    return await do_qywxamNotify(text, desp);\n  }\n}\n\nfunction do_qywxamNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { QYWX_AM, QYWX_ORIGIN } = push_config;\n    if (QYWX_AM) {\n      const QYWX_AM_AY = QYWX_AM.split(',');\n      const options_accesstoken = {\n        url: `${QYWX_ORIGIN}/cgi-bin/gettoken`,\n        json: {\n          corpid: `${QYWX_AM_AY[0]}`,\n          corpsecret: `${QYWX_AM_AY[1]}`,\n        },\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      $.post(options_accesstoken, (err, resp, json) => {\n        let html = desp.replace(/\\n/g, '<br/>');\n        let accesstoken = json.access_token;\n        let options;\n\n        switch (QYWX_AM_AY[4]) {\n          case '0':\n            options = {\n              msgtype: 'textcard',\n              textcard: {\n                title: `${text}`,\n                description: `${desp}`,\n                url: 'https://github.com/whyour/qinglong',\n                btntxt: '更多',\n              },\n            };\n            break;\n\n          case '1':\n            options = {\n              msgtype: 'text',\n              text: {\n                content: `${text}\\n\\n${desp}`,\n              },\n            };\n            break;\n\n          default:\n            options = {\n              msgtype: 'mpnews',\n              mpnews: {\n                articles: [\n                  {\n                    title: `${text}`,\n                    thumb_media_id: `${QYWX_AM_AY[4]}`,\n                    author: `智能助手`,\n                    content_source_url: ``,\n                    content: `${html}`,\n                    digest: `${desp}`,\n                  },\n                ],\n              },\n            };\n        }\n        if (!QYWX_AM_AY[4]) {\n          // 如不提供第四个参数,则默认进行文本消息类型推送\n          options = {\n            msgtype: 'text',\n            text: {\n              content: `${text}\\n\\n${desp}`,\n            },\n          };\n        }\n        options = {\n          url: `${QYWX_ORIGIN}/cgi-bin/message/send?access_token=${accesstoken}`,\n          json: {\n            touser: `${ChangeUserId(desp)}`,\n            agentid: `${QYWX_AM_AY[3]}`,\n            safe: '0',\n            ...options,\n          },\n          headers: {\n            'Content-Type': 'application/json',\n          },\n        };\n\n        $.post(options, (err, resp, data) => {\n          try {\n            if (err) {\n              console.log(\n                '成员ID:' +\n                  ChangeUserId(desp) +\n                  '企业微信应用消息发送通知消息失败😞\\n',\n                err,\n              );\n            } else {\n              if (data.errcode === 0) {\n                console.log(\n                  '成员ID:' +\n                    ChangeUserId(desp) +\n                    '企业微信应用消息发送通知消息成功🎉。\\n',\n                );\n              } else {\n                console.log(\n                  `企业微信应用消息发送通知消息异常 ${data.errmsg}\\n`,\n                );\n              }\n            }\n          } catch (e) {\n            $.logErr(e, resp);\n          } finally {\n            resolve(data);\n          }\n        });\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction iGotNotify(text, desp, params = {}) {\n  return new Promise((resolve) => {\n    const { IGOT_PUSH_KEY } = push_config;\n    if (IGOT_PUSH_KEY) {\n      // 校验传入的IGOT_PUSH_KEY是否有效\n      const IGOT_PUSH_KEY_REGX = new RegExp('^[a-zA-Z0-9]{24}$');\n      if (!IGOT_PUSH_KEY_REGX.test(IGOT_PUSH_KEY)) {\n        console.log('您所提供的 IGOT_PUSH_KEY 无效\\n');\n        resolve();\n        return;\n      }\n      const options = {\n        url: `https://push.hellyw.com/${IGOT_PUSH_KEY.toLowerCase()}`,\n        body: `title=${text}&content=${desp}&${querystring.stringify(params)}`,\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('IGot 发送通知调用API失败😞\\n', err);\n          } else {\n            if (data.ret === 0) {\n              console.log('IGot 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`IGot 发送通知消息异常 ${data.errMsg}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction pushPlusNotify(text, desp) {\n  return new Promise((resolve) => {\n    const {\n      PUSH_PLUS_TOKEN,\n      PUSH_PLUS_USER,\n      PUSH_PLUS_TEMPLATE,\n      PUSH_PLUS_CHANNEL,\n      PUSH_PLUS_WEBHOOK,\n      PUSH_PLUS_CALLBACKURL,\n      PUSH_PLUS_TO,\n    } = push_config;\n    if (PUSH_PLUS_TOKEN) {\n      desp = desp.replace(/[\\n\\r]/g, '<br>'); // 默认为html, 不支持plaintext\n      const body = {\n        token: `${PUSH_PLUS_TOKEN}`,\n        title: `${text}`,\n        content: `${desp}`,\n        topic: `${PUSH_PLUS_USER}`,\n        template: `${PUSH_PLUS_TEMPLATE}`,\n        channel: `${PUSH_PLUS_CHANNEL}`,\n        webhook: `${PUSH_PLUS_WEBHOOK}`,\n        callbackUrl: `${PUSH_PLUS_CALLBACKURL}`,\n        to: `${PUSH_PLUS_TO}`,\n      };\n      const options = {\n        url: `https://www.pushplus.plus/send`,\n        body: JSON.stringify(body),\n        headers: {\n          'Content-Type': ' application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log(\n              `pushplus 发送${\n                PUSH_PLUS_USER ? '一对多' : '一对一'\n              }通知消息失败😞\\n`,\n              err,\n            );\n          } else {\n            if (data.code === 200) {\n              console.log(\n                `pushplus 发送${\n                  PUSH_PLUS_USER ? '一对多' : '一对一'\n                }通知请求成功🎉，可根据流水号查询推送结果：${\n                  data.data\n                }\\n注意：请求成功并不代表推送成功，如未收到消息，请到pushplus官网使用流水号查询推送最终结果`,\n              );\n            } else {\n              console.log(\n                `pushplus 发送${\n                  PUSH_PLUS_USER ? '一对多' : '一对一'\n                }通知消息异常 ${data.msg}\\n`,\n              );\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction wePlusBotNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { WE_PLUS_BOT_TOKEN, WE_PLUS_BOT_RECEIVER, WE_PLUS_BOT_VERSION } =\n      push_config;\n    if (WE_PLUS_BOT_TOKEN) {\n      let template = 'txt';\n      if (desp.length > 800) {\n        desp = desp.replace(/[\\n\\r]/g, '<br>');\n        template = 'html';\n      }\n      const body = {\n        token: `${WE_PLUS_BOT_TOKEN}`,\n        title: `${text}`,\n        content: `${desp}`,\n        template: `${template}`,\n        receiver: `${WE_PLUS_BOT_RECEIVER}`,\n        version: `${WE_PLUS_BOT_VERSION}`,\n      };\n      const options = {\n        url: `https://www.weplusbot.com/send`,\n        body: JSON.stringify(body),\n        headers: {\n          'Content-Type': ' application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log(`微加机器人发送通知消息失败😞\\n`, err);\n          } else {\n            if (data.code === 200) {\n              console.log(`微加机器人发送通知消息完成🎉\\n`);\n            } else {\n              console.log(`微加机器人发送通知消息异常 ${data.msg}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction aibotkNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { AIBOTK_KEY, AIBOTK_TYPE, AIBOTK_NAME } = push_config;\n    if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {\n      let json = {};\n      let url = '';\n      switch (AIBOTK_TYPE) {\n        case 'room':\n          url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';\n          json = {\n            apiKey: `${AIBOTK_KEY}`,\n            roomName: `${AIBOTK_NAME}`,\n            message: {\n              type: 1,\n              content: `【青龙快讯】\\n\\n${text}\\n${desp}`,\n            },\n          };\n          break;\n        case 'contact':\n          url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';\n          json = {\n            apiKey: `${AIBOTK_KEY}`,\n            name: `${AIBOTK_NAME}`,\n            message: {\n              type: 1,\n              content: `【青龙快讯】\\n\\n${text}\\n${desp}`,\n            },\n          };\n          break;\n      }\n      const options = {\n        url: url,\n        json,\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('智能微秘书发送通知消息失败😞\\n', err);\n          } else {\n            if (data.code === 0) {\n              console.log('智能微秘书发送通知消息成功🎉。\\n');\n            } else {\n              console.log(`智能微秘书发送通知消息异常 ${data.error}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction fsBotNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { FSKEY, FSSECRET } = push_config;\n    if (FSKEY) {\n      const body = {\n        msg_type: 'text',\n        content: { text: `${text}\\n\\n${desp}` },\n      };\n\n      // Add signature if secret is provided\n      // Note: Feishu's signature algorithm uses timestamp+\"\\n\"+secret as the HMAC key\n      // and signs an empty message, which differs from typical HMAC usage\n      if (FSSECRET) {\n        const crypto = require('crypto');\n        const timestamp = Math.floor(Date.now() / 1000).toString();\n        const stringToSign = `${timestamp}\\n${FSSECRET}`;\n        const hmac = crypto.createHmac('sha256', stringToSign);\n        const sign = hmac.digest('base64');\n        body.timestamp = timestamp;\n        body.sign = sign;\n      }\n\n      const options = {\n        url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,\n        json: body,\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('飞书发送通知调用API失败😞\\n', err);\n          } else {\n            if (data.StatusCode === 0 || data.code === 0) {\n              console.log('飞书发送通知消息成功🎉\\n');\n            } else {\n              console.log(`飞书发送通知消息异常 ${data.msg}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nasync function smtpNotify(text, desp) {\n  const { SMTP_EMAIL, SMTP_TO, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } =\n    push_config;\n  if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {\n    return;\n  }\n\n  try {\n    const nodemailer = require('nodemailer');\n    const transporter = nodemailer.createTransport({\n      service: SMTP_SERVICE,\n      auth: {\n        user: SMTP_EMAIL,\n        pass: SMTP_PASSWORD,\n      },\n    });\n\n    const addr = SMTP_NAME ? `\"${SMTP_NAME}\" <${SMTP_EMAIL}>` : SMTP_EMAIL;\n    const info = await transporter.sendMail({\n      from: addr,\n      to: SMTP_TO ? SMTP_TO.split(';') : addr,\n      subject: text,\n      html: `${desp.replace(/\\n/g, '<br/>')}`,\n    });\n\n    transporter.close();\n\n    if (info.messageId) {\n      console.log('SMTP 发送通知消息成功🎉\\n');\n      return true;\n    }\n    console.log('SMTP 发送通知消息失败😞\\n');\n  } catch (e) {\n    console.log('SMTP 发送通知消息出现异常😞\\n', e);\n  }\n}\n\nfunction pushMeNotify(text, desp, params = {}) {\n  return new Promise((resolve) => {\n    const { PUSHME_KEY, PUSHME_URL } = push_config;\n    if (PUSHME_KEY) {\n      const options = {\n        url: PUSHME_URL || 'https://push.i-i.me',\n        json: { push_key: PUSHME_KEY, title: text, content: desp, ...params },\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('PushMe 发送通知调用API失败😞\\n', err);\n          } else {\n            if (data === 'success') {\n              console.log('PushMe 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`PushMe 发送通知消息异常 ${data}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction chronocatNotify(title, desp) {\n  return new Promise((resolve) => {\n    const { CHRONOCAT_TOKEN, CHRONOCAT_QQ, CHRONOCAT_URL } = push_config;\n    if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {\n      resolve();\n      return;\n    }\n\n    const user_ids = CHRONOCAT_QQ.match(/user_id=(\\d+)/g)?.map(\n      (match) => match.split('=')[1],\n    );\n    const group_ids = CHRONOCAT_QQ.match(/group_id=(\\d+)/g)?.map(\n      (match) => match.split('=')[1],\n    );\n\n    const url = `${CHRONOCAT_URL}/api/message/send`;\n    const headers = {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${CHRONOCAT_TOKEN}`,\n    };\n\n    for (const [chat_type, ids] of [\n      [1, user_ids],\n      [2, group_ids],\n    ]) {\n      if (!ids) {\n        continue;\n      }\n      for (const chat_id of ids) {\n        const data = {\n          peer: {\n            chatType: chat_type,\n            peerUin: chat_id,\n          },\n          elements: [\n            {\n              elementType: 1,\n              textElement: {\n                content: `${title}\\n\\n${desp}`,\n              },\n            },\n          ],\n        };\n        const options = {\n          url: url,\n          json: data,\n          headers,\n          timeout,\n        };\n        $.post(options, (err, resp, data) => {\n          try {\n            if (err) {\n              console.log('Chronocat 发送QQ通知消息失败😞\\n', err);\n            } else {\n              if (chat_type === 1) {\n                console.log(`Chronocat 个人消息 ${ids}推送成功🎉`);\n              } else {\n                console.log(`Chronocat 群消息 ${ids}推送成功🎉`);\n              }\n            }\n          } catch (e) {\n            $.logErr(e, resp);\n          } finally {\n            resolve(data);\n          }\n        });\n      }\n    }\n  });\n}\n\nfunction qmsgNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { QMSG_KEY, QMSG_TYPE } = push_config;\n    if (QMSG_KEY && QMSG_TYPE) {\n      const options = {\n        url: `https://qmsg.zendee.cn/${QMSG_TYPE}/${QMSG_KEY}`,\n        body: `msg=${text}\\n\\n${desp.replace('----', '-')}`,\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded',\n        },\n        timeout,\n      };\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Qmsg 发送通知调用API失败😞\\n', err);\n          } else {\n            if (data.code === 0) {\n              console.log('Qmsg 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`Qmsg 发送通知消息异常 ${data}\\n`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction webhookNotify(text, desp) {\n  return new Promise((resolve) => {\n    const {\n      WEBHOOK_URL,\n      WEBHOOK_BODY,\n      WEBHOOK_HEADERS,\n      WEBHOOK_CONTENT_TYPE,\n      WEBHOOK_METHOD,\n    } = push_config;\n    if (\n      !WEBHOOK_METHOD ||\n      !WEBHOOK_URL ||\n      (!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title'))\n    ) {\n      resolve();\n      return;\n    }\n\n    const headers = parseHeaders(WEBHOOK_HEADERS);\n    const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) =>\n      v\n        ?.replaceAll('$title', text?.replaceAll('\\n', '\\\\n'))\n        ?.replaceAll('$content', desp?.replaceAll('\\n', '\\\\n')),\n    );\n    const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);\n    const options = {\n      method: WEBHOOK_METHOD,\n      headers,\n      allowGetBody: true,\n      ...bodyParam,\n      timeout,\n      retry: 1,\n    };\n\n    const formatUrl = WEBHOOK_URL.replaceAll(\n      '$title',\n      encodeURIComponent(text),\n    ).replaceAll('$content', encodeURIComponent(desp));\n    httpClient.request(formatUrl, options).then(async (resp) => {\n      const body = await resp.body.text();\n      try {\n        if (resp.statusCode !== 200) {\n          console.log(`自定义发送通知消息失败😞 ${body}\\n`);\n        } else {\n          console.log(`自定义发送通知消息成功🎉 ${body}\\n`);\n        }\n      } catch (e) {\n        $.logErr(e, resp);\n      } finally {\n        resolve(body);\n      }\n    });\n  });\n}\n\nfunction ntfyNotify(text, desp) {\n  function encodeRFC2047(text) {\n    const encodedBase64 = Buffer.from(text).toString('base64');\n    return `=?utf-8?B?${encodedBase64}?=`;\n  }\n\n  return new Promise((resolve) => {\n    const {\n      NTFY_URL,\n      NTFY_TOPIC,\n      NTFY_PRIORITY,\n      NTFY_TOKEN,\n      NTFY_USERNAME,\n      NTFY_PASSWORD,\n      NTFY_ACTIONS,\n    } = push_config;\n    if (NTFY_TOPIC) {\n      const options = {\n        url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,\n        body: `${desp}`,\n        headers: {\n          Title: `${encodeRFC2047(text)}`,\n          Priority: NTFY_PRIORITY || '3',\n          Icon: 'https://qn.whyour.cn/logo.png',\n        },\n        timeout,\n      };\n      if (NTFY_TOKEN) {\n        options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;\n      } else if (NTFY_USERNAME && NTFY_PASSWORD) {\n        options.headers['Authorization'] =\n          `Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`;\n      }\n      if (NTFY_ACTIONS) {\n        options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);\n      }\n\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('Ntfy 通知调用API失败😞\\n', err);\n          } else {\n            if (data.id) {\n              console.log('Ntfy 发送通知消息成功🎉\\n');\n            } else {\n              console.log(`Ntfy 发送通知消息异常 ${JSON.stringify(data)}`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction wxPusherNotify(text, desp) {\n  return new Promise((resolve) => {\n    const { WXPUSHER_APP_TOKEN, WXPUSHER_TOPIC_IDS, WXPUSHER_UIDS } =\n      push_config;\n    if (WXPUSHER_APP_TOKEN) {\n      // 处理topic_ids，将分号分隔的字符串转为数组\n      const topicIds = WXPUSHER_TOPIC_IDS\n        ? WXPUSHER_TOPIC_IDS.split(';')\n            .map((id) => id.trim())\n            .filter((id) => id)\n            .map((id) => parseInt(id))\n        : [];\n\n      // 处理uids，将分号分隔的字符串转为数组\n      const uids = WXPUSHER_UIDS\n        ? WXPUSHER_UIDS.split(';')\n            .map((uid) => uid.trim())\n            .filter((uid) => uid)\n        : [];\n\n      // topic_ids uids 至少有一个\n      if (!topicIds.length && !uids.length) {\n        console.log(\n          'wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!',\n        );\n        return resolve();\n      }\n\n      const body = {\n        appToken: WXPUSHER_APP_TOKEN,\n        content: `<h1>${text}</h1><br/><div style='white-space: pre-wrap;'>${desp}</div>`,\n        summary: text,\n        contentType: 2,\n        topicIds: topicIds,\n        uids: uids,\n        verifyPayType: 0,\n      };\n\n      const options = {\n        url: 'https://wxpusher.zjiecode.com/api/send/message',\n        body: JSON.stringify(body),\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        timeout,\n      };\n\n      $.post(options, (err, resp, data) => {\n        try {\n          if (err) {\n            console.log('wxpusher发送通知消息失败！\\n', err);\n          } else {\n            if (data.code === 1000) {\n              console.log('wxpusher发送通知消息完成！');\n            } else {\n              console.log(`wxpusher发送通知消息异常：${data.msg}`);\n            }\n          }\n        } catch (e) {\n          $.logErr(e, resp);\n        } finally {\n          resolve(data);\n        }\n      });\n    } else {\n      resolve();\n    }\n  });\n}\n\nfunction parseString(input, valueFormatFn) {\n  const regex = /(\\w+):\\s*((?:(?!\\n\\w+:).)*)/g;\n  const matches = {};\n\n  let match;\n  while ((match = regex.exec(input)) !== null) {\n    const [, key, value] = match;\n    const _key = key.trim();\n    if (!_key || matches[_key]) {\n      continue;\n    }\n\n    let _value = value.trim();\n\n    try {\n      _value = valueFormatFn ? valueFormatFn(_value) : _value;\n      const jsonValue = JSON.parse(_value);\n      matches[_key] = jsonValue;\n    } catch (error) {\n      matches[_key] = _value;\n    }\n  }\n\n  return matches;\n}\n\nfunction parseHeaders(headers) {\n  if (!headers) return {};\n\n  const parsed = {};\n  let key;\n  let val;\n  let i;\n\n  headers &&\n    headers.split('\\n').forEach(function parser(line) {\n      i = line.indexOf(':');\n      key = line.substring(0, i).trim().toLowerCase();\n      val = line.substring(i + 1).trim();\n\n      if (!key) {\n        return;\n      }\n\n      parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;\n    });\n\n  return parsed;\n}\n\nfunction parseBody(body, contentType, valueFormatFn) {\n  if (contentType === 'text/plain' || !body) {\n    return valueFormatFn && body ? valueFormatFn(body) : body;\n  }\n\n  const parsed = parseString(body, valueFormatFn);\n\n  switch (contentType) {\n    case 'multipart/form-data':\n      return Object.keys(parsed).reduce((p, c) => {\n        p.append(c, parsed[c]);\n        return p;\n      }, new FormData());\n    case 'application/x-www-form-urlencoded':\n      return Object.keys(parsed).reduce((p, c) => {\n        return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;\n      });\n  }\n\n  return parsed;\n}\n\nfunction formatBodyFun(contentType, body) {\n  if (!body) return {};\n  switch (contentType) {\n    case 'application/json':\n      return { json: body };\n    case 'multipart/form-data':\n      return { form: body };\n    case 'application/x-www-form-urlencoded':\n    case 'text/plain':\n      return { body };\n  }\n  return {};\n}\n\n/**\n * sendNotify 推送通知功能\n * @param text 通知头\n * @param desp 通知体\n * @param params 某些推送通知方式点击弹窗可跳转, 例：{ url: 'https://abc.com' }\n * @returns {Promise<unknown>}\n */\nasync function sendNotify(text, desp, params = {}) {\n  // 根据标题跳过一些消息推送，环境变量：SKIP_PUSH_TITLE 用回车分隔\n  let skipTitle = process.env.SKIP_PUSH_TITLE;\n  if (skipTitle) {\n    if (skipTitle.split('\\n').includes(text)) {\n      console.info(text + '在 SKIP_PUSH_TITLE 环境变量内，跳过推送');\n      return;\n    }\n  }\n\n  if (push_config.HITOKOTO !== 'false') {\n    desp += '\\n\\n' + (await one());\n  }\n\n  await Promise.all([\n    serverNotify(text, desp), // 微信server酱\n    pushPlusNotify(text, desp), // pushplus\n    wePlusBotNotify(text, desp), // 微加机器人\n    barkNotify(text, desp, params), // iOS Bark APP\n    tgBotNotify(text, desp), // telegram 机器人\n    ddBotNotify(text, desp), // 钉钉机器人\n    qywxBotNotify(text, desp), // 企业微信机器人\n    qywxamNotify(text, desp), // 企业微信应用消息推送\n    iGotNotify(text, desp, params), // iGot\n    gobotNotify(text, desp), // go-cqhttp\n    gotifyNotify(text, desp), // gotify\n    chatNotify(text, desp), // synolog chat\n    pushDeerNotify(text, desp), // PushDeer\n    aibotkNotify(text, desp), // 智能微秘书\n    fsBotNotify(text, desp), // 飞书机器人\n    smtpNotify(text, desp), // SMTP 邮件\n    pushMeNotify(text, desp, params), // PushMe\n    chronocatNotify(text, desp), // Chronocat\n    webhookNotify(text, desp), // 自定义通知\n    qmsgNotify(text, desp), // 自定义通知\n    ntfyNotify(text, desp), // Ntfy\n    wxPusherNotify(text, desp), // wxpusher\n  ]);\n}\n\nmodule.exports = {\n  sendNotify,\n};\n"
  },
  {
    "path": "sample/notify.py",
    "content": "#!/usr/bin/env python3\n# _*_ coding:utf-8 _*_\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport re\nimport threading\nimport time\nimport urllib.parse\nimport smtplib\nfrom email.mime.text import MIMEText\nfrom email.header import Header\nfrom email.utils import formataddr\n\nimport requests\n\n# 原先的 print 函数和主线程的锁\n_print = print\nmutex = threading.Lock()\n\n\n# 定义新的 print 函数\ndef print(text, *args, **kw):\n    \"\"\"\n    使输出有序进行，不出现多线程同一时间输出导致错乱的问题。\n    \"\"\"\n    with mutex:\n        _print(text, *args, **kw)\n\n\n# 通知服务\n# fmt: off\npush_config = {\n    'HITOKOTO': True,                  # 启用一言（随机句子）\n\n    'BARK_PUSH': '',                    # bark IP 或设备码，例：https://api.day.app/DxHcxxxxxRxxxxxxcm/\n    'BARK_ARCHIVE': '',                 # bark 推送是否存档\n    'BARK_GROUP': '',                   # bark 推送分组\n    'BARK_SOUND': '',                   # bark 推送声音\n    'BARK_ICON': '',                    # bark 推送图标\n    'BARK_LEVEL': '',                   # bark 推送时效性\n    'BARK_URL': '',                     # bark 推送跳转URL\n\n    'CONSOLE': False,                    # 控制台输出\n\n    'DD_BOT_SECRET': '',                # 钉钉机器人的 DD_BOT_SECRET\n    'DD_BOT_TOKEN': '',                 # 钉钉机器人的 DD_BOT_TOKEN\n\n    'FSKEY': '',                        # 飞书机器人的 FSKEY\n    'FSSECRET': '',                     # 飞书机器人的 FSSECRET，对应安全设置里的签名校验密钥\n\n    'GOBOT_URL': '',                    # go-cqhttp\n                                        # 推送到个人QQ：http://127.0.0.1/send_private_msg\n                                        # 群：http://127.0.0.1/send_group_msg\n    'GOBOT_QQ': '',                     # go-cqhttp 的推送群或用户\n                                        # GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ\n                                        #               /send_group_msg   时填入 group_id=QQ群\n    'GOBOT_TOKEN': '',                  # go-cqhttp 的 access_token\n\n    'GOTIFY_URL': '',                   # gotify地址,如https://push.example.de:8080\n    'GOTIFY_TOKEN': '',                 # gotify的消息应用token\n    'GOTIFY_PRIORITY': 0,               # 推送消息优先级,默认为0\n\n    'IGOT_PUSH_KEY': '',                # iGot 聚合推送的 IGOT_PUSH_KEY\n\n    'PUSH_KEY': '',                     # server 酱的 PUSH_KEY，兼容旧版与 Turbo 版\n\n    'DEER_KEY': '',                     # PushDeer 的 PUSHDEER_KEY\n    'DEER_URL': '',                     # PushDeer 的 PUSHDEER_URL\n\n    'CHAT_URL': '',                     # synology chat url\n    'CHAT_TOKEN': '',                   # synology chat token\n\n    'PUSH_PLUS_TOKEN': '',              # pushplus 推送的用户令牌\n    'PUSH_PLUS_USER': '',               # pushplus 推送的群组编码\n    'PUSH_PLUS_TEMPLATE': 'html',       # pushplus 发送模板，支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay\n    'PUSH_PLUS_CHANNEL': 'wechat',      # pushplus 发送渠道，支持wechat,webhook,cp,mail,sms\n    'PUSH_PLUS_WEBHOOK': '',            # pushplus webhook编码，可在pushplus公众号上扩展配置出更多渠道\n    'PUSH_PLUS_CALLBACKURL': '',        # pushplus 发送结果回调地址，会把推送最终结果通知到这个地址上\n    'PUSH_PLUS_TO': '',                 # pushplus 好友令牌，微信公众号渠道填写好友令牌，企业微信渠道填写企业微信用户id\n\n    'WE_PLUS_BOT_TOKEN': '',            # 微加机器人的用户令牌\n    'WE_PLUS_BOT_RECEIVER': '',         # 微加机器人的消息接收者\n    'WE_PLUS_BOT_VERSION': 'pro',          # 微加机器人的调用版本\n\n    'QMSG_KEY': '',                     # qmsg 酱的 QMSG_KEY\n    'QMSG_TYPE': '',                    # qmsg 酱的 QMSG_TYPE\n\n    'QYWX_ORIGIN': '',                  # 企业微信代理地址\n\n    'QYWX_AM': '',                      # 企业微信应用\n\n    'QYWX_KEY': '',                     # 企业微信机器人\n\n    'TG_BOT_TOKEN': '',                 # tg 机器人的 TG_BOT_TOKEN，例：1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ\n    'TG_USER_ID': '',                   # tg 机器人的 TG_USER_ID，例：1434078534\n    'TG_API_HOST': '',                  # tg 代理 api\n    'TG_PROXY_AUTH': '',                # tg 代理认证参数\n    'TG_PROXY_HOST': '',                # tg 机器人的 TG_PROXY_HOST\n    'TG_PROXY_PORT': '',                # tg 机器人的 TG_PROXY_PORT\n\n    'AIBOTK_KEY': '',                   # 智能微秘书 个人中心的apikey 文档地址：http://wechat.aibotk.com/docs/about\n    'AIBOTK_TYPE': '',                  # 智能微秘书 发送目标 room 或 contact\n    'AIBOTK_NAME': '',                  # 智能微秘书  发送群名 或者好友昵称和type要对应好\n\n    'SMTP_SERVER': '',                  # SMTP 发送邮件服务器，形如 smtp.exmail.qq.com:465\n    'SMTP_SSL': 'false',                # SMTP 发送邮件服务器是否使用 SSL，填写 true 或 false\n    'SMTP_EMAIL': '',                   # SMTP 收发件邮箱，通知将会由自己发给自己\n    'SMTP_PASSWORD': '',                # SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\n    'SMTP_NAME': '',                    # SMTP 收发件人姓名，可随意填写\n\n    'PUSHME_KEY': '',                   # PushMe 的 PUSHME_KEY\n    'PUSHME_URL': '',                   # PushMe 的 PUSHME_URL\n\n    'CHRONOCAT_QQ': '',                 # qq号\n    'CHRONOCAT_TOKEN': '',              # CHRONOCAT 的token\n    'CHRONOCAT_URL': '',                # CHRONOCAT的url地址\n\n    'WEBHOOK_URL': '',                  # 自定义通知 请求地址\n    'WEBHOOK_BODY': '',                 # 自定义通知 请求体\n    'WEBHOOK_HEADERS': '',              # 自定义通知 请求头\n    'WEBHOOK_METHOD': '',               # 自定义通知 请求方法\n    'WEBHOOK_CONTENT_TYPE': '',         # 自定义通知 content-type\n\n    'NTFY_URL': '',                     # ntfy地址,如https://ntfy.sh\n    'NTFY_TOPIC': '',                   # ntfy的消息应用topic\n    'NTFY_PRIORITY':'3',                # 推送消息优先级,默认为3\n    'NTFY_TOKEN': '',                   # 推送token,可选\n    'NTFY_USERNAME': '',                # 推送用户名称,可选\n    'NTFY_PASSWORD': '',                # 推送用户密码,可选\n    'NTFY_ACTIONS': '',                 # 推送用户动作,可选\n\n    'WXPUSHER_APP_TOKEN': '',           # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/\n    'WXPUSHER_TOPIC_IDS': '',           # wxpusher 的 主题ID，多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行\n    'WXPUSHER_UIDS': '',                # wxpusher 的 用户ID，多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行\n}\n# fmt: on\n\nfor k in push_config:\n    if os.getenv(k):\n        v = os.getenv(k)\n        push_config[k] = v\n\n\ndef bark(title: str, content: str) -> None:\n    \"\"\"\n    使用 bark 推送消息。\n    \"\"\"\n    if not push_config.get(\"BARK_PUSH\"):\n        return\n    print(\"bark 服务启动\")\n\n    if push_config.get(\"BARK_PUSH\").startswith(\"http\"):\n        url = f'{push_config.get(\"BARK_PUSH\")}'\n    else:\n        url = f'https://api.day.app/{push_config.get(\"BARK_PUSH\")}'\n\n    bark_params = {\n        \"BARK_ARCHIVE\": \"isArchive\",\n        \"BARK_GROUP\": \"group\",\n        \"BARK_SOUND\": \"sound\",\n        \"BARK_ICON\": \"icon\",\n        \"BARK_LEVEL\": \"level\",\n        \"BARK_URL\": \"url\",\n    }\n    data = {\n        \"title\": title,\n        \"body\": content,\n    }\n    for pair in filter(\n        lambda pairs: pairs[0].startswith(\"BARK_\")\n        and pairs[0] != \"BARK_PUSH\"\n        and pairs[1]\n        and bark_params.get(pairs[0]),\n        push_config.items(),\n    ):\n        data[bark_params.get(pair[0])] = pair[1]\n    headers = {\"Content-Type\": \"application/json;charset=utf-8\"}\n    response = requests.post(\n        url=url, data=json.dumps(data), headers=headers, timeout=15\n    ).json()\n\n    if response[\"code\"] == 200:\n        print(\"bark 推送成功！\")\n    else:\n        print(\"bark 推送失败！\")\n\n\ndef console(title: str, content: str) -> None:\n    \"\"\"\n    使用 控制台 推送消息。\n    \"\"\"\n    print(f\"{title}\\n\\n{content}\")\n\n\ndef dingding_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 钉钉机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"DD_BOT_SECRET\") or not push_config.get(\"DD_BOT_TOKEN\"):\n        return\n    print(\"钉钉机器人 服务启动\")\n\n    timestamp = str(round(time.time() * 1000))\n    secret_enc = push_config.get(\"DD_BOT_SECRET\").encode(\"utf-8\")\n    string_to_sign = \"{}\\n{}\".format(timestamp, push_config.get(\"DD_BOT_SECRET\"))\n    string_to_sign_enc = string_to_sign.encode(\"utf-8\")\n    hmac_code = hmac.new(\n        secret_enc, string_to_sign_enc, digestmod=hashlib.sha256\n    ).digest()\n    sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))\n    url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get(\"DD_BOT_TOKEN\")}&timestamp={timestamp}&sign={sign}'\n    headers = {\"Content-Type\": \"application/json;charset=utf-8\"}\n    data = {\"msgtype\": \"text\", \"text\": {\"content\": f\"{title}\\n\\n{content}\"}}\n    response = requests.post(\n        url=url, data=json.dumps(data), headers=headers, timeout=15\n    ).json()\n\n    if not response[\"errcode\"]:\n        print(\"钉钉机器人 推送成功！\")\n    else:\n        print(\"钉钉机器人 推送失败！\")\n\n\ndef feishu_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 飞书机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"FSKEY\"):\n        return\n    print(\"飞书 服务启动\")\n\n    url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get(\"FSKEY\")}'\n    data = {\"msg_type\": \"text\", \"content\": {\"text\": f\"{title}\\n\\n{content}\"}}\n\n    # Add signature if secret is provided\n    # Note: Feishu's signature algorithm uses timestamp+\"\\n\"+secret as the HMAC key\n    # and signs an empty message, which differs from typical HMAC usage\n    if push_config.get(\"FSSECRET\"):\n        timestamp = str(int(time.time()))\n        string_to_sign = f'{timestamp}\\n{push_config.get(\"FSSECRET\")}'\n        hmac_code = hmac.new(\n            string_to_sign.encode(\"utf-8\"), digestmod=hashlib.sha256\n        ).digest()\n        sign = base64.b64encode(hmac_code).decode(\"utf-8\")\n        data[\"timestamp\"] = timestamp\n        data[\"sign\"] = sign\n\n    response = requests.post(url, data=json.dumps(data)).json()\n\n    if response.get(\"StatusCode\") == 0 or response.get(\"code\") == 0:\n        print(\"飞书 推送成功！\")\n    else:\n        print(\"飞书 推送失败！错误信息如下：\\n\", response)\n\n\ndef go_cqhttp(title: str, content: str) -> None:\n    \"\"\"\n    使用 go_cqhttp 推送消息。\n    \"\"\"\n    if not push_config.get(\"GOBOT_URL\") or not push_config.get(\"GOBOT_QQ\"):\n        return\n    print(\"go-cqhttp 服务启动\")\n\n    url = f'{push_config.get(\"GOBOT_URL\")}?access_token={push_config.get(\"GOBOT_TOKEN\")}&{push_config.get(\"GOBOT_QQ\")}&message=标题:{title}\\n内容:{content}'\n    response = requests.get(url).json()\n\n    if response[\"status\"] == \"ok\":\n        print(\"go-cqhttp 推送成功！\")\n    else:\n        print(\"go-cqhttp 推送失败！\")\n\n\ndef gotify(title: str, content: str) -> None:\n    \"\"\"\n    使用 gotify 推送消息。\n    \"\"\"\n    if not push_config.get(\"GOTIFY_URL\") or not push_config.get(\"GOTIFY_TOKEN\"):\n        return\n    print(\"gotify 服务启动\")\n\n    url = f'{push_config.get(\"GOTIFY_URL\")}/message?token={push_config.get(\"GOTIFY_TOKEN\")}'\n    data = {\n        \"title\": title,\n        \"message\": content,\n        \"priority\": push_config.get(\"GOTIFY_PRIORITY\"),\n    }\n    response = requests.post(url, data=data).json()\n\n    if response.get(\"id\"):\n        print(\"gotify 推送成功！\")\n    else:\n        print(\"gotify 推送失败！\")\n\n\ndef iGot(title: str, content: str) -> None:\n    \"\"\"\n    使用 iGot 推送消息。\n    \"\"\"\n    if not push_config.get(\"IGOT_PUSH_KEY\"):\n        return\n    print(\"iGot 服务启动\")\n\n    url = f'https://push.hellyw.com/{push_config.get(\"IGOT_PUSH_KEY\")}'\n    data = {\"title\": title, \"content\": content}\n    headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n    response = requests.post(url, data=data, headers=headers).json()\n\n    if response[\"ret\"] == 0:\n        print(\"iGot 推送成功！\")\n    else:\n        print(f'iGot 推送失败！{response[\"errMsg\"]}')\n\n\ndef serverJ(title: str, content: str) -> None:\n    \"\"\"\n    通过 serverJ 推送消息。\n    \"\"\"\n    if not push_config.get(\"PUSH_KEY\"):\n        return\n    print(\"serverJ 服务启动\")\n\n    data = {\"text\": title, \"desp\": content.replace(\"\\n\", \"\\n\\n\")}\n\n    match = re.match(r\"sctp(\\d+)t\", push_config.get(\"PUSH_KEY\"))\n    if match:\n        num = match.group(1)\n        url = f'https://{num}.push.ft07.com/send/{push_config.get(\"PUSH_KEY\")}.send'\n    else:\n        url = f'https://sctapi.ftqq.com/{push_config.get(\"PUSH_KEY\")}.send'\n\n    response = requests.post(url, data=data).json()\n\n    if response.get(\"errno\") == 0 or response.get(\"code\") == 0:\n        print(\"serverJ 推送成功！\")\n    else:\n        print(f'serverJ 推送失败！错误码：{response[\"message\"]}')\n\n\ndef pushdeer(title: str, content: str) -> None:\n    \"\"\"\n    通过PushDeer 推送消息\n    \"\"\"\n    if not push_config.get(\"DEER_KEY\"):\n        return\n    print(\"PushDeer 服务启动\")\n    data = {\n        \"text\": title,\n        \"desp\": content,\n        \"type\": \"markdown\",\n        \"pushkey\": push_config.get(\"DEER_KEY\"),\n    }\n    url = \"https://api2.pushdeer.com/message/push\"\n    if push_config.get(\"DEER_URL\"):\n        url = push_config.get(\"DEER_URL\")\n\n    response = requests.post(url, data=data).json()\n\n    if len(response.get(\"content\").get(\"result\")) > 0:\n        print(\"PushDeer 推送成功！\")\n    else:\n        print(\"PushDeer 推送失败！错误信息：\", response)\n\n\ndef chat(title: str, content: str) -> None:\n    \"\"\"\n    通过Chat 推送消息\n    \"\"\"\n    if not push_config.get(\"CHAT_URL\") or not push_config.get(\"CHAT_TOKEN\"):\n        return\n    print(\"chat 服务启动\")\n    data = \"payload=\" + json.dumps({\"text\": title + \"\\n\" + content})\n    url = push_config.get(\"CHAT_URL\") + push_config.get(\"CHAT_TOKEN\")\n    response = requests.post(url, data=data)\n\n    if response.status_code == 200:\n        print(\"Chat 推送成功！\")\n    else:\n        print(\"Chat 推送失败！错误信息：\", response)\n\n\ndef pushplus_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 pushplus 推送消息。\n    \"\"\"\n    if not push_config.get(\"PUSH_PLUS_TOKEN\"):\n        return\n    print(\"PUSHPLUS 服务启动\")\n\n    url = \"https://www.pushplus.plus/send\"\n    data = {\n        \"token\": push_config.get(\"PUSH_PLUS_TOKEN\"),\n        \"title\": title,\n        \"content\": content,\n        \"topic\": push_config.get(\"PUSH_PLUS_USER\"),\n        \"template\": push_config.get(\"PUSH_PLUS_TEMPLATE\"),\n        \"channel\": push_config.get(\"PUSH_PLUS_CHANNEL\"),\n        \"webhook\": push_config.get(\"PUSH_PLUS_WEBHOOK\"),\n        \"callbackUrl\": push_config.get(\"PUSH_PLUS_CALLBACKURL\"),\n        \"to\": push_config.get(\"PUSH_PLUS_TO\"),\n    }\n    body = json.dumps(data).encode(encoding=\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, data=body, headers=headers).json()\n\n    code = response[\"code\"]\n    if code == 200:\n        print(\"PUSHPLUS 推送请求成功，可根据流水号查询推送结果:\" + response[\"data\"])\n        print(\n            \"注意：请求成功并不代表推送成功，如未收到消息，请到pushplus官网使用流水号查询推送最终结果\"\n        )\n    elif code == 900 or code == 903 or code == 905 or code == 999:\n        print(response[\"msg\"])\n\n    else:\n        url_old = \"http://pushplus.hxtrip.com/send\"\n        headers[\"Accept\"] = \"application/json\"\n        response = requests.post(url=url_old, data=body, headers=headers).json()\n\n        if response[\"code\"] == 200:\n            print(\"PUSHPLUS(hxtrip) 推送成功！\")\n\n        else:\n            print(\"PUSHPLUS 推送失败！\")\n\n\ndef weplus_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 微加机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"WE_PLUS_BOT_TOKEN\"):\n        return\n    print(\"微加机器人 服务启动\")\n\n    template = \"txt\"\n    if len(content) > 800:\n        template = \"html\"\n\n    url = \"https://www.weplusbot.com/send\"\n    data = {\n        \"token\": push_config.get(\"WE_PLUS_BOT_TOKEN\"),\n        \"title\": title,\n        \"content\": content,\n        \"template\": template,\n        \"receiver\": push_config.get(\"WE_PLUS_BOT_RECEIVER\"),\n        \"version\": push_config.get(\"WE_PLUS_BOT_VERSION\"),\n    }\n    body = json.dumps(data).encode(encoding=\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, data=body, headers=headers).json()\n\n    if response[\"code\"] == 200:\n        print(\"微加机器人 推送成功！\")\n    else:\n        print(\"微加机器人 推送失败！\")\n\n\ndef qmsg_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 qmsg 推送消息。\n    \"\"\"\n    if not push_config.get(\"QMSG_KEY\") or not push_config.get(\"QMSG_TYPE\"):\n        return\n    print(\"qmsg 服务启动\")\n\n    url = f'https://qmsg.zendee.cn/{push_config.get(\"QMSG_TYPE\")}/{push_config.get(\"QMSG_KEY\")}'\n    payload = {\"msg\": f'{title}\\n\\n{content.replace(\"----\", \"-\")}'.encode(\"utf-8\")}\n    response = requests.post(url=url, params=payload).json()\n\n    if response[\"code\"] == 0:\n        print(\"qmsg 推送成功！\")\n    else:\n        print(f'qmsg 推送失败！{response[\"reason\"]}')\n\n\ndef wecom_app(title: str, content: str) -> None:\n    \"\"\"\n    通过 企业微信 APP 推送消息。\n    \"\"\"\n    if not push_config.get(\"QYWX_AM\"):\n        return\n    QYWX_AM_AY = re.split(\",\", push_config.get(\"QYWX_AM\"))\n    if 4 < len(QYWX_AM_AY) > 5:\n        print(\"QYWX_AM 设置错误!!\")\n        return\n    print(\"企业微信 APP 服务启动\")\n\n    corpid = QYWX_AM_AY[0]\n    corpsecret = QYWX_AM_AY[1]\n    touser = QYWX_AM_AY[2]\n    agentid = QYWX_AM_AY[3]\n    try:\n        media_id = QYWX_AM_AY[4]\n    except IndexError:\n        media_id = \"\"\n    wx = WeCom(corpid, corpsecret, agentid)\n    # 如果没有配置 media_id 默认就以 text 方式发送\n    if not media_id:\n        message = title + \"\\n\\n\" + content\n        response = wx.send_text(message, touser)\n    else:\n        response = wx.send_mpnews(title, content, media_id, touser)\n\n    if response == \"ok\":\n        print(\"企业微信推送成功！\")\n    else:\n        print(\"企业微信推送失败！错误信息如下：\\n\", response)\n\n\nclass WeCom:\n    def __init__(self, corpid, corpsecret, agentid):\n        self.CORPID = corpid\n        self.CORPSECRET = corpsecret\n        self.AGENTID = agentid\n        self.ORIGIN = \"https://qyapi.weixin.qq.com\"\n        if push_config.get(\"QYWX_ORIGIN\"):\n            self.ORIGIN = push_config.get(\"QYWX_ORIGIN\")\n\n    def get_access_token(self):\n        url = f\"{self.ORIGIN}/cgi-bin/gettoken\"\n        values = {\n            \"corpid\": self.CORPID,\n            \"corpsecret\": self.CORPSECRET,\n        }\n        req = requests.post(url, params=values)\n        data = json.loads(req.text)\n        return data[\"access_token\"]\n\n    def send_text(self, message, touser=\"@all\"):\n        send_url = (\n            f\"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}\"\n        )\n        send_values = {\n            \"touser\": touser,\n            \"msgtype\": \"text\",\n            \"agentid\": self.AGENTID,\n            \"text\": {\"content\": message},\n            \"safe\": \"0\",\n        }\n        send_msges = bytes(json.dumps(send_values), \"utf-8\")\n        respone = requests.post(send_url, send_msges)\n        respone = respone.json()\n        return respone[\"errmsg\"]\n\n    def send_mpnews(self, title, message, media_id, touser=\"@all\"):\n        send_url = (\n            f\"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}\"\n        )\n        send_values = {\n            \"touser\": touser,\n            \"msgtype\": \"mpnews\",\n            \"agentid\": self.AGENTID,\n            \"mpnews\": {\n                \"articles\": [\n                    {\n                        \"title\": title,\n                        \"thumb_media_id\": media_id,\n                        \"author\": \"Author\",\n                        \"content_source_url\": \"\",\n                        \"content\": message.replace(\"\\n\", \"<br/>\"),\n                        \"digest\": message,\n                    }\n                ]\n            },\n        }\n        send_msges = bytes(json.dumps(send_values), \"utf-8\")\n        respone = requests.post(send_url, send_msges)\n        respone = respone.json()\n        return respone[\"errmsg\"]\n\n\ndef wecom_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 企业微信机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"QYWX_KEY\"):\n        return\n    print(\"企业微信机器人服务启动\")\n\n    origin = \"https://qyapi.weixin.qq.com\"\n    if push_config.get(\"QYWX_ORIGIN\"):\n        origin = push_config.get(\"QYWX_ORIGIN\")\n\n    url = f\"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}\"\n    headers = {\"Content-Type\": \"application/json;charset=utf-8\"}\n    data = {\"msgtype\": \"text\", \"text\": {\"content\": f\"{title}\\n\\n{content}\"}}\n    response = requests.post(\n        url=url, data=json.dumps(data), headers=headers, timeout=15\n    ).json()\n\n    if response[\"errcode\"] == 0:\n        print(\"企业微信机器人推送成功！\")\n    else:\n        print(\"企业微信机器人推送失败！\")\n\n\ndef telegram_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 telegram 机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"TG_BOT_TOKEN\") or not push_config.get(\"TG_USER_ID\"):\n        return\n    print(\"tg 服务启动\")\n\n    if push_config.get(\"TG_API_HOST\"):\n        url = f\"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage\"\n    else:\n        url = (\n            f\"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage\"\n        )\n    headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n    payload = {\n        \"chat_id\": str(push_config.get(\"TG_USER_ID\")),\n        \"text\": f\"{title}\\n\\n{content}\",\n        \"disable_web_page_preview\": \"true\",\n    }\n    proxies = None\n    if push_config.get(\"TG_PROXY_HOST\") and push_config.get(\"TG_PROXY_PORT\"):\n        if push_config.get(\"TG_PROXY_AUTH\") is not None and \"@\" not in push_config.get(\n            \"TG_PROXY_HOST\"\n        ):\n            push_config[\"TG_PROXY_HOST\"] = (\n                push_config.get(\"TG_PROXY_AUTH\")\n                + \"@\"\n                + push_config.get(\"TG_PROXY_HOST\")\n            )\n        proxyStr = \"http://{}:{}\".format(\n            push_config.get(\"TG_PROXY_HOST\"), push_config.get(\"TG_PROXY_PORT\")\n        )\n        proxies = {\"http\": proxyStr, \"https\": proxyStr}\n    response = requests.post(\n        url=url, headers=headers, params=payload, proxies=proxies\n    ).json()\n\n    if response[\"ok\"]:\n        print(\"tg 推送成功！\")\n    else:\n        print(\"tg 推送失败！\")\n\n\ndef aibotk(title: str, content: str) -> None:\n    \"\"\"\n    使用 智能微秘书 推送消息。\n    \"\"\"\n    if (\n        not push_config.get(\"AIBOTK_KEY\")\n        or not push_config.get(\"AIBOTK_TYPE\")\n        or not push_config.get(\"AIBOTK_NAME\")\n    ):\n        return\n    print(\"智能微秘书 服务启动\")\n\n    if push_config.get(\"AIBOTK_TYPE\") == \"room\":\n        url = \"https://api-bot.aibotk.com/openapi/v1/chat/room\"\n        data = {\n            \"apiKey\": push_config.get(\"AIBOTK_KEY\"),\n            \"roomName\": push_config.get(\"AIBOTK_NAME\"),\n            \"message\": {\"type\": 1, \"content\": f\"【青龙快讯】\\n\\n{title}\\n{content}\"},\n        }\n    else:\n        url = \"https://api-bot.aibotk.com/openapi/v1/chat/contact\"\n        data = {\n            \"apiKey\": push_config.get(\"AIBOTK_KEY\"),\n            \"name\": push_config.get(\"AIBOTK_NAME\"),\n            \"message\": {\"type\": 1, \"content\": f\"【青龙快讯】\\n\\n{title}\\n{content}\"},\n        }\n    body = json.dumps(data).encode(encoding=\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, data=body, headers=headers).json()\n    print(response)\n    if response[\"code\"] == 0:\n        print(\"智能微秘书 推送成功！\")\n    else:\n        print(f'智能微秘书 推送失败！{response[\"error\"]}')\n\n\ndef smtp(title: str, content: str) -> None:\n    \"\"\"\n    使用 SMTP 邮件 推送消息。\n    \"\"\"\n    if (\n        not push_config.get(\"SMTP_SERVER\")\n        or not push_config.get(\"SMTP_SSL\")\n        or not push_config.get(\"SMTP_EMAIL\")\n        or not push_config.get(\"SMTP_PASSWORD\")\n        or not push_config.get(\"SMTP_NAME\")\n    ):\n        return\n    print(\"SMTP 邮件 服务启动\")\n\n    message = MIMEText(content, \"plain\", \"utf-8\")\n    message[\"From\"] = formataddr(\n        (\n            Header(push_config.get(\"SMTP_NAME\"), \"utf-8\").encode(),\n            push_config.get(\"SMTP_EMAIL\"),\n        )\n    )\n    message[\"To\"] = formataddr(\n        (\n            Header(push_config.get(\"SMTP_NAME\"), \"utf-8\").encode(),\n            push_config.get(\"SMTP_EMAIL\"),\n        )\n    )\n    message[\"Subject\"] = Header(title, \"utf-8\")\n\n    try:\n        smtp_server = (\n            smtplib.SMTP_SSL(push_config.get(\"SMTP_SERVER\"))\n            if push_config.get(\"SMTP_SSL\") == \"true\"\n            else smtplib.SMTP(push_config.get(\"SMTP_SERVER\"))\n        )\n        smtp_server.login(\n            push_config.get(\"SMTP_EMAIL\"), push_config.get(\"SMTP_PASSWORD\")\n        )\n        smtp_server.sendmail(\n            push_config.get(\"SMTP_EMAIL\"),\n            push_config.get(\"SMTP_EMAIL\"),\n            message.as_bytes(),\n        )\n        smtp_server.close()\n        print(\"SMTP 邮件 推送成功！\")\n    except Exception as e:\n        print(f\"SMTP 邮件 推送失败！{e}\")\n\n\ndef pushme(title: str, content: str) -> None:\n    \"\"\"\n    使用 PushMe 推送消息。\n    \"\"\"\n    if not push_config.get(\"PUSHME_KEY\"):\n        return\n    print(\"PushMe 服务启动\")\n\n    url = (\n        push_config.get(\"PUSHME_URL\")\n        if push_config.get(\"PUSHME_URL\")\n        else \"https://push.i-i.me/\"\n    )\n    data = {\n        \"push_key\": push_config.get(\"PUSHME_KEY\"),\n        \"title\": title,\n        \"content\": content,\n        \"date\": push_config.get(\"date\") if push_config.get(\"date\") else \"\",\n        \"type\": push_config.get(\"type\") if push_config.get(\"type\") else \"\",\n    }\n    response = requests.post(url, data=data)\n\n    if response.status_code == 200 and response.text == \"success\":\n        print(\"PushMe 推送成功！\")\n    else:\n        print(f\"PushMe 推送失败！{response.status_code} {response.text}\")\n\n\ndef chronocat(title: str, content: str) -> None:\n    \"\"\"\n    使用 CHRONOCAT 推送消息。\n    \"\"\"\n    if (\n        not push_config.get(\"CHRONOCAT_URL\")\n        or not push_config.get(\"CHRONOCAT_QQ\")\n        or not push_config.get(\"CHRONOCAT_TOKEN\")\n    ):\n        return\n\n    print(\"CHRONOCAT 服务启动\")\n\n    user_ids = re.findall(r\"user_id=(\\d+)\", push_config.get(\"CHRONOCAT_QQ\"))\n    group_ids = re.findall(r\"group_id=(\\d+)\", push_config.get(\"CHRONOCAT_QQ\"))\n\n    url = f'{push_config.get(\"CHRONOCAT_URL\")}/api/message/send'\n    headers = {\n        \"Content-Type\": \"application/json\",\n        \"Authorization\": f'Bearer {push_config.get(\"CHRONOCAT_TOKEN\")}',\n    }\n\n    for chat_type, ids in [(1, user_ids), (2, group_ids)]:\n        if not ids:\n            continue\n        for chat_id in ids:\n            data = {\n                \"peer\": {\"chatType\": chat_type, \"peerUin\": chat_id},\n                \"elements\": [\n                    {\n                        \"elementType\": 1,\n                        \"textElement\": {\"content\": f\"{title}\\n\\n{content}\"},\n                    }\n                ],\n            }\n            response = requests.post(url, headers=headers, data=json.dumps(data))\n            if response.status_code == 200:\n                if chat_type == 1:\n                    print(f\"QQ个人消息:{ids}推送成功！\")\n                else:\n                    print(f\"QQ群消息:{ids}推送成功！\")\n            else:\n                if chat_type == 1:\n                    print(f\"QQ个人消息:{ids}推送失败！\")\n                else:\n                    print(f\"QQ群消息:{ids}推送失败！\")\n\n\ndef ntfy(title: str, content: str) -> None:\n    \"\"\"\n    通过 Ntfy 推送消息\n    \"\"\"\n\n    def encode_rfc2047(text: str) -> str:\n        \"\"\"将文本编码为符合 RFC 2047 标准的格式\"\"\"\n        encoded_bytes = base64.b64encode(text.encode(\"utf-8\"))\n        encoded_str = encoded_bytes.decode(\"utf-8\")\n        return f\"=?utf-8?B?{encoded_str}?=\"\n\n    if not push_config.get(\"NTFY_TOPIC\"):\n        return\n    print(\"ntfy 服务启动\")\n    priority = \"3\"\n    if not push_config.get(\"NTFY_PRIORITY\"):\n        print(\"ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3\")\n    else:\n        priority = push_config.get(\"NTFY_PRIORITY\")\n\n    # 使用 RFC 2047 编码 title\n    encoded_title = encode_rfc2047(title)\n\n    data = content.encode(encoding=\"utf-8\")\n    headers = {\"Title\": encoded_title, \"Priority\": priority, \"Icon\": \"https://qn.whyour.cn/logo.png\"}  # 使用编码后的 title\n    if push_config.get(\"NTFY_TOKEN\"):\n        headers['Authorization'] = \"Bearer \" + push_config.get(\"NTFY_TOKEN\")\n    elif push_config.get(\"NTFY_USERNAME\") and push_config.get(\"NTFY_PASSWORD\"):\n        authStr = push_config.get(\"NTFY_USERNAME\") + \":\" + push_config.get(\"NTFY_PASSWORD\")\n        headers['Authorization'] = \"Basic \" + base64.b64encode(authStr.encode('utf-8')).decode('utf-8')\n    if push_config.get(\"NTFY_ACTIONS\"):\n        headers['Actions'] = encode_rfc2047(push_config.get(\"NTFY_ACTIONS\"))\n\n    url = push_config.get(\"NTFY_URL\") + \"/\" + push_config.get(\"NTFY_TOPIC\")\n    response = requests.post(url, data=data, headers=headers)\n    if response.status_code == 200:  # 使用 response.status_code 进行检查\n        print(\"Ntfy 推送成功！\")\n    else:\n        print(\"Ntfy 推送失败！错误信息：\", response.text)\n\n\ndef wxpusher_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 wxpusher 推送消息。\n    支持的环境变量:\n    - WXPUSHER_APP_TOKEN: appToken\n    - WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔\n    - WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔\n    \"\"\"\n    if not push_config.get(\"WXPUSHER_APP_TOKEN\"):\n        return\n\n    url = \"https://wxpusher.zjiecode.com/api/send/message\"\n\n    # 处理topic_ids和uids，将分号分隔的字符串转为数组\n    topic_ids = []\n    if push_config.get(\"WXPUSHER_TOPIC_IDS\"):\n        topic_ids = [\n            int(id.strip())\n            for id in push_config.get(\"WXPUSHER_TOPIC_IDS\").split(\";\")\n            if id.strip()\n        ]\n\n    uids = []\n    if push_config.get(\"WXPUSHER_UIDS\"):\n        uids = [\n            uid.strip()\n            for uid in push_config.get(\"WXPUSHER_UIDS\").split(\";\")\n            if uid.strip()\n        ]\n\n    # topic_ids uids 至少有一个\n    if not topic_ids and not uids:\n        print(\"wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!\")\n        return\n\n    print(\"wxpusher 服务启动\")\n\n    data = {\n        \"appToken\": push_config.get(\"WXPUSHER_APP_TOKEN\"),\n        \"content\": f\"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>\",\n        \"summary\": title,\n        \"contentType\": 2,\n        \"topicIds\": topic_ids,\n        \"uids\": uids,\n        \"verifyPayType\": 0,\n    }\n\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, json=data, headers=headers).json()\n\n    if response.get(\"code\") == 1000:\n        print(\"wxpusher 推送成功！\")\n    else:\n        print(f\"wxpusher 推送失败！错误信息：{response.get('msg')}\")\n\n\ndef parse_headers(headers):\n    if not headers:\n        return {}\n\n    parsed = {}\n    lines = headers.split(\"\\n\")\n\n    for line in lines:\n        i = line.find(\":\")\n        if i == -1:\n            continue\n\n        key = line[:i].strip().lower()\n        val = line[i + 1 :].strip()\n        parsed[key] = parsed.get(key, \"\") + \", \" + val if key in parsed else val\n\n    return parsed\n\n\ndef parse_string(input_string, value_format_fn=None):\n    matches = {}\n    pattern = r\"(\\w+):\\s*((?:(?!\\n\\w+:).)*)\"\n    regex = re.compile(pattern)\n    for match in regex.finditer(input_string):\n        key, value = match.group(1).strip(), match.group(2).strip()\n        try:\n            value = value_format_fn(value) if value_format_fn else value\n            json_value = json.loads(value)\n            matches[key] = json_value\n        except:\n            matches[key] = value\n    return matches\n\n\ndef parse_body(body, content_type, value_format_fn=None):\n    if not body or content_type == \"text/plain\":\n        return value_format_fn(body) if value_format_fn and body else body\n\n    parsed = parse_string(body, value_format_fn)\n\n    if content_type == \"application/x-www-form-urlencoded\":\n        data = urllib.parse.urlencode(parsed, doseq=True)\n        return data\n\n    if content_type == \"application/json\":\n        data = json.dumps(parsed)\n        return data\n\n    return parsed\n\n\ndef custom_notify(title: str, content: str) -> None:\n    \"\"\"\n    通过 自定义通知 推送消息。\n    \"\"\"\n    if not push_config.get(\"WEBHOOK_URL\") or not push_config.get(\"WEBHOOK_METHOD\"):\n        return\n\n    print(\"自定义通知服务启动\")\n\n    WEBHOOK_URL = push_config.get(\"WEBHOOK_URL\")\n    WEBHOOK_METHOD = push_config.get(\"WEBHOOK_METHOD\")\n    WEBHOOK_CONTENT_TYPE = push_config.get(\"WEBHOOK_CONTENT_TYPE\")\n    WEBHOOK_BODY = push_config.get(\"WEBHOOK_BODY\")\n    WEBHOOK_HEADERS = push_config.get(\"WEBHOOK_HEADERS\")\n\n    if \"$title\" not in WEBHOOK_URL and \"$title\" not in WEBHOOK_BODY:\n        print(\"请求头或者请求体中必须包含 $title 和 $content\")\n        return\n\n    headers = parse_headers(WEBHOOK_HEADERS)\n    body = parse_body(\n        WEBHOOK_BODY,\n        WEBHOOK_CONTENT_TYPE,\n        lambda v: v.replace(\"$title\", title.replace(\"\\n\", \"\\\\n\")).replace(\n            \"$content\", content.replace(\"\\n\", \"\\\\n\")\n        ),\n    )\n    formatted_url = WEBHOOK_URL.replace(\n        \"$title\", urllib.parse.quote_plus(title)\n    ).replace(\"$content\", urllib.parse.quote_plus(content))\n    response = requests.request(\n        method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body\n    )\n\n    if response.status_code == 200:\n        print(\"自定义通知推送成功！\")\n    else:\n        print(f\"自定义通知推送失败！{response.status_code} {response.text}\")\n\n\ndef one() -> str:\n    \"\"\"\n    获取一条一言。\n    :return:\n    \"\"\"\n    url = \"https://v1.hitokoto.cn/\"\n    res = requests.get(url).json()\n    return res[\"hitokoto\"] + \"    ----\" + res[\"from\"]\n\n\ndef add_notify_function():\n    notify_function = []\n    if push_config.get(\"BARK_PUSH\"):\n        notify_function.append(bark)\n    if push_config.get(\"CONSOLE\"):\n        notify_function.append(console)\n    if push_config.get(\"DD_BOT_TOKEN\") and push_config.get(\"DD_BOT_SECRET\"):\n        notify_function.append(dingding_bot)\n    if push_config.get(\"FSKEY\"):\n        notify_function.append(feishu_bot)\n    if push_config.get(\"GOBOT_URL\") and push_config.get(\"GOBOT_QQ\"):\n        notify_function.append(go_cqhttp)\n    if push_config.get(\"GOTIFY_URL\") and push_config.get(\"GOTIFY_TOKEN\"):\n        notify_function.append(gotify)\n    if push_config.get(\"IGOT_PUSH_KEY\"):\n        notify_function.append(iGot)\n    if push_config.get(\"PUSH_KEY\"):\n        notify_function.append(serverJ)\n    if push_config.get(\"DEER_KEY\"):\n        notify_function.append(pushdeer)\n    if push_config.get(\"CHAT_URL\") and push_config.get(\"CHAT_TOKEN\"):\n        notify_function.append(chat)\n    if push_config.get(\"PUSH_PLUS_TOKEN\"):\n        notify_function.append(pushplus_bot)\n    if push_config.get(\"WE_PLUS_BOT_TOKEN\"):\n        notify_function.append(weplus_bot)\n    if push_config.get(\"QMSG_KEY\") and push_config.get(\"QMSG_TYPE\"):\n        notify_function.append(qmsg_bot)\n    if push_config.get(\"QYWX_AM\"):\n        notify_function.append(wecom_app)\n    if push_config.get(\"QYWX_KEY\"):\n        notify_function.append(wecom_bot)\n    if push_config.get(\"TG_BOT_TOKEN\") and push_config.get(\"TG_USER_ID\"):\n        notify_function.append(telegram_bot)\n    if (\n        push_config.get(\"AIBOTK_KEY\")\n        and push_config.get(\"AIBOTK_TYPE\")\n        and push_config.get(\"AIBOTK_NAME\")\n    ):\n        notify_function.append(aibotk)\n    if (\n        push_config.get(\"SMTP_SERVER\")\n        and push_config.get(\"SMTP_SSL\")\n        and push_config.get(\"SMTP_EMAIL\")\n        and push_config.get(\"SMTP_PASSWORD\")\n        and push_config.get(\"SMTP_NAME\")\n    ):\n        notify_function.append(smtp)\n    if push_config.get(\"PUSHME_KEY\"):\n        notify_function.append(pushme)\n    if (\n        push_config.get(\"CHRONOCAT_URL\")\n        and push_config.get(\"CHRONOCAT_QQ\")\n        and push_config.get(\"CHRONOCAT_TOKEN\")\n    ):\n        notify_function.append(chronocat)\n    if push_config.get(\"WEBHOOK_URL\") and push_config.get(\"WEBHOOK_METHOD\"):\n        notify_function.append(custom_notify)\n    if push_config.get(\"NTFY_TOPIC\"):\n        notify_function.append(ntfy)\n    if push_config.get(\"WXPUSHER_APP_TOKEN\") and (\n        push_config.get(\"WXPUSHER_TOPIC_IDS\") or push_config.get(\"WXPUSHER_UIDS\")\n    ):\n        notify_function.append(wxpusher_bot)\n    if not notify_function:\n        print(f\"无推送渠道，请检查通知变量是否正确\")\n    return notify_function\n\n\ndef send(title: str, content: str, ignore_default_config: bool = False, **kwargs):\n    if kwargs:\n        global push_config\n        if ignore_default_config:\n            push_config = kwargs  # 清空从环境变量获取的配置\n        else:\n            push_config.update(kwargs)\n\n    if not content:\n        print(f\"{title} 推送内容为空！\")\n        return\n\n    # 根据标题跳过一些消息推送，环境变量：SKIP_PUSH_TITLE 用回车分隔\n    skipTitle = os.getenv(\"SKIP_PUSH_TITLE\")\n    if skipTitle:\n        if title in re.split(\"\\n\", skipTitle):\n            print(f\"{title} 在SKIP_PUSH_TITLE环境变量内，跳过推送！\")\n            return\n\n    hitokoto = push_config.get(\"HITOKOTO\")\n    content += \"\\n\\n\" + one() if hitokoto != \"false\" else \"\"\n\n    notify_function = add_notify_function()\n    ts = [\n        threading.Thread(target=mode, args=(title, content), name=mode.__name__)\n        for mode in notify_function\n    ]\n    [t.start() for t in ts]\n    [t.join() for t in ts]\n\n\ndef main():\n    send(\"title\", \"content\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "sample/notify.py.save",
    "content": "#!/usr/bin/env python3\n# _*_ coding:utf-8 _*_\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport re\nimport threading\nimport time\nimport urllib.parse\nimport smtplib\nfrom email.mime.text import MIMEText\nfrom email.header import Header\nfrom email.utils import formataddr\n\nimport requests\n\n# 原先的 print 函数和主线程的锁\n_print = print\nmutex = threading.Lock()\n\n\n# 定义新的 print 函数\ndef print(text, *args, **kw):\n    \"\"\"\n    使输出有序进行，不出现多线程同一时间输出导致错乱的问题。\n    \"\"\"\n    with mutex:\n        _print(text, *args, **kw)\n\n\n# 通知服务\n# fmt: off\npush_config = {\n    'HITOKOTO': True,                  # 启用一言（随机句子）\n\n    'BARK_PUSH': '',                    # bark IP 或设备码，例：https://api.day.app/DxHcxxxxxRxxxxxxcm/\n    'BARK_ARCHIVE': '',                 # bark 推送是否存档\n    'BARK_GROUP': '',                   # bark 推送分组\n    'BARK_SOUND': '',                   # bark 推送声音\n    'BARK_ICON': '',                    # bark 推送图标\n    'BARK_LEVEL': '',                   # bark 推送时效性\n    'BARK_URL': '',                     # bark 推送跳转URL\n\n    'CONSOLE': False,                    # 控制台输出\n\n    'DD_BOT_SECRET': '',                # 钉钉机器人的 DD_BOT_SECRET\n    'DD_BOT_TOKEN': '',                 # 钉钉机器人的 DD_BOT_TOKEN\n\n    'FSKEY': '',                        # 飞书机器人的 FSKEY\n\n    'GOBOT_URL': '',                    # go-cqhttp\n                                        # 推送到个人QQ：http://127.0.0.1/send_private_msg\n                                        # 群：http://127.0.0.1/send_group_msg\n    'GOBOT_QQ': '',                     # go-cqhttp 的推送群或用户\n                                        # GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ\n                                        #               /send_group_msg   时填入 group_id=QQ群\n    'GOBOT_TOKEN': '',                  # go-cqhttp 的 access_token\n\n    'GOTIFY_URL': '',                   # gotify地址,如https://push.example.de:8080\n    'GOTIFY_TOKEN': '',                 # gotify的消息应用token\n    'GOTIFY_PRIORITY': 0,               # 推送消息优先级,默认为0\n\n    'IGOT_PUSH_KEY': '',                # iGot 聚合推送的 IGOT_PUSH_KEY\n\n    'PUSH_KEY': '',                     # server 酱的 PUSH_KEY，兼容旧版与 Turbo 版\n\n    'DEER_KEY': '',                     # PushDeer 的 PUSHDEER_KEY\n    'DEER_URL': '',                     # PushDeer 的 PUSHDEER_URL\n\n    'CHAT_URL': '',                     # synology chat url\n    'CHAT_TOKEN': '',                   # synology chat token\n\n    'PUSH_PLUS_TOKEN': '',              # push+ 微信推送的用户令牌\n    'PUSH_PLUS_USER': '',               # push+ 微信推送的群组编码\n\n    'WE_PLUS_BOT_TOKEN': '',            # 微加机器人的用户令牌\n    'WE_PLUS_BOT_RECEIVER': '',         # 微加机器人的消息接收者\n    'WE_PLUS_BOT_VERSION': 'pro',          # 微加机器人的调用版本\n\n    'QMSG_KEY': '',                     # qmsg 酱的 QMSG_KEY\n    'QMSG_TYPE': '',                    # qmsg 酱的 QMSG_TYPE\n\n    'QYWX_ORIGIN': '',                  # 企业微信代理地址\n\n    'QYWX_AM': '',                      # 企业微信应用\n\n    'QYWX_KEY': '',                     # 企业微信机器人\n\n    'TG_BOT_TOKEN': '',                 # tg 机器人的 TG_BOT_TOKEN，例：1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ\n    'TG_USER_ID': '',                   # tg 机器人的 TG_USER_ID，例：1434078534\n    'TG_API_HOST': '',                  # tg 代理 api\n    'TG_PROXY_AUTH': '',                # tg 代理认证参数\n    'TG_PROXY_HOST': '',                # tg 机器人的 TG_PROXY_HOST\n    'TG_PROXY_PORT': '',                # tg 机器人的 TG_PROXY_PORT\n\n    'AIBOTK_KEY': '',                   # 智能微秘书 个人中心的apikey 文档地址：http://wechat.aibotk.com/docs/about\n    'AIBOTK_TYPE': '',                  # 智能微秘书 发送目标 room 或 contact\n    'AIBOTK_NAME': '',                  # 智能微秘书  发送群名 或者好友昵称和type要对应好\n\n    'SMTP_SERVER': '',                  # SMTP 发送邮件服务器，形如 smtp.exmail.qq.com:465\n    'SMTP_SSL': 'false',                # SMTP 发送邮件服务器是否使用 SSL，填写 true 或 false\n    'SMTP_EMAIL': '',                   # SMTP 收发件邮箱，通知将会由自己发给自己\n    'SMTP_PASSWORD': '',                # SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\n    'SMTP_NAME': '',                    # SMTP 收发件人姓名，可随意填写\n\n    'PUSHME_KEY': '',                   # PushMe 的 PUSHME_KEY\n    'PUSHME_URL': '',                   # PushMe 的 PUSHME_URL\n\n    'CHRONOCAT_QQ': '',                 # qq号\n    'CHRONOCAT_TOKEN': '',              # CHRONOCAT 的token\n    'CHRONOCAT_URL': '',                # CHRONOCAT的url地址\n\n    'WEBHOOK_URL': '',                  # 自定义通知 请求地址\n    'WEBHOOK_BODY': '',                 # 自定义通知 请求体\n    'WEBHOOK_HEADERS': '',              # 自定义通知 请求头\n    'WEBHOOK_METHOD': '',               # 自定义通知 请求方法\n    'WEBHOOK_CONTENT_TYPE': ''          # 自定义通知 content-type\n\n    'NTFY_URL': '',                     # ntfy地址,如https://ntfy.sh\n    'NTFY_TOPIC': '',                   # ntfy的消息应用topic\n    'NTFY_PRIORITY': '3',               # 推送消息优先级,默认为3\n}\n# fmt: on\n\nfor k in push_config:\n    if os.getenv(k):\n        v = os.getenv(k)\n        push_config[k] = v\n\n\ndef bark(title: str, content: str) -> None:\n    \"\"\"\n    使用 bark 推送消息。\n    \"\"\"\n    if not push_config.get(\"BARK_PUSH\"):\n        print(\"bark 服务的 BARK_PUSH 未设置!!\\n取消推送\")\n        return\n    print(\"bark 服务启动\")\n\n    if push_config.get(\"BARK_PUSH\").startswith(\"http\"):\n        url = f'{push_config.get(\"BARK_PUSH\")}'\n    else:\n        url = f'https://api.day.app/{push_config.get(\"BARK_PUSH\")}'\n\n    bark_params = {\n        \"BARK_ARCHIVE\": \"isArchive\",\n        \"BARK_GROUP\": \"group\",\n        \"BARK_SOUND\": \"sound\",\n        \"BARK_ICON\": \"icon\",\n        \"BARK_LEVEL\": \"level\",\n        \"BARK_URL\": \"url\",\n    }\n    data = {\n        \"title\": title,\n        \"body\": content,\n    }\n    for pair in filter(\n        lambda pairs: pairs[0].startswith(\"BARK_\")\n        and pairs[0] != \"BARK_PUSH\"\n        and pairs[1]\n        and bark_params.get(pairs[0]),\n        push_config.items(),\n    ):\n        data[bark_params.get(pair[0])] = pair[1]\n    headers = {\"Content-Type\": \"application/json;charset=utf-8\"}\n    response = requests.post(\n        url=url, data=json.dumps(data), headers=headers, timeout=15\n    ).json()\n\n    if response[\"code\"] == 200:\n        print(\"bark 推送成功！\")\n    else:\n        print(\"bark 推送失败！\")\n\n\ndef console(title: str, content: str) -> None:\n    \"\"\"\n    使用 控制台 推送消息。\n    \"\"\"\n    print(f\"{title}\\n\\n{content}\")\n\n\ndef dingding_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 钉钉机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"DD_BOT_SECRET\") or not push_config.get(\"DD_BOT_TOKEN\"):\n        print(\"钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\\n取消推送\")\n        return\n    print(\"钉钉机器人 服务启动\")\n\n    timestamp = str(round(time.time() * 1000))\n    secret_enc = push_config.get(\"DD_BOT_SECRET\").encode(\"utf-8\")\n    string_to_sign = \"{}\\n{}\".format(timestamp, push_config.get(\"DD_BOT_SECRET\"))\n    string_to_sign_enc = string_to_sign.encode(\"utf-8\")\n    hmac_code = hmac.new(\n        secret_enc, string_to_sign_enc, digestmod=hashlib.sha256\n    ).digest()\n    sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))\n    url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get(\"DD_BOT_TOKEN\")}&timestamp={timestamp}&sign={sign}'\n    headers = {\"Content-Type\": \"application/json;charset=utf-8\"}\n    data = {\"msgtype\": \"text\", \"text\": {\"content\": f\"{title}\\n\\n{content}\"}}\n    response = requests.post(\n        url=url, data=json.dumps(data), headers=headers, timeout=15\n    ).json()\n\n    if not response[\"errcode\"]:\n        print(\"钉钉机器人 推送成功！\")\n    else:\n        print(\"钉钉机器人 推送失败！\")\n\n\ndef feishu_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 飞书机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"FSKEY\"):\n        print(\"飞书 服务的 FSKEY 未设置!!\\n取消推送\")\n        return\n    print(\"飞书 服务启动\")\n\n    url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get(\"FSKEY\")}'\n    data = {\"msg_type\": \"text\", \"content\": {\"text\": f\"{title}\\n\\n{content}\"}}\n    response = requests.post(url, data=json.dumps(data)).json()\n\n    if response.get(\"StatusCode\") == 0 or response.get(\"code\") == 0:\n        print(\"飞书 推送成功！\")\n    else:\n        print(\"飞书 推送失败！错误信息如下：\\n\", response)\n\n\ndef go_cqhttp(title: str, content: str) -> None:\n    \"\"\"\n    使用 go_cqhttp 推送消息。\n    \"\"\"\n    if not push_config.get(\"GOBOT_URL\") or not push_config.get(\"GOBOT_QQ\"):\n        print(\"go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\\n取消推送\")\n        return\n    print(\"go-cqhttp 服务启动\")\n\n    url = f'{push_config.get(\"GOBOT_URL\")}?access_token={push_config.get(\"GOBOT_TOKEN\")}&{push_config.get(\"GOBOT_QQ\")}&message=标题:{title}\\n内容:{content}'\n    response = requests.get(url).json()\n\n    if response[\"status\"] == \"ok\":\n        print(\"go-cqhttp 推送成功！\")\n    else:\n        print(\"go-cqhttp 推送失败！\")\n\n\ndef gotify(title: str, content: str) -> None:\n    \"\"\"\n    使用 gotify 推送消息。\n    \"\"\"\n    if not push_config.get(\"GOTIFY_URL\") or not push_config.get(\"GOTIFY_TOKEN\"):\n        print(\"gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\\n取消推送\")\n        return\n    print(\"gotify 服务启动\")\n\n    url = f'{push_config.get(\"GOTIFY_URL\")}/message?token={push_config.get(\"GOTIFY_TOKEN\")}'\n    data = {\n        \"title\": title,\n        \"message\": content,\n        \"priority\": push_config.get(\"GOTIFY_PRIORITY\"),\n    }\n    response = requests.post(url, data=data).json()\n\n    if response.get(\"id\"):\n        print(\"gotify 推送成功！\")\n    else:\n        print(\"gotify 推送失败！\")\n\n\ndef iGot(title: str, content: str) -> None:\n    \"\"\"\n    使用 iGot 推送消息。\n    \"\"\"\n    if not push_config.get(\"IGOT_PUSH_KEY\"):\n        print(\"iGot 服务的 IGOT_PUSH_KEY 未设置!!\\n取消推送\")\n        return\n    print(\"iGot 服务启动\")\n\n    url = f'https://push.hellyw.com/{push_config.get(\"IGOT_PUSH_KEY\")}'\n    data = {\"title\": title, \"content\": content}\n    headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n    response = requests.post(url, data=data, headers=headers).json()\n\n    if response[\"ret\"] == 0:\n        print(\"iGot 推送成功！\")\n    else:\n        print(f'iGot 推送失败！{response[\"errMsg\"]}')\n\n\ndef serverJ(title: str, content: str) -> None:\n    \"\"\"\n    通过 serverJ 推送消息。\n    \"\"\"\n    if not push_config.get(\"PUSH_KEY\"):\n        print(\"serverJ 服务的 PUSH_KEY 未设置!!\\n取消推送\")\n        return\n    print(\"serverJ 服务启动\")\n\n    data = {\"text\": title, \"desp\": content.replace(\"\\n\", \"\\n\\n\")}\n    if push_config.get(\"PUSH_KEY\").startswith(\"sctp\"):\n        url = f'https://{push_config.get(\"PUSH_KEY\")}.push.ft07.com/send'\n    else:\n        url = f'https://sctapi.ftqq.com/{push_config.get(\"PUSH_KEY\")}.send'\n    response = requests.post(url, data=data).json()\n\n    if response.get(\"errno\") == 0 or response.get(\"code\") == 0:\n        print(\"serverJ 推送成功！\")\n    else:\n        print(f'serverJ 推送失败！错误码：{response[\"message\"]}')\n\n\ndef pushdeer(title: str, content: str) -> None:\n    \"\"\"\n    通过PushDeer 推送消息\n    \"\"\"\n    if not push_config.get(\"DEER_KEY\"):\n        print(\"PushDeer 服务的 DEER_KEY 未设置!!\\n取消推送\")\n        return\n    print(\"PushDeer 服务启动\")\n    data = {\n        \"text\": title,\n        \"desp\": content,\n        \"type\": \"markdown\",\n        \"pushkey\": push_config.get(\"DEER_KEY\"),\n    }\n    url = \"https://api2.pushdeer.com/message/push\"\n    if push_config.get(\"DEER_URL\"):\n        url = push_config.get(\"DEER_URL\")\n\n    response = requests.post(url, data=data).json()\n\n    if len(response.get(\"content\").get(\"result\")) > 0:\n        print(\"PushDeer 推送成功！\")\n    else:\n        print(\"PushDeer 推送失败！错误信息：\", response)\n\n\ndef chat(title: str, content: str) -> None:\n    \"\"\"\n    通过Chat 推送消息\n    \"\"\"\n    if not push_config.get(\"CHAT_URL\") or not push_config.get(\"CHAT_TOKEN\"):\n        print(\"chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\\n取消推送\")\n        return\n    print(\"chat 服务启动\")\n    data = \"payload=\" + json.dumps({\"text\": title + \"\\n\" + content})\n    url = push_config.get(\"CHAT_URL\") + push_config.get(\"CHAT_TOKEN\")\n    response = requests.post(url, data=data)\n\n    if response.status_code == 200:\n        print(\"Chat 推送成功！\")\n    else:\n        print(\"Chat 推送失败！错误信息：\", response)\n\n\ndef pushplus_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 push+ 推送消息。\n    \"\"\"\n    if not push_config.get(\"PUSH_PLUS_TOKEN\"):\n        print(\"PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\\n取消推送\")\n        return\n    print(\"PUSHPLUS 服务启动\")\n\n    url = \"http://www.pushplus.plus/send\"\n    data = {\n        \"token\": push_config.get(\"PUSH_PLUS_TOKEN\"),\n        \"title\": title,\n        \"content\": content,\n        \"topic\": push_config.get(\"PUSH_PLUS_USER\"),\n    }\n    body = json.dumps(data).encode(encoding=\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, data=body, headers=headers).json()\n\n    if response[\"code\"] == 200:\n        print(\"PUSHPLUS 推送成功！\")\n\n    else:\n        url_old = \"http://pushplus.hxtrip.com/send\"\n        headers[\"Accept\"] = \"application/json\"\n        response = requests.post(url=url_old, data=body, headers=headers).json()\n\n        if response[\"code\"] == 200:\n            print(\"PUSHPLUS(hxtrip) 推送成功！\")\n\n        else:\n            print(\"PUSHPLUS 推送失败！\")\n\n\ndef weplus_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 微加机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"WE_PLUS_BOT_TOKEN\"):\n        print(\"微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\\n取消推送\")\n        return\n    print(\"微加机器人 服务启动\")\n\n    template = \"txt\"\n    if len(content) > 800:\n        template = \"html\"\n\n    url = \"https://www.weplusbot.com/send\"\n    data = {\n        \"token\": push_config.get(\"WE_PLUS_BOT_TOKEN\"),\n        \"title\": title,\n        \"content\": content,\n        \"template\": template,\n        \"receiver\": push_config.get(\"WE_PLUS_BOT_RECEIVER\"),\n        \"version\": push_config.get(\"WE_PLUS_BOT_VERSION\"),\n    }\n    body = json.dumps(data).encode(encoding=\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, data=body, headers=headers).json()\n\n    if response[\"code\"] == 200:\n        print(\"微加机器人 推送成功！\")\n    else:\n        print(\"微加机器人 推送失败！\")\n\n\ndef qmsg_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 qmsg 推送消息。\n    \"\"\"\n    if not push_config.get(\"QMSG_KEY\") or not push_config.get(\"QMSG_TYPE\"):\n        print(\"qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\\n取消推送\")\n        return\n    print(\"qmsg 服务启动\")\n\n    url = f'https://qmsg.zendee.cn/{push_config.get(\"QMSG_TYPE\")}/{push_config.get(\"QMSG_KEY\")}'\n    payload = {\"msg\": f'{title}\\n\\n{content.replace(\"----\", \"-\")}'.encode(\"utf-8\")}\n    response = requests.post(url=url, params=payload).json()\n\n    if response[\"code\"] == 0:\n        print(\"qmsg 推送成功！\")\n    else:\n        print(f'qmsg 推送失败！{response[\"reason\"]}')\n\n\ndef wecom_app(title: str, content: str) -> None:\n    \"\"\"\n    通过 企业微信 APP 推送消息。\n    \"\"\"\n    if not push_config.get(\"QYWX_AM\"):\n        print(\"QYWX_AM 未设置!!\\n取消推送\")\n        return\n    QYWX_AM_AY = re.split(\",\", push_config.get(\"QYWX_AM\"))\n    if 4 < len(QYWX_AM_AY) > 5:\n        print(\"QYWX_AM 设置错误!!\\n取消推送\")\n        return\n    print(\"企业微信 APP 服务启动\")\n\n    corpid = QYWX_AM_AY[0]\n    corpsecret = QYWX_AM_AY[1]\n    touser = QYWX_AM_AY[2]\n    agentid = QYWX_AM_AY[3]\n    try:\n        media_id = QYWX_AM_AY[4]\n    except IndexError:\n        media_id = \"\"\n    wx = WeCom(corpid, corpsecret, agentid)\n    # 如果没有配置 media_id 默认就以 text 方式发送\n    if not media_id:\n        message = title + \"\\n\\n\" + content\n        response = wx.send_text(message, touser)\n    else:\n        response = wx.send_mpnews(title, content, media_id, touser)\n\n    if response == \"ok\":\n        print(\"企业微信推送成功！\")\n    else:\n        print(\"企业微信推送失败！错误信息如下：\\n\", response)\n\n\nclass WeCom:\n    def __init__(self, corpid, corpsecret, agentid):\n        self.CORPID = corpid\n        self.CORPSECRET = corpsecret\n        self.AGENTID = agentid\n        self.ORIGIN = \"https://qyapi.weixin.qq.com\"\n        if push_config.get(\"QYWX_ORIGIN\"):\n            self.ORIGIN = push_config.get(\"QYWX_ORIGIN\")\n\n    def get_access_token(self):\n        url = f\"{self.ORIGIN}/cgi-bin/gettoken\"\n        values = {\n            \"corpid\": self.CORPID,\n            \"corpsecret\": self.CORPSECRET,\n        }\n        req = requests.post(url, params=values)\n        data = json.loads(req.text)\n        return data[\"access_token\"]\n\n    def send_text(self, message, touser=\"@all\"):\n        send_url = (\n            f\"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}\"\n        )\n        send_values = {\n            \"touser\": touser,\n            \"msgtype\": \"text\",\n            \"agentid\": self.AGENTID,\n            \"text\": {\"content\": message},\n            \"safe\": \"0\",\n        }\n        send_msges = bytes(json.dumps(send_values), \"utf-8\")\n        respone = requests.post(send_url, send_msges)\n        respone = respone.json()\n        return respone[\"errmsg\"]\n\n    def send_mpnews(self, title, message, media_id, touser=\"@all\"):\n        send_url = (\n            f\"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}\"\n        )\n        send_values = {\n            \"touser\": touser,\n            \"msgtype\": \"mpnews\",\n            \"agentid\": self.AGENTID,\n            \"mpnews\": {\n                \"articles\": [\n                    {\n                        \"title\": title,\n                        \"thumb_media_id\": media_id,\n                        \"author\": \"Author\",\n                        \"content_source_url\": \"\",\n                        \"content\": message.replace(\"\\n\", \"<br/>\"),\n                        \"digest\": message,\n                    }\n                ]\n            },\n        }\n        send_msges = bytes(json.dumps(send_values), \"utf-8\")\n        respone = requests.post(send_url, send_msges)\n        respone = respone.json()\n        return respone[\"errmsg\"]\n\n\ndef wecom_bot(title: str, content: str) -> None:\n    \"\"\"\n    通过 企业微信机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"QYWX_KEY\"):\n        print(\"企业微信机器人 服务的 QYWX_KEY 未设置!!\\n取消推送\")\n        return\n    print(\"企业微信机器人服务启动\")\n\n    origin = \"https://qyapi.weixin.qq.com\"\n    if push_config.get(\"QYWX_ORIGIN\"):\n        origin = push_config.get(\"QYWX_ORIGIN\")\n\n    url = f\"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}\"\n    headers = {\"Content-Type\": \"application/json;charset=utf-8\"}\n    data = {\"msgtype\": \"text\", \"text\": {\"content\": f\"{title}\\n\\n{content}\"}}\n    response = requests.post(\n        url=url, data=json.dumps(data), headers=headers, timeout=15\n    ).json()\n\n    if response[\"errcode\"] == 0:\n        print(\"企业微信机器人推送成功！\")\n    else:\n        print(\"企业微信机器人推送失败！\")\n\n\ndef telegram_bot(title: str, content: str) -> None:\n    \"\"\"\n    使用 telegram 机器人 推送消息。\n    \"\"\"\n    if not push_config.get(\"TG_BOT_TOKEN\") or not push_config.get(\"TG_USER_ID\"):\n        print(\"tg 服务的 bot_token 或者 user_id 未设置!!\\n取消推送\")\n        return\n    print(\"tg 服务启动\")\n\n    if push_config.get(\"TG_API_HOST\"):\n        url = f\"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage\"\n    else:\n        url = (\n            f\"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage\"\n        )\n    headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n    payload = {\n        \"chat_id\": str(push_config.get(\"TG_USER_ID\")),\n        \"text\": f\"{title}\\n\\n{content}\",\n        \"disable_web_page_preview\": \"true\",\n    }\n    proxies = None\n    if push_config.get(\"TG_PROXY_HOST\") and push_config.get(\"TG_PROXY_PORT\"):\n        if push_config.get(\"TG_PROXY_AUTH\") is not None and \"@\" not in push_config.get(\n            \"TG_PROXY_HOST\"\n        ):\n            push_config[\"TG_PROXY_HOST\"] = (\n                push_config.get(\"TG_PROXY_AUTH\")\n                + \"@\"\n                + push_config.get(\"TG_PROXY_HOST\")\n            )\n        proxyStr = \"http://{}:{}\".format(\n            push_config.get(\"TG_PROXY_HOST\"), push_config.get(\"TG_PROXY_PORT\")\n        )\n        proxies = {\"http\": proxyStr, \"https\": proxyStr}\n    response = requests.post(\n        url=url, headers=headers, params=payload, proxies=proxies\n    ).json()\n\n    if response[\"ok\"]:\n        print(\"tg 推送成功！\")\n    else:\n        print(\"tg 推送失败！\")\n\n\ndef aibotk(title: str, content: str) -> None:\n    \"\"\"\n    使用 智能微秘书 推送消息。\n    \"\"\"\n    if (\n        not push_config.get(\"AIBOTK_KEY\")\n        or not push_config.get(\"AIBOTK_TYPE\")\n        or not push_config.get(\"AIBOTK_NAME\")\n    ):\n        print(\n            \"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\\n取消推送\"\n        )\n        return\n    print(\"智能微秘书 服务启动\")\n\n    if push_config.get(\"AIBOTK_TYPE\") == \"room\":\n        url = \"https://api-bot.aibotk.com/openapi/v1/chat/room\"\n        data = {\n            \"apiKey\": push_config.get(\"AIBOTK_KEY\"),\n            \"roomName\": push_config.get(\"AIBOTK_NAME\"),\n            \"message\": {\"type\": 1, \"content\": f\"【青龙快讯】\\n\\n{title}\\n{content}\"},\n        }\n    else:\n        url = \"https://api-bot.aibotk.com/openapi/v1/chat/contact\"\n        data = {\n            \"apiKey\": push_config.get(\"AIBOTK_KEY\"),\n            \"name\": push_config.get(\"AIBOTK_NAME\"),\n            \"message\": {\"type\": 1, \"content\": f\"【青龙快讯】\\n\\n{title}\\n{content}\"},\n        }\n    body = json.dumps(data).encode(encoding=\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    response = requests.post(url=url, data=body, headers=headers).json()\n    print(response)\n    if response[\"code\"] == 0:\n        print(\"智能微秘书 推送成功！\")\n    else:\n        print(f'智能微秘书 推送失败！{response[\"error\"]}')\n\n\ndef smtp(title: str, content: str) -> None:\n    \"\"\"\n    使用 SMTP 邮件 推送消息。\n    \"\"\"\n    if (\n        not push_config.get(\"SMTP_SERVER\")\n        or not push_config.get(\"SMTP_SSL\")\n        or not push_config.get(\"SMTP_EMAIL\")\n        or not push_config.get(\"SMTP_PASSWORD\")\n        or not push_config.get(\"SMTP_NAME\")\n    ):\n        print(\n            \"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\\n取消推送\"\n        )\n        return\n    print(\"SMTP 邮件 服务启动\")\n\n    message = MIMEText(content, \"plain\", \"utf-8\")\n    message[\"From\"] = formataddr(\n        (\n            Header(push_config.get(\"SMTP_NAME\"), \"utf-8\").encode(),\n            push_config.get(\"SMTP_EMAIL\"),\n        )\n    )\n    message[\"To\"] = formataddr(\n        (\n            Header(push_config.get(\"SMTP_NAME\"), \"utf-8\").encode(),\n            push_config.get(\"SMTP_EMAIL\"),\n        )\n    )\n    message[\"Subject\"] = Header(title, \"utf-8\")\n\n    try:\n        smtp_server = (\n            smtplib.SMTP_SSL(push_config.get(\"SMTP_SERVER\"))\n            if push_config.get(\"SMTP_SSL\") == \"true\"\n            else smtplib.SMTP(push_config.get(\"SMTP_SERVER\"))\n        )\n        smtp_server.login(\n            push_config.get(\"SMTP_EMAIL\"), push_config.get(\"SMTP_PASSWORD\")\n        )\n        smtp_server.sendmail(\n            push_config.get(\"SMTP_EMAIL\"),\n            push_config.get(\"SMTP_EMAIL\"),\n            message.as_bytes(),\n        )\n        smtp_server.close()\n        print(\"SMTP 邮件 推送成功！\")\n    except Exception as e:\n        print(f\"SMTP 邮件 推送失败！{e}\")\n\n\ndef pushme(title: str, content: str) -> None:\n    \"\"\"\n    使用 PushMe 推送消息。\n    \"\"\"\n    if not push_config.get(\"PUSHME_KEY\"):\n        print(\"PushMe 服务的 PUSHME_KEY 未设置!!\\n取消推送\")\n        return\n    print(\"PushMe 服务启动\")\n\n    url = (\n        push_config.get(\"PUSHME_URL\")\n        if push_config.get(\"PUSHME_URL\")\n        else \"https://push.i-i.me/\"\n    )\n    data = {\n        \"push_key\": push_config.get(\"PUSHME_KEY\"),\n        \"title\": title,\n        \"content\": content,\n        \"date\": push_config.get(\"date\") if push_config.get(\"date\") else \"\",\n        \"type\": push_config.get(\"type\") if push_config.get(\"type\") else \"\",\n    }\n    response = requests.post(url, data=data)\n\n    if response.status_code == 200 and response.text == \"success\":\n        print(\"PushMe 推送成功！\")\n    else:\n        print(f\"PushMe 推送失败！{response.status_code} {response.text}\")\n\n\ndef chronocat(title: str, content: str) -> None:\n    \"\"\"\n    使用 CHRONOCAT 推送消息。\n    \"\"\"\n    if (\n        not push_config.get(\"CHRONOCAT_URL\")\n        or not push_config.get(\"CHRONOCAT_QQ\")\n        or not push_config.get(\"CHRONOCAT_TOKEN\")\n    ):\n        print(\"CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\\n取消推送\")\n        return\n\n    print(\"CHRONOCAT 服务启动\")\n\n    user_ids = re.findall(r\"user_id=(\\d+)\", push_config.get(\"CHRONOCAT_QQ\"))\n    group_ids = re.findall(r\"group_id=(\\d+)\", push_config.get(\"CHRONOCAT_QQ\"))\n\n    url = f'{push_config.get(\"CHRONOCAT_URL\")}/api/message/send'\n    headers = {\n        \"Content-Type\": \"application/json\",\n        \"Authorization\": f'Bearer {push_config.get(\"CHRONOCAT_TOKEN\")}',\n    }\n\n    for chat_type, ids in [(1, user_ids), (2, group_ids)]:\n        if not ids:\n            continue\n        for chat_id in ids:\n            data = {\n                \"peer\": {\"chatType\": chat_type, \"peerUin\": chat_id},\n                \"elements\": [\n                    {\n                        \"elementType\": 1,\n                        \"textElement\": {\"content\": f\"{title}\\n\\n{content}\"},\n                    }\n                ],\n            }\n            response = requests.post(url, headers=headers, data=json.dumps(data))\n            if response.status_code == 200:\n                if chat_type == 1:\n                    print(f\"QQ个人消息:{ids}推送成功！\")\n                else:\n                    print(f\"QQ群消息:{ids}推送成功！\")\n            else:\n                if chat_type == 1:\n                    print(f\"QQ个人消息:{ids}推送失败！\")\n                else:\n                    print(f\"QQ群消息:{ids}推送失败！\")\n\ndef ntfy(title: str, content: str) -> None:\n    \"\"\"\n    使用 Ntfy 推送消息。\n    \"\"\"\n    if not push_config.get(\"Ntfy_T\"):\n        print(\"PushMe 服务的 PUSHME_KEY 未设置!!\\n取消推送\")\n        return\n    print(\"PushMe 服务启动\")\n\n    url = push_config.get(\"PUSHME_URL\") if push_config.get(\"PUSHME_URL\") else \"https://push.i-i.me/\"\n    data = {\n        \"push_key\": push_config.get(\"PUSHME_KEY\"),\n        \"title\": title,\n        \"content\": content,\n        \"date\": push_config.get(\"date\") if push_config.get(\"date\") else \"\",\n        \"type\": push_config.get(\"type\") if push_config.get(\"type\") else \"\",\n    }\n    response = requests.post(url, data=data)\n\n    if response.status_code == 200 and response.text == \"success\":\n        print(\"PushMe 推送成功！\")\n    else:\n        print(f\"PushMe 推送失败！{response.status_code} {response.text}\")\n\n\ndef parse_headers(headers):\n    if not headers:\n        return {}\n\n    parsed = {}\n    lines = headers.split(\"\\n\")\n\n    for line in lines:\n        i = line.find(\":\")\n        if i == -1:\n            continue\n\n        key = line[:i].strip().lower()\n        val = line[i + 1 :].strip()\n        parsed[key] = parsed.get(key, \"\") + \", \" + val if key in parsed else val\n\n    return parsed\n\n\ndef parse_string(input_string, value_format_fn=None):\n    matches = {}\n    pattern = r\"(\\w+):\\s*((?:(?!\\n\\w+:).)*)\"\n    regex = re.compile(pattern)\n    for match in regex.finditer(input_string):\n        key, value = match.group(1).strip(), match.group(2).strip()\n        try:\n            value = value_format_fn(value) if value_format_fn else value\n            json_value = json.loads(value)\n            matches[key] = json_value\n        except:\n            matches[key] = value\n    return matches\n\n\ndef parse_body(body, content_type, value_format_fn=None):\n    if not body or content_type == \"text/plain\":\n        return value_format_fn(body) if value_format_fn and body else body\n\n    parsed = parse_string(body, value_format_fn)\n\n    if content_type == \"application/x-www-form-urlencoded\":\n        data = urllib.parse.urlencode(parsed, doseq=True)\n        return data\n\n    if content_type == \"application/json\":\n        data = json.dumps(parsed)\n        return data\n\n    return parsed\n\n\ndef custom_notify(title: str, content: str) -> None:\n    \"\"\"\n    通过 自定义通知 推送消息。\n    \"\"\"\n    if not push_config.get(\"WEBHOOK_URL\") or not push_config.get(\"WEBHOOK_METHOD\"):\n        print(\"自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\\n取消推送\")\n        return\n\n    print(\"自定义通知服务启动\")\n\n    WEBHOOK_URL = push_config.get(\"WEBHOOK_URL\")\n    WEBHOOK_METHOD = push_config.get(\"WEBHOOK_METHOD\")\n    WEBHOOK_CONTENT_TYPE = push_config.get(\"WEBHOOK_CONTENT_TYPE\")\n    WEBHOOK_BODY = push_config.get(\"WEBHOOK_BODY\")\n    WEBHOOK_HEADERS = push_config.get(\"WEBHOOK_HEADERS\")\n\n    if \"$title\" not in WEBHOOK_URL and \"$title\" not in WEBHOOK_BODY:\n        print(\"请求头或者请求体中必须包含 $title 和 $content\")\n        return\n\n    headers = parse_headers(WEBHOOK_HEADERS)\n    body = parse_body(\n        WEBHOOK_BODY,\n        WEBHOOK_CONTENT_TYPE,\n        lambda v: v.replace(\"$title\", title.replace(\"\\n\", \"\\\\n\")).replace(\n            \"$content\", content.replace(\"\\n\", \"\\\\n\")\n        ),\n    )\n    formatted_url = WEBHOOK_URL.replace(\n        \"$title\", urllib.parse.quote_plus(title)\n    ).replace(\"$content\", urllib.parse.quote_plus(content))\n    response = requests.request(\n        method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body\n    )\n\n    if response.status_code == 200:\n        print(\"自定义通知推送成功！\")\n    else:\n        print(f\"自定义通知推送失败！{response.status_code} {response.text}\")\n\n\ndef one() -> str:\n    \"\"\"\n    获取一条一言。\n    :return:\n    \"\"\"\n    url = \"https://v1.hitokoto.cn/\"\n    res = requests.get(url).json()\n    return res[\"hitokoto\"] + \"    ----\" + res[\"from\"]\n\n\ndef add_notify_function():\n    notify_function = []\n    if push_config.get(\"BARK_PUSH\"):\n        notify_function.append(bark)\n    if push_config.get(\"CONSOLE\"):\n        notify_function.append(console)\n    if push_config.get(\"DD_BOT_TOKEN\") and push_config.get(\"DD_BOT_SECRET\"):\n        notify_function.append(dingding_bot)\n    if push_config.get(\"FSKEY\"):\n        notify_function.append(feishu_bot)\n    if push_config.get(\"GOBOT_URL\") and push_config.get(\"GOBOT_QQ\"):\n        notify_function.append(go_cqhttp)\n    if push_config.get(\"GOTIFY_URL\") and push_config.get(\"GOTIFY_TOKEN\"):\n        notify_function.append(gotify)\n    if push_config.get(\"IGOT_PUSH_KEY\"):\n        notify_function.append(iGot)\n    if push_config.get(\"PUSH_KEY\"):\n        notify_function.append(serverJ)\n    if push_config.get(\"DEER_KEY\"):\n        notify_function.append(pushdeer)\n    if push_config.get(\"CHAT_URL\") and push_config.get(\"CHAT_TOKEN\"):\n        notify_function.append(chat)\n    if push_config.get(\"PUSH_PLUS_TOKEN\"):\n        notify_function.append(pushplus_bot)\n    if push_config.get(\"WE_PLUS_BOT_TOKEN\"):\n        notify_function.append(weplus_bot)\n    if push_config.get(\"QMSG_KEY\") and push_config.get(\"QMSG_TYPE\"):\n        notify_function.append(qmsg_bot)\n    if push_config.get(\"QYWX_AM\"):\n        notify_function.append(wecom_app)\n    if push_config.get(\"QYWX_KEY\"):\n        notify_function.append(wecom_bot)\n    if push_config.get(\"TG_BOT_TOKEN\") and push_config.get(\"TG_USER_ID\"):\n        notify_function.append(telegram_bot)\n    if (\n        push_config.get(\"AIBOTK_KEY\")\n        and push_config.get(\"AIBOTK_TYPE\")\n        and push_config.get(\"AIBOTK_NAME\")\n    ):\n        notify_function.append(aibotk)\n    if (\n        push_config.get(\"SMTP_SERVER\")\n        and push_config.get(\"SMTP_SSL\")\n        and push_config.get(\"SMTP_EMAIL\")\n        and push_config.get(\"SMTP_PASSWORD\")\n        and push_config.get(\"SMTP_NAME\")\n    ):\n        notify_function.append(smtp)\n    if push_config.get(\"PUSHME_KEY\"):\n        notify_function.append(pushme)\n    if (\n        push_config.get(\"CHRONOCAT_URL\")\n        and push_config.get(\"CHRONOCAT_QQ\")\n        and push_config.get(\"CHRONOCAT_TOKEN\")\n    ):\n        notify_function.append(chronocat)\n    if push_config.get(\"WEBHOOK_URL\") and push_config.get(\"WEBHOOK_METHOD\"):\n        notify_function.append(custom_notify)\n\n    if not notify_function:\n        print(f\"无推送渠道，请检查通知变量是否正确\")\n    return notify_function\n\n\ndef send(title: str, content: str, ignore_default_config: bool = False, **kwargs):\n    if kwargs:\n        global push_config\n        if ignore_default_config:\n            push_config = kwargs  # 清空从环境变量获取的配置\n        else:\n            push_config.update(kwargs)\n\n    if not content:\n        print(f\"{title} 推送内容为空！\")\n        return\n\n    # 根据标题跳过一些消息推送，环境变量：SKIP_PUSH_TITLE 用回车分隔\n    skipTitle = os.getenv(\"SKIP_PUSH_TITLE\")\n    if skipTitle:\n        if title in re.split(\"\\n\", skipTitle):\n            print(f\"{title} 在SKIP_PUSH_TITLE环境变量内，跳过推送！\")\n            return\n\n    hitokoto = push_config.get(\"HITOKOTO\")\n    content += \"\\n\\n\" + one() if hitokoto != \"false\" else \"\"\n\n    notify_function = add_notify_function()\n    ts = [\n        threading.Thread(target=mode, args=(title, content), name=mode.__name__)\n        for mode in notify_function\n    ]\n    [t.start() for t in ts]\n    [t.join() for t in ts]\n\n\ndef main():\n    send(\"title\", \"content\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "sample/ql_sample.js",
    "content": "/**\n * 任务名称\n * name: script name\n * 定时规则\n * cron: 1 9 * * *\n */\nconsole.log('test scripts');\nQLAPI.notify('test scripts', 'test desc');\nQLAPI.getEnvs({ searchValue: 'dddd' }).then((x) => {\n  console.log('getEnvs', x);\n});\nQLAPI.systemNotify({ title: '123', content: '231' }).then((x) => {\n  console.log('systemNotify', x);\n});\n\n// 查询定时任务 (Query cron tasks)\nQLAPI.getCrons({ searchValue: 'test' }).then((x) => {\n  console.log('getCrons', x);\n});\n\n// 通过ID查询定时任务 (Get cron by ID)\nQLAPI.getCronById({ id: 1 }).then((x) => {\n  console.log('getCronById', x);\n}).catch((err) => {\n  console.log('getCronById error', err);\n});\n\n// 启用定时任务 (Enable cron tasks)\nQLAPI.enableCrons({ ids: [1, 2] }).then((x) => {\n  console.log('enableCrons', x);\n});\n\n// 禁用定时任务 (Disable cron tasks)\nQLAPI.disableCrons({ ids: [1, 2] }).then((x) => {\n  console.log('disableCrons', x);\n});\n\n// 手动执行定时任务 (Run cron tasks manually)\nQLAPI.runCrons({ ids: [1] }).then((x) => {\n  console.log('runCrons', x);\n});\n\nconsole.log('test desc');\n"
  },
  {
    "path": "sample/ql_sample.py",
    "content": "\"\"\"\n任务名称\nname: script name\n定时规则\ncron: 1 9 * * *\n\"\"\"\n\nprint(\"test script\")\nprint(QLAPI.notify(\"test script\", \"test desc\"))\nprint(\"test systemNotify\")\nprint(QLAPI.systemNotify({\"title\": \"test script\", \"content\": \"dddd\"}))\nprint(\"test getEnvs\")\nprint(QLAPI.getEnvs({\"searchValue\": \"1\"}))\nprint(\"test desc\")\n"
  },
  {
    "path": "sample/task.sample.sh",
    "content": "#!/usr/bin/env bash\n"
  },
  {
    "path": "sample/tool.ts",
    "content": "import * as qiniu from 'qiniu';\nimport dotenv from 'dotenv';\nconst envFound = dotenv.config();\n\nconst accessKey = process.env.QINIU_AK;\nconst secretKey = process.env.QINIU_SK;\nconst mac = new qiniu.auth.digest.Mac(accessKey, secretKey);\nconst key = 'version.yaml';\nconst options = {\n  scope: `${process.env.QINIU_SCOPE}:${key}`,\n};\nconst putPolicy = new qiniu.rs.PutPolicy(options);\nconst uploadToken = putPolicy.uploadToken(mac);\n\nconst localFile = 'version.yaml';\nconst config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });\nconst formUploader = new qiniu.form_up.FormUploader(config);\nconst putExtra = new qiniu.form_up.PutExtra(\n  '',\n  {},\n  'text/plain; charset=utf-8',\n);\n// 文件上传\nformUploader.putFile(\n  uploadToken,\n  key,\n  localFile,\n  putExtra,\n  function (respErr, respBody, respInfo) {\n    if (respErr) {\n      throw respErr;\n    }\n    if (respInfo.statusCode == 200) {\n      console.log(respBody);\n    } else {\n      console.log(respInfo.statusCode);\n      console.log(respBody);\n    }\n  },\n);\n"
  },
  {
    "path": "shell/api.sh",
    "content": "#!/usr/bin/env bash\n\ncreate_token() {\n  local token_command=\"ts-node-transpile-only ${dir_root}/back/token.ts\"\n  local token_file=\"${dir_root}/static/build/token.js\"\n  if [[ -f $token_file ]]; then\n    token_command=\"node ${token_file}\"\n  fi\n  __ql_token__=$(eval \"$token_command\")\n}\n\nget_token() {\n  if [[ -f $file_auth_token ]]; then\n    __ql_token__=$(cat $file_auth_token | jq -r .value)\n    local expiration=$(cat $file_auth_token | jq -r .expiration)\n    local currentTimeStamp=$(date +%s)\n    if [[ $currentTimeStamp -ge $expiration ]]; then\n      create_token\n    fi\n  else\n    create_token\n  fi\n}\n\nadd_cron_api() {\n  local currentTimeStamp=$(date +%s)\n  if [[ $# -eq 1 ]]; then\n    local schedule=$(echo \"$1\" | awk -F \":\" '{print $1}')\n    local command=$(echo \"$1\" | awk -F \":\" '{print $2}')\n    local name=$(echo \"$1\" | awk -F \":\" '{print $3}')\n    local sub_id=$(echo \"$1\" | awk -F \":\" '{print $4}')\n  else\n    local schedule=\"$1\"\n    local command=\"$2\"\n    local name=\"$3\"\n    local sub_id=\"$4\"\n  fi\n\n  if [[ ! $sub_id ]]; then\n    sub_id=\"null\"\n  fi\n\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp\" \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"{\\\"name\\\":\\\"${name//\\\"/\\\\\\\"}\\\",\\\"command\\\":\\\"${command//\\\"/\\\\\\\"}\\\",\\\"schedule\\\":\\\"$schedule\\\",\\\"sub_id\\\":$sub_id}\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code == 200 ]]; then\n    echo -e \"$name -> 添加成功\"\n  else\n    echo -e \"$name -> 添加失败(${message})\"\n  fi\n}\n\nupdate_cron_api() {\n  local currentTimeStamp=$(date +%s)\n  if [[ $# -eq 1 ]]; then\n    local schedule=$(echo \"$1\" | awk -F \":\" '{print $1}')\n    local command=$(echo \"$1\" | awk -F \":\" '{print $2}')\n    local name=$(echo \"$1\" | awk -F \":\" '{print $3}')\n    local id=$(echo \"$1\" | awk -F \":\" '{print $4}')\n  else\n    local schedule=\"$1\"\n    local command=\"$2\"\n    local name=\"$3\"\n    local id=\"$4\"\n  fi\n\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp\" \\\n      -X 'PUT' \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"{\\\"name\\\":\\\"${name//\\\"/\\\\\\\"}\\\",\\\"command\\\":\\\"${command//\\\"/\\\\\\\"}\\\",\\\"schedule\\\":\\\"$schedule\\\",\\\"id\\\":\\\"$id\\\"}\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code == 200 ]]; then\n    echo -e \"$name -> 更新成功\"\n  else\n    echo -e \"$name -> 更新失败(${message})\"\n  fi\n}\n\nupdate_cron_command_api() {\n  local currentTimeStamp=$(date +%s)\n  if [[ $# -eq 1 ]]; then\n    local command=$(echo \"$1\" | awk -F \":\" '{print $1}')\n    local id=$(echo \"$1\" | awk -F \":\" '{print $2}')\n  else\n    local command=\"$1\"\n    local id=\"$2\"\n  fi\n\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp\" \\\n      -X 'PUT' \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"{\\\"command\\\":\\\"${command//\\\"/\\\\\\\"}\\\",\\\"id\\\":\\\"$id\\\"}\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code == 200 ]]; then\n    echo -e \"$command -> 更新成功\"\n  else\n    echo -e \"$command -> 更新失败(${message})\"\n  fi\n}\n\ndel_cron_api() {\n  local ids=\"$1\"\n  local currentTimeStamp=$(date +%s)\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp\" \\\n      -X 'DELETE' \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"[$ids]\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code == 200 ]]; then\n    echo -e \"成功\"\n  else\n    echo -e \"失败(${message})\"\n  fi\n}\n\nupdate_cron() {\n  local ids=\"$1\"\n  local status=\"$2\"\n  local pid=\"${3:-''}\"\n  local logPath=\"$4\"\n  local lastExecutingTime=\"${5:-0}\"\n  local runningTime=\"${6:-0}\"\n  local currentTimeStamp=$(date +%s)\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/crons/status?t=$currentTimeStamp\" \\\n      -X 'PUT' \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"{\\\"ids\\\":[$ids],\\\"status\\\":\\\"$status\\\",\\\"pid\\\":\\\"$pid\\\",\\\"log_path\\\":\\\"$logPath\\\",\\\"last_execution_time\\\":$lastExecutingTime,\\\"last_running_time\\\":$runningTime}\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code != 200 ]]; then\n    if [[ ! $message ]]; then\n      message=\"$api\"\n    fi\n    echo -e \"${message}\"\n  fi\n}\n\nnotify_api() {\n  local title=\"$1\"\n  local content=\"$2\"\n  local currentTimeStamp=$(date +%s)\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/system/notify?t=$currentTimeStamp\" \\\n      -X 'PUT' \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"{\\\"title\\\":\\\"${title//\\\"/\\\\\\\"}\\\",\\\"content\\\":\\\"${content//\\\"/\\\\\\\"}\\\"}\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code == 200 ]]; then\n    echo -e \"通知发送成功🎉\"\n  else\n    echo -e \"通知失败(${message})\"\n  fi\n}\n\nfind_cron_api() {\n  local params=\"$1\"\n  local currentTimeStamp=$(date +%s)\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/crons/detail?$params&t=$currentTimeStamp\" \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --compressed\n  )\n  data=$(echo \"$api\" | jq -r .data)\n  if [[ $data == 'null' ]]; then\n    echo -e \"\"\n  else\n    name=$(echo \"$api\" | jq -r .data.name)\n    echo -e \"$name\"\n  fi\n}\n\nupdate_auth_config() {\n  local body=\"$1\"\n  local tip=\"$2\"\n  local currentTimeStamp=$(date +%s)\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/open/system/auth/reset?t=$currentTimeStamp\" \\\n      -X 'PUT' \\\n      -H \"Authorization: Bearer ${__ql_token__}\" \\\n      -H \"Content-Type: application/json;charset=UTF-8\" \\\n      --data-raw \"{$body}\" \\\n      --compressed\n  )\n  code=$(echo \"$api\" | jq -r .code)\n  message=$(echo \"$api\" | jq -r .message)\n  if [[ $code == 200 ]]; then\n    echo -e \"${tip}成功🎉\"\n  else\n    echo -e \"${tip}失败(${message})\"\n  fi\n}\n\nget_token\n"
  },
  {
    "path": "shell/bot.sh",
    "content": "#!/usr/bin/env bash\n\nif [[ -z ${BotRepoUrl} ]]; then\n  url=\"https://github.com/SuMaiKaDe/bot.git\"\n  repo_path=\"${dir_repo}/dockerbot\"\nelse\n  url=${BotRepoUrl}\n  repo_path=\"${dir_repo}/diybot\"\nfi\n\necho -e \"\\n1、安装bot依赖...\\n\"\napk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev\necho -e \"\\nbot依赖安装成功...\\n\"\n\necho -e \"2、下载bot所需文件...\\n\"\nif [[ ! -d ${repo_path}/.git ]]; then\n  rm -rf ${repo_path}\n  git_clone_scripts ${url} ${repo_path} \"main\"\nfi\n\ncp -rf \"$repo_path/jbot\" $dir_data\nif [[ ! -f \"$dir_config/bot.json\" ]]; then\n  cp -f \"$repo_path/config/bot.json\" \"$dir_config\"\nfi\necho -e \"\\nbot文件下载成功...\\n\"\n\necho -e \"3、安装python3依赖...\\n\"\ncp -f \"$repo_path/jbot/requirements.txt\" \"$dir_data\"\n\ncd $dir_data\ncat requirements.txt | while read LREAD; do\n  if [[ ! $(pip3 show \"${LREAD%%=*}\" 2>/dev/null) ]]; then\n    pip3 --default-timeout=100 install ${LREAD}\n  fi\ndone\n\necho -e \"\\npython3依赖安装成功...\\n\"\n\necho -e \"4、启动bot程序...\\n\"\nmake_dir $dir_log/bot\ncd $dir_data\nps -eo pid,command | grep \"python3 -m jbot\" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null\nnohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &\necho -e \"bot启动成功...\\n\"\n"
  },
  {
    "path": "shell/check.sh",
    "content": "#!/usr/bin/env bash\n\nreset_env() {\n  echo -e \"---> 1. 开始检测配置文件\\n\"\n  fix_config\n  echo -e \"---> 配置文件检测完成\\n\"\n\n  echo -e \"---> 2. 开始安装青龙依赖\\n\"\n  npm_install_2 $dir_root\n  echo -e \"---> 青龙依赖安装完成\\n\"\n\n  echo -e \"---> 脚本依赖安装完成\\n\"\n}\n\ncopy_dep() {\n  echo -e \"---> 1. 复制通知文件\\n\"\n  echo -e \"---> 复制一份 $file_notify_py_sample 为 $file_notify_py\\n\"\n  cp -fv $file_notify_py_sample $file_notify_py\n  echo\n  echo -e \"---> 复制一份 $file_notify_js_sample 为 $file_notify_js\\n\"\n  cp -fv $file_notify_js_sample $file_notify_js\n  echo -e \"---> 通知文件复制完成\\n\"\n}\n\npm2_log() {\n  echo -e \"---> pm2日志\"\n  local panelOut=\"/root/.pm2/logs/qinglong-out.log\"\n  local panelError=\"/root/.pm2/logs/qinglong-error.log\"\n  tail -n 300 \"$panelOut\"\n  tail -n 300 \"$panelError\"\n}\n\ncheck_ql() {\n  local api=$(curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}\")\n  echo -e \"\\n=====> 检测面板\\n\\n$api\\n\"\n  if [[ $api =~ \"<div id=\\\"root\\\"></div>\" ]]; then\n    echo -e \"=====> 面板服务启动正常\\n\"\n  fi\n}\n\ncheck_pm2() {\n  pm2_log\n  local currentTimeStamp=$(date +%s)\n  local api=$(\n    curl -s --noproxy \"*\" \"http://0.0.0.0:${ql_port}/api/system?t=$currentTimeStamp\" \\\n      -H 'Accept: */*' \\\n      -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \\\n      -H \"Referer: http://0.0.0.0:${ql_port}/crontab\" \\\n      -H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \\\n      --compressed\n  )\n  echo -e \"\\n=====> 检测后台\\n\\n$api\\n\"\n  if [[ $api =~ \"{\\\"code\\\"\" ]]; then\n    echo -e \"=====> 后台服务启动正常\\n\"\n  fi\n}\n\nmain() {\n  echo -e \"=====> 开始检测\"\n  npm i -g pnpm@8.3.1 pm2 ts-node\n\n  reset_env\n  copy_dep\n  check_ql\n  check_pm2\n  reload_pm2\n  echo -e \"\\n=====> 检测结束\\n\"\n}\n\nmain\n"
  },
  {
    "path": "shell/env.sh",
    "content": "#!/usr/bin/env bash\n\nstore_env_vars() {\n  initial_vars=($(env | cut -d= -f1))\n}\n\nrestore_env_vars() {\n  for key in $(env | cut -d= -f1); do\n    if ! [[ \" ${initial_vars[@]} \" =~ \" $key \" ]]; then\n      unset \"$key\"\n    fi\n  done\n}\n\nstore_env_vars\n"
  },
  {
    "path": "shell/otask.sh",
    "content": "#!/usr/bin/env bash\n\nrandom_delay() {\n  local random_delay_max=$RandomDelay\n  if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then\n    local file_param=$1\n    local file_extensions=${RandomDelayFileExtensions-\"js\"}\n    local ignored_minutes=${RandomDelayIgnoredMinutes-\"0 30\"}\n\n    if [[ -n $file_extensions ]]; then\n      if ! echo \"$file_param\" | grep -qE \"\\.${file_extensions// /$|\\\\.}$\"; then\n        # echo -e \"\\n当前文件需要准点运行, 放弃随机延迟\\n\"\n        return\n      fi\n    fi\n\n    local current_min\n    current_min=$(date \"+%-M\")\n    for minute in $ignored_minutes; do\n      if [[ $current_min -eq $minute ]]; then\n        # echo -e \"\\n当前时间需要准点运行, 放弃随机延迟\\n\"\n        return\n      fi\n    done\n\n    local delay_second=$(($(gen_random_num \"$random_delay_max\") + 1))\n    echo -e \"任务随机延迟 $delay_second 秒，配置文件参数 RandomDelay 置空可取消延迟 \\n\"\n    sleep $delay_second\n  fi\n}\n\n## scripts目录下所有可运行脚本数组\ngen_array_scripts() {\n  local dir_current=$(pwd)\n  local i=\"-1\"\n  cd $dir_scripts\n  for file in $(ls); do\n    if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then\n      let i++\n      array_scripts[i]=$(echo \"$file\" | perl -pe \"s|$dir_scripts/||g\")\n      array_scripts_name[i]=$(grep \"new Env\" $file | awk -F \"\\(\" '{print $2}' | awk -F \"\\)\" '{print $1}' | sed 's:.*\\('\\''\\|\"\\)\\([^\"'\\'']*\\)\\('\\''\\|\"\\).*:\\2:' | sed 's:\"::g' | sed \"s:'::g\" | head -1)\n      [[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]=\"<未识别出活动名称>\"\n    fi\n  done\n  cd $dir_current\n}\n\n## 使用说明\nusage() {\n  gen_array_scripts\n  echo -e \"task命令运行本程序自动添加进crontab的脚本，需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径（定时任务中请写作相对路径），用法为：\"\n  echo -e \"1.$cmd_task <file_name>                                             # 依次执行，如果设置了随机延迟，将随机延迟一定秒数\"\n  echo -e \"2.$cmd_task <file_name> now                                         # 依次执行，无论是否设置了随机延迟，均立即运行，前台会输出日志，同时记录在日志文件中\"\n  echo -e \"3.$cmd_task <file_name> conc <环境变量名称> <账号编号，空格分隔>(可选的)  # 并发执行，无论是否设置了随机延迟，均立即运行，前台不产生日志，直接记录在日志文件中，且可指定账号执行\"\n  echo -e \"4.$cmd_task <file_name> desi <环境变量名称> <账号编号，空格分隔>         # 指定账号执行，无论是否设置了随机延迟，均立即运行\"\n  if [[ ${#array_scripts[*]} -gt 0 ]]; then\n    echo -e \"\\n当前有以下脚本可以运行:\"\n    for ((i = 0; i < ${#array_scripts[*]}; i++)); do\n      echo -e \"$(($i + 1)). ${array_scripts_name[i]}：${array_scripts[i]}\"\n    done\n  else\n    echo -e \"\\n暂无脚本可以执行\"\n  fi\n}\n\n## run nohup，$1：文件名，不含路径，带后缀\nrun_nohup() {\n  local file_name=$1\n  nohup node $file_name &>$log_path &\n}\n\nenv_str_to_array() {\n  . $file_env\n  local IFS=\"&\"\n  read -ra array <<<\"${!env_param}\"\n  array_length=${#array[@]}\n  clear_env\n}\n\nclear_non_sh_env() {\n  if [[ $file_param != *.sh ]]; then\n    clear_env\n  fi\n}\n\n## 正常运行单个脚本，$1：传入参数\nrun_normal() {\n  local file_param=$1\n  if [[ $# -eq 1 ]] && [[ \"$real_time\" != \"true\" ]] && [[ \"$no_delay\" != \"true\" ]]; then\n    random_delay \"$file_param\"\n  fi\n\n  cd $dir_scripts\n  local relative_path=\"${file_param%/*}\"\n  if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ \"/\" ]]; then\n    cd ${relative_path}\n    file_param=${file_param/$relative_path\\//}\n  fi\n\n  if [[ $isJsOrPythonFile == 'false' ]]; then\n    clear_non_sh_env\n  fi\n  $timeoutCmd $which_program $file_param \"${script_params[@]}\"\n}\n\nhandle_env_split() {\n  if [[ ! $num_param ]]; then\n    num_param=\"1-max\"\n  fi\n\n  env_str_to_array\n  local tempArr=$(echo $num_param | sed \"s/-max/-${array_length}/g\" | sed \"s/max-/${array_length}-/g\" | perl -pe \"s|(\\d+)(-\\|~\\|_)(\\d+)|{\\1..\\3}|g\")\n  local runArr=($(eval echo $tempArr))\n  array_run=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))\n}\n\n## 并发执行时，设定的 RandomDelay 不会生效，即所有任务立即执行\nrun_concurrent() {\n  local file_param=\"$1\"\n  local env_param=\"$2\"\n  local num_param=$(echo \"$3\" | perl -pe \"s|.*$2(.*)|\\1|\" | awk '{$1=$1};1')\n  if [[ ! $env_param ]]; then\n    echo -e \"\\n 缺少并发运行的环境变量参数\"\n    exit 1\n  fi\n\n  handle_env_split\n  time=$(date \"+$mtime_format\")\n  single_log_time=$(format_log_time \"$mtime_format\" \"$time\")\n\n  cd $dir_scripts\n  local relative_path=\"${file_param%/*}\"\n  if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ \"/\" ]]; then\n    cd ${relative_path}\n    file_param=${file_param/$relative_path\\//}\n  fi\n\n  local j=0\n  for i in ${array_run[@]}; do\n    single_log_path=\"$dir_log/$log_dir/${single_log_time}_$((j + 1)).log\"\n    let j++\n\n    if [[ $isJsOrPythonFile == 'false' ]]; then\n      export \"${env_param}=${array[$i - 1]}\"\n      clear_non_sh_env\n    fi\n    eval envParam=\"${env_param}\" numParam=\"${i}\" $timeoutCmd $which_program $file_param \"${script_params[@]}\" &>$single_log_path &\n  done\n\n  wait\n  local k=0\n  for i in ${array_run[@]}; do\n    single_log_path=\"$dir_log/$log_dir/${single_log_time}_$((k + 1)).log\"\n    let k++\n    cat $single_log_path\n    [[ -f $single_log_path ]] && rm -f $single_log_path\n  done\n}\n\nrun_designated() {\n  local file_param=\"$1\"\n  local env_param=\"$2\"\n  local num_param=$(echo \"$3\" | perl -pe \"s|.*$2(.*)|\\1|\" | awk '{$1=$1};1')\n  if [[ ! $env_param ]]; then\n    echo -e \"\\n 缺少单独运行的参数 task xxx.js desi Test\"\n    exit 1\n  fi\n\n  handle_env_split\n\n  if [[ $isJsOrPythonFile == 'false' ]]; then\n    local n=0\n    for i in ${array_run[@]}; do\n      array_str[n]=${array[$i - 1]}\n      let n++\n    done\n    local envStr=$(\n      IFS=\"&\"\n      echo \"${array_str[*]}\"\n    )\n    [[ ! -z $envStr ]] && export \"${env_param}=${envStr}\"\n    clear_non_sh_env\n  fi\n\n  cd $dir_scripts\n  local relative_path=\"${file_param%/*}\"\n  if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ \"/\" ]]; then\n    cd ${relative_path}\n    file_param=${file_param/$relative_path\\//}\n  fi\n\n  envParam=\"${env_param}\" numParam=\"${num_param}\" $timeoutCmd $which_program $file_param \"${script_params[@]}\"\n}\n\n## 运行其他命令\nrun_else() {\n  local file_param=\"$1\"\n\n  cd $dir_scripts\n  local relative_path=\"${file_param%/*}\"\n  if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ \"/\" ]]; then\n    cd ${relative_path}\n    file_param=${file_param/$relative_path\\//.\\/}\n  fi\n\n  shift\n\n  clear_non_sh_env\n  $timeoutCmd $which_program $file_param \"$@\"\n}\n\ncheck_file() {\n  isJsOrPythonFile=\"false\"\n  if [[ $1 == *.js ]] || [[ $1 == *.mjs ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.ts ]]; then\n    isJsOrPythonFile=\"true\"\n  fi\n  if [[ -f $file_env ]]; then\n    get_env_array\n    if [[ $isJsOrPythonFile == 'true' ]]; then\n      export PREV_NODE_OPTIONS=\"${NODE_OPTIONS:=}\"\n      export PREV_PYTHONPATH=\"${PYTHONPATH:=}\"\n      if [[ $1 == *.js ]] || [[ $1 == *.ts ]] || [[ $1 == *.mjs ]]; then\n        export NODE_OPTIONS=\"-r ${file_preload_js} ${NODE_OPTIONS}\"\n      else\n        export PYTHONPATH=\"${dir_preload}:${dir_config}:${PYTHONPATH}\"\n      fi\n    else\n      . $file_env\n    fi\n  fi\n}\n\ncheck_nounset() {\n  local output=$(set -o)\n  while read -r line; do\n    if [[ \"$line\" =~ \"nounset\" ]] && [[ \"$line\" =~ \"on\" ]]; then\n      set_u_on=\"true\"\n      set +u\n      break\n    fi\n  done <<<\"$output\"\n}\n\nmain() {\n  if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then\n    if [[ $1 == *.sh ]]; then\n      timeoutCmd=\"\"\n    fi\n\n    case $# in\n    1)\n      run_normal \"$1\"\n      ;;\n    *)\n      case $2 in\n      now)\n        run_normal \"$1\" \"$2\"\n        ;;\n      conc)\n        run_concurrent \"$1\" \"$3\" \"$*\"\n        ;;\n      desi)\n        run_designated \"$1\" \"$3\" \"$*\"\n        ;;\n      *)\n        run_else \"$@\"\n        ;;\n      esac\n      ;;\n    esac\n  elif [[ $# -eq 0 ]]; then\n    echo\n    usage\n  else\n    run_else \"$@\"\n  fi\n}\n\nhandle_task_start \"${task_shell_params[@]}\"\ncheck_file \"${task_shell_params[@]}\"\nif [[ $isJsOrPythonFile == 'false' ]]; then\n  run_task_before \"${task_shell_params[@]}\"\nfi\nset_u_on=\"false\"\ncheck_nounset\nmain \"${task_shell_params[@]}\"\nif [[ \"$set_u_on\" == 'true' ]]; then\n  set -u\nfi\nif [[ $isJsOrPythonFile == 'true' ]]; then\n  export NODE_OPTIONS=\"${PREV_NODE_OPTIONS}\"\n  export PYTHONPATH=\"${PREV_PYTHONPATH}\"\nfi\nrun_task_after \"${task_shell_params[@]}\"\nclear_env\nhandle_task_end \"${task_shell_params[@]}\"\n"
  },
  {
    "path": "shell/preload/client.js",
    "content": "const grpc = require('@grpc/grpc-js');\nconst protoLoader = require('@grpc/proto-loader');\nconst { join } = require('path');\n\nclass GrpcClient {\n  static #config = {\n    protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),\n    serverAddress: `0.0.0.0:${process.env.GRPC_PORT || '5500'}`,\n    protoOptions: {\n      keepCase: true,\n      longs: String,\n      enums: String,\n      defaults: true,\n    },\n    grpcOptions: {\n      'grpc.enable_http_proxy': 0,\n    },\n    defaultTimeout: 30000,\n  };\n\n  static #methods = [\n    'getEnvs',\n    'createEnv',\n    'updateEnv',\n    'deleteEnvs',\n    'moveEnv',\n    'disableEnvs',\n    'enableEnvs',\n    'updateEnvNames',\n    'getEnvById',\n    'systemNotify',\n    'getCronDetail',\n    'createCron',\n    'updateCron',\n    'deleteCrons',\n    'getCrons',\n    'getCronById',\n    'enableCrons',\n    'disableCrons',\n    'runCrons',\n  ];\n\n  #client;\n  #api = {};\n\n  constructor() {\n    this.#initializeClient();\n    this.#bindMethods();\n  }\n\n  #initializeClient() {\n    try {\n      const { protoPath, protoOptions, serverAddress, grpcOptions } =\n        GrpcClient.#config;\n\n      const packageDefinition = protoLoader.loadSync(protoPath, protoOptions);\n      const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api;\n\n      this.#client = new apiProto.Api(\n        serverAddress,\n        grpc.credentials.createInsecure(),\n        grpcOptions,\n      );\n    } catch (error) {\n      console.error('Failed to initialize gRPC client:', error);\n      process.exit(1);\n    }\n  }\n\n  #promisifyMethod(methodName) {\n    const capitalizedMethod =\n      methodName.charAt(0).toUpperCase() + methodName.slice(1);\n    const method = this.#client[capitalizedMethod].bind(this.#client);\n\n    return async (params = {}) => {\n      return new Promise((resolve, reject) => {\n        const metadata = new grpc.Metadata();\n        const deadline = new Date(\n          Date.now() + GrpcClient.#config.defaultTimeout,\n        );\n\n        method(params, metadata, { deadline }, (error, response) => {\n          if (error) {\n            return reject(error);\n          }\n          resolve(response);\n        });\n      });\n    };\n  }\n\n  #bindMethods() {\n    GrpcClient.#methods.forEach((method) => {\n      this.#api[method] = this.#promisifyMethod(method);\n    });\n  }\n\n  getApi() {\n    return {\n      ...this.#api,\n      close: this.close.bind(this),\n    };\n  }\n\n  close() {\n    if (this.#client) {\n      this.#client.close();\n      this.#client = null;\n    }\n  }\n}\n\nconst grpcClient = new GrpcClient();\n\nprocess.on('SIGTERM', () => {\n  grpcClient.close();\n  process.exit(0);\n});\n\nprocess.on('SIGINT', () => {\n  grpcClient.close();\n  process.exit(0);\n});\n\nprocess.on('unhandledRejection', (reason, promise) => {\n  if (reason instanceof Error) {\n    if (reason.stack) {\n      const relevantStack = reason.stack\n        .split('\\n')\n        .filter((line) => {\n          return (\n            !line.includes('node:internal') &&\n            !line.includes('node_modules/@grpc') &&\n            !line.includes('processTicksAndRejections')\n          );\n        })\n        .join('\\n');\n      console.error(relevantStack);\n    }\n  } else {\n    console.error(reason);\n  }\n});\n\nmodule.exports = grpcClient.getApi();\n"
  },
  {
    "path": "shell/preload/client.py",
    "content": "import subprocess\nimport json\nimport tempfile\nimport os\nfrom typing import Dict, List, TypedDict, Optional\nfrom functools import wraps\n\n\ndef error_handler(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        try:\n            return func(*args, **kwargs)\n        except TypeError as e:\n            if \"missing\" in str(e):\n                func_name = func.__name__\n                annotations = func.__annotations__\n                param_type = next(\n                    (t for name, t in annotations.items() if name != \"return\"), None\n                )\n                if param_type and hasattr(param_type, \"__annotations__\"):\n                    required_fields = {\n                        k: v\n                        for k, v in param_type.__annotations__.items()\n                        if not getattr(param_type, \"__total__\", True)\n                        or k in getattr(param_type, \"__required_keys__\", set())\n                    }\n                    fields_str = \", \".join(\n                        f'\"{k}\": {v.__name__}' for k, v in required_fields.items()\n                    )\n                    raise Exception(\n                        f\"{func_name}() requires a dictionary with parameters: {{{fields_str}}}\"\n                    ) from None\n            raise Exception(f\"{str(e)}\") from None\n        except Exception as e:\n            error_msg = str(e)\n            if \"Error:\" in error_msg:\n                error_msg = error_msg.split(\"Error:\")[-1].split(\"\\n\")[0].strip()\n            raise Exception(f\"{error_msg}\") from None\n\n    return wrapper\n\n\nclass EnvItem(TypedDict, total=False):\n    id: Optional[int]\n    name: Optional[str]\n    value: Optional[str]\n    remarks: Optional[str]\n    status: Optional[int]\n    position: Optional[int]\n\n\nclass GetEnvsParams(TypedDict, total=False):\n    searchValue: str\n\n\nclass CreateEnvParams(TypedDict):\n    envs: List[EnvItem]\n\n\nclass UpdateEnvParams(TypedDict):\n    env: EnvItem\n\n\nclass DeleteEnvsParams(TypedDict):\n    ids: List[int]\n\n\nclass MoveEnvParams(TypedDict):\n    id: int\n    fromIndex: int\n    toIndex: int\n\n\nclass DisableEnvsParams(TypedDict):\n    ids: List[int]\n\n\nclass EnableEnvsParams(TypedDict):\n    ids: List[int]\n\n\nclass UpdateEnvNamesParams(TypedDict):\n    ids: List[int]\n    name: str\n\n\nclass GetEnvByIdParams(TypedDict):\n    id: int\n\n\nclass SystemNotifyParams(TypedDict):\n    title: str\n    content: str\n\n\nclass EnvsResponse(TypedDict):\n    code: int\n    data: List[EnvItem]\n    message: Optional[str]\n\n\nclass EnvResponse(TypedDict):\n    code: int\n    data: EnvItem\n    message: Optional[str]\n\n\nclass Response(TypedDict):\n    code: int\n    message: Optional[str]\n\n\nclass ExtraScheduleItem(TypedDict, total=False):\n    schedule: Optional[str]\n\n\nclass CronItem(TypedDict, total=False):\n    id: Optional[int]\n    command: Optional[str]\n    schedule: Optional[str]\n    name: Optional[str]\n    labels: List[str]\n    sub_id: Optional[int]\n    extra_schedules: List[ExtraScheduleItem]\n    task_before: Optional[str]\n    task_after: Optional[str]\n    status: Optional[int]\n    log_path: Optional[str]\n    pid: Optional[int]\n    last_running_time: Optional[int]\n    last_execution_time: Optional[int]\n\n\nclass CreateCronParams(TypedDict):\n    command: str\n    schedule: str\n    name: Optional[str]\n    labels: List[str]\n    sub_id: Optional[int]\n    extra_schedules: List[ExtraScheduleItem]\n    task_before: Optional[str]\n    task_after: Optional[str]\n\n\nclass UpdateCronParams(TypedDict):\n    id: int\n    command: str\n    schedule: str\n    name: Optional[str]\n    labels: List[str]\n    sub_id: Optional[int]\n    extra_schedules: List[ExtraScheduleItem]\n    task_before: Optional[str]\n    task_after: Optional[str]\n\n\nclass DeleteCronsParams(TypedDict):\n    ids: List[int]\n\n\nclass CronDetailParams(TypedDict):\n    log_path: str\n\n\nclass CronsResponse(TypedDict):\n    code: int\n    data: List[CronItem]\n    message: Optional[str]\n\n\nclass CronResponse(TypedDict):\n    code: int\n    data: CronItem\n    message: Optional[str]\n\n\nclass Client:\n    def __init__(self):\n        self.temp_dir = tempfile.mkdtemp(prefix=\"node_client_\")\n        self.temp_script = os.path.join(self.temp_dir, \"temp_script.js\")\n\n    def __del__(self):\n        try:\n            if os.path.exists(self.temp_script):\n                os.remove(self.temp_script)\n            os.rmdir(self.temp_dir)\n        except Exception:\n            pass\n\n    @error_handler\n    def _execute_node(self, method: str, params: Dict = None) -> Dict:\n        node_code = f\"\"\"\n        const api = require('{os.getenv(\"QL_DIR\")}/shell/preload/client.js');\n        \n        (async () => {{\n            try {{\n                const result = await api.{method}({json.dumps(params) if params else ''});\n                console.log(JSON.stringify(result));\n            }} catch (error) {{\n                console.error(JSON.stringify({{\n                    error: error.message,\n                    stack: error.stack,\n                    name: error.name\n                }}));\n                process.exit(1);\n            }}\n        }})();\n        \"\"\"\n\n        with open(self.temp_script, \"w\", encoding=\"utf-8\") as f:\n            f.write(node_code)\n\n        result = subprocess.run(\n            [\"node\", self.temp_script],\n            capture_output=True,\n            text=True,\n            timeout=30,\n        )\n\n        if result.returncode != 0:\n            error_data = json.loads(result.stderr)\n            raise Exception(\n                f\"{error_data.get('name', 'Error')}: {error_data.get('stack')}\"\n            )\n\n        return json.loads(result.stdout)\n\n    @error_handler\n    def getEnvs(self, params: GetEnvsParams = None) -> EnvsResponse:\n        return self._execute_node(\"getEnvs\", params)\n\n    @error_handler\n    def createEnv(self, data: CreateEnvParams) -> EnvsResponse:\n        return self._execute_node(\"createEnv\", data)\n\n    @error_handler\n    def updateEnv(self, data: UpdateEnvParams) -> EnvResponse:\n        return self._execute_node(\"updateEnv\", data)\n\n    @error_handler\n    def deleteEnvs(self, data: DeleteEnvsParams) -> Response:\n        return self._execute_node(\"deleteEnvs\", data)\n\n    @error_handler\n    def moveEnv(self, data: MoveEnvParams) -> EnvResponse:\n        return self._execute_node(\"moveEnv\", data)\n\n    @error_handler\n    def disableEnvs(self, data: DisableEnvsParams) -> Response:\n        return self._execute_node(\"disableEnvs\", data)\n\n    @error_handler\n    def enableEnvs(self, data: EnableEnvsParams) -> Response:\n        return self._execute_node(\"enableEnvs\", data)\n\n    @error_handler\n    def updateEnvNames(self, data: UpdateEnvNamesParams) -> Response:\n        return self._execute_node(\"updateEnvNames\", data)\n\n    @error_handler\n    def getEnvById(self, data: GetEnvByIdParams) -> EnvResponse:\n        return self._execute_node(\"getEnvById\", data)\n\n    @error_handler\n    def systemNotify(self, data: SystemNotifyParams) -> Response:\n        return self._execute_node(\"systemNotify\", data)\n\n    @error_handler\n    def getCronDetail(self, data: CronDetailParams) -> CronResponse:\n        return self._execute_node(\"getCronDetail\", data)\n\n    @error_handler\n    def createCron(self, data: CreateCronParams) -> CronResponse:\n        return self._execute_node(\"createCron\", data)\n\n    @error_handler\n    def updateCron(self, data: UpdateCronParams) -> CronResponse:\n        return self._execute_node(\"updateCron\", data)\n\n    @error_handler\n    def deleteCrons(self, data: DeleteCronsParams) -> Response:\n        return self._execute_node(\"deleteCrons\", data)\n"
  },
  {
    "path": "shell/preload/sitecustomize.js",
    "content": "const { execSync } = require('child_process');\nconst client = require('./client.js');\nrequire(`./env.js`);\n\nfunction expandRange(rangeStr, max) {\n  const tempRangeStr = rangeStr\n    .trim()\n    .replace(/-max/g, `-${max}`)\n    .replace(/max-/g, `${max}-`);\n\n  return tempRangeStr.split(' ').flatMap((part) => {\n    const rangeMatch = part.match(/^(\\d+)([-~_])(\\d+)$/);\n    if (rangeMatch) {\n      const [, start, , end] = rangeMatch.map(Number);\n      const step = start < end ? 1 : -1;\n      return Array.from(\n        { length: Math.abs(end - start) + 1 },\n        (_, i) => start + i * step,\n      );\n    }\n    return Number(part);\n  });\n}\n\nfunction run() {\n  const {\n    envParam,\n    numParam,\n    file_task_before,\n    file_task_before_js,\n    dir_scripts,\n    task_before,\n    PREV_NODE_OPTIONS,\n  } = process.env;\n\n  try {\n    process.env.NODE_OPTIONS = PREV_NODE_OPTIONS;\n\n    const splitStr = '__sitecustomize__';\n    const fileName = process.argv[1].replace(`${dir_scripts}/`, '');\n    const tempFile = `/tmp/env_${process.pid}.json`;\n\n    const commands = [\n      `source ${file_task_before} ${fileName}`,\n      task_before ? `eval '${task_before.replace(/'/g, \"'\\\\''\")}'` : null,\n      `echo -e '${splitStr}'`,\n      `node -e \"require('fs').writeFileSync('${tempFile}', JSON.stringify(process.env))\"`,\n    ].filter(Boolean);\n\n    if (task_before) {\n      console.log('执行前置命令\\n');\n    }\n\n    const res = execSync(commands.join(' && '), {\n      encoding: 'utf-8',\n      maxBuffer: 50 * 1024 * 1024,\n      shell: '/bin/bash',\n    });\n\n    const [output] = res.split(splitStr);\n\n    try {\n      const envStr = require('fs').readFileSync(tempFile, 'utf-8');\n      const newEnvObject = JSON.parse(envStr);\n      if (typeof newEnvObject === 'object' && newEnvObject !== null) {\n        for (const key in newEnvObject) {\n          if (Object.prototype.hasOwnProperty.call(newEnvObject, key)) {\n            process.env[key] = newEnvObject[key];\n          }\n        }\n      }\n      require('fs').unlinkSync(tempFile);\n    } catch (jsonError) {\n      console.log(\n        '\\ue926 Failed to parse environment variables:',\n        jsonError.message,\n      );\n      try {\n        require('fs').unlinkSync(tempFile);\n      } catch (e) {}\n    }\n\n    if (output) {\n      console.log(output);\n    }\n    if (task_before) {\n      console.log('执行前置命令结束\\n');\n    }\n  } catch (error) {\n    if (!error.message.includes('spawnSync /bin/bash E2BIG')) {\n      console.log(`\\ue926 run task before error: `, error);\n    } else {\n      // environment variable is too large\n    }\n    if (task_before) {\n      console.log('执行前置命令结束\\n');\n    }\n  }\n\n  require(file_task_before_js);\n\n  if (envParam && numParam) {\n    const array = (process.env[envParam] || '').split('&');\n    const runArr = expandRange(numParam, array.length);\n    const arrayRun = runArr.map((i) => array[i - 1]);\n    const envStr = arrayRun.join('&');\n    process.env[envParam] = envStr;\n  }\n}\n\ntry {\n  if (!process.argv[1]) {\n    return;\n  }\n\n  process.on('SIGTERM', (code) => {\n    process.exit(15);\n  });\n\n  run();\n\n  const { sendNotify } = require('./__ql_notify__.js');\n  global.QLAPI = {\n    notify: sendNotify,\n    ...client,\n  };\n} catch (error) {\n  console.log(`run builtin code error: `, error, '\\n');\n}\n"
  },
  {
    "path": "shell/preload/sitecustomize.py",
    "content": "import os\nimport re\nimport subprocess\nimport json\nimport builtins\nimport sys\nimport env\nimport signal\nfrom client import Client\n\n\ndef try_parse_int(value):\n    try:\n        return int(value)\n    except ValueError:\n        return None\n\n\ndef expand_range(range_str, max_value):\n    temp_range_str = (\n        range_str.strip()\n        .replace(\"-max\", f\"-{max_value}\")\n        .replace(\"max-\", f\"{max_value}-\")\n    )\n\n    result = []\n    for part in temp_range_str.split(\" \"):\n        range_match = re.match(r\"^(\\d+)([-~_])(\\d+)$\", part)\n        if range_match:\n            start, _, end = map(try_parse_int, range_match.groups())\n            step = 1 if start < end else -1\n            result.extend(range(start, end + step, step))\n        else:\n            result.append(int(part))\n\n    return result\n\n\ndef run():\n    try:\n        prev_pythonpath = os.getenv(\"PREV_PYTHONPATH\", \"\")\n        os.environ[\"PYTHONPATH\"] = prev_pythonpath\n\n        split_str = \"__sitecustomize__\"\n        file_name = sys.argv[0].replace(f\"{os.getenv('dir_scripts')}/\", \"\")\n        \n        # 创建临时文件路径\n        temp_file = f\"/tmp/env_{os.getpid()}.json\"\n        \n        # 构建命令数组\n        commands = [\n            f'source {os.getenv(\"file_task_before\")} {file_name}'\n        ]\n        \n        task_before = os.getenv(\"task_before\")\n        if task_before:\n            escaped_task_before = task_before.replace('\"', '\\\\\"').replace(\"$\", \"\\\\$\")\n            commands.append(f\"eval '{escaped_task_before}'\")\n            print(\"执行前置命令\\n\")\n            \n        commands.append(f\"echo -e '{split_str}'\")\n        \n        # 修改 Python 命令，使用单行并正确处理引号\n        python_cmd = f\"python3 -c 'import os,json; f=open(\\\\\\\"{temp_file}\\\\\\\",\\\\\\\"w\\\\\\\"); json.dump(dict(os.environ),f); f.close()'\"\n        commands.append(python_cmd)\n        \n        command = \" && \".join(cmd for cmd in commands if cmd)\n        command = f'bash -c \"{command}\"'\n\n        res = subprocess.check_output(command, shell=True, encoding=\"utf-8\")\n        output = res.split(split_str)[0]\n\n        try:\n            with open(temp_file, 'r') as f:\n                env_json = json.loads(f.read())\n\n            for key, value in env_json.items():\n                os.environ[key] = value\n\n            os.unlink(temp_file)\n        except Exception as json_error:\n            print(f\"\\ue926 Failed to parse environment variables: {json_error}\")\n            try:\n                os.unlink(temp_file)\n            except:\n                pass\n\n        if len(output) > 0:\n            print(output)\n        if task_before:\n            print(\"执行前置命令结束\\n\")\n\n    except subprocess.CalledProcessError as error:\n        print(f\"\\ue926 run task before error: {error}\")\n        if task_before:\n            print(\"执行前置命令结束\\n\")\n    except OSError as error:\n        error_message = str(error)\n        if \"Argument list too long\" not in error_message:\n            print(f\"\\ue926 run task before error: {error}\")\n        # else:\n            # environment variable is too large\n        if task_before:\n            print(\"执行前置命令结束\\n\")\n    except Exception as error:\n        print(f\"\\ue926 run task before error: {error}\")\n        if task_before:\n            print(\"执行前置命令结束\\n\")\n\n    import task_before\n\n    env_param = os.getenv(\"envParam\")\n    num_param = os.getenv(\"numParam\")\n\n    if env_param and num_param:\n        array = (os.getenv(env_param) or \"\").split(\"&\")\n        run_arr = expand_range(num_param, len(array))\n        array_run = [array[i - 1] for i in run_arr if i - 1 < len(array) and i > 0]\n        env_str = \"&\".join(array_run)\n        os.environ[env_param] = env_str\n\n\ndef handle_sigterm(signum, frame):\n    sys.exit(15)\n\n\ntry:\n    signal.signal(signal.SIGTERM, handle_sigterm)\n\n    run()\n\n    from __ql_notify__ import send\n\n    class BaseApi(Client):\n        def notify(self, *args, **kwargs):\n            return send(*args, **kwargs)\n\n    QLAPI = BaseApi()\n    builtins.QLAPI = QLAPI\nexcept Exception as error:\n    print(f\"run builtin code error: {error}\\n\")\n"
  },
  {
    "path": "shell/pub.sh",
    "content": "#!/usr/bin/env bash\necho -e \"开始发布\"\n\necho -e \"切换master分支\"\ngit branch -D master\ngit checkout -b master\ngit push --set-upstream origin master -f\n\necho -e \"更新cdn文件\"\nts-node-transpile-only sample/tool.ts\n\nstring=$(cat version.yaml | grep \"version\" | egrep \"[^ ]*\" -o | egrep \"\\d\\.*\")\nversion=\"v$string\"\necho -e \"当前版本$version\"\n\necho -e \"删除已经存在的本地tag\"\ngit tag -d \"$version\" &>/dev/null\n\necho -e \"删除已经存在的远程tag\"\ngit push origin :refs/tags/$version &>/dev/null\n\necho -e \"创建新tag\"\ngit tag -a \"$version\" -m \"release $version\"\n\necho -e \"提交tag\"\ngit push --tags\n\necho -e \"完成发布\"\n"
  },
  {
    "path": "shell/rmlog.sh",
    "content": "#!/usr/bin/env bash\n\ndays=$1\n\nremove_js_log() {\n  local log_full_path_list=$(find $dir_log -name \"*.log\")\n  local diff_time\n  for log in $log_full_path_list; do\n    local log_date=$(echo $log | awk -F \"/\" '{print $NF}' | cut -c1-10)\n    if ! [[ $log_date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then\n      if [[ $is_macos -eq 1 ]]; then\n        log_date=$(stat -f %Sm -t \"%Y-%m-%d\" \"$log\")\n      else\n        log_date=$(stat -c %y \"$log\" | cut -d ' ' -f 1)\n      fi\n    fi\n    if [[ $is_macos -eq 1 ]]; then\n      diff_time=$(($(date +%s) - $(date -j -f \"%Y-%m-%d\" \"$log_date\" +%s)))\n    else\n      diff_time=$(($(date +%s) - $(date +%s -d \"$log_date\")))\n    fi\n    if [[ $diff_time -gt $((${days} * 86400)) ]]; then\n      local log_path=$(echo \"$log\" | sed \"s,${dir_log}/,,g\")\n      local result=$(find_cron_api \"log_path=$log_path\")\n      echo -e \"查询文件 $log_path\"\n      if [[ -z $result ]]; then\n        echo -e \"删除中~\"\n        rm -vf $log\n      else\n        echo -e \"正在被 $result 使用，跳过~\"\n      fi\n    fi\n  done\n}\n\nremove_empty_dir() {\n  cd $dir_log\n  for dir in $(ls); do\n    if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then\n      rm -rf $dir\n    fi\n  done\n}\n\nif [[ ${days} ]]; then\n  echo -e \"查找旧日志文件中...\\n\"\n  remove_js_log\n  remove_empty_dir\n  echo -e \"删除旧日志执行完毕\\n\"\nfi\n"
  },
  {
    "path": "shell/share.sh",
    "content": "#!/usr/bin/env bash\n\n## 目录\nexport dir_root=$QL_DIR\nexport dir_tmp=$dir_root/.tmp\nexport dir_data=$dir_root/data\n\nif [[ ${QL_DATA_DIR:=} ]]; then\n  export dir_data=\"${QL_DATA_DIR%/}\"\nfi\n\nexport dir_shell=$dir_root/shell\nexport dir_preload=$dir_shell/preload\nexport dir_sample=$dir_root/sample\nexport dir_static=$dir_root/static\nexport dir_config=$dir_data/config\nexport dir_scripts=$dir_data/scripts\nexport dir_repo=$dir_data/repo\nexport dir_raw=$dir_data/raw\nexport dir_log=$dir_data/log\nexport dir_db=$dir_data/db\nexport dir_dep=$dir_data/deps\nexport dir_list_tmp=$dir_log/.tmp\nexport dir_update_log=$dir_log/update\nexport ql_static_repo=$dir_repo/static\n\n## 文件\nexport file_config_sample=$dir_sample/config.sample.sh\nexport file_env=$dir_preload/env.sh\nexport file_preload_js=$dir_preload/sitecustomize.js\nexport file_sharecode=$dir_config/sharecode.sh\nexport file_config_user=$dir_config/config.sh\nexport file_auth_sample=$dir_sample/auth.sample.json\nexport file_auth_user=$dir_config/auth.json\nexport file_auth_token=$dir_config/token.json\nexport file_extra_shell=$dir_config/extra.sh\nexport file_task_before=$dir_config/task_before.sh\nexport file_task_before_js=$dir_config/task_before.js\nexport file_task_before_py=$dir_config/task_before.py\nexport file_task_after=$dir_config/task_after.sh\nexport file_task_sample=$dir_sample/task.sample.sh\nexport file_extra_sample=$dir_sample/extra.sample.sh\nexport file_notify_js_sample=$dir_sample/notify.js\nexport file_notify_py_sample=$dir_sample/notify.py\nexport file_test_js_sample=$dir_sample/ql_sample.js\nexport file_test_py_sample=$dir_sample/ql_sample.py\nexport file_notify_py=$dir_scripts/notify.py\nexport file_notify_js=$dir_scripts/sendNotify.js\nexport file_test_js=$dir_scripts/ql_sample.js\nexport file_test_py=$dir_scripts/ql_sample.py\nexport dep_notify_py=$dir_dep/notify.py\nexport dep_notify_js=$dir_dep/sendNotify.js\n\n## 清单文件\nlist_crontab_user=$dir_config/crontab.list\nlist_crontab_sample=$dir_sample/crontab.sample.list\nlist_own_scripts=$dir_list_tmp/own_scripts.list\nlist_own_user=$dir_list_tmp/own_user.list\nlist_own_add=$dir_list_tmp/own_add.list\nlist_own_drop=$dir_list_tmp/own_drop.list\n\nlink_name=(\n  task\n  ql\n)\n\ninit_env() {\n  local pnpm_global_path=$(pnpm root -g 2>/dev/null)\n  export NODE_PATH=\"/usr/local/bin:/usr/local/lib/node_modules${pnpm_global_path:+:${pnpm_global_path}}\"\n\n  # 如果存在 pnpm 全局路径，创建软链接\n  if [[ -n \"$pnpm_global_path\" ]]; then\n    # 确保目标目录存在\n    mkdir -p \"${dir_root}/node_modules\"\n    # 链接全局模块到项目的 node_modules\n    ln -sf \"${pnpm_global_path}/\"* \"${dir_root}/node_modules/\" 2>/dev/null || true\n  fi\n\n  export PYTHONUNBUFFERED=1\n}\n\nload_ql_envs() {\n  ql_base_url=${QlBaseUrl:-\"/\"}\n  ql_port=${QlPort:-\"5700\"}\n  ql_grpc_port=${QlGrpcPort:-\"5500\"}\n  current_branch=${QL_BRANCH:-\"\"}\n}\n\nimport_config() {\n  [[ -f $file_config_user ]] && . $file_config_user\n\n  load_ql_envs\n  command_timeout_time=${CommandTimeoutTime:-\"\"}\n  file_extensions=${RepoFileExtensions:-\"js py\"}\n  proxy_url=${ProxyUrl:-\"\"}\n\n  if [[ -n \"${DefaultCronRule}\" ]]; then\n    default_cron=\"${DefaultCronRule}\"\n  else\n    default_cron=\"$(random_range 0 59) $(random_range 0 23) * * *\"\n  fi\n}\n\nset_proxy() {\n  local proxy=\"$1\"\n  if [[ $proxy ]]; then\n    proxy_url=\"$proxy\"\n  fi\n  if [[ $proxy_url ]]; then\n    export http_proxy=\"${proxy_url}\"\n    export https_proxy=\"${proxy_url}\"\n  fi\n}\n\nunset_proxy() {\n  unset http_proxy\n  unset https_proxy\n}\n\nmake_dir() {\n  local dir=$1\n  if [[ ! -d $dir ]]; then\n    mkdir -p $dir\n  fi\n}\n\ndetect_termux() {\n  if [[ $PATH == *com.termux* ]]; then\n    is_termux=1\n  else\n    is_termux=0\n  fi\n}\n\ndetect_macos() {\n  [[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0\n}\n\ngen_random_num() {\n  local divi=$1\n  echo $((${RANDOM} % $divi))\n}\n\ndefine_cmd() {\n  local cmd_prefix cmd_suffix\n  if type task &>/dev/null; then\n    cmd_suffix=\"\"\n    if [[ -f \"$dir_shell/task.sh\" ]]; then\n      cmd_prefix=\"\"\n    else\n      cmd_prefix=\"bash \"\n    fi\n  else\n    cmd_suffix=\".sh\"\n    if [[ -f \"$dir_shell/task.sh\" ]]; then\n      cmd_prefix=\"$dir_shell/\"\n    else\n      cmd_prefix=\"bash $dir_shell/\"\n    fi\n  fi\n  for ((i = 0; i < ${#link_name[*]}; i++)); do\n    export cmd_${link_name[i]}=\"${cmd_prefix}${link_name[i]}${cmd_suffix}\"\n  done\n}\n\nfix_config() {\n  make_dir $dir_tmp\n  make_dir $dir_static\n  make_dir $dir_data\n  make_dir $dir_config\n  make_dir $dir_log\n  make_dir $dir_db\n  make_dir $dir_scripts\n  make_dir $dir_list_tmp\n  make_dir $dir_repo\n  make_dir $dir_raw\n  make_dir $dir_update_log\n  make_dir $dir_dep\n\n  if [[ ! -s $file_config_user ]]; then\n    cp -f $file_config_sample $file_config_user\n  fi\n\n  if [[ ! -f $file_task_before ]]; then\n    cp -f $file_task_sample $file_task_before\n  fi\n\n  if [[ ! -f $file_task_after ]]; then\n    cp -f $file_task_sample $file_task_after\n  fi\n\n  if [[ ! -f $file_extra_shell ]]; then\n    cp -f $file_extra_sample $file_extra_shell\n  fi\n\n  if [[ ! -s $file_notify_py ]]; then\n    cp -f $file_notify_py_sample $file_notify_py\n  fi\n\n  if [[ ! -s $file_notify_js ]]; then\n    cp -f $file_notify_js_sample $file_notify_js\n  fi\n\n  if [[ ! -s $file_test_js ]]; then\n    cp -f $file_test_js_sample $file_test_js\n  fi\n\n  if [[ ! -s $file_test_py ]]; then\n    cp -f $file_test_py_sample $file_test_py\n  fi\n\n  if [[ ! -s $dep_notify_js ]]; then\n    cp -f $file_notify_js_sample $dep_notify_js\n  fi\n\n  if [[ ! -s $dep_notify_py ]]; then\n    cp -f $file_notify_py_sample $dep_notify_py\n  fi\n\n}\n\nnpm_install_sub() {\n  if [ $is_termux -eq 1 ]; then\n    npm install --production --no-bin-links\n  elif ! type pnpm &>/dev/null; then\n    npm install --production\n  else\n    pnpm install --loglevel error --production\n  fi\n  exit_status=$?\n}\n\nnpm_install_2() {\n  local dir_current=$(pwd)\n  local dir_work=$1\n\n  cd $dir_work\n  echo -e \"安装 $dir_work 依赖包...\\n\"\n  npm_install_sub\n  cd $dir_current\n}\n\ndiff_and_copy() {\n  local copy_source=$1\n  local copy_to=$2\n  if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then\n    cp -f $copy_source $copy_to\n  fi\n}\n\ngit_clone_scripts() {\n  local url=\"$1\"\n  local dir=\"$2\"\n  local branch=\"$3\"\n  local proxy=\"$4\"\n  [[ $branch ]] && local part_cmd=\"-b $branch \"\n  echo -e \"开始拉取仓库 ${uniq_path} 到 $dir\\n\"\n\n  set_proxy \"$proxy\"\n\n  git clone -q --depth=1 $part_cmd $url $dir\n  exit_status=$?\n\n  unset_proxy\n}\n\nrandom_range() {\n  local beg=$1\n  local end=$2\n  echo $((RANDOM % ($end - $beg) + $beg))\n}\n\ndelete_pm2() {\n  cd $dir_root\n  # Try to delete PM2 processes, but don't fail if PM2 is not available\n  pm2 delete ecosystem.config.js 2>/dev/null || true\n  # Also try to kill any directly spawned node processes\n  pkill -f \"node.*static/build/app.js\" 2>/dev/null || true\n}\n\nreload_pm2() {\n  cd $dir_root\n  restore_env_vars\n  \n  # Try to start PM2, but handle failures gracefully\n  if pm2 flush &>/dev/null && pm2 startOrGracefulReload ecosystem.config.js --update-env; then\n    return 0\n  else\n    local exit_code=$?\n    echo \"警告: PM2 启动失败 (退出码: $exit_code)，可能是由于硬件不兼容\"\n    echo \"正在尝试直接使用 Node.js 启动服务...\"\n    \n    # Kill any existing node processes for qinglong\n    pkill -f \"node.*static/build/app.js\" 2>/dev/null || true\n    \n    # Start node directly in the background\n    nohup node static/build/app.js > $dir_log/qinglong.log 2>&1 &\n    local node_pid=$!\n    \n    echo \"已使用 Node.js 直接启动服务 (PID: $node_pid)\"\n    echo \"注意: 使用此模式时，部分 PM2 管理功能将不可用\"\n    return 0\n  fi\n}\n\ndiff_time() {\n  local format=\"$1\"\n  local begin_time=\"$2\"\n  local end_time=\"$3\"\n\n  if [[ $is_macos -eq 1 ]]; then\n    diff_time=$(($(date -j -f \"$format\" \"$end_time\" +%s) - $(date -j -f \"$format\" \"$begin_time\" +%s)))\n  else\n    diff_time=$(($(date +%s -d \"$end_time\") - $(date +%s -d \"$begin_time\")))\n  fi\n  echo \"$diff_time\"\n}\n\nformat_time() {\n  local format=\"$1\"\n  local time=\"$2\"\n\n  if [[ $is_macos -eq 1 ]]; then\n    echo $(date -j -f \"$format\" \"$time\" \"+%Y-%m-%d %H:%M:%S\")\n  else\n    echo $(date -d \"$time\" \"+%Y-%m-%d %H:%M:%S\")\n  fi\n}\n\nformat_log_time() {\n  local format=\"$1\"\n  local time=\"$2\"\n\n  if [[ $is_macos -eq 1 ]]; then\n    echo $(python3 -c 'from datetime import datetime; print(datetime.now().strftime(\"%Y-%m-%d-%H-%M-%S-%f\")[:-3])')\n  else\n    echo $(date -d \"$time\" \"+%Y-%m-%d-%H-%M-%S-%3N\")\n  fi\n}\n\nformat_timestamp() {\n  local format=\"$1\"\n  local time=\"$2\"\n\n  if [[ $is_macos -eq 1 ]]; then\n    echo $(date -j -f \"$format\" \"$time\" \"+%s\")\n  else\n    echo $(date -d \"$time\" \"+%s\")\n  fi\n}\n\nget_env_array() {\n  exported_variables=()\n  while IFS= read -r line; do\n    exported_variables+=(\"$line\")\n  done < <(grep '^export ' $file_env | awk '{print $2}' | cut -d= -f1)\n}\n\nclear_env() {\n  for var in \"${exported_variables[@]}\"; do\n    unset \"$var\"\n  done\n}\n\nhandle_task_start() {\n  local error_message=\"\"\n  if [[ $ID ]]; then\n    local error=$(update_cron \"\\\"$ID\\\"\" \"0\" \"$$\" \"$log_path\" \"$begin_timestamp\")\n    if [[ $error ]]; then\n      error_message=\", 任务状态更新失败(${error})\"\n    fi\n  fi\n  echo -e \"## 开始执行... ${begin_time}${error_message}\\n\"\n}\n\nrun_task_before() {\n  . $file_task_before \"$@\"\n\n  if [[ ${task_before:=} ]]; then\n    echo -e \"执行前置命令\\n\"\n    eval \"${task_before%;}\"\n    echo -e \"\\n执行前置命令结束\\n\"\n  fi\n}\n\nrun_task_after() {\n  . $file_task_after \"$@\"\n\n  if [[ ${task_after:=} ]]; then\n    echo -e \"\\n执行后置命令\\n\"\n    eval \"${task_after%;}\"\n    echo -e \"\\n执行后置命令结束\"\n  fi\n}\n\nhandle_task_end() {\n  local etime=$(date \"+$time_format\")\n  local end_time=$(format_time \"$time_format\" \"$etime\")\n  local end_timestamp=$(format_timestamp \"$time_format\" \"$etime\")\n  local diff_time=$(($end_timestamp - $begin_timestamp))\n  local suffix=\"\"\n  [[ \"${MANUAL:=}\" == \"true\" ]] && suffix=\"(手动停止)\"\n\n  [[ \"$diff_time\" == 0 ]] && diff_time=1\n\n  if [[ $ID ]]; then\n    local error=$(update_cron \"\\\"$ID\\\"\" \"1\" \"$$\" \"$log_path\" \"$begin_timestamp\" \"$diff_time\")\n    if [[ $error ]]; then\n      error_message=\", 任务状态更新失败(${error})\"\n    fi\n  fi\n  echo -e \"\\n## 执行结束$suffix... $end_time  耗时 $diff_time 秒${error_message:=}　　　　　\"\n}\n\ninit_env\ndetect_termux\ndetect_macos\ndefine_cmd\n"
  },
  {
    "path": "shell/task.sh",
    "content": "#!/usr/bin/env bash\n\ndir_shell=$QL_DIR/shell\n. $dir_shell/share.sh\n. $dir_shell/api.sh\n\ntrap \"single_hanle\" 2 3 20 15 14 19 1\nsingle_hanle() {\n  eval MANUAL=true handle_task_end \"$@\" \"$cmd\"\n  exit 1\n}\n\ndefine_program() {\n  local file_param=$1\n  if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then\n    which_program=\"node\"\n  elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then\n    which_program=\"python3\"\n  elif [[ $file_param == *.sh ]]; then\n    which_program=\".\"\n  elif [[ $file_param == *.ts ]]; then\n    which_program=\"ts-node-transpile-only\"\n  else\n    which_program=\"\"\n  fi\n}\n\nhandle_log_path() {\n  local file_param=$1\n\n  if [[ -z $file_param ]]; then\n    file_param=\"task\"\n  fi\n\n  if [[ -z ${ID:=} ]]; then\n    ID=$(cat $list_crontab_user | grep -E \"$cmd_task.* $file_param\" | perl -pe \"s|.*ID=(.*) $cmd_task.* $file_param\\.*|\\1|\" | head -1 | awk -F \" \" '{print $1}')\n  fi\n  local suffix=\"\"\n  if [[ ! -z $ID ]]; then\n    if [[ \"$ID\" -gt 0 ]] 2>/dev/null; then\n      suffix=\"_${ID}\"\n    else\n      ID=\"\"\n    fi\n  fi\n\n  time=$(date \"+$mtime_format\")\n  log_time=$(format_log_time \"$mtime_format\" \"$time\")\n  if [[ -z $log_name ]]; then\n    log_dir_tmp=\"${file_param##*/}\"\n    if [[ $file_param =~ \"/\" ]]; then\n      if [[ $file_param == /* ]]; then\n        log_dir_tmp_path=\"${file_param:1}\"\n      else\n        log_dir_tmp_path=\"${file_param}\"\n      fi\n    fi\n    log_dir_tmp_path=\"${log_dir_tmp_path%/*}\"\n    log_dir_tmp_path=\"${log_dir_tmp_path##*/}\"\n    [[ $log_dir_tmp_path ]] && log_dir_tmp=\"${log_dir_tmp_path}_${log_dir_tmp}\"\n    log_dir=\"${log_dir_tmp%.*}${suffix}\"\n  else\n    log_dir=\"$log_name\"\n  fi\n  log_path=\"$log_dir/$log_time.log\"\n\n  if [[ ${real_log_path:=} ]]; then\n    log_path=\"$real_log_path\"\n  fi\n\n  cmd=\"2>&1 | tee -a $dir_log/$log_path\"\n  make_dir \"$dir_log/$log_dir\"\n  if [[ \"${no_tee:=}\" == \"true\" ]]; then\n    cmd=\">> $dir_log/$log_path 2>&1\"\n  fi\n\n  if [[ \"${real_time:=}\" == \"true\" ]]; then\n    cmd=\"\"\n  fi\n\n  if [[ \"${log_dir:=}\" == \"/dev/null\" ]]; then\n    cmd=\">> /dev/null\"\n    log_path=\"/dev/null\"\n  fi\n}\n\nformat_params() {\n  time_format=\"%Y-%m-%d %H:%M:%S\"\n  if [[ $is_macos -eq 1 ]]; then\n    mtime_format=$time_format\n  else\n    mtime_format=\"%Y-%m-%d %H:%M:%S.%3N\"\n  fi\n  timeoutCmd=\"\"\n  if [[ $command_timeout_time ]]; then\n    if type timeout &>/dev/null; then\n      timeoutCmd=\"timeout --foreground -s 2 -k 10s $command_timeout_time \"\n    fi\n  fi\n  # params=$(echo \"$@\" | sed -E 's/([^ ])&([^ ])/\\1\\\\\\&\\2/g')\n\n  # 分割 task 内置参数和脚本参数\n  task_shell_params=()\n  script_params=()\n  found_double_dash=false\n\n  for arg in \"$@\"; do\n    if $found_double_dash; then\n      script_params+=(\"$arg\")\n    elif [ \"$arg\" == \"--\" ]; then\n      found_double_dash=true\n    else\n      task_shell_params+=(\"$arg\")\n    fi\n  done\n}\n\ninit_begin_time() {\n  begin_time=$(format_time \"$time_format\" \"$time\")\n  begin_timestamp=$(format_timestamp \"$time_format\" \"$time\")\n}\n\nimport_config \"$@\"\nwhile getopts \":lm:\" opt; do\n  case $opt in\n  l)\n    show_log=\"true\"\n    ;;\n  m)\n    max_time=\"$OPTARG\"\n    ;;\n  esac\ndone\n[[ ${show_log:=} ]] && shift $(($OPTIND - 1))\nif [[ ${max_time:=} ]]; then\n  shift $(($OPTIND - 1))\n  command_timeout_time=\"$max_time\"\nfi\n\nformat_params \"$@\"\ndefine_program \"${task_shell_params[@]}\"\nhandle_log_path \"${task_shell_params[@]}\"\ninit_begin_time\n\neval . $dir_shell/otask.sh \"$cmd\"\nexit 0\n"
  },
  {
    "path": "shell/update.sh",
    "content": "#!/usr/bin/env bash\n\ndir_shell=$QL_DIR/shell\n. $dir_shell/share.sh\n. $dir_shell/api.sh\nload_ql_envs\n. $dir_shell/env.sh\n\nsend_mark=$dir_shell/send_mark\n\n## 检测cron的差异，$1：脚本清单文件路径，$2：cron任务清单文件路径，$3：增加任务清单文件路径，$4：删除任务清单文件路径\ndiff_cron() {\n  local list_scripts=\"$1\"\n  local list_task=\"$2\"\n  local list_add=\"$3\"\n  local list_drop=\"$4\"\n  if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then\n    grep -vwf $list_task $list_scripts >$list_add\n    grep -vwf $list_scripts $list_task >$list_drop\n  fi\n\n  if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then\n    cp -f $list_scripts $list_add\n  fi\n\n  if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then\n    cp -f $list_task $list_drop\n  fi\n}\n\n## 输出是否有新的或失效的定时任务，$1：新的或失效的任务清单文件路径，$2：新/失效\noutput_list_add_drop() {\n  local list=$1\n  local type=$2\n  if [[ -s $list ]]; then\n    echo -e \"检测到有${type}的定时任务:\"\n    cat $list\n  fi\n}\n\n## 自动删除失效的脚本与定时任务，需要：1.AutoDelCron 设置为 true；2.正常更新js脚本，没有报错；3.存在失效任务\n## $1：失效任务清单文件路径\ndel_cron() {\n  local list_drop=$1\n  local path=$2\n  local detail=\"\"\n  local ids=\"\"\n  echo -e \"\\n开始尝试自动删除失效的定时任务...\"\n  for cron in $(cat $list_drop); do\n    local id=$(cat $list_crontab_user | grep -E \"$cmd_task.* $cron\" | perl -pe \"s|.*ID=(.*) $cmd_task.* $cron\\.*|\\1|\" | head -1 | awk -F \" \" '{print $1}')\n    if [[ $ids ]]; then\n      ids=\"$ids,\\\"$id\\\"\"\n    else\n      ids=\"\\\"$id\\\"\"\n    fi\n    cron_file=\"$dir_scripts/${cron}\"\n    if [[ -f $cron_file ]]; then\n      cron_name=$(grep \"new Env\" $cron_file | awk -F \"\\(\" '{print $2}' | awk -F \"\\)\" '{print $1}' | sed 's:.*\\('\\''\\|\"\\)\\([^\"'\\'']*\\)\\('\\''\\|\"\\).*:\\2:' | sed 's:\"::g' | sed \"s:'::g\" | head -1)\n      rm -f $cron_file\n    fi\n    [[ -z $cron_name ]] && cron_name=\"$cron\"\n    if [[ $detail ]]; then\n      detail=\"${detail}\\n${cron_name}\"\n    else\n      detail=\"${cron_name}\"\n    fi\n  done\n  if [[ $ids ]]; then\n    result=$(del_cron_api \"$ids\")\n    notify_api \"$path 删除任务${result}\" \"$detail\"\n  fi\n}\n\n## 自动增加定时任务，需要：1.AutoAddCron 设置为 true；2.正常更新js脚本，没有报错；3.存在新任务；4.crontab.list存在并且不为空\n## $1：新任务清单文件路径\nadd_cron() {\n  local list_add=$1\n  local path=$2\n  echo -e \"\\n开始尝试自动添加定时任务...\"\n  local detail=\"\"\n  cd $dir_scripts\n  for file in $(cat $list_add); do\n    local file_name=${file/${path}\\//}\n    file_name=${file_name/${path}\\_/}\n    if [[ -f $file ]]; then\n      cron_line=$(\n        perl -ne \"{\n                        print if /.*([\\d\\*]*[\\*-\\/,\\d]*[\\d\\*] ){4,5}[\\d\\*]*[\\*-\\/,\\d]*[\\d\\*]( |,|\\\").*$file_name/\n                    }\" $file 2>/dev/null |\n          perl -pe \"{\n                        s|[^\\d\\*]*(([\\d\\*]*[\\*-\\/,\\d]*[\\d\\*] ){4,5}[\\d\\*]*[\\*-\\/,\\d]*[\\d\\*])( \\|,\\|\\\").*/?$file_name.*|\\1|g;\n                        s|\\*([\\d\\*])(.*)|\\1\\2|g;\n                        s|  | |g;\n                    }\" 2>/dev/null | sort -u | head -1\n      )\n      [[ -z $cron_line ]] && cron_line=$(grep \"cron:\" $file | awk -F \":\" '{print $2}' | head -1 | xargs)\n      [[ -z $cron_line ]] && cron_line=$(grep \"cron \" $file | awk -F \"cron \\\"\" '{print $2}' | awk -F \"\\\" \" '{print $1}' | head -1 | xargs)\n      [[ -z $cron_line ]] && cron_line=\"$default_cron\"\n      cron_name=$(grep \"new Env\" $file | awk -F \"\\(\" '{print $2}' | awk -F \"\\)\" '{print $1}' | sed 's:.*\\('\\''\\|\"\\)\\([^\"'\\'']*\\)\\('\\''\\|\"\\).*:\\2:' | sed 's:\"::g' | sed \"s:'::g\" | head -1)\n      [[ -z $cron_name ]] && cron_name=$(grep \"name:\" $file | awk -F \":\" '{print $2}' | head -1 | xargs)\n      [[ -z $cron_name ]] && cron_name=$(basename \"$file_name\")\n      result=$(add_cron_api \"${cron_line}:${cmd_task} ${file}:${cron_name}:${SUB_ID}\")\n      echo -e \"$result\"\n      if [[ $detail ]]; then\n        detail=\"${detail}${result}\\n\"\n      else\n        detail=\"${result}\\n\"\n      fi\n    fi\n  done\n  notify_api \"$path 新增任务\" \"$detail\"\n}\n\n## 更新仓库\nupdate_repo() {\n  local url=\"$1\"\n  local path=\"$2\"\n  local blackword=\"$3\"\n  local dependence=\"$4\"\n  local branch=\"$5\"\n  local extensions=\"$6\"\n  local proxy=\"$7\"\n  local autoAddCron=\"$8\"\n  local autoDelCron=\"$9\"\n  local tmp=\"${url%/*}\"\n  local authorTmp1=\"${tmp##*/}\"\n  local authorTmp2=\"${authorTmp1##*:}\"\n  local author=\"${authorTmp2##*.}\"\n\n  local repo_path=\"${dir_repo}/${uniq_path}\"\n\n  make_dir \"${dir_scripts}/${uniq_path}\"\n\n  local formatUrl=\"$url\"\n  rm -rf ${repo_path} &>/dev/null\n  git_clone_scripts \"${formatUrl}\" ${repo_path} \"${branch}\" \"${proxy}\"\n\n  if [[ $exit_status -eq 0 ]]; then\n    echo -e \"拉取 ${uniq_path} 成功...\\n\"\n    diff_scripts \"$repo_path\" \"$author\" \"$path\" \"$blackword\" \"$dependence\" \"$extensions\" \"$autoAddCron\" \"$autoDelCron\"\n  else\n    echo -e \"拉取 ${uniq_path} 失败，请检查日志...\\n\"\n  fi\n}\n\n## 更新所有 raw 文件\nupdate_raw() {\n  local url=\"$1\"\n  local proxy=\"$2\"\n  local autoAddCron=\"$3\"\n  local autoDelCron=\"$4\"\n\n  if [[ ! $autoAddCron ]]; then\n    autoAddCron=${AutoAddCron}\n  fi\n  if [[ ! $autoDelCron ]]; then\n    autoDelCron=${AutoDelCron}\n  fi\n\n  local raw_url=\"$url\"\n  local suffix=\"${raw_url##*.}\"\n  local raw_file_name=\"${uniq_path}.${suffix}\"\n  echo -e \"开始下载：${raw_url} \\n\\n保存路径：$dir_raw/${raw_file_name}\\n\"\n\n  set_proxy \"$proxy\"\n  wget -q --no-check-certificate -O \"$dir_raw/${raw_file_name}.new\" ${raw_url}\n  exit_status=$?\n  unset_proxy\n\n  if [[ $? -eq 0 ]]; then\n    mv \"$dir_raw/${raw_file_name}.new\" \"$dir_raw/${raw_file_name}\"\n    echo -e \"下载 ${raw_file_name} 成功...\\n\"\n    cd $dir_raw\n    local filename=\"raw_${raw_file_name}\"\n    local cron_id=$(cat $list_crontab_user | grep -E \"$cmd_task.* $filename\" | perl -pe \"s|.*ID=(.*) $cmd_task.* $filename\\.*|\\1|\" | head -1 | awk -F \" \" '{print $1}')\n    cp -f $raw_file_name $dir_scripts/${filename}\n    if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then\n      cron_line=$(\n        perl -ne \"{\n                      print if /.*([\\d\\*]*[\\*-\\/,\\d]*[\\d\\*] ){4,5}[\\d\\*]*[\\*-\\/,\\d]*[\\d\\*]( |,|\\\").*$raw_file_name/\n                  }\" $raw_file_name |\n          perl -pe \"{\n                      s|[^\\d\\*]*(([\\d\\*]*[\\*-\\/,\\d]*[\\d\\*] ){4,5}[\\d\\*]*[\\*-\\/,\\d]*[\\d\\*])( \\|,\\|\\\").*/?$raw_file_name.*|\\1|g;\n                      s|\\*([\\d\\*])(.*)|\\1\\2|g;\n                      s|  | |g;\n                  }\" | sort -u | head -1\n      )\n      cron_name=$(grep \"new Env\" $raw_file_name | awk -F \"\\(\" '{print $2}' | awk -F \"\\)\" '{print $1}' | sed 's:.*\\('\\''\\|\"\\)\\([^\"'\\'']*\\)\\('\\''\\|\"\\).*:\\2:' | sed 's:\"::g' | sed \"s:'::g\" | head -1)\n      [[ -z $cron_name ]] && cron_name=\"$raw_file_name\"\n      [[ -z $cron_line ]] && cron_line=$(grep \"cron:\" $raw_file_name | awk -F \":\" '{print $2}' | head -1 | xargs)\n      [[ -z $cron_line ]] && cron_line=$(grep \"cron \" $raw_file_name | awk -F \"cron \\\"\" '{print $2}' | awk -F \"\\\" \" '{print $1}' | head -1 | xargs)\n      [[ -z $cron_line ]] && cron_line=\"$default_cron\"\n      result=$(add_cron_api \"${cron_line}:${cmd_task} ${filename}:${cron_name}:${SUB_ID}\")\n      echo -e \"$result\\n\"\n      notify_api \"新增任务通知\" \"\\n$result\"\n      # update_cron_api \"$cron_line:$cmd_task $filename:$cron_name:$cron_id\"\n    fi\n  else\n    echo -e \"下载 ${raw_file_name} 失败，保留之前正常下载的版本...\\n\"\n    [[ -f \"$dir_raw/${raw_file_name}.new\" ]] && rm -f \"$dir_raw/${raw_file_name}.new\"\n  fi\n\n}\n\n## 调用用户自定义的extra.sh\nrun_extra_shell() {\n  if [[ -f $file_extra_shell ]]; then\n    . $file_extra_shell\n  else\n    echo -e \"$file_extra_shell文件不存在，跳过执行...\\n\"\n  fi\n}\n\n## 脚本用法\nusage() {\n  echo -e \"$cmd_update 命令使用方法：\"\n  echo -e \"1.  $cmd_update update                                                                  # 更新并重启青龙\"\n  echo -e \"2.  $cmd_update extra                                                                   # 运行自定义脚本\"\n  echo -e \"3.  $cmd_update raw <fileurl>                                                           # 更新单个脚本文件\"\n  echo -e \"4.  $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions>    # 更新单个仓库的脚本\"\n  echo -e \"5.  $cmd_update rmlog <days>                                                            # 删除旧日志\"\n  echo -e \"6.  $cmd_update bot                                                                     # 启动tg-bot\"\n  echo -e \"7.  $cmd_update check                                                                   # 检测青龙环境并修复\"\n  echo -e \"8.  $cmd_update resetlet                                                                # 重置登录错误次数\"\n  echo -e \"9.  $cmd_update resettfa                                                                # 禁用两步登录\"\n  echo -e \"10. $cmd_update resetpwd                                                                # 修改登录密码\"\n  echo -e \"11. $cmd_update resetname                                                               # 修改登录用户名\"\n}\n\nreload_qinglong() {\n  echo -e \"[reload_qinglong] deleting Triggered at $(date)\" >>${dir_log}/reload.log\n  sleep 3\n  delete_pm2\n  echo -e \"[reload_qinglong] deleted Triggered at $(date)\" >>${dir_log}/reload.log\n\n  local reload_target=\"${1}\"\n  local primary_branch=\"master\"\n  if [[ \"${QL_BRANCH}\" == \"develop\" ]] || [[ \"${QL_BRANCH}\" == \"debian\" ]] || [[ \"${QL_BRANCH}\" == \"debian-dev\" ]]; then\n    primary_branch=\"${QL_BRANCH}\"\n  fi\n\n  if [[ \"$reload_target\" == 'system' ]]; then\n    rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src\n    mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/\n    rm -rf $dir_static/*\n    mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/\n    cp -f $file_config_sample $dir_config/config.sample.sh\n  fi\n\n  if [[ \"$reload_target\" == 'data' ]]; then\n    rm -rf ${dir_data}/*\n    mv -f ${dir_tmp}/data/* ${dir_data}/\n  fi\n  echo -e \"[reload_qinglong] starting Triggered at $(date)\" >>${dir_log}/reload.log\n  reload_pm2\n  echo -e \"[reload_qinglong] started Triggered at $(date)\\n\" >>${dir_log}/reload.log\n}\n\n## 更新 qinglong\nupdate_qinglong() {\n  rm -rf ${dir_tmp}/*\n  local mirror=\"gitee\"\n  local downloadQLUrl=\"https://gitee.com/whyour/qinglong/repository/archive\"\n  local downloadStaticUrl=\"https://gitee.com/whyour/qinglong-static/repository/archive\"\n  local githubStatus=$(curl -s --noproxy \"*\" -m 2 -IL \"https://google.com\" | grep 200)\n  if [[ ! -z $githubStatus ]]; then\n    mirror=\"github\"\n    downloadQLUrl=\"https://github.com/whyour/qinglong/archive/refs/heads\"\n    downloadStaticUrl=\"https://github.com/whyour/qinglong-static/archive/refs/heads\"\n  fi\n  echo -e \"使用 ${mirror} 源更新...\\n\"\n\n  local primary_branch=\"master\"\n  if [[ \"${QL_BRANCH}\" == \"develop\" ]] || [[ \"${QL_BRANCH}\" == \"debian\" ]] || [[ \"${QL_BRANCH}\" == \"debian-dev\" ]]; then\n    primary_branch=\"${QL_BRANCH}\"\n  fi\n\n  wget -cqO \"${dir_tmp}/ql.zip\" \"${downloadQLUrl}/${primary_branch}.zip\"\n  exit_status=$?\n\n  if [[ $exit_status -eq 0 ]]; then\n    echo -e \"更新青龙源文件成功...\\n\"\n\n    unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}\n\n    update_qinglong_static\n  else\n    echo -e \"更新青龙源文件失败，请检查网络...\\n\"\n  fi\n}\n\nupdate_qinglong_static() {\n  wget -cqO \"${dir_tmp}/static.zip\" \"${downloadStaticUrl}/${primary_branch}.zip\"\n  exit_status=$?\n\n  if [[ $exit_status -eq 0 ]]; then\n    echo -e \"更新青龙静态资源成功...\\n\"\n    unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}\n\n    check_update_dep\n  else\n    echo -e \"更新青龙静态资源失败，请检查网络...\\n\"\n  fi\n}\n\ncheck_update_dep() {\n  echo -e \"\\n开始检测依赖...\\n\"\n  if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then\n    npm_install_2 \"${dir_tmp}/qinglong-${primary_branch}\"\n  fi\n\n  if [[ $exit_status -eq 0 ]]; then\n    echo -e \"\\n依赖检测安装成功...\\n\"\n    echo -e \"更新包下载成功...\"\n\n    if [[ \"$needRestart\" == 'true' ]]; then\n      reload_qinglong \"system\"\n    fi\n  else\n    echo -e \"\\n依赖检测安装失败，请检查网络...\\n\"\n  fi\n}\n\n## 对比脚本\ndiff_scripts() {\n  local dir_current=$(pwd)\n  local repo_path=\"$1\"\n  local author=\"$2\"\n  local path=\"$3\"\n  local blackword=\"$4\"\n  local dependence=\"$5\"\n  local extensions=\"$6\"\n  local autoAddCron=\"$7\"\n  local autoDelCron=\"$8\"\n\n  if [[ ! $autoAddCron ]]; then\n    autoAddCron=${AutoAddCron}\n  fi\n  if [[ ! $autoDelCron ]]; then\n    autoDelCron=${AutoDelCron}\n  fi\n\n  gen_list_repo \"$repo_path\" \"$author\" \"$path\" \"$blackword\" \"$dependence\" \"$extensions\"\n\n  local list_add=\"$dir_list_tmp/${uniq_path}_add.list\"\n  local list_drop=\"$dir_list_tmp/${uniq_path}_drop.list\"\n  diff_cron \"$dir_list_tmp/${uniq_path}_scripts.list\" \"$dir_list_tmp/${uniq_path}_user.list\" $list_add $list_drop\n\n  if [[ -s $list_drop ]]; then\n    output_list_add_drop $list_drop \"失效\"\n    if [[ ${autoDelCron} == true ]]; then\n      del_cron $list_drop $uniq_path\n    fi\n  fi\n  if [[ -s $list_add ]]; then\n    output_list_add_drop $list_add \"新\"\n    if [[ ${autoAddCron} == true ]]; then\n      add_cron $list_add $uniq_path\n    fi\n  fi\n  cd $dir_current\n}\n\n## 生成脚本的路径清单文件\ngen_list_repo() {\n  local dir_current=$(pwd)\n  local repo_path=\"$1\"\n  local author=\"$2\"\n  local path=\"$3\"\n  local blackword=\"$4\"\n  local dependence=\"$5\"\n\n  rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null\n\n  cd ${repo_path}\n\n  local cmd=\"find .\"\n  local index=0\n  if [[ $6 ]]; then\n    file_extensions=\"$6\"\n    if [[ $file_extensions =~ \"|\" ]]; then\n      file_extensions=$(echo $file_extensions | sed 's/|/ /g')\n    fi\n  fi\n  for extension in $file_extensions; do\n    if [[ $index -eq 0 ]]; then\n      cmd=\"${cmd} -name \\\"*.${extension}\\\"\"\n    else\n      cmd=\"${cmd} -o -name \\\"*.${extension}\\\"\"\n    fi\n    let index+=1\n  done\n  files=$(eval $cmd | sed 's/^..//')\n  if [[ $path ]]; then\n    files=$(echo \"$files\" | egrep \"$path\")\n  fi\n  if [[ $blackword ]]; then\n    files=$(echo \"$files\" | egrep -v \"$blackword\")\n  fi\n\n  cp -f $file_notify_js \"${dir_scripts}/${uniq_path}\"\n  cp -f $file_notify_py \"${dir_scripts}/${uniq_path}\"\n\n  if [[ $dependence ]]; then\n    cd ${repo_path}\n    results=$(eval $cmd | sed 's/^..//' | egrep \"$dependence\")\n    for _file in ${results}; do\n      file_path=$(dirname $_file)\n      make_dir \"${dir_scripts}/${uniq_path}/${file_path}\"\n      cp -f $_file \"${dir_scripts}/${uniq_path}/${file_path}\"\n    done\n  fi\n\n  if [[ -d $dir_dep ]]; then\n    cp -rf $dir_dep/* \"${dir_scripts}/${uniq_path}\" &>/dev/null\n  fi\n\n  for file in ${files}; do\n    dirPath=$(dirname \"$file\")\n    filename=$(basename \"$file\")\n    filePath=\"${uniq_path}/${filename}\"\n    if [[ $dirPath ]] && [[ $dirPath != '.' ]]; then\n      mkdir -p \"${dir_scripts}/${uniq_path}/${dirPath}\"\n      filePath=\"${uniq_path}/${dirPath}/${filename}\"\n    fi\n    cp -f $file \"${dir_scripts}/$filePath\"\n    echo \"$filePath\" >>\"$dir_list_tmp/${uniq_path}_scripts.list\"\n    # cron_id=$(cat $list_crontab_user | grep -E \"$cmd_task.* ${uniq_path}_${filename}\" | perl -pe \"s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\\.*|\\1|\" | head -1 | awk -F \" \" '{print $1}')\n    # if [[ $cron_id ]]; then\n    #   result=$(update_cron_command_api \"$cmd_task ${uniq_path}/${filename}:$cron_id\")\n    # fi\n  done\n  grep -E \"${cmd_task}.* ${uniq_path}\" ${list_crontab_user} | perl -pe \"s|.*ID=(.*) ${cmd_task}.* (${uniq_path}.*)\\.*|\\2|\" | awk -F \" \" '{print $1}' | sort -u >\"$dir_list_tmp/${uniq_path}_user.list\"\n  cd $dir_current\n}\n\nget_uniq_path() {\n  local url=\"$1\"\n  local branch=\"$2\"\n  local urlTmp=\"${url%*/}\"\n  local repoTmp=\"${urlTmp##*/}\"\n  local repo=\"${repoTmp%.*}\"\n  local tmp=\"${url%/*}\"\n  local authorTmp1=\"${tmp##*/}\"\n  local authorTmp2=\"${authorTmp1##*:}\"\n  local author=\"${authorTmp2##*.}\"\n\n  uniq_path=\"${author}_${repo}\"\n  [[ $branch ]] && uniq_path=\"${uniq_path}_${branch}\"\n}\n\nmain() {\n  ## for ql update\n  show_log=\"false\"\n  while getopts \":l\" opt; do\n    case $opt in\n    l)\n      show_log=\"true\"\n      ;;\n    esac\n  done\n  [[ \"$show_log\" == \"true\" ]] && shift $(($OPTIND - 1))\n\n  local p1=\"${1}\"\n  local p2=\"${2}\"\n  local p3=\"${3}\"\n  local p4=\"${4}\"\n  local p5=\"${5}\"\n  local p6=\"${6}\"\n  local p7=\"${7}\"\n  local p8=\"${8}\"\n  local p9=\"${9}\"\n  local p10=\"${10}\"\n  local log_dir=\"${p1}\"\n  make_dir \"$dir_log/$log_dir\"\n  local log_time=$(date \"+%Y-%m-%d-%H-%M-%S\")\n  local log_path=\"${log_dir}/${log_time}.log\"\n  local file_path=\"$dir_log/$log_path\"\n\n  cmd=\"2>&1 | tee -a $file_path\"\n  if [[ \"$no_tee\" == \"true\" ]]; then\n    cmd=\">> $file_path 2>&1\"\n  fi\n  if [[ \"$real_time\" == \"true\" ]]; then\n    cmd=\"\"\n  fi\n\n  local time_format=\"%Y-%m-%d %H:%M:%S\"\n  local time=$(date \"+$time_format\")\n  local begin_timestamp=$(format_timestamp \"$time_format\" \"$time\")\n\n  local begin_time=$(format_time \"$time_format\" \"$time\")\n\n  if [[ \"$p1\" != \"repo\" ]] && [[ \"$p1\" != \"raw\" ]]; then\n    eval echo -e \"\\#\\# 开始执行... $begin_time\\\\\\n\" $cmd\n  fi\n\n  [[ $ID ]] && update_cron \"\\\"$ID\\\"\" \"0\" \"$$\" \"$log_path\" \"$begin_timestamp\"\n\n  case $p1 in\n  update)\n    fix_config\n    local needRestart=${p2:-\"true\"}\n    eval update_qinglong $cmd\n    ;;\n  reload)\n    eval reload_qinglong \"$p2\" $cmd\n    ;;\n  extra)\n    eval run_extra_shell $cmd\n    ;;\n  repo)\n    get_uniq_path \"$p2\" \"$p6\"\n    if [[ -n $p2 ]]; then\n      update_repo \"$p2\" \"$p3\" \"$p4\" \"$p5\" \"$p6\" \"$p7\" \"$p8\" \"$p9\" \"$p10\"\n    else\n      eval echo -e \"命令输入错误...\\\\\\n\" $cmd\n      eval usage $cmd\n    fi\n    ;;\n  raw)\n    get_uniq_path \"$p2\"\n    if [[ -n $p2 ]]; then\n      update_raw \"$p2\" \"$p3\" \"$p4\" \"$p5\"\n    else\n      eval echo -e \"命令输入错误...\\\\\\n\" $cmd\n      eval usage $cmd\n    fi\n    ;;\n  rmlog)\n    eval . $dir_shell/rmlog.sh \"$p2\" $cmd\n    ;;\n  bot)\n    eval . $dir_shell/bot.sh $cmd\n    ;;\n  check)\n    eval . $dir_shell/check.sh $cmd\n    ;;\n  resetlet)\n    eval update_auth_config \"\\\\\\\"retries\\\\\\\":0\" \"重置登录错误次数\" $cmd\n    ;;\n  resettfa)\n    eval update_auth_config \"\\\\\\\"twoFactorActivated\\\\\\\":false\" \"禁用两步验证\" $cmd\n    ;;\n  resetpwd)\n    eval update_auth_config \"\\\\\\\"password\\\\\\\":\\\\\\\"$p2\\\\\\\"\" \"重置密码\" $cmd\n    ;;\n  resetname)\n    eval update_auth_config \"\\\\\\\"username\\\\\\\":\\\\\\\"$p2\\\\\\\"\" \"重置用户名\" $cmd\n    ;;\n  *)\n    eval echo -e \"命令输入错误...\\\\\\n\" $cmd\n    eval usage $cmd\n    ;;\n  esac\n\n  local etime=$(date \"+$time_format\")\n  local end_time=$(format_time \"$time_format\" \"$etime\")\n  local end_timestamp=$(format_timestamp \"$time_format\" \"$etime\")\n  local diff_time=$(($end_timestamp - $begin_timestamp))\n  [[ $ID ]] && update_cron \"\\\"$ID\\\"\" \"1\" \"$$\" \"$log_path\" \"$begin_timestamp\" \"$diff_time\"\n\n  if [[ \"$p1\" != \"repo\" ]] && [[ \"$p1\" != \"raw\" ]]; then\n    eval echo -e \"\\\\\\n\\#\\# 执行结束... $end_time  耗时 $diff_time 秒　　　　　\" $cmd\n  fi\n}\n\nimport_config \"$@\"\nmain \"$@\"\n\nexit 0\n"
  },
  {
    "path": "src/app.ts",
    "content": "const baseUrl = window.__ENV__QlBaseUrl || '/';\nimport { setLocale } from '@umijs/max';\nimport intl from 'react-intl-universal';\n\nexport function rootContainer(container: any) {\n  const locales = {\n    'en': require('./locales/en-US.json'),\n    'zh': require('./locales/zh-CN.json'),\n  };\n  let currentLocale: string;\n  try {\n    currentLocale = intl.determineLocale({\n      urlLocaleKey: 'lang',\n      cookieLocaleKey: 'lang',\n      localStorageLocaleKey: 'lang',\n    }).slice(0, 2);\n  } catch (e: unknown) {\n    // Handle decodeURIComponent errors from malformed cookies\n    console.warn('Failed to determine locale from cookies:', e);\n    currentLocale = '';\n  }\n\n  if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {\n    currentLocale = 'zh';\n  }\n\n  intl.init({ currentLocale, locales });\n  setLocale(currentLocale === 'zh' ? 'zh-CN' : 'en-US');\n  return container;\n}\n\nexport function modifyClientRenderOpts(memo: any) {\n  return {\n    ...memo,\n    publicPath: baseUrl,\n    basename: baseUrl,\n  };\n}\n\nexport function modifyContextOpts(memo: any) {\n  return {\n    ...memo,\n    basename: baseUrl,\n  };\n}\n"
  },
  {
    "path": "src/components/copy.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useRef, useState, useEffect } from 'react';\nimport { Tooltip, Typography, message } from 'antd';\nimport { CopyOutlined, CheckOutlined } from '@ant-design/icons';\nimport { CopyToClipboard } from 'react-copy-to-clipboard';\n\nconst { Link } = Typography;\n\nconst Copy = ({ text }: { text: string }) => {\n  const [copied, setCopied] = useState(false);\n  const copyIdRef = useRef<number>();\n\n  const handleCopy = (text: string, result: boolean) => {\n    if (result) {\n      setCopied(true);\n      message.success(intl.get('复制成功'));\n\n      cleanCopyId();\n      copyIdRef.current = window.setTimeout(() => {\n        setCopied(false);\n      }, 3000);\n    }\n  };\n\n  const handleClick = (e?: React.MouseEvent) => {\n    e?.preventDefault();\n    e?.stopPropagation();\n  };\n\n  const cleanCopyId = () => {\n    window.clearTimeout(copyIdRef.current!);\n  };\n\n  return (\n    <Link onClick={handleClick} style={{ marginLeft: 4 }}>\n      <CopyToClipboard text={text} onCopy={handleCopy}>\n        <Tooltip\n          key=\"copy\"\n          title={copied ? intl.get('复制成功') : intl.get('复制')}\n        >\n          {copied ? <CheckOutlined /> : <CopyOutlined />}\n        </Tooltip>\n      </CopyToClipboard>\n    </Link>\n  );\n};\n\nexport default Copy;\n"
  },
  {
    "path": "src/components/iconfont.tsx",
    "content": "import { createFromIconfontCN } from '@ant-design/icons';\n\nconst IconFont = createFromIconfontCN({\n  scriptUrl: ['//at.alicdn.com/t/c/font_3354854_lc939gab1iq.js'],\n});\n\nexport default IconFont;\n"
  },
  {
    "path": "src/components/index.less",
    "content": ".react-terminal-wrapper {\n  width: 100%;\n  background: #252a33;\n  color: #eee;\n  font-size: 18px;\n  font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier,\n    monospace;\n  border-radius: 4px;\n  padding: 75px 45px 35px;\n  position: relative;\n  -webkit-box-sizing: border-box;\n  box-sizing: border-box;\n}\n\n.react-terminal {\n  overflow: auto;\n  display: flex;\n  flex-direction: column;\n}\n\n.react-terminal-wrapper.react-terminal-light {\n  background: #ddd;\n  color: #1a1e24;\n}\n\n.react-terminal-wrapper:before {\n  content: '';\n  position: absolute;\n  top: 15px;\n  left: 15px;\n  display: inline-block;\n  width: 15px;\n  height: 15px;\n  border-radius: 50%;\n  background: #d9515d;\n  -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;\n  box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;\n}\n\n.react-terminal-wrapper:after {\n  content: attr(data-terminal-name);\n  position: absolute;\n  color: #a2a2a2;\n  top: 5px;\n  left: 0;\n  width: 100%;\n  text-align: center;\n}\n\n.react-terminal-wrapper.react-terminal-light:after {\n  color: #d76d77;\n}\n\n.react-terminal-line {\n  display: block;\n  line-height: 1.5;\n}\n\n.react-terminal-line:before {\n  content: '';\n  display: inline-block;\n  vertical-align: middle;\n  color: #a2a2a2;\n}\n\n.react-terminal-light .react-terminal-line:before {\n  color: #d76d77;\n}\n\n.react-terminal-input:before {\n  margin-right: 0.75em;\n  content: '$';\n}\n\n.react-terminal-input[data-terminal-prompt]:before {\n  content: attr(data-terminal-prompt);\n}\n"
  },
  {
    "path": "src/components/name.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { Service, Options } from 'ahooks/lib/useRequest/src/types';\nimport { Spin, Typography } from 'antd';\n\nexport default function Name<\n  TData extends { data?: { name: string } },\n  TParams,\n>({\n  service,\n  options,\n}: {\n  service: Service<TData, [TParams]>;\n  options: Options<TData, [TParams]>;\n}) {\n  const { loading, data } = useRequest(service, options);\n\n  return (\n    <Spin spinning={loading}>\n      <Typography.Text ellipsis={true}>{data?.data?.name}</Typography.Text>\n    </Spin>\n  );\n}\n"
  },
  {
    "path": "src/components/tag.tsx",
    "content": "import intl from 'react-intl-universal';\nimport { Tag, Input } from 'antd';\nimport { TweenOneGroup } from 'rc-tween-one';\nimport { PlusOutlined } from '@ant-design/icons';\nimport { useEffect, useRef, useState } from 'react';\n\nconst EditableTagGroup = ({\n  value,\n  onChange,\n}: {\n  value?: string[];\n  onChange?: (tags: string[]) => void;\n}) => {\n  const [inputValue, setInputValue] = useState('');\n  const [inputVisible, setInputVisible] = useState(false);\n  const [tags, setTags] = useState<string[]>([]);\n  const saveInputRef = useRef<any>();\n\n  const handleClose = (removedTag: string) => {\n    const _tags = tags.filter((tag) => tag !== removedTag);\n    setTags(_tags);\n    onChange?.(_tags);\n  };\n\n  const showInput = () => {\n    setInputVisible(true);\n  };\n\n  const handleInputChange = (e) => {\n    setInputValue(e.target.value);\n  };\n\n  const handleInputConfirm = () => {\n    if (inputValue && !tags.includes(inputValue)) {\n      setTags([...tags, inputValue]);\n      onChange?.([...tags, inputValue]);\n    }\n    setInputVisible(false);\n    setInputValue('');\n  };\n\n  const tagChild = tags.map((tag) => {\n    const tagElem = (\n      <Tag\n        closable\n        onClose={(e) => {\n          e.preventDefault();\n          handleClose(tag);\n        }}\n      >\n        {tag}\n      </Tag>\n    );\n\n    return (\n      <span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>\n        {tagElem}\n      </span>\n    );\n  });\n\n  useEffect(() => {\n    if (inputVisible && saveInputRef) {\n      saveInputRef.current.focus();\n    }\n  }, [inputVisible]);\n\n  useEffect(() => {\n    if (value) {\n      setTags(value);\n    }\n  }, [value]);\n\n  return (\n    <>\n      <TweenOneGroup\n        enter={{\n          scale: 0.8,\n          opacity: 0,\n          type: 'from',\n          duration: 100,\n        }}\n        leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}\n        appear={false}\n      >\n        {tagChild}\n      </TweenOneGroup>\n      {inputVisible && (\n        <Input\n          ref={saveInputRef}\n          type=\"text\"\n          size=\"small\"\n          style={{ width: 78 }}\n          value={inputValue}\n          onChange={handleInputChange}\n          onBlur={handleInputConfirm}\n          onPressEnter={handleInputConfirm}\n        />\n      )}\n      {!inputVisible && (\n        <Tag\n          onClick={showInput}\n          style={{ borderStyle: 'dashed', cursor: 'pointer' }}\n        >\n          <PlusOutlined /> {intl.get('新建')}\n        </Tag>\n      )}\n    </>\n  );\n};\n\nexport default EditableTagGroup;\n"
  },
  {
    "path": "src/components/terminal.tsx",
    "content": "import React, { useEffect, useRef } from 'react';\nimport './index.less';\n\nexport enum LineType {\n  Input,\n  Output,\n}\n\nexport enum ColorMode {\n  Light,\n  Dark,\n}\n\nexport interface Props {\n  name?: string;\n  prompt?: string;\n  colorMode?: ColorMode;\n  lineData: Array<{ type: LineType; value: string | React.ReactNode }>;\n  startingInputValue?: string;\n}\n\nconst Terminal = ({\n  name,\n  prompt,\n  colorMode,\n  lineData,\n  startingInputValue = '',\n}: Props) => {\n  const lastLineRef = useRef<null | HTMLElement>(null);\n\n  // An effect that handles scrolling into view the last line of terminal input or output\n  const performScrolldown = useRef(false);\n  useEffect(() => {\n    if (performScrolldown.current) {\n      // skip scrolldown when the component first loads\n      setTimeout(\n        () =>\n          lastLineRef?.current?.scrollIntoView({\n            behavior: 'smooth',\n            block: 'nearest',\n          }),\n        500,\n      );\n    }\n    performScrolldown.current = true;\n  }, [lineData.length]);\n\n  const renderedLineData = lineData.map((ld, i) => {\n    const classes = ['react-terminal-line'];\n    if (ld.type === LineType.Input) {\n      classes.push('react-terminal-input');\n    }\n    // `lastLineRef` is used to ensure the terminal scrolls into view to the last line; make sure to add the ref to the last\n    if (lineData.length === i + 1) {\n      return (\n        <span className={classes.join(' ')} key={i} ref={lastLineRef}>\n          {ld.value}\n        </span>\n      );\n    } else {\n      return (\n        <span className={classes.join(' ')} key={i}>\n          {ld.value}\n        </span>\n      );\n    }\n  });\n\n  const classes = ['react-terminal-wrapper'];\n  if (colorMode === ColorMode.Light) {\n    classes.push('react-terminal-light');\n  }\n  return (\n    <div className={classes.join(' ')} data-terminal-name={name}>\n      <div className=\"react-terminal\">{renderedLineData}</div>\n    </div>\n  );\n};\n\nexport default Terminal;\n"
  },
  {
    "path": "src/hooks/useFilterTreeData.ts",
    "content": "import { useMemo, useState } from 'react';\n\nexport default (\n  treeData: any[],\n  searchValue: string,\n  {\n    treeNodeFilterProp,\n  }: {\n    treeNodeFilterProp: string;\n  },\n) => {\n  return useMemo(() => {\n    const keys: string[] = [];\n\n    if (!searchValue) {\n      return { treeData, keys };\n    }\n\n    const upperStr = searchValue.toUpperCase();\n    function filterOptionFunc(_: string, dataNode: any[]) {\n      const value = dataNode[treeNodeFilterProp as any];\n\n      return String(value).toUpperCase().includes(upperStr);\n    }\n\n    function dig(list: any[], keepAll: boolean = false): any[] {\n      return list\n        .map((dataNode) => {\n          const children = dataNode.children;\n\n          const match = keepAll || filterOptionFunc!(searchValue, dataNode);\n          const childList = dig(children || [], match);\n\n          if (match || childList.length) {\n            childList.length && keys.push(dataNode.key);\n            return {\n              ...dataNode,\n              children: childList,\n            };\n          }\n          return null;\n        })\n        .filter((node) => node);\n    }\n\n    return { treeData: dig(treeData), keys };\n  }, [treeData, searchValue, treeNodeFilterProp]);\n};\n"
  },
  {
    "path": "src/hooks/useScrollHeight.ts",
    "content": "import { RefObject, useState } from 'react';\nimport useResizeObserver from '@react-hook/resize-observer';\n\nexport default <T extends HTMLElement>(target: RefObject<T>) => {\n  const [height, setHeight] = useState<number>(0);\n\n  useResizeObserver(target, (entry) => {\n    let _height = entry.target.clientHeight;\n    if (height !== _height) {\n      setHeight(_height);\n    }\n  });\n  return height;\n};\n"
  },
  {
    "path": "src/hooks/useTableScrollHeight.ts",
    "content": "import { RefObject, useState } from 'react';\nimport useResizeObserver from '@react-hook/resize-observer';\nimport { getTableScroll } from '@/utils';\n\nexport default <T extends HTMLElement>(\n  target: RefObject<T>,\n  extraHeight?: number,\n) => {\n  const [height, setHeight] = useState<number>(0);\n\n  useResizeObserver(target, (entry) => {\n    let _target = entry.target as any;\n    if (!_target.classList.contains('ant-table-wrapper')) {\n      _target = entry.target.querySelector('.ant-table-wrapper');\n    }\n    setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));\n  });\n  return height;\n};\n"
  },
  {
    "path": "src/layouts/defaultProps.tsx",
    "content": "import intl from 'react-intl-universal';\nimport { SettingOutlined } from '@ant-design/icons';\nimport IconFont from '@/components/iconfont';\nimport { BasicLayoutProps } from '@ant-design/pro-layout';\n\nexport default {\n  route: {\n    routes: [\n      {\n        name: intl.get('登录'),\n        path: '/login',\n        hideInMenu: true,\n        component: '@/pages/login/index',\n      },\n      {\n        name: intl.get('初始化'),\n        path: '/initialization',\n        hideInMenu: true,\n        component: '@/pages/initialization/index',\n      },\n      {\n        name: intl.get('错误'),\n        path: '/error',\n        hideInMenu: true,\n        component: '@/pages/error/index',\n      },\n      {\n        path: '/crontab',\n        name: intl.get('定时任务'),\n        icon: <IconFont type=\"ql-icon-crontab\" />,\n        component: '@/pages/crontab/index',\n      },\n      {\n        path: '/subscription',\n        name: intl.get('订阅管理'),\n        icon: <IconFont type=\"ql-icon-subs\" />,\n        component: '@/pages/subscription/index',\n      },\n      {\n        path: '/env',\n        name: intl.get('环境变量'),\n        icon: <IconFont type=\"ql-icon-env\" />,\n        component: '@/pages/env/index',\n      },\n      {\n        path: '/config',\n        name: intl.get('配置文件'),\n        icon: <IconFont type=\"ql-icon-config\" />,\n        component: '@/pages/config/index',\n      },\n      {\n        path: '/script',\n        name: intl.get('脚本管理'),\n        icon: <IconFont type=\"ql-icon-script\" />,\n        component: '@/pages/script/index',\n      },\n      {\n        path: '/dependence',\n        name: intl.get('依赖管理'),\n        icon: <IconFont type=\"ql-icon-dependence\" />,\n        component: '@/pages/dependence/index',\n      },\n      {\n        path: '/log',\n        name: intl.get('日志管理'),\n        icon: <IconFont type=\"ql-icon-log\" />,\n        component: '@/pages/log/index',\n      },\n      {\n        path: '/diff',\n        name: intl.get('对比工具'),\n        icon: <IconFont type=\"ql-icon-diff\" />,\n        component: '@/pages/diff/index',\n      },\n      {\n        path: '/setting',\n        name: intl.get('系统设置'),\n        icon: <SettingOutlined />,\n        component: '@/pages/password/index',\n      },\n    ],\n  },\n  navTheme: 'light',\n  fixSiderbar: true,\n  contentWidth: 'Fixed',\n  splitMenus: false,\n  siderWidth: 180,\n} as BasicLayoutProps;\n"
  },
  {
    "path": "src/layouts/index.less",
    "content": "@import '~antd/es/style/themes/default.less';\n@import '~@/styles/variable.less';\n\n@font-face {\n  font-family: 'Source Code Pro';\n  src: url('../assets/fonts/SourceCodePro-Regular.ttf.woff2') format('woff2'),\n    url('../assets/fonts/SourceCodePro-Regular.otf.woff') format('woff'),\n    url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');\n}\n\n@font-face {\n  font-family: Log;\n  src: url('../assets/fonts/log.woff2') format('woff2'),\n    url('../assets/fonts/log.woff') format('woff'),\n    url('../assets/fonts/log.ttf') format('truetype');\n}\n\nbody {\n  // 禁止手机页面下拉刷新\n  overflow: hidden;\n\n  // 禁止手机页面弹簧效果\n  position: fixed;\n  top: 0;\n  left: 0;\n}\n\n#root {\n  height: 100vh;\n  height: calc(100vh - var(--vh-offset, 0px));\n  -webkit-overflow-scrolling: touch;\n}\n\n.ant-modal-header {\n  padding-right: 54px;\n}\n\n.ant-modal-body {\n  max-height: calc(80vh - 110px);\n  max-height: calc(80vh - var(--vh-offset, 110px));\n  overflow-y: auto;\n}\n\n.log-modal {\n  &.ant-modal {\n    max-width: 1000px !important;\n    width: 80vw !important;\n  }\n\n  .ant-modal-body {\n    overflow-y: auto;\n    min-height: 300px;\n    max-height: calc(80vh - 110px);\n    max-height: calc(80vh - var(--vh-offset, 110px));\n    padding: 0;\n    display: flex;\n\n    .log-container {\n      width: 100%;\n      padding: 24px;\n      overflow-y: auto;\n      code,\n      span {\n        font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,\n          Courier, monospace, Log;\n      }\n    }\n  }\n\n  pre {\n    white-space: break-spaces;\n    line-height: 17px;\n    margin-bottom: 0;\n    overflow: hidden;\n  }\n}\n\n.ͼ1 .cm-scroller {\n  font-family: monospace, Log;\n}\n\n.monaco-editor:not(.rename-box) {\n  height: calc(100vh - 128px) !important;\n  height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;\n\n  .view-overlays .current-line {\n    border-width: 0;\n  }\n}\n\n.rename-box {\n  height: 0;\n\n  .rename-input {\n    height: 0;\n    padding: 0 !important;\n    display: none !important;\n  }\n}\n\n.ant-pro-grid-content.wide {\n  max-width: unset !important;\n  overflow: auto;\n\n  .ant-pro-page-container-children-content {\n    overflow: auto;\n    height: 100%;\n    background-color: @component-background;\n    padding: 12px;\n  }\n}\n\n.ql-container-wrapper-has-tab {\n  .ant-pro-grid-content.wide .ant-pro-page-container-children-content {\n    padding-top: 0;\n  }\n}\n\n.ant-table-cell-ellipsis {\n  text-align: left !important;\n}\n\n.ant-tooltip {\n  max-width: 300px !important;\n\n  .ant-tooltip-inner {\n    word-break: break-all !important;\n    max-height: 300px !important;\n    overflow-y: auto !important;\n  }\n}\n\n.env-wrapper {\n  th {\n    white-space: nowrap;\n  }\n}\n\n.log-wrapper {\n  .log-select {\n    width: 250px;\n  }\n\n  .ant-page-header-heading-left {\n    min-width: 100px;\n  }\n}\n\n.config-wrapper {\n  .config-select {\n    width: 250px;\n  }\n\n  .ant-page-header-heading-left {\n    min-width: 100px;\n  }\n}\n\n.ant-tree {\n  .ant-tree-treenode {\n    width: 100%;\n    padding-right: 8px !important;\n  }\n\n  .ant-tree-node-content-wrapper {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    flex: 1;\n  }\n}\n\n.ant-select-tree {\n  .ant-select-tree-treenode {\n    width: 100%;\n    padding-right: 8px !important;\n  }\n\n  .ant-select-tree-node-content-wrapper {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    flex: 1;\n  }\n}\n\n.ant-pro-page-container.ql-container-wrapper {\n  display: flex;\n  flex-direction: column;\n  height: calc(100vh - 48px);\n  height: calc(100vh - var(--vh-offset, 0px) - 48px);\n\n  .ant-pro-grid-content.wide {\n    flex: 1;\n\n    .ant-pro-grid-content-children {\n      height: 100%;\n\n      > div,\n      .log-container,\n      .react-codemirror2,\n      .CodeMirror {\n        height: 100%;\n        overflow: auto;\n      }\n    }\n  }\n}\n\n@media (max-width: 768px) {\n  .ql-container-wrapper {\n    &.crontab-wrapper,\n    &.log-wrapper,\n    &.env-wrapper,\n    &.config-wrapper {\n      .CodeMirror {\n        width: calc(100vw - 24px);\n      }\n    }\n  }\n\n  .log-modal {\n    &.ant-modal {\n      width: calc(100vw - 16px) !important;\n    }\n  }\n\n  .ant-tooltip {\n    max-width: 300px !important;\n  }\n}\n\n.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {\n  margin-bottom: 0 !important;\n  min-height: calc(100vh - 72px);\n  min-height: calc(100vh - var(--vh-offset, 0px) - 72px);\n}\n\n.Resizer {\n  background: @component-background;\n  opacity: 0.8;\n  z-index: 100;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  box-sizing: border-box;\n  -moz-background-clip: padding;\n  -webkit-background-clip: padding;\n  background-clip: padding-box;\n  border-left: 5px solid rgba(42, 161, 255, 0);\n  border-right: 5px solid rgba(42, 161, 255, 0);\n}\n\n.Resizer:hover {\n  -webkit-transition: all 2s ease;\n  transition: all 2s ease;\n}\n\n.Resizer.horizontal {\n  height: 11px;\n  margin: -5px 0;\n  cursor: row-resize;\n  width: 100%;\n}\n\n.Resizer.vertical {\n  width: 11px;\n  margin: 0 -5px;\n  cursor: col-resize;\n}\n\n.Resizer.horizontal:hover,\n.Resizer.vertical:hover {\n  border-left-color: rgba(42, 161, 255, 0.5);\n  border-right-color: rgba(42, 161, 255, 0.5);\n}\n\n.Resizer.disabled {\n  cursor: not-allowed;\n}\n\n.Resizer.disabled:hover {\n  border-color: transparent;\n}\n\n.edit-modal {\n  .ant-drawer-body {\n    padding: 0;\n  }\n}\n\n.inline-countdown.ant-statistic {\n  display: inline-block;\n\n  .ant-statistic-content {\n    font-size: 14px;\n    padding: 0 3px;\n  }\n}\n\n.ant-form-item-extra {\n  word-break: break-all;\n  font-size: 13px;\n}\n\n/* Change autocomplete styles in WebKit */\ninput:-webkit-autofill,\ninput:-webkit-autofill:hover,\ninput:-webkit-autofill:focus,\ntextarea:-webkit-autofill,\ntextarea:-webkit-autofill:hover,\ntextarea:-webkit-autofill:focus,\nselect:-webkit-autofill,\nselect:-webkit-autofill:hover,\nselect:-webkit-autofill:focus {\n  box-shadow: none;\n  transition: background-color 5000s ease-in-out 0s;\n  -webkit-text-fill-color: @text-color;\n  caret-color: @text-color;\n  color: @text-color;\n}\n\n::placeholder {\n  opacity: 0.5 !important;\n}\n\n.ant-select-selection-placeholder {\n  opacity: 0.5 !important;\n}\n\n.ant-pro-basicLayout-content {\n  margin: 12px;\n\n  .ant-pro-page-container {\n    margin: -12px;\n  }\n\n  .ant-pro-page-container-warp .ant-page-header {\n    border-bottom: 1px solid #eee;\n  }\n\n  .ant-pro-page-container-children-content {\n    margin: 0;\n  }\n}\n\n.ant-pro-global-header {\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);\n}\n\n.ant-menu-item.ant-pro-sider-collapsed-button {\n  margin: 0;\n}\n\n.side-menu-container {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  color: @text-color;\n\n  .side-menu-collapse-button:hover {\n    color: #1890ff;\n  }\n\n  .side-menu-user-wrapper {\n    display: flex;\n    align-items: center;\n  }\n}\n\n.side-menu-user-drop-menu {\n  position: relative;\n  text-align: left;\n  padding: 2px 10px;\n  overflow: auto;\n}\n\n.ant-pro-sider-logo {\n  padding-inline: 5px !important;\n\n  .title {\n    display: flex;\n    height: 32px;\n    margin: 0 5px;\n    font-weight: 600;\n    font-size: 16px;\n    line-height: 32px;\n    vertical-align: middle;\n    animation: pro-layout-title-hide 0.3s;\n\n    a {\n      display: inline-flex;\n      align-items: center;\n    }\n  }\n\n  img {\n    width: 32px !important;\n    border-radius: 52% !important;\n  }\n}\n\n.ant-pro-global-header-logo {\n  a img {\n    // 移动端logo被拉伸\n    width: auto !important;\n  }\n\n  .ant-image {\n    display: inline-flex;\n  }\n}\n\npre {\n  word-break: break-all;\n  white-space: break-spaces;\n  padding: 0;\n}\n\n.virtuallist {\n  .ant-table-tbody > tr > td > div {\n    white-space: unset !important;\n  }\n}\n\n.virtuallist .ant-table-tbody > tr > td > div {\n  box-sizing: border-box;\n  white-space: nowrap;\n  vertical-align: middle;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  width: 100%;\n}\n\n.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {\n  overflow: inherit;\n}\n\n.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {\n  border-right: 1px solid #f0f0f0;\n}\n\n.ant-table-column-title {\n  flex: unset;\n}\n\n.ant-table-column-sorters,\n.ant-table-filter-column {\n  justify-content: unset;\n}\n\ntextarea.ant-input {\n  word-break: break-all;\n}\n\n.ant-pro-sider-collapsed,\nbody[data-mode='phone'] header {\n  .title {\n    display: none;\n  }\n}\n\n.ant-design-pro .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {\n  box-shadow: none;\n  border-right: 1px solid #eee;\n}\n\n.ant-table.ant-table-middle .ant-table-title,\n.ant-table.ant-table-middle .ant-table-footer,\n.ant-table.ant-table-middle .ant-table-thead > tr > th,\n.ant-table.ant-table-middle .ant-table-tbody > tr > td,\n.ant-table.ant-table-middle tfoot > tr > th,\n.ant-table.ant-table-middle tfoot > tr > td {\n  padding: 12px 16px;\n}\n\nbody[data-dark='true'] {\n  .ant-popover-arrow-content {\n    --antd-arrow-background-color: rgb(24, 26, 27);\n  }\n}\n\n.ant-tabs-content-holder {\n  flex: 1;\n  overflow-y: auto;\n}\n"
  },
  {
    "path": "src/layouts/index.tsx",
    "content": "import config from '@/utils/config';\nimport { useCtx, useTheme } from '@/utils/hooks';\nimport { request } from '@/utils/http';\nimport {\n  LogoutOutlined,\n  MenuFoldOutlined,\n  MenuUnfoldOutlined,\n  UserOutlined,\n} from '@ant-design/icons';\nimport ProLayout, { PageLoading } from '@ant-design/pro-layout';\nimport { history, Link, Outlet, useLocation } from '@umijs/max';\nimport * as DarkReader from '@umijs/ssr-darkreader';\nimport { Avatar, Badge, Dropdown, Image, MenuProps, Tooltip } from 'antd';\nimport React, { useEffect, useState } from 'react';\nimport intl from 'react-intl-universal';\nimport vhCheck from 'vh-check';\nimport defaultProps from './defaultProps';\nimport './index.less';\nimport { init } from '../utils/init';\nimport WebSocketManager from '../utils/websocket';\n\nexport interface SharedContext {\n  headerStyle: React.CSSProperties;\n  isPhone: boolean;\n  theme: 'vs' | 'vs-dark';\n  user: any;\n  reloadUser: (needLoading?: boolean) => void;\n  reloadTheme: () => void;\n  systemInfo: TSystemInfo;\n}\n\ninterface TSystemInfo {\n  branch: 'develop' | 'master';\n  isInitialized: boolean;\n  publishTime: number;\n  version: string;\n  changeLog: string;\n  changeLogLink: string;\n}\n\nexport default function () {\n  const location = useLocation();\n  const ctx = useCtx();\n  const { theme, reloadTheme } = useTheme();\n  const [user, setUser] = useState<any>({});\n  const [loading, setLoading] = useState<boolean>(true);\n  const [systemInfo, setSystemInfo] = useState<TSystemInfo>();\n  const [collapsed, setCollapsed] = useState(false);\n  const [initLoading, setInitLoading] = useState<boolean>(true);\n  const {\n    enable: enableDarkMode,\n    disable: disableDarkMode,\n    exportGeneratedCSS: collectCSS,\n    setFetchMethod,\n    auto: followSystemColorScheme,\n  } = DarkReader || {};\n\n  const logout = () => {\n    request.post(`${config.apiPrefix}user/logout`).then(() => {\n      localStorage.removeItem(config.authKey);\n      history.push('/login');\n    });\n  };\n\n  const getSystemInfo = () => {\n    request\n      .get(`${config.apiPrefix}system`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setSystemInfo(data);\n          if (!data.isInitialized) {\n            history.push('/initialization');\n          } else {\n            init(data.version);\n            getUser();\n          }\n        }\n      })\n      .catch((error) => {\n        console.log(error);\n      });\n  };\n\n  const getUser = (needLoading = true) => {\n    needLoading && setLoading(true);\n    request\n      .get(`${config.apiPrefix}user`)\n      .then(({ code, data }) => {\n        if (code === 200 && data.username) {\n          setUser(data);\n          if (location.pathname === '/') {\n            history.push('/crontab');\n          }\n        }\n        needLoading && setLoading(false);\n      })\n      .catch((error) => {\n        console.log(error);\n      });\n  };\n\n  const getHealthStatus = () => {\n    request\n      .get(`${config.apiPrefix}health`)\n      .then((res) => {\n        if (res?.data?.status === 'ok') {\n          getSystemInfo();\n        } else {\n          history.push('/error');\n        }\n      })\n      .catch((error) => {\n        const responseStatus = error.response.status;\n        if (responseStatus !== 401) {\n          history.push('/error');\n        } else {\n          window.location.reload();\n        }\n      })\n      .finally(() => setInitLoading(false));\n  };\n\n  const reloadUser = (needLoading = false) => {\n    getUser(needLoading);\n  };\n\n  useEffect(() => {\n    if (systemInfo && systemInfo.isInitialized && !user) {\n      getUser();\n    }\n  }, [location.pathname]);\n\n  useEffect(() => {\n    getHealthStatus();\n  }, []);\n\n  useEffect(() => {\n    if (theme === 'vs-dark') {\n      document.body.setAttribute('data-dark', 'true');\n    } else {\n      document.body.setAttribute('data-dark', 'false');\n    }\n  }, [theme]);\n\n  useEffect(() => {\n    vhCheck();\n\n    const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';\n    if (typeof window === 'undefined') return;\n    if (typeof window.matchMedia === 'undefined') return;\n    if (!DarkReader) {\n      return () => null;\n    }\n    setFetchMethod(fetch);\n\n    if (_theme === 'dark') {\n      enableDarkMode({});\n    } else if (_theme === 'light') {\n      disableDarkMode();\n    } else {\n      followSystemColorScheme({});\n    }\n\n    return () => {\n      disableDarkMode();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!user || !user.username) return;\n    const ws = WebSocketManager.getInstance(\n      `${window.location.origin}${\n        config.apiPrefix\n      }ws?token=${localStorage.getItem(config.authKey)}`,\n    );\n\n    return () => {\n      ws.close();\n    };\n  }, [user]);\n\n  useEffect(() => {\n    window.onload = () => {\n      const timing = performance.timing;\n      console.log(`白屏时间: ${timing.responseStart - timing.navigationStart}`);\n      console.log(\n        `请求完毕至DOM加载: ${timing.domInteractive - timing.responseEnd}`,\n      );\n      console.log(\n        `解释dom树耗时: ${timing.domComplete - timing.domInteractive}`,\n      );\n      console.log(\n        `从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,\n      );\n    };\n  }, []);\n\n  if (initLoading) {\n    return <PageLoading />;\n  }\n\n  if (['/login', '/initialization', '/error'].includes(location.pathname)) {\n    if (systemInfo?.isInitialized && location.pathname === '/initialization') {\n      history.push('/crontab');\n    }\n\n    if (systemInfo || location.pathname === '/error') {\n      return (\n        <Outlet\n          context={{\n            ...ctx,\n            theme,\n            user,\n            reloadUser,\n            reloadTheme,\n          }}\n        />\n      );\n    }\n  }\n\n  const isFirefox = navigator.userAgent.includes('Firefox');\n  const isSafari =\n    navigator.userAgent.includes('Safari') &&\n    !navigator.userAgent.includes('Chrome');\n  const isQQBrowser = navigator.userAgent.includes('QQBrowser');\n\n  const menu: MenuProps = {\n    items: [\n      {\n        label: intl.get('退出登录'),\n        className: 'side-menu-user-drop-menu',\n        onClick: logout,\n        key: 'logout',\n        icon: <LogoutOutlined />,\n      },\n    ],\n  };\n  return loading ? (\n    <PageLoading />\n  ) : (\n    <ProLayout\n      selectedKeys={[location.pathname]}\n      loading={loading}\n      logo={\n        <>\n          <Image preview={false} src=\"https://qn.whyour.cn/logo.png\" />\n          <div className=\"title\">\n            <span className=\"title\">{intl.get('青龙')}</span>\n            <span\n              onClick={(e) => {\n                e.stopPropagation();\n                window.open(systemInfo?.changeLogLink, '_blank');\n              }}\n            >\n              <Tooltip\n                title={\n                  systemInfo?.branch === 'develop'\n                    ? intl.get('开发版')\n                    : intl.get('正式版')\n                }\n              >\n                <Badge size=\"small\" dot={systemInfo?.branch === 'develop'}>\n                  <span\n                    style={{\n                      fontSize: isFirefox ? 9 : 12,\n                      color: '#666',\n                      marginLeft: 2,\n                      zoom: isSafari ? 0.66 : 0.8,\n                      letterSpacing: isQQBrowser ? -2 : 0,\n                    }}\n                  >\n                    v{systemInfo?.version}\n                  </span>\n                </Badge>\n              </Tooltip>\n            </span>\n          </div>\n        </>\n      }\n      title={false}\n      menuItemRender={(menuItemProps: any, defaultDom: any) => {\n        if (\n          menuItemProps.isUrl ||\n          !menuItemProps.path ||\n          location.pathname === menuItemProps.path\n        ) {\n          return defaultDom;\n        }\n        return <Link to={menuItemProps.path}>{defaultDom}</Link>;\n      }}\n      pageTitleRender={(props, pageName, info) => {\n        const title =\n          (config.documentTitleMap as any)[location.pathname] ||\n          intl.get('未找到');\n        return `${title} - ${intl.get('青龙')}`;\n      }}\n      onCollapse={setCollapsed}\n      collapsed={collapsed}\n      rightContentRender={() =>\n        ctx.isPhone && (\n          <Dropdown menu={menu} placement=\"bottomRight\" trigger={['click']}>\n            <span className=\"side-menu-user-wrapper\">\n              <Avatar\n                shape=\"square\"\n                size=\"small\"\n                icon={<UserOutlined />}\n                src={\n                  user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''\n                }\n              />\n              <span style={{ marginLeft: 5 }}>{user.username}</span>\n            </span>\n          </Dropdown>\n        )\n      }\n      collapsedButtonRender={(collapsed) => (\n        <span\n          className=\"side-menu-container\"\n          onClick={(e) => {\n            e.preventDefault();\n            e.stopPropagation();\n          }}\n        >\n          {!collapsed && !ctx.isPhone && (\n            <Dropdown menu={menu} placement=\"topLeft\" trigger={['hover']}>\n              <span className=\"side-menu-user-wrapper\">\n                <Avatar\n                  shape=\"square\"\n                  size=\"small\"\n                  icon={<UserOutlined />}\n                  src={\n                    user.avatar\n                      ? `${config.apiPrefix}static/${user.avatar}`\n                      : ''\n                  }\n                />\n                <span style={{ marginLeft: 5 }}>{user.username}</span>\n              </span>\n            </Dropdown>\n          )}\n          <span\n            className=\"side-menu-collapse-button\"\n            onClick={() => setCollapsed(!collapsed)}\n          >\n            {collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}\n          </span>\n        </span>\n      )}\n      {...defaultProps}\n    >\n      <Outlet\n        context={{\n          ...ctx,\n          theme,\n          user,\n          reloadUser,\n          reloadTheme,\n          systemInfo,\n        }}\n      />\n    </ProLayout>\n  );\n}\n"
  },
  {
    "path": "src/loading.tsx",
    "content": "import { PageLoading } from '@ant-design/pro-layout';\n\nconst NewPageLoading = () => {\n  return <PageLoading delay={1}></PageLoading>;\n};\n\nexport default NewPageLoading;\n"
  },
  {
    "path": "src/locales/en-US.json",
    "content": "{\n  \"复制成功\": \"Copy successful\",\n  \"复制\": \"Copy\",\n  \"新建\": \"New\",\n  \"登录\": \"Login\",\n  \"初始化\": \"Initialize\",\n  \"错误\": \"Error\",\n  \"定时任务\": \"Scheduled Tasks\",\n  \"订阅管理\": \"Subscription Management\",\n  \"环境变量\": \"Environment Variables\",\n  \"配置文件\": \"Configuration Files\",\n  \"脚本管理\": \"Script Management\",\n  \"依赖管理\": \"Dependency Management\",\n  \"日志管理\": \"Log Management\",\n  \"对比工具\": \"Comparison Tool\",\n  \"系统设置\": \"System Settings\",\n  \"退出登录\": \"Logout\",\n  \"青龙\": \"Qinglong\",\n  \"返回首页\": \"Return to Home\",\n  \"保存\": \"Save\",\n  \"日志\": \"Log\",\n  \"脚本\": \"Script\",\n  \"确认保存文件\": \"Confirm to Save File\",\n  \"，保存后不可恢复\": \", it can't be recovered after saving.\",\n  \"确认运行\": \"Confirm to Run\",\n  \"确认运行定时任务\": \"Confirm to Run Scheduled Task\",\n  \"吗\": \"?\",\n  \"确认停止\": \"Confirm to Stop\",\n  \"确认停止定时任务\": \"Confirm to Stop Scheduled Task\",\n  \"确认\": \"Confirm\",\n  \"任务\": \"Task\",\n  \"状态\": \"Status\",\n  \"空闲中\": \"Idle\",\n  \"运行中\": \"Running\",\n  \"队列中\": \"In Queue\",\n  \"已禁用\": \"Disabled\",\n  \"定时\": \"Schedule\",\n  \"最后运行时间\": \"Last Run Time\",\n  \"最后运行时长\": \"Last Run Duration\",\n  \"下次运行时间\": \"Next Run Time\",\n  \"名称\": \"Name\",\n  \"命令/脚本\": \"Command/Script\",\n  \"定时规则\": \"Schedule Rule\",\n  \"操作\": \"Action\",\n  \"确认删除\": \"Confirm to Delete\",\n  \"确认删除定时任务\": \"Confirm to Delete Scheduled Task\",\n  \"编辑\": \"Edit\",\n  \"删除\": \"Delete\",\n  \"确认删除选中的定时任务吗\": \"Confirm to delete the selected scheduled tasks?\",\n  \"选中的定时任务吗\": \"selected scheduled tasks?\",\n  \"创建视图\": \"Create View\",\n  \"视图管理\": \"View Management\",\n  \"请输入名称或者关键词\": \"Please enter a name or keyword\",\n  \"创建任务\": \"Create Task\",\n  \"更多\": \"More\",\n  \"批量删除\": \"Batch Delete\",\n  \"批量启用\": \"Batch Enable\",\n  \"批量禁用\": \"Batch Disable\",\n  \"批量运行\": \"Batch Run\",\n  \"批量停止\": \"Batch Stop\",\n  \"批量置顶\": \"Batch Top\",\n  \"批量取消置顶\": \"Batch Un-top\",\n  \"批量修改标签\": \"Batch Modify Tags\",\n  \"已选择\": \"Selected\",\n  \"项\": \"items\",\n  \"知道了\": \"Got it\",\n  \"请输入任务名称\": \"Please enter the task name\",\n  \"支持输入脚本路径/任意系统可执行命令/task 脚本路径\": \"Supports input of script paths / any system executable commands / task script paths\",\n  \"秒(可选) 分 时 天 月 周\": \"Seconds (optional) Minutes Hours Day Month Week\",\n  \"标签\": \"Tags\",\n  \"取消\": \"Cancel\",\n  \"添加\": \"Add\",\n  \"启用\": \"Enable\",\n  \"禁用\": \"Disable\",\n  \"运行\": \"Run\",\n  \"停止\": \"Stop\",\n  \"置顶\": \"Top\",\n  \"取消置顶\": \"Un-top\",\n  \"命令\": \"Command\",\n  \"包含\": \"Include\",\n  \"不包含\": \"Exclude\",\n  \"属于\": \"Belong to\",\n  \"不属于\": \"Not Belong to\",\n  \"顺序\": \"Order\",\n  \"倒序\": \"Reverse\",\n  \"且\": \"And\",\n  \"或\": \"Or\",\n  \"输入后回车增加自定义选项\": \"Press Enter to add custom options\",\n  \"视图名称\": \"View Name\",\n  \"请输入视图名称\": \"Please enter the view name\",\n  \"请输入内容\": \"Please enter the content\",\n  \"新增筛选条件\": \"Add Filter\",\n  \"新增排序方式\": \"Add Sort\",\n  \"类型\": \"Type\",\n  \"显示\": \"Display\",\n  \"确认删除视图\": \"Confirm to delete the view\",\n  \"安装中\": \"Installing\",\n  \"已安装\": \"Installed\",\n  \"安装失败\": \"Installation Failed\",\n  \"删除中\": \"Deleting\",\n  \"已删除\": \"Deleted\",\n  \"删除失败\": \"Deletion Failed\",\n  \"已取消\": \"Cancelled\",\n  \"序号\": \"Number\",\n  \"备注\": \"Remarks\",\n  \"更新时间\": \"Update Time\",\n  \"创建时间\": \"Created Time\",\n  \"确认删除依赖\": \"Confirm to delete the dependency\",\n  \"确认重新安装\": \"Confirm to reinstall\",\n  \"确认取消安装\": \"Confirm to cancel install\",\n  \"确认删除选中的依赖吗\": \"Confirm to delete the selected dependencies?\",\n  \"确认重新安装选中的依赖吗\": \"Confirm to reinstall the selected dependencies?\",\n  \"请输入名称\": \"Please enter a name\",\n  \"创建依赖\": \"Create Dependency\",\n  \"批量安装\": \"Batch Install\",\n  \"批量强制删除\": \"Batch Force Delete\",\n  \"日志 -\": \"Log -\",\n  \"依赖类型\": \"Dependency Type\",\n  \"自动拆分\": \"Auto Split\",\n  \"多个依赖是否换行分割\": \"Whether to separate multiple dependencies with new lines\",\n  \"是\": \"Yes\",\n  \"否\": \"No\",\n  \"请输入依赖名称，支持指定版本\": \"Please enter the dependency name, version specification is supported\",\n  \"请输入依赖名称\": \"Please enter the dependency name\",\n  \"请输入备注\": \"Please enter remarks\",\n  \"源文件\": \"Source File\",\n  \"当前文件\": \"Current File\",\n  \"修改环境变量名称\": \"Modify Environment Variable Name\",\n  \"请输入新的环境变量名称\": \"Please enter the new environment variable name\",\n  \"已启用\": \"Enabled\",\n  \"值\": \"Value\",\n  \"确认删除变量\": \"Confirm to delete the variable\",\n  \"确认删除选中的变量吗\": \"Confirm to delete the selected variables?\",\n  \"选中的变量吗\": \"selected variables?\",\n  \"请输入名称/值/备注\": \"Please enter name/value/remarks\",\n  \"导入\": \"Import\",\n  \"创建变量\": \"Create Variable\",\n  \"批量修改变量名称\": \"Batch Modify Variable Names\",\n  \"批量导出\": \"Batch Export\",\n  \"请输入环境变量名称\": \"Please enter the environment variable name\",\n  \"只能输入字母数字下划线，且不能以数字开头\": \"Only letters, numbers, and underscores are allowed, and cannot start with a number\",\n  \"请输入环境变量值\": \"Please enter the environment variable value\",\n  \"服务启动超时\": \"Service startup timeout\",\n  \"请先按如下方式修复：\": \"Please fix it as follows:\",\n  \"1. 宿主机执行 docker run --rm -v\\n                  /var/run/docker.sock:/var/run/docker.sock\\n                  containrrr/watchtower -cR <容器名>\": \"1. Execute 'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR <container_name>' on the host machine\",\n  \"2. 容器内执行 ql check、ql update\": \"2. Execute 'ql check' and 'ql update' inside the container\",\n  \"3. 如果无法解决，容器内执行 pm2 logs，拷贝执行结果\": \"3. If the problem persists, execute 'pm2 logs' inside the container and copy the results\",\n  \"提交 issue\": \"Submit an issue\",\n  \"启动中，请稍后...\": \"Starting, please wait...\",\n  \"欢迎使用\": \"Welcome to use\",\n  \"欢迎使用青龙\": \"Welcome to use Qinglong\",\n  \"支持python3、javascript、shell、typescript 的定时任务管理面板\": \"A scheduling task management panel that supports python3, javascript, shell, and typescript\",\n  \"开始安装\": \"Start Installation\",\n  \"账户设置\": \"Account Settings\",\n  \"用户名\": \"Username\",\n  \"密码\": \"Password\",\n  \"密码不能为admin\": \"The password cannot be 'admin'\",\n  \"确认密码\": \"Confirm Password\",\n  \"您输入的两个密码不匹配！\": \"The two passwords you entered do not match!\",\n  \"提交\": \"Submit\",\n  \"通知设置\": \"Notification Settings\",\n  \"通知方式\": \"Notification Method\",\n  \"请选择通知方式\": \"Please select a notification method\",\n  \"跳过\": \"Skip\",\n  \"完成安装\": \"Installation Completed\",\n  \"恭喜安装完成！\": \"Congratulations, the installation is completed!\",\n  \"Telegram频道\": \"Telegram Channel\",\n  \"去登录\": \"Go to Login\",\n  \"初始化配置\": \"Initialize Configuration\",\n  \"文件\": \"File\",\n  \"，删除后不可恢复\": \", it can't be recovered after deletion\",\n  \"请选择日志\": \"Please select a log\",\n  \"请输入日志名\": \"Please enter the log name\",\n  \"暂无日志\": \"No logs available\",\n  \"登录成功！\": \"Login successful!\",\n  \"上次登录时间：\": \"Last login time: \",\n  \"上次登录地点：\": \"Last login location: \",\n  \"上次登录IP：\": \"Last login IP: \",\n  \"上次登录设备：\": \"Last login device: \",\n  \"上次登录状态：\": \"Last login status: \",\n  \"验证码\": \"Verification Code\",\n  \"验证码为6位数字\": \"Verification code is a 6-digit number\",\n  \"6位数字\": \"6-digit number\",\n  \"验证\": \"Verify\",\n  \"请\": \"Please\",\n  \"秒后重试\": \"Retry after seconds\",\n  \"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。\": \"Open the two-factor authentication application on your device to view your authentication code and verify your identity.\",\n  \"请选择脚本文件\": \"Please select a script file\",\n  \"当前文件不支持预览\": \"Current file type is not supported for preview\",\n  \"清空日志\": \"Clear Logs\",\n  \"设置\": \"Settings\",\n  \"退出\": \"Exit\",\n  \"空文件\": \"Empty File\",\n  \"本地文件\": \"Local File\",\n  \"文件夹\": \"Folder\",\n  \"文件名\": \"File Name\",\n  \"请输入文件名\": \"Please enter the file name\",\n  \"文件名不能包含斜杠\": \"File names cannot contain slashes\",\n  \"文件夹名\": \"Folder Name\",\n  \"请输入文件夹名\": \"Please enter the folder name\",\n  \"父目录\": \"Parent Directory\",\n  \"请选择父目录\": \"Please select a parent directory\",\n  \"点击或者拖拽文件到此区域上传\": \"Click or drag files here to upload\",\n  \"当前修改未保存，确定离开吗\": \"The current changes are not saved. Are you sure you want to leave?\",\n  \"退出编辑\": \"Exit Editing\",\n  \"重命名\": \"Rename\",\n  \"请选择脚本\": \"Please select a script\",\n  \"调试\": \"Debug\",\n  \"请输入脚本名\": \"Please enter the script name\",\n  \"暂无脚本\": \"No scripts available\",\n  \"请输入新名称\": \"Please enter a new name\",\n  \"保存文件\": \"Save File\",\n  \"保存目录\": \"Save Directory\",\n  \"请输入保存目录，默认scripts目录\": \"Please enter the save directory, default is 'scripts'\",\n  \"运行设置\": \"Run Settings\",\n  \"待开发\": \"To Be Developed\",\n  \"开发版\": \"Developer Edition\",\n  \"正式版\": \"Official Edition\",\n  \"版本\": \"Version\",\n  \"更新日志\": \"Changelog\",\n  \"查看\": \"View\",\n  \"提交BUG\": \"Submit Bug\",\n  \"名称不能为保留关键字\": \"The name cannot be a reserved keyword\",\n  \"请输入应用名称\": \"Please enter the application name\",\n  \"权限\": \"Permission\",\n  \"请选择模块权限\": \"Please select module permissions\",\n  \"更新\": \"Update\",\n  \"已经是最新版了！\": \"It is already the latest version!\",\n  \"是目前检测到的最新可用版本了。\": \"It is the latest available version currently detected.\",\n  \"重新下载\": \"Redownload\",\n  \"更新可用\": \"Update Available\",\n  \"新版本\": \"Create Version\",\n  \"可用，你使用的版本为\": \"available, the version you are using is\",\n  \"下载更新\": \"Download Update\",\n  \"以后再说\": \"Later\",\n  \"下载更新中...\": \"Downloading Update...\",\n  \"确认重启\": \"Confirm to Restart\",\n  \"系统安装包下载成功，确认重启\": \"System installation package downloaded successfully, confirm to restart\",\n  \"重启\": \"Restart\",\n  \"系统将在\": \"The system will restart in\",\n  \"秒后自动刷新\": \"seconds and automatically refresh\",\n  \"检查更新\": \"Check for Updates\",\n  \"重新启动\": \"Reboot\",\n  \"确认删除应用\": \"Confirm to delete the application\",\n  \"确认重置\": \"Confirm to reset\",\n  \"确认重置应用\": \"Confirm to reset the application\",\n  \"的Secret吗\": \"'s Secret?\",\n  \"重置Secret会让当前应用所有token失效\": \"Resetting the Secret will invalidate all tokens for the current application\",\n  \"创建应用\": \"Create Application\",\n  \"安全设置\": \"Security Settings\",\n  \"应用设置\": \"Application Settings\",\n  \"登录日志\": \"Login Logs\",\n  \"其他设置\": \"Other Settings\",\n  \"关于\": \"About\",\n  \"成功\": \"Successfully\",\n  \"失败\": \"Failure\",\n  \"登录时间\": \"Login Time\",\n  \"登录地址\": \"Login Address\",\n  \"登录IP\": \"Login IP\",\n  \"登录设备\": \"Login Device\",\n  \"登录状态\": \"Login Status\",\n  \"亮色\": \"Light\",\n  \"暗色\": \"Dark\",\n  \"跟随系统\": \"Follow System\",\n  \"备份数据上传成功，确认覆盖数据\": \"Data backup uploaded successfully, confirm data overwrite\",\n  \"主题设置\": \"Theme Settings\",\n  \"日志删除频率\": \"Log Deletion Frequency\",\n  \"每x天自动删除x天以前的日志\": \"Automatically delete logs older than x days every x days\",\n  \"每\": \"Every\",\n  \"天\": \"day(s)\",\n  \"定时任务并发数\": \"Concurrent Scheduled Tasks\",\n  \"数据备份还原\": \"Data Backup & Restore\",\n  \"还原数据\": \"Restore Data\",\n  \"第一步\": \"Step 1\",\n  \"下载两步验证手机应用，比如 Google Authenticator 、\": \"Download a two-factor authentication mobile app, like Google Authenticator,\",\n  \"第二步\": \"Step 2\",\n  \"使用手机应用扫描二维码，或者输入秘钥\": \"Scan the QR code with the mobile app or enter the key\",\n  \"第三步\": \"Step 3\",\n  \"输入手机应用上的6位数字\": \"Enter the 6-digit code from the mobile app\",\n  \"完成设置\": \"Finish Setup\",\n  \"修改用户名密码\": \"Change Username and Password\",\n  \"两步验证\": \"Two-Factor Authentication\",\n  \"头像\": \"Profile Picture\",\n  \"更换头像\": \"Change Profile Picture\",\n  \"时\": \"hour(s)\",\n  \"分\": \"minute(s)\",\n  \"秒\": \"second(s)\",\n  \"私有仓库\": \"Private Repository\",\n  \"公开仓库\": \"Public Repository\",\n  \"单文件\": \"Single File\",\n  \"链接\": \"Link\",\n  \"分支\": \"Branch\",\n  \"确认删除定时订阅\": \"Confirm Deletion of Scheduled Subscription\",\n  \"定时订阅\": \"Scheduled Subscription\",\n  \"创建订阅\": \"Create Subscription\",\n  \"私钥\": \"Private Key\",\n  \"请输入私钥\": \"Please enter the private key\",\n  \"请输入认证用户名\": \"Please enter the authentication username\",\n  \"Github已不支持密码认证，请使用Token方式\": \"Github no longer supports password authentication. Please use the Token method.\",\n  \"密码/Token\": \"Password/Token\",\n  \"请输入密码或者Token\": \"Please enter the password or Token\",\n  \"支持拷贝 ql repo/raw 命令，粘贴导入\": \"Supports copying ql repo/raw command for import\",\n  \"请输入订阅链接\": \"Please enter the subscription link\",\n  \"请输入分支\": \"Please enter the branch\",\n  \"唯一值\": \"Unique Value\",\n  \"唯一值用于日志目录和私钥别名\": \"Unique value used for log directory and private key alias\",\n  \"自动生成\": \"Auto-generated\",\n  \"拉取方式\": \"Pull Method\",\n  \"用户名密码/Token\": \"Username/Password or Token\",\n  \"定时类型\": \"Schedule Type\",\n  \"白名单\": \"Whitelist\",\n  \"多个关键词竖线分割，支持正则表达式\": \"Multiple keywords separated by vertical lines (|), supports regular expressions\",\n  \"请输入脚本筛选白名单关键词，多个关键词竖线分割\": \"Please enter script filtering whitelist keywords, multiple keywords separated by vertical lines (|)\",\n  \"黑名单\": \"Blacklist\",\n  \"请输入脚本筛选黑名单关键词，多个关键词竖线分割\": \"Please enter script filtering blacklist keywords, multiple keywords separated by vertical lines (|)\",\n  \"依赖文件\": \"Dependency Files\",\n  \"请输入脚本依赖文件关键词，多个关键词竖线分割\": \"Please enter script dependency file keywords, multiple keywords separated by vertical lines (|)\",\n  \"文件后缀\": \"File Extension\",\n  \"仓库需要拉取的文件后缀，多个后缀空格分隔，默认使用配置文件中的RepoFileExtensions\": \"Repository requires pulling specific file extensions, multiple extensions separated by spaces, uses RepoFileExtensions from the configuration file by default\",\n  \"请输入文件后缀\": \"Please enter the file extension\",\n  \"执行前\": \"Before Execution\",\n  \"运行订阅前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"Run commands before executing the subscription, e.g., cp/mv/python3 xxx.py/node xxx.js\",\n  \"请输入运行订阅前要执行的命令\": \"Please enter the command to run before executing the subscription\",\n  \"执行后\": \"After Execution\",\n  \"运行订阅后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"Run commands after executing the subscription, e.g., cp/mv/python3 xxx.py/node xxx.js\",\n  \"请输入运行订阅后要执行的命令\": \"Please enter the command to run after executing the subscription\",\n  \"代理\": \"Proxy\",\n  \"公开仓库支持HTTP/SOCK5代理，私有仓库支持SOCK5代理\": \"Public repositories support HTTP/SOCK5 proxies, private repositories support SOCK5 proxies\",\n  \"自动添加任务\": \"Automatically Add Tasks\",\n  \"自动删除任务\": \"Automatically Delete Tasks\",\n  \"中文\": \"Chinese\",\n  \"系统信息\": \"System Information\",\n  \"Server酱\": \"ServerChan\",\n  \"Telegram机器人\": \"Telegram Bot\",\n  \"钉钉机器人\": \"DingTalk Bot\",\n  \"企业微信机器人\": \"WeChat Work Bot\",\n  \"企业微信应用\": \"WeChat Work App\",\n  \"智能微秘书\": \"Smart WeChat Assistant\",\n  \"群晖chat\": \"Synology Chat\",\n  \"微加机器人\": \"WePlusBot\",\n  \"邮箱\": \"Email\",\n  \"飞书机器人\": \"Feishu Bot\",\n  \"自定义通知\": \"Custom Notification\",\n  \"已关闭\": \"Disabled\",\n  \"gotify的url地址，例如 https://push.example.de:8080\": \"gotify URL address, e.g., https://push.example.de:8080\",\n  \"gotify的消息应用token码\": \"gotify message application token code\",\n  \"推送消息的优先级\": \"Priority of Push Messages\",\n  \"synologyChat的url地址\": \"Synology Chat Webhook URL address\",\n  \"chat的token码\": \"Chat token code\",\n  \"推送到个人QQ: http://127.0.0.1/send_private_msg，群：http://127.0.0.1/send_group_msg\": \"Push to personal QQ: http://127.0.0.1/send_private_msg, group: http://127.0.0.1/send_group_msg\",\n  \"访问密钥\": \"Access Key\",\n  \"如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群\": \"If GOBOT_URL is set to /send_private_msg, enter user_id=personal QQ; if set to /send_group_msg, enter group_id=QQ group\",\n  \"Server酱SENDKEY\": \"ServerChan SENDKEY, https://sct.ftqq.com/r/13363\",\n  \"PushDeer的Key，https://github.com/easychen/pushdeer\": \"PushDeer Key, https://github.com/easychen/pushdeer\",\n  \"PushDeer的自架API endpoint，默认是 https://api2.pushdeer.com/message/push\": \"PushDeer's self-hosted API endpoint, default is https://api2.pushdeer.com/message/push\",\n  \"Bark的信息IP/设备码，例如：https://api.day.app/XXXXXXXX\": \"Bark information IP/device code, e.g., https://api.day.app/XXXXXXXX\",\n  \"BARK推送图标，自定义推送图标 (需iOS15或以上才能显示)\": \"BARK push icon, custom push icon (requires iOS 15 or above to display)\",\n  \"BARK推送铃声，铃声列表去APP查看复制填写\": \"BARK push ringtone, check and copy from the APP's ringtone list\",\n  \"BARK推送消息的分组，默认为qinglong\": \"BARK push message grouping, default is qinglong\",\n  \"BARK推送消息的时效性，默认为active\": \"BARK push message redirecting URL\",\n  \"BARK推送消息的跳转URL\": \"BARK push message grouping, default is qinglong\",\n  \"BARK是否保存推送消息\": \"Does BARK save push messages\",\n  \"telegram机器人的token，例如：1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw\": \"Telegram Bot token, e.g., 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw\",\n  \"telegram用户的id，例如：129xxx206\": \"Telegram user ID, e.g., 129xxx206\",\n  \"代理IP\": \"Proxy IP\",\n  \"代理端口\": \"Proxy Port\",\n  \"telegram代理配置认证参数，用户名与密码用英文冒号连接 user:password\": \"Telegram proxy configuration authentication parameters, connect username and password with a colon, e.g., user:password\",\n  \"telegram api自建的反向代理地址，默认tg官方api\": \"Telegram API's self-built reverse proxy address, default is official tg API\",\n  \"钉钉机器人webhook token，例如：5a544165465465645d0f31dca676e7bd07415asdasd\": \"DingTalk Bot webhook token, e.g., 5a544165465465645d0f31dca676e7bd07415asdasd\",\n  \"密钥，机器人安全设置页面，加签一栏下面显示的SEC开头的字符串\": \"Secret key, shown below the signing section on the robot's security settings page, starts with SEC\",\n  \"企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)，例如：693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa\": \"WeChat Work Bot webhook (see documentation at https://work.weixin.qq.com/api/doc/90000/90136/91770), e.g., 693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa\",\n  \"企业微信代理地址\": \"WeChat Work Proxy Address\",\n  \"corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填，不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号)，例如：wwcfrs,B-76WERQ,qinglong,1000001,2COat\": \"corpid, corpsecret, touser (note: separate multiple member IDs with |), agentid, message type (optional, defaults to text message type) – separated by commas (`,`), e.g., wwcfrs, B-76WERQ, qinglong, 1000001, 2COat\",\n  \"密钥key，智能微秘书个人中心获取apikey，申请地址：https://wechat.aibotk.com/signup?from=ql\": \"Key, obtain the API key from the Smart WeChat Assistant's personal center, apply at: https://wechat.aibotk.com/signup?from=ql\",\n  \"发送的目标，群组或者好友\": \"Recipient, group, or friend\",\n  \"请输入要发送的目标\": \"Please enter the recipient's name (group or friend)\",\n  \"群聊\": \"Group Chat\",\n  \"好友\": \"Friend\",\n  \"要发送的用户昵称或群名，如果目标是群，需要填群名，如果目标是好友，需要填好友昵称\": \"Enter the recipient's nickname if it's a friend or the group name if it's a group\",\n  \"iGot的信息推送key，例如：https://push.hellyw.com/XXXXXXXX\": \"iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX\",\n  \"微信扫码登录后一对一推送或一对多推送下面的token(您的Token)，不提供PUSH_PLUS_USER则默认为一对一推送，参考 https://www.pushplus.plus/\": \"After WeChat scan login, one-to-one or one-to-many push using the provided token (your Token). If PUSH_PLUS_USER is not provided, it defaults to one-to-one push. See reference at https://www.pushplus.plus/\",\n  \"一对多推送的“群组编码”（一对多推送下面->您的群组(如无则创建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）\": \"The 'group code' for one-to-many push (one-to-many push -> your group (if none, create one) -> group code). If you are the creator of the group, you need to click 'View QR code' to scan and bind, otherwise, you won't receive group messages.\",\n  \"发送模板\": \"send template, can use type: 'html,txt,json,markdown,cloudMonitor,jenkins,route,pay'\",\n  \"发送渠道\": \"send channel, can use type: 'wechat,webhook,cp,mail,sms'\",\n  \"webhook编码\": \"webhook code\",\n  \"发送结果回调地址\": \"send result callback url\",\n  \"好友令牌\": \"friend token\",\n  \"用户令牌，扫描登录后 我的—>设置->令牌 中获取，参考 https://www.weplusbot.com/\": \"Token, which can be obtained after scanning and logging in, is available under 'My Account' -> 'Settings' -> 'Tokens'. Please refer to the instructions for detailed steps: https://www.weplusbot.com/\",\n  \"消息接收人\": \"message recipient\",\n  \"调用版本；专业版填写pro，个人版填写personal，为空默认使用专业版\": \"Version, you can specify 'pro' for the Professional version and 'personal' for the Personal version. If left blank, it will default to the Professional version.\",\n  \"飞书群组机器人：https://www.feishu.cn/hc/zh-CN/articles/360024984973\": \"Feishu group bot: https://www.feishu.cn/hc/zh-CN/articles/360024984973\",\n  \"飞书群组机器人加签密钥，安全设置中开启签名校验后获得\": \"Feishu group bot signature secret, obtained after enabling signature verification in security settings\",\n  \"邮箱服务名称，比如126、163、Gmail、QQ等，支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json\": \"Email service name, e.g., 126, 163, Gmail, QQ, etc. Supported list: https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json\",\n  \"邮箱地址\": \"Email Address\",\n  \"SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\": \"The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions\",\n  \"PushMe的Key，https://push.i-i.me/\": \"PushMe key, https://push.i-i.me/\",\n  \"自建的PushMeServer消息接口地址，例如：http://127.0.0.1:3010，不填则使用官方消息接口\": \"The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface\",\n  \"ntfy的url地址，例如 https://ntfy.sh\": \"The URL address of ntfy, for example, https://ntfy.sh.\",\n  \"ntfy应用topic\": \"The topic for ntfy's application.\",\n  \"ntfy应用token\": \"The token for ntfy's application, see https://docs.ntfy.sh/config/#access-tokens\",\n  \"ntfy应用用户名\": \"The username for ntfy's application, see https://docs.ntfy.sh/config/#users-and-roles\",\n  \"ntfy应用密码\": \"The password for ntfy's application, see https://docs.ntfy.sh/config/#users-and-roles\",\n  \"ntfy用户动作\": \"The user actions for ntfy's application, up to three actions, see https://docs.ntfy.sh/publish/?h=actions#action-buttons\",\n  \"wxPusherBot的appToken\": \"wxPusherBot's appToken, obtain according to docs https://wxpusher.zjiecode.com/docs/\",\n  \"wxPusherBot的topicIds\": \"wxPusherBot's topicIds, at least one of topicIds or uids must be configured\",\n  \"wxPusherBot的uids\": \"wxPusherBot's uids, at least one of topicIds or uids must be configured\",\n  \"请求方法\": \"Request Method\",\n  \"请求头Content-Type\": \"Request Header Content-Type\",\n  \"请求链接以http或者https开头。url或者body中必须包含$title，$content可选，对应api内容的位置\": \"Request URL should start with http or https. URL or body must contain $title, $content is optional and corresponds to the API content position.\",\n  \"请求头格式Custom-Header1: Header1，多个换行分割\": \"Request header format: Custom-Header1: Header1 (separate multiple headers with line breaks)\",\n  \"请求体格式key1: value1，多个换行分割。url或者body中必须包含$title，$content可选，对应api内容的位置\": \"Request body format: key1: value1 (separate multiple keys with line breaks). URL or body must contain $title, $content is optional and corresponds to the API content position.\",\n  \"错误日志\": \"Error Log\",\n  \"执行结束\": \"Execution Finished\",\n  \"备份\": \"Backup\",\n  \"生成数据中...\": \"Generating data...\",\n  \"请选择日志文件\": \"Please select a log file\",\n  \"筛选条件\": \"Filter Conditions\",\n  \"系统\": \"System\",\n  \"个人\": \"Personal\",\n  \"重新安装\": \"Reinstall\",\n  \"取消安装\": \"Cancel Install\",\n  \"强制删除\": \"Force Delete\",\n  \"全部任务\": \"All Tasks\",\n  \"关联订阅\": \"Associate Subscription\",\n  \"订阅\": \"Subscription\",\n  \"创建\": \"Create\",\n  \"同时删除关联任务和脚本\": \"Delete associated tasks and scripts as well\",\n  \"未找到\": \"Not Found\",\n  \"保存成功\": \"Saved successfully\",\n  \"删除成功\": \"Deleted successfully\",\n  \"批量删除成功\": \"Batch deleted successfully\",\n  \"启动中...\": \"Starting...\",\n  \"任务未运行\": \"Task not running\",\n  \"更新任务成功\": \"Task updated successfully\",\n  \"创建任务成功\": \"Task created successfully\",\n  \"编辑任务\": \"Edit Task\",\n  \"Cron表达式格式有误\": \"Incorrect Cron Expression Format\",\n  \"添加Labels成功\": \"Labels added successfully\",\n  \"删除Labels成功\": \"Labels deleted successfully\",\n  \"编辑视图\": \"Edit View\",\n  \"排序方式\": \"Sort Order\",\n  \"开始时间\": \"Start Time\",\n  \"安装\": \"Install\",\n  \"结束时间\": \"End Time\",\n  \"编辑依赖\": \"Edit Dependency\",\n  \"更新环境变量名称成功\": \"Environment Variable Name updated successfully\",\n  \"更新变量成功\": \"Variable updated successfully\",\n  \"创建变量成功\": \"Variable created successfully\",\n  \"编辑变量\": \"Edit Variable\",\n  \"加载中...\": \"Loading...\",\n  \"夹下所有日志\": \"Folder and All Subfiles\",\n  \"创建文件夹成功\": \"Folder created successfully\",\n  \"创建文件成功\": \"File created successfully\",\n  \"夹及其子文件\": \"Folder and Its Subfiles\",\n  \"更新名称成功\": \"Name updated successfully\",\n  \"保存文件成功\": \"File saved successfully\",\n  \"更新应用成功\": \"Application updated successfully\",\n  \"创建应用成功\": \"Application created successfully\",\n  \"编辑应用\": \"Edit Application\",\n  \"检查更新中...\": \"Checking for updates...\",\n  \"失败，请检查\": \"Failed, please check\",\n  \"更新失败，请检查网络及日志或稍后再试\": \"Update failed, please check network and logs or try again later\",\n  \"更新包下载成功\": \"Update package download successful\",\n  \"重置secret\": \"Reset secret\",\n  \"重置成功\": \"Reset successful\",\n  \"通知发送成功\": \"Notification sent successfully\",\n  \"通知关闭成功\": \"Notification closed successfully\",\n  \"测试中...\": \"Testing...\",\n  \"上传\": \"Upload\",\n  \"下载\": \"Download\",\n  \"更新成功\": \"Update successful\",\n  \"激活成功\": \"Activation successful\",\n  \"验证失败\": \"Validation failed\",\n  \"更新订阅成功\": \"Subscription updated successfully\",\n  \"创建订阅成功\": \"Subscription created successfully\",\n  \"编辑订阅\": \"Edit Subscription\",\n  \"Subscription表达式格式有误\": \"Incorrect Subscription Expression Format\",\n  \"一对多推送的“群组编码”（一对多推送下面->您的群组(如无则新建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）\": \"The 'Group Code' for One-to-Many Push (Below One-to-Many Push->Your Group (if not, create one)->Group Code, if you are the creator of the group, you also need to click 'View QR Code' to scan and bind, otherwise you cannot receive group messages)\",\n  \"登录已过期，请重新登录\": \"Login session has expired, please log in again\",\n  \"系统日志\": \"System Logs\",\n  \"主题\": \"Theme\",\n  \"语言\": \"Language\",\n  \"中...\": \"ing...\",\n  \"请选择操作符\": \"Please select operator\",\n  \"新增定时规则\": \"Add Timing Rules\",\n  \"运行任务前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"Run commands before executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js\",\n  \"运行任务后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"Run commands after executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js\",\n  \"请输入运行任务前要执行的命令，不能包含 task 命令\": \"Please enter the command to run before executing the task, cannot contain task commands\",\n  \"请输入运行任务后要执行的命令，不能包含 task 命令\": \"Please enter the command to run after executing the task, cannot contain task commands\",\n  \"不能包含 task 命令\": \"Cannot contain task commands\",\n  \"Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/\": \"Connection address of the Chronocat Red service https://chronocat.vercel.app/install/docker/official/\",\n  \"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如：user_id=xxx;group_id=xxxx;group_id=xxxxx\": \"Individuals: user_id=individual QQ Groups fill in group_id=QQ Groups more than one with English; separated by the same time to support individuals and groups such as: user_id=xxx;group_id=xxxx;group_id=xxxxx\",\n  \"docker安装在持久化config目录下的chronocat.yml文件可找到\": \"The docker installation can be found in the persistence config directory in the chronocat.yml file\",\n  \"请选择\": \"Please select\",\n  \"请输入\": \"Please input\",\n  \"依赖设置\": \"Dependence Settings\",\n  \"Node 软件包镜像源\": \"Node Software Package Mirror Source\",\n  \"Python 软件包镜像源\": \"Python Software Package Mirror Source\",\n  \"Linux 软件包镜像源\": \"Linux Software Package Mirror Source\",\n  \"代理与镜像源二选一即可\": \"Either Proxy or Mirror Source can be chosen\",\n  \"代理地址, 支持HTTP(S)/SOCK5\": \"Proxy Address, supports HTTP(S)/SOCK5\",\n  \"NPM 镜像源\": \"NPM Mirror Source\",\n  \"PyPI 镜像源\": \"PyPI Mirror Source\",\n  \"alpine linux 镜像源\": \"Alpine Linux Mirror Source\",\n  \"如果恢复失败，可进入容器执行\": \"If recovery fails, you can enter the container and execute\",\n  \"常规定时\": \"Normal Timing\",\n  \"手动运行\": \"Manual Run\",\n  \"开机运行\": \"Boot Run\",\n  \"时区\": \"Timezone\",\n  \"强制打开\": \"Force Open\",\n  \"强制打开可能会导致编辑器显示异常\": \"Force opening may cause display issues in the editor\",\n  \"确认离开\": \"Confirm Leave\",\n  \"当前文件未保存，确认离开吗\": \"Current file is not saved, are you sure to leave?\",\n  \"收件邮箱地址，多个分号分隔，默认发送给发件邮箱地址\": \"Receiving email address, multiple semicolon separated, sent to the sending email address by default\",\n  \"选择备份模块\": \"Select backup module\",\n  \"开始备份\": \"Start backup\",\n  \"基础数据\": \"Basic data\",\n  \"脚本文件\": \"Script files\",\n  \"日志文件\": \"Log files\",\n  \"依赖缓存\": \"Dependency cache\",\n  \"远程脚本缓存\": \"Remote script cache\",\n  \"远程仓库缓存\": \"Remote repository cache\",\n  \"SSH 文件缓存\": \"SSH file cache\",\n  \"清除依赖缓存\": \"Clean dependency cache\",\n  \"清除成功\": \"Clean successful\",\n  \"日志名称\": \"Log Name\",\n  \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成\": \"Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate\",\n  \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持绝对路径如 /dev/null\": \"Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports absolute paths like /dev/null\",\n  \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持 /dev/null 丢弃日志，其他绝对路径必须在日志目录内\": \"Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports /dev/null to discard logs, other absolute paths must be within log directory\",\n  \"请输入自定义日志文件夹名称\": \"Please enter a custom log folder name\",\n  \"请输入自定义日志文件夹名称或绝对路径\": \"Please enter a custom log folder name or absolute path\",\n  \"请输入自定义日志文件夹名称或 /dev/null\": \"Please enter a custom log folder name or /dev/null\",\n  \"日志名称只能包含字母、数字、下划线和连字符\": \"Log name can only contain letters, numbers, underscores and hyphens\",\n  \"日志名称不能超过100个字符\": \"Log name cannot exceed 100 characters\",\n  \"未启用\": \"Not enabled\",\n  \"默认为 CPU 个数\": \"Default is the number of CPUs\",\n  \"Minimum is 4\": \"Minimum is 4\",\n  \"实例模式\": \"Instance Mode\",\n  \"单实例模式：定时启动新任务前会自动停止旧任务；多实例模式：允许同时运行多个任务实例\": \"Single instance mode: automatically stop old task before starting new scheduled task; Multi-instance mode: allow multiple task instances to run simultaneously\",\n  \"请选择实例模式\": \"Please select instance mode\",\n  \"单实例\": \"Single Instance\",\n  \"多实例\": \"Multi-Instance\",\n  \"SSH密钥\": \"SSH Keys\",\n  \"别名\": \"Alias\",\n  \"编辑SSH密钥\": \"Edit SSH Key\",\n  \"创建SSH密钥\": \"Create SSH Key\",\n  \"更新SSH密钥成功\": \"SSH key updated successfully\",\n  \"创建SSH密钥成功\": \"SSH key created successfully\",\n  \"请输入SSH密钥别名\": \"Please enter SSH key alias\",\n  \"请输入SSH私钥\": \"Please enter SSH private key\",\n  \"请输入SSH私钥内容（以 -----BEGIN 开头）\": \"Please enter SSH private key content (starts with -----BEGIN)\",\n  \"确认删除SSH密钥\": \"Confirm to delete SSH key\",\n  \"批量\": \"Batch\",\n  \"全局SSH私钥\": \"Global SSH Private Key\",\n  \"用于访问所有私有仓库的全局SSH私钥\": \"Global SSH private key for accessing all private repositories\",\n  \"请输入完整的SSH私钥内容\": \"Please enter the complete SSH private key content\"\n}\n"
  },
  {
    "path": "src/locales/zh-CN.json",
    "content": "{\n  \"复制成功\": \"复制成功\",\n  \"复制\": \"复制\",\n  \"新建\": \"新建\",\n  \"登录\": \"登录\",\n  \"初始化\": \"初始化\",\n  \"错误\": \"错误\",\n  \"定时任务\": \"定时任务\",\n  \"订阅管理\": \"订阅管理\",\n  \"环境变量\": \"环境变量\",\n  \"配置文件\": \"配置文件\",\n  \"脚本管理\": \"脚本管理\",\n  \"依赖管理\": \"依赖管理\",\n  \"日志管理\": \"日志管理\",\n  \"对比工具\": \"对比工具\",\n  \"系统设置\": \"系统设置\",\n  \"退出登录\": \"退出登录\",\n  \"青龙\": \"青龙\",\n  \"返回首页\": \"返回首页\",\n  \"保存\": \"保存\",\n  \"日志\": \"日志\",\n  \"脚本\": \"脚本\",\n  \"确认保存文件\": \"确认保存文件\",\n  \"，保存后不可恢复\": \"，保存后不可恢复\",\n  \"确认运行\": \"确认运行\",\n  \"确认运行定时任务\": \"确认运行定时任务\",\n  \"吗\": \"吗\",\n  \"确认停止\": \"确认停止\",\n  \"确认停止定时任务\": \"确认停止定时任务\",\n  \"确认\": \"确认\",\n  \"任务\": \"任务\",\n  \"状态\": \"状态\",\n  \"空闲中\": \"空闲中\",\n  \"运行中\": \"运行中\",\n  \"队列中\": \"队列中\",\n  \"已禁用\": \"已禁用\",\n  \"定时\": \"定时\",\n  \"最后运行时间\": \"最后运行时间\",\n  \"最后运行时长\": \"最后运行时长\",\n  \"下次运行时间\": \"下次运行时间\",\n  \"名称\": \"名称\",\n  \"命令/脚本\": \"命令/脚本\",\n  \"定时规则\": \"定时规则\",\n  \"操作\": \"操作\",\n  \"确认删除\": \"确认删除\",\n  \"确认删除定时任务\": \"确认删除定时任务\",\n  \"编辑\": \"编辑\",\n  \"删除\": \"删除\",\n  \"确认删除选中的定时任务吗\": \"确认删除选中的定时任务吗\",\n  \"选中的定时任务吗\": \"选中的定时任务吗\",\n  \"创建视图\": \"创建视图\",\n  \"视图管理\": \"视图管理\",\n  \"请输入名称或者关键词\": \"请输入名称或者关键词\",\n  \"创建任务\": \"创建任务\",\n  \"更多\": \"更多\",\n  \"批量删除\": \"批量删除\",\n  \"批量启用\": \"批量启用\",\n  \"批量禁用\": \"批量禁用\",\n  \"批量运行\": \"批量运行\",\n  \"批量停止\": \"批量停止\",\n  \"批量置顶\": \"批量置顶\",\n  \"批量取消置顶\": \"批量取消置顶\",\n  \"批量修改标签\": \"批量修改标签\",\n  \"已选择\": \"已选择\",\n  \"项\": \"项\",\n  \"知道了\": \"知道了\",\n  \"请输入任务名称\": \"请输入任务名称\",\n  \"支持输入脚本路径/任意系统可执行命令/task 脚本路径\": \"支持输入脚本路径/任意系统可执行命令/task 脚本路径\",\n  \"秒(可选) 分 时 天 月 周\": \"秒(可选) 分 时 天 月 周\",\n  \"标签\": \"标签\",\n  \"取消\": \"取消\",\n  \"添加\": \"添加\",\n  \"启用\": \"启用\",\n  \"禁用\": \"禁用\",\n  \"运行\": \"运行\",\n  \"停止\": \"停止\",\n  \"置顶\": \"置顶\",\n  \"取消置顶\": \"取消置顶\",\n  \"命令\": \"命令\",\n  \"包含\": \"包含\",\n  \"不包含\": \"不包含\",\n  \"属于\": \"属于\",\n  \"不属于\": \"不属于\",\n  \"顺序\": \"顺序\",\n  \"倒序\": \"倒序\",\n  \"且\": \"且\",\n  \"或\": \"或\",\n  \"输入后回车增加自定义选项\": \"输入后回车增加自定义选项\",\n  \"视图名称\": \"视图名称\",\n  \"请输入视图名称\": \"请输入视图名称\",\n  \"请输入内容\": \"请输入内容\",\n  \"新增筛选条件\": \"新增筛选条件\",\n  \"新增排序方式\": \"新增排序方式\",\n  \"类型\": \"类型\",\n  \"显示\": \"显示\",\n  \"确认删除视图\": \"确认删除视图\",\n  \"安装中\": \"安装中\",\n  \"已安装\": \"已安装\",\n  \"安装失败\": \"安装失败\",\n  \"删除中\": \"删除中\",\n  \"已删除\": \"已删除\",\n  \"删除失败\": \"删除失败\",\n  \"已取消\": \"已取消\",\n  \"序号\": \"序号\",\n  \"备注\": \"备注\",\n  \"更新时间\": \"更新时间\",\n  \"创建时间\": \"创建时间\",\n  \"确认删除依赖\": \"确认删除依赖\",\n  \"确认重新安装\": \"确认重新安装\",\n  \"确认取消安装\": \"确认取消安装\",\n  \"确认删除选中的依赖吗\": \"确认删除选中的依赖吗\",\n  \"确认重新安装选中的依赖吗\": \"确认重新安装选中的依赖吗\",\n  \"请输入名称\": \"请输入名称\",\n  \"创建依赖\": \"创建依赖\",\n  \"批量安装\": \"批量安装\",\n  \"批量强制删除\": \"批量强制删除\",\n  \"日志 -\": \"日志 -\",\n  \"依赖类型\": \"依赖类型\",\n  \"自动拆分\": \"自动拆分\",\n  \"多个依赖是否换行分割\": \"多个依赖是否换行分割\",\n  \"是\": \"是\",\n  \"否\": \"否\",\n  \"请输入依赖名称，支持指定版本\": \"请输入依赖名称，支持指定版本\",\n  \"请输入依赖名称\": \"请输入依赖名称\",\n  \"请输入备注\": \"请输入备注\",\n  \"源文件\": \"源文件\",\n  \"当前文件\": \"当前文件\",\n  \"修改环境变量名称\": \"修改环境变量名称\",\n  \"请输入新的环境变量名称\": \"请输入新的环境变量名称\",\n  \"已启用\": \"已启用\",\n  \"值\": \"值\",\n  \"确认删除变量\": \"确认删除变量\",\n  \"确认删除选中的变量吗\": \"确认删除选中的变量吗\",\n  \"选中的变量吗\": \"选中的变量吗\",\n  \"请输入名称/值/备注\": \"请输入名称/值/备注\",\n  \"导入\": \"导入\",\n  \"创建变量\": \"创建变量\",\n  \"批量修改变量名称\": \"批量修改变量名称\",\n  \"批量导出\": \"批量导出\",\n  \"请输入环境变量名称\": \"请输入环境变量名称\",\n  \"只能输入字母数字下划线，且不能以数字开头\": \"只能输入字母数字下划线，且不能以数字开头\",\n  \"请输入环境变量值\": \"请输入环境变量值\",\n  \"服务启动超时\": \"服务启动超时\",\n  \"请先按如下方式修复：\": \"请先按如下方式修复：\",\n  \"1. 宿主机执行 docker run --rm -v\\n                  /var/run/docker.sock:/var/run/docker.sock\\n                  containrrr/watchtower -cR <容器名>\": \"1. 宿主机执行 docker run --rm -v\\n                  /var/run/docker.sock:/var/run/docker.sock\\n                  containrrr/watchtower -cR <容器名>\",\n  \"2. 容器内执行 ql check、ql update\": \"2. 容器内执行 ql check、ql update\",\n  \"3. 如果无法解决，容器内执行 pm2 logs，拷贝执行结果\": \"3. 如果无法解决，容器内执行 pm2 logs，拷贝执行结果\",\n  \"提交 issue\": \"提交 issue\",\n  \"启动中，请稍后...\": \"启动中，请稍后...\",\n  \"欢迎使用\": \"欢迎使用\",\n  \"欢迎使用青龙\": \"欢迎使用青龙\",\n  \"支持python3、javascript、shell、typescript 的定时任务管理面板\": \"支持python3、javascript、shell、typescript 的定时任务管理面板\",\n  \"开始安装\": \"开始安装\",\n  \"账户设置\": \"账户设置\",\n  \"用户名\": \"用户名\",\n  \"密码\": \"密码\",\n  \"密码不能为admin\": \"密码不能为admin\",\n  \"确认密码\": \"确认密码\",\n  \"您输入的两个密码不匹配！\": \"您输入的两个密码不匹配！\",\n  \"提交\": \"提交\",\n  \"通知设置\": \"通知设置\",\n  \"通知方式\": \"通知方式\",\n  \"请选择通知方式\": \"请选择通知方式\",\n  \"跳过\": \"跳过\",\n  \"完成安装\": \"完成安装\",\n  \"恭喜安装完成！\": \"恭喜安装完成！\",\n  \"Telegram频道\": \"Telegram频道\",\n  \"去登录\": \"去登录\",\n  \"初始化配置\": \"初始化配置\",\n  \"文件\": \"文件\",\n  \"，删除后不可恢复\": \"，删除后不可恢复\",\n  \"请选择日志\": \"请选择日志\",\n  \"请输入日志名\": \"请输入日志名\",\n  \"暂无日志\": \"暂无日志\",\n  \"登录成功！\": \"登录成功！\",\n  \"上次登录时间：\": \"上次登录时间：\",\n  \"上次登录地点：\": \"上次登录地点：\",\n  \"上次登录IP：\": \"上次登录IP：\",\n  \"上次登录设备：\": \"上次登录设备：\",\n  \"上次登录状态：\": \"上次登录状态：\",\n  \"验证码\": \"验证码\",\n  \"验证码为6位数字\": \"验证码为6位数字\",\n  \"6位数字\": \"6位数字\",\n  \"验证\": \"验证\",\n  \"请\": \"请\",\n  \"秒后重试\": \"秒后重试\",\n  \"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。\": \"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。\",\n  \"请选择脚本文件\": \"请选择脚本文件\",\n  \"当前文件不支持预览\": \"当前文件不支持预览\",\n  \"清空日志\": \"清空日志\",\n  \"设置\": \"设置\",\n  \"退出\": \"退出\",\n  \"空文件\": \"空文件\",\n  \"本地文件\": \"本地文件\",\n  \"文件夹\": \"文件夹\",\n  \"文件名\": \"文件名\",\n  \"请输入文件名\": \"请输入文件名\",\n  \"文件名不能包含斜杠\": \"文件名不能包含斜杠\",\n  \"文件夹名\": \"文件夹名\",\n  \"请输入文件夹名\": \"请输入文件夹名\",\n  \"父目录\": \"父目录\",\n  \"请选择父目录\": \"请选择父目录\",\n  \"点击或者拖拽文件到此区域上传\": \"点击或者拖拽文件到此区域上传\",\n  \"当前修改未保存，确定离开吗\": \"当前修改未保存，确定离开吗\",\n  \"退出编辑\": \"退出编辑\",\n  \"重命名\": \"重命名\",\n  \"请选择脚本\": \"请选择脚本\",\n  \"调试\": \"调试\",\n  \"请输入脚本名\": \"请输入脚本名\",\n  \"暂无脚本\": \"暂无脚本\",\n  \"请输入新名称\": \"请输入新名称\",\n  \"保存文件\": \"保存文件\",\n  \"保存目录\": \"保存目录\",\n  \"请输入保存目录，默认scripts目录\": \"请输入保存目录，默认scripts目录\",\n  \"运行设置\": \"运行设置\",\n  \"待开发\": \"待开发\",\n  \"开发版\": \"开发版\",\n  \"正式版\": \"正式版\",\n  \"版本\": \"版本\",\n  \"更新日志\": \"更新日志\",\n  \"查看\": \"查看\",\n  \"提交BUG\": \"提交BUG\",\n  \"名称不能为保留关键字\": \"名称不能为保留关键字\",\n  \"请输入应用名称\": \"请输入应用名称\",\n  \"权限\": \"权限\",\n  \"请选择模块权限\": \"请选择模块权限\",\n  \"更新\": \"更新\",\n  \"已经是最新版了！\": \"已经是最新版了！\",\n  \"是目前检测到的最新可用版本了。\": \"是目前检测到的最新可用版本了。\",\n  \"重新下载\": \"重新下载\",\n  \"更新可用\": \"更新可用\",\n  \"新版本\": \"新版本\",\n  \"可用，你使用的版本为\": \"可用，你使用的版本为\",\n  \"下载更新\": \"下载更新\",\n  \"以后再说\": \"以后再说\",\n  \"下载更新中...\": \"下载更新中...\",\n  \"确认重启\": \"确认重启\",\n  \"系统安装包下载成功，确认重启\": \"系统安装包下载成功，确认重启\",\n  \"重启\": \"重启\",\n  \"系统将在\": \"系统将在\",\n  \"秒后自动刷新\": \"秒后自动刷新\",\n  \"检查更新\": \"检查更新\",\n  \"重新启动\": \"重新启动\",\n  \"确认删除应用\": \"确认删除应用\",\n  \"确认重置\": \"确认重置\",\n  \"确认重置应用\": \"确认重置应用\",\n  \"的Secret吗\": \"的Secret吗\",\n  \"重置Secret会让当前应用所有token失效\": \"重置Secret会让当前应用所有token失效\",\n  \"创建应用\": \"创建应用\",\n  \"安全设置\": \"安全设置\",\n  \"应用设置\": \"应用设置\",\n  \"登录日志\": \"登录日志\",\n  \"其他设置\": \"其他设置\",\n  \"关于\": \"关于\",\n  \"成功\": \"成功\",\n  \"失败\": \"失败\",\n  \"登录时间\": \"登录时间\",\n  \"登录地址\": \"登录地址\",\n  \"登录IP\": \"登录IP\",\n  \"登录设备\": \"登录设备\",\n  \"登录状态\": \"登录状态\",\n  \"亮色\": \"亮色\",\n  \"暗色\": \"暗色\",\n  \"跟随系统\": \"跟随系统\",\n  \"备份数据上传成功，确认覆盖数据\": \"备份数据上传成功，确认覆盖数据\",\n  \"主题设置\": \"主题设置\",\n  \"日志删除频率\": \"日志删除频率\",\n  \"每x天自动删除x天以前的日志\": \"每x天自动删除x天以前的日志\",\n  \"每\": \"每\",\n  \"天\": \"天\",\n  \"定时任务并发数\": \"定时任务并发数\",\n  \"数据备份还原\": \"数据备份还原\",\n  \"还原数据\": \"还原数据\",\n  \"第一步\": \"第一步\",\n  \"下载两步验证手机应用，比如 Google Authenticator 、\": \"下载两步验证手机应用，比如 Google Authenticator 、\",\n  \"第二步\": \"第二步\",\n  \"使用手机应用扫描二维码，或者输入秘钥\": \"使用手机应用扫描二维码，或者输入秘钥\",\n  \"第三步\": \"第三步\",\n  \"输入手机应用上的6位数字\": \"输入手机应用上的6位数字\",\n  \"完成设置\": \"完成设置\",\n  \"修改用户名密码\": \"修改用户名密码\",\n  \"两步验证\": \"两步验证\",\n  \"头像\": \"头像\",\n  \"更换头像\": \"更换头像\",\n  \"时\": \"时\",\n  \"分\": \"分\",\n  \"秒\": \"秒\",\n  \"私有仓库\": \"私有仓库\",\n  \"公开仓库\": \"公开仓库\",\n  \"单文件\": \"单文件\",\n  \"链接\": \"链接\",\n  \"分支\": \"分支\",\n  \"确认删除定时订阅\": \"确认删除定时订阅\",\n  \"定时订阅\": \"定时订阅\",\n  \"创建订阅\": \"创建订阅\",\n  \"私钥\": \"私钥\",\n  \"请输入私钥\": \"请输入私钥\",\n  \"请输入认证用户名\": \"请输入认证用户名\",\n  \"Github已不支持密码认证，请使用Token方式\": \"Github已不支持密码认证，请使用Token方式\",\n  \"密码/Token\": \"密码/Token\",\n  \"请输入密码或者Token\": \"请输入密码或者Token\",\n  \"支持拷贝 ql repo/raw 命令，粘贴导入\": \"支持拷贝 ql repo/raw 命令，粘贴导入\",\n  \"请输入订阅链接\": \"请输入订阅链接\",\n  \"请输入分支\": \"请输入分支\",\n  \"唯一值\": \"唯一值\",\n  \"唯一值用于日志目录和私钥别名\": \"唯一值用于日志目录和私钥别名\",\n  \"自动生成\": \"自动生成\",\n  \"拉取方式\": \"拉取方式\",\n  \"用户名密码/Token\": \"用户名密码/Token\",\n  \"定时类型\": \"定时类型\",\n  \"白名单\": \"白名单\",\n  \"多个关键词竖线分割，支持正则表达式\": \"多个关键词竖线分割，支持正则表达式\",\n  \"请输入脚本筛选白名单关键词，多个关键词竖线分割\": \"请输入脚本筛选白名单关键词，多个关键词竖线分割\",\n  \"黑名单\": \"黑名单\",\n  \"请输入脚本筛选黑名单关键词，多个关键词竖线分割\": \"请输入脚本筛选黑名单关键词，多个关键词竖线分割\",\n  \"依赖文件\": \"依赖文件\",\n  \"请输入脚本依赖文件关键词，多个关键词竖线分割\": \"请输入脚本依赖文件关键词，多个关键词竖线分割\",\n  \"文件后缀\": \"文件后缀\",\n  \"仓库需要拉取的文件后缀，多个后缀空格分隔，默认使用配置文件中的RepoFileExtensions\": \"仓库需要拉取的文件后缀，多个后缀空格分隔，默认使用配置文件中的RepoFileExtensions\",\n  \"请输入文件后缀\": \"请输入文件后缀\",\n  \"执行前\": \"执行前\",\n  \"运行订阅前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"运行订阅前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\",\n  \"请输入运行订阅前要执行的命令\": \"请输入运行订阅前要执行的命令\",\n  \"执行后\": \"执行后\",\n  \"运行订阅后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"运行订阅后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\",\n  \"请输入运行订阅后要执行的命令\": \"请输入运行订阅后要执行的命令\",\n  \"代理\": \"代理\",\n  \"公开仓库支持HTTP/SOCK5代理，私有仓库支持SOCK5代理\": \"公开仓库支持HTTP/SOCK5代理，私有仓库支持SOCK5代理\",\n  \"自动添加任务\": \"自动添加任务\",\n  \"自动删除任务\": \"自动删除任务\",\n  \"中文\": \"中文\",\n  \"系统信息\": \"系统信息\",\n  \"Server酱\": \"Server酱\",\n  \"Telegram机器人\": \"Telegram机器人\",\n  \"钉钉机器人\": \"钉钉机器人\",\n  \"企业微信机器人\": \"企业微信机器人\",\n  \"企业微信应用\": \"企业微信应用\",\n  \"智能微秘书\": \"智能微秘书\",\n  \"群晖chat\": \"群晖chat\",\n  \"微加机器人\": \"微加机器人\",\n  \"邮箱\": \"邮箱\",\n  \"飞书机器人\": \"飞书机器人\",\n  \"自定义通知\": \"自定义通知\",\n  \"已关闭\": \"已关闭\",\n  \"gotify的url地址，例如 https://push.example.de:8080\": \"gotify的url地址，例如 https://push.example.de:8080\",\n  \"gotify的消息应用token码\": \"gotify的消息应用token码\",\n  \"推送消息的优先级\": \"推送消息的优先级\",\n  \"synologyChat的url地址\": \"synologyChat的webhook url地址\",\n  \"chat的token码\": \"chat的token码\",\n  \"推送到个人QQ: http://127.0.0.1/send_private_msg，群：http://127.0.0.1/send_group_msg\": \"推送到个人QQ: http://127.0.0.1/send_private_msg，群：http://127.0.0.1/send_group_msg\",\n  \"访问密钥\": \"访问密钥\",\n  \"如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群\": \"如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群\",\n  \"Server酱SENDKEY\": \"Server 酱 SENDKEY，https://sct.ftqq.com/r/13363\",\n  \"PushDeer的Key，https://github.com/easychen/pushdeer\": \"PushDeer的Key，https://github.com/easychen/pushdeer\",\n  \"PushDeer的自架API endpoint，默认是 https://api2.pushdeer.com/message/push\": \"PushDeer的自架API endpoint，默认是 https://api2.pushdeer.com/message/push\",\n  \"Bark的信息IP/设备码，例如：https://api.day.app/XXXXXXXX\": \"Bark的信息IP/设备码，例如：https://api.day.app/XXXXXXXX\",\n  \"BARK推送图标，自定义推送图标 (需iOS15或以上才能显示)\": \"BARK推送图标，自定义推送图标 (需iOS15或以上才能显示)\",\n  \"BARK推送铃声，铃声列表去APP查看复制填写\": \"BARK推送铃声，铃声列表去APP查看复制填写\",\n  \"BARK推送消息的分组，默认为qinglong\": \"BARK推送消息的分组，默认为qinglong\",\n  \"BARK推送消息的时效性，默认为active\": \"BARK推送消息的时效性，默认为active\",\n  \"BARK推送消息的跳转URL\": \"BARK推送消息的跳转URL\",\n  \"BARK是否保存推送消息\": \"BARK是否保存推送消息\",\n  \"telegram机器人的token，例如：1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw\": \"telegram机器人的token，例如：1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw\",\n  \"telegram用户的id，例如：129xxx206\": \"telegram用户的id，例如：129xxx206\",\n  \"代理IP\": \"代理IP\",\n  \"代理端口\": \"代理端口\",\n  \"telegram代理配置认证参数，用户名与密码用英文冒号连接 user:password\": \"telegram代理配置认证参数, 用户名与密码用英文冒号连接 user:password\",\n  \"telegram api自建的反向代理地址，默认tg官方api\": \"telegram api自建的反向代理地址，默认tg官方api\",\n  \"钉钉机器人webhook token，例如：5a544165465465645d0f31dca676e7bd07415asdasd\": \"钉钉机器人webhook token，例如：5a544165465465645d0f31dca676e7bd07415asdasd\",\n  \"密钥，机器人安全设置页面，加签一栏下面显示的SEC开头的字符串\": \"密钥，机器人安全设置页面，加签一栏下面显示的SEC开头的字符串\",\n  \"企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)，例如：693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa\": \"企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)，例如：693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa\",\n  \"企业微信代理地址\": \"企业微信代理地址\",\n  \"corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填，不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号)，例如：wwcfrs,B-76WERQ,qinglong,1000001,2COat\": \"corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号)，例如：wwcfrs,B-76WERQ,qinglong,1000001,2COat\",\n  \"密钥key，智能微秘书个人中心获取apikey，申请地址：https://wechat.aibotk.com/signup?from=ql\": \"密钥key,智能微秘书个人中心获取apikey，申请地址：https://wechat.aibotk.com/signup?from=ql\",\n  \"发送的目标，群组或者好友\": \"发送的目标，群组或者好友\",\n  \"请输入要发送的目标\": \"请输入要发送的目标\",\n  \"群聊\": \"群聊\",\n  \"好友\": \"好友\",\n  \"要发送的用户昵称或群名，如果目标是群，需要填群名，如果目标是好友，需要填好友昵称\": \"要发送的用户昵称或群名，如果目标是群，需要填群名，如果目标是好友，需要填好友昵称\",\n  \"iGot的信息推送key，例如：https://push.hellyw.com/XXXXXXXX\": \"iGot的信息推送key，例如：https://push.hellyw.com/XXXXXXXX\",\n  \"微信扫码登录后一对一推送或一对多推送下面的token(您的Token)，不提供PUSH_PLUS_USER则默认为一对一推送，参考 https://www.pushplus.plus/\": \"微信扫码登录后一对一推送或一对多推送下面的token(你的Token)，不提供PUSH_PLUS_USER则默认为一对一推送，参考 https://www.pushplus.plus/\",\n  \"一对多推送的“群组编码”（一对多推送下面->您的群组(如无则创建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）\": \"一对多推送的“群组编码”（一对多推送下面->您的群组(如无则创建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）\",\n  \"发送模板\": \"发送模板，支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay\",\n  \"发送渠道\": \"发送渠道，支持wechat,webhook,cp,mail,sms\",\n  \"webhook编码\": \"webhook编码，可在pushplus公众号上扩展配置出更多渠道\",\n  \"发送结果回调地址\": \"发送结果回调地址，会把推送最终结果通知到这个地址上\",\n  \"好友令牌\": \"好友令牌，微信公众号渠道填写好友令牌，企业微信渠道填写企业微信用户id\",\n  \"用户令牌，扫描登录后 我的—>设置->令牌 中获取，参考 https://www.weplusbot.com/\": \"用户令牌，扫描登录后 我的—>设置->令牌 中获取，参考 https://www.weplusbot.com/\",\n  \"消息接收人\": \"消息接收人\",\n  \"调用版本；专业版填写pro，个人版填写personal，为空默认使用专业版\": \"调用版本；专业版填写pro，个人版填写personal，为空默认使用专业版\",\n  \"飞书群组机器人：https://www.feishu.cn/hc/zh-CN/articles/360024984973\": \"飞书群组机器人：https://www.feishu.cn/hc/zh-CN/articles/360024984973\",\n  \"飞书群组机器人加签密钥，安全设置中开启签名校验后获得\": \"飞书群组机器人加签密钥，安全设置中开启签名校验后获得\",\n  \"邮箱服务名称，比如126、163、Gmail、QQ等，支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json\": \"邮箱服务名称，比如126、163、Gmail、QQ等，支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json\",\n  \"邮箱地址\": \"邮箱地址\",\n  \"SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\": \"SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定\",\n  \"PushMe的Key，https://push.i-i.me/\": \"PushMe的Key，https://push.i-i.me/\",\n  \"自建的PushMeServer消息接口地址，例如：http://127.0.0.1:3010，不填则使用官方消息接口\": \"自建的PushMeServer消息接口地址，例如：http://127.0.0.1:3010，不填则使用官方消息接口\",\n  \"ntfy的url地址，例如 https://ntfy.sh\": \"ntfy的url地址，例如 https://ntfy.sh\",\n  \"ntfy应用topic\": \"ntfy应用topic\",\n  \"ntfy应用token\": \"ntfy应用token，参考 https://docs.ntfy.sh/config/#access-tokens\",\n  \"ntfy应用用户名\": \"ntfy应用用户名，参考 https://docs.ntfy.sh/config/#users-and-roles\",\n  \"ntfy应用密码\": \"ntfy应用密码，参考 https://docs.ntfy.sh/config/#users-and-roles\",\n  \"ntfy用户动作\": \"ntfy用户动作，最多三个动作，参考 https://docs.ntfy.sh/publish/?h=actions#action-buttons\",\n  \"wxPusherBot的appToken\": \"wxPusherBot的appToken, 按照文档获取 https://wxpusher.zjiecode.com/docs/\",\n  \"wxPusherBot的topicIds\": \"wxPusherBot的topicIds, topicIds 和 uids 至少配置一个才行\",\n  \"wxPusherBot的uids\": \"wxPusherBot的uids, topicIds 和 uids 至少配置一个才行\",\n  \"请求方法\": \"请求方法\",\n  \"请求头Content-Type\": \"请求头Content-Type\",\n  \"请求链接以http或者https开头。url或者body中必须包含$title，$content可选，对应api内容的位置\": \"请求链接以http或者https开头。url或者body中必须包含$title，$content可选，对应api内容的位置\",\n  \"请求头格式Custom-Header1: Header1，多个换行分割\": \"请求头格式Custom-Header1: Header1，多个换行分割\",\n  \"请求体格式key1: value1，多个换行分割。url或者body中必须包含$title，$content可选，对应api内容的位置\": \"请求体格式key1: value1，多个换行分割。url或者body中必须包含$title，$content可选，对应api内容的位置\",\n  \"错误日志\": \"错误日志\",\n  \"执行结束\": \"执行结束\",\n  \"备份\": \"备份\",\n  \"生成数据中...\": \"生成数据中...\",\n  \"请选择日志文件\": \"请选择日志文件\",\n  \"筛选条件\": \"筛选条件\",\n  \"系统\": \"系统\",\n  \"个人\": \"个人\",\n  \"重新安装\": \"重新安装\",\n  \"取消安装\": \"取消安装\",\n  \"强制删除\": \"强制删除\",\n  \"全部任务\": \"全部任务\",\n  \"关联订阅\": \"关联订阅\",\n  \"订阅\": \"订阅\",\n  \"创建\": \"创建\",\n  \"同时删除关联任务和脚本\": \"同时删除关联任务和脚本\",\n  \"未找到\": \"未找到\",\n  \"保存成功\": \"保存成功\",\n  \"删除成功\": \"删除成功\",\n  \"批量删除成功\": \"批量删除成功\",\n  \"启动中...\": \"启动中...\",\n  \"任务未运行\": \"任务未运行\",\n  \"更新任务成功\": \"更新任务成功\",\n  \"创建任务成功\": \"创建任务成功\",\n  \"编辑任务\": \"编辑任务\",\n  \"Cron表达式格式有误\": \"Cron表达式格式有误\",\n  \"添加Labels成功\": \"添加Labels成功\",\n  \"删除Labels成功\": \"删除Labels成功\",\n  \"编辑视图\": \"编辑视图\",\n  \"排序方式\": \"排序方式\",\n  \"开始时间\": \"开始时间\",\n  \"安装\": \"安装\",\n  \"结束时间\": \"结束时间\",\n  \"编辑依赖\": \"编辑依赖\",\n  \"更新环境变量名称成功\": \"更新环境变量名称成功\",\n  \"更新变量成功\": \"更新变量成功\",\n  \"创建变量成功\": \"创建变量成功\",\n  \"编辑变量\": \"编辑变量\",\n  \"加载中...\": \"加载中...\",\n  \"夹下所有日志\": \"夹下所有日志\",\n  \"创建文件夹成功\": \"创建文件夹成功\",\n  \"创建文件成功\": \"创建文件成功\",\n  \"夹及其子文件\": \"夹及其子文件\",\n  \"更新名称成功\": \"更新名称成功\",\n  \"保存文件成功\": \"保存文件成功\",\n  \"更新应用成功\": \"更新应用成功\",\n  \"创建应用成功\": \"创建应用成功\",\n  \"编辑应用\": \"编辑应用\",\n  \"检查更新中...\": \"检查更新中...\",\n  \"失败，请检查\": \"失败，请检查\",\n  \"更新失败，请检查网络及日志或稍后再试\": \"更新失败，请检查网络及日志或稍后再试\",\n  \"更新包下载成功\": \"更新包下载成功\",\n  \"重置secret\": \"重置secret\",\n  \"重置成功\": \"重置成功\",\n  \"通知发送成功\": \"通知发送成功\",\n  \"通知关闭成功\": \"通知关闭成功\",\n  \"测试中...\": \"测试中...\",\n  \"上传\": \"上传\",\n  \"下载\": \"下载\",\n  \"更新成功\": \"更新成功\",\n  \"激活成功\": \"激活成功\",\n  \"验证失败\": \"验证失败\",\n  \"更新订阅成功\": \"更新订阅成功\",\n  \"创建订阅成功\": \"创建订阅成功\",\n  \"编辑订阅\": \"编辑订阅\",\n  \"Subscription表达式格式有误\": \"Subscription表达式格式有误\",\n  \"一对多推送的“群组编码”（一对多推送下面->您的群组(如无则新建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）\": \"一对多推送的“群组编码”（一对多推送下面->您的群组(如无则新建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）\",\n  \"登录已过期，请重新登录\": \"登录已过期，请重新登录\",\n  \"系统日志\": \"系统日志\",\n  \"主题\": \"主题\",\n  \"语言\": \"语言\",\n  \"中...\": \"中...\",\n  \"请选择操作符\": \"请选择操作符\",\n  \"新增定时规则\": \"新增定时规则\",\n  \"运行任务前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"运行任务前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\",\n  \"运行任务后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\": \"运行任务后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js\",\n  \"请输入运行任务前要执行的命令，不能包含 task 命令\": \"请输入运行任务前要执行的命令，不能包含 task 命令\",\n  \"请输入运行任务后要执行的命令，不能包含 task 命令\": \"请输入运行任务后要执行的命令，不能包含 task 命令\",\n  \"不能包含 task 命令\": \"不能包含 task 命令\",\n  \"Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/\": \"Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/\",\n  \"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如：user_id=xxx;group_id=xxxx;group_id=xxxxx\": \"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如：user_id=xxx;group_id=xxxx;group_id=xxxxx\",\n  \"docker安装在持久化config目录下的chronocat.yml文件可找到\": \"docker安装在持久化config目录下的chronocat.yml文件可找到\",\n  \"请选择\": \"请选择\",\n  \"请输入\": \"请输入\",\n  \"依赖设置\": \"依赖设置\",\n  \"Node 软件包镜像源\": \"Node 软件包镜像源\",\n  \"Python 软件包镜像源\": \"Python 软件包镜像源\",\n  \"Linux 软件包镜像源\": \"Linux 软件包镜像源\",\n  \"代理与镜像源二选一即可\": \"代理与镜像源二选一即可\",\n  \"代理地址, 支持HTTP(S)/SOCK5\": \"代理地址, 支持HTTP(S)/SOCK5\",\n  \"NPM 镜像源\": \"NPM 镜像源\",\n  \"PyPI 镜像源\": \"PyPI 镜像源\",\n  \"alpine linux 镜像源\": \"alpine linux 镜像源\",\n  \"如果恢复失败，可进入容器执行\": \"如果恢复失败，可进入容器执行\",\n  \"常规定时\": \"常规定时\",\n  \"手动运行\": \"手动运行\",\n  \"开机运行\": \"开机运行\",\n  \"时区\": \"时区\",\n  \"强制打开\": \"强制打开\",\n  \"强制打开可能会导致编辑器显示异常\": \"强制打开可能会导致编辑器显示异常\",\n  \"确认离开\": \"确认离开\",\n  \"当前文件未保存，确认离开吗\": \"当前文件未保存，确认离开吗\",\n  \"收件邮箱地址，多个分号分隔，默认发送给发件邮箱地址\": \"收件邮箱地址，多个分号分隔，默认发送给发件邮箱地址\",\n  \"选择备份模块\": \"选择备份模块\",\n  \"开始备份\": \"开始备份\",\n  \"基础数据\": \"基础数据\",\n  \"脚本文件\": \"脚本文件\",\n  \"日志文件\": \"日志文件\",\n  \"依赖缓存\": \"依赖缓存\",\n  \"远程脚本缓存\": \"远程脚本缓存\",\n  \"远程仓库缓存\": \"远程仓库缓存\",\n  \"SSH 文件缓存\": \"SSH 文件缓存\",\n  \"清除依赖缓存\": \"清除依赖缓存\",\n  \"清除成功\": \"清除成功\",\n  \"日志名称\": \"日志名称\",\n  \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成\": \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成\",\n  \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持绝对路径如 /dev/null\": \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持绝对路径如 /dev/null\",\n  \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持 /dev/null 丢弃日志，其他绝对路径必须在日志目录内\": \"自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持 /dev/null 丢弃日志，其他绝对路径必须在日志目录内\",\n  \"请输入自定义日志文件夹名称\": \"请输入自定义日志文件夹名称\",\n  \"请输入自定义日志文件夹名称或绝对路径\": \"请输入自定义日志文件夹名称或绝对路径\",\n  \"请输入自定义日志文件夹名称或 /dev/null\": \"请输入自定义日志文件夹名称或 /dev/null\",\n  \"日志名称只能包含字母、数字、下划线和连字符\": \"日志名称只能包含字母、数字、下划线和连字符\",\n  \"日志名称不能超过100个字符\": \"日志名称不能超过100个字符\",\n  \"未启用\": \"未启用\",\n  \"默认为 CPU 个数\": \"默认为 CPU 个数\",\n  \"最小是 4\": \"最小是 4\",\n  \"实例模式\": \"实例模式\",\n  \"单实例模式：定时启动新任务前会自动停止旧任务；多实例模式：允许同时运行多个任务实例\": \"单实例模式：定时启动新任务前会自动停止旧任务；多实例模式：允许同时运行多个任务实例\",\n  \"请选择实例模式\": \"请选择实例模式\",\n  \"单实例\": \"单实例\",\n  \"多实例\": \"多实例\",\n  \"SSH密钥\": \"SSH密钥\",\n  \"别名\": \"别名\",\n  \"编辑SSH密钥\": \"编辑SSH密钥\",\n  \"创建SSH密钥\": \"创建SSH密钥\",\n  \"更新SSH密钥成功\": \"更新SSH密钥成功\",\n  \"创建SSH密钥成功\": \"创建SSH密钥成功\",\n  \"请输入SSH密钥别名\": \"请输入SSH密钥别名\",\n  \"请输入SSH私钥\": \"请输入SSH私钥\",\n  \"请输入SSH私钥内容（以 -----BEGIN 开头）\": \"请输入SSH私钥内容（以 -----BEGIN 开头）\",\n  \"确认删除SSH密钥\": \"确认删除SSH密钥\",\n  \"批量\": \"批量\",\n  \"全局SSH私钥\": \"全局SSH私钥\",\n  \"用于访问所有私有仓库的全局SSH私钥\": \"用于访问所有私有仓库的全局SSH私钥\",\n  \"请输入完整的SSH私钥内容\": \"请输入完整的SSH私钥内容\"\n}\n"
  },
  {
    "path": "src/pages/404.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React from 'react';\nimport { Button, Result, Typography } from 'antd';\n\nconst { Link } = Typography;\n\nconst NotFound: React.FC = () => (\n  <Result\n    status=\"404\"\n    title=\"404\"\n    extra={\n      <Button type=\"primary\">\n        <Link href=\"/\">{intl.get('返回首页')}</Link>\n      </Button>\n    }\n  />\n);\n\nexport default NotFound;\n"
  },
  {
    "path": "src/pages/config/index.less",
    "content": ""
  },
  {
    "path": "src/pages/config/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, {\n  PureComponent,\n  Fragment,\n  useState,\n  useEffect,\n  useRef,\n} from 'react';\nimport { Button, message, Modal, TreeSelect } from 'antd';\nimport config from '@/utils/config';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { request } from '@/utils/http';\nimport Editor from '@monaco-editor/react';\nimport CodeMirror from '@uiw/react-codemirror';\nimport { useOutletContext } from '@umijs/max';\nimport { SharedContext } from '@/layouts';\nimport { langs } from '@uiw/codemirror-extensions-langs';\nimport { useHotkeys } from 'react-hotkeys-hook';\nimport { getEditorMode } from '@/utils';\n\nconst Config = () => {\n  const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();\n  const [value, setValue] = useState('');\n  const [loading, setLoading] = useState(true);\n  const [title, setTitle] = useState('config.sh');\n  const [select, setSelect] = useState('config.sh');\n  const [data, setData] = useState<any[]>([]);\n  const editorRef = useRef<any>(null);\n  const [confirmLoading, setConfirmLoading] = useState(false);\n  const [language, setLanguage] = useState<string>('shell');\n\n  const getConfig = (name: string) => {\n    request\n      .get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(name)}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n        }\n      });\n  };\n\n  const getFiles = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}configs/files`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setData(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const updateConfig = () => {\n    setConfirmLoading(true);\n    const content = editorRef.current\n      ? editorRef.current.getValue().replace(/\\r\\n/g, '\\n')\n      : value;\n\n    request\n      .post(`${config.apiPrefix}configs/save`, { content, name: select })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('保存成功'));\n        }\n        setConfirmLoading(false);\n      });\n  };\n\n  const onSelect = (value: any, node: any) => {\n    setSelect(value);\n    setTitle(node.value);\n    getConfig(node.value);\n    const newMode = getEditorMode(value);\n    setLanguage(newMode);\n  };\n\n  useHotkeys(\n    'mod+s',\n    (e) => {\n      updateConfig();\n    },\n    { enableOnFormTags: ['textarea'], preventDefault: true },\n  );\n\n  useEffect(() => {\n    getFiles();\n    getConfig('config.sh');\n  }, []);\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper config-wrapper\"\n      title={title}\n      loading={loading}\n      extra={[\n        <TreeSelect\n          treeExpandAction=\"click\"\n          className=\"config-select\"\n          value={select}\n          dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}\n          treeData={data}\n          key=\"value\"\n          defaultValue=\"config.sh\"\n          onSelect={onSelect}\n        />,\n        <Button\n          key=\"1\"\n          loading={confirmLoading}\n          type=\"primary\"\n          onClick={updateConfig}\n        >\n          {intl.get('保存')}\n        </Button>,\n      ]}\n      header={{\n        style: headerStyle,\n      }}\n    >\n      {isPhone ? (\n        <CodeMirror\n          value={value}\n          theme={theme.includes('dark') ? 'dark' : 'light'}\n          extensions={[langs.shell()]}\n          onChange={(value) => {\n            setValue(value);\n          }}\n        />\n      ) : (\n        <Editor\n          language={language}\n          value={value}\n          theme={theme}\n          options={{\n            fontSize: 12,\n            lineNumbersMinChars: 3,\n            folding: false,\n            glyphMargin: false,\n            accessibilitySupport: 'off'\n          }}\n          onMount={(editor) => {\n            editorRef.current = editor;\n          }}\n        />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Config;\n"
  },
  {
    "path": "src/pages/crontab/const.ts",
    "content": "import { ScheduleType } from './type';\n\nexport const scheduleTypeMap = {\n  [ScheduleType.Normal]: '',\n  [ScheduleType.Once]: '@once',\n  [ScheduleType.Boot]: '@boot',\n};\n\nexport const getScheduleType = (schedule?: string): ScheduleType => {\n  if (schedule?.startsWith('@once')) return ScheduleType.Once;\n  if (schedule?.startsWith('@boot')) return ScheduleType.Boot;\n  return ScheduleType.Normal;\n};\n"
  },
  {
    "path": "src/pages/crontab/detail.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useRef, useState } from 'react';\nimport {\n  Modal,\n  message,\n  Input,\n  Form,\n  Button,\n  Card,\n  Tag,\n  List,\n  Divider,\n  Typography,\n  Tooltip,\n} from 'antd';\nimport {\n  ClockCircleOutlined,\n  CloseCircleOutlined,\n  FieldTimeOutlined,\n  Loading3QuartersOutlined,\n  FileOutlined,\n  PlayCircleOutlined,\n  PauseCircleOutlined,\n  FullscreenOutlined,\n} from '@ant-design/icons';\nimport { CrontabStatus } from './type';\nimport { diffTime } from '@/utils/date';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport CronLogModal from './logModal';\nimport Editor from '@monaco-editor/react';\nimport IconFont from '@/components/iconfont';\nimport { getCommandScript, getEditorMode } from '@/utils';\nimport VirtualList from 'rc-virtual-list';\nimport useScrollHeight from '@/hooks/useScrollHeight';\nimport dayjs from 'dayjs';\n\nconst { Text } = Typography;\n\nconst tabList = [\n  {\n    key: 'log',\n    tab: intl.get('日志'),\n  },\n  {\n    key: 'script',\n    tab: intl.get('脚本'),\n  },\n];\n\ninterface LogItem {\n  directory: string;\n  filename: string;\n}\n\nconst CronDetailModal = ({\n  cron = {},\n  handleCancel,\n  theme,\n  isPhone,\n}: {\n  cron?: any;\n  handleCancel: (needUpdate?: boolean) => void;\n  theme: string;\n  isPhone: boolean;\n}) => {\n  const [activeTabKey, setActiveTabKey] = useState('log');\n  const [loading, setLoading] = useState(true);\n  const [logs, setLogs] = useState<LogItem[]>([]);\n  const [log, setLog] = useState('');\n  const [value, setValue] = useState('');\n  const [isLogModalVisible, setIsLogModalVisible] = useState(false);\n  const editorRef = useRef<any>(null);\n  const [scriptInfo, setScriptInfo] = useState<any>({});\n  const [logUrl, setLogUrl] = useState('');\n  const [validTabs, setValidTabs] = useState(tabList);\n  const [currentCron, setCurrentCron] = useState<any>({});\n  const listRef = useRef<HTMLDivElement>(null);\n  const tableScrollHeight = useScrollHeight(listRef);\n\n  const contentList: any = {\n    log: (\n      <div ref={listRef}>\n        <List>\n          <VirtualList\n            data={logs}\n            height={tableScrollHeight}\n            itemHeight={47}\n            itemKey=\"filename\"\n          >\n            {(item) => (\n              <List.Item className=\"log-item\" onClick={() => onClickItem(item)}>\n                <FileOutlined style={{ marginRight: 10 }} />\n                {item.directory}/{item.filename}\n              </List.Item>\n            )}\n          </VirtualList>\n        </List>\n      </div>\n    ),\n    script: scriptInfo.filename && (\n      <Editor\n        language={getEditorMode(scriptInfo.filename)}\n        theme={theme}\n        value={value}\n        options={{\n          fontSize: 12,\n          minimap: { enabled: false },\n          lineNumbersMinChars: 3,\n          glyphMargin: false,\n          accessibilitySupport: 'off',\n        }}\n        onMount={(editor, monaco) => {\n          editorRef.current = editor;\n        }}\n      />\n    ),\n  };\n\n  const onClickItem = (item: LogItem) => {\n    const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${\n      item.directory || ''\n    }`;\n    localStorage.setItem('logCron', url);\n    setLogUrl(url);\n    request.get(url).then(({ code, data }) => {\n      if (code === 200) {\n        setLog(data);\n        setIsLogModalVisible(true);\n      }\n    });\n  };\n\n  const onTabChange = (key: string) => {\n    setActiveTabKey(key);\n  };\n\n  const getLogs = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}crons/${cron.id}/logs`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setLogs(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const getScript = () => {\n    const result = getCommandScript(cron.command);\n    if (Array.isArray(result)) {\n      setValidTabs(validTabs);\n      const [s, p] = result;\n      setScriptInfo({ parent: p, filename: s });\n      request\n        .get(`${config.apiPrefix}scripts/detail?file=${s}&path=${p || ''}`)\n        .then(({ code, data }) => {\n          if (code === 200) {\n            setValue(data);\n          }\n        });\n    } else {\n      setValidTabs([validTabs[0]]);\n      setActiveTabKey('log');\n    }\n  };\n\n  const saveFile = () => {\n    Modal.confirm({\n      title: `确认保存`,\n      content: (\n        <>\n          {intl.get('确认保存文件')}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {' '}\n            {scriptInfo.filename}\n          </Text>\n          {intl.get('，保存后不可恢复')}\n        </>\n      ),\n      onOk() {\n        const content = editorRef.current\n          ? editorRef.current.getValue().replace(/\\r\\n/g, '\\n')\n          : value;\n        return new Promise((resolve, reject) => {\n          request\n            .put(`${config.apiPrefix}scripts`, {\n              filename: scriptInfo.filename,\n              path: scriptInfo.parent || '',\n              content,\n            })\n            .then(({ code, data }) => {\n              if (code === 200) {\n                setValue(content);\n                message.success(`保存成功`);\n              }\n              resolve(null);\n            })\n            .catch((e) => reject(e));\n        });\n      },\n    });\n  };\n\n  const runCron = () => {\n    Modal.confirm({\n      title: intl.get('确认运行'),\n      content: (\n        <>\n          {intl.get('确认运行定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {currentCron.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}crons/run`, [currentCron.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              setCurrentCron({ ...currentCron, status: CrontabStatus.running });\n              setTimeout(() => {\n                getLogs();\n              }, 1000);\n            }\n          });\n      },\n    });\n  };\n\n  const stopCron = () => {\n    Modal.confirm({\n      title: intl.get('确认停止'),\n      content: (\n        <>\n          {intl.get('确认停止定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {currentCron.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}crons/stop`, [currentCron.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              setCurrentCron({ ...currentCron, status: CrontabStatus.idle });\n            }\n          });\n      },\n    });\n  };\n\n  const enabledOrDisabledCron = () => {\n    Modal.confirm({\n      title: `确认${\n        currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')\n      }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')}\n          {intl.get('定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {currentCron.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}crons/${\n              currentCron.isDisabled === 1 ? 'enable' : 'disable'\n            }`,\n            [currentCron.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              setCurrentCron({\n                ...currentCron,\n                isDisabled: currentCron.isDisabled === 1 ? 0 : 1,\n              });\n            }\n          });\n      },\n    });\n  };\n\n  const pinOrUnPinCron = () => {\n    Modal.confirm({\n      title: `确认${\n        currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')\n      }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}\n          {intl.get('定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {currentCron.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}crons/${\n              currentCron.isPinned === 1 ? 'unpin' : 'pin'\n            }`,\n            [currentCron.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              setCurrentCron({\n                ...currentCron,\n                isPinned: currentCron.isPinned === 1 ? 0 : 1,\n              });\n            }\n          });\n      },\n    });\n  };\n\n  const fullscreen = () => {\n    const editorElement = editorRef.current._domElement as HTMLElement;\n    editorElement.parentElement?.requestFullscreen();\n  };\n\n  useEffect(() => {\n    if (cron && cron.id) {\n      setCurrentCron(cron);\n      getLogs();\n      getScript();\n    }\n  }, [cron]);\n\n  return (\n    <Modal\n      title={\n        <div className=\"crontab-title-wrapper\">\n          <div style={{ minWidth: 0, display: 'flex', alignItems: 'center' }}>\n            <Typography.Text\n              style={{ width: 200, boxSizing: 'content-box' }}\n              ellipsis={{\n                onEllipsis(ellipsis) {\n                  return ellipsis;\n                },\n                tooltip: currentCron.name,\n              }}\n            >\n              {currentCron.name}\n            </Typography.Text>\n            {currentCron.labels?.length > 0 && currentCron.labels[0] !== '' && (\n              <Divider type=\"vertical\"></Divider>\n            )}\n            {currentCron.labels?.length > 0 &&\n              currentCron.labels[0] !== '' &&\n              currentCron.labels?.map((label: string, i: number) => (\n                <Tag key={label} color=\"blue\" style={{ marginRight: 5 }}>\n                  {label}\n                </Tag>\n              ))}\n          </div>\n\n          <div className=\"operations\">\n            <Tooltip\n              title={\n                currentCron.status === CrontabStatus.idle\n                  ? intl.get('运行')\n                  : intl.get('停止')\n              }\n            >\n              <Button\n                type=\"link\"\n                icon={\n                  currentCron.status === CrontabStatus.idle ? (\n                    <PlayCircleOutlined />\n                  ) : (\n                    <PauseCircleOutlined />\n                  )\n                }\n                size=\"small\"\n                onClick={\n                  currentCron.status === CrontabStatus.idle ? runCron : stopCron\n                }\n              />\n            </Tooltip>\n            <Tooltip\n              title={\n                currentCron.isDisabled === 1\n                  ? intl.get('启用')\n                  : intl.get('禁用')\n              }\n            >\n              <Button\n                type=\"link\"\n                icon={\n                  <IconFont\n                    type={\n                      currentCron.isDisabled === 1\n                        ? 'ql-icon-enable'\n                        : 'ql-icon-disable'\n                    }\n                  />\n                }\n                size=\"small\"\n                onClick={enabledOrDisabledCron}\n              />\n            </Tooltip>\n            <Tooltip\n              title={\n                currentCron.isPinned === 1\n                  ? intl.get('取消置顶')\n                  : intl.get('置顶')\n              }\n            >\n              <Button\n                type=\"link\"\n                icon={\n                  <IconFont\n                    type={\n                      currentCron.isPinned === 1\n                        ? 'ql-icon-untop'\n                        : 'ql-icon-top'\n                    }\n                  />\n                }\n                size=\"small\"\n                onClick={pinOrUnPinCron}\n              />\n            </Tooltip>\n          </div>\n        </div>\n      }\n      centered\n      open={true}\n      forceRender\n      footer={false}\n      onCancel={() => handleCancel()}\n      wrapClassName=\"crontab-detail\"\n      width={!isPhone ? '80vw' : ''}\n    >\n      <div className=\"card-wrapper\">\n        <Card>\n          <div className=\"cron-detail-info-item\">\n            <div className=\"cron-detail-info-title\">{intl.get('任务')}</div>\n            <div className=\"cron-detail-info-value\">{currentCron.command}</div>\n          </div>\n        </Card>\n        <Card style={{ marginTop: 10 }}>\n          <div className=\"cron-detail-info-item\">\n            <div className=\"cron-detail-info-title\">{intl.get('状态')}</div>\n            <div className=\"cron-detail-info-value\">\n              {(!currentCron.isDisabled ||\n                currentCron.status !== CrontabStatus.idle) && (\n                <>\n                  {currentCron.status === CrontabStatus.idle && (\n                    <Tag icon={<ClockCircleOutlined />} color=\"default\">\n                      {intl.get('空闲中')}\n                    </Tag>\n                  )}\n                  {currentCron.status === CrontabStatus.running && (\n                    <Tag\n                      icon={<Loading3QuartersOutlined spin />}\n                      color=\"processing\"\n                    >\n                      {intl.get('运行中')}\n                    </Tag>\n                  )}\n                  {currentCron.status === CrontabStatus.queued && (\n                    <Tag icon={<FieldTimeOutlined />} color=\"default\">\n                      {intl.get('队列中')}\n                    </Tag>\n                  )}\n                </>\n              )}\n              {currentCron.isDisabled === 1 &&\n                currentCron.status === CrontabStatus.idle && (\n                  <Tag icon={<CloseCircleOutlined />} color=\"error\">\n                    {intl.get('已禁用')}\n                  </Tag>\n                )}\n            </div>\n          </div>\n          <div className=\"cron-detail-info-item\">\n            <div className=\"cron-detail-info-title\">{intl.get('定时')}</div>\n            <div className=\"cron-detail-info-value\">\n              <div>{currentCron.schedule}</div>\n              {currentCron.extra_schedules?.map((x) => (\n                <div key={x.schedule}>{x.schedule}</div>\n              ))}\n            </div>\n          </div>\n          <div className=\"cron-detail-info-item\">\n            <div className=\"cron-detail-info-title\">\n              {intl.get('最后运行时间')}\n            </div>\n            <div className=\"cron-detail-info-value\">\n              {currentCron.last_execution_time\n                ? dayjs(currentCron.last_execution_time * 1000).format(\n                    'YYYY-MM-DD HH:mm:ss',\n                  )\n                : '-'}\n            </div>\n          </div>\n          <div className=\"cron-detail-info-item\">\n            <div className=\"cron-detail-info-title\">\n              {intl.get('最后运行时长')}\n            </div>\n            <div className=\"cron-detail-info-value\">\n              {currentCron.last_running_time\n                ? diffTime(currentCron.last_running_time)\n                : '-'}\n            </div>\n          </div>\n          <div className=\"cron-detail-info-item\">\n            <div className=\"cron-detail-info-title\">\n              {intl.get('下次运行时间')}\n            </div>\n            <div className=\"cron-detail-info-value\">\n              {currentCron.nextRunTime &&\n                dayjs(currentCron.nextRunTime).format('YYYY-MM-DD HH:mm:ss')}\n            </div>\n          </div>\n        </Card>\n        <Card\n          style={{ marginTop: 10 }}\n          tabList={validTabs}\n          activeTabKey={activeTabKey}\n          onTabChange={(key) => {\n            onTabChange(key);\n          }}\n          tabBarExtraContent={\n            activeTabKey === 'script' && (\n              <>\n                <Button\n                  type=\"primary\"\n                  style={{ marginRight: 8 }}\n                  onClick={saveFile}\n                >\n                  {intl.get('保存')}\n                </Button>\n                <Button\n                  type=\"primary\"\n                  icon={<FullscreenOutlined />}\n                  onClick={fullscreen}\n                />\n              </>\n            )\n          }\n        >\n          {contentList[activeTabKey]}\n        </Card>\n      </div>\n      {isLogModalVisible && (\n        <CronLogModal\n          handleCancel={() => {\n            setIsLogModalVisible(false);\n          }}\n          cron={cron}\n          data={log}\n          logUrl={logUrl}\n        />\n      )}\n    </Modal>\n  );\n};\n\nexport default CronDetailModal;\n"
  },
  {
    "path": "src/pages/crontab/index.less",
    "content": ".ant-table-pagination.ant-pagination {\n  margin-bottom: 0 !important;\n}\n\n.crontab-detail {\n  .card-wrapper {\n    .ant-card:last-child {\n      .ant-card-body {\n        min-height: 0;\n        height: calc(80vh - 314px);\n        height: calc(80vh - var(--vh-offset, 0px) - 314px);\n        overflow-y: auto;\n\n        > div {\n          height: 100%;\n        }\n      }\n    }\n  }\n\n  .ant-modal-body {\n    background: #eee;\n    padding: 12px;\n    max-height: calc(80vh - 57px);\n    max-height: calc(80vh - var(--vh-offset, 57px));\n    word-wrap: unset;\n  }\n\n  .ant-card-body {\n    padding: 18px;\n  }\n  .ant-card-head {\n    padding: 0 18px;\n  }\n\n  .ant-card:first-child {\n    max-height: 66px;\n    overflow: auto;\n\n    .ant-card-body {\n      min-width: 1000px;\n    }\n\n    .cron-detail-info-item {\n      display: flex;\n\n      .cron-detail-info-title {\n        width: 50px;\n      }\n\n      .cron-detail-info-value {\n        flex: 1;\n        margin-top: 0;\n      }\n    }\n  }\n\n  .ant-card:nth-child(2) {\n    overflow-x: auto;\n\n    .ant-card-body {\n      display: flex;\n      justify-content: space-between;\n      min-width: 1000px;\n    }\n  }\n\n  .cron-detail-info-item {\n    flex: auto;\n\n    .cron-detail-info-title {\n      color: #888;\n    }\n\n    .cron-detail-info-value {\n      margin-top: 12px;\n    }\n  }\n\n  .crontab-title-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 24px;\n\n    .operations {\n      display: flex;\n      align-items: center;\n\n      .ant-btn:not(:first-child) {\n        margin-left: 8px;\n      }\n    }\n  }\n}\n\n.log-item {\n  cursor: pointer;\n  &:hover {\n    background: #f2f2f2;\n  }\n}\n\n.crontab-view {\n  .ant-tabs-nav-wrap {\n    flex: unset !important;\n  }\n\n  .ant-tabs-nav-operations {\n    position: absolute;\n    visibility: hidden;\n    pointer-events: none;\n  }\n\n  .view-more {\n    margin-left: 32px;\n    padding: 8px 0;\n    cursor: pointer;\n\n    .ant-tabs-ink-bar {\n      width: 0;\n    }\n\n    &:hover,\n    &:focus-visible {\n      color: #1890ff;\n\n      .ant-tabs-ink-bar {\n        width: 50px;\n      }\n    }\n\n    &.active {\n      color: #1890ff;\n\n      .ant-tabs-ink-bar {\n        width: 50px;\n      }\n    }\n  }\n\n  &.more-active {\n    .ant-tabs-nav-list {\n      .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {\n        color: unset;\n      }\n\n      .ant-tabs-ink-bar {\n        width: 0 !important;\n      }\n    }\n  }\n}\n\n.view-create-modal-filters {\n  display: flex;\n\n  .ant-space-item:nth-child(3) {\n    flex: 1;\n  }\n}\n\n.view-create-modal-sorts {\n  display: flex;\n\n  .ant-space-item:nth-child(1) {\n    flex: 1;\n  }\n}\n\ntr.drop-over-downward td {\n  border-bottom: 2px dashed #1890ff;\n}\n\ntr.drop-over-upward td {\n  border-top: 2px dashed #1890ff;\n}\n\n.view-manage-modal {\n  .ant-modal-body {\n    padding-top: 10px;\n  }\n}\n\n.view-filters-container.active {\n  .filter-item > div > .ant-form-item-control {\n    margin-left: 40px;\n    width: calc(100% - 40px);\n  }\n}\n\nbody[data-mode='desktop'] {\n  .crontab-wrapper {\n    tbody .ant-table-cell {\n      height: 69px !important;\n    }\n  }\n}\n\n.cron.pinned-cron > td {\n  background: #f2f2f2;\n}\n"
  },
  {
    "path": "src/pages/crontab/index.tsx",
    "content": "import useTableScrollHeight from '@/hooks/useTableScrollHeight';\nimport { SharedContext } from '@/layouts';\nimport { getCommandScript, getCrontabsNextDate } from '@/utils';\nimport config from '@/utils/config';\nimport { diffTime } from '@/utils/date';\nimport { request } from '@/utils/http';\nimport {\n  CheckCircleOutlined,\n  CheckOutlined,\n  ClockCircleOutlined,\n  CloseCircleOutlined,\n  CopyOutlined,\n  DeleteOutlined,\n  DownOutlined,\n  EditOutlined,\n  EllipsisOutlined,\n  FieldTimeOutlined,\n  Loading3QuartersOutlined,\n  PlusOutlined,\n  PushpinOutlined,\n  SettingOutlined,\n  StopOutlined,\n  UnorderedListOutlined,\n} from '@ant-design/icons';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { history, useOutletContext } from '@umijs/max';\nimport {\n  Button,\n  Dropdown,\n  Input,\n  MenuProps,\n  message,\n  Modal,\n  Space,\n  Table,\n  TablePaginationConfig,\n  Tabs,\n  Tag,\n  Typography,\n} from 'antd';\nimport { ColumnProps } from 'antd/lib/table';\nimport { FilterValue, SorterResult } from 'antd/lib/table/interface';\nimport dayjs from 'dayjs';\nimport { noop, omit } from 'lodash';\nimport React, { useEffect, useRef, useState } from 'react';\nimport intl from 'react-intl-universal';\nimport { useVT } from 'virtualizedtableforantd4';\nimport { getScheduleType } from './const';\nimport CronDetailModal from './detail';\nimport './index.less';\nimport CronLogModal from './logModal';\nimport CronModal, { CronLabelModal } from './modal';\nimport {\n  CrontabStatus,\n  ICrontab,\n  OperationName,\n  OperationPath,\n  ScheduleType,\n} from './type';\nimport ViewCreateModal from './viewCreateModal';\nimport ViewManageModal from './viewManageModal';\n\nconst { Text, Paragraph, Link } = Typography;\nconst { Search } = Input;\nconst SHOW_TAB_COUNT = 10;\n\nconst Crontab = () => {\n  const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();\n  const [allSubscriptions, setAllSubscriptions] = useState<any[]>([]);\n  const columns: ColumnProps<ICrontab>[] = [\n    {\n      title: intl.get('名称'),\n      dataIndex: 'name',\n      key: 'name',\n      fixed: 'left',\n      width: 120,\n      render: (text: string, record: any) => (\n        <Paragraph\n          style={{\n            wordBreak: 'break-all',\n            marginBottom: 0,\n            color: '#1890ff',\n            cursor: 'pointer',\n          }}\n          ellipsis={{ tooltip: text, rows: 2 }}\n          onClick={() => {\n            setDetailCron(record);\n            setIsDetailModalVisible(true);\n          }}\n        >\n          <Link>{record.name || '-'}</Link>\n        </Paragraph>\n      ),\n      sorter: {\n        compare: (a, b) => a?.name?.localeCompare(b?.name),\n      },\n    },\n    {\n      title: intl.get('命令/脚本'),\n      dataIndex: 'command',\n      key: 'command',\n      width: 240,\n      render: (text, record) => {\n        return (\n          <Paragraph\n            style={{\n              wordBreak: 'break-all',\n              marginBottom: 0,\n            }}\n            ellipsis={{ tooltip: text, rows: 2 }}\n          >\n            <a\n              onClick={() => {\n                goToScriptManager(record);\n              }}\n            >\n              {text}\n            </a>\n          </Paragraph>\n        );\n      },\n      sorter: {\n        compare: (a: any, b: any) => a.command.localeCompare(b.command),\n      },\n    },\n    {\n      title: intl.get('状态'),\n      key: 'status',\n      dataIndex: 'status',\n      width: 100,\n      filters: [\n        {\n          text: intl.get('运行中'),\n          value: CrontabStatus.running,\n        },\n        {\n          text: intl.get('空闲中'),\n          value: CrontabStatus.idle,\n        },\n        {\n          text: intl.get('已禁用'),\n          value: CrontabStatus.disabled,\n        },\n        {\n          text: intl.get('队列中'),\n          value: CrontabStatus.queued,\n        },\n      ],\n      render: (text, record) => (\n        <>\n          {(!record.isDisabled || record.status !== CrontabStatus.idle) && (\n            <>\n              {record.status === CrontabStatus.idle && (\n                <Tag icon={<ClockCircleOutlined />} color=\"default\">\n                  {intl.get('空闲中')}\n                </Tag>\n              )}\n              {record.status === CrontabStatus.running && (\n                <Tag\n                  icon={<Loading3QuartersOutlined spin />}\n                  color=\"processing\"\n                >\n                  {intl.get('运行中')}\n                </Tag>\n              )}\n              {record.status === CrontabStatus.queued && (\n                <Tag icon={<FieldTimeOutlined />} color=\"default\">\n                  {intl.get('队列中')}\n                </Tag>\n              )}\n            </>\n          )}\n          {record.isDisabled === 1 && record.status === CrontabStatus.idle && (\n            <Tag icon={<CloseCircleOutlined />} color=\"error\">\n              {intl.get('已禁用')}\n            </Tag>\n          )}\n        </>\n      ),\n    },\n    {\n      title: intl.get('定时规则'),\n      dataIndex: 'schedule',\n      key: 'schedule',\n      width: 150,\n      sorter: {\n        compare: (a, b) => a.schedule.localeCompare(b.schedule),\n      },\n      render: (text, record) => {\n        return (\n          <Paragraph\n            style={{\n              wordBreak: 'break-all',\n              marginBottom: 0,\n            }}\n            ellipsis={{\n              tooltip: {\n                placement: 'right',\n                title: (\n                  <>\n                    <div>{text}</div>\n                    {record.extra_schedules?.map((x) => (\n                      <div key={x.schedule}>{x.schedule}</div>\n                    ))}\n                  </>\n                ),\n              },\n              rows: 2,\n            }}\n          >\n            {text}\n          </Paragraph>\n        );\n      },\n    },\n    {\n      title: intl.get('最后运行时长'),\n      width: 167,\n      dataIndex: 'last_running_time',\n      key: 'last_running_time',\n      sorter: {\n        compare: (a: any, b: any) => {\n          return a.last_running_time - b.last_running_time;\n        },\n      },\n      render: (text, record) => {\n        return record.last_running_time\n          ? diffTime(record.last_running_time)\n          : '-';\n      },\n    },\n    {\n      title: intl.get('最后运行时间'),\n      dataIndex: 'last_execution_time',\n      key: 'last_execution_time',\n      width: 141,\n      sorter: {\n        compare: (a, b) => {\n          return (a.last_execution_time || 0) - (b.last_execution_time || 0);\n        },\n      },\n      render: (text, record) => {\n        return (\n          <span\n            style={{\n              display: 'block',\n            }}\n          >\n            {record.last_execution_time\n              ? dayjs(record.last_execution_time * 1000).format(\n                'YYYY-MM-DD HH:mm:ss',\n              )\n              : '-'}\n          </span>\n        );\n      },\n    },\n    {\n      title: intl.get('下次运行时间'),\n      width: 144,\n      sorter: {\n        compare: (a: any, b: any) => {\n          return a.nextRunTime - b.nextRunTime;\n        },\n      },\n      render: (text, record) => {\n        return record.nextRunTime\n          ? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')\n          : '-';\n      },\n    },\n    {\n      title: intl.get('关联订阅'),\n      width: 185,\n      render: (text, record: any) => record?.subscription?.name || '-',\n      key: 'sub_id',\n      dataIndex: 'sub_id',\n      filters: allSubscriptions.map((sub) => ({\n        text: sub.name || sub.alias,\n        value: sub.id,\n      })),\n    },\n    {\n      title: intl.get('操作'),\n      key: 'action',\n      width: 140,\n      fixed: isPhone ? undefined : 'right',\n      render: (text, record, index) => {\n        const isPc = !isPhone;\n        return (\n          <Space size=\"middle\">\n            {record.status === CrontabStatus.idle && (\n              <a\n                onClick={(e) => {\n                  e.stopPropagation();\n                  runCron(record, index);\n                }}\n              >\n                {intl.get('运行')}\n              </a>\n            )}\n            {record.status !== CrontabStatus.idle && (\n              <a\n                onClick={(e) => {\n                  e.stopPropagation();\n                  stopCron(record, index);\n                }}\n              >\n                {intl.get('停止')}\n              </a>\n            )}\n            <a\n              onClick={(e) => {\n                e.stopPropagation();\n                setLogCron({ ...record, timestamp: Date.now() });\n              }}\n            >\n              {intl.get('日志')}\n            </a>\n            <MoreBtn key=\"more\" record={record} index={index} />\n          </Space>\n        );\n      },\n    },\n  ];\n\n  const [value, setValue] = useState<any[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [isLabelModalVisible, setIsLabelModalVisible] = useState(false);\n  const [editedCron, setEditedCron] = useState();\n  const [searchText, setSearchText] = useState('');\n  const [isLogModalVisible, setIsLogModalVisible] = useState(false);\n  const [logCron, setLogCron] = useState<any>();\n  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);\n  const [pageConf, setPageConf] = useState<{\n    page: number;\n    size: number;\n    sorter: any;\n    filters: any;\n  }>({} as any);\n  const [viewConf, setViewConf] = useState<any>();\n  const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);\n  const [detailCron, setDetailCron] = useState<any>();\n  const [searchValue, setSearchValue] = useState('');\n  const [total, setTotal] = useState<number>();\n  const [isCreateViewModalVisible, setIsCreateViewModalVisible] =\n    useState(false);\n  const [isViewManageModalVisible, setIsViewManageModalVisible] =\n    useState(false);\n  const [cronViews, setCronViews] = useState<any[]>([]);\n  const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);\n  const [moreMenuActive, setMoreMenuActive] = useState(false);\n  const tableRef = useRef<HTMLDivElement>(null);\n  const tableScrollHeight = useTableScrollHeight(tableRef);\n  const [activeKey, setActiveKey] = useState('');\n\n  const goToScriptManager = (record: any) => {\n    const result = getCommandScript(record.command);\n    if (Array.isArray(result)) {\n      const [s, p] = result;\n      history.push(`/script?p=${p}&s=${s}`);\n    } else if (result) {\n      location.href = result;\n    }\n  };\n\n  const getCrons = () => {\n    setLoading(true);\n    const { page, size, sorter, filters } = pageConf;\n    let url = `${config.apiPrefix\n      }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(\n        filters,\n      )}`;\n    if (sorter && sorter.column && sorter.order) {\n      url += `&sorter=${JSON.stringify({\n        field: sorter.column.key,\n        type: sorter.order === 'ascend' ? 'ASC' : 'DESC',\n      })}`;\n    }\n    if (viewConf) {\n      url += `&queryString=${JSON.stringify({\n        filters: viewConf.filters,\n        sorts: viewConf.sorts,\n        filterRelation: viewConf.filterRelation || 'and',\n      })}`;\n    }\n    request\n      .get(url)\n      .then(async ({ code, data: _data }) => {\n        if (code === 200) {\n          const { data, total } = _data;\n          const subscriptions = await request.get(\n            `${config.apiPrefix}subscriptions?ids=${JSON.stringify([\n              ...new Set(data.map((x) => x.sub_id).filter(Boolean)),\n            ])}`,\n            {\n              onError: noop,\n            },\n          );\n          const subscriptionMap = Object.fromEntries(\n            subscriptions?.data?.map((x) => [x.id, x]),\n          );\n\n          setValue(\n            data.map((x) => {\n              const scheduleType = getScheduleType(x.schedule);\n              const nextRunTime =\n                scheduleType === ScheduleType.Normal\n                  ? getCrontabsNextDate(x.schedule, x.extra_schedules)\n                  : null;\n              return {\n                ...x,\n                nextRunTime,\n                subscription: subscriptionMap?.[x.sub_id],\n              };\n            }),\n          );\n          setTotal(total);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const addCron = () => {\n    setEditedCron(null as any);\n    setIsModalVisible(true);\n  };\n\n  const editCron = (record: any, index: number) => {\n    setEditedCron(record);\n    setIsModalVisible(true);\n  };\n\n  const delCron = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: (\n        <>\n          {intl.get('确认删除定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}crons`, { data: [record.id] })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('删除成功'));\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1);\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const runCron = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认运行'),\n      content: (\n        <>\n          {intl.get('确认运行定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}crons/run`, [record.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  status: CrontabStatus.running,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const stopCron = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认停止'),\n      content: (\n        <>\n          {intl.get('确认停止定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}crons/stop`, [record.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  pid: null,\n                  status: CrontabStatus.idle,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const enabledOrDisabledCron = (record: any, index: number) => {\n    Modal.confirm({\n      title: `确认${record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')\n        }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')}\n          {intl.get('定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'\n            }`,\n            [record.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const newStatus = record.isDisabled === 1 ? 0 : 1;\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  isDisabled: newStatus,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const pinOrUnPinCron = (record: any, index: number) => {\n    Modal.confirm({\n      title: `确认${record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')\n        }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}\n          {intl.get('定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'\n            }`,\n            [record.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const newStatus = record.isPinned === 1 ? 0 : 1;\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  isPinned: newStatus,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const getMenuItems = (record: any) => {\n    return [\n      { label: intl.get('编辑'), key: 'edit', icon: <EditOutlined /> },\n      {\n        label: record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用'),\n        key: 'enableOrDisable',\n        icon:\n          record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />,\n      },\n      { label: intl.get('复制'), key: 'copy', icon: <CopyOutlined /> },\n      { label: intl.get('删除'), key: 'delete', icon: <DeleteOutlined /> },\n      {\n        label: record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'),\n        key: 'pinOrUnPin',\n        icon: record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />,\n      },\n    ];\n  };\n\n  const MoreBtn: React.FC<{\n    record: any;\n    index: number;\n  }> = ({ record, index }) => (\n    <Dropdown\n      placement=\"bottomRight\"\n      trigger={['click']}\n      menu={{\n        items: getMenuItems(record),\n        onClick: ({ key, domEvent }) => {\n          domEvent.stopPropagation();\n          action(key, record, index);\n        },\n      }}\n    >\n      <a onClick={(e) => e.stopPropagation()}>\n        <EllipsisOutlined />\n      </a>\n    </Dropdown>\n  );\n\n  const action = (key: string | number, record: any, index: number) => {\n    switch (key) {\n      case 'edit':\n        editCron(record, index);\n        break;\n      case 'copy':\n        editCron(omit(record, 'id'), index);\n        break;\n      case 'enableOrDisable':\n        enabledOrDisabledCron(record, index);\n        break;\n      case 'delete':\n        delCron(record, index);\n        break;\n      case 'pinOrUnPin':\n        pinOrUnPinCron(record, index);\n        break;\n      default:\n        break;\n    }\n  };\n\n  const handleCancel = () => {\n    setIsModalVisible(false);\n    getCrons();\n  };\n\n  const onSearch = (value: string) => {\n    setSearchText(value.trim());\n  };\n\n  const getCronDetail = (cron: any) => {\n    request\n      .get(`${config.apiPrefix}crons/${cron.id}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          const index = value.findIndex((x) => x.id === cron.id);\n          const result = [...value];\n          data.nextRunTime = getCrontabsNextDate(\n            data.schedule,\n            data.extra_schedules,\n          );\n          if (index !== -1) {\n            result.splice(index, 1, {\n              ...cron,\n              ...data,\n            });\n            setValue(result);\n          }\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const onSelectChange = (selectedIds: any[]) => {\n    setSelectedRowIds(selectedIds);\n  };\n\n  const rowSelection = {\n    selectedRowKeys: selectedRowIds,\n    onChange: onSelectChange,\n  };\n\n  const delCrons = () => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: <>{intl.get('确认删除选中的定时任务吗')}</>,\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}crons`, { data: selectedRowIds })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('批量删除成功'));\n              setSelectedRowIds([]);\n              getCrons();\n            }\n          });\n      },\n    });\n  };\n\n  const operateCrons = (operationStatus: number) => {\n    Modal.confirm({\n      title: `确认${OperationName[operationStatus]}`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {OperationName[operationStatus]}\n          {intl.get('选中的定时任务吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}crons/${OperationPath[operationStatus]}`,\n            selectedRowIds,\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              getCrons();\n            }\n          });\n      },\n    });\n  };\n\n  const onPageChange = (\n    pagination: TablePaginationConfig,\n    filters: Record<string, FilterValue | null>,\n    sorter: SorterResult<any> | SorterResult<any>[],\n  ) => {\n    const { current, pageSize } = pagination;\n    setPageConf({\n      page: current as number,\n      size: pageSize as number,\n      sorter,\n      filters,\n    });\n    localStorage.setItem('pageSize', String(pageSize));\n  };\n\n  const getRowClassName = (record: any, index: number) => {\n    return record.isPinned ? 'pinned-cron cron' : 'cron';\n  };\n\n  useEffect(() => {\n    if (logCron) {\n      localStorage.setItem('logCron', logCron.id);\n      setIsLogModalVisible(true);\n    }\n  }, [logCron]);\n\n  useEffect(() => {\n    setPageConf({ ...pageConf, page: 1 });\n  }, [searchText]);\n\n  useEffect(() => {\n    if (pageConf.page && pageConf.size) {\n      getCrons();\n    }\n    if (viewConf && viewConf.id) {\n      setActiveKey(viewConf.id);\n    }\n  }, [pageConf, viewConf]);\n\n  useEffect(() => {\n    if (viewConf && enabledCronViews && enabledCronViews.length > 0) {\n      const view = enabledCronViews\n        .slice(SHOW_TAB_COUNT)\n        .find((x) => x.id === viewConf.id);\n      setMoreMenuActive(!!view);\n    }\n  }, [viewConf, enabledCronViews]);\n\n  const getAllSubscriptions = () => {\n    request\n      .get(`${config.apiPrefix}subscriptions`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setAllSubscriptions(data || []);\n        }\n      })\n      .catch(() => {});\n  };\n\n  useEffect(() => {\n    getCronViews();\n    getAllSubscriptions();\n  }, []);\n\n  const viewAction = (key: string) => {\n    switch (key) {\n      case 'new':\n        setIsCreateViewModalVisible(true);\n        break;\n      case 'manage':\n        setIsViewManageModalVisible(true);\n        break;\n\n      default:\n        tabClick(key);\n        break;\n    }\n  };\n\n  const menu: MenuProps = {\n    onClick: ({ key, domEvent }) => {\n      domEvent.stopPropagation();\n      viewAction(key);\n    },\n    items: [\n      ...[...enabledCronViews].slice(SHOW_TAB_COUNT).map((x) => ({\n        label: (\n          <Space style={{ display: 'flex', justifyContent: 'space-between' }}>\n            <span>{x.name}</span>\n            {viewConf?.id === x.id && (\n              <CheckOutlined style={{ color: '#1890ff' }} />\n            )}\n          </Space>\n        ),\n        key: x.id,\n        icon: <UnorderedListOutlined />,\n      })),\n      {\n        type: 'divider' as 'group',\n      },\n      {\n        label: intl.get('创建视图'),\n        key: 'new',\n        icon: <PlusOutlined />,\n      },\n      {\n        label: intl.get('视图管理'),\n        key: 'manage',\n        icon: <SettingOutlined />,\n      },\n    ],\n    style: {\n      maxHeight: 350,\n      overflowY: 'auto',\n    },\n  };\n\n  const getCronViews = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}crons/views`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setCronViews(data);\n          const firstEnableView = data\n            .filter((x) => !x.isDisabled)\n            .map((x) => ({\n              ...x,\n              name: x.name === '全部任务' ? intl.get('全部任务') : x.name,\n            }));\n          setEnabledCronViews(firstEnableView);\n          setPageConf({\n            page: 1,\n            size: parseInt(localStorage.getItem('pageSize') || '20'),\n            sorter: {},\n            filters: {},\n          });\n          setViewConf({\n            ...firstEnableView[0],\n          });\n        }\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  };\n\n  const tabClick = (key: string) => {\n    const view = enabledCronViews.find((x) => x.id == key);\n    setSelectedRowIds([]);\n    setPageConf({ ...pageConf, page: 1 });\n    setViewConf(view ? view : null);\n  };\n\n  const [vt] = useVT(\n    () => ({ scroll: { y: tableScrollHeight } }),\n    [tableScrollHeight],\n  );\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab\"\n      title={intl.get('定时任务')}\n      extra={[\n        <Search\n          placeholder={intl.get('请输入名称或者关键词')}\n          style={{ width: 'auto' }}\n          enterButton\n          allowClear\n          loading={loading}\n          value={searchValue}\n          onChange={(e) => setSearchValue(e.target.value)}\n          onSearch={onSearch}\n        />,\n        <Button key=\"2\" type=\"primary\" onClick={() => addCron()}>\n          {intl.get('创建任务')}\n        </Button>,\n      ]}\n      header={{\n        style: headerStyle,\n      }}\n    >\n      <Tabs\n        defaultActiveKey=\"all\"\n        size=\"small\"\n        activeKey={activeKey}\n        tabPosition=\"top\"\n        className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}\n        tabBarExtraContent={\n          <Dropdown\n            menu={menu}\n            trigger={['click']}\n            overlayStyle={{ minWidth: 200 }}\n          >\n            <div className={`view-more ${moreMenuActive ? 'active' : ''}`}>\n              <Space>\n                {intl.get('更多')}\n                <DownOutlined />\n              </Space>\n              <div className=\"ant-tabs-ink-bar ant-tabs-ink-bar-animated\"></div>\n            </div>\n          </Dropdown>\n        }\n        onTabClick={tabClick}\n        items={[\n          ...[...enabledCronViews].slice(0, SHOW_TAB_COUNT).map((x) => ({\n            key: x.id,\n            label: x.name,\n          })),\n        ]}\n      />\n      <div ref={tableRef}>\n        {selectedRowIds.length > 0 && (\n          <div style={{ marginBottom: 16 }}>\n            <Button\n              type=\"primary\"\n              style={{ marginBottom: 5 }}\n              onClick={delCrons}\n            >\n              {intl.get('批量删除')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateCrons(0)}\n              style={{ marginLeft: 8, marginBottom: 5 }}\n            >\n              {intl.get('批量启用')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateCrons(1)}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量禁用')}\n            </Button>\n            <Button\n              type=\"primary\"\n              style={{ marginRight: 8 }}\n              onClick={() => operateCrons(2)}\n            >\n              {intl.get('批量运行')}\n            </Button>\n            <Button type=\"primary\" onClick={() => operateCrons(3)}>\n              {intl.get('批量停止')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateCrons(4)}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量置顶')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateCrons(5)}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量取消置顶')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => setIsLabelModalVisible(true)}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量修改标签')}\n            </Button>\n            <span style={{ marginLeft: 8 }}>\n              {intl.get('已选择')}\n              <a>{selectedRowIds?.length}</a>\n              {intl.get('项')}\n            </span>\n          </div>\n        )}\n        <Table\n          columns={columns}\n          sortDirections={['descend', 'ascend']}\n          pagination={{\n            current: pageConf.page,\n            pageSize: pageConf.size,\n            showSizeChanger: true,\n            simple: isPhone,\n            total,\n            showTotal: (total: number, range: number[]) =>\n              `第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,\n            pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(\n              (a, b) => a - b,\n            ),\n          }}\n          dataSource={value}\n          rowKey=\"id\"\n          size=\"middle\"\n          scroll={{ x: 1200, y: tableScrollHeight }}\n          loading={loading}\n          rowSelection={rowSelection}\n          rowClassName={getRowClassName}\n          onChange={onPageChange}\n          components={isPhone || pageConf.size < 50 ? undefined : vt}\n        />\n      </div>\n      {isLogModalVisible && (\n        <CronLogModal\n          handleCancel={() => {\n            getCronDetail(logCron);\n            setIsLogModalVisible(false);\n          }}\n          cron={logCron}\n        />\n      )}\n      {isModalVisible && (\n        <CronModal handleCancel={handleCancel} cron={editedCron} />\n      )}\n      {isLabelModalVisible && (\n        <CronLabelModal\n          handleCancel={(needUpdate?: boolean) => {\n            setIsLabelModalVisible(false);\n            if (needUpdate) {\n              getCrons();\n            }\n          }}\n          ids={selectedRowIds}\n        />\n      )}\n      {isDetailModalVisible && (\n        <CronDetailModal\n          handleCancel={() => {\n            setIsDetailModalVisible(false);\n          }}\n          cron={detailCron}\n          theme={theme}\n          isPhone={isPhone}\n        />\n      )}\n      {isCreateViewModalVisible && (\n        <ViewCreateModal\n          handleCancel={(data) => {\n            setIsCreateViewModalVisible(false);\n            getCronViews();\n          }}\n        />\n      )}\n      {isViewManageModalVisible && (\n        <ViewManageModal\n          cronViews={cronViews}\n          handleCancel={() => {\n            setIsViewManageModalVisible(false);\n          }}\n          cronViewChange={(data) => {\n            getCronViews();\n          }}\n        />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Crontab;\n"
  },
  {
    "path": "src/pages/crontab/logModal.tsx",
    "content": "import intl from \"react-intl-universal\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport {\n  Modal,\n  message,\n  Input,\n  Form,\n  Statistic,\n  Button,\n  Typography,\n} from \"antd\";\nimport { request } from \"@/utils/http\";\nimport config from \"@/utils/config\";\nimport {\n  Loading3QuartersOutlined,\n  CheckCircleOutlined,\n} from \"@ant-design/icons\";\nimport { PageLoading } from \"@ant-design/pro-layout\";\nimport { logEnded } from \"@/utils\";\nimport { CrontabStatus } from \"./type\";\nimport Ansi from \"ansi-to-react\";\n\nconst { Countdown } = Statistic;\n\nconst CronLogModal = ({\n  cron,\n  handleCancel,\n  data,\n  logUrl,\n}: {\n  cron?: any;\n  handleCancel: () => void;\n  data?: string;\n  logUrl?: string;\n}) => {\n  const [value, setValue] = useState<string>(intl.get(\"启动中...\"));\n  const [loading, setLoading] = useState<any>(true);\n  const [executing, setExecuting] = useState<any>(true);\n  const [isPhone, setIsPhone] = useState(false);\n  const scrollInfoRef = useRef({ value: 0, down: true });\n  const uniqPath = logUrl ? logUrl : String(cron?.id);\n\n  const getCronLog = (isFirst?: boolean) => {\n    if (isFirst) {\n      setLoading(true);\n    }\n    request\n      .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)\n      .then(({ code, data }) => {\n        if (\n          code === 200 &&\n          localStorage.getItem(\"logCron\") === uniqPath &&\n          data !== value\n        ) {\n          const log = data as string;\n          setValue(log || intl.get(\"暂无日志\"));\n          const hasNext = Boolean(\n            log && !logEnded(log) && !log.includes(\"日志不存在\") && !log.includes(\"日志设置为忽略\"),\n          );\n          if (!hasNext && !logEnded(value) && value !== intl.get(\"启动中...\")) {\n            setTimeout(() => {\n              autoScroll();\n            });\n          }\n          setExecuting(hasNext);\n          if (hasNext) {\n            setTimeout(() => {\n              autoScroll();\n              getCronLog();\n            }, 2000);\n          }\n        }\n      })\n      .finally(() => {\n        if (isFirst) {\n          setLoading(false);\n        }\n      });\n  };\n\n  const autoScroll = () => {\n    if (!scrollInfoRef.current.down) {\n      return;\n    }\n\n    setTimeout(() => {\n      document\n        .querySelector(\"#log-flag\")\n        ?.scrollIntoView({ behavior: \"smooth\" });\n    }, 600);\n  };\n\n  const cancel = () => {\n    localStorage.removeItem(\"logCron\");\n    handleCancel();\n  };\n\n  const handleScroll: React.UIEventHandler<HTMLDivElement> = (e) => {\n    const sTop = (e.target as HTMLDivElement).scrollTop;\n    if (scrollInfoRef.current.down) {\n      scrollInfoRef.current = {\n        value: sTop,\n        down: sTop - scrollInfoRef.current.value > -5 || !sTop,\n      };\n    }\n  };\n\n  const titleElement = () => {\n    return (\n      <div style={{ display: \"flex\", alignItems: \"center\" }}>\n        {(executing || loading) && <Loading3QuartersOutlined spin />}\n        {!executing && !loading && <CheckCircleOutlined />}\n        <Typography.Text ellipsis={true} style={{ marginLeft: 5 }}>\n          {cron && cron.name}\n        </Typography.Text>\n      </div>\n    );\n  };\n\n  useEffect(() => {\n    if (cron && cron.id) {\n      getCronLog(true);\n    }\n  }, [cron]);\n\n  useEffect(() => {\n    if (data) {\n      setValue(data);\n    }\n  }, [data]);\n\n  useEffect(() => {\n    setIsPhone(document.body.clientWidth < 768);\n  }, []);\n\n  return (\n    <Modal\n      title={titleElement()}\n      open={true}\n      centered\n      className=\"log-modal\"\n      forceRender\n      onOk={() => cancel()}\n      onCancel={() => cancel()}\n      footer={[\n        <Button type=\"primary\" onClick={() => cancel()}>\n          {intl.get(\"知道了\")}\n        </Button>,\n      ]}\n    >\n      <div onScroll={handleScroll} className=\"log-container\">\n        {loading ? (\n          <PageLoading />\n        ) : (\n          <pre\n            style={\n              isPhone\n                ? {\n                  fontFamily: \"Source Code Pro\",\n                  zoom: 0.83,\n                }\n                : {}\n            }\n          >\n            <Ansi>{value}</Ansi>\n          </pre>\n        )}\n        <div id=\"log-flag\"></div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default CronLogModal;\n"
  },
  {
    "path": "src/pages/crontab/modal.tsx",
    "content": "import EditableTagGroup from '@/components/tag';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';\nimport { Button, Form, Input, Modal, Select, Space, message } from 'antd';\nimport CronExpressionParser from 'cron-parser';\nimport { useEffect, useState } from 'react';\nimport intl from 'react-intl-universal';\nimport { getScheduleType, scheduleTypeMap } from './const';\nimport { ScheduleType } from './type';\n\nconst CronModal = ({\n  cron,\n  handleCancel,\n}: {\n  cron?: any;\n  handleCancel: (needUpdate?: boolean) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n  const [scheduleType, setScheduleType] = useState<ScheduleType>(\n    cron ? getScheduleType(cron.schedule) : ScheduleType.Normal,\n  );\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    try {\n      const method = cron?.id ? 'put' : 'post';\n      const payload = {\n        ...values,\n        schedule:\n          scheduleType !== ScheduleType.Normal\n            ? scheduleTypeMap[scheduleType]\n            : values.schedule,\n      };\n\n      if (cron?.id) {\n        payload.id = cron.id;\n      }\n\n      const { code, data } = await request[method](\n        `${config.apiPrefix}crons`,\n        payload,\n      );\n\n      if (code === 200) {\n        message.success(\n          cron?.id ? intl.get('更新任务成功') : intl.get('创建任务成功'),\n        );\n        handleCancel(data);\n      }\n    } catch (error: any) {\n      console.error(error);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleScheduleTypeChange = (type: ScheduleType) => {\n    setScheduleType(type);\n    form.setFieldValue('schedule', '');\n  };\n\n  const renderScheduleOptions = () => (\n    <Select\n      defaultValue={scheduleType}\n      value={scheduleType}\n      onChange={handleScheduleTypeChange}\n    >\n      <Select.Option value={ScheduleType.Normal}>\n        {intl.get('常规定时')}\n      </Select.Option>\n      <Select.Option value={ScheduleType.Once}>\n        {intl.get('手动运行')}\n      </Select.Option>\n      <Select.Option value={ScheduleType.Boot}>\n        {intl.get('开机运行')}\n      </Select.Option>\n    </Select>\n  );\n\n  const renderScheduleFields = () => {\n    if (scheduleType !== ScheduleType.Normal) return null;\n\n    return (\n      <>\n        <Form.Item\n          name=\"schedule\"\n          label={intl.get('定时规则')}\n          rules={[\n            { required: true },\n            {\n              validator: (_, value) => {\n                try {\n                  if (!value || CronExpressionParser.parse(value).hasNext()) {\n                    return Promise.resolve();\n                  }\n                  return Promise.reject(intl.get('Cron表达式格式有误'));\n                } catch (e) {\n                  return Promise.reject(intl.get('Cron表达式格式有误'));\n                }\n              },\n            },\n          ]}\n        >\n          <Input placeholder={intl.get('秒(可选) 分 时 天 月 周')} />\n        </Form.Item>\n        <Form.List name=\"extra_schedules\">\n          {(fields, { add, remove }, { errors }) => (\n            <>\n              {fields.map(({ key, name, ...restField }) => (\n                <Form.Item key={key} noStyle>\n                  <Space className=\"view-create-modal-sorts\" align=\"baseline\">\n                    <Form.Item\n                      {...restField}\n                      name={[name, 'schedule']}\n                      rules={[{ required: true }]}\n                    >\n                      <Input\n                        placeholder={intl.get('秒(可选) 分 时 天 月 周')}\n                      />\n                    </Form.Item>\n                    <MinusCircleOutlined\n                      className=\"dynamic-delete-button\"\n                      onClick={() => remove(name)}\n                    />\n                  </Space>\n                </Form.Item>\n              ))}\n              <Form.Item>\n                <a onClick={() => add({ schedule: '' })}>\n                  <PlusOutlined /> {intl.get('新增定时规则')}\n                </a>\n              </Form.Item>\n              <Form.ErrorList errors={errors} />\n            </>\n          )}\n        </Form.List>\n      </>\n    );\n  };\n\n  return (\n    <Modal\n      title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => form.validateFields().then(handleOk)}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        layout=\"vertical\"\n        name=\"form_in_modal\"\n        initialValues={cron}\n      >\n        <Form.Item\n          name=\"name\"\n          label={intl.get('名称')}\n          rules={[{ required: true, whitespace: true }]}\n        >\n          <Input placeholder={intl.get('请输入任务名称')} />\n        </Form.Item>\n        <Form.Item\n          name=\"command\"\n          label={intl.get('命令/脚本')}\n          rules={[{ required: true, whitespace: true }]}\n        >\n          <Input.TextArea\n            rows={4}\n            autoSize={{ minRows: 1, maxRows: 5 }}\n            placeholder={intl.get(\n              '支持输入脚本路径/任意系统可执行命令/task 脚本路径',\n            )}\n          />\n        </Form.Item>\n        <Form.Item label={intl.get('定时类型')} required>\n          {renderScheduleOptions()}\n        </Form.Item>\n        {renderScheduleFields()}\n        <Form.Item name=\"labels\" label={intl.get('标签')}>\n          <EditableTagGroup />\n        </Form.Item>\n        <Form.Item\n          name=\"allow_multiple_instances\"\n          label={intl.get('实例模式')}\n          tooltip={intl.get(\n            '单实例模式：定时启动新任务前会自动停止旧任务；多实例模式：允许同时运行多个任务实例',\n          )}\n        >\n          <Select placeholder={intl.get('请选择实例模式')}>\n            <Select.Option value={0}>{intl.get('单实例')}</Select.Option>\n            <Select.Option value={1}>{intl.get('多实例')}</Select.Option>\n          </Select>\n        </Form.Item>\n        <Form.Item\n          name=\"log_name\"\n          label={intl.get('日志名称')}\n          tooltip={intl.get(\n            '自定义日志文件夹名称，用于区分不同任务的日志，留空则自动生成。支持 /dev/null 丢弃日志，其他绝对路径必须在日志目录内',\n          )}\n          rules={[\n            {\n              validator: (_, value) => {\n                if (!value) return Promise.resolve();\n                if (value === '/dev/null') return Promise.resolve();\n                if (value.length > 100) {\n                  return Promise.reject(intl.get('日志名称不能超过100个字符'));\n                }\n                if (\n                  !/^(?!.*(?:^|\\/)\\.{1,2}(?:\\/|$))(?:\\/)?(?:[\\w.-]+\\/)*[\\w.-]+\\/?$/.test(\n                    value,\n                  )\n                ) {\n                  return Promise.reject(\n                    intl.get('日志名称只能包含字母、数字、下划线和连字符'),\n                  );\n                }\n                return Promise.resolve();\n              },\n            },\n          ]}\n        >\n          <Input\n            placeholder={intl.get('请输入自定义日志文件夹名称或 /dev/null')}\n            maxLength={200}\n          />\n        </Form.Item>\n        <Form.Item\n          name=\"task_before\"\n          label={intl.get('执行前')}\n          tooltip={intl.get(\n            '运行任务前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js',\n          )}\n          rules={[\n            {\n              validator(_, value) {\n                if (\n                  value &&\n                  (value.includes(' task ') || value.startsWith('task '))\n                ) {\n                  return Promise.reject(intl.get('不能包含 task 命令'));\n                }\n                return Promise.resolve();\n              },\n            },\n          ]}\n        >\n          <Input.TextArea\n            rows={4}\n            autoSize={{ minRows: 1, maxRows: 5 }}\n            placeholder={intl.get(\n              '请输入运行任务前要执行的命令，不能包含 task 命令',\n            )}\n          />\n        </Form.Item>\n        <Form.Item\n          name=\"task_after\"\n          label={intl.get('执行后')}\n          tooltip={intl.get(\n            '运行任务后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js',\n          )}\n          rules={[\n            {\n              validator(_, value) {\n                if (\n                  value &&\n                  (value.includes(' task ') || value.startsWith('task '))\n                ) {\n                  return Promise.reject(intl.get('不能包含 task 命令'));\n                }\n                return Promise.resolve();\n              },\n            },\n          ]}\n        >\n          <Input.TextArea\n            rows={4}\n            autoSize={{ minRows: 1, maxRows: 5 }}\n            placeholder={intl.get(\n              '请输入运行任务后要执行的命令，不能包含 task 命令',\n            )}\n          />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nconst CronLabelModal = ({\n  ids,\n  handleCancel,\n}: {\n  ids: Array<string>;\n  handleCancel: (needUpdate?: boolean) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const update = async (action: 'delete' | 'post') => {\n    form\n      .validateFields()\n      .then(async (values) => {\n        setLoading(true);\n        const payload = { ids, labels: values.labels };\n        try {\n          const { code, data } = await request[action](\n            `${config.apiPrefix}crons/labels`,\n            payload,\n          );\n\n          if (code === 200) {\n            message.success(\n              action === 'post'\n                ? intl.get('添加Labels成功')\n                : intl.get('删除Labels成功'),\n            );\n            handleCancel(true);\n          }\n          setLoading(false);\n        } catch (error) {\n          setLoading(false);\n        }\n      })\n      .catch((info) => {\n        console.log('Validate Failed:', info);\n      });\n  };\n\n  const buttons = [\n    <Button onClick={() => handleCancel(false)}>{intl.get('取消')}</Button>,\n    <Button type=\"primary\" danger onClick={() => update('delete')}>\n      {intl.get('删除')}\n    </Button>,\n    <Button type=\"primary\" onClick={() => update('post')}>\n      {intl.get('添加')}\n    </Button>,\n  ];\n\n  return (\n    <Modal\n      title={intl.get('批量修改标签')}\n      open={true}\n      footer={buttons}\n      centered\n      maskClosable={false}\n      forceRender\n      onCancel={() => handleCancel(false)}\n      confirmLoading={loading}\n    >\n      <Form form={form} layout=\"vertical\" name=\"form_in_label_modal\">\n        <Form.Item name=\"labels\" label={intl.get('标签')}>\n          <EditableTagGroup />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport { CronLabelModal, CronModal as default };\n"
  },
  {
    "path": "src/pages/crontab/type.ts",
    "content": "export enum CrontabStatus {\n  'running' = 0,\n  'queued' = 0.5,\n  'idle' = 1,\n  'disabled',\n}\n\nexport enum OperationName {\n  '启用',\n  '禁用',\n  '运行',\n  '停止',\n  '置顶',\n  '取消置顶',\n}\n\nexport enum OperationPath {\n  'enable',\n  'disable',\n  'run',\n  'stop',\n  'pin',\n  'unpin',\n}\n\nexport interface ICrontab {\n  name: string;\n  command: string;\n  schedule: string;\n  id: number;\n  status: number;\n  isDisabled?: 1 | 0;\n  isPinned?: 1 | 0;\n  labels?: string[];\n  last_running_time?: number;\n  last_execution_time?: number;\n  nextRunTime: Date;\n  sub_id: number;\n  extra_schedules?: Array<{ schedule: string }>;\n  allow_multiple_instances?: 1 | 0;\n}\n\nexport enum ScheduleType {\n  Normal = 'normal',\n  Once = 'once',\n  Boot = 'boot',\n}\n"
  },
  {
    "path": "src/pages/crontab/viewCreateModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport {\n  Modal,\n  message,\n  Input,\n  Form,\n  Statistic,\n  Button,\n  Space,\n  Select,\n} from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';\nimport IconFont from '@/components/iconfont';\nimport { CrontabStatus } from './type';\nimport { useRequest } from 'ahooks';\n\nconst PROPERTIES = [\n  { name: intl.get('命令'), value: 'command' },\n  { name: intl.get('名称'), value: 'name' },\n  { name: intl.get('定时规则'), value: 'schedule' },\n  { name: intl.get('状态'), value: 'status', onlySelect: true },\n  { name: intl.get('标签'), value: 'labels' },\n  { name: intl.get('订阅'), value: 'sub_id', onlySelect: true },\n];\n\nconst EOperation: any = {\n  Reg: '',\n  NotReg: '',\n  In: 'select',\n  Nin: 'select',\n};\nconst OPERATIONS = [\n  { name: intl.get('包含'), value: 'Reg' },\n  { name: intl.get('不包含'), value: 'NotReg' },\n  { name: intl.get('属于'), value: 'In', type: 'select' },\n  { name: intl.get('不属于'), value: 'Nin', type: 'select' },\n  // { name: '等于', value: 'Eq' },\n  // { name: '不等于', value: 'Ne' },\n  // { name: '为空', value: 'IsNull' },\n  // { name: '不为空', value: 'NotNull' },\n];\n\nconst SORTTYPES = [\n  { name: intl.get('顺序'), value: 'ASC' },\n  { name: intl.get('倒序'), value: 'DESC' },\n];\n\nenum ViewFilterRelation {\n  'and' = '且',\n  'or' = '或',\n}\n\nconst ViewCreateModal = ({\n  view,\n  handleCancel,\n}: {\n  view?: any;\n  handleCancel: (param?: any) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n  const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');\n  const filtersValue = Form.useWatch('filters', form);\n  const { data } = useRequest(\n    () => request.get(`${config.apiPrefix}subscriptions`),\n    {\n      cacheKey: 'subscriptions',\n    },\n  );\n\n  const STATUS_MAP = {\n    status: [\n      { name: intl.get('运行中'), value: CrontabStatus.running },\n      { name: intl.get('空闲中'), value: CrontabStatus.idle },\n      { name: intl.get('已禁用'), value: CrontabStatus.disabled },\n    ],\n    sub_id: data?.data.map((x) => ({ name: x.name, value: x.id })),\n  };\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    values.filterRelation = filterRelation;\n    const method = view ? 'put' : 'post';\n    try {\n      const { code, data } = await request[method](\n        `${config.apiPrefix}crons/views`,\n        view ? { ...values, id: view.id } : values,\n      );\n\n      if (code === 200) {\n        handleCancel(data);\n      }\n      setLoading(false);\n    } catch (error: any) {\n      setLoading(false);\n    }\n  };\n\n  const OperationElement = ({ name, ...others }: { name: number }) => {\n    const property = form.getFieldValue(['filters', name, 'property']);\n    return (\n      <Select\n        style={{ width: 120 }}\n        placeholder={intl.get('请选择操作符')}\n        {...others}\n      >\n        {OPERATIONS.filter((x) =>\n          STATUS_MAP[property as 'status' | 'sub_id'] ? x.type === 'select' : x,\n        ).map((x) => (\n          <Select.Option key={x.name} value={x.value}>\n            {x.name}\n          </Select.Option>\n        ))}\n      </Select>\n    );\n  };\n\n  const propertyElement = (props: any, style: React.CSSProperties = {}) => {\n    return (\n      <Select style={style}>\n        {props.map((x) => (\n          <Select.Option key={x.name} value={x.value}>\n            {x.name}\n          </Select.Option>\n        ))}\n      </Select>\n    );\n  };\n\n  const typeElement = (\n    <Select style={{ width: 80 }}>\n      {SORTTYPES.map((x) => (\n        <Select.Option key={x.name} value={x.value}>\n          {x.name}\n        </Select.Option>\n      ))}\n    </Select>\n  );\n\n  const statusElement = (property: keyof typeof STATUS_MAP) => {\n    return (\n      <Select\n        mode=\"tags\"\n        allowClear\n        placeholder={intl.get('输入后回车增加自定义选项')}\n      >\n        {STATUS_MAP[property]?.map((x) => (\n          <Select.Option key={x.name} value={x.value}>\n            {x.name}\n          </Select.Option>\n        ))}\n      </Select>\n    );\n  };\n\n  return (\n    <Modal\n      title={view ? intl.get('编辑视图') : intl.get('创建视图')}\n      open={true}\n      forceRender\n      width={580}\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        layout=\"vertical\"\n        initialValues={\n          view || {\n            filters: [{ property: 'command' }],\n          }\n        }\n        name=\"env_modal\"\n      >\n        <Form.Item\n          name=\"name\"\n          label={intl.get('视图名称')}\n          rules={[{ required: true, message: intl.get('请输入视图名称') }]}\n        >\n          <Input placeholder={intl.get('请输入视图名称')} />\n        </Form.Item>\n        <Form.List name=\"filters\">\n          {(fields, { add, remove }, { errors }) => (\n            <div\n              style={{ position: 'relative' }}\n              className={`view-filters-container ${\n                fields.length > 1 ? 'active' : ''\n              }`}\n            >\n              {fields.length > 1 && (\n                <div\n                  style={{\n                    position: 'absolute',\n                    width: 50,\n                    borderRadius: 10,\n                    border: '1px solid rgb(190, 220, 255)',\n                    borderRight: 'none',\n                    height: 56 * (fields.length - 1),\n                    top: 46,\n                    left: 15,\n                  }}\n                >\n                  <Button\n                    type=\"primary\"\n                    size=\"small\"\n                    style={{\n                      position: 'absolute',\n                      top: '50%',\n                      translate: '-50% -50%',\n                      padding: '0 3px',\n                      cursor: 'pointer',\n                    }}\n                    onClick={() => {\n                      setFilterRelation(\n                        filterRelation === 'and' ? 'or' : 'and',\n                      );\n                    }}\n                  >\n                    <>\n                      <span>{ViewFilterRelation[filterRelation]}</span>\n                      <IconFont type=\"ql-icon-d-caret\" />\n                    </>\n                  </Button>\n                </div>\n              )}\n              <div>\n                {fields.map(({ key, name, ...restField }) => (\n                  <Form.Item\n                    label={name === 0 ? intl.get('筛选条件') : ''}\n                    key={key}\n                    style={{ marginBottom: 0 }}\n                    required\n                    className=\"filter-item\"\n                  >\n                    <Space\n                      className=\"view-create-modal-filters\"\n                      align=\"baseline\"\n                    >\n                      <Form.Item\n                        {...restField}\n                        name={[name, 'property']}\n                        rules={[{ required: true }]}\n                      >\n                        {propertyElement(PROPERTIES, { width: 120 })}\n                      </Form.Item>\n                      <Form.Item\n                        {...restField}\n                        name={[name, 'operation']}\n                        rules={[\n                          { required: true, message: intl.get('请选择操作符') },\n                        ]}\n                      >\n                        <OperationElement name={name} />\n                      </Form.Item>\n                      <Form.Item\n                        {...restField}\n                        name={[name, 'value']}\n                        rules={[\n                          { required: true, message: intl.get('请输入内容') },\n                        ]}\n                      >\n                        {EOperation[filtersValue?.[name]['operation']] ===\n                        'select' ? (\n                          statusElement(filtersValue?.[name]['property'])\n                        ) : (\n                          <Input placeholder={intl.get('请输入内容')} />\n                        )}\n                      </Form.Item>\n                      {name !== 0 && (\n                        <MinusCircleOutlined onClick={() => remove(name)} />\n                      )}\n                    </Space>\n                  </Form.Item>\n                ))}\n                <Form.Item>\n                  <a\n                    onClick={() =>\n                      add({ property: 'command', operation: 'Reg' })\n                    }\n                  >\n                    <PlusOutlined />\n                    {intl.get('新增筛选条件')}\n                  </a>\n                </Form.Item>\n                <Form.ErrorList errors={errors} />\n              </div>\n            </div>\n          )}\n        </Form.List>\n        <Form.List name=\"sorts\">\n          {(fields, { add, remove }, { errors }) => (\n            <div\n              style={{ position: 'relative' }}\n              className={`view-filters-container ${\n                fields.length > 1 ? 'active' : ''\n              }`}\n            >\n              {fields.length > 1 && (\n                <div\n                  style={{\n                    position: 'absolute',\n                    width: 50,\n                    borderRadius: 10,\n                    border: '1px solid rgb(190, 220, 255)',\n                    borderRight: 'none',\n                    height: 56 * (fields.length - 1),\n                    top: 46,\n                    left: 15,\n                  }}\n                >\n                  <Button\n                    type=\"primary\"\n                    size=\"small\"\n                    style={{\n                      position: 'absolute',\n                      top: '50%',\n                      translate: '-50% -50%',\n                      padding: '0 3px',\n                      cursor: 'pointer',\n                    }}\n                  >\n                    <>\n                      <span>{ViewFilterRelation[filterRelation]}</span>\n                    </>\n                  </Button>\n                </div>\n              )}\n              <div>\n                {fields.map(({ key, name, ...restField }) => (\n                  <Form.Item\n                    label={name === 0 ? intl.get('排序方式') : ''}\n                    key={key}\n                    style={{ marginBottom: 0 }}\n                    className=\"filter-item\"\n                  >\n                    <Space className=\"view-create-modal-sorts\" align=\"baseline\">\n                      <Form.Item\n                        {...restField}\n                        name={[name, 'property']}\n                        rules={[{ required: true }]}\n                      >\n                        {propertyElement(PROPERTIES)}\n                      </Form.Item>\n                      <Form.Item\n                        {...restField}\n                        name={[name, 'type']}\n                        rules={[{ required: true }]}\n                      >\n                        {typeElement}\n                      </Form.Item>\n                      <MinusCircleOutlined onClick={() => remove(name)} />\n                    </Space>\n                  </Form.Item>\n                ))}\n                <Form.Item>\n                  <a onClick={() => add({ property: 'command', type: 'ASC' })}>\n                    <PlusOutlined />\n                    {intl.get('新增排序方式')}\n                  </a>\n                </Form.Item>\n                <Form.ErrorList errors={errors} />\n              </div>\n            </div>\n          )}\n        </Form.List>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default ViewCreateModal;\n"
  },
  {
    "path": "src/pages/crontab/viewManageModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n  Modal,\n  message,\n  Space,\n  Table,\n  Tag,\n  Typography,\n  Button,\n  Switch,\n} from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport { DeleteOutlined, EditOutlined } from '@ant-design/icons';\nimport { DndProvider, useDrag, useDrop } from 'react-dnd';\nimport { HTML5Backend } from 'react-dnd-html5-backend';\nimport ViewCreateModal from './viewCreateModal';\n\nconst { Text } = Typography;\n\nconst type = 'DragableBodyRow';\n\nconst DragableBodyRow = ({\n  index,\n  moveRow,\n  className,\n  style,\n  ...restProps\n}: any) => {\n  const ref = useRef();\n  const [{ isOver, dropClassName }, drop] = useDrop({\n    accept: type,\n    collect: (monitor) => {\n      const { index: dragIndex } = (monitor.getItem() as any) || {};\n      if (dragIndex === index) {\n        return {};\n      }\n      return {\n        isOver: monitor.isOver(),\n        dropClassName:\n          dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',\n      };\n    },\n    drop: (item: any) => {\n      moveRow(item.index, index);\n    },\n  });\n  const [, drag] = useDrag({\n    type,\n    item: { index },\n    collect: (monitor) => ({\n      isDragging: monitor.isDragging(),\n    }),\n  });\n  drop(drag(ref));\n\n  return (\n    <tr\n      ref={ref}\n      className={`${className}${isOver ? dropClassName : ''}`}\n      style={{ cursor: 'move', ...style }}\n      {...restProps}\n    />\n  );\n};\n\nconst ViewManageModal = ({\n  cronViews,\n  handleCancel,\n  cronViewChange,\n}: {\n  cronViews: any[];\n  handleCancel: () => void;\n  cronViewChange: (data?: any) => void;\n}) => {\n  const islastEnableView = (record) => {\n    return list.filter((x) => !x.isDisabled).length <= 1 && !record.isDisabled;\n  };\n\n  const columns: any = [\n    {\n      title: intl.get('名称'),\n      dataIndex: 'name',\n      key: 'name',\n      render: (v) => (v === '全部任务' ? intl.get('全部任务') : v),\n    },\n    {\n      title: intl.get('类型'),\n      dataIndex: 'type',\n      key: 'type',\n      render: (v) => (v === 1 ? intl.get('系统') : intl.get('个人')),\n    },\n    {\n      title: intl.get('显示'),\n      key: 'isDisabled',\n      dataIndex: 'isDisabled',\n      width: 100,\n      render: (text: string, record: any, index: number) => {\n        return (\n          <Switch\n            disabled={islastEnableView(record)}\n            checked={!record.isDisabled}\n            onChange={(checked) => onShowChange(checked, record, index)}\n          />\n        );\n      },\n    },\n    {\n      title: intl.get('操作'),\n      key: 'action',\n      width: 100,\n      render: (text: string, record: any, index: number) => {\n        return record.type !== 1 ? (\n          <Space size=\"middle\">\n            <a onClick={() => editView(record, index)}>\n              <EditOutlined />\n            </a>\n            {!islastEnableView(record) && (\n              <a onClick={() => deleteView(record, index)}>\n                <DeleteOutlined />\n              </a>\n            )}\n          </Space>\n        ) : (\n          '-'\n        );\n      },\n    },\n  ];\n  const [list, setList] = useState<any[]>([]);\n  const [isCreateViewModalVisible, setIsCreateViewModalVisible] =\n    useState<boolean>(false);\n  const [editedView, setEditedView] = useState<any>(null);\n\n  const editView = (record: any, index: number) => {\n    setEditedView(record);\n    setIsCreateViewModalVisible(true);\n  };\n\n  const deleteView = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: (\n        <>\n          {intl.get('确认删除视图')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}crons/views`, { data: [record.id] })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('删除成功'));\n              cronViewChange();\n            }\n          });\n      },\n    });\n  };\n\n  const onShowChange = (checked: boolean, record: any, index: number) => {\n    request\n      .put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, [\n        record.id,\n      ])\n      .then(({ code, data }) => {\n        if (code === 200) {\n          const _list = [...list];\n          _list.splice(index, 1, { ...list[index], isDisabled: !checked });\n          setList(_list);\n          cronViewChange();\n        }\n      });\n  };\n\n  const components = {\n    body: {\n      row: DragableBodyRow,\n    },\n  };\n\n  const moveRow = useCallback(\n    (dragIndex, hoverIndex) => {\n      if (dragIndex === hoverIndex) {\n        return;\n      }\n      const dragRow = list[dragIndex];\n      request\n        .put(`${config.apiPrefix}crons/views/move`, {\n          fromIndex: dragIndex,\n          toIndex: hoverIndex,\n          id: dragRow.id,\n        })\n        .then(({ code, data }) => {\n          if (code === 200) {\n            const newData = [...list];\n            newData.splice(dragIndex, 1);\n            newData.splice(hoverIndex, 0, { ...dragRow, ...data });\n            setList(newData);\n            cronViewChange();\n          }\n        });\n    },\n    [list],\n  );\n\n  useEffect(() => {\n    setList(cronViews);\n  }, [cronViews]);\n\n  return (\n    <Modal\n      title={intl.get('视图管理')}\n      open={true}\n      centered\n      width={620}\n      onCancel={() => handleCancel()}\n      className=\"view-manage-modal\"\n      forceRender\n      footer={false}\n      maskClosable={false}\n    >\n      <Space\n        style={{\n          display: 'flex',\n          justifyContent: 'flex-end',\n          marginBottom: 10,\n        }}\n      >\n        <Button\n          key=\"2\"\n          type=\"primary\"\n          onClick={() => {\n            setEditedView(null);\n            setIsCreateViewModalVisible(true);\n          }}\n        >\n          {intl.get('创建视图')}\n        </Button>\n      </Space>\n      <DndProvider backend={HTML5Backend}>\n        <Table\n          bordered\n          columns={columns}\n          pagination={false}\n          dataSource={list}\n          rowKey=\"id\"\n          size=\"middle\"\n          style={{ marginBottom: 20 }}\n          components={components}\n          onRow={(record: any, index: number) => {\n            return {\n              index,\n              moveRow,\n            } as any;\n          }}\n        />\n      </DndProvider>\n      {isCreateViewModalVisible && (\n        <ViewCreateModal\n          view={editedView}\n          handleCancel={(data) => {\n            setIsCreateViewModalVisible(false);\n            cronViewChange(data);\n          }}\n        />\n      )}\n    </Modal>\n  );\n};\n\nexport default ViewManageModal;\n"
  },
  {
    "path": "src/pages/dependence/index.less",
    "content": ""
  },
  {
    "path": "src/pages/dependence/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useCallback, useRef, useState, useEffect } from 'react';\nimport {\n  Button,\n  message,\n  Modal,\n  Table,\n  Tag,\n  Space,\n  Typography,\n  Tooltip,\n  Input,\n  Tabs,\n} from 'antd';\nimport {\n  EditOutlined,\n  DeleteOutlined,\n  SyncOutlined,\n  CheckCircleOutlined,\n  DeleteFilled,\n  BugOutlined,\n  FileTextOutlined,\n  CloseCircleOutlined,\n  ClockCircleOutlined,\n  MinusCircleOutlined,\n} from '@ant-design/icons';\nimport config from '@/utils/config';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { request } from '@/utils/http';\nimport DependenceModal from './modal';\nimport { DndProvider, useDrag, useDrop } from 'react-dnd';\nimport { HTML5Backend } from 'react-dnd-html5-backend';\nimport './index.less';\nimport DependenceLogModal from './logModal';\nimport { useOutletContext } from '@umijs/max';\nimport { SharedContext } from '@/layouts';\nimport useTableScrollHeight from '@/hooks/useTableScrollHeight';\nimport dayjs from 'dayjs';\nimport WebSocketManager from '@/utils/websocket';\nimport { DependenceStatus, Status } from './type';\nimport IconFont from '@/components/iconfont';\nimport useResizeObserver from '@react-hook/resize-observer';\n\nconst { Text } = Typography;\nconst { Search } = Input;\n\nenum StatusColor {\n  'processing',\n  'success',\n  'error',\n}\n\nconst StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {\n  0: {\n    icon: <SyncOutlined spin />,\n    color: 'processing',\n  },\n  1: {\n    icon: <CheckCircleOutlined />,\n    color: 'success',\n  },\n  2: {\n    icon: <CloseCircleOutlined />,\n    color: 'error',\n  },\n  3: {\n    icon: <SyncOutlined spin />,\n    color: 'processing',\n  },\n  4: {\n    icon: <CheckCircleOutlined />,\n    color: 'success',\n  },\n  5: {\n    icon: <CloseCircleOutlined />,\n    color: 'error',\n  },\n  6: {\n    icon: <ClockCircleOutlined />,\n    color: 'default',\n  },\n  7: {\n    icon: <MinusCircleOutlined />,\n    color: 'default',\n  },\n};\n\nconst Dependence = () => {\n  const { headerStyle, isPhone } = useOutletContext<SharedContext>();\n  const columns: any = [\n    {\n      title: intl.get('序号'),\n      width: 90,\n      render: (text: string, record: any, index: number) => {\n        return <span style={{ cursor: 'text' }}>{index + 1} </span>;\n      },\n    },\n    {\n      title: intl.get('名称'),\n      dataIndex: 'name',\n      width: 180,\n      key: 'name',\n    },\n    {\n      title: intl.get('状态'),\n      key: 'status',\n      width: 120,\n      dataIndex: 'status',\n      filters: [\n        {\n          text: intl.get('队列中'),\n          value: DependenceStatus.queued,\n        },\n        {\n          text: intl.get('安装中'),\n          value: DependenceStatus.installing,\n        },\n        {\n          text: intl.get('已安装'),\n          value: DependenceStatus.installed,\n        },\n        {\n          text: intl.get('安装失败'),\n          value: DependenceStatus.installFailed,\n        },\n        {\n          text: intl.get('删除中'),\n          value: DependenceStatus.removing,\n        },\n        {\n          text: intl.get('已删除'),\n          value: DependenceStatus.removed,\n        },\n        {\n          text: intl.get('删除失败'),\n          value: DependenceStatus.removeFailed,\n        },\n        {\n          text: intl.get('已取消'),\n          value: DependenceStatus.cancelled,\n        },\n      ],\n      render: (text: string, record: any, index: number) => {\n        return (\n          <Space size=\"middle\" style={{ cursor: 'text' }}>\n            <Tag\n              color={StatusMap[record.status]?.color}\n              icon={StatusMap[record.status]?.icon}\n              style={{ marginRight: 0 }}\n            >\n              {intl.get(Status[record.status])}\n            </Tag>\n          </Space>\n        );\n      },\n    },\n    {\n      title: intl.get('备注'),\n      dataIndex: 'remark',\n      width: 100,\n      key: 'remark',\n    },\n    {\n      title: intl.get('更新时间'),\n      key: 'updatedAt',\n      dataIndex: 'updatedAt',\n      width: 150,\n      render: (text: string) => {\n        return <span>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</span>;\n      },\n    },\n    {\n      title: intl.get('创建时间'),\n      key: 'createdAt',\n      dataIndex: 'createdAt',\n      width: 150,\n      render: (text: string) => {\n        return <span>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</span>;\n      },\n    },\n    {\n      title: intl.get('操作'),\n      key: 'action',\n      width: 140,\n      render: (text: string, record: any, index: number) => {\n        const isPc = !isPhone;\n        return (\n          <Space size=\"middle\">\n            {![Status.队列中, Status.已取消].includes(record.status) && (\n              <Tooltip title={isPc ? intl.get('日志') : ''}>\n                <a\n                  onClick={() => {\n                    setLogDependence({ ...record, timestamp: Date.now() });\n                  }}\n                >\n                  <FileTextOutlined />\n                </a>\n              </Tooltip>\n            )}\n            {[Status.队列中, Status.安装中, Status.删除中].includes(\n              record.status,\n            ) ? (\n              <Tooltip title={isPc ? intl.get('取消安装') : ''}>\n                <a onClick={() => cancelDependence(record)}>\n                  <IconFont type=\"ql-icon-quxiaoanzhuang\" />\n                </a>\n              </Tooltip>\n            ) : (\n              <>\n                <Tooltip title={isPc ? intl.get('重新安装') : ''}>\n                  <a onClick={() => reInstallDependence(record, index)}>\n                    <BugOutlined />\n                  </a>\n                </Tooltip>\n                {Status.已安装 === record.status && (\n                  <Tooltip title={isPc ? intl.get('删除') : ''}>\n                    <a onClick={() => deleteDependence(record, index)}>\n                      <DeleteOutlined />\n                    </a>\n                  </Tooltip>\n                )}\n                <Tooltip title={isPc ? intl.get('强制删除') : ''}>\n                  <a onClick={() => deleteDependence(record, index, true)}>\n                    <DeleteFilled />\n                  </a>\n                </Tooltip>\n              </>\n            )}\n          </Space>\n        );\n      },\n    },\n  ];\n  const [value, setValue] = useState<any[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [editedDependence, setEditedDependence] = useState();\n  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);\n  const [searchText, setSearchText] = useState('');\n  const [logDependence, setLogDependence] = useState<any>();\n  const [isLogModalVisible, setIsLogModalVisible] = useState(false);\n  const [type, setType] = useState('nodejs');\n  const tableRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState<number>(0);\n\n  useResizeObserver(tableRef, (entry) => {\n    const _height =\n      entry.target?.parentElement?.parentElement?.parentElement?.offsetHeight;\n    let threshold = 113;\n    if (selectedRowIds.length) {\n      threshold += 53;\n    }\n    if (_height && height !== _height - threshold) {\n      setHeight(_height - threshold);\n    }\n  });\n\n  const getDependencies = (status?: number[]) => {\n    setLoading(true);\n    request\n      .get(\n        `${\n          config.apiPrefix\n        }dependencies?searchValue=${searchText}&type=${type}&status=${\n          status || ''\n        }`,\n      )\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const addDependence = () => {\n    setEditedDependence(null as any);\n    setIsModalVisible(true);\n  };\n\n  const editDependence = (record: any, index: number) => {\n    setEditedDependence(record);\n    setIsModalVisible(true);\n  };\n\n  const deleteDependence = (\n    record: any,\n    index: number,\n    force: boolean = false,\n  ) => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: (\n        <>\n          {intl.get('确认删除依赖')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {\n            data: [record.id],\n          })\n          .then(({ code, data }) => {\n            if (code === 200 && force) {\n              const i = value.findIndex((x) => x.id === data[0].id);\n              if (i !== -1) {\n                const result = [...value];\n                result.splice(i, 1);\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const reInstallDependence = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认重新安装'),\n      content: (\n        <>\n          {intl.get('确认重新安装')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}dependencies/reinstall`, [record.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              handleDependence(data[0]);\n            }\n          });\n      },\n    });\n  };\n\n  const cancelDependence = (record: any) => {\n    Modal.confirm({\n      title: intl.get('确认取消安装'),\n      content: (\n        <>\n          {intl.get('确认取消安装')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}dependencies/cancel`, [record.id])\n          .then(() => {\n            getDependencies();\n          });\n      },\n    });\n  };\n\n  const handleCancel = (dependence?: any[]) => {\n    setIsModalVisible(false);\n    dependence && handleDependence(dependence);\n  };\n\n  const handleDependence = (dependence: any) => {\n    const result = [...value];\n    if (Array.isArray(dependence)) {\n      result.unshift(...dependence);\n    } else {\n      const index = value.findIndex((x) => x.id === dependence.id);\n      if (index !== -1) {\n        result.splice(index, 1, {\n          ...dependence,\n        });\n      }\n    }\n    setValue(result);\n  };\n\n  const onSelectChange = (selectedIds: any[]) => {\n    setSelectedRowIds(selectedIds);\n  };\n\n  const rowSelection = {\n    selectedRowKeys: selectedRowIds,\n    onChange: onSelectChange,\n  };\n\n  const delDependencies = (force: boolean) => {\n    const forceUrl = force ? '/force' : '';\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: <>{intl.get('确认删除选中的依赖吗')}</>,\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}dependencies${forceUrl}`, {\n            data: selectedRowIds,\n          })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              setSelectedRowIds([]);\n              getDependencies();\n            }\n          });\n      },\n    });\n  };\n\n  const handlereInstallDependencies = () => {\n    Modal.confirm({\n      title: intl.get('确认重新安装'),\n      content: <>{intl.get('确认重新安装选中的依赖吗')}</>,\n      onOk() {\n        request\n          .put(`${config.apiPrefix}dependencies/reinstall`, selectedRowIds)\n          .then(({ code, data }) => {\n            if (code === 200) {\n              setSelectedRowIds([]);\n              getDependencies();\n            }\n          });\n      },\n    });\n  };\n\n  const getDependenceDetail = (dependence: any) => {\n    request\n      .get(`${config.apiPrefix}dependencies/${dependence.id}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          const index = value.findIndex((x) => x.id === dependence.id);\n          const result = [...value];\n          if (index !== -1) {\n            result.splice(index, 1, {\n              ...dependence,\n              ...data,\n            });\n            setValue(result);\n          }\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const onSearch = (value: string) => {\n    setSearchText(value.trim());\n  };\n\n  useEffect(() => {\n    getDependencies();\n  }, [searchText, type]);\n\n  useEffect(() => {\n    if (logDependence) {\n      localStorage.setItem('logDependence', logDependence.id);\n      setIsLogModalVisible(true);\n    }\n  }, [logDependence]);\n\n  const handleMessage = useCallback((payload: any) => {\n    const { message, references } = payload;\n    let status: number | undefined = undefined;\n    if (message.includes('开始时间') && references.length > 0) {\n      status = message.includes('安装') ? Status.安装中 : Status.删除中;\n    }\n    if (message.includes('结束时间') && references.length > 0) {\n      if (message.includes('安装')) {\n        status = message.includes('成功') ? Status.已安装 : Status.安装失败;\n      } else {\n        status = message.includes('成功') ? Status.已删除 : Status.删除失败;\n      }\n\n      if (status === Status.已删除) {\n        setTimeout(() => {\n          setValue((p) => {\n            const _result = [...p];\n            for (let i = 0; i < references.length; i++) {\n              const index = p.findIndex((x) => x.id === references[i]);\n              if (index !== -1) {\n                _result.splice(index, 1);\n              }\n            }\n            return _result;\n          });\n        }, 300);\n        return;\n      }\n    }\n    if (typeof status === 'number') {\n      setValue((p) => {\n        const result = [...p];\n        for (let i = 0; i < references.length; i++) {\n          const index = p.findIndex((x) => x.id === references[i]);\n          if (index !== -1) {\n            result.splice(index, 1, {\n              ...p[index],\n              status,\n            });\n          }\n        }\n        return result;\n      });\n    }\n  }, []);\n\n  useEffect(() => {\n    const ws = WebSocketManager.getInstance();\n    ws.subscribe('installDependence', handleMessage);\n    ws.subscribe('uninstallDependence', handleMessage);\n\n    return () => {\n      ws.unsubscribe('installDependence', handleMessage);\n      ws.unsubscribe('uninstallDependence', handleMessage);\n    };\n  }, []);\n\n  const onTabChange = (activeKey: string) => {\n    setSelectedRowIds([]);\n    setType(activeKey);\n  };\n\n  const children = (\n    <div ref={tableRef}>\n      {selectedRowIds.length > 0 && (\n        <div style={{ marginBottom: 16 }}>\n          <Button\n            type=\"primary\"\n            style={{ marginBottom: 5, marginLeft: 8 }}\n            onClick={() => handlereInstallDependencies()}\n          >\n            {intl.get('批量安装')}\n          </Button>\n          <Button\n            type=\"primary\"\n            style={{ marginBottom: 5, marginLeft: 8 }}\n            onClick={() => delDependencies(false)}\n          >\n            {intl.get('批量删除')}\n          </Button>\n          <Button\n            type=\"primary\"\n            style={{ marginBottom: 5, marginLeft: 8 }}\n            onClick={() => delDependencies(true)}\n          >\n            {intl.get('批量强制删除')}\n          </Button>\n          <span style={{ marginLeft: 8 }}>\n            {intl.get('已选择')}\n            <a>{selectedRowIds?.length}</a>\n            {intl.get('项')}\n          </span>\n        </div>\n      )}\n      <DndProvider backend={HTML5Backend}>\n        <Table\n          columns={columns}\n          rowSelection={rowSelection}\n          pagination={false}\n          dataSource={value}\n          rowKey=\"id\"\n          size=\"middle\"\n          scroll={{ x: 768, y: height }}\n          loading={loading}\n          onChange={(pagination, filters) => {\n            getDependencies(filters?.status as number[]);\n          }}\n        />\n      </DndProvider>\n    </div>\n  );\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab\"\n      title={intl.get('依赖管理')}\n      extra={[\n        <Search\n          placeholder={intl.get('请输入名称')}\n          style={{ width: 'auto' }}\n          enterButton\n          loading={loading}\n          onSearch={onSearch}\n        />,\n        <Button key=\"2\" type=\"primary\" onClick={() => addDependence()}>\n          {intl.get('创建依赖')}\n        </Button>,\n      ]}\n      header={{\n        style: headerStyle,\n      }}\n    >\n      <Tabs\n        defaultActiveKey=\"nodejs\"\n        size=\"small\"\n        tabPosition=\"top\"\n        destroyInactiveTabPane\n        onChange={onTabChange}\n        items={[\n          {\n            key: 'nodejs',\n            label: 'NodeJs',\n          },\n          {\n            key: 'python3',\n            label: 'Python3',\n          },\n          {\n            key: 'linux',\n            label: 'Linux',\n          },\n        ]}\n      />\n      {children}\n      {isModalVisible && (\n        <DependenceModal\n          handleCancel={handleCancel}\n          dependence={editedDependence}\n          defaultType={type}\n        />\n      )}\n      {logDependence && isLogModalVisible && (\n        <DependenceLogModal\n          handleCancel={(needRemove?: boolean) => {\n            setIsLogModalVisible(false);\n            if (needRemove) {\n              const index = value.findIndex((x) => x.id === logDependence.id);\n              const result = [...value];\n              if (index !== -1) {\n                result.splice(index, 1);\n                setValue(result);\n              }\n            } else if ([...value].map((x) => x.id).includes(logDependence.id)) {\n              getDependenceDetail(logDependence);\n            }\n          }}\n          dependence={logDependence}\n        />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Dependence;\n"
  },
  {
    "path": "src/pages/dependence/logModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form, Statistic, Button } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport {\n  Loading3QuartersOutlined,\n  CheckCircleOutlined,\n} from '@ant-design/icons';\nimport { PageLoading } from '@ant-design/pro-layout';\nimport Ansi from 'ansi-to-react';\nimport WebSocketManager from '@/utils/websocket';\nimport { Status } from './type';\n\nconst DependenceLogModal = ({\n  dependence,\n  handleCancel,\n}: {\n  dependence?: any;\n  handleCancel: (needRemove?: boolean) => void;\n}) => {\n  const [value, setValue] = useState<string>('');\n  const [executing, setExecuting] = useState<any>(true);\n  const [isPhone, setIsPhone] = useState(false);\n  const [loading, setLoading] = useState<boolean>(true);\n  const [isRemoveFailed, setIsRemoveFailed] = useState(false);\n  const [removeLoading, setRemoveLoading] = useState<boolean>(false);\n\n  const cancel = (needRemove: boolean = false) => {\n    localStorage.removeItem('logDependence');\n    handleCancel(needRemove);\n  };\n\n  const titleElement = () => {\n    return (\n      <>\n        {executing && <Loading3QuartersOutlined spin />}\n        {!executing && <CheckCircleOutlined />}\n        <span style={{ marginLeft: 5 }}>\n          {intl.get('日志 -')} {dependence && dependence.name}\n        </span>{' '}\n      </>\n    );\n  };\n\n  const getDependenceLog = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}dependencies/${dependence.id}`)\n      .then(({ code, data }) => {\n        if (\n          code === 200 &&\n          localStorage.getItem('logDependence') === String(dependence.id)\n        ) {\n          const log = (data?.log || []).join('') as string;\n          setValue(log);\n          setExecuting(!log.includes('结束时间'));\n          setIsRemoveFailed(log.includes('删除失败'));\n        }\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  };\n\n  const forceRemoveDependence = () => {\n    setRemoveLoading(true);\n    request\n      .delete(`${config.apiPrefix}dependencies/force`, {\n        data: [dependence.id],\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          cancel(true);\n        }\n      })\n      .finally(() => {\n        setRemoveLoading(false);\n      });\n  };\n\n  const footerClick = () => {\n    if (isRemoveFailed) {\n      forceRemoveDependence();\n    } else {\n      cancel();\n    }\n  };\n\n  useEffect(() => {\n    if (dependence) {\n      getDependenceLog();\n    }\n  }, [dependence]);\n\n  const handleMessage = (payload: any) => {\n    const { message, references } = payload;\n    if (\n      references.length > 0 &&\n      references.includes(dependence.id) &&\n      [Status.删除中, Status.安装中].includes(dependence.status)\n    ) {\n      if (message.includes('结束时间')) {\n        setExecuting(false);\n        setIsRemoveFailed(message.includes('删除失败'));\n      }\n      setValue((p) => `${p}${message}`);\n    }\n  };\n\n  useEffect(() => {\n    const ws = WebSocketManager.getInstance();\n    ws.subscribe('installDependence', handleMessage);\n    ws.subscribe('uninstallDependence', handleMessage);\n\n    return () => {\n      ws.unsubscribe('installDependence', handleMessage);\n      ws.unsubscribe('uninstallDependence', handleMessage);\n    };\n  }, [dependence]);\n\n  useEffect(() => {\n    setIsPhone(document.body.clientWidth < 768);\n  }, []);\n\n  return (\n    <Modal\n      title={titleElement()}\n      open={true}\n      centered\n      className=\"log-modal\"\n      forceRender\n      onOk={() => cancel()}\n      onCancel={() => cancel()}\n      footer={[\n        <Button type=\"primary\" onClick={footerClick} loading={removeLoading}>\n          {isRemoveFailed ? intl.get('强制删除') : intl.get('知道了')}\n        </Button>,\n      ]}\n    >\n      <div className=\"log-container\">\n        {loading ? (\n          <PageLoading />\n        ) : (\n          <pre\n            style={\n              isPhone\n                ? {\n                    fontFamily: 'Source Code Pro',\n                    zoom: 0.83,\n                  }\n                : {}\n            }\n          >\n            <Ansi>{value}</Ansi>\n          </pre>\n        )}\n      </div>\n    </Modal>\n  );\n};\n\nexport default DependenceLogModal;\n"
  },
  {
    "path": "src/pages/dependence/modal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form, Radio, Select } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst { Option } = Select;\nenum DependenceTypes {\n  'nodejs',\n  'python3',\n  'linux',\n}\n\nconst DependenceModal = ({\n  dependence,\n  handleCancel,\n  defaultType,\n}: {\n  dependence?: any;\n  handleCancel: (cks?: any[]) => void;\n  defaultType: string;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const { name, split, type, remark } = values;\n    const method = dependence ? 'put' : 'post';\n    let payload;\n    if (!dependence) {\n      if (split === '1') {\n        const symbol = name.includes('&') ? '&' : '\\n';\n        payload = name.split(symbol).map((x: any) => {\n          return {\n            name: x,\n            type,\n            remark,\n          };\n        });\n      } else {\n        payload = [{ name, type, remark }];\n      }\n    } else {\n      payload = { ...values, id: dependence.id };\n    }\n    try {\n      const { code, data } = await request[method](\n        `${config.apiPrefix}dependencies`,\n        payload,\n      );\n\n      if (code === 200) {\n        handleCancel(data);\n      }\n      setLoading(false);\n    } catch (error) {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Modal\n      title={dependence ? intl.get('编辑依赖') : intl.get('创建依赖')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        layout=\"vertical\"\n        name=\"dependence_modal\"\n        initialValues={dependence}\n      >\n        <Form.Item\n          name=\"type\"\n          label={intl.get('依赖类型')}\n          initialValue={DependenceTypes[defaultType as any]}\n        >\n          <Select>\n            {config.dependenceTypes.map((x, i) => (\n              <Option key={i} value={i}>\n                {x}\n              </Option>\n            ))}\n          </Select>\n        </Form.Item>\n        {!dependence && (\n          <Form.Item\n            name=\"split\"\n            label={intl.get('自动拆分')}\n            initialValue=\"0\"\n            tooltip={intl.get('多个依赖是否换行分割')}\n          >\n            <Radio.Group>\n              <Radio value=\"1\">{intl.get('是')}</Radio>\n              <Radio value=\"0\">{intl.get('否')}</Radio>\n            </Radio.Group>\n          </Form.Item>\n        )}\n        <Form.Item\n          name=\"name\"\n          label={intl.get('名称')}\n          rules={[\n            {\n              required: true,\n              message: intl.get('请输入依赖名称，支持指定版本'),\n              whitespace: true,\n            },\n          ]}\n        >\n          <Input.TextArea\n            rows={4}\n            autoSize={{ minRows: 1, maxRows: 5 }}\n            placeholder={intl.get('请输入依赖名称')}\n          />\n        </Form.Item>\n        <Form.Item name=\"remark\" label={intl.get('备注')}>\n          <Input placeholder={intl.get('请输入备注')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default DependenceModal;\n"
  },
  {
    "path": "src/pages/dependence/type.ts",
    "content": "export enum DependenceStatus {\n  'installing',\n  'installed',\n  'installFailed',\n  'removing',\n  'removed',\n  'removeFailed',\n  'queued',\n  'cancelled',\n}\n\nexport enum Status {\n  '安装中',\n  '已安装',\n  '安装失败',\n  '删除中',\n  '已删除',\n  '删除失败',\n  '队列中',\n  '已取消',\n}"
  },
  {
    "path": "src/pages/diff/index.less",
    "content": ".d2h-files-diff {\n  height: calc(100vh - 130px);\n  height: calc(100vh - var(--vh-offset, 0px) - 130px);\n  overflow: auto;\n}\n\n.d2h-code-side-linenumber {\n  position: relative;\n}\n\n.d2h-code-side-line {\n  padding: 0 0.5em;\n}\n\n.diff-switch-file {\n  min-width: 768px;\n  .ant-form-item {\n    margin-bottom: 8px;\n  }\n\n  + section {\n    height: calc(100% - 40px) !important;\n  }\n}\n"
  },
  {
    "path": "src/pages/diff/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { PureComponent, useRef, useState, useEffect } from 'react';\nimport { Button, message, Select, Form, Row, Col } from 'antd';\nimport config from '@/utils/config';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { request } from '@/utils/http';\nimport './index.less';\nimport { DiffEditor } from '@monaco-editor/react';\nimport ReactDiffViewer from 'react-diff-viewer';\nimport { useOutletContext } from '@umijs/max';\nimport { SharedContext } from '@/layouts';\nimport { getEditorMode } from '@/utils';\n\nconst { Option } = Select;\n\nconst Diff = () => {\n  const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();\n  const [origin, setOrigin] = useState('sample/config.sample.sh');\n  const [current, setCurrent] = useState('config.sh');\n  const [originValue, setOriginValue] = useState('');\n  const [currentValue, setCurrentValue] = useState('');\n  const [loading, setLoading] = useState(true);\n  const [files, setFiles] = useState<any[]>([]);\n  const editorRef = useRef<any>(null);\n  const [language, setLanguage] = useState<string>('shell');\n\n  const getConfig = () => {\n    request\n      .get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(current)}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setCurrentValue(data);\n        }\n      });\n  };\n\n  const getSample = () => {\n    request\n      .get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(origin)}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setOriginValue(data);\n        }\n      });\n  };\n\n  const updateConfig = () => {\n    const content = editorRef.current\n      ? editorRef.current.getModel().modified.getValue().replace(/\\r\\n/g, '\\n')\n      : currentValue;\n\n    request\n      .post(`${config.apiPrefix}configs/save`, {\n        content,\n        name: current,\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('保存成功'));\n        }\n      });\n  };\n\n  const getFiles = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}configs/sample`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setFiles(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const originFileChange = (value: string, op) => {\n    setCurrent(op.extra.target);\n    setOrigin(value);\n    const newMode = getEditorMode(value);\n    setLanguage(newMode);\n  };\n\n  useEffect(() => {\n    getFiles();\n  }, []);\n\n  useEffect(() => {\n    getSample();\n  }, [origin]);\n\n  useEffect(() => {\n    getConfig();\n  }, [current]);\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper\"\n      title={intl.get('对比工具')}\n      loading={loading}\n      header={{\n        style: headerStyle,\n      }}\n      extra={\n        !isPhone && [\n          <Button key=\"1\" type=\"primary\" onClick={updateConfig}>\n            {intl.get('保存')}\n          </Button>,\n        ]\n      }\n    >\n      <Row gutter={24} className=\"diff-switch-file\">\n        <Col span={12}>\n          <Form.Item label={intl.get('源文件')}>\n            <Select value={origin} onChange={originFileChange}>\n              {files.map((x) => (\n                <Option key={x.value} value={x.value} extra={x}>\n                  {x.title}\n                </Option>\n              ))}\n            </Select>\n          </Form.Item>\n        </Col>\n        <Col span={12}>\n          <Form.Item label={intl.get('当前文件')}>\n            <span className=\"ant-form-text\">{current}</span>\n          </Form.Item>\n        </Col>\n      </Row>\n      {isPhone ? (\n        <ReactDiffViewer\n          styles={{\n            diffContainer: {\n              overflowX: 'auto',\n              minWidth: 768,\n            },\n            diffRemoved: {\n              overflowX: 'auto',\n              maxWidth: 300,\n            },\n            diffAdded: {\n              overflowX: 'auto',\n              maxWidth: 300,\n            },\n            line: {\n              wordBreak: 'break-word',\n            },\n          }}\n          oldValue={originValue}\n          newValue={currentValue}\n          splitView={true}\n          leftTitle=\"config.sh\"\n          rightTitle=\"config.sample.sh\"\n          disableWordDiff={true}\n        />\n      ) : (\n        <DiffEditor\n          language={language}\n          original={originValue}\n          modified={currentValue}\n          options={{\n            fontSize: 12,\n            lineNumbersMinChars: 3,\n            folding: false,\n            glyphMargin: false,\n            wordWrap: 'on',\n          }}\n          theme={theme}\n          onMount={(editor) => {\n            editorRef.current = editor;\n          }}\n        />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Diff;\n"
  },
  {
    "path": "src/pages/env/editNameModal.tsx",
    "content": "import intl from 'react-intl-universal'\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst EditNameModal = ({\n  ids,\n  handleCancel,\n}: {\n  ids?: string[];\n  handleCancel: () => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    try {\n      const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {\n        ids,\n        name: values.name,\n      });\n\n      if (code === 200) {\n        message.success(intl.get('更新环境变量名称成功'));\n        handleCancel();\n      }\n      setLoading(false);\n    } catch (error) {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Modal\n      title={intl.get('修改环境变量名称')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form form={form} layout=\"vertical\" name=\"edit_name_modal\">\n        <Form.Item\n          name=\"name\"\n          rules={[{ required: true, message: intl.get('请输入新的环境变量名称') }]}\n        >\n          <Input placeholder={intl.get('请输入新的环境变量名称')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default EditNameModal;\n"
  },
  {
    "path": "src/pages/env/index.less",
    "content": "tr.drop-over-downward td {\n  border-bottom: 2px dashed #1890ff;\n}\n\ntr.drop-over-upward td {\n  border-top: 2px dashed #1890ff;\n}\n\n.text-ellipsis {\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n}\n"
  },
  {
    "path": "src/pages/env/index.tsx",
    "content": "import useTableScrollHeight from '@/hooks/useTableScrollHeight';\nimport { SharedContext } from '@/layouts';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport { exportJson } from '@/utils/index';\nimport {\n  CheckCircleOutlined,\n  DeleteOutlined,\n  EditOutlined,\n  PushpinFilled,\n  PushpinOutlined,\n  StopOutlined,\n  UploadOutlined,\n} from '@ant-design/icons';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { useOutletContext } from '@umijs/max';\nimport {\n  Button,\n  Input,\n  Modal,\n  Space,\n  Table,\n  Tag,\n  Tooltip,\n  Typography,\n  Upload,\n  UploadProps,\n  message,\n} from 'antd';\nimport dayjs from 'dayjs';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { DndProvider, useDrag, useDrop } from 'react-dnd';\nimport { HTML5Backend } from 'react-dnd-html5-backend';\nimport intl from 'react-intl-universal';\nimport { useVT } from 'virtualizedtableforantd4';\nimport Copy from '../../components/copy';\nimport EditNameModal from './editNameModal';\nimport './index.less';\nimport EnvModal from './modal';\n\nconst { Paragraph } = Typography;\nconst { Search } = Input;\n\nenum Status {\n  '已启用',\n  '已禁用',\n}\n\nenum StatusColor {\n  'success',\n  'error',\n}\n\nenum OperationName {\n  '启用',\n  '禁用',\n  '置顶',\n  '取消置顶',\n}\n\nenum OperationPath {\n  'enable',\n  'disable',\n  'pin',\n  'unpin',\n}\n\nconst type = 'DragableBodyRow';\n\nconst Env = () => {\n  const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();\n  const columns: any = [\n    {\n      title: intl.get('序号'),\n      width: 80,\n      render: (text: string, record: any, index: number) => {\n        return <span style={{ cursor: 'text' }}>{index + 1} </span>;\n      },\n    },\n    {\n      title: intl.get('名称'),\n      dataIndex: 'name',\n      key: 'name',\n      sorter: (a: any, b: any) => a.name.localeCompare(b.name),\n      render: (text: string, record: any) => {\n        return (\n          <div style={{ display: 'flex', alignItems: 'center' }}>\n            <Tooltip title={text} placement=\"topLeft\">\n              <div className=\"text-ellipsis\">{text}</div>\n            </Tooltip>\n            <Copy text={text} />\n          </div>\n        );\n      },\n    },\n    {\n      title: intl.get('值'),\n      dataIndex: 'value',\n      key: 'value',\n      width: '35%',\n      render: (text: string, record: any) => {\n        return (\n          <div style={{ display: 'flex', alignItems: 'center' }}>\n            <Tooltip title={text} placement=\"topLeft\">\n              <div className=\"text-ellipsis\">{text}</div>\n            </Tooltip>\n            <Copy text={text} />\n          </div>\n        );\n      },\n    },\n    {\n      title: intl.get('备注'),\n      dataIndex: 'remarks',\n      key: 'remarks',\n      render: (text: string, record: any) => {\n        return (\n          <Tooltip title={text} placement=\"topLeft\">\n            <div className=\"text-ellipsis\">{text}</div>\n          </Tooltip>\n        );\n      },\n    },\n    {\n      title: intl.get('更新时间'),\n      dataIndex: 'timestamp',\n      key: 'timestamp',\n      width: 165,\n      ellipsis: {\n        showTitle: false,\n      },\n      sorter: {\n        compare: (a: any, b: any) => {\n          const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();\n          const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();\n          return updatedAtA - updatedAtB;\n        },\n      },\n      render: (text: string, record: any) => {\n        const date = dayjs(record.updatedAt || record.timestamp).format(\n          'YYYY-MM-DD HH:mm:ss',\n        );\n        return (\n          <Tooltip\n            placement=\"topLeft\"\n            title={date}\n            trigger={['hover', 'click']}\n          >\n            <span>{date}</span>\n          </Tooltip>\n        );\n      },\n    },\n    {\n      title: intl.get('状态'),\n      key: 'status',\n      dataIndex: 'status',\n      width: 100,\n      filters: [\n        {\n          text: intl.get('已启用'),\n          value: 0,\n        },\n        {\n          text: intl.get('已禁用'),\n          value: 1,\n        },\n      ],\n      onFilter: (value: number, record: any) => record.status === value,\n      render: (text: string, record: any, index: number) => {\n        return (\n          <Space size=\"middle\" style={{ cursor: 'text' }}>\n            <Tag color={StatusColor[record.status]} style={{ marginRight: 0 }}>\n              {intl.get(Status[record.status])}\n            </Tag>\n          </Space>\n        );\n      },\n    },\n    {\n      title: intl.get('操作'),\n      key: 'action',\n      width: 160,\n      render: (text: string, record: any, index: number) => {\n        const isPc = !isPhone;\n        return (\n          <Space size=\"middle\">\n            <Tooltip title={isPc ? intl.get('编辑') : ''}>\n              <a onClick={() => editEnv(record, index)}>\n                <EditOutlined />\n              </a>\n            </Tooltip>\n            <Tooltip\n              title={\n                isPc\n                  ? record.status === Status.已禁用\n                    ? intl.get('启用')\n                    : intl.get('禁用')\n                  : ''\n              }\n            >\n              <a onClick={() => enabledOrDisabledEnv(record, index)}>\n                {record.status === Status.已禁用 ? (\n                  <CheckCircleOutlined />\n                ) : (\n                  <StopOutlined />\n                )}\n              </a>\n            </Tooltip>\n            <Tooltip\n              title={\n                isPc\n                  ? record.isPinned === 1\n                    ? intl.get('取消置顶')\n                    : intl.get('置顶')\n                  : ''\n              }\n            >\n              <a onClick={() => pinOrUnpinEnv(record, index)}>\n                {record.isPinned === 1 ? (\n                  <PushpinFilled />\n                ) : (\n                  <PushpinOutlined />\n                )}\n              </a>\n            </Tooltip>\n            <Tooltip title={isPc ? intl.get('删除') : ''}>\n              <a onClick={() => deleteEnv(record, index)}>\n                <DeleteOutlined />\n              </a>\n            </Tooltip>\n          </Space>\n        );\n      },\n    },\n  ];\n  const [value, setValue] = useState<any[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [isEditNameModalVisible, setIsEditNameModalVisible] = useState(false);\n  const [editedEnv, setEditedEnv] = useState();\n  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);\n  const [searchText, setSearchText] = useState('');\n  const [importLoading, setImportLoading] = useState(false);\n  const tableRef = useRef<HTMLDivElement>(null);\n  const tableScrollHeight = useTableScrollHeight(tableRef, 59);\n\n  const getEnvs = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}envs?searchValue=${searchText}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const enabledOrDisabledEnv = (record: any, index: number) => {\n    Modal.confirm({\n      title: `确认${\n        record.status === Status.已禁用 ? intl.get('启用') : intl.get('禁用')\n      }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {record.status === Status.已禁用\n            ? intl.get('启用')\n            : intl.get('禁用')}\n          Env{' '}\n          <Paragraph\n            style={{ wordBreak: 'break-all', display: 'inline' }}\n            ellipsis={{ rows: 6, expandable: true }}\n            type=\"warning\"\n            copyable\n          >\n            {record.value}\n          </Paragraph>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}envs/${\n              record.status === Status.已禁用 ? 'enable' : 'disable'\n            }`,\n            [record.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(\n                `${\n                  record.status === Status.已禁用\n                    ? intl.get('启用')\n                    : intl.get('禁用')\n                }${intl.get('成功')}`,\n              );\n              const newStatus =\n                record.status === Status.已禁用 ? Status.已启用 : Status.已禁用;\n              const result = [...value];\n              result.splice(index, 1, {\n                ...record,\n                status: newStatus,\n              });\n              setValue(result);\n            }\n          });\n      },\n    });\n  };\n\n  const addEnv = () => {\n    setEditedEnv(null as any);\n    setIsModalVisible(true);\n  };\n\n  const editEnv = (record: any, index: number) => {\n    setEditedEnv(record);\n    setIsModalVisible(true);\n  };\n\n  const pinOrUnpinEnv = (record: any, index: number) => {\n    Modal.confirm({\n      title: `确认${\n        record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')\n      }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}\n          Env{' '}\n          <Paragraph\n            style={{ wordBreak: 'break-all', display: 'inline' }}\n            ellipsis={{ rows: 6, expandable: true }}\n            type=\"warning\"\n            copyable\n          >\n            {record.name}: {record.value}\n          </Paragraph>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}envs/${\n              record.isPinned === 1 ? 'unpin' : 'pin'\n            }`,\n            [record.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(\n                `${\n                  record.isPinned === 1\n                    ? intl.get('取消置顶')\n                    : intl.get('置顶')\n                }${intl.get('成功')}`,\n              );\n              getEnvs();\n            }\n          });\n      },\n    });\n  };\n\n  const deleteEnv = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: (\n        <>\n          {intl.get('确认删除变量')}{' '}\n          <Paragraph\n            style={{ wordBreak: 'break-all', display: 'inline' }}\n            ellipsis={{ rows: 6, expandable: true }}\n            type=\"warning\"\n            copyable\n          >\n            {record.name}: {record.value}\n          </Paragraph>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}envs`, { data: [record.id] })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('删除成功'));\n              const result = [...value];\n              result.splice(index, 1);\n              setValue(result);\n            }\n          });\n      },\n    });\n  };\n\n  const handleCancel = (env?: any[]) => {\n    setIsModalVisible(false);\n    getEnvs();\n  };\n\n  const handleEditNameCancel = (env?: any[]) => {\n    setIsEditNameModalVisible(false);\n    getEnvs();\n  };\n\n  const [vt, setVT] = useVT(\n    () => ({ scroll: { y: tableScrollHeight } }),\n    [tableScrollHeight],\n  );\n\n  const DragableBodyRow = React.forwardRef((props: any, ref) => {\n    const { index, moveRow, className, style, ...restProps } = props;\n    const [{ isOver, dropClassName }, drop] = useDrop({\n      accept: type,\n      collect: (monitor) => {\n        const { index: dragIndex } = (monitor.getItem() as any) || {};\n        if (dragIndex === index) {\n          return {};\n        }\n        return {\n          isOver: monitor.isOver(),\n          dropClassName:\n            dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',\n        };\n      },\n      drop: (item: any) => {\n        moveRow(item.index, index);\n      },\n    });\n    const [, drag] = useDrag({\n      type,\n      item: { index },\n      collect: (monitor) => ({\n        isDragging: monitor.isDragging(),\n      }),\n    });\n\n    useEffect(() => {\n      drop(drag(ref));\n    }, [ref]);\n\n    return (\n      <tr\n        ref={ref}\n        className={`${className}${isOver ? dropClassName : ''}`}\n        style={{ cursor: 'move', ...style }}\n        {...restProps}\n      />\n    );\n  });\n\n  useEffect(\n    () =>\n      setVT({\n        body: {\n          row: DragableBodyRow,\n        },\n      }),\n    [],\n  );\n\n  const moveRow = useCallback(\n    (dragIndex: number, hoverIndex: number) => {\n      if (dragIndex === hoverIndex) {\n        return;\n      }\n      const dragRow = value[dragIndex];\n      request\n        .put(`${config.apiPrefix}envs/${dragRow.id}/move`, {\n          fromIndex: dragIndex,\n          toIndex: hoverIndex,\n        })\n        .then(({ code, data }) => {\n          if (code === 200) {\n            const newData = [...value];\n            newData.splice(dragIndex, 1);\n            newData.splice(hoverIndex, 0, { ...dragRow, ...data });\n            setValue([...newData]);\n          }\n        });\n    },\n    [value],\n  );\n\n  const onSelectChange = (selectedIds: any[]) => {\n    setSelectedRowIds(selectedIds);\n  };\n\n  const rowSelection = {\n    selectedRowKeys: selectedRowIds,\n    onChange: onSelectChange,\n  };\n\n  const delEnvs = () => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: <>{intl.get('确认删除选中的变量吗')}</>,\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}envs`, { data: selectedRowIds })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('批量删除成功'));\n              setSelectedRowIds([]);\n              getEnvs();\n            }\n          });\n      },\n    });\n  };\n\n  const operateEnvs = (operationStatus: number) => {\n    Modal.confirm({\n      title: `确认${OperationName[operationStatus]}`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {OperationName[operationStatus]}\n          {intl.get('选中的变量吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}envs/${OperationPath[operationStatus]}`,\n            selectedRowIds,\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              getEnvs();\n            }\n          });\n      },\n    });\n  };\n\n  const exportEnvs = () => {\n    const envs = value\n      .filter((x) => selectedRowIds.includes(x.id))\n      .map((x) => ({ value: x.value, name: x.name, remarks: x.remarks }));\n    exportJson('env.json', JSON.stringify(envs));\n  };\n\n  const modifyName = () => {\n    setIsEditNameModalVisible(true);\n  };\n\n  const onSearch = (value: string) => {\n    setSearchText(value.trim());\n  };\n\n  const uploadProps: UploadProps = {\n    accept: 'application/json',\n    beforeUpload: async (file) => {\n      const formData = new FormData();\n      formData.append('env', file);\n      setImportLoading(true);\n      try {\n        const { code, data } = await request.post(\n          `${config.apiPrefix}envs/upload`,\n          formData,\n        );\n\n        if (code === 200) {\n          message.success(`成功上传${data.length}个环境变量`);\n          getEnvs();\n        }\n        setImportLoading(false);\n      } catch (error: any) {\n        setImportLoading(false);\n      }\n      return false;\n    },\n    fileList: [],\n  };\n\n  useEffect(() => {\n    getEnvs();\n  }, [searchText]);\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper env-wrapper\"\n      title={intl.get('环境变量')}\n      extra={[\n        <Search\n          placeholder={intl.get('请输入名称/值/备注')}\n          style={{ width: 'auto' }}\n          enterButton\n          loading={loading}\n          onSearch={onSearch}\n        />,\n        <Upload {...uploadProps}>\n          <Button\n            type=\"primary\"\n            icon={<UploadOutlined />}\n            loading={importLoading}\n          >\n            {intl.get('导入')}\n          </Button>\n        </Upload>,\n        <Button key=\"2\" type=\"primary\" onClick={() => addEnv()}>\n          {intl.get('创建变量')}\n        </Button>,\n      ]}\n      header={{\n        style: headerStyle,\n      }}\n    >\n      <div ref={tableRef}>\n        {selectedRowIds.length > 0 && (\n          <div style={{ marginBottom: 16 }}>\n            <Button\n              type=\"primary\"\n              style={{ marginBottom: 5 }}\n              onClick={modifyName}\n            >\n              {intl.get('批量修改变量名称')}\n            </Button>\n            <Button\n              type=\"primary\"\n              style={{ marginBottom: 5, marginLeft: 8 }}\n              onClick={delEnvs}\n            >\n              {intl.get('批量删除')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => exportEnvs()}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量导出')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateEnvs(0)}\n              style={{ marginLeft: 8, marginBottom: 5 }}\n            >\n              {intl.get('批量启用')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateEnvs(1)}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量禁用')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateEnvs(2)}\n              style={{ marginLeft: 8, marginBottom: 5 }}\n            >\n              {intl.get('批量置顶')}\n            </Button>\n            <Button\n              type=\"primary\"\n              onClick={() => operateEnvs(3)}\n              style={{ marginLeft: 8, marginRight: 8 }}\n            >\n              {intl.get('批量取消置顶')}\n            </Button>\n            <span style={{ marginLeft: 8 }}>\n              {intl.get('已选择')}\n              <a>{selectedRowIds?.length}</a>\n              {intl.get('项')}\n            </span>\n          </div>\n        )}\n        <DndProvider backend={HTML5Backend}>\n          <Table\n            columns={columns}\n            rowSelection={rowSelection}\n            pagination={false}\n            dataSource={value}\n            rowKey=\"id\"\n            size=\"middle\"\n            scroll={{ x: 1200, y: tableScrollHeight }}\n            components={vt}\n            loading={loading}\n            onRow={(record: any, index: number | undefined) => {\n              return {\n                index,\n                moveRow,\n              } as any;\n            }}\n          />\n        </DndProvider>\n      </div>\n      {isModalVisible && (\n        <EnvModal handleCancel={handleCancel} env={editedEnv} />\n      )}\n      {isEditNameModalVisible && (\n        <EditNameModal\n          handleCancel={handleEditNameCancel}\n          ids={selectedRowIds}\n        />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Env;\n"
  },
  {
    "path": "src/pages/env/modal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form, Radio } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst EnvModal = ({\n  env,\n  handleCancel,\n}: {\n  env?: any;\n  handleCancel: (cks?: any[]) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const { value, split, name, remarks } = values;\n    const method = env ? 'put' : 'post';\n    let payload;\n    if (!env) {\n      if (split === '1') {\n        const symbol = value.includes('&') ? '&' : '\\n';\n        payload = value.split(symbol).map((x: any) => {\n          return {\n            name: name,\n            value: x,\n            remarks: remarks,\n          };\n        });\n      } else {\n        payload = [{ value, name, remarks }];\n      }\n    } else {\n      payload = { ...values, id: env.id };\n    }\n    try {\n      const { code, data } = await request[method](\n        `${config.apiPrefix}envs`,\n        payload,\n      );\n\n      if (code === 200) {\n        message.success(\n          env ? intl.get('更新变量成功') : intl.get('创建变量成功'),\n        );\n        handleCancel(data);\n      }\n      setLoading(false);\n    } catch (error: any) {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Modal\n      title={env ? intl.get('编辑变量') : intl.get('创建变量')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form form={form} layout=\"vertical\" name=\"env_modal\" initialValues={env}>\n        <Form.Item\n          name=\"name\"\n          label={intl.get('名称')}\n          rules={[\n            {\n              required: true,\n              message: intl.get('请输入环境变量名称'),\n              whitespace: true,\n            },\n            {\n              pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,\n              message: intl.get('只能输入字母数字下划线，且不能以数字开头'),\n            },\n          ]}\n        >\n          <Input placeholder={intl.get('请输入环境变量名称')} />\n        </Form.Item>\n        {!env && (\n          <Form.Item\n            name=\"split\"\n            label={intl.get('自动拆分')}\n            initialValue=\"0\"\n            tooltip={intl.get('多个依赖是否换行分割')}\n          >\n            <Radio.Group>\n              <Radio value=\"1\">{intl.get('是')}</Radio>\n              <Radio value=\"0\">{intl.get('否')}</Radio>\n            </Radio.Group>\n          </Form.Item>\n        )}\n        <Form.Item\n          name=\"value\"\n          label={intl.get('值')}\n          rules={[\n            {\n              required: true,\n              message: intl.get('请输入环境变量值'),\n              whitespace: true,\n            },\n          ]}\n        >\n          <Input.TextArea\n            autoSize={{ minRows: 1, maxRows: 8 }}\n            placeholder={intl.get('请输入环境变量值')}\n          />\n        </Form.Item>\n        <Form.Item name=\"remarks\" label={intl.get('备注')}>\n          <Input placeholder={intl.get('请输入备注')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default EnvModal;\n"
  },
  {
    "path": "src/pages/error/index.less",
    "content": ".error-wrapper {\n  display: flex;\n  justify-content: center;\n  height: 100vh;\n\n  .react-terminal-wrapper {\n    max-width: 90%;\n    height: calc(100vh - 80px);\n    overflow-y: auto;\n  }\n\n  .code-box {\n    position: relative;\n    display: inline-block;\n    width: 80vw;\n    height: 90vh;\n    margin: 16px;\n    background-color: #ffffff;\n    border: 1px solid rgba(5, 5, 5, 0.06);\n    border-radius: 6px;\n    -webkit-transition: all 0.2s;\n    transition: all 0.2s;\n    border-radius: 6px 6px 0 0;\n    color: rgba(0, 0, 0, 0.88);\n    border-bottom: 1px solid rgba(5, 5, 5, 0.06);\n\n    .browser-markup {\n      position: relative;\n      border-top: 2em solid rgba(230, 230, 230, 0.7);\n      border-radius: 3px 3px 0 0;\n\n      &::before {\n        position: absolute;\n        top: -1.25em;\n        left: 1em;\n        display: block;\n        width: 0.5em;\n        height: 0.5em;\n        background-color: #f44;\n        border-radius: 50%;\n        box-shadow: 0 0 0 2px #f44, 1.5em 0 0 2px #9b3, 3em 0 0 2px #fb5;\n        content: '';\n      }\n    }\n\n    .log {\n      height: calc(90vh - 150px);\n      overflow-y: auto;\n      padding: 12px;\n      white-space: pre-line;\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/error/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useState, useEffect, useRef } from 'react';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport { PageLoading } from '@ant-design/pro-layout';\nimport { history, useOutletContext } from '@umijs/max';\nimport './index.less';\nimport { SharedContext } from '@/layouts';\nimport { Alert, Typography } from 'antd';\n\nconst Error = () => {\n  const { user } = useOutletContext<SharedContext>();\n  const [loading, setLoading] = useState(false);\n  const [data, setData] = useState(intl.get('暂无日志'));\n  const retryTimes = useRef(1);\n\n  const loopStatus = (message: string) => {\n    if (retryTimes.current > 3) {\n      setData(message);\n      return;\n    }\n    retryTimes.current += 1;\n    setTimeout(() => {\n      getHealthStatus(false);\n    }, 3000);\n  };\n\n  const getHealthStatus = (needLoading: boolean = true) => {\n    needLoading && setLoading(true);\n    request\n      .get(`${config.apiPrefix}health`)\n      .then(({ error, data }) => {\n        if (data?.status === 'ok') {\n          if (retryTimes.current > 1) {\n            setTimeout(() => {\n              window.location.reload();\n            });\n          }\n          return;\n        }\n\n        loopStatus(error?.details);\n      })\n      .catch((error) => {\n        const responseStatus = error.response.status;\n        if (responseStatus === 401) {\n          history.push('/login');\n        } else {\n          loopStatus(error.response?.message || error?.message);\n        }\n      })\n      .finally(() => needLoading && setLoading(false));\n  };\n\n  useEffect(() => {\n    if (user && user.username) {\n      history.push('/crontab');\n    }\n  }, [user]);\n\n  useEffect(() => {\n    getHealthStatus();\n  }, []);\n\n  return (\n    <div className=\"error-wrapper\">\n      {loading ? (\n        <PageLoading />\n      ) : retryTimes.current > 3 ? (\n        <div className=\"code-box\">\n          <div className=\"browser-markup\"></div>\n          <Alert\n            type=\"error\"\n            message={\n              <Typography.Title level={5} type=\"danger\">\n                {intl.get('服务启动超时')}\n              </Typography.Title>\n            }\n            description={\n              <Typography.Text type=\"danger\">\n                <div>{intl.get('请先按如下方式修复：')}</div>\n                <div>\n                  1. 宿主机执行 docker run --rm -v\n                  /var/run/docker.sock:/var/run/docker.sock\n                  containrrr/watchtower -cR &lt;容器名&gt;\n                </div>\n                <div>{intl.get('2. 容器内执行 ql check、ql update')}</div>\n                <div>\n                  {intl.get(\n                    '3. 如果无法解决，容器内执行 pm2 logs，拷贝执行结果',\n                  )}\n                  <Typography.Link href=\"https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml\">\n                    {intl.get('提交 issue')}\n                  </Typography.Link>\n                </div>\n              </Typography.Text>\n            }\n            banner\n          />\n          <Typography.Paragraph code className=\"log\">\n            {data}\n          </Typography.Paragraph>\n        </div>\n      ) : (\n        <PageLoading tip={intl.get('启动中，请稍后...')} />\n      )}\n    </div>\n  );\n};\n\nexport default Error;\n"
  },
  {
    "path": "src/pages/initialization/index.less",
    "content": "@import '~antd/es/style/themes/default.less';\n\n.container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  height: 100vh;\n  height: calc(100vh - var(--vh-offset, 0px));\n  overflow: auto;\n  background: @layout-body-background;\n  padding-top: 70px;\n}\n\n@media (min-width: @screen-md-min) {\n  .container {\n    background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');\n    background-repeat: no-repeat;\n    background-position: center 110px;\n    background-size: 100%;\n  }\n}\n\n.top {\n  text-align: center;\n}\n\n.header {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n\n.logo {\n  width: 48px;\n  display: block;\n  margin-bottom: 24px;\n  margin-top: 20px;\n}\n\n.title {\n  font-size: 20px;\n  margin-bottom: 16px;\n}\n\n.desc {\n  margin-top: 12px;\n  margin-bottom: 40px;\n  color: @text-color-secondary;\n  font-size: @font-size-base;\n}\n\n.main {\n  padding: 20px;\n  border-radius: 6px;\n  background-color: #f6f8fa;\n  border: 1px solid #ebedef;\n  display: flex;\n  max-width: 500px;\n  width: 90%;\n  height: 400px;\n\n  .ant-steps {\n    width: 35%;\n    min-width: 110px;\n    display: flex;\n    align-items: center;\n    position: relative;\n    top: 6%;\n  }\n  .steps-container {\n    flex: 1;\n    overflow-y: auto;\n  }\n}\n\n.extra {\n  margin-top: 20px;\n}\n"
  },
  {
    "path": "src/pages/initialization/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { Fragment, useEffect, useState } from 'react';\nimport {\n  Button,\n  Row,\n  Input,\n  Form,\n  message,\n  Typography,\n  Steps,\n  Select,\n} from 'antd';\nimport config from '@/utils/config';\nimport { history } from '@umijs/max';\nimport styles from './index.less';\nimport { request } from '@/utils/http';\n\nconst FormItem = Form.Item;\nconst { Step } = Steps;\nconst { Option } = Select;\nconst { Link } = Typography;\n\nconst Initialization = () => {\n  const [loading, setLoading] = useState(false);\n  const [current, setCurrent] = React.useState(0);\n  const [fields, setFields] = useState<any[]>([]);\n\n  const next = () => {\n    setCurrent(current + 1);\n  };\n\n  const prev = () => {\n    setCurrent(current - 1);\n  };\n\n  const submitAccountSetting = (values: any) => {\n    setLoading(true);\n    request\n      .put(`${config.apiPrefix}user/init`, {\n        username: values.username,\n        password: values.password,\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          next();\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const submitNotification = (values: any) => {\n    setLoading(true);\n    request\n      .put(`${config.apiPrefix}user/notification/init`, values)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          next();\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const notificationModeChange = (value: string) => {\n    const _fields = (config.notificationModeMap as any)[value];\n    setFields(_fields || []);\n  };\n\n  useEffect(() => {\n    localStorage.removeItem(config.authKey);\n  }, []);\n\n  const steps = [\n    {\n      title: intl.get('欢迎使用'),\n      content: (\n        <div className={styles.top} style={{ marginTop: 30 }}>\n          <div className={styles.header}>\n            <span className={styles.title}>{intl.get('欢迎使用青龙')}</span>\n            <span className={styles.desc}>\n              {intl.get(\n                '支持python3、javascript、shell、typescript 的定时任务管理面板',\n              )}\n            </span>\n          </div>\n          <div className={styles.action}>\n            <Button\n              type=\"primary\"\n              onClick={() => {\n                next();\n              }}\n            >\n              {intl.get('开始安装')}\n            </Button>\n          </div>\n        </div>\n      ),\n    },\n    {\n      title: intl.get('通知设置'),\n      content: (\n        <Form onFinish={submitNotification} layout=\"vertical\">\n          <Form.Item\n            label={intl.get('通知方式')}\n            name=\"type\"\n            rules={[{ required: true, message: intl.get('请选择通知方式') }]}\n            style={{ maxWidth: 350 }}\n          >\n            <Select\n              onChange={notificationModeChange}\n              placeholder={intl.get('请选择通知方式')}\n            >\n              {config.notificationModes\n                .filter((x) => x.value !== 'closed')\n                .map((x) => (\n                  <Option key={x.value} value={x.value}>\n                    {x.label}\n                  </Option>\n                ))}\n            </Select>\n          </Form.Item>\n          {fields.map((x) => (\n            <Form.Item\n              key={x.label}\n              label={x.label}\n              name={x.label}\n              extra={x.tip}\n              rules={[{ required: x.required }]}\n              style={{ maxWidth: 400 }}\n            >\n              <Input.TextArea\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={`请输入${x.label}`}\n              />\n            </Form.Item>\n          ))}\n          <Button type=\"primary\" htmlType=\"submit\" loading={loading}>\n            {intl.get('保存')}\n          </Button>\n          <Button type=\"link\" htmlType=\"button\" onClick={() => next()}>\n            {intl.get('跳过')}\n          </Button>\n        </Form>\n      ),\n    },\n    {\n      title: intl.get('账户设置'),\n      content: (\n        <Form onFinish={submitAccountSetting} layout=\"vertical\">\n          <Form.Item\n            label={intl.get('用户名')}\n            name=\"username\"\n            rules={[{ required: true }]}\n            style={{ maxWidth: 350 }}\n          >\n            <Input placeholder={intl.get('用户名')} />\n          </Form.Item>\n          <Form.Item\n            label={intl.get('密码')}\n            name=\"password\"\n            rules={[\n              { required: true },\n              {\n                pattern: /^(?!admin$).*$/,\n                message: intl.get('密码不能为admin'),\n              },\n            ]}\n            hasFeedback\n            style={{ maxWidth: 350 }}\n          >\n            <Input type=\"password\" placeholder={intl.get('密码')} />\n          </Form.Item>\n          <Form.Item\n            name=\"confirm\"\n            label={intl.get('确认密码')}\n            dependencies={['password']}\n            hasFeedback\n            style={{ maxWidth: 350 }}\n            rules={[\n              {\n                required: true,\n              },\n              ({ getFieldValue }) => ({\n                validator(_, value) {\n                  if (!value || getFieldValue('password') === value) {\n                    return Promise.resolve();\n                  }\n                  return Promise.reject(\n                    new Error(intl.get('您输入的两个密码不匹配！')),\n                  );\n                },\n              }),\n            ]}\n          >\n            <Input.Password placeholder={intl.get('确认密码')} />\n          </Form.Item>\n          <Button type=\"primary\" htmlType=\"submit\" loading={loading}>\n            {intl.get('提交')}\n          </Button>\n        </Form>\n      ),\n    },\n    {\n      title: intl.get('完成安装'),\n      content: (\n        <div className={styles.top} style={{ marginTop: 80 }}>\n          <div className={styles.header}>\n            <span className={styles.title}>{intl.get('恭喜安装完成！')}</span>\n            <Link href=\"https://github.com/whyour/qinglong\" target=\"_blank\">\n              Github\n            </Link>\n            <Link href=\"https://t.me/jiao_long\" target=\"_blank\">\n              {intl.get('Telegram频道')}\n            </Link>\n          </div>\n          <div style={{ marginTop: 16 }}>\n            <Button\n              type=\"primary\"\n              onClick={() => {\n                window.location.reload();\n              }}\n            >\n              {intl.get('去登录')}\n            </Button>\n          </div>\n        </div>\n      ),\n    },\n  ];\n\n  return (\n    <div className={styles.container}>\n      <div className={styles.top}>\n        <div className={styles.header}>\n          <img\n            alt=\"logo\"\n            className={styles.logo}\n            src=\"https://qn.whyour.cn/logo.png\"\n          />\n          <span className={styles.title}>{intl.get('初始化配置')}</span>\n        </div>\n      </div>\n      <div className={styles.main}>\n        <Steps\n          current={current}\n          direction=\"vertical\"\n          size=\"small\"\n          className={styles['ant-steps']}\n        >\n          {steps.map((item) => (\n            <Step key={item.title} title={item.title} />\n          ))}\n        </Steps>\n        <div className={styles['steps-container']}>\n          {steps[current].content}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Initialization;\n"
  },
  {
    "path": "src/pages/log/index.module.less",
    "content": "@import '~antd/es/style/themes/default.less';\n@import '~@/styles/variable.less';\n\n.left-tree {\n  &-container {\n    overflow: hidden;\n    position: relative;\n    background-color: @component-background;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n  }\n  &-scroller {\n    flex: 1;\n    overflow: auto;\n    padding-top: 6px;\n  }\n}\n\n.log-container {\n  display: flex;\n  position: relative;\n}\n\n:global {\n  .Pane.vertical.Pane1 {\n    padding-right: 5px;\n    border-right: 1px dashed @text-color;\n  }\n}\n"
  },
  {
    "path": "src/pages/log/index.tsx",
    "content": "import useFilterTreeData from '@/hooks/useFilterTreeData';\nimport { SharedContext } from '@/layouts';\nimport { depthFirstSearch } from '@/utils';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport { CloudDownloadOutlined, DeleteOutlined } from '@ant-design/icons';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport Editor from '@monaco-editor/react';\nimport CodeMirror from '@uiw/react-codemirror';\nimport { useOutletContext } from '@umijs/max';\nimport {\n  Button,\n  Empty,\n  Input,\n  message,\n  Modal,\n  Tooltip,\n  Tree,\n  TreeSelect,\n  Typography,\n} from 'antd';\nimport { saveAs } from 'file-saver';\nimport debounce from 'lodash/debounce';\nimport uniq from 'lodash/uniq';\nimport prettyBytes from 'pretty-bytes';\nimport { Key, useCallback, useEffect, useRef, useState } from 'react';\nimport intl from 'react-intl-universal';\nimport SplitPane from 'react-split-pane';\nimport styles from './index.module.less';\n\nconst { Text } = Typography;\n\nconst Log = () => {\n  const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();\n  const [value, setValue] = useState(intl.get('请选择日志文件'));\n  const [select, setSelect] = useState<string>(intl.get('请选择日志文件'));\n  const [data, setData] = useState<any[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [height, setHeight] = useState<number>();\n  const treeDom = useRef<any>();\n  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);\n  const [currentNode, setCurrentNode] = useState<any>();\n  const [searchValue, setSearchValue] = useState('');\n\n  const getLogs = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}logs`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setData(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const getLog = (node: any) => {\n    request\n      .get(\n        `${config.apiPrefix}logs/detail?file=${node.title}&path=${\n          node.parent || ''\n        }`,\n      )\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n        }\n      });\n  };\n\n  const downloadLog = () => {\n    request\n      .post<Blob>(\n        `${config.apiPrefix}logs/download`,\n        {\n          filename: currentNode.title,\n          path: currentNode.parent || '',\n        },\n        { responseType: 'blob' },\n      )\n      .then((res) => {\n        saveAs(res, currentNode.title);\n      });\n  };\n\n  const onSelect = (value: any, node: any) => {\n    if (node.key === select || !value) {\n      return;\n    }\n\n    setCurrentNode(node);\n    setSelect(value);\n\n    if (node.type === 'directory') {\n      setValue(intl.get('请选择日志文件'));\n      return;\n    }\n\n    setValue(intl.get('加载中...'));\n    getLog(node);\n  };\n\n  const onTreeSelect = useCallback((keys: Key[], e: any) => {\n    onSelect(keys[0], e.node);\n  }, []);\n\n  const onSearch = useCallback(\n    (e) => {\n      const keyword = e.target.value;\n      debounceSearch(keyword);\n    },\n    [data],\n  );\n\n  const debounceSearch = useCallback(\n    debounce((keyword) => {\n      setSearchValue(keyword);\n    }, 300),\n    [data],\n  );\n\n  const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(\n    data,\n    searchValue,\n    { treeNodeFilterProp: 'title' },\n  );\n\n  useEffect(() => {\n    setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));\n  }, [searchExpandedKeys]);\n\n  const deleteFile = () => {\n    Modal.confirm({\n      title: `确认删除`,\n      content: (\n        <>\n          {intl.get('确认删除')}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {' '}\n            {select}{' '}\n          </Text>\n          {intl.get('文件')}\n          {currentNode.type === 'directory' ? intl.get('夹下所有日志') : ''}\n          {intl.get('，删除后不可恢复')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}logs`, {\n            data: {\n              filename: currentNode.title,\n              path: currentNode.parent || '',\n              type: currentNode.type,\n            },\n          })\n          .then(({ code }) => {\n            if (code === 200) {\n              message.success(`删除成功`);\n              let newData = [...data];\n              if (currentNode.parent) {\n                newData = depthFirstSearch(\n                  newData,\n                  (c) => c.key === currentNode.key,\n                );\n              } else {\n                const index = newData.findIndex(\n                  (x) => x.key === currentNode.key,\n                );\n                if (index !== -1) {\n                  newData.splice(index, 1);\n                }\n              }\n              setData(newData);\n              initState();\n            }\n          });\n      },\n    });\n  };\n\n  const initState = () => {\n    setSelect('');\n    setCurrentNode(null);\n    setValue(intl.get('请选择脚本文件'));\n  };\n\n  const onExpand = (expKeys: any) => {\n    setExpandedKeys(expKeys);\n  };\n\n  useEffect(() => {\n    getLogs();\n  }, []);\n\n  useEffect(() => {\n    if (treeDom.current) {\n      setHeight(treeDom.current.clientHeight);\n    }\n  }, [treeDom.current, data]);\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper log-wrapper\"\n      title={\n        <>\n          {select}\n          {currentNode?.type === 'file' && (\n            <span\n              style={{\n                marginLeft: 6,\n                fontSize: 12,\n                color: '#999',\n                display: 'inline-block',\n                height: 14,\n              }}\n            >\n              {prettyBytes(currentNode.size)}\n            </span>\n          )}\n        </>\n      }\n      loading={loading}\n      extra={\n        isPhone\n          ? [\n              <TreeSelect\n                treeExpandAction=\"click\"\n                className=\"log-select\"\n                value={select}\n                dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}\n                treeData={data}\n                placeholder={intl.get('请选择日志')}\n                fieldNames={{ value: 'key' }}\n                treeNodeFilterProp=\"title\"\n                showSearch\n                allowClear\n                onSelect={onSelect}\n              />,\n            ]\n          : [\n              <Tooltip title={intl.get('下载')}>\n                <Button\n                  disabled={!currentNode || currentNode.type === 'directory'}\n                  type=\"primary\"\n                  onClick={downloadLog}\n                  icon={<CloudDownloadOutlined />}\n                />\n              </Tooltip>,\n              <Tooltip title={intl.get('删除')}>\n                <Button\n                  type=\"primary\"\n                  disabled={!currentNode}\n                  onClick={deleteFile}\n                  icon={<DeleteOutlined />}\n                />\n              </Tooltip>,\n            ]\n      }\n      header={{\n        style: headerStyle,\n      }}\n    >\n      <div className={`${styles['log-container']} log-container`}>\n        {!isPhone && (\n          /*// @ts-ignore*/\n          <SplitPane split=\"vertical\" size={200} maxSize={-100}>\n            <div className={styles['left-tree-container']}>\n              {data.length > 0 ? (\n                <>\n                  <Input.Search\n                    className={styles['left-tree-search']}\n                    onChange={onSearch}\n                    placeholder={intl.get('请输入日志名')}\n                    allowClear\n                  ></Input.Search>\n                  <div className={styles['left-tree-scroller']} ref={treeDom}>\n                    <Tree\n                      expandAction=\"click\"\n                      className={styles['left-tree']}\n                      treeData={filterData}\n                      showIcon={true}\n                      height={height}\n                      selectedKeys={[select]}\n                      showLine={{ showLeafIcon: true }}\n                      onSelect={onTreeSelect}\n                      expandedKeys={expandedKeys}\n                      onExpand={onExpand}\n                    ></Tree>\n                  </div>\n                </>\n              ) : (\n                <div\n                  style={{\n                    display: 'flex',\n                    justifyContent: 'center',\n                    alignItems: 'center',\n                    height: '100%',\n                  }}\n                >\n                  <Empty\n                    description={intl.get('暂无日志')}\n                    image={Empty.PRESENTED_IMAGE_SIMPLE}\n                  />\n                </div>\n              )}\n            </div>\n            <Editor\n              language=\"shell\"\n              theme={theme}\n              value={value}\n              options={{\n                readOnly: true,\n                fontSize: 12,\n                minimap: { enabled: false },\n                lineNumbersMinChars: 3,\n                folding: false,\n                glyphMargin: false,\n              }}\n            />\n          </SplitPane>\n        )}\n        {isPhone && (\n          <CodeMirror\n            value={value}\n            readOnly={true}\n            theme={theme.includes('dark') ? 'dark' : 'light'}\n            onChange={(value, viewUpdate) => {\n              setValue(value);\n            }}\n          />\n        )}\n      </div>\n    </PageContainer>\n  );\n};\n\nexport default Log;\n"
  },
  {
    "path": "src/pages/login/index.less",
    "content": "@import '~antd/es/style/themes/default.less';\n\n.container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  height: 100vh;\n  height: calc(100vh - var(--vh-offset, 0px));\n  overflow: auto;\n  background: @layout-body-background;\n  padding-top: 70px;\n}\n\n@media (min-width: @screen-md-min) {\n  .container {\n    background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');\n    background-repeat: no-repeat;\n    background-position: center 110px;\n    background-size: 100%;\n  }\n}\n\n.top {\n  text-align: center;\n}\n\n.header {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n\n.logo {\n  width: 48px;\n  height: 48px;\n  display: block;\n  margin-bottom: 24px;\n}\n\n.title {\n  font-size: 20px;\n  margin-bottom: 16px;\n}\n\n.desc {\n  margin-top: 12px;\n  margin-bottom: 40px;\n  color: @text-color-secondary;\n  font-size: @font-size-base;\n}\n\n.main {\n  padding: 20px;\n  border-radius: 6px;\n  background-color: #f6f8fa;\n  border: 1px solid #ebedef;\n  width: 340px;\n\n  @media screen and (max-width: @screen-sm) {\n    width: 95%;\n    max-width: 320px;\n  }\n\n  :global {\n    .@{ant-prefix}-tabs-nav-list {\n      margin: auto;\n      font-size: 16px;\n    }\n  }\n\n  .icon {\n    margin-left: 16px;\n    color: rgba(0, 0, 0, 0.2);\n    font-size: 24px;\n    vertical-align: middle;\n    cursor: pointer;\n    transition: color 0.3s;\n\n    &:hover {\n      color: @primary-color;\n    }\n  }\n\n  .other {\n    margin-top: 24px;\n    line-height: 22px;\n    text-align: left;\n    .register {\n      float: right;\n    }\n  }\n\n  .prefixIcon {\n    color: @primary-color;\n    font-size: @font-size-base;\n  }\n}\n\n.extra {\n  margin-top: 20px;\n  width: 340px;\n}\n"
  },
  {
    "path": "src/pages/login/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { Fragment, useEffect, useState } from 'react';\nimport {\n  Button,\n  Row,\n  Input,\n  Form,\n  message,\n  notification,\n  Statistic,\n} from 'antd';\nimport config from '@/utils/config';\nimport { history, useOutletContext } from '@umijs/max';\nimport styles from './index.less';\nimport { request } from '@/utils/http';\nimport { useTheme } from '@/utils/hooks';\nimport { MobileOutlined } from '@ant-design/icons';\nimport { SharedContext } from '@/layouts';\nimport dayjs from 'dayjs';\n\nconst FormItem = Form.Item;\nconst { Countdown } = Statistic;\nconst isDemoEnv = window.__ENV__DeployEnv === 'demo';\n\nconst Login = () => {\n  const { reloadUser } = useOutletContext<SharedContext>();\n  const [loading, setLoading] = useState(false);\n  const [waitTime, setWaitTime] = useState<any>();\n  const { theme } = useTheme();\n  const [twoFactor, setTwoFactor] = useState(false);\n  const [verifying, setVerifying] = useState(false);\n  const [loginInfo, setLoginInfo] = useState<any>();\n\n  const handleOk = (values: any) => {\n    setLoading(true);\n    setTwoFactor(false);\n    setWaitTime(null);\n    request\n      .post(`${config.apiPrefix}user/login`, {\n        username: values.username,\n        password: values.password,\n      })\n      .then((data) => {\n        checkResponse(data, values);\n        setLoading(false);\n      })\n      .catch(function (error) {\n        console.log(error);\n        setLoading(false);\n      });\n  };\n\n  const completeTowFactor = (values: any) => {\n    setVerifying(true);\n    request\n      .put(`${config.apiPrefix}user/two-factor/login`, {\n        ...loginInfo,\n        code: values.code,\n      })\n      .then((data: any) => {\n        checkResponse(data);\n        setVerifying(false);\n      })\n      .catch((error: any) => {\n        console.log(error);\n        setVerifying(false);\n      });\n  };\n\n  const checkResponse = (\n    { code, data, message: _message }: any,\n    values?: any,\n  ) => {\n    if (code === 200) {\n      const {\n        token,\n        lastip,\n        lastaddr,\n        lastlogon,\n        retries = 0,\n        platform,\n      } = data;\n      localStorage.setItem(config.authKey, token);\n      notification.success({\n        message: intl.get('登录成功！'),\n        description: (\n          <>\n            <div>\n              {intl.get('上次登录时间：')}\n              {lastlogon ? dayjs(lastlogon).format('YYYY-MM-DD HH:mm:ss') : '-'}\n            </div>\n            <div>\n              {intl.get('上次登录地点：')}\n              {lastaddr || '-'}\n            </div>\n            <div>\n              {intl.get('上次登录IP：')}\n              {lastip || '-'}\n            </div>\n            <div>\n              {intl.get('上次登录设备：')}\n              {platform || '-'}\n            </div>\n            <div>\n              {intl.get('上次登录状态：')}\n              {retries > 0 ? `失败${retries}次` : intl.get('成功')}\n            </div>\n          </>\n        ),\n      });\n      reloadUser(true);\n      history.push('/crontab');\n    } else if (code === 410) {\n      setWaitTime(data);\n    } else if (code === 420) {\n      setLoginInfo({\n        username: values.username,\n        password: values.password,\n      });\n      setTwoFactor(true);\n    } else if (code === 100) {\n      setTimeout(() => {\n        location.reload();\n      }, 1000);\n    }\n  };\n\n  const codeInputChange = (e: React.ChangeEvent) => {\n    const { value } = e.target as any;\n    const regx = /^[0-9]{6}$/;\n    if (regx.test(value)) {\n      completeTowFactor({ code: value });\n    }\n  };\n\n  useEffect(() => {\n    const isAuth = localStorage.getItem(config.authKey);\n    if (isAuth) {\n      history.push('/crontab');\n    }\n  }, []);\n\n  return (\n    <div className={styles.container}>\n      <div className={styles.top}>\n        <div className={styles.header}>\n          <img\n            alt=\"logo\"\n            className={styles.logo}\n            src=\"https://qn.whyour.cn/logo.png\"\n          />\n          <span className={styles.title}>\n            {twoFactor ? intl.get('两步验证') : config.siteName}\n          </span>\n        </div>\n      </div>\n      <div className={styles.main}>\n        {twoFactor ? (\n          <Form layout=\"vertical\" onFinish={completeTowFactor}>\n            <FormItem\n              name=\"code\"\n              label={intl.get('验证码')}\n              rules={[\n                {\n                  pattern: /^[0-9]{6}$/,\n                  message: intl.get('验证码为6位数字'),\n                },\n              ]}\n              validateTrigger=\"onBlur\"\n            >\n              <Input\n                placeholder={intl.get('6位数字')}\n                onChange={codeInputChange}\n                autoFocus\n                autoComplete=\"off\"\n              />\n            </FormItem>\n            <Button\n              type=\"primary\"\n              htmlType=\"submit\"\n              style={{ width: '100%' }}\n              loading={verifying}\n            >\n              {intl.get('验证')}\n            </Button>\n          </Form>\n        ) : (\n          <Form layout=\"vertical\" onFinish={handleOk}>\n            <FormItem name=\"username\" label={intl.get('用户名')} hasFeedback>\n              <Input\n                placeholder={`${intl.get('用户名')}${\n                  isDemoEnv ? ': admin' : ''\n                }`}\n                autoFocus\n              />\n            </FormItem>\n            <FormItem name=\"password\" label={intl.get('密码')} hasFeedback>\n              <Input\n                type=\"password\"\n                placeholder={`${intl.get('密码')}${isDemoEnv ? ': 123' : ''}`}\n              />\n            </FormItem>\n            <Row>\n              {waitTime ? (\n                <Button type=\"primary\" style={{ width: '100%' }} disabled>\n                  {intl.get('请')}\n                  <Countdown\n                    valueStyle={{\n                      color:\n                        theme === 'vs'\n                          ? 'rgba(0,0,0,.25)'\n                          : 'rgba(232, 230, 227, 0.25)',\n                    }}\n                    className=\"inline-countdown\"\n                    onFinish={() => setWaitTime(null)}\n                    format=\"ss\"\n                    value={Date.now() + 1000 * waitTime}\n                  />\n                  {intl.get('秒后重试')}\n                </Button>\n              ) : (\n                <Button\n                  type=\"primary\"\n                  htmlType=\"submit\"\n                  style={{ width: '100%' }}\n                  loading={loading}\n                >\n                  {intl.get('登录')}\n                </Button>\n              )}\n            </Row>\n          </Form>\n        )}\n      </div>\n      <div className={styles.extra}>\n        {twoFactor ? (\n          <div style={{ paddingLeft: 20, position: 'relative' }}>\n            <MobileOutlined style={{ position: 'absolute', left: 0, top: 4 }} />\n            {intl.get(\n              '在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。',\n            )}\n          </div>\n        ) : (\n          ''\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default Login;\n"
  },
  {
    "path": "src/pages/script/components/UnsupportedFilePreview/index.module.less",
    "content": ".container {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background: var(--background-color);\n  padding: 16px;\n}\n\n.content {\n  text-align: center;\n  background: var(--card-background);\n  padding: 24px;\n  border-radius: 12px;\n  max-width: 390px;\n  width: 100%;\n  transition: all 0.3s ease;\n}\n\n.iconWrapper {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 64px;\n  height: 64px;\n  border-radius: 50%;\n  background: var(--background-color);\n  margin-bottom: 16px;\n}\n\n.icon {\n  font-size: 32px;\n  color: var(--text-color-secondary);\n}\n\n.message {\n  font-size: 16px;\n  color: var(--text-color);\n  margin-bottom: 16px;\n  font-weight: 500;\n  line-height: 1.5;\n}\n\n.actionArea {\n  width: 100%;\n}\n\n.button {\n  min-width: 140px;\n  height: 36px;\n  font-size: 14px;\n}\n\n.warning {\n  font-size: 13px;\n  color: var(--text-color-secondary);\n  line-height: 1.5;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n}\n\n.warningIcon {\n  font-size: 14px;\n  color: #faad14;\n} "
  },
  {
    "path": "src/pages/script/components/UnsupportedFilePreview/index.tsx",
    "content": "import React from 'react';\nimport { Button, Space } from 'antd';\nimport { FileUnknownOutlined, WarningOutlined } from '@ant-design/icons';\nimport intl from 'react-intl-universal';\nimport styles from './index.module.less';\n\ninterface UnsupportedFilePreviewProps {\n  onForceOpen: () => void;\n}\n\nconst UnsupportedFilePreview: React.FC<UnsupportedFilePreviewProps> = ({\n  onForceOpen,\n}) => {\n  return (\n    <div className={styles.container}>\n      <div className={styles.content}>\n        <div className={styles.iconWrapper}>\n          <FileUnknownOutlined className={styles.icon} />\n        </div>\n        <div className={styles.message}>\n          {intl.get('当前文件不支持预览')}\n        </div>\n        <Space direction=\"vertical\" size={8} className={styles.actionArea}>\n          <Button \n            type=\"primary\" \n            onClick={onForceOpen}\n            className={styles.button}\n          >\n            {intl.get('强制打开')}\n          </Button>\n          <div className={styles.warning}>\n            <WarningOutlined className={styles.warningIcon} />\n            {intl.get('强制打开可能会导致编辑器显示异常')}\n          </div>\n        </Space>\n      </div>\n    </div>\n  );\n};\n\nexport default UnsupportedFilePreview; "
  },
  {
    "path": "src/pages/script/editModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, {\n  useEffect,\n  useState,\n  useRef,\n  useCallback,\n  useReducer,\n} from 'react';\nimport { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport SplitPane from 'react-split-pane';\nimport Editor from '@monaco-editor/react';\nimport SaveModal from './saveModal';\nimport SettingModal from './setting';\nimport { useTheme } from '@/utils/hooks';\nimport { getEditorMode, logEnded } from '@/utils';\nimport WebSocketManager from '@/utils/websocket';\nimport Ansi from 'ansi-to-react';\n\nconst { Option } = Select;\n\nconst EditModal = ({\n  treeData,\n  currentNode,\n  content,\n  handleCancel,\n}: {\n  treeData?: any;\n  content?: string;\n  currentNode: any;\n  handleCancel: () => void;\n}) => {\n  const [value, setValue] = useState('');\n  const [language, setLanguage] = useState<string>();\n  const [cNode, setCNode] = useState<any>();\n  const [selectedKey, setSelectedKey] = useState<string>();\n  const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);\n  const [settingModalVisible, setSettingModalVisible] =\n    useState<boolean>(false);\n  const [log, setLog] = useState('');\n  const { theme } = useTheme();\n  const editorRef = useRef<any>(null);\n  const [isRunning, setIsRunning] = useState(false);\n  const [currentPid, setCurrentPid] = useState(null);\n  const cancel = () => {\n    handleCancel();\n  };\n\n  const onSelect = (value: any, node: any) => {\n    if (node.key === selectedKey || !value) {\n      return;\n    }\n\n    if (node.type === 'directory') {\n      return;\n    }\n\n    const newMode = getEditorMode(value);\n    setCNode(node);\n    setLanguage(newMode);\n    getDetail(node);\n    setSelectedKey(node.key);\n  };\n\n  const getDetail = (node: any) => {\n    request\n      .get(\n        `${config.apiPrefix}scripts/detail?file=${node.title}&path=${\n          node.parent || ''\n        }`,\n      )\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n        }\n      });\n  };\n\n  const run = () => {\n    setLog('');\n    const content = editorRef.current.getValue().replace(/\\r\\n/g, '\\n');\n    request\n      .put(`${config.apiPrefix}scripts/run`, {\n        filename: cNode.title,\n        path: cNode.parent || '',\n        content,\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setIsRunning(true);\n          setCurrentPid(data);\n        }\n      });\n  };\n\n  const stop = () => {\n    if (!cNode || !cNode.title || !currentPid) {\n      return;\n    }\n    request\n      .put(`${config.apiPrefix}scripts/stop`, {\n        filename: cNode.title,\n        path: cNode.parent || '',\n        pid: currentPid,\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setIsRunning(false);\n        }\n      });\n  };\n\n  const handleMessage = useCallback((payload: any) => {\n    let { message: _message } = payload;\n    if (logEnded(_message)) {\n      setTimeout(() => {\n        setIsRunning(false);\n      }, 300);\n    }\n\n    setLog((p) => `${p}${_message}`);\n  }, []);\n\n  useEffect(() => {\n    const ws = WebSocketManager.getInstance();\n    ws.subscribe('manuallyRunScript', handleMessage);\n\n    return () => {\n      ws.unsubscribe('manuallyRunScript', handleMessage);\n    };\n  }, []);\n\n  useEffect(() => {\n    setLog('');\n    if (currentNode) {\n      setCNode(currentNode);\n      setValue(content as string);\n      setSelectedKey(currentNode.key);\n      const newMode = getEditorMode(currentNode.title);\n      setLanguage(newMode);\n    }\n  }, [content, currentNode]);\n\n  return (\n    <Drawer\n      className=\"edit-modal\"\n      closable={false}\n      title={\n        <>\n          <TreeSelect\n            treeExpandAction=\"click\"\n            style={{ marginRight: 8, width: 300 }}\n            value={selectedKey}\n            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}\n            treeData={treeData}\n            placeholder={intl.get('请选择脚本文件')}\n            fieldNames={{ value: 'key', label: 'title' }}\n            showSearch\n            onSelect={onSelect}\n            treeLine={{ showLeafIcon: true }}\n          />\n          <Select\n            value={language}\n            style={{ width: 110, marginRight: 8 }}\n            onChange={(e) => {\n              setLanguage(e);\n            }}\n          >\n            <Option value=\"javascript\">javascript</Option>\n            <Option value=\"typescript\">typescript</Option>\n            <Option value=\"shell\">shell</Option>\n            <Option value=\"python\">python</Option>\n          </Select>\n          <Button\n            type=\"primary\"\n            style={{ marginRight: 8 }}\n            onClick={isRunning ? stop : run}\n          >\n            {isRunning ? intl.get('停止') : intl.get('运行')}\n          </Button>\n          <Button\n            type=\"primary\"\n            style={{ marginRight: 8 }}\n            onClick={() => {\n              setLog('');\n            }}\n          >\n            {intl.get('清空日志')}\n          </Button>\n          {/* <Button\n            type=\"primary\"\n            style={{ marginRight: 8 }}\n            onClick={() => {\n              setSettingModalVisible(true);\n            }}\n          >\n            {intl.get('设置')}\n          </Button> */}\n          <Button\n            type=\"primary\"\n            style={{ marginRight: 8 }}\n            onClick={() => {\n              setSaveModalVisible(true);\n            }}\n          >\n            {intl.get('保存')}\n          </Button>\n          <Button\n            type=\"primary\"\n            style={{ marginRight: 8 }}\n            onClick={() => {\n              stop();\n              handleCancel();\n            }}\n          >\n            {intl.get('退出')}\n          </Button>\n        </>\n      }\n      width={'100%'}\n      headerStyle={{ padding: '11px 24px' }}\n      onClose={cancel}\n      open={true}\n    >\n      {/* @ts-ignore */}\n      <SplitPane\n        split=\"vertical\"\n        minSize={200}\n        defaultSize=\"50%\"\n        style={{ height: 'calc(100vh - 55px)' }}\n        pane2Style={{ overflowY: 'auto' }}\n      >\n        <Editor\n          language={language}\n          value={value}\n          theme={theme}\n          options={{\n            fontSize: 12,\n            minimap: { enabled: false },\n            lineNumbersMinChars: 3,\n            glyphMargin: false,\n            accessibilitySupport: 'off',\n          }}\n          onMount={(editor) => {\n            editorRef.current = editor;\n          }}\n        />\n        <pre\n          style={{\n            padding: '0 15px',\n          }}\n        >\n          <Ansi>{log}</Ansi>\n        </pre>\n      </SplitPane>\n      {saveModalVisible && (\n        <SaveModal\n          handleCancel={() => {\n            setSaveModalVisible(false);\n          }}\n          file={{\n            content:\n              editorRef.current &&\n              editorRef.current.getValue().replace(/\\r\\n/g, '\\n'),\n            ...cNode,\n          }}\n        />\n      )}\n      {settingModalVisible && (\n        <SettingModal\n          handleCancel={() => {\n            setSettingModalVisible(false);\n          }}\n        />\n      )}\n    </Drawer>\n  );\n};\n\nexport default EditModal;\n"
  },
  {
    "path": "src/pages/script/editNameModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport {\n  Modal,\n  message,\n  Input,\n  Form,\n  Select,\n  Upload,\n  Radio,\n  TreeSelect,\n} from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport { UploadOutlined } from '@ant-design/icons';\n\nconst { Option } = Select;\n\nconst EditScriptNameModal = ({\n  handleCancel,\n  treeData,\n}: {\n  treeData: any[];\n  handleCancel: (file?: {\n    filename: string;\n    path: string;\n    key: string;\n    type: string;\n  }) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n  const [dirs, setDirs] = useState<any[]>([]);\n  const [file, setFile] = useState<File>();\n  const [type, setType] = useState<'blank' | 'upload' | 'directory'>('blank');\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const { path = '', filename: inputFilename, directory = '' } = values;\n    const formData = new FormData();\n    formData.append('file', file || '');\n    formData.append('filename', file?.name || inputFilename);\n    formData.append('path', path);\n    formData.append('content', '');\n    formData.append('directory', directory);\n    request\n      .post(`${config.apiPrefix}scripts`, formData)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(\n            directory ? intl.get('创建文件夹成功') : intl.get('创建文件成功'),\n          );\n          const key = path ? `${path}/` : '';\n          const filename = file ? file.name : directory || inputFilename;\n          handleCancel({\n            filename,\n            path,\n            key: `${key}${filename}`,\n            type: directory ? 'directory' : 'file',\n          });\n        }\n        setLoading(false);\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const beforeUpload = (file: File) => {\n    setFile(file);\n    return false;\n  };\n\n  const typeChange = (e) => {\n    setType(e.target.value);\n  };\n\n  const getDirs = (data) => {\n    for (const item of data) {\n      if (item.children && item.children.length > 0) {\n        item.children = item.children\n          .filter((x) => x.type === 'directory')\n          .map((x) => ({ ...x, disabled: false }));\n        getDirs(item.children);\n      }\n    }\n    return data;\n  };\n\n  useEffect(() => {\n    const originDirs = treeData\n      .filter((x) => x.type === 'directory')\n      .map((x) => ({ ...x, disabled: false }));\n    const dirs = getDirs(originDirs);\n    setDirs(dirs);\n  }, [treeData]);\n\n  return (\n    <Modal\n      title={intl.get('创建')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form form={form} layout=\"vertical\" name=\"edit_name_modal\">\n        <Form.Item\n          name=\"type\"\n          label={intl.get('类型')}\n          rules={[{ required: true }]}\n          initialValue={'blank'}\n        >\n          <Radio.Group onChange={typeChange}>\n            <Radio value=\"blank\">{intl.get('空文件')}</Radio>\n            <Radio value=\"upload\">{intl.get('本地文件')}</Radio>\n            <Radio value=\"directory\">{intl.get('文件夹')}</Radio>\n          </Radio.Group>\n        </Form.Item>\n        {type === 'blank' && (\n          <Form.Item\n            name=\"filename\"\n            label={intl.get('文件名')}\n            rules={[\n              { required: true, message: intl.get('请输入文件名') },\n              {\n                validator: (_, value) =>\n                  value.includes('/')\n                    ? Promise.reject(new Error(intl.get('文件名不能包含斜杠')))\n                    : Promise.resolve(),\n              },\n            ]}\n          >\n            <Input placeholder={intl.get('请输入文件名')} />\n          </Form.Item>\n        )}\n        {type === 'directory' && (\n          <Form.Item\n            name=\"directory\"\n            label={intl.get('文件夹名')}\n            rules={[{ required: true, message: intl.get('请输入文件夹名') }]}\n          >\n            <Input placeholder={intl.get('请输入文件夹名')} />\n          </Form.Item>\n        )}\n        <Form.Item label={intl.get('父目录')} name=\"path\">\n          <TreeSelect\n            allowClear\n            treeData={dirs}\n            fieldNames={{ value: 'key', label: 'title' }}\n            placeholder={intl.get('请选择父目录')}\n            treeDefaultExpandAll\n          />\n        </Form.Item>\n        {type === 'upload' && (\n          <Form.Item label={intl.get('文件')} name=\"file\">\n            <Upload.Dragger beforeUpload={beforeUpload} maxCount={1}>\n              <p className=\"ant-upload-drag-icon\">\n                <UploadOutlined />\n              </p>\n              <p className=\"ant-upload-text\">\n                {intl.get('点击或者拖拽文件到此区域上传')}\n              </p>\n            </Upload.Dragger>\n          </Form.Item>\n        )}\n      </Form>\n    </Modal>\n  );\n};\n\nexport default EditScriptNameModal;\n"
  },
  {
    "path": "src/pages/script/index.module.less",
    "content": "@import '~antd/es/style/themes/default.less';\n@import '~@/styles/variable.less';\n\n.left-tree {\n  &-container {\n    overflow: hidden;\n    position: relative;\n    background-color: @component-background;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n  }\n  &-scroller {\n    flex: 1;\n    overflow: auto;\n    padding-top: 6px;\n  }\n}\n\n.log-container {\n  display: flex;\n  position: relative;\n}\n\n:global {\n  .Pane.vertical.Pane1 {\n    padding-right: 5px;\n    border-right: 1px dashed @text-color;\n  }\n}\n"
  },
  {
    "path": "src/pages/script/index.tsx",
    "content": "import IconFont from '@/components/iconfont';\nimport useFilterTreeData from '@/hooks/useFilterTreeData';\nimport { SharedContext } from '@/layouts';\nimport { depthFirstSearch, findNode, getEditorMode } from '@/utils';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport { canPreviewInMonaco } from '@/utils/monaco';\nimport {\n  CloudDownloadOutlined,\n  DeleteOutlined,\n  EditOutlined,\n  EllipsisOutlined,\n  PlusOutlined,\n} from '@ant-design/icons';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport Editor from '@monaco-editor/react';\nimport { langs } from '@uiw/codemirror-extensions-langs';\nimport CodeMirror from '@uiw/react-codemirror';\nimport { history, useOutletContext } from '@umijs/max';\nimport {\n  Button,\n  Dropdown,\n  Empty,\n  Input,\n  MenuProps,\n  message,\n  Modal,\n  Tooltip,\n  Tree,\n  TreeSelect,\n  Typography,\n} from 'antd';\nimport { saveAs } from 'file-saver';\nimport debounce from 'lodash/debounce';\nimport uniq from 'lodash/uniq';\nimport prettyBytes from 'pretty-bytes';\nimport { parse } from 'query-string';\nimport { Key, useCallback, useEffect, useRef, useState } from 'react';\nimport { useHotkeys } from 'react-hotkeys-hook';\nimport intl from 'react-intl-universal';\nimport SplitPane from 'react-split-pane';\nimport EditModal from './editModal';\nimport EditScriptNameModal from './editNameModal';\nimport styles from './index.module.less';\nimport RenameModal from './renameModal';\nimport UnsupportedFilePreview from './components/UnsupportedFilePreview';\nconst { Text } = Typography;\n\nconst Script = () => {\n  const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();\n  const [value, setValue] = useState(intl.get('请选择脚本文件'));\n  const [select, setSelect] = useState<string>(intl.get('请选择脚本文件'));\n  const [data, setData] = useState<any[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [mode, setMode] = useState('');\n  const [height, setHeight] = useState<number>();\n  const treeDom = useRef<any>();\n  const [isLogModalVisible, setIsLogModalVisible] = useState(false);\n  const [searchValue, setSearchValue] = useState('');\n  const [isEditing, setIsEditing] = useState(false);\n  const editorRef = useRef<any>(null);\n  const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);\n  const [isRenameFileModalVisible, setIsRenameFileModalVisible] =\n    useState(false);\n  const [currentNode, setCurrentNode] = useState<any>();\n  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);\n  const [showMonaco, setShowMonaco] = useState(true);\n\n  const handleIsEditing = (filename: string, value: boolean) => {\n    setIsEditing(value && canPreviewInMonaco(filename));\n  };\n\n  const getScripts = (needLoading: boolean = true) => {\n    needLoading && setLoading(true);\n    request\n      .get(`${config.apiPrefix}scripts`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setData(data);\n          initState();\n          initGetScript(data);\n        }\n      })\n      .finally(() => needLoading && setLoading(false));\n  };\n\n  const getDetail = (node: any, options: any = {}) => {\n    request\n      .get(\n        `${config.apiPrefix}scripts/detail?file=${encodeURIComponent(\n          node.title,\n        )}&path=${node.parent || ''}`,\n      )\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n          if (options.callback) {\n            options.callback();\n          }\n        }\n      });\n  };\n\n  const downloadScript = () => {\n    request\n      .post<Blob>(\n        `${config.apiPrefix}scripts/download`,\n        {\n          filename: currentNode.title,\n          path: currentNode.parent || '',\n        },\n        { responseType: 'blob' },\n      )\n      .then((res) => {\n        saveAs(res, currentNode.title);\n      });\n  };\n\n  const initGetScript = (_data: any) => {\n    const { p, s } = parse(history.location.search);\n    if (s) {\n      const vkey = `${p}/${s}`;\n      const obj = {\n        node: {\n          title: s,\n          key: p ? vkey : s,\n          parent: p,\n        },\n      };\n      const item = findNode(_data, (c) => c.key === obj.node.key);\n      if (item) {\n        obj.node = item;\n        setExpandedKeys([p as string]);\n        onTreeSelect([vkey], obj);\n      }\n    }\n  };\n\n  const onSelect = (value: any, node: any) => {\n    if (node.key === select || !value) {\n      return;\n    }\n\n    setSelect(node.key);\n    setCurrentNode(node);\n\n    if (node.type === 'directory') {\n      setValue(intl.get('请选择脚本文件'));\n      setShowMonaco(true);\n      return;\n    }\n\n    if (!canPreviewInMonaco(node.title)) {\n      setShowMonaco(false);\n      return;\n    }\n\n    setShowMonaco(true);\n    const newMode = getEditorMode(value);\n    setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);\n    setValue(intl.get('加载中...'));\n\n    getDetail(node, {\n      callback: () => {\n        if (isEditing) {\n          setIsEditing(true);\n        }\n      },\n    });\n  };\n\n  const onTreeSelect = useCallback(\n    (keys: Key[], e: any) => {\n      const node = e.node;\n      if (node.key === select && isEditing) {\n        return;\n      }\n\n      const currentContent = editorRef.current\n        ? editorRef.current.getValue().replace(/\\r\\n/g, '\\n')\n        : value;\n      const originalContent = value.replace(/\\r\\n/g, '\\n');\n\n      if (currentContent !== originalContent && isEditing) {\n        Modal.confirm({\n          title: intl.get('确认离开'),\n          content: <>{intl.get('当前文件未保存，确认离开吗')}</>,\n          onOk() {\n            onSelect(keys[0], e.node);\n            handleIsEditing(e.node.title, false);\n          },\n        });\n      } else {\n        handleIsEditing(e.node.title, false);\n        onSelect(keys[0], e.node);\n      }\n    },\n    [value, select, isEditing],\n  );\n\n  const onSearch = useCallback(\n    (e) => {\n      const keyword = e.target.value;\n      debounceSearch(keyword);\n    },\n    [data],\n  );\n\n  const debounceSearch = useCallback(\n    debounce((keyword) => {\n      setSearchValue(keyword);\n    }, 300),\n    [data],\n  );\n\n  const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(\n    data,\n    searchValue,\n    { treeNodeFilterProp: 'title' },\n  );\n\n  useEffect(() => {\n    setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));\n  }, [searchExpandedKeys]);\n\n  const onExpand = (expKeys: any) => {\n    setExpandedKeys(expKeys);\n  };\n\n  const onDoubleClick = (e: any, node: any) => {\n    if (node.type === 'file') {\n      setSelect(node.key);\n      setCurrentNode(node);\n      handleIsEditing(node.title, true);\n    }\n  };\n\n  const editFile = () => {\n    setTimeout(() => {\n      handleIsEditing(currentNode.title, true);\n    }, 300);\n  };\n\n  const cancelEdit = () => {\n    handleIsEditing(currentNode.title, false);\n    setValue(intl.get('加载中...'));\n    getDetail(currentNode);\n  };\n\n  const saveFile = () => {\n    Modal.confirm({\n      title: `确认保存`,\n      content: (\n        <>\n          {intl.get('确认保存文件')}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {' '}\n            {currentNode.title}\n          </Text>\n          {intl.get('，保存后不可恢复')}\n        </>\n      ),\n      onOk() {\n        const content = editorRef.current\n          ? editorRef.current.getValue().replace(/\\r\\n/g, '\\n')\n          : value;\n        return new Promise((resolve, reject) => {\n          request\n            .put(`${config.apiPrefix}scripts`, {\n              filename: currentNode.title,\n              path: currentNode.parent || '',\n              content,\n            })\n            .then(({ code, data }) => {\n              if (code === 200) {\n                message.success(`保存成功`);\n                setValue(content);\n                handleIsEditing(currentNode.title, false);\n              }\n              resolve(null);\n            })\n            .catch((e) => reject(e));\n        });\n      },\n    });\n  };\n\n  const deleteFile = () => {\n    Modal.confirm({\n      title: `确认删除`,\n      content: (\n        <>\n          {intl.get('确认删除')}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {' '}\n            {select}{' '}\n          </Text>\n          {intl.get('文件')}\n          {currentNode.type === 'directory' ? intl.get('夹及其子文件') : ''}\n          {intl.get('，删除后不可恢复')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}scripts`, {\n            data: {\n              filename: currentNode.title,\n              path: currentNode.parent || '',\n              type: currentNode.type,\n            },\n          })\n          .then(({ code }) => {\n            if (code === 200) {\n              message.success(`删除成功`);\n              let newData = [...data];\n              if (currentNode.parent) {\n                newData = depthFirstSearch(\n                  newData,\n                  (c) => c.key === currentNode.key,\n                );\n              } else {\n                const index = newData.findIndex(\n                  (x) => x.key === currentNode.key,\n                );\n                if (index !== -1) {\n                  newData.splice(index, 1);\n                }\n              }\n              setData(newData);\n              initState();\n            }\n          });\n      },\n    });\n  };\n\n  const renameFile = () => {\n    setIsRenameFileModalVisible(true);\n  };\n\n  const handleRenameFileCancel = () => {\n    setIsRenameFileModalVisible(false);\n    getScripts(false);\n  };\n\n  const addFile = () => {\n    setIsAddFileModalVisible(true);\n  };\n\n  const addFileModalClose = async (\n    {\n      filename,\n      path,\n      key,\n      type,\n    }: { filename: string; path: string; key: string; type?: string } = {\n      filename: '',\n      path: '',\n      key: '',\n    },\n  ) => {\n    if (filename) {\n      const res = await request.get(`${config.apiPrefix}scripts`);\n      let newData = res.data;\n      if (type === 'directory' && filename.includes('/')) {\n        const parts = filename.split('/');\n        parts.pop();\n        const parentPath = parts.join('/');\n        path = path ? `${path}/${parentPath}` : parentPath;\n      }\n      const item = findNode(newData, (c) => c.key === key);\n      if (path) {\n        const keys = path.split('/');\n        const sKeys: string[] = [];\n        keys.reduce((p, c) => {\n          sKeys.push(p);\n          return `${p}/${c}`;\n        });\n        setExpandedKeys([...expandedKeys, ...sKeys, path]);\n      }\n      setData(newData);\n      onSelect(item.title, item);\n      handleIsEditing(item.title, true);\n    }\n    setIsAddFileModalVisible(false);\n  };\n\n  const initState = () => {\n    setSelect(intl.get('请选择脚本文件'));\n    setCurrentNode(null);\n    setValue(intl.get('请选择脚本文件'));\n  };\n\n  useEffect(() => {\n    getScripts();\n  }, []);\n\n  useEffect(() => {\n    if (treeDom.current) {\n      setHeight(treeDom.current.clientHeight - 6);\n    }\n  }, [treeDom.current, data]);\n\n  useHotkeys(\n    'mod+s',\n    (e) => {\n      if (isEditing) {\n        saveFile();\n      }\n    },\n    { enableOnFormTags: ['textarea'], preventDefault: true },\n  );\n\n  useHotkeys(\n    'mod+d',\n    (e) => {\n      if (currentNode.title) {\n        deleteFile();\n      }\n    },\n    { preventDefault: true },\n  );\n\n  useHotkeys(\n    'mod+o',\n    (e) => {\n      if (!isEditing) {\n        addFile();\n      }\n    },\n    { preventDefault: true },\n  );\n\n  useHotkeys(\n    'mod+e',\n    (e) => {\n      if (currentNode.title) {\n        cancelEdit();\n      }\n    },\n    { preventDefault: true },\n  );\n\n  const action = (key: string | number) => {\n    switch (key) {\n      case 'save':\n        saveFile();\n        break;\n      case 'exit':\n        cancelEdit();\n        break;\n      default:\n        break;\n    }\n  };\n\n  const menuAction = (key: string | number) => {\n    switch (key) {\n      case 'add':\n        addFile();\n        break;\n      case 'edit':\n        editFile();\n        break;\n      case 'delete':\n        deleteFile();\n        break;\n      case 'rename':\n        renameFile();\n        break;\n      default:\n        break;\n    }\n  };\n\n  const menu: MenuProps = isEditing\n    ? {\n        items: [\n          { label: intl.get('保存'), key: 'save', icon: <PlusOutlined /> },\n          { label: intl.get('退出编辑'), key: 'exit', icon: <EditOutlined /> },\n        ],\n        onClick: ({ key, domEvent }) => {\n          domEvent.stopPropagation();\n          action(key);\n        },\n      }\n    : {\n        items: [\n          { label: intl.get('创建'), key: 'add', icon: <PlusOutlined /> },\n          {\n            label: intl.get('编辑'),\n            key: 'edit',\n            icon: <EditOutlined />,\n            disabled: !currentNode,\n          },\n          {\n            label: intl.get('重命名'),\n            key: 'rename',\n            icon: <IconFont type=\"ql-icon-rename\" />,\n            disabled: !currentNode,\n          },\n          {\n            label: intl.get('删除'),\n            key: 'delete',\n            icon: <DeleteOutlined />,\n            disabled: !currentNode,\n          },\n        ],\n        onClick: ({ key, domEvent }) => {\n          domEvent.stopPropagation();\n          menuAction(key);\n        },\n      };\n\n  const handleForceOpen = () => {\n    if (!currentNode) return;\n\n    setMode('plaintext');\n    setValue(intl.get('加载中...'));\n    setShowMonaco(true);\n\n    getDetail(currentNode, {\n      callback: () => {\n        setIsEditing(true);\n      },\n    });\n  };\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper log-wrapper\"\n      title={\n        <>\n          {select}\n          {currentNode?.type === 'file' && (\n            <span\n              style={{\n                marginLeft: 6,\n                fontSize: 12,\n                color: '#999',\n                display: 'inline-block',\n                height: 14,\n              }}\n            >\n              {prettyBytes(currentNode.size)}\n            </span>\n          )}\n        </>\n      }\n      loading={loading}\n      extra={\n        isPhone\n          ? [\n              <TreeSelect\n                treeExpandAction=\"click\"\n                className=\"log-select\"\n                value={select}\n                dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}\n                treeData={data}\n                placeholder={intl.get('请选择脚本')}\n                fieldNames={{ value: 'key' }}\n                treeNodeFilterProp=\"title\"\n                showSearch\n                allowClear\n                onSelect={onSelect}\n              />,\n              <Dropdown menu={menu} trigger={['click']}>\n                <Button type=\"primary\" icon={<EllipsisOutlined />} />\n              </Dropdown>,\n            ]\n          : isEditing\n          ? [\n              <Button type=\"primary\" onClick={saveFile}>\n                {intl.get('保存')}\n              </Button>,\n              <Button type=\"primary\" onClick={cancelEdit}>\n                {intl.get('退出编辑')}\n              </Button>,\n            ]\n          : [\n              <Tooltip title={intl.get('创建')}>\n                <Button\n                  type=\"primary\"\n                  onClick={addFile}\n                  icon={<PlusOutlined />}\n                />\n              </Tooltip>,\n              <Tooltip title={intl.get('编辑')}>\n                <Button\n                  disabled={!currentNode}\n                  type=\"primary\"\n                  onClick={editFile}\n                  icon={<EditOutlined />}\n                />\n              </Tooltip>,\n              <Tooltip title={intl.get('重命名')}>\n                <Button\n                  disabled={!currentNode}\n                  type=\"primary\"\n                  onClick={renameFile}\n                  icon={<IconFont type=\"ql-icon-rename\" />}\n                />\n              </Tooltip>,\n              <Tooltip title={intl.get('下载')}>\n                <Button\n                  disabled={!currentNode || currentNode.type === 'directory'}\n                  type=\"primary\"\n                  onClick={downloadScript}\n                  icon={<CloudDownloadOutlined />}\n                />\n              </Tooltip>,\n              <Tooltip title={intl.get('删除')}>\n                <Button\n                  type=\"primary\"\n                  disabled={!currentNode}\n                  onClick={deleteFile}\n                  icon={<DeleteOutlined />}\n                />\n              </Tooltip>,\n              <Button\n                type=\"primary\"\n                onClick={() => {\n                  setIsLogModalVisible(true);\n                }}\n              >\n                {intl.get('调试')}\n              </Button>,\n            ]\n      }\n      header={{\n        style: headerStyle,\n      }}\n    >\n      <div className={`${styles['log-container']} log-container`}>\n        {!isPhone && (\n          /*// @ts-ignore*/\n          <SplitPane split=\"vertical\" size={200} maxSize={-100}>\n            <div className={styles['left-tree-container']}>\n              {data.length > 0 ? (\n                <>\n                  <Input.Search\n                    className={styles['left-tree-search']}\n                    onChange={onSearch}\n                    placeholder={intl.get('请输入脚本名')}\n                    allowClear\n                  ></Input.Search>\n                  <div className={styles['left-tree-scroller']} ref={treeDom}>\n                    <Tree\n                      expandAction=\"click\"\n                      className={styles['left-tree']}\n                      treeData={filterData}\n                      showIcon={true}\n                      height={height}\n                      selectedKeys={[select]}\n                      expandedKeys={expandedKeys}\n                      onExpand={onExpand}\n                      showLine={{ showLeafIcon: true }}\n                      onSelect={onTreeSelect}\n                      onDoubleClick={onDoubleClick}\n                    ></Tree>\n                  </div>\n                </>\n              ) : (\n                <div\n                  style={{\n                    display: 'flex',\n                    justifyContent: 'center',\n                    alignItems: 'center',\n                    height: '100%',\n                  }}\n                >\n                  <Empty\n                    description={intl.get('暂无脚本')}\n                    image={Empty.PRESENTED_IMAGE_SIMPLE}\n                  />\n                </div>\n              )}\n            </div>\n            {showMonaco ? (\n              <Editor\n                language={mode}\n                value={value}\n                theme={theme}\n                options={{\n                  readOnly: !isEditing,\n                  fontSize: 12,\n                  lineNumbersMinChars: 3,\n                  glyphMargin: false,\n                  accessibilitySupport: 'off',\n                }}\n                onMount={(editor) => {\n                  editorRef.current = editor;\n                }}\n              />\n            ) : (\n              <UnsupportedFilePreview onForceOpen={handleForceOpen} />\n            )}\n          </SplitPane>\n        )}\n        {isPhone && (\n          <CodeMirror\n            value={value}\n            extensions={\n              mode ? [langs[mode as keyof typeof langs]()] : undefined\n            }\n            theme={theme.includes('dark') ? 'dark' : 'light'}\n            readOnly={!isEditing}\n            onChange={(value) => {\n              setValue(value);\n            }}\n          />\n        )}\n        {isLogModalVisible && isLogModalVisible && (\n          <EditModal\n            treeData={data}\n            currentNode={currentNode}\n            content={value}\n            handleCancel={() => {\n              setIsLogModalVisible(false);\n            }}\n          />\n        )}\n        {isAddFileModalVisible && (\n          <EditScriptNameModal\n            treeData={data}\n            handleCancel={addFileModalClose}\n          />\n        )}\n        {isRenameFileModalVisible && (\n          <RenameModal\n            handleCancel={handleRenameFileCancel}\n            currentNode={currentNode}\n          />\n        )}\n      </div>\n    </PageContainer>\n  );\n};\n\nexport default Script;\n"
  },
  {
    "path": "src/pages/script/renameModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst RenameModal = ({\n  currentNode,\n  handleCancel,\n}: {\n  currentNode?: any;\n  handleCancel: () => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    try {\n      const { code, data } = await request.put(\n        `${config.apiPrefix}scripts/rename`,\n        {\n          filename: currentNode.title,\n          path: currentNode.parent || '',\n          newFilename: values.name,\n        },\n      );\n\n      if (code === 200) {\n        message.success(intl.get('更新名称成功'));\n        handleCancel();\n      }\n      setLoading(false);\n    } catch (error) {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Modal\n      title={intl.get('重命名')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        initialValues={{ name: currentNode?.title }}\n        layout=\"vertical\"\n        name=\"edit_name_modal\"\n      >\n        <Form.Item\n          name=\"name\"\n          rules={[{ required: true, message: intl.get('请输入新名称') }]}\n        >\n          <Input placeholder={intl.get('请输入新名称')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default RenameModal;\n"
  },
  {
    "path": "src/pages/script/saveModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst SaveModal = ({\n  file,\n  handleCancel,\n}: {\n  file?: any;\n  handleCancel: (cks?: any[]) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const payload = { ...values, originFilename: file.title, content: file.content };\n    request\n      .post(`${config.apiPrefix}scripts`, payload)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('保存文件成功'));\n          handleCancel(data);\n        }\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  };\n\n  return (\n    <Modal\n      title={intl.get('保存文件')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        layout=\"vertical\"\n        name=\"script_modal\"\n        initialValues={{ filename: file?.title, path: file?.parent || '' }}\n      >\n        <Form.Item\n          name=\"filename\"\n          label={intl.get('文件名')}\n          rules={[{ required: true, message: intl.get('请输入文件名') }]}\n        >\n          <Input placeholder={intl.get('请输入文件名')} />\n        </Form.Item>\n        <Form.Item name=\"path\" label={intl.get('保存目录')}>\n          <Input placeholder={intl.get('请输入保存目录，默认scripts目录')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default SaveModal;\n"
  },
  {
    "path": "src/pages/script/setting.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst SettingModal = ({\n  file,\n  handleCancel,\n}: {\n  file?: any;\n  handleCancel: (cks?: any[]) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const payload = { ...file, ...values };\n    request\n      .post(`${config.apiPrefix}scripts`, payload)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('保存文件成功'));\n          handleCancel(data);\n        }\n        setLoading(false);\n      });\n  };\n\n  return (\n    <Modal\n      title={intl.get('运行设置')}\n      open={true}\n      forceRender\n      centered\n      onCancel={() => handleCancel()}\n    >\n      <Form\n        form={form}\n        layout=\"vertical\"\n        name=\"setting_modal\"\n        initialValues={file}\n      >\n        <Form.Item\n          name=\"filename\"\n          label={intl.get('待开发')}\n          rules={[{ required: true, message: intl.get('待开发') }]}\n        >\n          <Input placeholder={intl.get('待开发')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default SettingModal;\n"
  },
  {
    "path": "src/pages/setting/about.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Typography, Input, Form, Button, message, Descriptions } from 'antd';\nimport styles from './index.less';\nimport { SharedContext } from '@/layouts';\nimport dayjs from 'dayjs';\n\nconst { Link } = Typography;\n\nconst About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {\n  return (\n    <div className={styles.container}>\n      <img\n        alt=\"logo\"\n        style={{ width: 140, marginRight: 20 }}\n        src=\"https://qn.whyour.cn/logo.png\"\n      />\n      <div className={styles.right}>\n        <span className={styles.title}>{intl.get('青龙')}</span>\n        <span className={styles.desc}>\n          {intl.get(\n            '支持python3、javascript、shell、typescript 的定时任务管理面板',\n          )}\n        </span>\n        <Descriptions>\n          <Descriptions.Item label={intl.get('版本')} span={3}>\n            {systemInfo?.branch === 'develop'\n              ? intl.get('开发版')\n              : intl.get('正式版')}{' '}\n            v{systemInfo.version}\n          </Descriptions.Item>\n          <Descriptions.Item label={intl.get('更新时间')} span={3}>\n            {dayjs(systemInfo.publishTime * 1000).format('YYYY-MM-DD HH:mm')}\n          </Descriptions.Item>\n          <Descriptions.Item label={intl.get('更新日志')} span={3}>\n            <Link\n              href={`https://qn.whyour.cn/version.yaml?t=${Date.now()}`}\n              target=\"_blank\"\n            >\n              {intl.get('查看')}\n            </Link>\n          </Descriptions.Item>\n        </Descriptions>\n        <div>\n          <Link\n            href=\"https://github.com/whyour/qinglong\"\n            target=\"_blank\"\n            style={{ marginRight: 15 }}\n          >\n            Github\n          </Link>\n          <Link\n            href=\"https://t.me/jiao_long\"\n            target=\"_blank\"\n            style={{ marginRight: 15 }}\n          >\n            {intl.get('Telegram频道')}\n          </Link>\n          <Link\n            href=\"https://github.com/whyour/qinglong/issues\"\n            target=\"_blank\"\n          >\n            {intl.get('提交BUG')}\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default About;\n"
  },
  {
    "path": "src/pages/setting/appModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form, Select } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst AppModal = ({\n  app,\n  handleCancel,\n}: {\n  app?: any;\n  handleCancel: (needUpdate?: boolean) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const method = app ? 'put' : 'post';\n    const payload = { ...values };\n    if (app) {\n      payload.id = app.id;\n    }\n    try {\n      const { code, data } = await request[method](\n        `${config.apiPrefix}apps`,\n        payload,\n      );\n\n      if (code === 200) {\n        message.success(\n          app ? intl.get('更新应用成功') : intl.get('创建应用成功'),\n        );\n        handleCancel(data);\n      }\n      setLoading(false);\n    } catch (error) {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Modal\n      title={app ? intl.get('编辑应用') : intl.get('创建应用')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        layout=\"vertical\"\n        name=\"form_app_modal\"\n        initialValues={app}\n      >\n        <Form.Item\n          name=\"name\"\n          label={intl.get('名称')}\n          rules={[\n            {\n              validator: (_, value) =>\n                ['system'].includes(value)\n                  ? Promise.reject(new Error(intl.get('名称不能为保留关键字')))\n                  : Promise.resolve(),\n            },\n          ]}\n        >\n          <Input placeholder={intl.get('请输入应用名称')} />\n        </Form.Item>\n        <Form.Item\n          name=\"scopes\"\n          label={intl.get('权限')}\n          rules={[{ required: true }]}\n        >\n          <Select\n            mode=\"multiple\"\n            placeholder={intl.get('请选择模块权限')}\n            allowClear\n            style={{ width: '100%' }}\n          >\n            {config.scopes.map((x) => {\n              return (\n                <Select.Option key={x.value} value={x.value}>\n                  {x.name}\n                </Select.Option>\n              );\n            })}\n          </Select>\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default AppModal;\n"
  },
  {
    "path": "src/pages/setting/checkUpdate.tsx",
    "content": "import { disableBody } from \"@/utils\";\nimport config from \"@/utils/config\";\nimport { request } from \"@/utils/http\";\nimport WebSocketManager from \"@/utils/websocket\";\nimport Ansi from \"ansi-to-react\";\nimport { Button, Modal, Statistic, message } from \"antd\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport intl from \"react-intl-universal\";\n\nconst { Countdown } = Statistic;\n\nconst CheckUpdate = ({ systemInfo }: any) => {\n  const [updateLoading, setUpdateLoading] = useState(false);\n  const [value, setValue] = useState(\"\");\n  const modalRef = useRef<any>();\n\n  const checkUpgrade = () => {\n    if (updateLoading) return;\n    setUpdateLoading(true);\n    message.loading(intl.get(\"检查更新中...\"), 0);\n    request\n      .put(`${config.apiPrefix}system/update-check`)\n      .then(({ code, data }) => {\n        message.destroy();\n        if (code === 200) {\n          if (data.hasNewVersion) {\n            showConfirmUpdateModal(data);\n          } else {\n            showForceUpdateModal(data);\n          }\n        }\n      })\n      .catch((error: any) => {\n        message.destroy();\n        console.log(error);\n      })\n      .finally(() => {\n        setUpdateLoading(false);\n      });\n  };\n\n  const showForceUpdateModal = (data: any) => {\n    Modal.confirm({\n      width: 500,\n      title: intl.get(\"更新\"),\n      content: (\n        <>\n          <div>{intl.get(\"已经是最新版了！\")}</div>\n          <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>\n            {intl.get(\"青龙\")} {data.lastVersion}{\" \"}\n            {intl.get(\"是目前检测到的最新可用版本了。\")}\n          </div>\n        </>\n      ),\n      okText: intl.get(\"重新下载\"),\n      onOk() {\n        showUpdatingModal();\n        request\n          .put(`${config.apiPrefix}system/update`)\n          .then((_data: any) => { })\n          .catch((error: any) => {\n            console.log(error);\n          });\n      },\n    });\n  };\n\n  const showConfirmUpdateModal = (data: any) => {\n    const { lastVersion, lastLog } = data;\n    Modal.confirm({\n      width: 500,\n      title: (\n        <>\n          <div>{intl.get(\"更新可用\")}</div>\n          <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>\n            {intl.get(\"新版本\")} {lastVersion}{\" \"}\n            {intl.get(\"可用，你使用的版本为\")} {systemInfo.version}。\n          </div>\n        </>\n      ),\n      content: (\n        <pre>\n          <Ansi>{lastLog}</Ansi>\n        </pre>\n      ),\n      okText: intl.get(\"下载更新\"),\n      cancelText: intl.get(\"以后再说\"),\n      onOk() {\n        showUpdatingModal();\n        request\n          .put(`${config.apiPrefix}system/update`)\n          .then((_data: any) => { })\n          .catch((error: any) => {\n            console.log(error);\n          });\n      },\n    });\n  };\n\n  const showUpdatingModal = () => {\n    setValue(\"\");\n    modalRef.current = Modal.info({\n      width: 600,\n      maskClosable: false,\n      closable: false,\n      keyboard: false,\n      okButtonProps: { disabled: true },\n      title: intl.get(\"下载更新中...\"),\n      centered: true,\n      content: (\n        <pre>\n          <Ansi>{value}</Ansi>\n        </pre>\n      ),\n    });\n  };\n\n  const reloadSystem = (type?: string) => {\n    request\n      .put(`${config.apiPrefix}update/${type}`)\n      .then((_data: any) => {\n        message.success({\n          content: (\n            <span>\n              {intl.get(\"系统将在\")}\n              <Countdown\n                className=\"inline-countdown\"\n                format=\"ss\"\n                value={Date.now() + 1000 * 30}\n              />\n              {intl.get(\"秒后自动刷新\")}\n            </span>\n          ),\n          duration: 30,\n        });\n        disableBody();\n        setTimeout(() => {\n          window.location.reload();\n        }, 30000);\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const showReloadModal = () => {\n    Modal.confirm({\n      width: 600,\n      maskClosable: false,\n      title: intl.get(\"确认重启\"),\n      centered: true,\n      content: intl.get(\"系统安装包下载成功，确认重启\"),\n      okText: intl.get(\"重启\"),\n      onOk() {\n        reloadSystem(\"system\");\n      },\n      onCancel() {\n        modalRef.current.update({\n          maskClosable: true,\n          closable: true,\n          okButtonProps: { disabled: false },\n        });\n      },\n    });\n  };\n\n  useEffect(() => {\n    if (!value) return;\n    const updateFailed = value.includes(\"失败，请检查\");\n\n    modalRef.current.update({\n      maskClosable: updateFailed,\n      closable: updateFailed,\n      okButtonProps: { disabled: !updateFailed },\n      content: (\n        <>\n          <pre>\n            <Ansi>{value}</Ansi>\n          </pre>\n          <div id=\"log-identifier\" style={{ paddingBottom: 5 }}></div>\n        </>\n      ),\n    });\n  }, [value]);\n\n  const handleMessage = useCallback((payload: any) => {\n    let { message: _message } = payload;\n    const updateFailed = _message.includes(\"失败，请检查\");\n\n    if (updateFailed) {\n      message.error(intl.get(\"更新失败，请检查网络及日志或稍后再试\"));\n    }\n\n    setTimeout(() => {\n      document\n        .querySelector(\"#log-identifier\")\n        ?.scrollIntoView({ behavior: \"smooth\" });\n    }, 600);\n\n    if (_message.includes(\"更新包下载成功\")) {\n      setTimeout(() => {\n        showReloadModal();\n      }, 1000);\n    }\n\n    setValue((p) => `${p}${_message}`);\n  }, []);\n\n  useEffect(() => {\n    const ws = WebSocketManager.getInstance();\n    ws.subscribe(\"updateSystemVersion\", handleMessage);\n\n    return () => {\n      ws.unsubscribe(\"updateSystemVersion\", handleMessage);\n    };\n  }, []);\n\n  return (\n    <>\n      <Button type=\"primary\" onClick={checkUpgrade}>\n        {intl.get(\"检查更新\")}\n      </Button>\n      <Button\n        type=\"primary\"\n        onClick={() => reloadSystem(\"reload\")}\n        style={{ marginLeft: 8 }}\n      >\n        {intl.get(\"重新启动\")}\n      </Button>\n    </>\n  );\n};\n\nexport default CheckUpdate;\n"
  },
  {
    "path": "src/pages/setting/dependence.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useState, useEffect, useRef } from 'react';\nimport { Button, InputNumber, Form, message, Input, Alert, Select } from 'antd';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport './index.less';\nimport Ansi from 'ansi-to-react';\nimport pick from 'lodash/pick';\nimport WebSocketManager from '@/utils/websocket';\n\nconst dataMap = {\n  'dependence-proxy': 'dependenceProxy',\n  'node-mirror': 'nodeMirror',\n  'python-mirror': 'pythonMirror',\n  'linux-mirror': 'linuxMirror',\n};\n\nconst Dependence = () => {\n  const [systemConfig, setSystemConfig] = useState<{\n    dependenceProxy?: string;\n    nodeMirror?: string;\n    pythonMirror?: string;\n    linuxMirror?: string;\n  }>();\n  const [form] = Form.useForm();\n  const [log, setLog] = useState<string>('');\n  const [loading, setLoading] = useState<boolean>(false);\n  const [cleanType, setCleanType] = useState<string>('node');\n\n  const getSystemConfig = () => {\n    request\n      .get(`${config.apiPrefix}system/config`)\n      .then(({ code, data }) => {\n        if (code === 200 && data.info) {\n          setSystemConfig(data.info);\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const updateSystemConfigStream = (path: keyof typeof dataMap) => {\n    setLoading(true);\n    setLog('in progress...\\n');\n    request\n      .put<string>(\n        `${config.apiPrefix}system/config/${path}`,\n        pick(systemConfig, dataMap[path]),\n      )\n      .then((res) => {})\n      .catch(() => {\n        setLoading(false);\n        setLog((p) => `${p}update mirror error`);\n      });\n  };\n\n  const updateSystemConfig = (path: keyof typeof dataMap) => {\n    setLoading(true);\n    setLog('');\n    request\n      .put(\n        `${config.apiPrefix}system/config/${path}`,\n        pick(systemConfig, dataMap[path]),\n      )\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('更新成功'));\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const handleMessage = (payload: any) => {\n    const { message } = payload;\n    setLog((p) => `${p}${message}`);\n    if (\n      message.includes('update node mirror end') ||\n      message.includes('update linux mirror end')\n    ) {\n      setLoading(false);\n    }\n  };\n\n  const cleanDependenceCache = (type: string) => {\n    setLoading(true);\n    setLog('');\n    request\n      .put(`${config.apiPrefix}system/config/dependence-clean`, {\n        type,\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('清除成功'));\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      })\n      .finally(() => setLoading(false));\n  };\n\n  useEffect(() => {\n    const ws = WebSocketManager.getInstance();\n    ws.subscribe('updateNodeMirror', handleMessage);\n    ws.subscribe('updateLinuxMirror', handleMessage);\n\n    return () => {\n      ws.subscribe('updateNodeMirror', handleMessage);\n      ws.unsubscribe('updateLinuxMirror', handleMessage);\n    };\n  }, []);\n\n  useEffect(() => {\n    getSystemConfig();\n  }, []);\n\n  return (\n    <div className=\"dependence-config-wrapper\">\n      <Form layout=\"vertical\" form={form} style={{ flexShrink: 0 }}>\n        <Form.Item\n          label={intl.get('代理')}\n          name=\"proxy\"\n          extra={intl.get('代理与镜像源二选一即可')}\n          tooltip={{\n            title: intl.get('代理地址, 支持HTTP(S)/SOCK5'),\n            placement: 'topLeft',\n          }}\n        >\n          <Input.Group compact>\n            <Input\n              placeholder={'http://1.1.1.1:8080'}\n              style={{ width: 250 }}\n              value={systemConfig?.dependenceProxy}\n              onChange={(e) => {\n                setSystemConfig({\n                  ...systemConfig,\n                  dependenceProxy: e.target.value,\n                });\n              }}\n            />\n            <Button\n              type=\"primary\"\n              loading={loading}\n              onClick={() => {\n                updateSystemConfig('dependence-proxy');\n              }}\n              style={{ width: 100 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item\n          label={intl.get('Node 软件包镜像源')}\n          name=\"node\"\n          tooltip={intl.get('NPM 镜像源')}\n        >\n          <Input.Group compact>\n            <Input\n              style={{ width: 250 }}\n              placeholder={'https://registry.npmmirror.com'}\n              value={systemConfig?.nodeMirror}\n              onChange={(e) => {\n                setSystemConfig({\n                  ...systemConfig,\n                  nodeMirror: e.target.value,\n                });\n              }}\n            />\n            <Button\n              type=\"primary\"\n              loading={loading}\n              onClick={() => {\n                updateSystemConfigStream('node-mirror');\n              }}\n              style={{ width: 100 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item\n          label={intl.get('Python 软件包镜像源')}\n          name=\"python\"\n          tooltip={intl.get('PyPI 镜像源')}\n        >\n          <Input.Group compact>\n            <Input\n              style={{ width: 250 }}\n              placeholder={'https://pypi.doubanio.com/simple/'}\n              value={systemConfig?.pythonMirror}\n              onChange={(e) => {\n                setSystemConfig({\n                  ...systemConfig,\n                  pythonMirror: e.target.value,\n                });\n              }}\n            />\n            <Button\n              type=\"primary\"\n              loading={loading}\n              onClick={() => {\n                updateSystemConfig('python-mirror');\n              }}\n              style={{ width: 100 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item\n          label={intl.get('Linux 软件包镜像源')}\n          name=\"linux\"\n          tooltip={intl.get('alpine linux 镜像源')}\n        >\n          <Input.Group compact>\n            <Input\n              style={{ width: 250 }}\n              placeholder={'https://mirrors.aliyun.com'}\n              value={systemConfig?.linuxMirror}\n              onChange={(e) => {\n                setSystemConfig({\n                  ...systemConfig,\n                  linuxMirror: e.target.value,\n                });\n              }}\n            />\n            <Button\n              type=\"primary\"\n              loading={loading}\n              onClick={() => {\n                updateSystemConfigStream('linux-mirror');\n              }}\n              style={{ width: 100 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item\n          label={intl.get('清除依赖缓存')}\n          name=\"clean\"\n          tooltip={{\n            title: intl.get('清除依赖缓存'),\n            placement: 'topLeft',\n          }}\n        >\n          <Input.Group compact>\n            <Select\n              defaultValue={'node'}\n              style={{ width: 100 }}\n              onChange={(value) => {\n                setCleanType(value);\n              }}\n              options={[\n                { label: 'node', value: 'node' },\n                { label: 'python3', value: 'python3' },\n              ]}\n            />\n            <Button\n              type=\"primary\"\n              loading={loading}\n              onClick={() => {\n                cleanDependenceCache(cleanType);\n              }}\n              style={{ width: 100 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n      </Form>\n      <pre\n        style={{\n          fontFamily: 'Source Code Pro',\n          zoom: 0.83,\n          maxHeight: '100%',\n          overflowY: 'auto',\n        }}\n      >\n        <Ansi>{log}</Ansi>\n      </pre>\n    </div>\n  );\n};\n\nexport default Dependence;\n"
  },
  {
    "path": "src/pages/setting/index.less",
    "content": ".container {\n  display: flex;\n  justify-content: center;\n  flex-wrap: wrap;\n  max-width: 800px;\n  margin: 20px auto;\n\n  .right {\n    display: flex;\n    justify-content: center;\n    flex-direction: column;\n\n    .title {\n      font-size: 25px;\n      margin-bottom: 10px;\n    }\n\n    .desc {\n      margin-bottom: 10px;\n    }\n\n    :global {\n      .ant-descriptions-row > th,\n      .ant-descriptions-row > td {\n        padding-bottom: 10px;\n      }\n    }\n  }\n}\n\n.dependence-config-wrapper {\n  display: flex;\n  gap: 40px;\n}\n\n.ql-container-wrapper.ql-setting-container {\n  .ant-tabs-tabpane > div {\n    padding-left: 2px;\n  }\n\n  .ant-tabs-tabpane > .ant-form {\n    padding-left: 2px;\n  }\n}\n"
  },
  {
    "path": "src/pages/setting/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useState, useEffect, useRef } from 'react';\nimport {\n  Button,\n  InputNumber,\n  Form,\n  Radio,\n  Tabs,\n  Table,\n  Tooltip,\n  Space,\n  Tag,\n  Modal,\n  message,\n  Typography,\n  Input,\n} from 'antd';\nimport config from '@/utils/config';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { request } from '@/utils/http';\nimport AppModal from './appModal';\nimport {\n  EditOutlined,\n  DeleteOutlined,\n  ReloadOutlined,\n} from '@ant-design/icons';\nimport SecuritySettings from './security';\nimport LoginLog from './loginLog';\nimport NotificationSetting from './notification';\nimport Other from './other';\nimport About from './about';\nimport { useOutletContext } from '@umijs/max';\nimport { SharedContext } from '@/layouts';\nimport './index.less';\nimport useResizeObserver from '@react-hook/resize-observer';\nimport SystemLog from './systemLog';\nimport Dependence from './dependence';\n\nconst { Text } = Typography;\nconst isDemoEnv = window.__ENV__DeployEnv === 'demo';\n\nconst Setting = () => {\n  const {\n    headerStyle,\n    isPhone,\n    user,\n    theme,\n    reloadUser,\n    reloadTheme,\n    systemInfo,\n  } = useOutletContext<SharedContext>();\n  console.log('user',user)\n  const columns = [\n    {\n      title: intl.get('名称'),\n      dataIndex: 'name',\n      key: 'name',\n    },\n    {\n      title: 'Client ID',\n      dataIndex: 'client_id',\n      key: 'client_id',\n      render: (text: string, record: any) => {\n        return <Text copyable>{record.client_id}</Text>;\n      },\n    },\n    {\n      title: 'Client Secret',\n      dataIndex: 'client_secret',\n      key: 'client_secret',\n      render: (text: string, record: any) => {\n        return <Text copyable={{ text: record.client_secret }}>*******</Text>;\n      },\n    },\n    {\n      title: intl.get('权限'),\n      dataIndex: 'scopes',\n      key: 'scopes',\n      width: 500,\n      render: (text: string, record: any) => {\n        return (\n          <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>\n            {record.scopes.map((scope: any) => {\n              return (\n                <Tag style={{ marginRight: 0 }} key={scope}>\n                  {(config.scopesMap as any)[scope]}\n                </Tag>\n              );\n            })}\n          </div>\n        );\n      },\n    },\n    {\n      title: intl.get('操作'),\n      key: 'action',\n      render: (text: string, record: any, index: number) => {\n        const isPc = !isPhone;\n        return (\n          <Space size=\"middle\" style={{ paddingLeft: 8 }}>\n            <Tooltip title={isPc ? intl.get('编辑') : ''}>\n              <a onClick={() => editApp(record, index)}>\n                <EditOutlined />\n              </a>\n            </Tooltip>\n            <Tooltip title={isPc ? intl.get('重置secret') : ''}>\n              <a onClick={() => resetSecret(record, index)}>\n                <ReloadOutlined />\n              </a>\n            </Tooltip>\n            <Tooltip title={isPc ? intl.get('删除') : ''}>\n              <a onClick={() => deleteApp(record, index)}>\n                <DeleteOutlined />\n              </a>\n            </Tooltip>\n          </Space>\n        );\n      },\n    },\n  ];\n\n  const [loading, setLoading] = useState(true);\n  const [dataSource, setDataSource] = useState<any[]>([]);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [editedApp, setEditedApp] = useState<any>();\n  const [tabActiveKey, setTabActiveKey] = useState('security');\n  const [loginLogData, setLoginLogData] = useState<any[]>([]);\n  const [notificationInfo, setNotificationInfo] = useState<any>();\n  const containergRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState<number>(0);\n\n  useResizeObserver(containergRef, (entry) => {\n    const _height = (entry.target as HTMLElement)?.offsetHeight;\n    if (_height && height !== _height - 101) {\n      setHeight(_height - 101);\n    }\n  });\n\n  const getApps = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}apps`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setDataSource(data);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const addApp = () => {\n    setEditedApp(null);\n    setIsModalVisible(true);\n  };\n\n  const editApp = (record: any, index: number) => {\n    setEditedApp(record);\n    setIsModalVisible(true);\n  };\n\n  const deleteApp = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: (\n        <>\n          {intl.get('确认删除应用')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}apps`, { data: [record.id] })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('删除成功'));\n              const result = [...dataSource];\n              result.splice(index, 1);\n              setDataSource(result);\n            }\n          });\n      },\n    });\n  };\n\n  const resetSecret = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认重置'),\n      content: (\n        <>\n          {intl.get('确认重置应用')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('的Secret吗')}\n          <br />\n          <Text type=\"secondary\">\n            {intl.get('重置Secret会让当前应用所有token失效')}\n          </Text>\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}apps/${record.id}/reset-secret`)\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('重置成功'));\n              handleApp(data);\n            }\n          });\n      },\n    });\n  };\n\n  const handleCancel = (app?: any) => {\n    setIsModalVisible(false);\n    if (app) {\n      handleApp(app);\n    }\n  };\n\n  const handleApp = (app: any) => {\n    const index = dataSource.findIndex((x) => x.id === app.id);\n    const result = [...dataSource];\n    if (index === -1) {\n      result.push(app);\n    } else {\n      result.splice(index, 1, {\n        ...app,\n      });\n    }\n    setDataSource(result);\n  };\n\n  const getLoginLog = () => {\n    request\n      .get(`${config.apiPrefix}user/login-log`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setLoginLogData(data);\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const tabChange = (activeKey: string) => {\n    setTabActiveKey(activeKey);\n    if (activeKey === 'app') {\n      getApps();\n    } else if (activeKey === 'login') {\n      getLoginLog();\n    } else if (activeKey === 'notification') {\n      getNotification();\n    }\n  };\n\n  const getNotification = () => {\n    request\n      .get(`${config.apiPrefix}user/notification`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setNotificationInfo(data);\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  useEffect(() => {\n    if (isDemoEnv) {\n      getApps();\n    }\n  }, []);\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container\"\n      title={intl.get('系统设置')}\n      header={{\n        style: headerStyle,\n      }}\n      extra={\n        tabActiveKey === 'app'\n          ? [\n              <Button key=\"2\" type=\"primary\" onClick={() => addApp()}>\n                {intl.get('创建应用')}\n              </Button>,\n            ]\n          : []\n      }\n    >\n      <div ref={containergRef} style={{ height: '100%' }}>\n        <Tabs\n          style={{ height: '100%' }}\n          defaultActiveKey=\"security\"\n          size=\"small\"\n          tabPosition=\"top\"\n          onChange={tabChange}\n          destroyInactiveTabPane\n          items={[\n            ...(!isDemoEnv\n              ? [\n                  {\n                    key: 'security',\n                    label: intl.get('安全设置'),\n                    children: (\n                      <SecuritySettings user={user} userChange={reloadUser} />\n                    ),\n                  },\n                ]\n              : []),\n            {\n              key: 'app',\n              label: intl.get('应用设置'),\n              children: (\n                <Table\n                  columns={columns}\n                  pagination={false}\n                  dataSource={dataSource}\n                  rowKey=\"id\"\n                  size=\"middle\"\n                  scroll={{ x: 1000, y: height }}\n                  loading={loading}\n                />\n              ),\n            },\n            {\n              key: 'notification',\n              label: intl.get('通知设置'),\n              children: <NotificationSetting data={notificationInfo} />,\n            },\n            {\n              key: 'syslog',\n              label: intl.get('系统日志'),\n              children: <SystemLog height={height} theme={theme} />,\n            },\n            {\n              key: 'login',\n              label: intl.get('登录日志'),\n              children: <LoginLog height={height} data={loginLogData} />,\n            },\n            {\n              key: 'dependence',\n              label: intl.get('依赖设置'),\n              children: <Dependence />,\n            },\n            {\n              key: 'other',\n              label: intl.get('其他设置'),\n              children: (\n                <Other reloadTheme={reloadTheme} systemInfo={systemInfo} />\n              ),\n            },\n            {\n              key: 'about',\n              label: intl.get('关于'),\n              children: <About systemInfo={systemInfo} />,\n            },\n          ]}\n        />\n      </div>\n      {isModalVisible && (\n        <AppModal handleCancel={handleCancel} app={editedApp} />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Setting;\n"
  },
  {
    "path": "src/pages/setting/loginLog.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Typography, Table, Tag, Button, Spin, message } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport dayjs from 'dayjs';\n\nconst { Text, Link } = Typography;\n\nenum LoginStatus {\n  '成功',\n  '失败',\n}\n\nenum LoginStatusColor {\n  'success',\n  'error',\n}\n\nconst columns = [\n  {\n    title: intl.get('序号'),\n    width: 50,\n    render: (text: string, record: any, index: number) => {\n      return index + 1;\n    },\n  },\n  {\n    title: intl.get('登录时间'),\n    dataIndex: 'timestamp',\n    key: 'timestamp',\n    width: 120,\n    render: (text: string, record: any) => {\n      return dayjs(record.timestamp).format('YYYY-MM-DD HH:mm:ss');\n    },\n  },\n  {\n    title: intl.get('登录地址'),\n    dataIndex: 'address',\n    width: 120,\n    key: 'address',\n  },\n  {\n    title: intl.get('登录IP'),\n    dataIndex: 'ip',\n    width: 100,\n    key: 'ip',\n  },\n  {\n    title: intl.get('登录设备'),\n    dataIndex: 'platform',\n    key: 'platform',\n    width: 80,\n  },\n  {\n    title: intl.get('登录状态'),\n    dataIndex: 'status',\n    key: 'status',\n    width: 80,\n    render: (text: string, record: any) => {\n      return (\n        <Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>\n          {intl.get(LoginStatus[record.status])}\n        </Tag>\n      );\n    },\n  },\n];\n\nconst LoginLog = ({\n  data,\n  height,\n}: {\n  data: Array<object>;\n  height: number;\n}) => {\n  return (\n    <>\n      <Table\n        columns={columns}\n        pagination={false}\n        dataSource={data}\n        rowKey=\"id\"\n        size=\"middle\"\n        scroll={{ x: 1000, y: height }}\n      />\n    </>\n  );\n};\n\nexport default LoginLog;\n"
  },
  {
    "path": "src/pages/setting/notification.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Typography, Input, Form, Button, Select, message } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\n\nconst { Option } = Select;\n\nconst NotificationSetting = ({ data }: any) => {\n  const [loading, setLoading] = useState(false);\n  const [notificationMode, setNotificationMode] = useState<string>('closed');\n  const [fields, setFields] = useState<any[]>([]);\n  const [form] = Form.useForm();\n\n  const handleOk = (values: any) => {\n    setLoading(true);\n    const { type } = values;\n    if (type == 'closed') {\n      values.type = '';\n    }\n\n    request\n      .put(`${config.apiPrefix}user/notification`, values)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(\n            values.type ? intl.get('通知发送成功') : intl.get('通知关闭成功'),\n          );\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const notificationModeChange = (value: string) => {\n    setNotificationMode(value);\n    const _fields = (config.notificationModeMap as any)[value];\n    setFields(_fields || []);\n  };\n\n  useEffect(() => {\n    if (data && data.type) {\n      notificationModeChange(data.type);\n      form.setFieldsValue({ ...data });\n    }\n  }, [data]);\n\n  return (\n    <div>\n      <Form onFinish={handleOk} form={form} layout=\"vertical\">\n        <Form.Item\n          label={intl.get('通知方式')}\n          name=\"type\"\n          rules={[{ required: true }]}\n          style={{ maxWidth: 400 }}\n          initialValue={notificationMode}\n        >\n          <Select onChange={notificationModeChange} disabled={loading}>\n            {config.notificationModes.map((x) => (\n              <Option key={x.value} value={x.value}>\n                {x.label}\n              </Option>\n            ))}\n          </Select>\n        </Form.Item>\n        {fields.map((x) => (\n          <Form.Item\n            key={x.label}\n            label={x.label}\n            name={x.label}\n            extra={x.tip}\n            rules={[{ required: x.required }]}\n            style={{ maxWidth: 400 }}\n          >\n            {x.items ? (\n              <Select\n                placeholder={x.placeholder || `${intl.get('请选择')} ${x.label}`}\n                disabled={loading}\n              >\n                {x.items.map((y) => (\n                  <Option key={y.value} value={y.value}>\n                    {y.label || y.value}\n                  </Option>\n                ))}\n              </Select>\n            ) : (\n              <Input.TextArea\n                disabled={loading}\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={x.placeholder || `${intl.get('请输入')} ${x.label}`}\n              />\n            )}\n          </Form.Item>\n        ))}\n        <Button type=\"primary\" htmlType=\"submit\" disabled={loading}>\n          {loading ? intl.get('测试中...') : intl.get('保存')}\n        </Button>\n      </Form>\n    </div>\n  );\n};\n\nexport default NotificationSetting;\n"
  },
  {
    "path": "src/pages/setting/other.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useState, useEffect, useRef } from 'react';\nimport {\n  Button,\n  InputNumber,\n  Form,\n  Radio,\n  message,\n  Input,\n  Upload,\n  Modal,\n  Select,\n  Checkbox,\n} from 'antd';\nimport * as DarkReader from '@umijs/ssr-darkreader';\nimport config from '@/utils/config';\nimport { request } from '@/utils/http';\nimport CheckUpdate from './checkUpdate';\nimport { SharedContext } from '@/layouts';\nimport { saveAs } from 'file-saver';\nimport './index.less';\nimport { UploadOutlined } from '@ant-design/icons';\nimport Countdown from 'antd/lib/statistic/Countdown';\nimport useProgress from './progress';\nimport pick from 'lodash/pick';\nimport { disableBody } from '@/utils';\nimport { TIMEZONES } from '@/utils/const';\n\nconst dataMap = {\n  'log-remove-frequency': 'logRemoveFrequency',\n  'cron-concurrency': 'cronConcurrency',\n  timezone: 'timezone',\n  'global-ssh-key': 'globalSshKey',\n};\n\nconst exportModules = [\n  { value: 'base', label: intl.get('基础数据'), disabled: true },\n  { value: 'config', label: intl.get('配置文件') },\n  { value: 'scripts', label: intl.get('脚本文件') },\n  { value: 'log', label: intl.get('日志文件') },\n  { value: 'deps', label: intl.get('依赖文件') },\n  { value: 'syslog', label: intl.get('系统日志') },\n  { value: 'dep_cache', label: intl.get('依赖缓存') },\n  { value: 'raw', label: intl.get('远程脚本缓存') },\n  { value: 'repo', label: intl.get('远程仓库缓存') },\n  { value: 'ssh.d', label: intl.get('SSH 文件缓存') },\n];\n\nconst Other = ({\n  systemInfo,\n  reloadTheme,\n}: Pick<SharedContext, 'reloadTheme' | 'systemInfo'>) => {\n  const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';\n  const [systemConfig, setSystemConfig] = useState<{\n    logRemoveFrequency?: number | null;\n    cronConcurrency?: number | null;\n    timezone?: string | null;\n    globalSshKey?: string | null;\n  }>();\n  const [form] = Form.useForm();\n  const [exportLoading, setExportLoading] = useState(false);\n  const showUploadProgress = useProgress(intl.get('上传'));\n  const showDownloadProgress = useProgress(intl.get('下载'));\n  const [visible, setVisible] = useState(false);\n  const [selectedModules, setSelectedModules] = useState<string[]>(['base']);\n\n  const {\n    enable: enableDarkMode,\n    disable: disableDarkMode,\n    exportGeneratedCSS: collectCSS,\n    setFetchMethod,\n    auto: followSystemColorScheme,\n  } = DarkReader || {};\n\n  const themeChange = (e: any) => {\n    const _theme = e.target.value;\n    localStorage.setItem('qinglong_dark_theme', e.target.value);\n    setFetchMethod(fetch);\n\n    if (_theme === 'dark') {\n      enableDarkMode({});\n    } else if (_theme === 'light') {\n      disableDarkMode();\n    } else {\n      followSystemColorScheme({});\n    }\n    reloadTheme();\n  };\n\n  const handleLangChange = (v: string) => {\n    localStorage.setItem('lang', v);\n    setTimeout(() => {\n      window.location.reload();\n    }, 500);\n  };\n\n  const getSystemConfig = () => {\n    request\n      .get(`${config.apiPrefix}system/config`)\n      .then(({ code, data }) => {\n        if (code === 200 && data.info) {\n          setSystemConfig(data.info);\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const updateSystemConfig = (path: keyof typeof dataMap) => {\n    request\n      .put(\n        `${config.apiPrefix}system/config/${path}`,\n        pick(systemConfig, dataMap[path]),\n      )\n      .then(({ code, data }) => {\n        if (code === 200) {\n          message.success(intl.get('更新成功'));\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const exportData = () => {\n    setExportLoading(true);\n    request\n      .put<Blob>(\n        `${config.apiPrefix}system/data/export`,\n        { type: selectedModules },\n        {\n          responseType: 'blob',\n          timeout: 86400000,\n          onDownloadProgress: (e) => {\n            if (e.progress) {\n              showDownloadProgress(parseFloat((e.progress * 100).toFixed(1)));\n            }\n          },\n        },\n      )\n      .then((res) => {\n        saveAs(res, 'data.tgz');\n      })\n      .catch((error: any) => {\n        console.log(error);\n      })\n      .finally(() => {\n        setExportLoading(false);\n        setVisible(false);\n      });\n  };\n\n  const showReloadModal = () => {\n    Modal.confirm({\n      width: 600,\n      maskClosable: false,\n      title: intl.get('确认重启'),\n      centered: true,\n      content: (\n        <>\n          <div>{intl.get('备份数据上传成功，确认覆盖数据')}</div>\n          <div>{intl.get('如果恢复失败，可进入容器执行')} ql reload data</div>\n        </>\n      ),\n      okText: intl.get('重启'),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}update/data`)\n          .then(() => {\n            message.success({\n              content: (\n                <span>\n                  {intl.get('系统将在')}\n                  <Countdown\n                    className=\"inline-countdown\"\n                    format=\"ss\"\n                    value={Date.now() + 1000 * 30}\n                  />\n                  {intl.get('秒后自动刷新')}\n                </span>\n              ),\n              duration: 30,\n            });\n            disableBody();\n            setTimeout(() => {\n              window.location.reload();\n            }, 30000);\n          })\n          .catch((error: any) => {\n            console.log(error);\n          });\n      },\n    });\n  };\n\n  useEffect(() => {\n    getSystemConfig();\n  }, []);\n\n  return (\n    <>\n      <Form layout=\"vertical\" form={form}>\n        <Form.Item\n          label={intl.get('主题')}\n          name=\"theme\"\n          initialValue={defaultTheme}\n        >\n          <Radio.Group\n            onChange={themeChange}\n            value={defaultTheme}\n            optionType=\"button\"\n            buttonStyle=\"solid\"\n          >\n            <Radio.Button\n              value=\"light\"\n              style={{ width: 70, textAlign: 'center' }}\n            >\n              {intl.get('亮色')}\n            </Radio.Button>\n            <Radio.Button\n              value=\"dark\"\n              style={{ width: 66, textAlign: 'center' }}\n            >\n              {intl.get('暗色')}\n            </Radio.Button>\n            <Radio.Button\n              value=\"auto\"\n              style={{ width: 129, textAlign: 'center' }}\n            >\n              {intl.get('跟随系统')}\n            </Radio.Button>\n          </Radio.Group>\n        </Form.Item>\n        <Form.Item\n          label={intl.get('日志删除频率')}\n          name=\"frequency\"\n          tooltip={intl.get('每x天自动删除x天以前的日志')}\n        >\n          <Input.Group compact>\n            <InputNumber\n              addonBefore={intl.get('每')}\n              addonAfter={intl.get('天')}\n              style={{ width: 180 }}\n              placeholder={intl.get('未启用')}\n              min={0}\n              value={systemConfig?.logRemoveFrequency}\n              onChange={(value) => {\n                setSystemConfig({ ...systemConfig, logRemoveFrequency: value });\n              }}\n            />\n            <Button\n              type=\"primary\"\n              onClick={() => {\n                updateSystemConfig('log-remove-frequency');\n              }}\n              style={{ width: 84 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item label={intl.get('定时任务并发数')} name=\"frequency\">\n          <Input.Group compact>\n            <InputNumber\n              style={{ width: 180 }}\n              min={4}\n              value={systemConfig?.cronConcurrency}\n              placeholder={intl.get('默认为 CPU 个数')}\n              onChange={(value) => {\n                setSystemConfig({ ...systemConfig, cronConcurrency: value });\n              }}\n            />\n            <Button\n              type=\"primary\"\n              onClick={() => {\n                updateSystemConfig('cron-concurrency');\n              }}\n              style={{ width: 84 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item label={intl.get('时区')} name=\"timezone\">\n          <Input.Group compact>\n            <Select\n              value={systemConfig?.timezone}\n              style={{ width: 180 }}\n              onChange={(value) => {\n                setSystemConfig({ ...systemConfig, timezone: value });\n              }}\n              options={TIMEZONES.map((timezone) => ({\n                value: timezone,\n                label: timezone,\n              }))}\n              showSearch\n              filterOption={(input, option) =>\n                option?.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0\n              }\n            />\n            <Button\n              type=\"primary\"\n              onClick={() => {\n                updateSystemConfig('timezone');\n              }}\n              style={{ width: 84 }}\n            >\n              {intl.get('确认')}\n            </Button>\n          </Input.Group>\n        </Form.Item>\n        <Form.Item \n          label={intl.get('全局SSH私钥')} \n          name=\"globalSshKey\"\n          tooltip={intl.get('用于访问所有私有仓库的全局SSH私钥')}\n        >\n          <Input.Group compact>\n            <Input.TextArea\n              value={systemConfig?.globalSshKey || ''}\n              style={{ width: 264 }}\n              autoSize={{ minRows: 3, maxRows: 8 }}\n              placeholder={intl.get('请输入完整的SSH私钥内容')}\n              onChange={(e) => {\n                setSystemConfig({ ...systemConfig, globalSshKey: e.target.value });\n              }}\n            />\n          </Input.Group>\n          <Button\n            type=\"primary\"\n            onClick={() => {\n              updateSystemConfig('global-ssh-key');\n            }}\n            style={{ width: 264, marginTop: 8 }}\n          >\n            {intl.get('确认')}\n          </Button>\n        </Form.Item>\n        <Form.Item label={intl.get('语言')} name=\"lang\">\n          <Select\n            defaultValue={localStorage.getItem('lang') || ''}\n            style={{ width: 264 }}\n            onChange={handleLangChange}\n            options={[\n              { value: '', label: intl.get('跟随系统') },\n              { value: 'zh', label: '简体中文' },\n              { value: 'en', label: 'English' },\n            ]}\n          />\n        </Form.Item>\n        <Form.Item label={intl.get('数据备份还原')} name=\"frequency\">\n          <Button\n            type=\"primary\"\n            onClick={() => {\n              setSelectedModules(['base']);\n              setVisible(true);\n            }}\n            loading={exportLoading}\n          >\n            {exportLoading ? intl.get('生成数据中...') : intl.get('备份')}\n          </Button>\n          <Upload\n            method=\"put\"\n            showUploadList={false}\n            maxCount={1}\n            action={`${config.apiPrefix}system/data/import`}\n            onChange={({ file, event }) => {\n              if (event?.percent) {\n                showUploadProgress(\n                  Math.min(parseFloat(event?.percent.toFixed(1)), 99),\n                );\n              }\n              if (file.status === 'done') {\n                showUploadProgress(100);\n                showReloadModal();\n              }\n              if (file.status === 'error') {\n                message.error('上传失败');\n              }\n            }}\n            name=\"data\"\n            headers={{\n              Authorization: `Bearer ${localStorage.getItem(config.authKey)}`,\n            }}\n          >\n            <Button icon={<UploadOutlined />} style={{ marginLeft: 8 }}>\n              {intl.get('还原数据')}\n            </Button>\n          </Upload>\n        </Form.Item>\n        <Form.Item label={intl.get('检查更新')} name=\"update\">\n          <CheckUpdate systemInfo={systemInfo} />\n        </Form.Item>\n      </Form>\n      <Modal\n        title={intl.get('选择备份模块')}\n        open={visible}\n        onOk={exportData}\n        onCancel={() => setVisible(false)}\n        okText={intl.get('开始备份')}\n        cancelText={intl.get('取消')}\n        okButtonProps={{ loading: exportLoading }} // 绑定加载状态到按钮\n      >\n        <Checkbox.Group\n          value={selectedModules}\n          onChange={(v) => {\n            setSelectedModules(v as string[]);\n          }}\n          style={{\n            width: '100%',\n            display: 'flex',\n            flexWrap: 'wrap',\n            gap: '8px 16px',\n          }}\n        >\n          {exportModules.map((module) => (\n            <Checkbox\n              key={module.value}\n              value={module.value}\n              disabled={module.disabled}\n              style={{ marginLeft: 0 }}\n            >\n              {module.label}\n            </Checkbox>\n          ))}\n        </Checkbox.Group>\n      </Modal>\n    </>\n  );\n};\n\nexport default Other;\n"
  },
  {
    "path": "src/pages/setting/progress.tsx",
    "content": "import intl from 'react-intl-universal';\nimport { Modal, Progress } from 'antd';\nimport { useRef } from 'react';\n\nconst ProgressElement = ({ percent }: { percent: number }) => (\n  <Progress\n    style={{ display: 'flex', justifyContent: 'center' }}\n    type=\"circle\"\n    percent={percent}\n  />\n);\n\nexport default function useProgress(title: string) {\n  const modalRef = useRef<ReturnType<typeof Modal.info> | null>();\n\n  const showProgress = (percent: number) => {\n    if (modalRef.current) {\n      modalRef.current.update({\n        title: `${title}${\n          percent >= 100 ? intl.get('成功') : intl.get('中...')\n        }`,\n        content: <ProgressElement percent={percent} />,\n        okButtonProps: { disabled: percent !== 100 },\n      });\n      if (percent === 100) {\n        setTimeout(() => {\n          modalRef.current?.destroy();\n          modalRef.current = null;\n        });\n      }\n    } else {\n      modalRef.current = Modal.info({\n        width: 600,\n        maskClosable: false,\n        title: `${title}${\n          percent >= 100 ? intl.get('成功') : intl.get('中...')\n        }`,\n        centered: true,\n        content: <ProgressElement percent={percent} />,\n        okButtonProps: { disabled: true },\n      });\n    }\n  };\n\n  return showProgress;\n}\n"
  },
  {
    "path": "src/pages/setting/security.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport { history } from '@umijs/max';\nimport QRCode from 'qrcode.react';\nimport { PageLoading } from '@ant-design/pro-layout';\nimport { UploadOutlined, UserOutlined } from '@ant-design/icons';\nimport ImgCrop from 'antd-img-crop';\nimport 'antd/es/slider/style';\n\nconst { Title, Link } = Typography;\n\nconst SecuritySettings = ({ user, userChange }: any) => {\n  const [loading, setLoading] = useState(false);\n  const [twoFactorActivated, setTwoFactorActivated] = useState<boolean>();\n  const [twoFactoring, setTwoFactoring] = useState(false);\n  const [twoFactorInfo, setTwoFactorInfo] = useState<any>();\n  const [code, setCode] = useState<string>();\n  const [avatar, setAvatar] = useState<string>();\n\n  const handleOk = (values: any) => {\n    request\n      .put(`${config.apiPrefix}user`, {\n        username: values.username,\n        password: values.password,\n      })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          localStorage.removeItem(config.authKey);\n          history.push('/login');\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const activeOrDeactiveTwoFactor = () => {\n    if (twoFactorActivated) {\n      deactiveTowFactor();\n    } else {\n      getTwoFactorInfo();\n      setTwoFactoring(true);\n    }\n  };\n\n  const deactiveTowFactor = () => {\n    request\n      .put(`${config.apiPrefix}user/two-factor/deactive`)\n      .then(({ code, data }) => {\n        if (code === 200 && data) {\n          setTwoFactorActivated(false);\n          userChange();\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const completeTowFactor = () => {\n    setLoading(true);\n    request\n      .put(`${config.apiPrefix}user/two-factor/active`, { code })\n      .then(({ code, data }) => {\n        if (code === 200) {\n          if (data) {\n            message.success(intl.get('激活成功'));\n            setTwoFactoring(false);\n            setTwoFactorActivated(true);\n            userChange();\n          } else {\n            message.success(intl.get('验证失败'));\n          }\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const getTwoFactorInfo = () => {\n    request\n      .get(`${config.apiPrefix}user/two-factor/init`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setTwoFactorInfo(data);\n        }\n      })\n      .catch((error: any) => {\n        console.log(error);\n      });\n  };\n\n  const onChange = (e) => {\n    if (e.file && e.file.response) {\n      setAvatar(\n        `${config.apiPrefix}static/${e.file.response.data}`,\n      );\n      userChange();\n    }\n  };\n\n  useEffect(() => {\n    setTwoFactorActivated(user && user.twoFactorActivated);\n    setAvatar(user.avatar && `${config.apiPrefix}static/${user.avatar}`);\n  }, [user]);\n\n  return twoFactoring ? (\n    <>\n      {twoFactorInfo ? (\n        <div>\n          <Title level={5}>{intl.get('第一步')}</Title>\n          {intl.get('下载两步验证手机应用，比如 Google Authenticator 、')}\n          <Link\n            href=\"https://www.microsoft.com/en-us/security/mobile-authenticator-app\"\n            target=\"_blank\"\n          >\n            Microsoft Authenticator\n          </Link>\n          、\n          <Link href=\"https://authy.com/download/\" target=\"_blank\">\n            Authy\n          </Link>\n          、\n          <Link\n            href=\"https://support.1password.com/one-time-passwords/\"\n            target=\"_blank\"\n          >\n            1Password\n          </Link>\n          、\n          <Link\n            href=\"https://support.logmeininc.com/lastpass/help/lastpass-authenticator-lp030014\"\n            target=\"_blank\"\n          >\n            LastPass Authenticator\n          </Link>\n          <Title style={{ marginTop: 5 }} level={5}>\n            {intl.get('第二步')}\n          </Title>\n          {intl.get('使用手机应用扫描二维码，或者输入秘钥')}{' '}\n          {twoFactorInfo?.secret}\n          <div style={{ marginTop: 10 }}>\n            <QRCode\n              style={{ border: '1px solid #21262d', borderRadius: 6 }}\n              includeMargin={true}\n              size={187}\n              value={twoFactorInfo?.url}\n            />\n          </div>\n          <Title style={{ marginTop: 5 }} level={5}>\n            {intl.get('第三步')}\n          </Title>\n          {intl.get('输入手机应用上的6位数字')}\n          <Input\n            style={{ margin: '10px 0 10px 0', display: 'block', maxWidth: 200 }}\n            value={code}\n            onChange={(e) => setCode(e.target.value)}\n            placeholder=\"123456\"\n          />\n          <Button type=\"primary\" loading={loading} onClick={completeTowFactor}>\n            {intl.get('完成设置')}\n          </Button>\n        </div>\n      ) : (\n        <PageLoading />\n      )}\n    </>\n  ) : (\n    <>\n      <div\n        style={{\n          fontSize: 18,\n          borderBottom: '1px solid #f0f0f0',\n          marginBottom: 8,\n          paddingBottom: 4,\n        }}\n      >\n        {intl.get('修改用户名密码')}\n      </div>\n      <Form onFinish={handleOk} layout=\"vertical\">\n        <Form.Item\n          label={intl.get('用户名')}\n          name=\"username\"\n          rules={[{ required: true }]}\n          hasFeedback\n          style={{ maxWidth: 300 }}\n        >\n          <Input autoComplete=\"username\" placeholder={intl.get('用户名')} />\n        </Form.Item>\n        <Form.Item\n          label={intl.get('密码')}\n          name=\"password\"\n          rules={[\n            { required: true },\n            {\n              pattern: /^(?!admin$).*$/,\n              message: intl.get('密码不能为admin'),\n            },\n          ]}\n          hasFeedback\n          style={{ maxWidth: 300 }}\n        >\n          <Input\n            type=\"password\"\n            autoComplete=\"current-password\"\n            placeholder={intl.get('密码')}\n          />\n        </Form.Item>\n        <Button type=\"primary\" htmlType=\"submit\">\n          {intl.get('保存')}\n        </Button>\n      </Form>\n\n      <div\n        style={{\n          fontSize: 18,\n          borderBottom: '1px solid #f0f0f0',\n          marginBottom: 8,\n          paddingBottom: 4,\n          marginTop: 16,\n        }}\n      >\n        {intl.get('两步验证')}\n      </div>\n      <Button\n        type=\"primary\"\n        danger={twoFactorActivated}\n        onClick={activeOrDeactiveTwoFactor}\n      >\n        {twoFactorActivated ? intl.get('禁用') : intl.get('启用')}\n      </Button>\n\n      <div\n        style={{\n          fontSize: 18,\n          borderBottom: '1px solid #f0f0f0',\n          marginBottom: 8,\n          paddingBottom: 4,\n          marginTop: 16,\n        }}\n      >\n        {intl.get('头像')}\n      </div>\n      <Avatar size={128} shape=\"square\" icon={<UserOutlined />} src={avatar} />\n      <ImgCrop rotationSlider>\n        <Upload\n          method=\"put\"\n          showUploadList={false}\n          maxCount={1}\n          action={`${config.apiPrefix}user/avatar`}\n          onChange={onChange}\n          name=\"avatar\"\n          headers={{\n            Authorization: `Bearer ${localStorage.getItem(config.authKey)}`,\n          }}\n        >\n          <Button icon={<UploadOutlined />} style={{ marginLeft: 8 }}>\n            {intl.get('更换头像')}\n          </Button>\n        </Upload>\n      </ImgCrop>\n    </>\n  );\n};\n\nexport default SecuritySettings;\n"
  },
  {
    "path": "src/pages/setting/systemLog.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useRef, useState } from 'react';\nimport CodeMirror from '@uiw/react-codemirror';\nimport { Button, DatePicker, Empty, message, Spin } from 'antd';\nimport {\n  VerticalAlignBottomOutlined,\n  VerticalAlignTopOutlined,\n} from '@ant-design/icons';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport { useRequest } from 'ahooks';\nimport moment from 'moment';\nimport {\n  systemLogDebugHighlightPlugin,\n  systemLogErrorHighlightPlugin,\n  systemLogInfoHighlightPlugin,\n  systemLogTheme,\n  systemLogWarnHighlightPlugin,\n} from '@/utils/codemirror/systemLog';\n\nconst { RangePicker } = DatePicker;\n\nconst SystemLog = ({ height, theme }: any) => {\n  const editorRef = useRef<any>(null);\n  const panelVisiableRef = useRef<[string, string] | false>();\n  const [range, setRange] = useState<string[]>(['', '']);\n  const [systemLogData, setSystemLogData] = useState<string>('');\n\n  const { loading, refresh } = useRequest(\n    () => {\n      return request.get<Blob>(\n        `${config.apiPrefix}system/log?startTime=${range[0]}&endTime=${range[1]}`,\n        {\n          responseType: 'blob',\n        },\n      );\n    },\n    {\n      refreshDeps: [range],\n      async onSuccess(res) {\n        setSystemLogData(await res.text());\n      },\n    },\n  );\n\n  const scrollTo = (position: 'start' | 'end') => {\n    editorRef.current.scrollDOM.scrollTo({\n      top: position === 'start' ? 0 : editorRef.current.scrollDOM.scrollHeight,\n    });\n  };\n\n  const deleteLog = () => {\n    request.delete(`${config.apiPrefix}system/log`).then((x) => {\n      message.success('删除成功');\n      refresh();\n    });\n  };\n\n  return (\n    <div style={{ position: 'relative' }}>\n      <div>\n        <RangePicker\n          style={{ marginBottom: 12, marginRight: 12 }}\n          disabledDate={(date) =>\n            date > moment() || date < moment().subtract(7, 'days')\n          }\n          defaultValue={[moment(), moment()]}\n          onOpenChange={(v) => {\n            panelVisiableRef.current = v ? ['', ''] : false;\n          }}\n          onCalendarChange={(_, dates, { range }) => {\n            if (\n              !panelVisiableRef.current ||\n              typeof panelVisiableRef.current === 'boolean'\n            ) {\n              return;\n            }\n            if (range === 'start') {\n              panelVisiableRef.current[0] = dates[0];\n            }\n            if (range === 'end') {\n              panelVisiableRef.current[1] = dates[1];\n            }\n            if (panelVisiableRef.current[0] && panelVisiableRef.current[1]) {\n              setRange(dates);\n            }\n          }}\n        />\n        <Button\n          onClick={() => {\n            deleteLog();\n          }}\n        >\n          {intl.get('清空日志')}\n        </Button>\n      </div>\n      {systemLogData ? (\n        <>\n          <CodeMirror\n            maxHeight={`${height}px`}\n            value={systemLogData}\n            onCreateEditor={(view) => {\n              editorRef.current = view;\n            }}\n            extensions={[\n              systemLogDebugHighlightPlugin,\n              systemLogErrorHighlightPlugin,\n              systemLogInfoHighlightPlugin,\n              systemLogWarnHighlightPlugin,\n              systemLogTheme,\n            ]}\n            readOnly={true}\n            theme={theme.includes('dark') ? 'dark' : 'light'}\n          />\n          <div\n            style={{\n              position: 'absolute',\n              bottom: 20,\n              right: 20,\n              display: 'flex',\n              flexDirection: 'column',\n              gap: 10,\n            }}\n          >\n            <Button\n              size=\"small\"\n              icon={<VerticalAlignTopOutlined />}\n              onClick={() => {\n                scrollTo('start');\n              }}\n            />\n            <Button\n              size=\"small\"\n              icon={<VerticalAlignBottomOutlined />}\n              onClick={() => {\n                scrollTo('end');\n              }}\n            />\n          </div>\n        </>\n      ) : loading ? (\n        <Spin />\n      ) : (\n        <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />\n      )}\n    </div>\n  );\n};\n\nexport default SystemLog;\n"
  },
  {
    "path": "src/pages/subscription/index.less",
    "content": ".inline-form-item {\n  margin-bottom: 0;\n\n  .ant-form-item {\n    display: inline-block;\n    width: 50%;\n    margin-bottom: 0px;\n  }\n}\n"
  },
  {
    "path": "src/pages/subscription/index.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useState, useEffect, useRef, useCallback } from 'react';\nimport {\n  Button,\n  message,\n  Modal,\n  Table,\n  Tag,\n  Space,\n  Dropdown,\n  Menu,\n  Typography,\n  Input,\n  Tooltip,\n  Checkbox,\n} from 'antd';\nimport {\n  ClockCircleOutlined,\n  Loading3QuartersOutlined,\n  CloseCircleOutlined,\n  EllipsisOutlined,\n  CheckCircleOutlined,\n  EditOutlined,\n  StopOutlined,\n  DeleteOutlined,\n  FileTextOutlined,\n  PauseCircleOutlined,\n  PlayCircleOutlined,\n} from '@ant-design/icons';\nimport config from '@/utils/config';\nimport { PageContainer } from '@ant-design/pro-layout';\nimport { request } from '@/utils/http';\nimport SubscriptionModal from './modal';\nimport { history, useOutletContext } from '@umijs/max';\nimport './index.less';\nimport SubscriptionLogModal from './logModal';\nimport { SharedContext } from '@/layouts';\nimport useTableScrollHeight from '@/hooks/useTableScrollHeight';\nimport WebSocketManager from '@/utils/websocket';\n\nconst { Text, Paragraph } = Typography;\nconst { Search } = Input;\n\nexport enum SubscriptionStatus {\n  'running',\n  'idle',\n  'disabled',\n  'queued',\n}\n\nexport enum IntervalSchedule {\n  'days' = '天',\n  'hours' = '时',\n  'minutes' = '分',\n  'seconds' = '秒',\n}\n\nexport enum SubscriptionType {\n  'private-repo' = '私有仓库',\n  'public-repo' = '公开仓库',\n  'file' = '单文件',\n}\n\nconst Subscription = () => {\n  const { headerStyle, isPhone } = useOutletContext<SharedContext>();\n\n  const columns: any = [\n    {\n      title: intl.get('名称'),\n      dataIndex: 'name',\n      key: 'name',\n      width: 150,\n      sorter: {\n        compare: (a: any, b: any) => a.name.localeCompare(b.name),\n        multiple: 2,\n      },\n    },\n    {\n      title: intl.get('链接'),\n      dataIndex: 'url',\n      key: 'url',\n      sorter: {\n        compare: (a: any, b: any) => a.name.localeCompare(b.name),\n        multiple: 2,\n      },\n      render: (text: string, record: any) => {\n        return (\n          <Paragraph\n            style={{\n              wordBreak: 'break-all',\n              marginBottom: 0,\n            }}\n            ellipsis={{ tooltip: text, rows: 2 }}\n          >\n            {text}\n          </Paragraph>\n        );\n      },\n    },\n    {\n      title: intl.get('类型'),\n      dataIndex: 'type',\n      key: 'type',\n      width: 130,\n      render: (text: string, record: any) => {\n        return (SubscriptionType as any)[record.type];\n      },\n    },\n    {\n      title: intl.get('分支'),\n      dataIndex: 'branch',\n      key: 'branch',\n      width: 130,\n      render: (text: string, record: any) => {\n        return record.branch || '-';\n      },\n    },\n    {\n      title: intl.get('定时规则'),\n      width: 180,\n      render: (text: string, record: any) => {\n        if (record.schedule_type === 'interval') {\n          const { type, value } = record.interval_schedule;\n          return `每${value}${(IntervalSchedule as any)[type]}`;\n        }\n        return record.schedule;\n      },\n    },\n    {\n      title: intl.get('状态'),\n      key: 'status',\n      dataIndex: 'status',\n      width: 110,\n      filters: [\n        {\n          text: intl.get('运行中'),\n          value: 0,\n        },\n        {\n          text: intl.get('空闲中'),\n          value: 1,\n        },\n        {\n          text: intl.get('已禁用'),\n          value: 2,\n        },\n      ],\n      onFilter: (value: number, record: any) => {\n        if (record.is_disabled && record.status !== 0) {\n          return value === 2;\n        } else {\n          return record.status === value;\n        }\n      },\n      render: (text: string, record: any) => (\n        <>\n          {(!record.is_disabled ||\n            record.status !== SubscriptionStatus.idle) && (\n            <>\n              {record.status === SubscriptionStatus.idle && (\n                <Tag icon={<ClockCircleOutlined />} color=\"default\">\n                  {intl.get('空闲中')}\n                </Tag>\n              )}\n              {record.status === SubscriptionStatus.running && (\n                <Tag\n                  icon={<Loading3QuartersOutlined spin />}\n                  color=\"processing\"\n                >\n                  {intl.get('运行中')}\n                </Tag>\n              )}\n            </>\n          )}\n          {record.is_disabled === 1 &&\n            record.status === SubscriptionStatus.idle && (\n              <Tag icon={<CloseCircleOutlined />} color=\"error\">\n                {intl.get('已禁用')}\n              </Tag>\n            )}\n        </>\n      ),\n    },\n    {\n      title: intl.get('操作'),\n      key: 'action',\n      width: 140,\n      render: (text: string, record: any, index: number) => {\n        const isPc = !isPhone;\n        return (\n          <Space size=\"middle\">\n            {record.status === SubscriptionStatus.idle && (\n              <a\n                onClick={(e) => {\n                  e.stopPropagation();\n                  runSubscription(record, index);\n                }}\n              >\n                {intl.get('运行')}\n              </a>\n            )}\n            {record.status !== SubscriptionStatus.idle && (\n              <a\n                onClick={(e) => {\n                  e.stopPropagation();\n                  stopSubsciption(record, index);\n                }}\n              >\n                {intl.get('停止')}\n              </a>\n            )}\n            <a\n              onClick={(e) => {\n                e.stopPropagation();\n                setLogSubscription({ ...record, timestamp: Date.now() });\n              }}\n            >\n              {intl.get('日志')}\n            </a>\n            <MoreBtn key=\"more\" record={record} index={index} />\n          </Space>\n        );\n      },\n    },\n  ];\n\n  const [value, setValue] = useState<any[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [editedSubscription, setEditedSubscription] = useState();\n  const [searchText, setSearchText] = useState('');\n  const [currentPage, setCurrentPage] = useState(1);\n  const [pageSize, setPageSize] = useState(20);\n  const [isLogModalVisible, setIsLogModalVisible] = useState(false);\n  const [logSubscription, setLogSubscription] = useState<any>();\n  const tableRef = useRef<HTMLDivElement>(null);\n  const tableScrollHeight = useTableScrollHeight(tableRef);\n  const deleteCheckRef = useRef(false);\n\n  const runSubscription = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认运行'),\n      content: (\n        <>\n          {intl.get('确认运行定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}subscriptions/run`, [record.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  status: SubscriptionStatus.running,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const stopSubsciption = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认停止'),\n      content: (\n        <>\n          {intl.get('确认停止定时任务')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(`${config.apiPrefix}subscriptions/stop`, [record.id])\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  pid: null,\n                  status: SubscriptionStatus.idle,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const getSubscriptions = () => {\n    setLoading(true);\n    request\n      .get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)\n      .then(({ code, data }) => {\n        if (code === 200) {\n          setValue(data);\n          setCurrentPage(1);\n        }\n      })\n      .finally(() => setLoading(false));\n  };\n\n  const addSubscription = () => {\n    setEditedSubscription(null as any);\n    setIsModalVisible(true);\n  };\n\n  const editSubscription = (record: any, index: number) => {\n    setEditedSubscription(record);\n    setIsModalVisible(true);\n  };\n\n  const onCheckChange = (e) => {\n    deleteCheckRef.current = e.target.checked;\n  };\n\n  const delSubscription = (record: any, index: number) => {\n    Modal.confirm({\n      title: intl.get('确认删除'),\n      content: (\n        <>\n          {intl.get('确认删除定时订阅')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n          <div style={{ marginTop: 20 }}>\n            <Checkbox onChange={onCheckChange}>\n              {intl.get('同时删除关联任务和脚本')}\n            </Checkbox>\n          </div>\n        </>\n      ),\n      onOk() {\n        request\n          .delete(`${config.apiPrefix}subscriptions`, {\n            data: [record.id],\n            params: { force: deleteCheckRef.current },\n          })\n          .then(({ code, data }) => {\n            if (code === 200) {\n              message.success(intl.get('删除成功'));\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1);\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const enabledOrDisabledSubscription = (record: any, index: number) => {\n    Modal.confirm({\n      title: `确认${\n        record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用')\n      }`,\n      content: (\n        <>\n          {intl.get('确认')}\n          {record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用')}\n          {intl.get('定时订阅')}{' '}\n          <Text style={{ wordBreak: 'break-all' }} type=\"warning\">\n            {record.name}\n          </Text>{' '}\n          {intl.get('吗')}\n        </>\n      ),\n      onOk() {\n        request\n          .put(\n            `${config.apiPrefix}subscriptions/${\n              record.is_disabled === 1 ? 'enable' : 'disable'\n            }`,\n            [record.id],\n          )\n          .then(({ code, data }) => {\n            if (code === 200) {\n              const newStatus = record.is_disabled === 1 ? 0 : 1;\n              const result = [...value];\n              const i = result.findIndex((x) => x.id === record.id);\n              if (i !== -1) {\n                result.splice(i, 1, {\n                  ...record,\n                  is_disabled: newStatus,\n                });\n                setValue(result);\n              }\n            }\n          });\n      },\n    });\n  };\n\n  const MoreBtn: React.FC<{\n    record: any;\n    index: number;\n  }> = ({ record, index }) => (\n    <Dropdown\n      placement=\"bottomRight\"\n      trigger={['click']}\n      menu={{\n        items: [\n          { label: intl.get('编辑'), key: 'edit', icon: <EditOutlined /> },\n          {\n            label:\n              record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用'),\n            key: 'enableOrDisable',\n            icon:\n              record.is_disabled === 1 ? (\n                <CheckCircleOutlined />\n              ) : (\n                <StopOutlined />\n              ),\n          },\n          { label: intl.get('删除'), key: 'delete', icon: <DeleteOutlined /> },\n        ],\n        onClick: ({ key, domEvent }) => {\n          domEvent.stopPropagation();\n          action(key, record, index);\n        },\n      }}\n    >\n      <a onClick={(e) => e.stopPropagation()}>\n        <EllipsisOutlined />\n      </a>\n    </Dropdown>\n  );\n\n  const action = (key: string | number, record: any, index: number) => {\n    switch (key) {\n      case 'edit':\n        editSubscription(record, index);\n        break;\n      case 'enableOrDisable':\n        enabledOrDisabledSubscription(record, index);\n        break;\n      case 'delete':\n        delSubscription(record, index);\n        break;\n      default:\n        break;\n    }\n  };\n\n  const handleCancel = (subscription?: any) => {\n    setIsModalVisible(false);\n    if (subscription) {\n      handleSubscriptions(subscription);\n    }\n  };\n\n  const onSearch = (value: string) => {\n    setSearchText(value.trim());\n  };\n\n  const handleSubscriptions = (subscription: any) => {\n    const index = value.findIndex((x) => x.id === subscription.id);\n    const result = [...value];\n    if (index === -1) {\n      result.unshift(subscription);\n    } else {\n      result.splice(index, 1, {\n        ...subscription,\n      });\n    }\n    setValue(result);\n  };\n\n  const onPageChange = (page: number, pageSize: number | undefined) => {\n    setCurrentPage(page);\n    setPageSize(pageSize as number);\n    localStorage.setItem('pageSize', pageSize + '');\n  };\n\n  const getRowClassName = (record: any, index: number) => {\n    return record.isPinned\n      ? 'pinned-subscription subscription'\n      : 'subscription';\n  };\n\n  const handleMessage = useCallback((payload: any) => {\n    const { message, references } = payload;\n    setValue((p) => {\n      const result = [...p];\n      for (let i = 0; i < references.length; i++) {\n        const index = p.findIndex((x) => x.id === references[i]);\n        if (index !== -1) {\n          result.splice(index, 1, {\n            ...p[index],\n            status: SubscriptionStatus.idle,\n          });\n        }\n      }\n      return result;\n    });\n  }, []);\n\n  useEffect(() => {\n    const ws = WebSocketManager.getInstance();\n    ws.subscribe('runSubscriptionEnd', handleMessage);\n\n    return () => {\n      ws.unsubscribe('runSubscriptionEnd', handleMessage);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (logSubscription) {\n      localStorage.setItem('logSubscription', logSubscription.id);\n      setIsLogModalVisible(true);\n    }\n  }, [logSubscription]);\n\n  useEffect(() => {\n    getSubscriptions();\n  }, [searchText]);\n\n  useEffect(() => {\n    setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));\n  }, []);\n\n  return (\n    <PageContainer\n      className=\"ql-container-wrapper subscriptiontab-wrapper\"\n      title={intl.get('订阅管理')}\n      extra={[\n        <Search\n          placeholder={intl.get('请输入名称或者关键词')}\n          style={{ width: 'auto' }}\n          enterButton\n          allowClear\n          loading={loading}\n          onSearch={onSearch}\n        />,\n        <Button key=\"2\" type=\"primary\" onClick={() => addSubscription()}>\n          {intl.get('创建订阅')}\n        </Button>,\n      ]}\n      header={{\n        style: headerStyle,\n      }}\n    >\n      <Table\n        ref={tableRef}\n        columns={columns}\n        pagination={{\n          current: currentPage,\n          onChange: onPageChange,\n          pageSize: pageSize,\n          showSizeChanger: true,\n          simple: isPhone,\n          defaultPageSize: 20,\n          showTotal: (total: number, range: number[]) =>\n            `第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,\n          pageSizeOptions: [20, 100, 500, 1000] as any,\n        }}\n        dataSource={value}\n        rowKey=\"id\"\n        size=\"middle\"\n        scroll={{ x: 1000, y: tableScrollHeight }}\n        loading={loading}\n        rowClassName={getRowClassName}\n      />\n      {isModalVisible && (\n        <SubscriptionModal\n          handleCancel={handleCancel}\n          subscription={editedSubscription}\n        />\n      )}\n      {isLogModalVisible && (\n        <SubscriptionLogModal\n          handleCancel={() => {\n            setIsLogModalVisible(false);\n          }}\n          subscription={logSubscription}\n        />\n      )}\n    </PageContainer>\n  );\n};\n\nexport default Subscription;\n"
  },
  {
    "path": "src/pages/subscription/logModal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useEffect, useState } from 'react';\nimport { Modal, message, Input, Form, Statistic, Button } from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport {\n  Loading3QuartersOutlined,\n  CheckCircleOutlined,\n} from '@ant-design/icons';\nimport { PageLoading } from '@ant-design/pro-layout';\nimport { logEnded } from '@/utils';\nimport Ansi from 'ansi-to-react';\n\nconst SubscriptionLogModal = ({\n  subscription,\n  handleCancel,\n  data,\n  logUrl,\n}: {\n  subscription?: any;\n  handleCancel: () => void;\n  data?: string;\n  logUrl?: string;\n}) => {\n  const [value, setValue] = useState<string>(intl.get('启动中...'));\n  const [loading, setLoading] = useState<any>(true);\n  const [executing, setExecuting] = useState<any>(true);\n  const [isPhone, setIsPhone] = useState(false);\n\n  const getCronLog = (isFirst?: boolean) => {\n    if (isFirst) {\n      setLoading(true);\n    }\n    request\n      .get(\n        logUrl\n          ? logUrl\n          : `${config.apiPrefix}subscriptions/${subscription.id}/log`,\n      )\n      .then(({ code, data }) => {\n        if (\n          code === 200 &&\n          localStorage.getItem('logSubscription') === String(subscription.id)\n        ) {\n          const log = data as string;\n          setValue(log || intl.get('暂无日志'));\n          setExecuting(log && !logEnded(log));\n          if (log && !logEnded(log)) {\n            setTimeout(() => {\n              getCronLog();\n            }, 2000);\n          }\n        }\n      })\n      .finally(() => {\n        if (isFirst) {\n          setLoading(false);\n        }\n      });\n  };\n\n  const cancel = () => {\n    localStorage.removeItem('logSubscription');\n    handleCancel();\n  };\n\n  const titleElement = () => {\n    return (\n      <>\n        {(executing || loading) && <Loading3QuartersOutlined spin />}\n        {!executing && !loading && <CheckCircleOutlined />}\n        <span style={{ marginLeft: 5 }}>\n          {subscription && subscription.name}\n        </span>\n      </>\n    );\n  };\n\n  useEffect(() => {\n    if (subscription && subscription.id) {\n      getCronLog(true);\n    }\n  }, [subscription]);\n\n  useEffect(() => {\n    if (data) {\n      setValue(data);\n    }\n  }, [data]);\n\n  useEffect(() => {\n    setIsPhone(document.body.clientWidth < 768);\n  }, []);\n\n  return (\n    <Modal\n      title={titleElement()}\n      open={true}\n      centered\n      className=\"log-modal\"\n      forceRender\n      onOk={() => cancel()}\n      onCancel={() => cancel()}\n      footer={[\n        <Button type=\"primary\" onClick={() => cancel()}>\n          {intl.get('知道了')}\n        </Button>,\n      ]}\n    >\n      <div className=\"log-container\">\n        {loading ? (\n          <PageLoading />\n        ) : (\n          <pre\n            style={\n              isPhone\n                ? {\n                    fontFamily: 'Source Code Pro',\n                    zoom: 0.83,\n                  }\n                : {}\n            }\n          >\n            <Ansi>{value}</Ansi>\n          </pre>\n        )}\n      </div>\n    </Modal>\n  );\n};\n\nexport default SubscriptionLogModal;\n"
  },
  {
    "path": "src/pages/subscription/modal.tsx",
    "content": "import intl from 'react-intl-universal';\nimport React, { useCallback, useEffect, useState } from 'react';\nimport {\n  Modal,\n  message,\n  InputNumber,\n  Form,\n  Radio,\n  Select,\n  Input,\n  Switch,\n} from 'antd';\nimport { request } from '@/utils/http';\nimport config from '@/utils/config';\nimport CronExpressionParser from 'cron-parser';\nimport isNil from 'lodash/isNil';\n\nconst { Option } = Select;\nconst repoUrlRegx = /([^\\/\\:]+\\/[^\\/]+)(?=\\.git)/;\nconst fileUrlRegx = /([^\\/\\:]+\\/[^\\/\\.]+)\\.[a-z]+$/;\n\nconst SubscriptionModal = ({\n  subscription,\n  handleCancel,\n}: {\n  subscription?: any;\n  handleCancel: (needUpdate?: boolean) => void;\n}) => {\n  const [form] = Form.useForm();\n  const [loading, setLoading] = useState(false);\n  const [type, setType] = useState(subscription?.type || 'public-repo');\n  const [scheduleType, setScheduleType] = useState(\n    subscription?.schedule_type || 'crontab',\n  );\n  const [pullType, setPullType] = useState<'ssh-key' | 'user-pwd'>(\n    subscription?.pull_type || 'ssh-key',\n  );\n\n  const handleOk = async (values: any) => {\n    setLoading(true);\n    const method = subscription ? 'put' : 'post';\n    const payload = {\n      ...values,\n      autoAddCron: Boolean(values.autoAddCron),\n      autoDelCron: Boolean(values.autoDelCron),\n    };\n    if (subscription) {\n      payload.id = subscription.id;\n    }\n    try {\n      const { code, data } = await request[method](\n        `${config.apiPrefix}subscriptions`,\n        payload,\n      );\n      if (code === 200) {\n        message.success(\n          subscription ? intl.get('更新订阅成功') : intl.get('创建订阅成功'),\n        );\n        handleCancel(data);\n      }\n      setLoading(false);\n    } catch (error: any) {\n      setLoading(false);\n    }\n  };\n\n  const typeChange = (e) => {\n    setType(e.target.value);\n    const _url = form.getFieldValue('url');\n    const _branch = form.getFieldValue('branch');\n    form.setFieldsValue({\n      alias: formatAlias(_url, _branch, e.target.value),\n    });\n    if (_url) {\n      form.validateFields(['url']);\n    }\n  };\n\n  const scheduleTypeChange = (e) => {\n    setScheduleType(e.target.value);\n    form.setFieldsValue({ schedule: '' });\n  };\n\n  const pullTypeChange = (e) => {\n    setPullType(e.target.value);\n  };\n\n  const onUrlChange = (e) => {\n    const _branch = form.getFieldValue('branch');\n    form.setFieldsValue({\n      alias: formatAlias(e.target.value, _branch),\n    });\n  };\n\n  const onBranchChange = (e) => {\n    const _url = form.getFieldValue('url');\n    form.setFieldsValue({\n      alias: formatAlias(_url, e.target.value),\n    });\n  };\n\n  const formatAlias = (_url: string, _branch: string, _type = type) => {\n    let _alias = '';\n    const _regx = _type === 'file' ? fileUrlRegx : repoUrlRegx;\n    if (_regx.test(_url)) {\n      _alias = _url.match(_regx)![1].replaceAll('/', '_').replaceAll('.', '_');\n    }\n    if (_branch) {\n      _alias = _alias + '_' + _branch;\n    }\n    return _alias;\n  };\n\n  const IntervalSelect = ({\n    value,\n    onChange,\n  }: {\n    value?: any;\n    onChange?: (param: any) => void;\n  }) => {\n    const [intervalType, setIntervalType] = useState('days');\n    const [intervalNumber, setIntervalNumber] = useState<number>();\n    const intervalTypeChange = (type: string) => {\n      setIntervalType(type);\n      if (intervalNumber && intervalNumber > 0) {\n        onChange?.({ type, value: intervalNumber });\n      }\n    };\n\n    const numberChange = (value: number | null) => {\n      setIntervalNumber(value || 1);\n      if (!value) {\n        onChange?.(null);\n      } else {\n        onChange?.({ type: intervalType, value });\n      }\n    };\n\n    useEffect(() => {\n      if (value) {\n        setIntervalType(value.type);\n        setIntervalNumber(value.value);\n      }\n    }, [value]);\n\n    return (\n      <Input.Group compact>\n        <InputNumber\n          addonBefore={intl.get('每')}\n          precision={0}\n          min={1}\n          value={intervalNumber}\n          style={{ width: 'calc(100% - 58px)' }}\n          onChange={numberChange}\n        />\n        <Select value={intervalType} onChange={intervalTypeChange}>\n          <Option value=\"days\">{intl.get('天')}</Option>\n          <Option value=\"hours\">{intl.get('时')}</Option>\n          <Option value=\"minutes\">{intl.get('分')}</Option>\n          <Option value=\"seconds\">{intl.get('秒')}</Option>\n        </Select>\n      </Input.Group>\n    );\n  };\n\n  const PullOptions = ({\n    value,\n    onChange,\n    type,\n  }: {\n    value?: any;\n    type: 'ssh-key' | 'user-pwd';\n    onChange?: (param: any) => void;\n  }) => {\n    return type === 'ssh-key' ? (\n      <Form.Item\n        name={['pull_option', 'private_key']}\n        label={intl.get('私钥')}\n        rules={[{ required: true }]}\n      >\n        <Input.TextArea\n          rows={4}\n          autoSize={{ minRows: 1, maxRows: 6 }}\n          placeholder={intl.get('请输入私钥')}\n        />\n      </Form.Item>\n    ) : (\n      <>\n        <Form.Item\n          name={['pull_option', 'username']}\n          label={intl.get('用户名')}\n          rules={[{ required: true }]}\n        >\n          <Input placeholder={intl.get('请输入认证用户名')} />\n        </Form.Item>\n        <Form.Item\n          name={['pull_option', 'password']}\n          tooltip={intl.get('Github已不支持密码认证，请使用Token方式')}\n          label={intl.get('密码/Token')}\n          rules={[{ required: true }]}\n        >\n          <Input placeholder={intl.get('请输入密码或者Token')} />\n        </Form.Item>\n      </>\n    );\n  };\n\n  const onPaste = useCallback((e: any) => {\n    const text = e.clipboardData.getData('text') as string;\n    if (text.startsWith('ql ')) {\n      const [\n        ,\n        type,\n        url,\n        whitelist,\n        blacklist,\n        dependences,\n        branch,\n        extensions,\n      ] = text\n        .split(' ')\n        .map((x) => x.trim().replace(/\\\"/g, '').replace(/\\'/, ''));\n      const _type =\n        type === 'raw'\n          ? 'file'\n          : url.startsWith('http')\n            ? 'public-repo'\n            : 'private-repo';\n\n      form.setFieldsValue({\n        type: _type,\n        url,\n        whitelist,\n        blacklist,\n        dependences,\n        branch,\n        extensions,\n        alias: formatAlias(url, branch, _type),\n      });\n      setType(_type);\n    }\n  }, []);\n\n  const onNamePaste = useCallback((e) => {\n    const text = e.clipboardData.getData('text') as string;\n    if (text.includes('ql repo') || text.includes('ql raw')) {\n      e.preventDefault();\n    }\n  }, []);\n\n  const formatParams = (sub) => {\n    return {\n      ...sub,\n      autoAddCron: isNil(sub?.autoAddCron) ? true : Boolean(sub?.autoAddCron),\n      autoDelCron: isNil(sub?.autoDelCron) ? true : Boolean(sub?.autoDelCron),\n    };\n  };\n\n  useEffect(() => {\n    window.addEventListener('paste', onPaste);\n\n    return () => {\n      window.removeEventListener('paste', onPaste);\n    };\n  }, []);\n\n  return (\n    <Modal\n      title={subscription ? intl.get('编辑订阅') : intl.get('创建订阅')}\n      open={true}\n      forceRender\n      centered\n      maskClosable={false}\n      onOk={() => {\n        form\n          .validateFields()\n          .then((values) => {\n            handleOk(values);\n          })\n          .catch((info) => {\n            console.log('Validate Failed:', info);\n          });\n      }}\n      onCancel={() => handleCancel()}\n      confirmLoading={loading}\n    >\n      <Form\n        form={form}\n        name=\"form_in_modal\"\n        layout=\"vertical\"\n        initialValues={{ ...subscription, ...formatParams(subscription) }}\n      >\n        <Form.Item\n          name=\"name\"\n          label={intl.get('名称')}\n          rules={[{ required: true }]}\n        >\n          <Input\n            placeholder={intl.get('支持拷贝 ql repo/raw 命令，粘贴导入')}\n            onPaste={onNamePaste}\n          />\n        </Form.Item>\n        <Form.Item\n          name=\"type\"\n          label={intl.get('类型')}\n          rules={[{ required: true }]}\n          initialValue={'public-repo'}\n        >\n          <Radio.Group onChange={typeChange}>\n            <Radio value=\"public-repo\">{intl.get('公开仓库')}</Radio>\n            <Radio value=\"private-repo\">{intl.get('私有仓库')}</Radio>\n            <Radio value=\"file\">{intl.get('单文件')}</Radio>\n          </Radio.Group>\n        </Form.Item>\n        <Form.Item\n          name=\"url\"\n          label={intl.get('链接')}\n          rules={[\n            { required: true },\n            { pattern: type === 'file' ? fileUrlRegx : repoUrlRegx },\n          ]}\n        >\n          <Input.TextArea\n            autoSize={{ minRows: 1, maxRows: 5 }}\n            placeholder={intl.get('请输入订阅链接')}\n            onPaste={onNamePaste}\n            onChange={onUrlChange}\n          />\n        </Form.Item>\n        {type !== 'file' && (\n          <Form.Item name=\"branch\" label={intl.get('分支')}>\n            <Input\n              placeholder={intl.get('请输入分支')}\n              onPaste={onNamePaste}\n              onChange={onBranchChange}\n            />\n          </Form.Item>\n        )}\n        <Form.Item\n          name=\"alias\"\n          label={intl.get('唯一值')}\n          rules={[{ required: true, message: '' }]}\n          tooltip={intl.get('唯一值用于日志目录和私钥别名')}\n        >\n          <Input placeholder={intl.get('自动生成')} disabled />\n        </Form.Item>\n        {type === 'private-repo' && (\n          <>\n            <Form.Item\n              name=\"pull_type\"\n              label={intl.get('拉取方式')}\n              initialValue={'ssh-key'}\n              rules={[{ required: true }]}\n            >\n              <Radio.Group onChange={pullTypeChange}>\n                <Radio value=\"ssh-key\">{intl.get('私钥')}</Radio>\n                <Radio value=\"user-pwd\">{intl.get('用户名密码/Token')}</Radio>\n              </Radio.Group>\n            </Form.Item>\n            <PullOptions type={pullType} />\n          </>\n        )}\n        <Form.Item\n          name=\"schedule_type\"\n          label={intl.get('定时类型')}\n          initialValue={'crontab'}\n          rules={[{ required: true }]}\n        >\n          <Radio.Group onChange={scheduleTypeChange}>\n            <Radio value=\"crontab\">crontab</Radio>\n            <Radio value=\"interval\">interval</Radio>\n          </Radio.Group>\n        </Form.Item>\n        <Form.Item\n          name={scheduleType === 'crontab' ? 'schedule' : 'interval_schedule'}\n          label={intl.get('定时规则')}\n          rules={[\n            { required: true },\n            {\n              validator: (rule, value) => {\n                try {\n                  if (\n                    scheduleType === 'interval' ||\n                    !value ||\n                    CronExpressionParser.parse(value).hasNext()\n                  ) {\n                    return Promise.resolve();\n                  } else {\n                    return Promise.reject(intl.get('Subscription表达式格式有误'));\n                  }\n                } catch (e) {\n                  return Promise.reject(intl.get('Subscription表达式格式有误'));\n                }\n              },\n            },\n          ]}\n        >\n          {scheduleType === 'interval' ? (\n            <IntervalSelect />\n          ) : (\n            <Input\n              onPaste={onNamePaste}\n              placeholder={intl.get('秒(可选) 分 时 天 月 周')}\n            />\n          )}\n        </Form.Item>\n        {type !== 'file' && (\n          <>\n            <Form.Item\n              name=\"whitelist\"\n              label={intl.get('白名单')}\n              tooltip={intl.get('多个关键词竖线分割，支持正则表达式')}\n            >\n              <Input.TextArea\n                rows={4}\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={intl.get(\n                  '请输入脚本筛选白名单关键词，多个关键词竖线分割',\n                )}\n                onPaste={onNamePaste}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"blacklist\"\n              label={intl.get('黑名单')}\n              tooltip={intl.get('多个关键词竖线分割，支持正则表达式')}\n            >\n              <Input.TextArea\n                rows={4}\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={intl.get(\n                  '请输入脚本筛选黑名单关键词，多个关键词竖线分割',\n                )}\n                onPaste={onNamePaste}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"dependences\"\n              label={intl.get('依赖文件')}\n              tooltip={intl.get('多个关键词竖线分割，支持正则表达式')}\n            >\n              <Input.TextArea\n                rows={4}\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={intl.get(\n                  '请输入脚本依赖文件关键词，多个关键词竖线分割',\n                )}\n                onPaste={onNamePaste}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"extensions\"\n              label={intl.get('文件后缀')}\n              tooltip={intl.get(\n                '仓库需要拉取的文件后缀，多个后缀空格分隔，默认使用配置文件中的RepoFileExtensions',\n              )}\n            >\n              <Input\n                onPaste={onNamePaste}\n                placeholder={intl.get('请输入文件后缀')}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"sub_before\"\n              label={intl.get('执行前')}\n              tooltip={intl.get(\n                '运行订阅前执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js',\n              )}\n            >\n              <Input.TextArea\n                onPaste={onNamePaste}\n                rows={4}\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={intl.get('请输入运行订阅前要执行的命令')}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"sub_after\"\n              label={intl.get('执行后')}\n              tooltip={intl.get(\n                '运行订阅后执行的命令，比如 cp/mv/python3 xxx.py/node xxx.js',\n              )}\n            >\n              <Input.TextArea\n                onPaste={onNamePaste}\n                rows={4}\n                autoSize={{ minRows: 1, maxRows: 5 }}\n                placeholder={intl.get('请输入运行订阅后要执行的命令')}\n              />\n            </Form.Item>\n          </>\n        )}\n        <Form.Item\n          name=\"proxy\"\n          label={intl.get('代理')}\n          tooltip={intl.get(\n            '公开仓库支持HTTP/SOCK5代理，私有仓库支持SOCK5代理',\n          )}\n        >\n          <Input\n            onPaste={onNamePaste}\n            placeholder={\n              type === 'private-repo'\n                ? 'SOCK5代理，例如 IP:PORT'\n                : 'HTTP/SOCK5代理，例如 http://127.0.0.1:1080'\n            }\n          />\n        </Form.Item>\n        <Form.Item style={{ marginBottom: 0 }} className=\"inline-form-item\">\n          <Form.Item\n            name=\"autoAddCron\"\n            label={intl.get('自动添加任务')}\n            valuePropName=\"checked\"\n            initialValue={true}\n          >\n            <Switch />\n          </Form.Item>\n          <Form.Item\n            name=\"autoDelCron\"\n            label={intl.get('自动删除任务')}\n            valuePropName=\"checked\"\n            initialValue={true}\n          >\n            <Switch />\n          </Form.Item>\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default SubscriptionModal;\n"
  },
  {
    "path": "src/styles/variable.less",
    "content": "@tree-width: 300px;\n"
  },
  {
    "path": "src/utils/codemirror/systemLog.ts",
    "content": "import {\n  Decoration,\n  EditorView,\n  ViewPlugin,\n  ViewUpdate,\n} from '@codemirror/view';\nimport { RangeSet, RangeSetBuilder } from '@codemirror/state';\n\nconst infoWord = /\\[ℹ️info/g;\nconst debugWord = /\\[⚠️debug/g;\nconst warnWord = /\\[❌warn/g;\nconst errorWord = /\\[🐛error/g;\n\nconst customWordClassMap = {\n  info: 'system-log-info',\n  warn: 'system-warn-info',\n  error: 'system-error-info',\n  debug: 'system-debug-info',\n};\n\nexport const systemLogInfoHighlightPlugin = ViewPlugin.fromClass(\n  class {\n    decorations: RangeSet<Decoration>;\n\n    constructor(view: EditorView) {\n      this.decorations = this.getDecorations(view);\n    }\n\n    update(update: ViewUpdate) {\n      if (update.docChanged) {\n        this.decorations = this.getDecorations(update.view);\n      }\n    }\n\n    getDecorations(view: EditorView) {\n      const builder = new RangeSetBuilder<Decoration>();\n      const doc = view.state.doc.toString();\n      let match;\n\n      while ((match = infoWord.exec(doc)) !== null) {\n        const deco = Decoration.mark({\n          class: customWordClassMap.info,\n        });\n\n        builder.add(match.index, match.index + match[0].length, deco);\n      }\n\n      return builder.finish();\n    }\n  },\n  {\n    decorations: (v) => v.decorations,\n  },\n);\n\nexport const systemLogWarnHighlightPlugin = ViewPlugin.fromClass(\n  class {\n    decorations: RangeSet<Decoration>;\n\n    constructor(view: EditorView) {\n      this.decorations = this.getDecorations(view);\n    }\n\n    update(update: ViewUpdate) {\n      if (update.docChanged) {\n        this.decorations = this.getDecorations(update.view);\n      }\n    }\n\n    getDecorations(view: EditorView) {\n      const builder = new RangeSetBuilder<Decoration>();\n      const doc = view.state.doc.toString();\n      let match;\n\n      while ((match = warnWord.exec(doc)) !== null) {\n        const deco = Decoration.mark({\n          class: customWordClassMap.warn,\n        });\n\n        builder.add(match.index, match.index + match[0].length, deco);\n      }\n\n      return builder.finish();\n    }\n  },\n  {\n    decorations: (v) => v.decorations,\n  },\n);\n\nexport const systemLogDebugHighlightPlugin = ViewPlugin.fromClass(\n  class {\n    decorations: RangeSet<Decoration>;\n\n    constructor(view: EditorView) {\n      this.decorations = this.getDecorations(view);\n    }\n\n    update(update: ViewUpdate) {\n      if (update.docChanged) {\n        this.decorations = this.getDecorations(update.view);\n      }\n    }\n\n    getDecorations(view: EditorView) {\n      const builder = new RangeSetBuilder<Decoration>();\n      const doc = view.state.doc.toString();\n      let match;\n\n      while ((match = debugWord.exec(doc)) !== null) {\n        const deco = Decoration.mark({\n          class: customWordClassMap.debug,\n        });\n\n        builder.add(match.index, match.index + match[0].length, deco);\n      }\n\n      return builder.finish();\n    }\n  },\n  {\n    decorations: (v) => v.decorations,\n  },\n);\n\nexport const systemLogErrorHighlightPlugin = ViewPlugin.fromClass(\n  class {\n    decorations: RangeSet<Decoration>;\n\n    constructor(view: EditorView) {\n      this.decorations = this.getDecorations(view);\n    }\n\n    update(update: ViewUpdate) {\n      if (update.docChanged) {\n        this.decorations = this.getDecorations(update.view);\n      }\n    }\n\n    getDecorations(view: EditorView) {\n      const builder = new RangeSetBuilder<Decoration>();\n      const doc = view.state.doc.toString();\n      let match;\n\n      while ((match = errorWord.exec(doc)) !== null) {\n        const deco = Decoration.mark({\n          class: customWordClassMap.error,\n        });\n\n        builder.add(match.index, match.index + match[0].length, deco);\n      }\n\n      return builder.finish();\n    }\n  },\n  {\n    decorations: (v) => v.decorations,\n  },\n);\n\nexport const systemLogTheme = EditorView.baseTheme({\n  '.system-log-info': {\n    color: '#2196F3',\n  },\n  '.system-warn-info': {\n    color: '#FFB827',\n  },\n  '.system-error-info': {\n    color: '#FA5151',\n  },\n  '.system-debug-info': {\n    color: '#009A29',\n  },\n});\n"
  },
  {
    "path": "src/utils/config.ts",
    "content": "import intl from 'react-intl-universal';\nconst baseUrl = window.__ENV__QlBaseUrl || '/';\n\nexport default {\n  siteName: intl.get('青龙'),\n  baseUrl,\n  apiPrefix: `${baseUrl}api/`,\n  authKey: 'token',\n\n  /* Layout configuration, specify which layout to use for route. */\n  layouts: [\n    {\n      name: 'primary',\n      include: [/.*/],\n      exclude: [/(\\/(en|zh))*\\/login/],\n    },\n  ],\n\n  /* I18n configuration, `languages` and `defaultLanguage` are required currently. */\n  i18n: {\n    /* Countrys flags: https://www.flaticon.com/packs/countrys-flags */\n    languages: [\n      {\n        key: 'pt-br',\n        title: 'Português',\n        flag: '/portugal.svg',\n      },\n      {\n        key: 'en',\n        title: 'English',\n        flag: '/america.svg',\n      },\n      {\n        key: 'zh',\n        title: intl.get('中文'),\n        flag: '/china.svg',\n      },\n    ],\n    defaultLanguage: 'en',\n  },\n  scopes: [\n    {\n      name: intl.get('定时任务'),\n      value: 'crons',\n    },\n    {\n      name: intl.get('环境变量'),\n      value: 'envs',\n    },\n    {\n      name: intl.get('订阅管理'),\n      value: 'subscriptions',\n    },\n    {\n      name: intl.get('配置文件'),\n      value: 'configs',\n    },\n    {\n      name: intl.get('脚本管理'),\n      value: 'scripts',\n    },\n    {\n      name: intl.get('日志管理'),\n      value: 'logs',\n    },\n    {\n      name: intl.get('依赖管理'),\n      value: 'dependencies',\n    },\n    {\n      name: intl.get('系统信息'),\n      value: 'system',\n    },\n  ],\n  scopesMap: {\n    crons: intl.get('定时任务'),\n    envs: intl.get('环境变量'),\n    subscriptions: intl.get('订阅管理'),\n    configs: intl.get('配置文件'),\n    scripts: intl.get('脚本管理'),\n    logs: intl.get('日志管理'),\n    dependencies: intl.get('依赖管理'),\n    system: intl.get('系统信息'),\n  },\n  notificationModes: [\n    { value: 'gotify', label: 'Gotify' },\n    { value: 'ntfy', label: 'Ntfy' },\n    { value: 'goCqHttpBot', label: 'GoCqHttpBot' },\n    { value: 'serverChan', label: intl.get('Server酱') },\n    { value: 'pushDeer', label: 'PushDeer' },\n    { value: 'bark', label: 'Bark' },\n    { value: 'telegramBot', label: intl.get('Telegram机器人') },\n    { value: 'dingtalkBot', label: intl.get('钉钉机器人') },\n    { value: 'weWorkBot', label: intl.get('企业微信机器人') },\n    { value: 'weWorkApp', label: intl.get('企业微信应用') },\n    { value: 'aibotk', label: intl.get('智能微秘书') },\n    { value: 'iGot', label: 'IGot' },\n    { value: 'pushPlus', label: 'PushPlus' },\n    { value: 'wePlusBot', label: intl.get('微加机器人') },\n    { value: 'wxPusherBot', label: 'wxPusher' },\n    { value: 'chat', label: intl.get('群晖chat') },\n    { value: 'email', label: intl.get('邮箱') },\n    { value: 'lark', label: intl.get('飞书机器人') },\n    { value: 'pushMe', label: 'PushMe' },\n    { value: 'chronocat', label: 'Chronocat' },\n    { value: 'webhook', label: intl.get('自定义通知') },\n    { value: 'closed', label: intl.get('已关闭') },\n  ],\n  notificationModeMap: {\n    gotify: [\n      {\n        label: 'gotifyUrl',\n        tip: intl.get('gotify的url地址，例如 https://push.example.de:8080'),\n        required: true,\n      },\n      {\n        label: 'gotifyToken',\n        tip: intl.get('gotify的消息应用token码'),\n        required: true,\n      },\n      { label: 'gotifyPriority', tip: intl.get('推送消息的优先级') },\n    ],\n    ntfy: [\n      {\n        label: 'ntfyUrl',\n        tip: intl.get('ntfy的url地址，例如 https://ntfy.sh'),\n        required: true,\n      },\n      {\n        label: 'ntfyTopic',\n        tip: intl.get('ntfy应用topic'),\n        required: true,\n      },\n      { label: 'ntfyPriority', tip: intl.get('推送消息的优先级') },\n      { label: 'ntfyToken', tip: intl.get('ntfy应用token') },\n      { label: 'ntfyUsername', tip: intl.get('ntfy应用用户名') },\n      { label: 'ntfyPassword', tip: intl.get('ntfy应用密码') },\n      { label: 'ntfyActions', tip: intl.get('ntfy用户动作') },\n    ],\n    chat: [\n      {\n        label: 'synologyChatUrl',\n        tip: intl.get('synologyChat的url地址'),\n        required: true,\n      },\n    ],\n    goCqHttpBot: [\n      {\n        label: 'goCqHttpBotUrl',\n        tip: intl.get(\n          '推送到个人QQ: http://127.0.0.1/send_private_msg，群：http://127.0.0.1/send_group_msg',\n        ),\n        required: true,\n      },\n      { label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true },\n      {\n        label: 'goCqHttpBotQq',\n        tip: intl.get(\n          '如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群',\n        ),\n        required: true,\n      },\n    ],\n    serverChan: [\n      {\n        label: 'serverChanKey',\n        tip: intl.get('Server酱SENDKEY'),\n        required: true,\n      },\n    ],\n    pushDeer: [\n      {\n        label: 'pushDeerKey',\n        tip: intl.get('PushDeer的Key，https://github.com/easychen/pushdeer'),\n        required: true,\n      },\n      {\n        label: 'pushDeerUrl',\n        tip: intl.get(\n          'PushDeer的自架API endpoint，默认是 https://api2.pushdeer.com/message/push',\n        ),\n      },\n    ],\n    bark: [\n      {\n        label: 'barkPush',\n        tip: intl.get(\n          'Bark的信息IP/设备码，例如：https://api.day.app/XXXXXXXX',\n        ),\n        required: true,\n      },\n      {\n        label: 'barkIcon',\n        tip: intl.get('BARK推送图标，自定义推送图标 (需iOS15或以上才能显示)'),\n      },\n      {\n        label: 'barkSound',\n        tip: intl.get('BARK推送铃声，铃声列表去APP查看复制填写'),\n      },\n      {\n        label: 'barkGroup',\n        tip: intl.get('BARK推送消息的分组，默认为qinglong'),\n      },\n      {\n        label: 'barkLevel',\n        tip: intl.get('BARK推送消息的时效性，默认为active'),\n      },\n      {\n        label: 'barkUrl',\n        tip: intl.get('BARK推送消息的跳转URL'),\n      },\n      {\n        label: 'barkArchive',\n        tip: intl.get('BARK是否保存推送消息'),\n      },\n    ],\n    telegramBot: [\n      {\n        label: 'telegramBotToken',\n        tip: intl.get(\n          'telegram机器人的token，例如：1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',\n        ),\n        required: true,\n      },\n      {\n        label: 'telegramBotUserId',\n        tip: intl.get('telegram用户的id，例如：129xxx206'),\n        required: true,\n      },\n      { label: 'telegramBotProxyHost', tip: intl.get('代理IP') },\n      { label: 'telegramBotProxyPort', tip: intl.get('代理端口') },\n      {\n        label: 'telegramBotProxyAuth',\n        tip: intl.get(\n          'telegram代理配置认证参数，用户名与密码用英文冒号连接 user:password',\n        ),\n      },\n      {\n        label: 'telegramBotApiHost',\n        tip: intl.get('telegram api自建的反向代理地址，默认tg官方api'),\n      },\n    ],\n    dingtalkBot: [\n      {\n        label: 'dingtalkBotToken',\n        tip: intl.get(\n          '钉钉机器人webhook token，例如：5a544165465465645d0f31dca676e7bd07415asdasd',\n        ),\n        required: true,\n      },\n      {\n        label: 'dingtalkBotSecret',\n        tip: intl.get(\n          '密钥，机器人安全设置页面，加签一栏下面显示的SEC开头的字符串',\n        ),\n      },\n    ],\n    weWorkBot: [\n      {\n        label: 'weWorkBotKey',\n        tip: intl.get(\n          '企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770)，例如：693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa',\n        ),\n        required: true,\n      },\n      {\n        label: 'weWorkOrigin',\n        tip: intl.get('企业微信代理地址'),\n      },\n    ],\n    weWorkApp: [\n      {\n        label: 'weWorkAppKey',\n        tip: intl.get(\n          'corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填，不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号)，例如：wwcfrs,B-76WERQ,qinglong,1000001,2COat',\n        ),\n        required: true,\n      },\n      {\n        label: 'weWorkOrigin',\n        tip: intl.get('企业微信代理地址'),\n      },\n    ],\n    aibotk: [\n      {\n        label: 'aibotkKey',\n        tip: intl.get(\n          '密钥key，智能微秘书个人中心获取apikey，申请地址：https://wechat.aibotk.com/signup?from=ql',\n        ),\n        required: true,\n      },\n      {\n        label: 'aibotkType',\n        tip: intl.get('发送的目标，群组或者好友'),\n        required: true,\n        placeholder: intl.get('请输入要发送的目标'),\n        items: [\n          { value: 'room', label: intl.get('群聊') },\n          { value: 'contact', label: intl.get('好友') },\n        ],\n      },\n      {\n        label: 'aibotkName',\n        tip: intl.get(\n          '要发送的用户昵称或群名，如果目标是群，需要填群名，如果目标是好友，需要填好友昵称',\n        ),\n        required: true,\n      },\n    ],\n    iGot: [\n      {\n        label: 'iGotPushKey',\n        tip: intl.get(\n          'iGot的信息推送key，例如：https://push.hellyw.com/XXXXXXXX',\n        ),\n        required: true,\n      },\n    ],\n    pushPlus: [\n      {\n        label: 'pushPlusToken',\n        tip: intl.get(\n          '微信扫码登录后一对一推送或一对多推送下面的token(您的Token)，不提供PUSH_PLUS_USER则默认为一对一推送，参考 https://www.pushplus.plus/',\n        ),\n        required: true,\n      },\n      {\n        label: 'pushPlusUser',\n        tip: intl.get(\n          '一对多推送的“群组编码”（一对多推送下面->您的群组(如无则创建)->群组编码，如果您是创建群组人。也需点击“查看二维码”扫描绑定，否则不能接受群组消息推送）',\n        ),\n      },\n      {\n        label: 'pushplusTemplate',\n        tip: intl.get('发送模板'),\n      },\n      {\n        label: 'pushplusChannel',\n        tip: intl.get('发送渠道'),\n      },\n      {\n        label: 'pushplusWebhook',\n        tip: intl.get('webhook编码'),\n      },\n      {\n        label: 'pushplusCallbackUrl',\n        tip: intl.get('发送结果回调地址'),\n      },\n      {\n        label: 'pushplusTo',\n        tip: intl.get('好友令牌'),\n      },\n    ],\n    wePlusBot: [\n      {\n        label: 'wePlusBotToken',\n        tip: intl.get(\n          '用户令牌，扫描登录后 我的—>设置->令牌 中获取，参考 https://www.weplusbot.com/',\n        ),\n        required: true,\n      },\n      {\n        label: 'wePlusBotReceiver',\n        tip: intl.get('消息接收人'),\n      },\n      {\n        label: 'wePlusBotVersion',\n        tip: intl.get(\n          '调用版本；专业版填写pro，个人版填写personal，为空默认使用专业版',\n        ),\n      },\n    ],\n    wxPusherBot: [\n      {\n        label: 'wxPusherBotAppToken',\n        tip: intl.get('wxPusherBot的appToken'),\n        required: true,\n      },\n      {\n        label: 'wxPusherBotTopicIds',\n        tip: intl.get('wxPusherBot的topicIds'),\n        required: false,\n      },\n      {\n        label: 'wxPusherBotUids',\n        tip: intl.get('wxPusherBot的uids'),\n        required: false,\n      },\n    ],\n    lark: [\n      {\n        label: 'larkKey',\n        tip: intl.get(\n          '飞书群组机器人：https://www.feishu.cn/hc/zh-CN/articles/360024984973',\n        ),\n        required: true,\n      },\n      {\n        label: 'larkSecret',\n        tip: intl.get(\n          '飞书群组机器人加签密钥，安全设置中开启签名校验后获得',\n        ),\n      },\n    ],\n    email: [\n      {\n        label: 'emailService',\n        tip: intl.get(\n          '邮箱服务名称，比如126、163、Gmail、QQ等，支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json',\n        ),\n        required: true,\n      },\n      { label: 'emailUser', tip: intl.get('邮箱认证地址'), required: true },\n      {\n        label: 'emailPass',\n        tip: intl.get(\n          'SMTP 登录密码，也可能为特殊口令，视具体邮件服务商说明而定',\n        ),\n        required: true,\n      },\n      {\n        label: 'emailTo',\n        tip: intl.get('收件邮箱地址，多个分号分隔，默认发送给发件邮箱地址'),\n      },\n    ],\n    pushMe: [\n      {\n        label: 'pushMeKey',\n        tip: intl.get('PushMe的Key，https://push.i-i.me/'),\n        required: true,\n      },\n      {\n        label: 'pushMeUrl',\n        tip: intl.get(\n          '自建的PushMeServer消息接口地址，例如：http://127.0.0.1:3010，不填则使用官方消息接口',\n        ),\n        required: false,\n      },\n    ],\n    chronocat: [\n      {\n        label: 'chronocatURL',\n        tip: intl.get(\n          'Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/',\n        ),\n        required: true,\n      },\n      {\n        label: 'chronocatQQ',\n        tip: intl.get(\n          '个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如：user_id=xxx;group_id=xxxx;group_id=xxxxx',\n        ),\n        required: true,\n      },\n      {\n        label: 'chronocatToken',\n        tip: intl.get(\n          'docker安装在持久化config目录下的chronocat.yml文件可找到',\n        ),\n        required: true,\n      },\n    ],\n    webhook: [\n      {\n        label: 'webhookMethod',\n        tip: intl.get('请求方法'),\n        required: true,\n        items: [{ value: 'GET' }, { value: 'POST' }, { value: 'PUT' }],\n      },\n      {\n        label: 'webhookContentType',\n        tip: intl.get('请求头Content-Type'),\n        required: true,\n        items: [\n          { value: 'text/plain' },\n          { value: 'application/json' },\n          { value: 'multipart/form-data' },\n          { value: 'application/x-www-form-urlencoded' },\n        ],\n      },\n      {\n        label: 'webhookUrl',\n        tip: intl.get(\n          '请求链接以http或者https开头。url或者body中必须包含$title，$content可选，对应api内容的位置',\n        ),\n        required: true,\n        placeholder: 'https://xxx.cn/api?content=$title\\n',\n      },\n      {\n        label: 'webhookHeaders',\n        tip: intl.get('请求头格式Custom-Header1: Header1，多个换行分割'),\n        placeholder: 'Custom-Header1: Header1\\nCustom-Header2: Header2',\n      },\n      {\n        label: 'webhookBody',\n        tip: intl.get(\n          '请求体格式key1: value1，多个换行分割。url或者body中必须包含$title，$content可选，对应api内容的位置',\n        ),\n        placeholder: 'key1: $title\\nkey2: $content',\n      },\n    ],\n  },\n  documentTitleMap: {\n    '/login': intl.get('登录'),\n    '/initialization': intl.get('初始化'),\n    '/crontab': intl.get('定时任务'),\n    '/env': intl.get('环境变量'),\n    '/subscription': intl.get('订阅管理'),\n    '/config': intl.get('配置文件'),\n    '/script': intl.get('脚本管理'),\n    '/diff': intl.get('对比工具'),\n    '/log': intl.get('日志管理'),\n    '/setting': intl.get('系统设置'),\n    '/error': intl.get('错误日志'),\n    '/dependence': intl.get('依赖管理'),\n  },\n  dependenceTypes: ['nodejs', 'python3', 'linux'],\n};\n"
  },
  {
    "path": "src/utils/const.ts",
    "content": "export const LOG_END_SYMBOL = '　　　　　';\n\nexport const LANG_MAP = {\n  '.py': 'python',\n  '.js': 'javascript',\n  '.mjs': 'javascript',\n  '.sh': 'shell',\n  '.ts': 'typescript',\n  '.ini': 'ini',\n  '.json': 'json',\n};\n\nexport const TIMEZONES = [\n  'UTC',\n  'Africa/Abidjan',\n  'Africa/Accra',\n  'Africa/Addis Ababa',\n  'Africa/Algiers',\n  'Africa/Asmara',\n  'Africa/Bamako',\n  'Africa/Bangui',\n  'Africa/Banjul',\n  'Africa/Bissau',\n  'Africa/Blantyre',\n  'Africa/Brazzaville',\n  'Africa/Bujumbura',\n  'Africa/Cairo',\n  'Africa/Casablanca',\n  'Africa/Ceuta',\n  'Africa/Conakry',\n  'Africa/Dakar',\n  'Africa/Dar es Salaam',\n  'Africa/Djibouti',\n  'Africa/Douala',\n  'Africa/El Aaiun',\n  'Africa/Freetown',\n  'Africa/Gaborone',\n  'Africa/Harare',\n  'Africa/Johannesburg',\n  'Africa/Juba',\n  'Africa/Kampala',\n  'Africa/Khartoum',\n  'Africa/Kigali',\n  'Africa/Kinshasa',\n  'Africa/Lagos',\n  'Africa/Libreville',\n  'Africa/Lome',\n  'Africa/Luanda',\n  'Africa/Lubumbashi',\n  'Africa/Lusaka',\n  'Africa/Malabo',\n  'Africa/Maputo',\n  'Africa/Maseru',\n  'Africa/Mbabane',\n  'Africa/Mogadishu',\n  'Africa/Monrovia',\n  'Africa/Nairobi',\n  'Africa/Ndjamena',\n  'Africa/Niamey',\n  'Africa/Nouakchott',\n  'Africa/Ouagadougou',\n  'Africa/Porto-Novo',\n  'Africa/Sao Tome',\n  'Africa/Tripoli',\n  'Africa/Tunis',\n  'Africa/Windhoek',\n  'America/Adak',\n  'America/Anchorage',\n  'America/Anguilla',\n  'America/Antigua',\n  'America/Araguaina',\n  'America/Argentina/Buenos Aires',\n  'America/Argentina/Catamarca',\n  'America/Argentina/Cordoba',\n  'America/Argentina/Jujuy',\n  'America/Argentina/La Rioja',\n  'America/Argentina/Mendoza',\n  'America/Argentina/Rio Gallegos',\n  'America/Argentina/Salta',\n  'America/Argentina/San Juan',\n  'America/Argentina/San Luis',\n  'America/Argentina/Tucuman',\n  'America/Argentina/Ushuaia',\n  'America/Aruba',\n  'America/Asuncion',\n  'America/Atikokan',\n  'America/Bahia',\n  'America/Bahia Banderas',\n  'America/Barbados',\n  'America/Belem',\n  'America/Belize',\n  'America/Blanc-Sablon',\n  'America/Boa Vista',\n  'America/Bogota',\n  'America/Boise',\n  'America/Cambridge Bay',\n  'America/Campo Grande',\n  'America/Cancun',\n  'America/Caracas',\n  'America/Cayenne',\n  'America/Cayman',\n  'America/Chicago',\n  'America/Chihuahua',\n  'America/Ciudad Juarez',\n  'America/Costa Rica',\n  'America/Creston',\n  'America/Cuiaba',\n  'America/Curacao',\n  'America/Danmarkshavn',\n  'America/Dawson',\n  'America/Dawson Creek',\n  'America/Denver',\n  'America/Detroit',\n  'America/Dominica',\n  'America/Edmonton',\n  'America/Eirunepe',\n  'America/El Salvador',\n  'America/Fort Nelson',\n  'America/Fortaleza',\n  'America/Glace Bay',\n  'America/Goose Bay',\n  'America/Grand Turk',\n  'America/Grenada',\n  'America/Guadeloupe',\n  'America/Guatemala',\n  'America/Guayaquil',\n  'America/Guyana',\n  'America/Halifax',\n  'America/Havana',\n  'America/Hermosillo',\n  'America/Indiana/Indianapolis',\n  'America/Indiana/Knox',\n  'America/Indiana/Marengo',\n  'America/Indiana/Petersburg',\n  'America/Indiana/Tell City',\n  'America/Indiana/Vevay',\n  'America/Indiana/Vincennes',\n  'America/Indiana/Winamac',\n  'America/Inuvik',\n  'America/Iqaluit',\n  'America/Jamaica',\n  'America/Juneau',\n  'America/Kentucky/Louisville',\n  'America/Kentucky/Monticello',\n  'America/Kralendijk',\n  'America/La Paz',\n  'America/Lima',\n  'America/Los Angeles',\n  'America/Lower Princes',\n  'America/Maceio',\n  'America/Managua',\n  'America/Manaus',\n  'America/Marigot',\n  'America/Martinique',\n  'America/Matamoros',\n  'America/Mazatlan',\n  'America/Menominee',\n  'America/Merida',\n  'America/Metlakatla',\n  'America/Mexico City',\n  'America/Miquelon',\n  'America/Moncton',\n  'America/Monterrey',\n  'America/Montevideo',\n  'America/Montserrat',\n  'America/Nassau',\n  'America/New York',\n  'America/Nome',\n  'America/Noronha',\n  'America/North Dakota/Beulah',\n  'America/North Dakota/Center',\n  'America/North Dakota/New Salem',\n  'America/Nuuk',\n  'America/Ojinaga',\n  'America/Panama',\n  'America/Paramaribo',\n  'America/Phoenix',\n  'America/Port of Spain',\n  'America/Port-au-Prince',\n  'America/Porto Velho',\n  'America/Puerto Rico',\n  'America/Punta Arenas',\n  'America/Rankin Inlet',\n  'America/Recife',\n  'America/Regina',\n  'America/Resolute',\n  'America/Rio Branco',\n  'America/Santarem',\n  'America/Santiago',\n  'America/Santo Domingo',\n  'America/Sao Paulo',\n  'America/Scoresbysund',\n  'America/Sitka',\n  'America/St Barthelemy',\n  'America/St Johns',\n  'America/St Kitts',\n  'America/St Lucia',\n  'America/St Thomas',\n  'America/St Vincent',\n  'America/Swift Current',\n  'America/Tegucigalpa',\n  'America/Thule',\n  'America/Tijuana',\n  'America/Toronto',\n  'America/Tortola',\n  'America/Vancouver',\n  'America/Whitehorse',\n  'America/Winnipeg',\n  'America/Yakutat',\n  'Antarctica/Casey',\n  'Antarctica/Davis',\n  'Antarctica/DumontDUrville',\n  'Antarctica/Macquarie',\n  'Antarctica/Mawson',\n  'Antarctica/McMurdo',\n  'Antarctica/Palmer',\n  'Antarctica/Rothera',\n  'Antarctica/Syowa',\n  'Antarctica/Troll',\n  'Antarctica/Vostok',\n  'Arctic/Longyearbyen',\n  'Asia/Aden',\n  'Asia/Almaty',\n  'Asia/Amman',\n  'Asia/Anadyr',\n  'Asia/Aqtau',\n  'Asia/Aqtobe',\n  'Asia/Ashgabat',\n  'Asia/Atyrau',\n  'Asia/Baghdad',\n  'Asia/Bahrain',\n  'Asia/Baku',\n  'Asia/Bangkok',\n  'Asia/Barnaul',\n  'Asia/Beirut',\n  'Asia/Bishkek',\n  'Asia/Brunei',\n  'Asia/Chita',\n  'Asia/Choibalsan',\n  'Asia/Colombo',\n  'Asia/Damascus',\n  'Asia/Dhaka',\n  'Asia/Dili',\n  'Asia/Dubai',\n  'Asia/Dushanbe',\n  'Asia/Famagusta',\n  'Asia/Gaza',\n  'Asia/Hebron',\n  'Asia/Ho Chi Minh',\n  'Asia/Hong Kong',\n  'Asia/Hovd',\n  'Asia/Irkutsk',\n  'Asia/Jakarta',\n  'Asia/Jayapura',\n  'Asia/Jerusalem',\n  'Asia/Kabul',\n  'Asia/Kamchatka',\n  'Asia/Karachi',\n  'Asia/Kathmandu',\n  'Asia/Khandyga',\n  'Asia/Kolkata',\n  'Asia/Krasnoyarsk',\n  'Asia/Kuala Lumpur',\n  'Asia/Kuching',\n  'Asia/Kuwait',\n  'Asia/Macau',\n  'Asia/Magadan',\n  'Asia/Makassar',\n  'Asia/Manila',\n  'Asia/Muscat',\n  'Asia/Nicosia',\n  'Asia/Novokuznetsk',\n  'Asia/Novosibirsk',\n  'Asia/Omsk',\n  'Asia/Oral',\n  'Asia/Phnom Penh',\n  'Asia/Pontianak',\n  'Asia/Pyongyang',\n  'Asia/Qatar',\n  'Asia/Qostanay',\n  'Asia/Qyzylorda',\n  'Asia/Riyadh',\n  'Asia/Sakhalin',\n  'Asia/Samarkand',\n  'Asia/Seoul',\n  'Asia/Shanghai',\n  'Asia/Singapore',\n  'Asia/Srednekolymsk',\n  'Asia/Taipei',\n  'Asia/Tashkent',\n  'Asia/Tbilisi',\n  'Asia/Tehran',\n  'Asia/Thimphu',\n  'Asia/Tokyo',\n  'Asia/Tomsk',\n  'Asia/Ulaanbaatar',\n  'Asia/Urumqi',\n  'Asia/Ust-Nera',\n  'Asia/Vientiane',\n  'Asia/Vladivostok',\n  'Asia/Yakutsk',\n  'Asia/Yangon',\n  'Asia/Yekaterinburg',\n  'Asia/Yerevan',\n  'Atlantic/Azores',\n  'Atlantic/Bermuda',\n  'Atlantic/Canary',\n  'Atlantic/Cape Verde',\n  'Atlantic/Faroe',\n  'Atlantic/Madeira',\n  'Atlantic/Reykjavik',\n  'Atlantic/South Georgia',\n  'Atlantic/St Helena',\n  'Atlantic/Stanley',\n  'Australia/Adelaide',\n  'Australia/Brisbane',\n  'Australia/Broken Hill',\n  'Australia/Darwin',\n  'Australia/Eucla',\n  'Australia/Hobart',\n  'Australia/Lindeman',\n  'Australia/Lord Howe',\n  'Australia/Melbourne',\n  'Australia/Perth',\n  'Australia/Sydney',\n  'Etc/GMT',\n  'Etc/GMT+1',\n  'Etc/GMT+10',\n  'Etc/GMT+11',\n  'Etc/GMT+12',\n  'Etc/GMT+2',\n  'Etc/GMT+3',\n  'Etc/GMT+4',\n  'Etc/GMT+5',\n  'Etc/GMT+6',\n  'Etc/GMT+7',\n  'Etc/GMT+8',\n  'Etc/GMT+9',\n  'Etc/GMT-1',\n  'Etc/GMT-10',\n  'Etc/GMT-11',\n  'Etc/GMT-12',\n  'Etc/GMT-13',\n  'Etc/GMT-14',\n  'Etc/GMT-2',\n  'Etc/GMT-3',\n  'Etc/GMT-4',\n  'Etc/GMT-5',\n  'Etc/GMT-6',\n  'Etc/GMT-7',\n  'Etc/GMT-8',\n  'Etc/GMT-9',\n  'Europe/Amsterdam',\n  'Europe/Andorra',\n  'Europe/Astrakhan',\n  'Europe/Athens',\n  'Europe/Belgrade',\n  'Europe/Berlin',\n  'Europe/Bratislava',\n  'Europe/Brussels',\n  'Europe/Bucharest',\n  'Europe/Budapest',\n  'Europe/Busingen',\n  'Europe/Chisinau',\n  'Europe/Copenhagen',\n  'Europe/Dublin',\n  'Europe/Gibraltar',\n  'Europe/Guernsey',\n  'Europe/Helsinki',\n  'Europe/Isle of Man',\n  'Europe/Istanbul',\n  'Europe/Jersey',\n  'Europe/Kaliningrad',\n  'Europe/Kirov',\n  'Europe/Kyiv',\n  'Europe/Lisbon',\n  'Europe/Ljubljana',\n  'Europe/London',\n  'Europe/Luxembourg',\n  'Europe/Madrid',\n  'Europe/Malta',\n  'Europe/Mariehamn',\n  'Europe/Minsk',\n  'Europe/Monaco',\n  'Europe/Moscow',\n  'Europe/Oslo',\n  'Europe/Paris',\n  'Europe/Podgorica',\n  'Europe/Prague',\n  'Europe/Riga',\n  'Europe/Rome',\n  'Europe/Samara',\n  'Europe/San Marino',\n  'Europe/Sarajevo',\n  'Europe/Saratov',\n  'Europe/Simferopol',\n  'Europe/Skopje',\n  'Europe/Sofia',\n  'Europe/Stockholm',\n  'Europe/Tallinn',\n  'Europe/Tirane',\n  'Europe/Ulyanovsk',\n  'Europe/Vaduz',\n  'Europe/Vatican',\n  'Europe/Vienna',\n  'Europe/Vilnius',\n  'Europe/Volgograd',\n  'Europe/Warsaw',\n  'Europe/Zagreb',\n  'Europe/Zurich',\n  'Indian/Antananarivo',\n  'Indian/Chagos',\n  'Indian/Christmas',\n  'Indian/Cocos',\n  'Indian/Comoro',\n  'Indian/Kerguelen',\n  'Indian/Mahe',\n  'Indian/Maldives',\n  'Indian/Mauritius',\n  'Indian/Mayotte',\n  'Indian/Reunion',\n  'Pacific/Apia',\n  'Pacific/Auckland',\n  'Pacific/Bougainville',\n  'Pacific/Chatham',\n  'Pacific/Chuuk',\n  'Pacific/Easter',\n  'Pacific/Efate',\n  'Pacific/Fakaofo',\n  'Pacific/Fiji',\n  'Pacific/Funafuti',\n  'Pacific/Galapagos',\n  'Pacific/Gambier',\n  'Pacific/Guadalcanal',\n  'Pacific/Guam',\n  'Pacific/Honolulu',\n  'Pacific/Kanton',\n  'Pacific/Kiritimati',\n  'Pacific/Kosrae',\n  'Pacific/Kwajalein',\n  'Pacific/Majuro',\n  'Pacific/Marquesas',\n  'Pacific/Midway',\n  'Pacific/Nauru',\n  'Pacific/Niue',\n  'Pacific/Norfolk',\n  'Pacific/Noumea',\n  'Pacific/Pago Pago',\n  'Pacific/Palau',\n  'Pacific/Pitcairn',\n  'Pacific/Pohnpei',\n  'Pacific/Port Moresby',\n  'Pacific/Rarotonga',\n  'Pacific/Saipan',\n  'Pacific/Tahiti',\n  'Pacific/Tarawa',\n  'Pacific/Tongatapu',\n  'Pacific/Wake',\n  'Pacific/Wallis',\n];\n"
  },
  {
    "path": "src/utils/date.ts",
    "content": "import Intl from 'react-intl-universal';\n\nexport function diffTime(num: number) {\n  const diff = num * 1000;\n\n  const days = Math.floor(diff / (24 * 3600 * 1000));\n\n  const leave1 = diff % (24 * 3600 * 1000);\n  const hours = Math.floor(leave1 / (3600 * 1000));\n\n  const leave2 = leave1 % (3600 * 1000);\n  const minutes = Math.floor(leave2 / (60 * 1000));\n\n  const leave3 = leave2 % (60 * 1000);\n  const seconds = Math.round(leave3 / 1000);\n\n  let returnStr = `${seconds} ${Intl.get('秒')}`;\n  if (minutes > 0) {\n    returnStr = `${minutes} ${Intl.get('分')} ` + returnStr;\n  }\n  if (hours > 0) {\n    returnStr = `${hours} ${Intl.get('时')} ` + returnStr;\n  }\n  if (days > 0) {\n    returnStr = `${days} ${Intl.get('天')} ` + returnStr;\n  }\n  return returnStr;\n}\n"
  },
  {
    "path": "src/utils/hooks.ts",
    "content": "import { useState, useEffect, useMemo } from 'react';\nimport browserType from './index';\n\nexport const useCtx = () => {\n  const [width, setWidth] = useState('100%');\n  const [marginLeft, setMarginLeft] = useState(0);\n  const [marginTop, setMarginTop] = useState(-48);\n  const [isPhone, setIsPhone] = useState(false);\n  const { platform } = useMemo(() => browserType(), []);\n\n  useEffect(() => {\n    if (platform === 'mobile' && document.body.offsetWidth < 768) {\n      setWidth('auto');\n      setMarginLeft(0);\n      setMarginTop(0);\n      setIsPhone(true);\n      document.body.setAttribute('data-mode', 'phone');\n    } else {\n      setWidth('100%');\n      setMarginLeft(0);\n      setMarginTop(-48);\n      setIsPhone(false);\n      document.body.setAttribute('data-mode', 'desktop');\n    }\n  }, []);\n\n  return {\n    headerStyle: {\n      padding: '4px 16px 4px 15px',\n      position: 'sticky',\n      top: 0,\n      left: 0,\n      zIndex: 20,\n      marginTop,\n      width,\n      marginLeft,\n    } as any,\n    isPhone,\n  };\n};\n\nexport const useTheme = () => {\n  const [theme, setTheme] = useState<'vs' | 'vs-dark'>();\n\n  const reloadTheme = () => {\n    const media = window.matchMedia('(prefers-color-scheme: dark)');\n    const storageTheme = localStorage.getItem('qinglong_dark_theme');\n    const isDark =\n      (media.matches && storageTheme !== 'light') || storageTheme === 'dark';\n    setTheme(isDark ? 'vs-dark' : 'vs');\n  };\n\n  useEffect(() => {\n    const media = window.matchMedia('(prefers-color-scheme: dark)');\n    const storageTheme = localStorage.getItem('qinglong_dark_theme');\n    const isDark =\n      (media.matches && storageTheme !== 'light') || storageTheme === 'dark';\n    setTheme(isDark ? 'vs-dark' : 'vs');\n\n    const cb = (e: any) => {\n      if (storageTheme === 'auto' || !storageTheme) {\n        if (e.matches) {\n          setTheme('vs-dark');\n        } else {\n          setTheme('vs');\n        }\n      }\n    };\n    if (typeof media.addEventListener === 'function') {\n      media.addEventListener('change', cb);\n    } else if (typeof media.addListener === 'function') {\n      media.addListener(cb);\n    }\n  }, []);\n\n  return { theme, reloadTheme };\n};\n"
  },
  {
    "path": "src/utils/http.tsx",
    "content": "import intl from 'react-intl-universal';\nimport { message, notification } from 'antd';\nimport config from './config';\nimport { history } from '@umijs/max';\nimport axios, {\n  AxiosError,\n  AxiosInstance,\n  AxiosRequestConfig,\n  AxiosResponse,\n  InternalAxiosRequestConfig,\n} from 'axios';\n\nexport interface IResponseData {\n  code?: number;\n  data?: any;\n  message?: string;\n  error?: any;\n}\n\nexport type Override<\n  T,\n  K extends Partial<{ [P in keyof T]: any }> | string,\n> = K extends string\n  ? Omit<T, K> & { [P in keyof T]: T[P] | unknown }\n  : Omit<T, keyof K> & K;\n\nexport interface ICustomConfig {\n  onError?: (res: AxiosResponse<unknown, any>) => void;\n}\n\nmessage.config({\n  duration: 2,\n});\n\nconst time = Date.now();\nconst errorHandler = function (\n  error: Override<\n    AxiosError<IResponseData>,\n    { config: InternalAxiosRequestConfig & ICustomConfig }\n  >,\n) {\n  if (error.response) {\n    const msg = error.response.data\n      ? error.response.data.message || error.message\n      : error.response.statusText;\n    const responseStatus = error.response.status;\n    if ([502, 504].includes(responseStatus)) {\n      history.push('/error');\n    } else if (responseStatus === 401) {\n      if (history.location.pathname !== '/login') {\n        message.error(intl.get('登录已过期，请重新登录'));\n        localStorage.removeItem(config.authKey);\n        history.push('/login');\n      }\n    } else {\n      if (typeof error.config?.onError === 'function') {\n        return error.config?.onError(error.response);\n      }\n\n      msg &&\n        notification.error({\n          message: msg,\n          description: error.response?.data?.errors ? (\n            <>\n              {error.response?.data?.errors?.map((item: any) => (\n                <div>\n                  {item.message} ({item.value})\n                </div>\n              ))}\n            </>\n          ) : undefined,\n        });\n    }\n  } else {\n    console.log(error.message);\n  }\n\n  return Promise.reject(error);\n};\n\nlet _request = axios.create({\n  timeout: 60000,\n  params: { t: time },\n});\n\nconst apiWhiteList = [\n  `${config.baseUrl}api/user/login`,\n  `${config.baseUrl}open/auth/token`,\n  `${config.baseUrl}api/user/two-factor/login`,\n  `${config.baseUrl}api/system`,\n  `${config.baseUrl}api/user/init`,\n  `${config.baseUrl}api/user/notification/init`,\n];\n\n_request.interceptors.request.use((_config) => {\n  const token = localStorage.getItem(config.authKey);\n  if (token && !apiWhiteList.includes(_config.url!)) {\n    _config.headers.Authorization = `Bearer ${token}`;\n    return _config;\n  }\n  return _config;\n});\n\n_request.interceptors.response.use(async (response) => {\n  const responseStatus = response.status;\n  if ([502, 504].includes(responseStatus)) {\n    history.push('/error');\n  } else if (responseStatus === 401) {\n    if (history.location.pathname !== '/login') {\n      localStorage.removeItem(config.authKey);\n      history.push('/login');\n    }\n  } else {\n    try {\n      const res = response.data;\n      if (res.code !== 200) {\n        const msg = res.message || res.data;\n        msg &&\n          notification.error({\n            message: msg,\n            description: res?.errors ? (\n              <>\n                {res?.errors.map((item: any) => (\n                  <div>{item.message}</div>\n                ))}\n              </>\n            ) : undefined,\n          });\n      }\n      return res;\n    } catch (error) {}\n    return response;\n  }\n  return response;\n}, errorHandler);\n\nexport const request = _request as Override<\n  AxiosInstance,\n  {\n    get<T = IResponseData, D = any>(\n      url: string,\n      config?: AxiosRequestConfig<D> & ICustomConfig,\n    ): Promise<T>;\n    delete<T = IResponseData, D = any>(\n      url: string,\n      config?: AxiosRequestConfig<D> & ICustomConfig,\n    ): Promise<T>;\n    post<T = IResponseData, D = any>(\n      url: string,\n      data?: D,\n      config?: AxiosRequestConfig<D> & ICustomConfig,\n    ): Promise<T>;\n    put<T = IResponseData, D = any>(\n      url: string,\n      data?: D,\n      config?: AxiosRequestConfig<D> & ICustomConfig,\n    ): Promise<T>;\n  }\n>;\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "import intl from 'react-intl-universal';\nimport { LANG_MAP, LOG_END_SYMBOL } from './const';\nimport CronExpressionParser from 'cron-parser';\nimport { ICrontab } from '@/pages/crontab/type';\n\nexport default function browserType() {\n  // 权重：系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本\n  const ua = navigator.userAgent.toLowerCase();\n  const testUa = (regexp: RegExp) => regexp.test(ua);\n  const testVs = (regexp: RegExp) =>\n    (ua.match(regexp) || [])\n      .toString()\n      .replace(/[^0-9|_.]/g, '')\n      .replace(/_/g, '.');\n\n  // 系统\n  let system = 'unknow';\n  if (testUa(/windows|win32|win64|wow32|wow64/g)) {\n    system = 'windows'; // windows系统\n  } else if (testUa(/macintosh|macintel/g)) {\n    system = 'macos'; // macos系统\n  } else if (testUa(/x11/g)) {\n    system = 'linux'; // linux系统\n  } else if (testUa(/android|adr/g)) {\n    system = 'android'; // android系统\n  } else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {\n    system = 'ios'; // ios系统\n  }\n\n  // 系统版本\n  let systemVs = 'unknow';\n  if (system === 'windows') {\n    if (testUa(/windows nt 5.0|windows 2000/g)) {\n      systemVs = '2000';\n    } else if (testUa(/windows nt 5.1|windows xp/g)) {\n      systemVs = 'xp';\n    } else if (testUa(/windows nt 5.2|windows 2003/g)) {\n      systemVs = '2003';\n    } else if (testUa(/windows nt 6.0|windows vista/g)) {\n      systemVs = 'vista';\n    } else if (testUa(/windows nt 6.1|windows 7/g)) {\n      systemVs = '7';\n    } else if (testUa(/windows nt 6.2|windows 8/g)) {\n      systemVs = '8';\n    } else if (testUa(/windows nt 6.3|windows 8.1/g)) {\n      systemVs = '8.1';\n    } else if (testUa(/windows nt 10.0|windows 10/g)) {\n      systemVs = '10';\n    }\n  } else if (system === 'macos') {\n    systemVs = testVs(/os x [\\d._]+/g);\n  } else if (system === 'android') {\n    systemVs = testVs(/android [\\d._]+/g);\n  } else if (system === 'ios') {\n    systemVs = testVs(/os [\\d._]+/g);\n  }\n\n  // 平台\n  let platform = 'unknow';\n  if (system === 'windows' || system === 'macos' || system === 'linux') {\n    platform = 'desktop'; // 桌面端\n  } else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {\n    platform = 'mobile'; // 移动端\n  }\n\n  // 内核和载体\n  let engine = 'unknow';\n  let supporter = 'unknow';\n  if (testUa(/applewebkit/g)) {\n    engine = 'webkit'; // webkit内核\n    if (testUa(/edge/g)) {\n      supporter = 'edge'; // edge浏览器\n    } else if (testUa(/opr/g)) {\n      supporter = 'opera'; // opera浏览器\n    } else if (testUa(/chrome/g)) {\n      supporter = 'chrome'; // chrome浏览器\n    } else if (testUa(/safari/g)) {\n      supporter = 'safari'; // safari浏览器\n    }\n  } else if (testUa(/gecko/g) && testUa(/firefox/g)) {\n    engine = 'gecko'; // gecko内核\n    supporter = 'firefox'; // firefox浏览器\n  } else if (testUa(/presto/g)) {\n    engine = 'presto'; // presto内核\n    supporter = 'opera'; // opera浏览器\n  } else if (testUa(/trident|compatible|msie/g)) {\n    engine = 'trident'; // trident内核\n    supporter = 'iexplore'; // iexplore浏览器\n  }\n\n  // 内核版本\n  let engineVs = 'unknow';\n  if (engine === 'webkit') {\n    engineVs = testVs(/applewebkit\\/[\\d._]+/g);\n  } else if (engine === 'gecko') {\n    engineVs = testVs(/gecko\\/[\\d._]+/g);\n  } else if (engine === 'presto') {\n    engineVs = testVs(/presto\\/[\\d._]+/g);\n  } else if (engine === 'trident') {\n    engineVs = testVs(/trident\\/[\\d._]+/g);\n  }\n\n  // 载体版本\n  let supporterVs = 'unknow';\n  if (supporter === 'chrome') {\n    supporterVs = testVs(/chrome\\/[\\d._]+/g);\n  } else if (supporter === 'safari') {\n    supporterVs = testVs(/version\\/[\\d._]+/g);\n  } else if (supporter === 'firefox') {\n    supporterVs = testVs(/firefox\\/[\\d._]+/g);\n  } else if (supporter === 'opera') {\n    supporterVs = testVs(/opr\\/[\\d._]+/g);\n  } else if (supporter === 'iexplore') {\n    supporterVs = testVs(/(msie [\\d._]+)|(rv:[\\d._]+)/g);\n  } else if (supporter === 'edge') {\n    supporterVs = testVs(/edge\\/[\\d._]+/g);\n  }\n\n  // 外壳和外壳版本\n  let shell = 'none';\n  let shellVs = 'unknow';\n  if (testUa(/micromessenger/g)) {\n    shell = 'wechat'; // 微信浏览器\n    shellVs = testVs(/micromessenger\\/[\\d._]+/g);\n  } else if (testUa(/qqbrowser/g)) {\n    shell = 'qq'; // QQ浏览器\n    shellVs = testVs(/qqbrowser\\/[\\d._]+/g);\n  } else if (testUa(/ucbrowser/g)) {\n    shell = 'uc'; // UC浏览器\n    shellVs = testVs(/ucbrowser\\/[\\d._]+/g);\n  } else if (testUa(/qihu 360se/g)) {\n    shell = '360'; // 360浏览器(无版本)\n  } else if (testUa(/2345explorer/g)) {\n    shell = '2345'; // 2345浏览器\n    shellVs = testVs(/2345explorer\\/[\\d._]+/g);\n  } else if (testUa(/metasr/g)) {\n    shell = 'sougou'; // 搜狗浏览器(无版本)\n  } else if (testUa(/lbbrowser/g)) {\n    shell = 'liebao'; // 猎豹浏览器(无版本)\n  } else if (testUa(/maxthon/g)) {\n    shell = 'maxthon'; // 遨游浏览器\n    shellVs = testVs(/maxthon\\/[\\d._]+/g);\n  }\n\n  const result = Object.assign(\n    {\n      engine, // webkit gecko presto trident\n      engineVs,\n      platform, // desktop mobile\n      supporter, // chrome safari firefox opera iexplore edge\n      supporterVs,\n      system, // windows macos linux android ios\n      systemVs,\n    },\n    shell === 'none'\n      ? {}\n      : {\n        shell, // wechat qq uc 360 2345 sougou liebao maxthon\n        shellVs,\n      },\n  );\n\n  console.log(\n    \"%c\\n .d88b.  d888888b d8b   db  d888b  db       .d88b.  d8b   db  d888b  \\n.8P  Y8.   `88'   888o  88 88' Y8b 88      .8P  Y8. 888o  88 88' Y8b \\n88    88    88    88V8o 88 88      88      88    88 88V8o 88 88      \\n88    88    88    88 V8o88 88  ooo 88      88    88 88 V8o88 88  ooo \\n`8P  d8'   .88.   88  V888 88. ~8~ 88booo. `8b  d8' 88  V888 88. ~8~ \\n `Y88'Y8 Y888888P VP   V8P  Y888P  Y88888P  `Y88P'  VP   V8P  Y888P  \\n                                                                     \\n                                                                     \\n\",\n    'color: blue;font-size: 14px;',\n  );\n  console.log(\n    '%c忘形雨笠烟蓑，知心牧唱樵歌。明月清风共我，闲人三个，从他今古消磨。\\n',\n    'color: yellow;font-size: 18px;',\n  );\n  console.log(\n    `%c青龙运行环境:\\n\\n系统：${result.system}/${result.systemVs}\\n浏览器：${result.supporter}/${result.supporterVs}\\n内核：${result.engine}/${result.engineVs}`,\n    'color: green;font-size: 14px;font-weight: bold;',\n  );\n\n  return result;\n}\n\n/**\n * 获取第一个表格的可视化高度\n * @param {*} extraHeight 额外的高度(表格底部的内容高度 Number类型,默认为74)\n * @param {*} id 当前页面中有多个table时需要制定table的id\n */\nexport function getTableScroll({\n  extraHeight,\n  target,\n}: { extraHeight?: number; target?: HTMLElement } = {}) {\n  if (typeof extraHeight === 'undefined') {\n    //  47 + 40 + 12\n    extraHeight = 99;\n  }\n  let tHeader = null;\n  if (target) {\n    tHeader = target;\n  } else {\n    tHeader = document.querySelector('.ant-table-wrapper');\n  }\n\n  //表格内容距离顶部的距离\n  let mainTop = 0;\n  if (tHeader) {\n    mainTop = tHeader.getBoundingClientRect().top;\n  }\n\n  //窗体高度-表格内容顶部的高度-表格内容底部的高度\n  let height = document.body.clientHeight - mainTop - extraHeight;\n  return height;\n}\n\n// 自动触发点击事件\nfunction automaticClick(elment: HTMLElement) {\n  const ev = document.createEvent('MouseEvents');\n  ev.initMouseEvent(\n    'click',\n    true,\n    false,\n    window,\n    0,\n    0,\n    0,\n    0,\n    0,\n    false,\n    false,\n    false,\n    false,\n    0,\n    null,\n  );\n  elment.dispatchEvent(ev);\n}\n\n// 导出文件\nexport function exportJson(name: string, data: string) {\n  const urlObject = window.URL || window.webkitURL || window;\n  const export_blob = new Blob([data]);\n  const createA = document.createElementNS(\n    'http://www.w3.org/1999/xhtml',\n    'a',\n  ) as any;\n  createA.href = urlObject.createObjectURL(export_blob);\n  createA.download = name;\n  automaticClick(createA);\n}\n\nexport function depthFirstSearch<\n  T extends Record<string, any> & { children?: T[] },\n>(children: T[], condition: (column: T) => boolean, item?: T) {\n  const c = [...children];\n  const keys = [];\n\n  (function find(cls: T[] | undefined) {\n    if (!cls) return;\n    for (let i = 0; i < cls?.length; i++) {\n      if (condition(cls[i])) {\n        if (!item) {\n          cls.splice(i, 1);\n          return;\n        }\n\n        if (cls[i].children) {\n          cls[i].children!.unshift(item);\n        } else {\n          cls[i].children = [item];\n        }\n        return;\n      }\n      if (cls[i].children) {\n        keys.push(cls[i].key);\n        find(cls[i].children);\n      }\n    }\n  })(c);\n\n  return c;\n}\n\nexport function findNode<T extends Record<string, any> & { children?: T[] }>(\n  children: T[],\n  condition: (column: T) => boolean,\n) {\n  const c = [...children];\n\n  let item;\n  function find(cls: T[] | undefined): T | undefined {\n    if (!cls) return;\n    for (let i = 0; i < cls?.length; i++) {\n      if (condition(cls[i])) {\n        item = cls[i];\n      } else if (cls[i].children) {\n        find(cls[i].children);\n      }\n    }\n  }\n\n  find(c);\n\n  return item as T | undefined;\n}\n\nexport function logEnded(log: string): boolean {\n  const endTips = [LOG_END_SYMBOL, intl.get('执行结束')];\n  return endTips.some((x) => log.includes(x));\n}\n\nexport function getCommandScript(\n  command: string,\n): [string, string] | string | undefined {\n  const cmd = command.split(' ') as string[];\n  if (cmd[1] === 'repo' || cmd[1] === 'raw') {\n    return cmd[2];\n  }\n  let scriptsPart = cmd.find((x) =>\n    ['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)),\n  );\n  if (!scriptsPart) return;\n  const scriptDir = `${window.__ENV__QL_DIR}/data/scripts`;\n  if (scriptsPart.startsWith(scriptDir)) {\n    scriptsPart = scriptsPart.replace(scriptDir, '');\n  }\n\n  let p: string, s: string;\n  let index = scriptsPart.lastIndexOf('/');\n  if (index >= 0) {\n    s = scriptsPart.slice(index + 1);\n    p = scriptsPart.slice(0, index);\n  } else {\n    s = scriptsPart;\n    p = '';\n  }\n  return [s, p];\n}\n\nexport function parseCrontab(schedule: string): Date | null {\n  try {\n    const time = CronExpressionParser.parse(schedule);\n    if (time) {\n      return time.next().toDate();\n    }\n  } catch (error) { }\n\n  return null;\n}\n\nexport function getCrontabsNextDate(\n  schedule: string,\n  extra_schedules: ICrontab['extra_schedules'],\n): Date | null {\n  let date = parseCrontab(schedule);\n  if (extra_schedules?.length) {\n    extra_schedules.forEach((x) => {\n      const _date = parseCrontab(x.schedule);\n      if (_date && (!date || _date < date)) {\n        date = _date;\n      }\n    });\n  }\n  return date;\n}\n\nexport function getExtension(filename: string) {\n  if (!filename) return '';\n  const arr = filename.split('.');\n  return `.${arr[arr.length - 1]}`;\n}\n\nexport function getEditorMode(filename: string) {\n  const extension = getExtension(filename) as keyof typeof LANG_MAP;\n  return LANG_MAP[extension];\n}\n\nexport function disableBody() {\n  const overlay = document.createElement('div');\n  overlay.style.position = 'fixed';\n  overlay.style.top = '0px';\n  overlay.style.left = '0px';\n  overlay.style.width = '100%';\n  overlay.style.height = '100%';\n  overlay.style.backgroundColor = 'transparent';\n  overlay.style.zIndex = '9999';\n  document.body.appendChild(overlay);\n\n  overlay.addEventListener('click', function (event) {\n    event.stopPropagation();\n    event.preventDefault();\n  });\n\n  document.body.style.overflow = 'hidden';\n}\n"
  },
  {
    "path": "src/utils/init.ts",
    "content": "import { loader } from '@monaco-editor/react';\nimport config from './config';\n\nexport function init(version: string) {\n  // monaco 编辑器配置cdn和locale\n  loader.config({\n    paths: {\n      vs: `${config.baseUrl}monaco-editor/min/vs`,\n    },\n    'vs/nls': {\n      availableLanguages: {\n        '*': 'zh-cn',\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "src/utils/monaco/index.ts",
    "content": "import * as monaco from 'monaco-editor';\n\ninterface FileTypeConfig {\n  extensions?: string[];      // 文件扩展名\n  filenames?: string[];      // 完整文件名\n  patterns?: RegExp[];       // 文件名正则匹配\n  startsWith?: string[];     // 文件名前缀匹配\n  endsWith?: string[];       // 文件名后缀匹配\n}\n\n// 文件类型分类配置（只包含特殊文件类型）\nconst fileTypeConfigs: Record<string, FileTypeConfig> = {\n  // 前端特殊文件\n  frontend: {\n    extensions: [\n      '.json5',    // JSON5\n      '.vue',      // Vue\n      '.svelte',   // Svelte\n      '.astro',    // Astro\n      '.wxss',     // 微信小程序样式\n      '.pcss',     // PostCSS\n      '.acss',     // 支付宝小程序样式\n    ],\n    patterns: [\n      /\\.env\\.(local|development|production|test)$/,\n      /\\.module\\.(css|less|scss|sass)$/,\n      /\\.d\\.ts$/,\n      /\\.config\\.(js|ts|json)$/,\n    ],\n  },\n\n  // 小程序相关\n  miniprogram: {\n    extensions: [\n      '.wxml',     // 微信小程序\n      '.wxs',      // 微信小程序\n      '.axml',     // 支付宝小程序\n      '.sjs',      // 支付宝小程序\n      '.swan',     // 百度小程序\n      '.ttml',     // 字节跳动小程序\n      '.ttss',     // 字节跳动小程序\n      '.wxl',      // 微信小程序语言包\n      '.qml',      // QQ小程序\n      '.qss',      // QQ小程序\n      '.ksml',     // 快手小程序\n      '.kss',      // 快手小程序\n    ],\n  },\n\n  // 开发工具相关\n  devtools: {\n    extensions: [\n      '.prisma',   // Prisma\n      '.mdx',      // MDX\n      '.swagger',  // Swagger\n      '.openapi',  // OpenAPI\n    ],\n  },\n\n  // 锁文件\n  lock: {\n    filenames: [\n      'yarn.lock',\n      'pnpm-lock.yaml',\n      'package-lock.json',\n      'composer.lock',\n      'Gemfile.lock',\n      'poetry.lock',\n      'Cargo.lock',\n    ],\n  },\n\n  // 无后缀配置文件\n  noExtension: {\n    filenames: [\n      '.dockerignore',\n      '.gitignore',\n      '.npmignore',\n      '.browserslistrc',\n      '.czrc',\n      '.huskyrc',\n      '.lintstagedrc',\n      '.nvmrc',\n      '.gcloudignore',\n      '.htaccess',\n    ],\n    patterns: [\n      /^\\.env\\./,\n    ],\n  },\n\n  // CI/CD 配置\n  cicd: {\n    patterns: [\n      /^\\.github\\/workflows\\/.*\\.yml$/,\n      /^\\.gitlab\\/.*\\.yml$/,\n      /^\\.circleci\\/.*\\.yml$/,\n    ],\n  },\n};\n\n/**\n * 检查文件是否可以在 Monaco 编辑器中预览\n * @param fileName 文件名\n * @returns boolean\n */\nexport function canPreviewInMonaco(fileName: string): boolean {\n  if (!fileName) return false;\n  \n  // 获取 Monaco 支持的语言\n  const supportedLanguages = monaco.languages.getLanguages();\n  const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();\n  const lowercaseFileName = fileName.toLowerCase();\n\n  // 检查 Monaco 原生支持\n  if (supportedLanguages.some((lang) => \n    lang.extensions?.includes(ext) || \n    (lang.filenames?.includes(lowercaseFileName))\n  )) {\n    return true;\n  }\n\n  // 检查额外支持的文件类型\n  return Object.values(fileTypeConfigs).some(config => {\n    return (\n      (config.extensions?.includes(ext)) ||\n      (config.filenames?.includes(lowercaseFileName)) ||\n      (config.patterns?.some(pattern => pattern.test(lowercaseFileName))) ||\n      (config.startsWith?.some(prefix => lowercaseFileName.startsWith(prefix))) ||\n      (config.endsWith?.some(suffix => lowercaseFileName.endsWith(suffix)))\n    );\n  });\n}\n\n/**\n * 获取文件类型分类\n * @param fileName 文件名\n * @returns string 文件类型分类名称\n */\nexport function getFileCategory(fileName: string): string {\n  if (!fileName) return 'unknown';\n  \n  const lowercaseFileName = fileName.toLowerCase();\n  const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();\n\n  for (const [category, config] of Object.entries(fileTypeConfigs)) {\n    if (\n      (config.extensions?.includes(ext)) ||\n      (config.filenames?.includes(lowercaseFileName)) ||\n      (config.patterns?.some(pattern => pattern.test(lowercaseFileName))) ||\n      (config.startsWith?.some(prefix => lowercaseFileName.startsWith(prefix))) ||\n      (config.endsWith?.some(suffix => lowercaseFileName.endsWith(suffix)))\n    ) {\n      return category;\n    }\n  }\n\n  // 检查 Monaco 原生支持\n  const supportedLanguages = monaco.languages.getLanguages();\n  if (supportedLanguages.some((lang) => \n    lang.extensions?.includes(ext) || \n    (lang.filenames?.includes(lowercaseFileName))\n  )) {\n    return 'monaco-native';\n  }\n\n  return 'unknown';\n}\n"
  },
  {
    "path": "src/utils/type.ts",
    "content": "export type SockMessageType =\n  | 'ping'\n  | 'installDependence'\n  | 'uninstallDependence'\n  | 'updateSystemVersion'\n  | 'manuallyRunScript'\n  | 'runSubscriptionEnd'\n  | 'reloadSystem'\n  | 'updateNodeMirror'\n  | 'updateLinuxMirror';\n"
  },
  {
    "path": "src/utils/websocket.ts",
    "content": "import SockJS from 'sockjs-client';\nimport { SockMessageType } from './type';\n\nclass WebSocketManager {\n  private static instance: WebSocketManager | null = null;\n  private url: string;\n  private socket: WebSocket | null = null;\n  private subscriptions: Map<SockMessageType, Set<(p: any) => void>> = new Map();\n  private options: {\n    maxReconnectAttempts: number;\n    reconnectInterval: number;\n    heartbeatInterval: number;\n  };\n  private reconnectAttempts: number = 0;\n  private heartbeatTimeout: NodeJS.Timeout | null = null;\n  private state: 'closed' | 'connecting' | 'open' = 'closed';\n\n  constructor(url: string, options: Partial<typeof WebSocketManager.prototype.options> = {}) {\n    this.url = url;\n    this.options = {\n      maxReconnectAttempts: options.maxReconnectAttempts || 5,\n      reconnectInterval: options.reconnectInterval || 3000,\n      heartbeatInterval: options.heartbeatInterval || 30000,\n    };\n\n    this.init();\n  }\n\n  public static getInstance(url: string = '', options?: Partial<typeof WebSocketManager.prototype.options>): WebSocketManager {\n    if (!WebSocketManager.instance) {\n      WebSocketManager.instance = new WebSocketManager(url, options);\n    }\n    return WebSocketManager.instance;\n  }\n\n  private async init() {\n    try {\n      this.state = 'connecting';\n      this.emit('connecting');\n\n      while (this.reconnectAttempts < this.options.maxReconnectAttempts) {\n        this.socket = new SockJS(this.url);\n        this.setupEventListeners();\n        this.startHeartbeat();\n        await this.waitForClose();\n        this.stopHeartbeat();\n        this.socket = null;\n        this.reconnectAttempts++;\n\n        await new Promise((resolve) => setTimeout(resolve, this.options.reconnectInterval));\n      }\n    } catch (error) {\n      this.handleError(error);\n    }\n  }\n\n  private setupEventListeners() {\n    if (!this.socket) return;\n\n    this.socket.onopen = () => {\n      this.state = 'open';\n      this.emit('open');\n    };\n\n    this.socket.onmessage = (event) => {\n      const message = JSON.parse(event.data);\n      this.dispatchMessage(message);\n    };\n\n    this.socket.onclose = () => {\n      this.state = 'closed';\n      this.emit('close');\n    };\n  }\n\n  private async waitForClose() {\n    while (this.socket?.readyState !== SockJS.CLOSED) {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n    }\n  }\n\n  public subscribe(topic: SockMessageType, callback: (v: any) => void) {\n    const topicSubscriptions = this.subscriptions.get(topic) || new Set();\n\n    if (!topicSubscriptions.has(callback)) {\n      topicSubscriptions.add(callback);\n      this.subscriptions.set(topic, topicSubscriptions);\n\n      const subscriptionMessage = { action: 'subscribe', topic };\n      this.send(subscriptionMessage);\n    }\n  }\n\n  public unsubscribe(topic: SockMessageType, callback: (v: any) => void) {\n    const topicSubscriptions = this.subscriptions.get(topic) || new Set();\n    if (topicSubscriptions.has(callback)) {\n      topicSubscriptions.delete(callback);\n\n      const unsubscribeMessage = { action: 'unsubscribe', topic };\n      this.send(unsubscribeMessage);\n    }\n  }\n\n  public send(message: any) {\n    if (this.socket?.readyState === SockJS.OPEN) {\n      this.socket.send(JSON.stringify(message));\n    }\n  }\n\n  private dispatchMessage(message: any) {\n    const { type, ...others } = message;\n    const topicSubscriptions = this.subscriptions.get(type) || new Set();\n\n    [...topicSubscriptions].forEach((callback) => callback(others));\n  }\n\n  private startHeartbeat() {\n    this.heartbeatTimeout = setInterval(() => {\n      if (this.socket?.readyState === SockJS.OPEN) {\n        this.socket.send(JSON.stringify({ type: 'heartbeat' }));\n      }\n    }, this.options.heartbeatInterval);\n  }\n\n  private stopHeartbeat() {\n    if (this.heartbeatTimeout) {\n      clearInterval(this.heartbeatTimeout);\n    }\n  }\n\n  public close() {\n    if (this.socket) {\n      this.state = 'closed';\n      this.stopHeartbeat();\n      this.socket.close();\n      this.emit('close');\n    }\n  }\n\n  private handleError(error: any) {\n    console.error('WebSocket错误:', error);\n    this.emit('error', error);\n  }\n\n  public on(event: string, listener: Function) {\n    // this.addListener(event, listener);\n  }\n\n  public emit(event: string, data?: any) {\n    // this.listeners(event).forEach((listener) => listener(data));\n  }\n}\n\nexport default WebSocketManager;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"importHelpers\": true,\n    \"jsx\": \"react-jsx\",\n    \"esModuleInterop\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \"./\",\n    \"strict\": true,\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@@/*\": [\"src/.umi/*\"]\n    },\n    \"lib\": [\"dom\", \"es2021\", \"esnext.asynciterable\"],\n    \"allowSyntheticDefaultImports\": true,\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"pretty\": true,\n    \"allowJs\": true,\n    \"noEmit\": false\n  },\n  \"include\": [\"src/**/*\", \".umirc.ts\", \"typings.d.ts\"],\n  \"exclude\": [\"node_modules\", \"static\", \"data\"]\n}\n"
  },
  {
    "path": "typings.d.ts",
    "content": "declare module '*.css';\ndeclare module '*.less';\ndeclare module '*.png';\ndeclare module '*.svg' {\n  export function ReactComponent(\n    props: React.SVGProps<SVGSVGElement>,\n  ): React.ReactElement;\n  const url: string;\n  export default url;\n}\n\ninterface Window {\n  __ENV__QlBaseUrl: string;\n  __ENV__DeployEnv: string;\n  __ENV__QL_DIR: string;\n}\n"
  },
  {
    "path": "version.yaml",
    "content": "version: 2.20.2\nchangeLogLink: https://t.me/jiao_long/434\npublishTime: 2026-03-01 1800\nchangeLog: |\n  1. 修复 path 安全漏洞（重要）\n  "
  }
]