[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 错误反馈\ndescription: \"提交错误反馈\"\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: checkboxes\n    id: ensure\n    attributes:\n      label: 验证步骤\n      description: 在提交之前，请勾选以下选项以证明您已经阅读并理解了以下要求，否则该 issue 将被关闭。\n      options:\n        - label: 我已在 [Issue](https://github.com/bestruirui/BestSub/issues) 中寻找过我要提出的问题，并且没有找到\n          required: true\n        \n  - type: textarea\n    attributes:\n      label: 描述\n      description: 请提供错误的详细描述。\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: 重现方式\n      description: 请提供重现错误的步骤\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: 日志\n      description: 在下方附上运行日志\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 功能请求\ndescription: 为该项目提出建议\ntitle: \"[Feature] \"\nlabels: [\"enhancement\"]\nbody:\n  - type: checkboxes\n    id: ensure\n    attributes:\n      label: 验证步骤\n      description: 在提交之前，请勾选以下选项以证明您已经阅读并理解了以下要求，否则该 issue 将被关闭。\n      options:\n        - label: 我已经阅读了 [README.md](https://github.com/bestruirui/BestSub/blob/master/README.md)，确认了该功能没有实现\n          required: true\n        - label: 我已在 [Issue](https://github.com/bestruirui/BestSub/issues) 中寻找过我要提出的功能请求，并且没有找到\n          required: true\n  - type: textarea\n    attributes:\n      label: 描述\n      description: 请提供对于该功能的详细描述，而不是莫名其妙的话术。\n    validations:\n      required: true"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: changelog\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  contents: write\n\njobs:\n  changelog:\n    name: Create Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n          token: ${{ secrets.ACTION_TOKEN }}\n\n      - run: npx changelogithub\n        env:\n          GITHUB_TOKEN: ${{secrets.ACTION_TOKEN}}\n\n      - name: Merge dev to master branch\n        run: |\n          git config --global user.name 'GitHub Actions'\n          git config --global user.email 'github-actions@github.com'\n          if ! git show-ref --verify --quiet refs/heads/master; then\n            git checkout -b master\n          else\n            git checkout master\n          fi\n          git fetch origin dev\n          git merge origin/dev\n          git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}\n          git push origin master --force\n        env:\n          GITHUB_TOKEN: ${{secrets.ACTION_TOKEN}}"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    branches:\n      - master\n\npermissions:\n  contents: write\n  packages: write\n  \njobs:\n  release:\n    name: release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n          ref: master\n\n      - name: Cache toolchains\n        uses: actions/cache@v4\n        with:\n          path: ~/.bestsub/toolchains\n          key: ${{ runner.os }}-toolchains-${{ hashFiles('go.mod') }}-${{ hashFiles('scripts/build.sh') }}\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 'latest'\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 'latest'\n          cache: 'pnpm'\n          cache-dependency-path: web/pnpm-lock.yaml\n          \n      - name: Build\n        run: bash scripts/build.sh release\n\n      - name: Get latest tag\n        id: tag\n        run: |\n          LATEST_TAG=$(git describe --tags --abbrev=0)\n          echo \"TAG_NAME=$LATEST_TAG\" >> $GITHUB_OUTPUT\n\n      - name: Upload Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/archives/*\n          prerelease: false\n          tag_name: ${{ steps.tag.outputs.TAG_NAME }}\n\n      - name: Docker meta (Debian)\n        id: meta-debian\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ghcr.io/${{ github.repository }}\n            ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}\n          tags: |\n            type=raw,value=latest\n            type=raw,value=${{ steps.tag.outputs.TAG_NAME }}\n\n      - name: Docker meta (Alpine)\n        id: meta-alpine\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ghcr.io/${{ github.repository }}\n            ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}\n          tags: |\n            type=raw,value=latest-alpine\n            type=raw,value=${{ steps.tag.outputs.TAG_NAME }}-alpine\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: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build and push (Alpine)\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: ./scripts/dockerfiles/Dockerfile.alpine\n          push: true\n          platforms: linux/amd64,linux/i386,linux/arm64,linux/arm/v7\n          tags: ${{ steps.meta-alpine.outputs.tags }}\n          labels: ${{ steps.meta-alpine.outputs.labels }}\n          build-args: |\n            TARGETPLATFORM\n\n      - name: Build and push (Debian)\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: ./scripts/dockerfiles/Dockerfile.debian\n          push: true\n          platforms: linux/amd64,linux/i386,linux/arm64,linux/arm/v7\n          tags: ${{ steps.meta-debian.outputs.tags }}\n          labels: ${{ steps.meta-debian.outputs.labels }}\n          build-args: |\n            TARGETPLATFORM"
  },
  {
    "path": ".gitignore",
    "content": "# web\r\nstatic/out/*\r\n!static/out/README.md\r\n\r\n# api\r\napi/*\r\n!api/README.md\r\n\r\n# build\r\ndist/*\r\nbuild\r\n\r\n# vscode\r\n.vscode\r\n\r\n\r\n# data\r\ndata\r\ninternal/core/subconv/subconv.es5.js\r\n"
  },
  {
    "path": "LICENSE",
    "content": "               GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>."
  },
  {
    "path": "README.md",
    "content": "# BestSub\n\nBestSub 是一个高性能的节点检测，订阅转换服务，基于 Go 语言开发。该项目提供了完整的 Web 界面和 API 接口，支持多种检测项目，多种订阅格式转换，为用户提供便捷的订阅管理解决方案。\n\n## ✨ 主要特性\n\n- 🎨 **现代的 WebUI**: 提供现代化的 Web 管理界面，完善的 API 文档，方便用户自定义开发\n- ⚡ **高性能**: 并发处理，低 CPU 占用，低内存消耗，优化的资源利用率\n- 🎲 **分享**: 高度自定义的分享功能,自定义节点名称,过期时间,最大访问量,节点类型,国家....\n- 🌍 **多架构**: 支持多种系统架构和操作系统，广泛的兼容性\n- 🗂️ **节点池**: 可持久化保存历史节点，智能淘汰质量低下的节点，确保最佳体验\n- 🔧 **扩展**: 模块化设计，支持 PR 扩展新功能，仅需创建单个文件即可添加新的通知、保存或检测方式\n- 📢 **通知**: 支持多样化的通知方式和自定义通知模板，满足不同场景的消息推送需求\n- 💾 **保存**: 支持多样化的数据保存方式，灵活的数据持久化选择\n- 🔍 **检测**: 支持多样化的节点检测方式，全面的质量评估体系\n\n## 🚀 快速开始\n\n### 方式一：直接运行\n\n1. 从 [Releases](https://github.com/bestruirui/BestSub/releases/latest) 页面下载适合您系统架构的可执行文件\n2. 直接运行程序，系统将自动：\n   - 创建必要的配置文件\n\n### 方式二：Docker\n\n```bash\ndocker run -d \\\n    --name bestsub \\\n    -e PUID=1000 \\\n    -e PGID=1000 \\\n    --restart unless-stopped \\\n    -v /path/to/data:/app/data \\\n    -p 8080:8080 \\\n    ghcr.io/bestruirui/bestsub\n```\n\n**参数说明:**\n- `--name bestsub`: 设置容器名称\n- `--restart unless-stopped`: 容器自动重启策略\n- `-v /path/to/data:/app/data`: 数据持久化挂载（请将 `/path/to/data` 替换为您的实际路径）\n- `-p 8080:8080`: 端口映射，访问地址为 `http://localhost:8080`\n\n### 方式三：Docker Compose\n\n创建 `docker-compose.yml` 文件：\n\n```yaml\nservices:\n  bestsub:\n    image: ghcr.io/bestruirui/bestsub:latest\n    container_name: bestsub\n    restart: unless-stopped\n    environment:\n      - PUID=1000\n      - PGID=1000\n    ports:\n      - \"8080:8080\"\n    volumes:\n      - ./data:/app/data\n  minisubconvert:\n    image: ghcr.io/bestruirui/minisubconvert:latest\n    container_name: minisubconvert\n    restart: unless-stopped\n    environment:\n      - PUID=1000\n      - PGID=1000\n    ports:\n      - \"3000:3000\"\n```\n\n启动服务：\n```bash\ndocker-compose up -d\n```\n\n## 📁 目录结构\n\n程序运行后将创建以下目录结构：\n\n```\nbestsub/\n├── config.json              # 主配置文件\n├── data/                    # 数据目录\n│   └── bestsub.db          # SQLite 数据库文件\n├── log/                     # 日志文件目录\n├── session/                 # 会话数据目录\n│   └── bestsub.session     # 会话文件\n```\n\n## 🔗 版本历史\n\n### 当前版本 (v1.x)\n- 全新的 Web 界面\n- 增强的性能和稳定性\n- 完整的容器化支持\n\n### 经典版本 (v0.3.5)\n- **命令行界面版本**\n- **[📖 查看文档](https://github.com/bestruirui/BestSub/blob/legacy/doc/README_zh.md)** \n- **[⬇️ 下载应用](https://github.com/bestruirui/BestSub/releases/tag/v0.3.5)**\n\n## 📋 版本规范\n\n### 版本格式\n\n版本号采用语义化版本格式：**`vX.Y.Z`**\n\n- **`X`** (主版本号 - Major)\n- **`Y`** (次版本号 - Minor)  \n- **`Z`** (修订版本号 - Patch)\n\n### 版本变更规则\n\n**🔢 主版本号 (X - Major)**\n- 主版本号增加表示**重大更新**\n- 包含**破坏性变更 (Breaking Changes)**，如：\n  - 数据结构、API 接口的不兼容性修改\n  - 重大的架构调整或重构\n- 当主版本号增加时，次版本号和修订版本号归零\n- 示例：`v1.5.3` → `v2.0.0`\n\n**⚡ 次版本号 (Y - Minor)**\n- 次版本号增加表示**功能更新**\n- 包含向后兼容的功能性新增或增强\n- **前后端版本号在此位必须保持一致**，确保功能正常调用和兼容\n- 当次版本号增加时，修订版本号归零\n- 示例：`v1.2.8` → `v1.3.0`\n\n**🔧 修订版本号 (Z - Patch)**\n- 修订版本号增加表示**问题修复**或微小优化\n- 用于修复向后兼容的 Bug 或进行小幅优化调整\n- 此版本更新不要求前后端严格同步，但建议保持一致\n- 示例：`v1.3.0` → `v1.3.1`\n\n## 🤝 贡献指南\n\n我们欢迎任何形式的贡献！\n\n### 项目图标\n- **格式要求**: SVG 格式  \n- **用途**: 项目 Logo 和品牌标识  \n- **提交方式**: 创建 Issue 或 Pull Request  \n\n### 更多功能\n\n- 新的节点检测项目  \n- 新的储存渠道  \n- 新的通知渠道  \n\n### 其他贡献方式\n- 🐛 报告 Bug\n- 💡 提出新功能建议\n- 📝 改进文档\n- 🧪 编写测试用例\n\n## ⚠️ 免责声明\n\n本项目仅供学习和研究使用。使用本软件时，请您：\n\n- ✅ 遵守当地法律法规和相关政策\n- ✅ 尊重网络服务提供商的使用条款\n- ✅ 承担使用本软件可能产生的一切后果和责任\n- ⚠️ 理解作者不对使用本软件造成的任何损失承担责任\n\n**请在合法合规的前提下使用本软件。如果您不同意上述条款，请勿使用本软件。**\n\n## ❤️ 支持项目\n\n如果这个项目对您有帮助，请考虑：\n\n- ⭐ 给项目点个 Star\n- 🍴 Fork 项目并参与开发\n- 📢 向朋友推荐本项目\n- 💬 在社区中分享使用体验\n\n## 📊 项目统计\n\n![Repobeats analytics image](https://repobeats.axiom.co/api/embed/dfefb13ae0ed117da68382c0ed63695992826039.svg \"Repobeats analytics image\")\n"
  },
  {
    "path": "cmd/bestsub/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/bestruirui/bestsub/internal/config\"\n\t\"github.com/bestruirui/bestsub/internal/core/cron\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/task\"\n\t\"github.com/bestruirui/bestsub/internal/database\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/server/server\"\n\t\"github.com/bestruirui/bestsub/internal/utils/info\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/bestruirui/bestsub/internal/utils/shutdown\"\n)\n\nfunc main() {\n\n\tinfo.Banner()\n\n\tcfg := config.Base()\n\n\tif err := log.Initialize(cfg.Log.Level, cfg.Log.Path, cfg.Log.Output); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := database.Initialize(cfg.Database.Type, cfg.Database.Path); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := server.Initialize(); err != nil {\n\t\tpanic(err)\n\t}\n\n\ttask.Init(op.GetSettingInt(setting.TASK_MAX_THREAD))\n\n\tcron.Start()\n\tcron.FetchLoad()\n\tcron.CheckLoad()\n\n\tnode.InitNodePool(op.GetSettingInt(setting.NODE_POOL_SIZE))\n\n\tlog.CleanupOldLogs(5)\n\n\tserver.Start()\n\n\tshutdown.Register(server.Close)       //   ↓↓\n\tshutdown.Register(database.Close)     //   ↓↓\n\tshutdown.Register(node.CloseNodePool) //   ↓↓\n\tshutdown.Register(log.Close)          //   ↓↓\n\n\tshutdown.Listen()\n}\n"
  },
  {
    "path": "deploy/README.md",
    "content": "# Depoly\n"
  },
  {
    "path": "deploy/docker-compose.yaml",
    "content": "services:\n    bestsub:\n        image: ghcr.io/bestruirui/bestsub:latest\n        container_name: bestsub\n        restart: always\n        ports:\n            - '8080:8080'\n        volumes:\n            - './bestsub:/app/data'\n\n"
  },
  {
    "path": "docs/api/swagger.json",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"BestSub -  API 文档\\n\\n这是 BestSub 的 API 文档\\n\\n## 认证\\n大多数接口需要使用 JWT 令牌进行认证。\\n认证时，请在 Authorization 头中包含 JWT 令牌：\\n`Authorization: Bearer \\u003cyour-jwt-token\\u003e`\\n\\n## 错误响应\\n所有错误响应都遵循统一格式，包含 code、message 和 error 字段。\\n\\n## 成功响应\\n所有成功响应都遵循统一格式，包含 code、message 和 data 字段。\",\n        \"title\": \"BestSub API\",\n        \"contact\": {\n            \"name\": \"BestSub API 支持\",\n            \"email\": \"support@bestsub.com\"\n        },\n        \"license\": {\n            \"name\": \"GPL-3.0\",\n            \"url\": \"https://opensource.org/license/gpl-3-0\"\n        },\n        \"version\": \"1.0.0\"\n    },\n    \"paths\": {\n        \"/api/v1/auth/login\": {\n            \"post\": {\n                \"description\": \"用户登录接口，验证用户名和密码，返回JWT令牌\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"认证\"\n                ],\n                \"summary\": \"用户登录\",\n                \"parameters\": [\n                    {\n                        \"description\": \"登录请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_auth.LoginRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"登录成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_auth.LoginResponse\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"用户名或密码错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/auth/logout\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"用户登出接口，使当前会话失效\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"认证\"\n                ],\n                \"summary\": \"用户登出\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"登出成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/auth/user\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取当前登录用户的详细信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"认证\"\n                ],\n                \"summary\": \"获取用户信息\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_auth.Data\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/auth/user/name\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"修改当前用户的用户名\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"认证\"\n                ],\n                \"summary\": \"修改用户名\",\n                \"parameters\": [\n                    {\n                        \"description\": \"修改用户名请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_auth.UpdateUserInfoRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"用户名修改成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"用户名已存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/auth/user/password\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"修改当前用户的密码\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"认证\"\n                ],\n                \"summary\": \"修改密码\",\n                \"parameters\": [\n                    {\n                        \"description\": \"修改密码请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_auth.ChangePasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"密码修改成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权或旧密码错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/check\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"获取检测列表\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"检测ID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Response\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"创建单个检测\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"创建检测\",\n                \"parameters\": [\n                    {\n                        \"description\": \"创建检测请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"创建成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/check/type\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取检测类型\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"获取检测类型\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"object\",\n                                            \"additionalProperties\": {\n                                                \"type\": \"array\",\n                                                \"items\": {\n                                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_core_check.Desc\"\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/check/{id}\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据请求体中的ID更新检测信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"更新检测\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"检测ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"更新检测请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"检测不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据ID删除单个检测\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"删除检测\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"检测ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"删除成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"检测不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/check/{id}/run\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"手动触发检测执行\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"手动运行检测\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"检测ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"运行成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"检测不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/check/{id}/stop\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"停止正在运行的检测\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"检测\"\n                ],\n                \"summary\": \"停止检测\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"检测ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"停止成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"检测不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/log/content\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取日志内容\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"日志\"\n                ],\n                \"summary\": \"获取日志内容\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"日志文件路径\",\n                        \"name\": \"path\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"format\": \"int64\",\n                        \"description\": \"日志文件时间戳\",\n                        \"name\": \"timestamp\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"type\": \"object\",\n                                                \"properties\": {\n                                                    \"level\": {\n                                                        \"type\": \"string\"\n                                                    },\n                                                    \"msg\": {\n                                                        \"type\": \"string\"\n                                                    },\n                                                    \"time\": {\n                                                        \"type\": \"string\"\n                                                    }\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"文件不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/log/list\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取日志列表\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"日志\"\n                ],\n                \"summary\": \"获取日志列表\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"日志文件路径\",\n                        \"name\": \"path\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"type\": \"integer\",\n                                                \"format\": \"int64\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/notify\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"获取通知\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Response\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据请求体中的ID更新通知信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"更新通知\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"通知ID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"更新通知请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"通知配置不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"创建单个通知\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"创建通知\",\n                \"parameters\": [\n                    {\n                        \"description\": \"创建通知请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"创建成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据ID删除单个通知\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"删除通知\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"通知ID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"删除成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"通知不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/notify/channel\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"获取通知渠道\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"type\": \"string\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/notify/channel/config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"获取渠道配置\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"渠道\",\n                        \"name\": \"channel\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"object\",\n                                            \"additionalProperties\": {\n                                                \"type\": \"array\",\n                                                \"items\": {\n                                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_modules_notify.Desc\"\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/notify/name\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"获取通知名称和ID\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.NameAndID\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/notify/template\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"获取通知模板\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Template\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据请求体中的ID更新通知模板信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"更新通知模板\",\n                \"parameters\": [\n                    {\n                        \"description\": \"更新通知模板请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Template\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Template\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"通知模板不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/notify/test\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"测试单个通知\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"通知\"\n                ],\n                \"summary\": \"测试通知\",\n                \"parameters\": [\n                    {\n                        \"description\": \"测试通知请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_notify.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"测试成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/setting\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取系统所有配置项，支持按分组过滤和关键字搜索\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"配置\"\n                ],\n                \"summary\": \"获取配置项\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"分组名称\",\n                        \"name\": \"group\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_setting.Setting\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据请求数据中的ID批量更新配置项的值和描述\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"配置\"\n                ],\n                \"summary\": \"更新配置项\",\n                \"parameters\": [\n                    {\n                        \"description\": \"更新配置项请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_setting.Setting\"\n                            }\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/share\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取分享链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"分享\"\n                ],\n                \"summary\": \"获取分享链接\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.Response\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"创建分享链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"分享\"\n                ],\n                \"summary\": \"创建分享链接\",\n                \"parameters\": [\n                    {\n                        \"description\": \"分享数据\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"创建成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/share/node/{token}\": {\n            \"get\": {\n                \"description\": \"获取订阅内容 纯Mihomo格式的节点\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"text/plain\"\n                ],\n                \"tags\": [\n                    \"分享\"\n                ],\n                \"summary\": \"获取订阅内容 纯Mihomo格式的节点\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"分享token\",\n                        \"name\": \"token\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功，内容为yaml/plain格式\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/share/sub/{token}\": {\n            \"get\": {\n                \"description\": \"获取订阅内容 带规则的订阅\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"text/plain\"\n                ],\n                \"tags\": [\n                    \"分享\"\n                ],\n                \"summary\": \"获取订阅内容 带规则的订阅\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"分享token\",\n                        \"name\": \"token\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功，内容为yaml/plain格式\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/share/{id}\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"更新分享链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"分享\"\n                ],\n                \"summary\": \"更新分享链接\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"分享ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"分享数据\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"删除分享链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"分享\"\n                ],\n                \"summary\": \"删除分享链接\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"分享ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"删除成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/storage\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取存储\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"存储\"\n                ],\n                \"summary\": \"获取存储\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_storage.Response\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"创建存储\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"存储\"\n                ],\n                \"summary\": \"创建存储\",\n                \"parameters\": [\n                    {\n                        \"description\": \"存储配置数据\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_storage.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"创建成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_storage.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/storage/channel\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"存储\"\n                ],\n                \"summary\": \"获取存储渠道\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"type\": \"string\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/storage/channel/config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"存储\"\n                ],\n                \"summary\": \"获取渠道配置\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"渠道\",\n                        \"name\": \"channel\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"object\",\n                                            \"additionalProperties\": {\n                                                \"type\": \"array\",\n                                                \"items\": {\n                                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_modules_storage.Desc\"\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/storage/{id}\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"更新存储\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"存储\"\n                ],\n                \"summary\": \"更新存储\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"存储ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"存储配置数据\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_storage.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_storage.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"删除存储\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"存储\"\n                ],\n                \"summary\": \"删除存储\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"存储ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"删除成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/sub\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"订阅\"\n                ],\n                \"summary\": \"获取订阅链接\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"链接ID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Response\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"创建单个订阅链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"订阅\"\n                ],\n                \"summary\": \"创建订阅链接\",\n                \"parameters\": [\n                    {\n                        \"description\": \"创建订阅链接请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"创建成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/sub/batch\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"批量创建多个订阅链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"订阅\"\n                ],\n                \"summary\": \"批量创建订阅链接\",\n                \"parameters\": [\n                    {\n                        \"description\": \"批量创建订阅链接请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Request\"\n                            }\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"创建成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Response\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/sub/refresh/{id}\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据ID手动刷新单个订阅\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"订阅\"\n                ],\n                \"summary\": \"手动刷新订阅\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"订阅链接ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"刷新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Result\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"订阅链接不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/sub/{id}\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据请求体中的ID更新订阅链接信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"订阅\"\n                ],\n                \"summary\": \"更新订阅链接\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"订阅链接ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"更新订阅链接请求\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Request\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"更新成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Response\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"订阅链接不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"根据ID删除单个订阅链接\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"订阅\"\n                ],\n                \"summary\": \"删除订阅链接\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"订阅链接ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"删除成功\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"请求参数错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"订阅链接不存在\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/system/health\": {\n            \"get\": {\n                \"description\": \"检查服务健康状态，包括数据库连接状态\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"系统\"\n                ],\n                \"summary\": \"健康检查\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"服务正常\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_system.HealthResponse\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"503\": {\n                        \"description\": \"服务不可用\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/system/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取程序运行相关信息，包括内存使用、运行时长、网络流量、CPU信息等\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"系统\"\n                ],\n                \"summary\": \"系统信息\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_system.Info\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/system/live\": {\n            \"get\": {\n                \"description\": \"检查服务是否存活（简单的ping检查）\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"系统\"\n                ],\n                \"summary\": \"存活检查\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"服务存活\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/system/ready\": {\n            \"get\": {\n                \"description\": \"检查服务是否准备好接收请求\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"系统\"\n                ],\n                \"summary\": \"就绪检查\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"服务就绪\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_system.HealthResponse\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"503\": {\n                        \"description\": \"服务未就绪\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/system/version\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取程序版本信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"系统\"\n                ],\n                \"summary\": \"系统版本\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_system.Version\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/update\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"获取程序最新版本信息\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"更新\"\n                ],\n                \"summary\": \"最新版本\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"object\",\n                                            \"additionalProperties\": {\n                                                \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_core_update.LatestInfo\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/api/v1/update/:name\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"更新程序\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"更新\"\n                ],\n                \"summary\": \"更新\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"获取成功\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"string\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"未授权\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"服务器内部错误\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"github_com_bestruirui_bestsub_internal_core_check.Desc\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"desc\": {\n                    \"type\": \"string\"\n                },\n                \"key\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"options\": {\n                    \"type\": \"string\"\n                },\n                \"require\": {\n                    \"type\": \"boolean\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_core_update.LatestInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"body\": {\n                    \"type\": \"string\"\n                },\n                \"message\": {\n                    \"type\": \"string\"\n                },\n                \"published_at\": {\n                    \"type\": \"string\"\n                },\n                \"tag_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_auth.ChangePasswordRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"new_password\",\n                \"old_password\",\n                \"username\"\n            ],\n            \"properties\": {\n                \"new_password\": {\n                    \"type\": \"string\",\n                    \"example\": \"new_password\"\n                },\n                \"old_password\": {\n                    \"type\": \"string\",\n                    \"example\": \"old_password\"\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"example\": \"admin\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_auth.Data\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_auth.LoginRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"password\",\n                \"username\"\n            ],\n            \"properties\": {\n                \"password\": {\n                    \"type\": \"string\",\n                    \"example\": \"admin\"\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"example\": \"admin\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_auth.LoginResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_expires_at\": {\n                    \"type\": \"string\",\n                    \"example\": \"2024-01-01T12:00:00Z\"\n                },\n                \"access_token\": {\n                    \"type\": \"string\",\n                    \"example\": \"access_token_string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_auth.UpdateUserInfoRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"username\"\n            ],\n            \"properties\": {\n                \"username\": {\n                    \"type\": \"string\",\n                    \"example\": \"admin\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_check.Request\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {},\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"测试检测任务\"\n                },\n                \"task\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Task\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_check.Response\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {},\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"result\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Result\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"task\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_check.Task\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_check.Result\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"duration\": {\n                    \"type\": \"integer\"\n                },\n                \"extra\": {},\n                \"last_run\": {\n                    \"type\": \"string\"\n                },\n                \"msg\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_check.Task\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"cron_expr\": {\n                    \"type\": \"string\",\n                    \"example\": \"0 0 * * *\"\n                },\n                \"log_level\": {\n                    \"type\": \"string\",\n                    \"example\": \"info\"\n                },\n                \"log_write_file\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                },\n                \"notify\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                },\n                \"notify_channel\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                },\n                \"sub_id\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"integer\"\n                    },\n                    \"example\": [\n                        1\n                    ]\n                },\n                \"sub_id_exclude\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                },\n                \"timeout\": {\n                    \"type\": \"integer\",\n                    \"example\": 60\n                },\n                \"type\": {\n                    \"type\": \"string\",\n                    \"example\": \"test\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_node.Filter\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"alive_status\": {\n                    \"type\": \"integer\"\n                },\n                \"country\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"country_exclude\": {\n                    \"type\": \"boolean\"\n                },\n                \"delay_less_than\": {\n                    \"type\": \"integer\"\n                },\n                \"risk_less_than\": {\n                    \"type\": \"integer\"\n                },\n                \"speed_down_more\": {\n                    \"type\": \"integer\"\n                },\n                \"speed_up_more\": {\n                    \"type\": \"integer\"\n                },\n                \"sub_id\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"integer\"\n                    }\n                },\n                \"sub_id_exclude\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_node.SimpleInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"type\": \"integer\"\n                },\n                \"delay\": {\n                    \"type\": \"integer\"\n                },\n                \"risk\": {\n                    \"type\": \"integer\"\n                },\n                \"speed_down\": {\n                    \"type\": \"integer\"\n                },\n                \"speed_up\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_notify.NameAndID\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_notify.Request\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {},\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_notify.Response\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {},\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_notify.Template\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"template\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_setting.Setting\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"key\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_share.GenConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"filter\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_node.Filter\"\n                },\n                \"proxy\": {\n                    \"type\": \"boolean\"\n                },\n                \"rename\": {\n                    \"type\": \"string\"\n                },\n                \"sub_converter\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.SubConverterConfig\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_share.Request\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"expires\": {\n                    \"type\": \"integer\"\n                },\n                \"gen\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.GenConfig\"\n                },\n                \"max_access_count\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"token\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_share.Response\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_count\": {\n                    \"type\": \"integer\"\n                },\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"expires\": {\n                    \"type\": \"integer\"\n                },\n                \"gen\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_share.GenConfig\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"max_access_count\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"token\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_share.SubConverterConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {\n                    \"type\": \"string\"\n                },\n                \"target\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_storage.Request\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {},\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"webdav\"\n                },\n                \"type\": {\n                    \"type\": \"string\",\n                    \"example\": \"webdav\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_storage.Response\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {},\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_sub.Config\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"protocol_filter\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"protocol_filter_enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"protocol_filter_mode\": {\n                    \"type\": \"boolean\"\n                },\n                \"proxy\": {\n                    \"type\": \"boolean\"\n                },\n                \"timeout\": {\n                    \"type\": \"integer\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_sub.Request\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Config\"\n                },\n                \"cron_expr\": {\n                    \"type\": \"string\",\n                    \"example\": \"0 0 * * *\"\n                },\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_sub.Response\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Config\"\n                },\n                \"created_at\": {\n                    \"type\": \"string\"\n                },\n                \"cron_expr\": {\n                    \"type\": \"string\"\n                },\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"info\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_node.SimpleInfo\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"result\": {\n                    \"$ref\": \"#/definitions/github_com_bestruirui_bestsub_internal_models_sub.Result\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_sub.Result\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"duration\": {\n                    \"type\": \"integer\"\n                },\n                \"fail\": {\n                    \"type\": \"integer\"\n                },\n                \"last_run\": {\n                    \"type\": \"string\"\n                },\n                \"msg\": {\n                    \"type\": \"string\"\n                },\n                \"node_null_count\": {\n                    \"type\": \"integer\"\n                },\n                \"raw_count\": {\n                    \"type\": \"integer\"\n                },\n                \"success\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_system.HealthResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"database\": {\n                    \"description\": \"数据库状态\",\n                    \"type\": \"string\",\n                    \"example\": \"connected\"\n                },\n                \"status\": {\n                    \"description\": \"服务状态\",\n                    \"type\": \"string\",\n                    \"example\": \"ok\"\n                },\n                \"timestamp\": {\n                    \"description\": \"检查时间\",\n                    \"type\": \"string\",\n                    \"example\": \"2024-01-01T12:00:00\"\n                },\n                \"version\": {\n                    \"description\": \"版本信息\",\n                    \"type\": \"string\",\n                    \"example\": \"1.0.0\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_system.Info\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"cpu_percent\": {\n                    \"description\": \"CPU 占用率\",\n                    \"type\": \"number\"\n                },\n                \"download_bytes\": {\n                    \"description\": \"下载流量 (bytes)\",\n                    \"type\": \"integer\"\n                },\n                \"memory_used\": {\n                    \"description\": \"已使用内存 (bytes)\",\n                    \"type\": \"integer\"\n                },\n                \"start_time\": {\n                    \"description\": \"启动时间\",\n                    \"type\": \"string\"\n                },\n                \"upload_bytes\": {\n                    \"description\": \"上传流量 (bytes)\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_models_system.Version\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"author\": {\n                    \"type\": \"string\"\n                },\n                \"build_time\": {\n                    \"type\": \"string\"\n                },\n                \"commit\": {\n                    \"type\": \"string\"\n                },\n                \"repo\": {\n                    \"type\": \"string\"\n                },\n                \"subconverter_version\": {\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_modules_notify.Desc\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"desc\": {\n                    \"type\": \"string\"\n                },\n                \"key\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"options\": {\n                    \"type\": \"string\"\n                },\n                \"require\": {\n                    \"type\": \"boolean\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_modules_storage.Desc\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"desc\": {\n                    \"type\": \"string\"\n                },\n                \"key\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"options\": {\n                    \"type\": \"string\"\n                },\n                \"require\": {\n                    \"type\": \"boolean\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"github_com_bestruirui_bestsub_internal_server_resp.ResponseStruct\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"data\": {},\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        }\n    },\n    \"securityDefinitions\": {\n        \"BearerAuth\": {\n            \"description\": \"类型为 \\\"Bearer\\\"，后跟空格和 JWT 令牌。\",\n            \"type\": \"apiKey\",\n            \"name\": \"Authorization\",\n            \"in\": \"header\"\n        }\n    }\n}\n"
  },
  {
    "path": "docs/database/BESTSUB.json",
    "content": "{\n  \"tables\": [\n    {\n      \"name\": \"auth\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"5fSQ6mGWfRsgg-k3iArX5\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"p_zxx7J6E2VHvda5XLwM4\",\n          \"name\": \"username\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": true,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"UZe2dA81_wloaZagtZqqa\",\n          \"name\": \"password\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"K9Bq53vxKnW3VYQjTlNhu\",\n      \"x\": 54,\n      \"y\": 40\n    },\n    {\n      \"name\": \"setting\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"C3TajAx2rVfJyKDr5wS7N\",\n          \"name\": \"key\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": true,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"VFICQh-IoYbADFMaVtyJM\",\n          \"name\": \"value\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"hjBAyr0lCMLDGLnEqx4mt\",\n      \"x\": 308,\n      \"y\": 40\n    },\n    {\n      \"name\": \"notify_template\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"aPVyZgMlY0rAXkhsiGN0S\",\n          \"name\": \"type\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"GedQIFxcSKnhDqDdMBmkd\",\n          \"name\": \"template\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"qvBLHFHDEQMcvN7Mg1Yzf\",\n      \"x\": 562,\n      \"y\": 40\n    },\n    {\n      \"name\": \"notify\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"yn7TOVZbZ2hHQsdn8YMdg\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": true,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"9oiTodm77XzAF_35GhPpy\",\n          \"name\": \"name\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"E8tUPCAZAWzs6Bo4R-gZ8\",\n          \"name\": \"type\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"ur8-HAWlUb3BVOYrignKn\",\n          \"name\": \"config\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"vpWds5GXNQffE5c_w8GTa\",\n      \"x\": 816,\n      \"y\": 40\n    },\n    {\n      \"name\": \"check_task\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"X8DIONkwFxIhFk84Vdw8H\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"z9fE6n_6oXg326t0wxI3z\",\n          \"name\": \"enable\",\n          \"type\": \"BOOLEAN\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"gDeq6xOXxK-EPPP25noY3\",\n          \"name\": \"name\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"ZnCL2M1C-zIXaWpvar0NN\",\n          \"name\": \"task\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"TTpDOH2ASKlaqGhQjaiN3\",\n          \"name\": \"config\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"Jh2P1spnA7OmJRSSc62_L\",\n          \"name\": \"result\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"P26OgjsewN5mmfipR1joA\",\n      \"x\": 1070,\n      \"y\": 40\n    },\n    {\n      \"name\": \"storage\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"IqL9ngLv_3lGHVB3CzhxN\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"GDKYuiA0I4uDeeoBl-RyA\",\n          \"name\": \"name\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"BeAtbwLSpgr6HVsYy_Zly\",\n          \"name\": \"type\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"SC60MqiPB7oKm-4P9OYmB\",\n          \"name\": \"config\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"6zB3TFuEBkmefWBH2oIzY\",\n      \"x\": 1070,\n      \"y\": 353\n    },\n    {\n      \"name\": \"sub_template\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"aQ_KOAUAP7ff2zUH1pgIP\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"3iQaDIqv5rUBrzOPmSH0y\",\n          \"name\": \"name\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"CACizfDmfOi6i_MDE4lig\",\n          \"name\": \"type\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"aUKGH4p_TMNRqmn_jl_cY\",\n          \"name\": \"template\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"Nd14xBak0Dylm-vCR6Kow\",\n      \"x\": 816,\n      \"y\": 353\n    },\n    {\n      \"name\": \"sub\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"cg8ZPMZMT6kxafhRqFHv2\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"j1xYw_Ql6gyzEftfgk0N7\",\n          \"name\": \"enable\",\n          \"type\": \"BOOLEAN\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"true\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"bMeAdLhPX96ajjp4Lvlt3\",\n          \"name\": \"name\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"1-jQ706xtwgEkdmzurPog\",\n          \"name\": \"cron_expr\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"D5bz0oxZ9bDt4_b82fMt7\",\n          \"name\": \"config\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"jLDqKaerWwupXaf4YcrZK\",\n          \"name\": \"result\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"{}\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"urC5qElwekXRwqWQ-sIMc\",\n          \"name\": \"created_at\",\n          \"type\": \"DATETIME\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"CURRENT_TIMESTAMP\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"a2ntNMPEMJ1itn3br8gG4\",\n          \"name\": \"updated_at\",\n          \"type\": \"DATETIME\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"CURRENT_TIMESTAMP\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"1M0JLZy3hO7B02pBQuOr0\",\n      \"x\": 562,\n      \"y\": 353\n    },\n    {\n      \"name\": \"share\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"-8pCJUAo4dNElTvun64wS\",\n          \"name\": \"id\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"mKyLNAICw3REd4o93Pl-8\",\n          \"name\": \"enable\",\n          \"type\": \"BOOLEAN\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"false\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"zHcTEPQxG3-K1kAdgQel8\",\n          \"name\": \"name\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"9xTMb9mFYBD9iY3EaXD3o\",\n          \"name\": \"gen\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"oAZFFOq8zzeDzaTkXXeY6\",\n          \"name\": \"token\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": true,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"O94DSBxsxULcrDKjHmS4k\",\n          \"name\": \"access_count\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"0\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"Y3jjjRZwP8nDwpHRM0SN8\",\n          \"name\": \"max_access_count\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"0\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"pwca4TOq6_tLLO6IDjGBE\",\n          \"name\": \"expires\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"0\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"C58fWZEDUJBkY_bfB0I64\",\n      \"x\": 308,\n      \"y\": 353\n    },\n    {\n      \"name\": \"migration\",\n      \"comment\": \"\",\n      \"color\": \"#175e7a\",\n      \"fields\": [\n        {\n          \"id\": \"GmjplhzoTgouk4gIEq188\",\n          \"name\": \"date\",\n          \"type\": \"INTEGER\",\n          \"comment\": \"\",\n          \"unique\": true,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": true,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"3GC8-bNPvJYl9TVHwYxCo\",\n          \"name\": \"version\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"xvNmYiFJ1iv5mEBAX7tHJ\",\n          \"name\": \"description\",\n          \"type\": \"TEXT\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": false,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        },\n        {\n          \"id\": \"YZtCo5MXDSmPb8zVfuovI\",\n          \"name\": \"applied_at\",\n          \"type\": \"DATETIME\",\n          \"comment\": \"\",\n          \"unique\": false,\n          \"increment\": false,\n          \"notNull\": true,\n          \"primary\": false,\n          \"default\": \"\",\n          \"check\": \"\"\n        }\n      ],\n      \"indices\": [],\n      \"id\": \"Rg-jzHbi39Wb6U2JG8omy\",\n      \"x\": 54,\n      \"y\": 353\n    }\n  ],\n  \"relationships\": [],\n  \"notes\": [],\n  \"subjectAreas\": [],\n  \"database\": \"sqlite\",\n  \"title\": \"BESTSUB\"\n}"
  },
  {
    "path": "docs/database/README.md",
    "content": "# 数据库设计文档\n\n## 概述\n本文档介绍项目的数据库设计方案及相关工具使用说明。\n\n## 设计图查看方法\n1. 下载本目录中的设计文件：[BESTSUB.json](./BESTSUB.json)\n2. 访问 [DrawDB](https://www.drawdb.app/) 在线工具\n3. 将下载的JSON文件导入到DrawDB中\n4. 即可查看完整的数据库设计图及表结构关系\n\n## 推荐工具\n- **数据库设计工具**：[DrawDB](https://github.com/drawdb-io/drawdb) - 免费且功能强大的数据库设计工具\n- **SQLite可视化工具**：[sqlite3-editor](https://github.com/yy0931/sqlite3-editor) - 方便直观的SQLite数据库查看和编辑工具\n\n## 使用说明\n在开发过程中，请参照数据库设计图进行数据库操作，确保数据结构的一致性和完整性。如需修改数据库设计，请更新设计文件并同步到项目中。"
  },
  {
    "path": "docs/rename/README.md",
    "content": "# BestSub 节点重命名模板指南\n\nBestSub 节点重命名功能允许用户自定义节点名称的显示格式，通过灵活的模板语法实现个性化的节点管理体验。\n\n---\n\n## 📋 可用变量\n\n重命名模板支持以下变量：\n\n| 变量                    | 说明                | 示例               |\n|-----------------------|-------------------|------------------|\n| `{{.Count}}`          | 节点序号 (必填，从1开始)    | 1, 2, 3          |\n| `{{.SpeedUp}}`        | 上行速度 (平均，单位：KB/s) | 102400, 51200    |\n| `{{.SpeedDown}}`      | 下行速度 (平均，单位：KB/s) | 102400, 51200    |\n| `{{.Delay}}`          | 延迟 (平均，单位：毫秒)     | 45, 120          |\n| `{{.Risk}}`           | 风险等级 (数字越小越好)     | 1, 2, 3          |\n| `{{.Country.NameEn}}` | 国家/地区代码           | JP, US, SG       |\n| `{{.Country.NameZh}}` | 国家/地区中文名称         | 日本, 美国, 新加坡      |\n| `{{.Country.Emoji}}`  | 国家/地区旗帜表情符号       | 🇯🇵, 🇺🇸, 🇸🇬 |\n| `{{.SubName}}`        | 订阅名称              | 未知订阅             |\n| `{{.SubTags}}`        | 订阅标签              | \\<Tag1\\|Tag2\\>   |\n| `{{.SubTagsOrigin}}`  | 订阅标签（原始数组）        | [\"Tag1\", \"Tag2\"] |\n\n> 注意：`.SubTagsOrigin` 类型为 `[]string`，因此不能直接在重命名模板中使用\n\n---\n\n## 🚀 快速开始\n\n### 立即可用的推荐模板\n\n#### 简洁美观格式\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}-{{.Delay}}ms\n```\n输出示例：`🇯🇵日本-45ms`, `🇺🇸美国-120ms`\n\n#### 游戏玩家格式 (低延迟优先)\n```go\n{{if le .Delay 50}}🚀{{else if le .Delay 100}}⚡{{else}}🐌{{end}}{{.Country.NameZh}}\n```\n输出示例：`🚀日本`, `⚡美国`, `🐌其他`\n\n#### 下载专用格式 (高速度优先)\n```go\n{{div .SpeedDown 1024}}MB/s-{{.Country.Emoji}}{{.Country.NameZh}}\n```\n输出示例：`100MB/s-🇯🇵日本`, `50MB/s-🇺🇸美国`\n\n---\n\n## 🎨 模板库\n\n### 基础模板\n\n#### 简单格式\n```go\n节点{{.Count}}\n```\n输出示例：`节点1`, `节点2`, `节点3`\n\n#### 带国家信息\n```go\n{{.Country.NameZh}}-节点{{.Count}}\n```\n输出示例：`日本-节点1`, `美国-节点2`\n\n#### 带国旗格式\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}-{{.Count}}\n```\n输出示例：`🇯🇵日本-1`, `🇺🇸美国-2`\n\n#### 基础性能格式\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}-{{.Delay}}ms\n```\n输出示例：`🇯🇵日本-45ms`, `🇺🇸美国-120ms`\n\n### 推荐模板\n\n#### 格式1：简洁信息\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}-{{.Delay}}ms\n```\n输出示例：`🇯🇵日本-45ms`, `🇺🇸美国-120ms`\n\n#### 格式2：速度优先\n```go\n{{div .SpeedDown 1024}}MB/s-{{.Country.Emoji}}{{.Country.NameZh}}\n```\n输出示例：`100MB/s-🇯🇵日本`, `50MB/s-🇺🇸美国`\n\n#### 格式3：完整信息\n```go\n{{printf \"%03d\" .Count}}-{{.Country.Emoji}}{{.Country.NameZh}}-{{.Delay}}ms-{{div .SpeedDown 1024}}MB/s\n```\n输出示例：`001-🇯🇵日本-45ms-100MB/s`\n\n#### 格式4：质量评级\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}-{{if le .Delay 50}}极速{{else if le .Delay 100}}快速{{else}}普通{{end}}\n```\n输出示例：`🇯🇵日本-极速`, `🇺🇸美国-快速`\n\n### 高级模板\n\n#### 数字格式化\n```go\n{{printf \"%03d\" .Count}}-{{.Country.NameZh}}\n```\n输出示例：`001-日本`, `002-美国`, `010-新加坡`\n\n#### 条件判断\n```go\n{{.Country.NameZh}}{{if eq .Risk 1}}✅{{else if eq .Risk 2}}⚠️{{else}}❌{{end}}\n```\n输出示例：`日本✅`, `美国⚠️`, `其他❌`\n\n#### 速度质量标识\n```go\n{{if ge .SpeedDown 51200}}🚀{{else if ge .SpeedDown 10240}}⚡{{else}}🐌{{end}}{{.Country.NameZh}}\n```\n输出示例：`🚀日本`, `⚡美国`, `🐌其他`\n\n#### 延迟分级\n```go\n{{if le .Delay 50}}极速{{else if le .Delay 100}}快速{{else if le .Delay 200}}普通{{else}}较慢{{end}}-{{.Country.NameZh}}\n```\n输出示例：`极速-日本`, `快速-美国`, `普通-新加坡`\n\n### 场景专用模板\n\n#### 游戏玩家专用\n```go\n🎮{{printf \"%03d\" .Count}}-{{.Country.Emoji}}{{.Country.NameZh}}-{{if le .Delay 50}}4星{{else if le .Delay 100}}3星{{else}}2星{{end}}\n```\n\n#### 工作办公专用\n```go\n💼{{.Country.NameZh}}-{{if le .Delay 100}}稳定{{else}}一般{{end}}-{{div .SpeedDown 1024}}MB\n```\n\n#### 视频流媒体专用\n```go\n📺{{.Country.Emoji}}{{.Country.NameZh}}-{{if ge .SpeedDown 25600}}4K{{else if ge .SpeedDown 10240}}1080P{{else}}720P{{end}}\n```\n\n#### 开发者专用\n```go\n{{.Country.NameEn}}{{printf \"%03d\" .Count}}|{{.Delay}}ms|{{div .SpeedDown 1024}}MB|Risk{{.Risk}}\n```\n\n#### 专业监控风格\n```go\n[{{.Country.NameEn}}] Ping:{{.Delay}}ms|Down:{{div .SpeedDown 1024}}MB/s|Risk:{{.Risk}}\n```\n输出示例：`[JP] Ping:45ms|Down:100MB/s|Risk:1`\n\n#### 质量评分系统\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}-{{if ge .SpeedDown 51200}}{{if le .Delay 50}}S{{else if le .Delay 100}}A{{else}}B{{end}}{{else if ge .SpeedDown 10240}}{{if le .Delay 50}}A{{else if le .Delay 100}}B{{else}}C{{end}}{{else}}C{{end}}\n```\n输出示例：`🇯🇵日本-S`, `🇺🇸美国-B`, `🇸🇬新加坡-C`\n\n---\n\n## 📚 函数参考\n\n### 数学运算\n- `add x y` - 加法：`{{add .Count 1}}`\n- `sub x y` - 减法：`{{sub 100 .Delay}}`\n- `div x y` - 除法：`{{div .SpeedDown 1024}}`\n- `mod x y` - 取余：`{{mod .Count 2}}`\n\n### 比较运算\n- `eq x y` - 等于：`{{if eq .Risk 1}}安全{{end}}`\n- `ne x y` - 不等于：`{{if ne .Delay 0}}正常{{end}}`\n- `lt x y` - 小于：`{{if lt .Delay 50}}极速{{end}}`\n- `le x y` - 小于等于：`{{if le .Delay 100}}快速{{end}}`\n- `gt x y` - 大于：`{{if gt .SpeedDown 51200}}高速{{end}}`\n- `ge x y` - 大于等于：`{{if ge .SpeedDown 10240}}可用{{end}}`\n\n### 逻辑运算\n- `and x y` - 逻辑与：`{{if and (ge .SpeedDown 51200) (le .Delay 50)}}极品{{end}}`\n- `or x y` - 逻辑或：`{{if or (le .Delay 50) (ge .SpeedDown 51200)}}推荐{{end}}`\n- `not x` - 逻辑非：`{{if not (eq .Risk 3)}}安全{{end}}`\n\n### 字符串处理\n- `printf format args...` - 格式化：`{{printf \"%03d\" .Count}}`\n- `slice s start end` - 切片：`{{slice .Country.NameEn 0 2}}`\n\n### 数组处理\n\n+ `for index item` - 循环：`<{{range $i, $v := .SubTags}}{{if $i}}|{{end}}{{$v}}{{end}}>`\n\n---\n\n## 💡 使用技巧\n\n### 单位转换\n- KB/s 转 MB/s：`{{div .SpeedDown 1024}}MB/s`\n- ms 转 s：`{{div .Delay 1000}}.{{mod .Delay 1000}}s`\n- 智能速度单位：`{{if ge .SpeedDown 1024}}{{div .SpeedDown 1024}}MB/s{{else}}{{.SpeedDown}}KB/s{{end}}`\n\n### 条件组合\n```go\n{{if and (ge .SpeedDown 51200) (le .Delay 50)}}🚀{{else if and (ge .SpeedDown 10240) (le .Delay 100)}}⚡{{else}}🐌{{end}}{{.Country.NameZh}}\n```\n\n### 性能分级\n```go\n{{.Country.NameZh}}-{{if le .Delay 30}}S+{{else if le .Delay 50}}S{{else if le .Delay 100}}A{{else if le .Delay 200}}B{{else}}C{{end}}\n```\n\n### 风险标识\n```go\n{{.Country.Emoji}}{{.Country.NameZh}}{{if eq .Risk 1}}🟢{{else if eq .Risk 2}}🟡{{else if eq .Risk 3}}🟠{{else}}🔴{{end}}\n```\n\n---\n\n## 📖 快速参考\n\n### 常用阈值\n- **延迟等级**：\n  - 极速：≤ 50ms\n  - 快速：≤ 100ms\n  - 普通：≤ 200ms\n  - 较慢：> 200ms\n\n- **速度等级** (KB/s)：\n  - 极速：≥ 51200 (50MB/s)\n  - 快速：≥ 10240 (10MB/s)\n  - 普通：≥ 2048 (2MB/s)\n  - 较慢：< 2048\n\n- **视频质量要求**：\n  - 4K：≥ 25600 KB/s\n  - 1080P：≥ 10240 KB/s\n  - 720P：≥ 5120 KB/s\n\n### 颜色建议\n- 🟢 安全：风险等级 1\n- 🟡 注意：风险等级 2\n- 🟠 警告：风险等级 3\n- 🔴 危险：风险等级 ≥ 4\n\n---\n\n## ⚠️ 注意事项\n\n- **必填项**：每个模板都必须包含 `{{.Count}}` 变量\n- **单位说明**：速度变量单位为 KB/s，延迟变量单位为毫秒\n- **语法规范**：使用 Go 语言的 `text/template` 语法\n- **大小写敏感**：所有变量名区分大小写，请确保使用正确的变量名\n- **字符转义**：模板中的引号需要转义，如 `\\\"`\n\n---\n\n## ❓ 常见问题\n\n**Q: 模板中必须包含哪些变量？**\nA: `{{.Count}}` 是必填项，其他变量可根据需要选择使用\n\n**Q: 如何将速度单位从 KB/s 转换为 MB/s？**\nA: 使用 `{{div .SpeedDown 1024}}MB/s` 进行单位转换\n\n**Q: 如何根据延迟给节点进行分级显示？**\nA: 使用条件判断语句，如 `{{if le .Delay 50}}极速{{end}}`\n\n**Q: 模板支持哪些数学运算？**\nA: 支持加减乘除、取余等基本数学运算\n\n**Q: 模板语法错误应该如何排查？**\nA: 请检查变量名大小写、括号匹配和条件语句完整性\n\n**Q: 为什么我的模板没有生效？**\nA: 请检查是否包含了必填的 `{{.Count}}` 变量\n\n**Q: 如何将节点序号显示为三位数格式？**\nA: 使用 `{{printf \"%03d\" .Count}}` 可以格式化为 001, 002...\n\n**Q: 如何根据不同的速度显示对应的图标？**\nA: 使用条件判断：`{{if ge .SpeedDown 51200}}🚀{{else}}⚡{{end}}`\n\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/bestruirui/bestsub\n\ngo 1.24.2\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0\n\tgithub.com/enfein/mieru/v3 v3.30.0\n\tgithub.com/gin-contrib/cors v1.7.6\n\tgithub.com/gin-gonic/gin v1.11.0\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/metacubex/mihomo v1.19.23\n\tgithub.com/panjf2000/ants/v2 v2.11.4\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/shirou/gopsutil/v4 v4.26.1\n\tgo.uber.org/zap v1.27.1\n\tgolang.org/x/crypto v0.47.0\n\tgopkg.in/yaml.v3 v3.0.1\n\tmodernc.org/sqlite v1.44.3\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/RyuaNerin/go-krypto v1.3.0 // indirect\n\tgithub.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/bytedance/gopkg v0.1.3 // indirect\n\tgithub.com/bytedance/sonic v1.15.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.5.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/coreos/go-iptables v0.8.0 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/dunglas/httpsfv v1.0.2 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/ebitengine/purego v0.9.1 // indirect\n\tgithub.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect\n\tgithub.com/ericlagergren/polyval v0.0.0-20230805202542-18692a1b76f9 // indirect\n\tgithub.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect\n\tgithub.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.13 // indirect\n\tgithub.com/gaukas/godicttls v0.0.4 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.30.1 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/gobwas/ws v1.4.0 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/gofrs/uuid/v5 v5.4.0 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.18.3 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/klauspost/reedsolomon v1.13.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mdlayher/netlink v1.8.0 // indirect\n\tgithub.com/mdlayher/socket v0.5.1 // indirect\n\tgithub.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d // indirect\n\tgithub.com/metacubex/ascon v0.1.0 // indirect\n\tgithub.com/metacubex/bart v0.26.0 // indirect\n\tgithub.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b // indirect\n\tgithub.com/metacubex/blake3 v0.1.0 // indirect\n\tgithub.com/metacubex/chacha v0.1.5 // indirect\n\tgithub.com/metacubex/chi v0.1.0 // indirect\n\tgithub.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727 // indirect\n\tgithub.com/metacubex/cpu v0.1.1 // indirect\n\tgithub.com/metacubex/edwards25519 v1.2.0 // indirect\n\tgithub.com/metacubex/fswatch v0.1.1 // indirect\n\tgithub.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect\n\tgithub.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect\n\tgithub.com/metacubex/hkdf v0.1.0 // indirect\n\tgithub.com/metacubex/hpke v0.1.0 // indirect\n\tgithub.com/metacubex/http v0.1.1 // indirect\n\tgithub.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect\n\tgithub.com/metacubex/mhurl v0.1.0 // indirect\n\tgithub.com/metacubex/mlkem v0.1.0 // indirect\n\tgithub.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect\n\tgithub.com/metacubex/qpack v0.6.0 // indirect\n\tgithub.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56 // indirect\n\tgithub.com/metacubex/randv2 v0.2.0 // indirect\n\tgithub.com/metacubex/restls-client-go v0.1.7 // indirect\n\tgithub.com/metacubex/sing v0.5.7 // indirect\n\tgithub.com/metacubex/sing-mux v0.3.5 // indirect\n\tgithub.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e // indirect\n\tgithub.com/metacubex/sing-shadowsocks v0.2.12 // indirect\n\tgithub.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect\n\tgithub.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect\n\tgithub.com/metacubex/sing-tun v0.4.17 // indirect\n\tgithub.com/metacubex/sing-vmess v0.2.5 // indirect\n\tgithub.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect\n\tgithub.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect\n\tgithub.com/metacubex/tfo-go v0.0.0-20251204144243-738de9e3cd15 // indirect\n\tgithub.com/metacubex/tls v0.1.5 // indirect\n\tgithub.com/metacubex/utls v1.8.4 // indirect\n\tgithub.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect\n\tgithub.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect\n\tgithub.com/miekg/dns v1.1.72 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/mroth/weightedrand/v2 v2.1.0 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect\n\tgithub.com/openacid/low v0.1.21 // indirect\n\tgithub.com/oschwald/maxminddb-golang v1.12.0 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.25 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/rogpeppe/go-internal v1.13.1 // indirect\n\tgithub.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect\n\tgithub.com/samber/lo v1.53.0 // indirect\n\tgithub.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect\n\tgithub.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect\n\tgithub.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgithub.com/ugorji/go/codec v1.3.1 // indirect\n\tgithub.com/vishvananda/netns v0.0.4 // indirect\n\tgithub.com/vmihailenco/msgpack/v5 v5.4.1 // indirect\n\tgithub.com/vmihailenco/tagparser/v2 v2.0.0 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect\n\tgitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect\n\tgo.uber.org/automaxprocs v1.6.0 // indirect\n\tgo.uber.org/mock v0.6.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect\n\tgolang.org/x/arch v0.23.0 // indirect\n\tgolang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n\tgolang.org/x/text v0.33.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tmodernc.org/libc v1.67.7 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=\ngithub.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=\ngithub.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=\ngithub.com/RyuaNerin/testingutil v0.1.0/go.mod h1:yTqj6Ta/ycHMPJHRyO12Mz3VrvTloWOsy23WOZH19AA=\ngithub.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=\ngithub.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=\ngithub.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=\ngithub.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=\ngithub.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=\ngithub.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=\ngithub.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=\ngithub.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=\ngithub.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=\ngithub.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/enfein/mieru/v3 v3.27.0 h1:+E/1TF7OfimS2h582atEXQxPtJMyvqUTFBJUgzn1rxg=\ngithub.com/enfein/mieru/v3 v3.27.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=\ngithub.com/enfein/mieru/v3 v3.30.0 h1:g7v0TuK7y0ZMn6TOdjOs8WEUQk8bvs6WYPBJ16SKdBU=\ngithub.com/enfein/mieru/v3 v3.30.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=\ngithub.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=\ngithub.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=\ngithub.com/ericlagergren/polyval v0.0.0-20230805202542-18692a1b76f9 h1:NUmyvuwVoDsIFzOGFKW4zpCtQTbX2T4JpSn1jal64gM=\ngithub.com/ericlagergren/polyval v0.0.0-20230805202542-18692a1b76f9/go.mod h1:aXxf//HFNaacVV7/YZ8qevpNZAEoxSCpoBjscNhjrCI=\ngithub.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=\ngithub.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=\ngithub.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=\ngithub.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=\ngithub.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=\ngithub.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=\ngithub.com/ericlagergren/testutil v0.0.0-20220814024112-d21c9429edc2 h1:j9adob+s2qXdvdeJywrVifDfHAIq0XwoaK/0q4D1BGw=\ngithub.com/ericlagergren/testutil v0.0.0-20220814024112-d21c9429edc2/go.mod h1:E4aJHbNMb6zjyVd1Mrpf3FIJ6kAtnVUq2yl0T6DHZ/I=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=\ngithub.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=\ngithub.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=\ngithub.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=\ngithub.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=\ngithub.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=\ngithub.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=\ngithub.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=\ngithub.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=\ngithub.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 h1:MEufgJohwIjFi2n3eJv4c/8UdRLQVUwPwSWQPoER+eU=\ngithub.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=\ngithub.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/klauspost/reedsolomon v1.13.0 h1:E0Cmgf2kMuhZTj6eefnvpKC4/Q4jhCi9YIjcZjK4arc=\ngithub.com/klauspost/reedsolomon v1.13.0/go.mod h1:ggJT9lc71Vu+cSOPBlxGvBN6TfAS77qB4fp8vJ05NSA=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=\ngithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=\ngithub.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHid9HK3594=\ngithub.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=\ngithub.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=\ngithub.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c=\ngithub.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY=\ngithub.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM=\ngithub.com/metacubex/ascon v0.1.0/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc=\ngithub.com/metacubex/bart v0.26.0 h1:d/bBTvVatfVWGfQbiDpYKI1bXUJgjaabB2KpK1Tnk6w=\ngithub.com/metacubex/bart v0.26.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=\ngithub.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY=\ngithub.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw=\ngithub.com/metacubex/blake3 v0.1.0 h1:KGnjh/56REO7U+cgZA8dnBhxdP7jByrG7hTP+bu6cqY=\ngithub.com/metacubex/blake3 v0.1.0/go.mod h1:CCkLdzFrqf7xmxCdhQFvJsRRV2mwOLDoSPg6vUTB9Uk=\ngithub.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M=\ngithub.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=\ngithub.com/metacubex/chi v0.1.0 h1:rjNDyDj50nRpicG43CNkIw4ssiCbmDL8d7wJXKlUCsg=\ngithub.com/metacubex/chi v0.1.0/go.mod h1:zM5u5oMQt8b2DjvDHvzadKrP6B2ztmasL1YHRMbVV+g=\ngithub.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727 h1:qbZQ0sO0bDBKPvTd/qNQK6513300WJ5GRsHnw3PO4Ho=\ngithub.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=\ngithub.com/metacubex/cpu v0.1.0 h1:8PeTdV9j6UKbN1K5Jvtbi/Jock7dknvzyYuLb8Conmk=\ngithub.com/metacubex/cpu v0.1.0/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=\ngithub.com/metacubex/cpu v0.1.1 h1:rRV5HGmeuGzjiKI3hYbL0dCd0qGwM7VUtk4ICXD06mI=\ngithub.com/metacubex/cpu v0.1.1/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=\ngithub.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4=\ngithub.com/metacubex/edwards25519 v1.2.0/go.mod h1:NCQF3J/Ki7382FJuokwsywEIIEI/gro/3smyXgQJsx0=\ngithub.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=\ngithub.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=\ngithub.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=\ngithub.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=\ngithub.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ=\ngithub.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=\ngithub.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=\ngithub.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=\ngithub.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=\ngithub.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=\ngithub.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=\ngithub.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=\ngithub.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw=\ngithub.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=\ngithub.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=\ngithub.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=\ngithub.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE=\ngithub.com/metacubex/mhurl v0.1.0/go.mod h1:2qpQImCbXoUs6GwJrjuEXKelPyoimsIXr07eNKZdS00=\ngithub.com/metacubex/mihomo v1.19.20 h1:S2sPZILo5VjsUVka/KJR0F9lyxGkeHKSkLYLcjx5PbE=\ngithub.com/metacubex/mihomo v1.19.20/go.mod h1:XC0nYFIkDkEFzggZLXLbcnGmjlMm2zivIDZrlmD/zd0=\ngithub.com/metacubex/mihomo v1.19.23 h1:yHxEyIwu1XstpUFw7SqHBCWO//KrQYkhG18XB86y0Ns=\ngithub.com/metacubex/mihomo v1.19.23/go.mod h1:xlgWFVL2IfTT8cOkJ8+oXeoc5xNQko+YGvw6MOS8au0=\ngithub.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I=\ngithub.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ=\ngithub.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=\ngithub.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=\ngithub.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=\ngithub.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=\ngithub.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af h1:do5o1rzn64NEN5oGswo7VruDkbz2055fhVT3rXehA8E=\ngithub.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=\ngithub.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56 h1:7yfF31COW2hiCovb5+3uSxRl3UKWOXjpS0j4N5U0qZ8=\ngithub.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=\ngithub.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=\ngithub.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=\ngithub.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=\ngithub.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g=\ngithub.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM=\ngithub.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=\ngithub.com/metacubex/sing-mux v0.3.5 h1:UqVN+o62SR8kJaC9/3VfOc5UiVqgVY/ef9WwfGYYkk0=\ngithub.com/metacubex/sing-mux v0.3.5/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc=\ngithub.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e h1:MLxp42z9Jd6LtY2suyawnl24oNzIsFxWc15bNeDIGxA=\ngithub.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e/go.mod h1:+lgKTd52xAarGtqugALISShyw4KxnoEpYe2u0zJh26w=\ngithub.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=\ngithub.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=\ngithub.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=\ngithub.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=\ngithub.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=\ngithub.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=\ngithub.com/metacubex/sing-tun v0.4.17 h1:ehzvPLyxG1vmjaKVeB0aEK1eqhR3reEzdbqQfM3+5XA=\ngithub.com/metacubex/sing-tun v0.4.17/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=\ngithub.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=\ngithub.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=\ngithub.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=\ngithub.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=\ngithub.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk=\ngithub.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=\ngithub.com/metacubex/tfo-go v0.0.0-20251204144243-738de9e3cd15 h1:XKUOMjFYUGOU5sLwbSbGgGI0oOcTrrs1gLCoUB0Kg4M=\ngithub.com/metacubex/tfo-go v0.0.0-20251204144243-738de9e3cd15/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=\ngithub.com/metacubex/tls v0.1.4 h1:Gm5GrkyMUh52gYOMIAQ1kHIym4v4M3Qb87Wsmd8Kpdc=\ngithub.com/metacubex/tls v0.1.4/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=\ngithub.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc=\ngithub.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=\ngithub.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=\ngithub.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=\ngithub.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=\ngithub.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=\ngithub.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E=\ngithub.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49/go.mod h1:MBeEa9IVBphH7vc3LNtW6ZujVXFizotPo3OEiHQ+TNU=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=\ngithub.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=\ngithub.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=\ngithub.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=\ngithub.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=\ngithub.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=\ngithub.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I=\ngithub.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do=\ngithub.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=\ngithub.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=\ngithub.com/panjf2000/ants/v2 v2.11.4 h1:UJQbtN1jIcI5CYNocTj0fuAUYvsLjPoYi0YuhqV/Y48=\ngithub.com/panjf2000/ants/v2 v2.11.4/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=\ngithub.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=\ngithub.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=\ngithub.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=\ngithub.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=\ngithub.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=\ngithub.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=\ngithub.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=\ngithub.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=\ngithub.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=\ngithub.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU=\ngithub.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo=\ngithub.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngithub.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=\ngithub.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngithub.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=\ngithub.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=\ngithub.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=\ngithub.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=\ngithub.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=\ngithub.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=\ngithub.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=\ngithub.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=\ngitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=\ngitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=\ngitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=\ngo.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=\ngo.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=\ngo.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=\ngolang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=\ngolang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=\ngolang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=\ngolang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=\ngolang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=\nmodernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=\nmodernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=\nmodernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=\nmodernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\n"
  },
  {
    "path": "internal/config/base.go",
    "content": "package config\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/config\"\n\t\"github.com/bestruirui/bestsub/internal/utils\"\n)\n\nvar baseConfig = config.DefaultBase()\n\nfunc init() {\n\texecPath, err := os.Executable()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"获取可执行文件路径失败: %v\", err))\n\t}\n\texecDir := filepath.Dir(execPath)\n\tdefaultConfigPath := filepath.Join(execDir, \"config.json\")\n\n\tconfigPath := flag.String(\"c\", defaultConfigPath, \"config file path\")\n\tflag.Parse()\n\tif *configPath == \"\" {\n\t\t*configPath = defaultConfigPath\n\t}\n\tif !filepath.IsAbs(*configPath) {\n\t\tabsPath, err := filepath.Abs(*configPath)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"无法转换为绝对路径: %v\", err))\n\t\t}\n\t\t*configPath = absPath\n\t}\n\n\tif err := loadFromFile(&baseConfig, *configPath); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tif err := createDefaultConfig(*configPath); err != nil {\n\t\t\t\tpanic(fmt.Errorf(\"创建默认配置文件失败: %v\", err))\n\t\t\t}\n\t\t\tif err := loadFromFile(&baseConfig, *configPath); err != nil {\n\t\t\t\tpanic(fmt.Errorf(\"加载默认配置文件失败: %v\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tpanic(fmt.Errorf(\"加载配置文件失败: %v\", err))\n\t\t}\n\t}\n\n\tsetupPaths(&baseConfig, *configPath)\n\n\tloadFromEnv(&baseConfig)\n\n\tif err := validateConfig(&baseConfig); err != nil {\n\t\tpanic(fmt.Errorf(\"配置验证失败: %v\", err))\n\t}\n}\n\nfunc Base() config.Base {\n\treturn baseConfig\n}\n\nfunc setupPaths(config *config.Base, configPath string) {\n\tconfigDir := filepath.Dir(configPath)\n\n\tif config.Server.UIPath == \"\" {\n\t\tconfig.Server.UIPath = filepath.Join(configDir, \"ui\")\n\t}\n\n\tif config.Database.Path == \"\" {\n\t\tconfig.Database.Path = filepath.Join(configDir, \"data\", \"bestsub.db\")\n\t}\n\n\tif config.Log.Path == \"\" {\n\t\tconfig.Log.Path = filepath.Join(configDir, \"log\")\n\t}\n\n\tif config.Session.NodePath == \"\" {\n\t\tconfig.Session.NodePath = filepath.Join(configDir, \"session\", \"node.session\")\n\t}\n\n}\n\nfunc loadFromFile(config *config.Base, filePath string) error {\n\tif _, err := os.Stat(filePath); os.IsNotExist(err) {\n\t\treturn err\n\t}\n\tdata, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"读取配置文件失败: %v\", err)\n\t}\n\tif err := json.Unmarshal(data, config); err != nil {\n\t\treturn fmt.Errorf(\"解析配置文件失败: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc loadFromEnv(config *config.Base) {\n\tif port := os.Getenv(\"BESTSUB_SERVER_PORT\"); port != \"\" {\n\t\tif p, err := parsePort(port); err == nil {\n\t\t\tconfig.Server.Port = p\n\t\t}\n\t}\n\tif host := os.Getenv(\"BESTSUB_SERVER_HOST\"); host != \"\" {\n\t\tconfig.Server.Host = host\n\t}\n\tif dbPath := os.Getenv(\"BESTSUB_DATABASE_PATH\"); dbPath != \"\" {\n\t\tconfig.Database.Path = dbPath\n\t}\n\tif dbType := os.Getenv(\"BESTSUB_DATABASE_TYPE\"); dbType != \"\" {\n\t\tconfig.Database.Type = dbType\n\t}\n\tif logLevel := os.Getenv(\"BESTSUB_LOG_LEVEL\"); logLevel != \"\" {\n\t\tconfig.Log.Level = logLevel\n\t}\n\tif logOutput := os.Getenv(\"BESTSUB_LOG_OUTPUT\"); logOutput != \"\" {\n\t\tconfig.Log.Output = logOutput\n\t}\n\tif logDir := os.Getenv(\"BESTSUB_LOG_DIR\"); logDir != \"\" {\n\t\tconfig.Log.Path = logDir\n\t}\n\tif jwtSecret := os.Getenv(\"BESTSUB_JWT_SECRET\"); jwtSecret != \"\" {\n\t\tconfig.JWT.Secret = jwtSecret\n\t}\n}\n\nfunc parsePort(portStr string) (int, error) {\n\tport, err := strconv.Atoi(portStr)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"无效的端口号: %s\", portStr)\n\t}\n\tif port <= 0 || port > 65535 {\n\t\treturn 0, fmt.Errorf(\"端口号超出范围: %d\", port)\n\t}\n\treturn port, nil\n}\n\nfunc createDefaultConfig(filePath string) error {\n\tdir := filepath.Dir(filePath)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn fmt.Errorf(\"创建配置目录失败: %v\", err)\n\t}\n\n\tbytes := make([]byte, 32)\n\trand.Read(bytes)\n\tbaseConfig.JWT.Secret = hex.EncodeToString(bytes)\n\n\tdata, err := json.MarshalIndent(baseConfig, \"\", \"    \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"序列化默认配置失败: %v\", err)\n\t}\n\n\tif err := os.WriteFile(filePath, data, 0644); err != nil {\n\t\treturn fmt.Errorf(\"写入配置文件失败: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc validateConfig(config *config.Base) error {\n\tif err := validateServerConfig(&config.Server); err != nil {\n\t\treturn fmt.Errorf(\"服务器配置验证失败: %v\", err)\n\t}\n\n\tif err := validateDatabaseConfig(&config.Database); err != nil {\n\t\treturn fmt.Errorf(\"数据库配置验证失败: %v\", err)\n\t}\n\n\tif err := validateLogConfig(&config.Log); err != nil {\n\t\treturn fmt.Errorf(\"日志配置验证失败: %v\", err)\n\t}\n\n\tif err := validateJWTConfig(&config.JWT); err != nil {\n\t\treturn fmt.Errorf(\"JWT配置验证失败: %v\", err)\n\t}\n\n\tif err := validateSessionConfig(&config.Session); err != nil {\n\t\treturn fmt.Errorf(\"会话配置验证失败: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc validateServerConfig(config *config.ServerConfig) error {\n\tif config.Port <= 0 || config.Port > 65535 {\n\t\treturn fmt.Errorf(\"端口号必须在1-65535范围内，当前值: %d\", config.Port)\n\t}\n\n\tif config.Host == \"\" {\n\t\treturn fmt.Errorf(\"主机地址不能为空\")\n\t}\n\n\tif ip := net.ParseIP(config.Host); ip == nil {\n\t\treturn fmt.Errorf(\"无效的主机地址格式: %s\", config.Host)\n\t}\n\n\treturn nil\n}\n\nfunc validateDatabaseConfig(config *config.DatabaseConfig) error {\n\tif config.Type == \"\" {\n\t\treturn fmt.Errorf(\"数据库类型不能为空\")\n\t}\n\tif config.Path == \"\" {\n\t\treturn fmt.Errorf(\"数据库路径不能为空\")\n\t}\n\tdir := filepath.Dir(config.Path)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn fmt.Errorf(\"无法创建数据库目录 %s: %v\", dir, err)\n\t}\n\tif !utils.IsWritableDir(dir) {\n\t\treturn fmt.Errorf(\"数据库目录 %s 不可写\", dir)\n\t}\n\treturn nil\n}\n\nfunc validateLogConfig(config *config.LogConfig) error {\n\tvalidLevels := []string{\"debug\", \"info\", \"warn\", \"error\"}\n\tif !utils.Contains(validLevels, strings.ToLower(config.Level)) {\n\t\treturn fmt.Errorf(\"无效的日志等级: %s，支持的等级: %v\", config.Level, validLevels)\n\t}\n\n\tvalidOutputs := []string{\"console\", \"file\", \"both\"}\n\tif !utils.Contains(validOutputs, strings.ToLower(config.Output)) {\n\t\treturn fmt.Errorf(\"无效的日志输出方式: %s，支持的方式: %v\", config.Output, validOutputs)\n\t}\n\n\tif config.Output == \"file\" || config.Output == \"both\" {\n\t\tif config.Path == \"\" {\n\t\t\treturn fmt.Errorf(\"日志输出到文件时，文件路径不能为空\")\n\t\t}\n\n\t\tdir := config.Path\n\t\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\t\treturn fmt.Errorf(\"无法创建日志目录 %s: %v\", dir, err)\n\t\t}\n\n\t\tif !utils.IsWritableDir(dir) {\n\t\t\treturn fmt.Errorf(\"日志目录 %s 不可写\", dir)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc validateJWTConfig(config *config.JWTConfig) error {\n\tif config.Secret == \"\" {\n\t\treturn fmt.Errorf(\"JWT密钥不能为空\")\n\t}\n\n\tif len(config.Secret) < 16 {\n\t\treturn fmt.Errorf(\"JWT密钥长度不能少于16个字符，当前长度: %d\", len(config.Secret))\n\t}\n\n\tif strings.Contains(config.Secret, \"change-me\") || config.Secret == \"bestsub-jwt-secret\" {\n\t\treturn fmt.Errorf(\"请修改默认的JWT密钥以确保安全性\")\n\t}\n\n\treturn nil\n}\n\nfunc validateSessionConfig(config *config.SessionConfig) error {\n\tif config.NodePath == \"\" {\n\t\treturn fmt.Errorf(\"节点会话文件路径不能为空\")\n\t}\n\n\tdir := filepath.Dir(config.NodePath)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn fmt.Errorf(\"无法创建会话目录 %s: %v\", dir, err)\n\t}\n\n\tif !utils.IsWritableDir(dir) {\n\t\treturn fmt.Errorf(\"会话目录 %s 不可写\", dir)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/core/check/check.go",
    "content": "package check\n\nimport (\n\t_ \"github.com/bestruirui/bestsub/internal/core/check/checker\"\n\t\"github.com/bestruirui/bestsub/internal/models/check\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t\"github.com/bestruirui/bestsub/internal/utils/desc\"\n)\n\ntype Desc = desc.Data\n\nfunc Get(m string, c string) (check.Instance, error) {\n\treturn register.Get[check.Instance](\"check\", m, c)\n}\n\nfunc GetTypes() []string {\n\treturn register.GetList(\"check\")\n}\n\nfunc GetInfoMap() map[string][]Desc {\n\treturn register.GetInfoMap(\"check\")\n}\n"
  },
  {
    "path": "internal/core/check/checker/alive.go",
    "content": "package checker\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/task\"\n\tcheckModel \"github.com/bestruirui/bestsub/internal/models/check\"\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype Alive struct {\n\tURL         string `json:\"url\" name:\"测试链接\" value:\"https://www.gstatic.com/generate_204\"`\n\tExptectCode int    `json:\"exptect_code\" name:\"期望状态码\" value:\"204\"`\n\tThread      int    `json:\"thread\" name:\"线程数\" value:\"100\"`\n\tTimeout     int    `json:\"timeout\" name:\"超时时间\" value:\"10\" desc:\"单个节点检测的超时时间(s)\"`\n}\ntype Result struct {\n\tAliveCount uint16 `json:\"alive_count\" desc:\"存活节点数量\"`\n\tDeadCount  uint16 `json:\"dead_count\" desc:\"死亡节点数量\"`\n\tDelay      uint16 `json:\"delay\" desc:\"平均延迟\"`\n}\n\nfunc (e *Alive) Init() error {\n\treturn nil\n}\n\nfunc (e *Alive) Run(ctx context.Context, log *log.Logger, subID []uint16) checkModel.Result {\n\tstartTime := time.Now()\n\tvar nodes []nodeModel.Data\n\tvar aliveCount, deadCount, totalDelay int64\n\tif len(subID) == 0 {\n\t\tnodes = node.GetAll()\n\t} else {\n\t\tnodes = *node.GetBySubId(subID)\n\t}\n\tthreads := e.Thread\n\tif threads <= 0 || threads > len(nodes) {\n\t\tthreads = len(nodes)\n\t}\n\tif threads > task.MaxThread() {\n\t\tthreads = task.MaxThread()\n\t}\n\tif threads == 0 || len(nodes) == 0 {\n\t\tlog.Warnf(\"alive check task failed, no nodes\")\n\t\treturn checkModel.Result{\n\t\t\tMsg:      \"no nodes\",\n\t\t\tLastRun:  time.Now(),\n\t\t\tDuration: time.Since(startTime).Milliseconds(),\n\t\t}\n\t}\n\tsem := make(chan struct{}, threads)\n\tdefer close(sem)\n\n\tvar wg sync.WaitGroup\n\tfor _, nd := range nodes {\n\t\tsem <- struct{}{}\n\t\twg.Add(1)\n\t\tn := nd\n\t\ttask.Submit(func() {\n\t\t\tdefer func() {\n\t\t\t\t<-sem\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\tvar raw map[string]any\n\t\t\tif err := yaml.Unmarshal(n.Raw, &raw); err != nil {\n\t\t\t\tlog.Warnf(\"yaml.Unmarshal failed: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstart := time.Now()\n\t\t\talive := e.detect(ctx, raw)\n\t\t\tif alive {\n\t\t\t\tlog.Debugf(\"Node %s is alive ✔\", raw[\"name\"].(string))\n\t\t\t\tatomic.AddInt64(&aliveCount, 1)\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.Alive, true)\n\t\t\t\tn.Info.Delay.Update(uint16(time.Since(start).Milliseconds()))\n\t\t\t\tlog.Debugf(\"Node %s delay: %dms\", raw[\"name\"].(string), n.Info.Delay.Average())\n\t\t\t\tatomic.AddInt64(&totalDelay, int64(n.Info.Delay.Average()))\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"Node %s is dead ✘\", raw[\"name\"].(string))\n\t\t\t\tatomic.AddInt64(&deadCount, 1)\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.Alive, false)\n\t\t\t\tn.Info.Delay.Update(uint16(65535))\n\t\t\t}\n\n\t\t})\n\t}\n\twg.Wait()\n\tavgDelay := int64(0)\n\tif aliveCount > 0 {\n\t\tavgDelay = totalDelay / aliveCount\n\t}\n\tlog.Debugf(\"alive check task end, alive: %d, dead: %d, average delay: %dms\", aliveCount, deadCount, avgDelay)\n\treturn checkModel.Result{\n\t\tMsg:      fmt.Sprintf(\"success, alive: %d, dead: %d, average delay: %dms\", aliveCount, deadCount, avgDelay),\n\t\tLastRun:  time.Now(),\n\t\tDuration: time.Since(startTime).Milliseconds(),\n\t\tExtra: map[string]any{\n\t\t\t\"alive\": aliveCount,\n\t\t\t\"dead\":  deadCount,\n\t\t\t\"delay\": avgDelay,\n\t\t},\n\t}\n}\n\nfunc (e *Alive) detect(ctx context.Context, raw map[string]any) bool {\n\tclient := mihomo.Proxy(raw)\n\tif client == nil {\n\t\treturn false\n\t}\n\tclient.Timeout = time.Duration(e.Timeout) * time.Second\n\tdefer client.Release()\n\trequest, err := http.NewRequestWithContext(ctx, \"GET\", e.URL, nil)\n\tif err != nil {\n\t\treturn false\n\t}\n\tresponse, err := client.Do(request)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer response.Body.Close()\n\treturn response.StatusCode == e.ExptectCode\n}\n\nfunc init() {\n\tregister.Check(&Alive{})\n}\n"
  },
  {
    "path": "internal/core/check/checker/country.go",
    "content": "package checker\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/task\"\n\tcheckModel \"github.com/bestruirui/bestsub/internal/models/check\"\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\t\"github.com/bestruirui/bestsub/internal/modules/country\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype Country struct {\n\tThread  int `json:\"thread\" name:\"线程数\" value:\"100\"`\n\tTimeout int `json:\"timeout\" name:\"超时时间\" value:\"10\" desc:\"单个节点检测的超时时间(s)\"`\n}\n\nfunc (e *Country) Init() error {\n\treturn nil\n}\n\nfunc (e *Country) Run(ctx context.Context, log *log.Logger, subID []uint16) checkModel.Result {\n\tstartTime := time.Now()\n\tvar nodes []nodeModel.Data\n\tif len(subID) == 0 {\n\t\tnodes = node.GetAll()\n\t} else {\n\t\tnodes = *node.GetBySubId(subID)\n\t}\n\tthreads := e.Thread\n\tif threads <= 0 || threads > len(nodes) {\n\t\tthreads = len(nodes)\n\t}\n\tif threads > task.MaxThread() {\n\t\tthreads = task.MaxThread()\n\t}\n\tif threads == 0 {\n\t\tlog.Warnf(\"country check task failed, no nodes\")\n\t\treturn checkModel.Result{\n\t\t\tMsg:      \"no nodes\",\n\t\t\tLastRun:  time.Now(),\n\t\t\tDuration: time.Since(startTime).Milliseconds(),\n\t\t}\n\t}\n\tsem := make(chan struct{}, threads)\n\tdefer close(sem)\n\n\tvar wg sync.WaitGroup\n\tfor _, nd := range nodes {\n\t\tif nd.Info.AliveStatus&nodeModel.Country != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tsem <- struct{}{}\n\t\twg.Add(1)\n\t\tn := nd\n\t\ttask.Submit(func() {\n\t\t\tdefer func() {\n\t\t\t\t<-sem\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\tvar raw map[string]any\n\t\t\tif err := yaml.Unmarshal(n.Raw, &raw); err != nil {\n\t\t\t\tlog.Warnf(\"yaml.Unmarshal failed: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclient := mihomo.Proxy(raw)\n\t\t\tif client == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclient.Timeout = time.Duration(e.Timeout) * time.Second\n\t\t\tdefer client.Release()\n\t\t\tcountryCode := country.GetCode(ctx, client.Client)\n\t\t\tif countryCode != \"\" {\n\t\t\t\tn.Info.Country = countryCode\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.Country, true)\n\t\t\t} else {\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.Country, false)\n\t\t\t}\n\t\t})\n\t}\n\twg.Wait()\n\treturn checkModel.Result{\n\t\tMsg:      \"success\",\n\t\tLastRun:  time.Now(),\n\t\tDuration: time.Since(startTime).Milliseconds(),\n\t}\n}\n\nfunc init() {\n\tregister.Check(&Country{})\n}\n"
  },
  {
    "path": "internal/core/check/checker/speed.go",
    "content": "package checker\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/system\"\n\t\"github.com/bestruirui/bestsub/internal/core/task\"\n\tcheckModel \"github.com/bestruirui/bestsub/internal/models/check\"\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nconst mbToBytes = 1024 * 1024\n\ntype Speed struct {\n\tThread  int `json:\"thread\" name:\"线程数\" value:\"5\"`\n\tTimeout int `json:\"timeout\" name:\"超时时间\" value:\"60\" desc:\"单个节点检测的超时时间(s)\"`\n\n\tDownload      bool   `json:\"download\" name:\"下载测试\" value:\"true\"`\n\tDownloadSkip  bool   `json:\"download_skip\" name:\"是否跳过已经有下载速度的节点\" value:\"false\"`\n\tDownloadUrl   string `json:\"download_url\" name:\"测试链接\" value:\"https://speed.cloudflare.com/__down?bytes=104857600\" desc:\"最好自定义一个测试链接,部分节点可能屏蔽此默认链接\"`\n\tDownloadSize  int64  `json:\"download_size\" name:\"下载大小\" value:\"100\" desc:\"到达指定大小后停止测速(MB)\"`\n\tDownloadSpeed int64  `json:\"download_speed\" name:\"下载速度\" value:\"1\" desc:\"下载速度达到指定值并且达到指定个数后停止测速(KB/s)\"`\n\tDownloadCount int    `json:\"download_count\" name:\"节点个数\" value:\"5\" desc:\"符合下载速度的节点个数,满足后停止测试\"`\n\n\tUpload      bool   `json:\"upload\" name:\"上传测试\" value:\"false\"`\n\tUploadSkip  bool   `json:\"upload_skip\" name:\"是否跳过已经有上传速度的节点\" value:\"false\"`\n\tUploadUrl   string `json:\"upload_url\" name:\"上传链接\" value:\"https://speed.cloudflare.com/__up\" desc:\"最好自定义一个测试链接,部分节点可能屏蔽此默认链接\"`\n\tUploadSize  int64  `json:\"upload_size\" name:\"上传大小\" value:\"100\" desc:\"到达指定大小后停止测速(MB)\"`\n\tUploadSpeed int64  `json:\"upload_speed\" name:\"上传速度\" value:\"1\" desc:\"上传速度达到指定值并且达到指定个数后停止测速(KB/s)\"`\n\tUploadCount int    `json:\"upload_count\" name:\"节点个数\" value:\"5\" desc:\"符合上传速度的节点个数,满足后停止测试\"`\n}\n\nfunc (e *Speed) Init() error {\n\treturn nil\n}\n\nfunc (e *Speed) Run(ctx context.Context, log *log.Logger, subID []uint16) checkModel.Result {\n\tstartTime := time.Now()\n\tvar nodes []nodeModel.Data\n\tif len(subID) == 0 {\n\t\tnodes = node.GetAll()\n\t} else {\n\t\tnodes = *node.GetBySubId(subID)\n\t}\n\tthreads := e.Thread\n\tif threads <= 0 || threads > len(nodes) {\n\t\tthreads = len(nodes)\n\t}\n\tif threads > task.MaxThread() {\n\t\tthreads = task.MaxThread()\n\t}\n\tif threads == 0 {\n\t\tlog.Warnf(\"speed check task failed, no nodes\")\n\t\treturn checkModel.Result{\n\t\t\tMsg:      \"no nodes\",\n\t\t\tLastRun:  time.Now(),\n\t\t\tDuration: time.Since(startTime).Milliseconds(),\n\t\t}\n\t}\n\tsem := make(chan struct{}, threads)\n\tdefer close(sem)\n\tvar downloadCount, uploadCount int\n\n\tvar wg sync.WaitGroup\n\tfor _, nd := range nodes {\n\t\tsem <- struct{}{}\n\t\twg.Add(1)\n\t\tn := nd\n\t\ttask.Submit(func() {\n\t\t\tdefer func() {\n\t\t\t\t<-sem\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\tvar raw map[string]any\n\t\t\tif err := yaml.Unmarshal(n.Raw, &raw); err != nil {\n\t\t\t\tlog.Warnf(\"yaml.Unmarshal failed: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclient := mihomo.Proxy(raw)\n\t\t\tif client == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer client.Release()\n\t\t\tclient.Timeout = time.Duration(e.Timeout) * time.Second\n\t\t\tif e.Download && downloadCount < e.DownloadCount && (!e.DownloadSkip || n.Info.SpeedDown.Average() == 0) {\n\t\t\t\tspeed := e.download(ctx, client.Client)\n\t\t\t\tif speed > 0 {\n\t\t\t\t\tn.Info.SpeedDown.Update(uint32(speed))\n\t\t\t\t\tlog.Debugf(\"node %s download speed: %d\", raw[\"name\"], speed)\n\t\t\t\t}\n\t\t\t\tif speed > e.DownloadSpeed {\n\t\t\t\t\tdownloadCount++\n\t\t\t\t}\n\t\t\t}\n\t\t\tclient.Timeout = time.Duration(e.Timeout) * time.Second\n\t\t\tif e.Upload && uploadCount < e.UploadCount && (!e.UploadSkip || n.Info.SpeedUp.Average() == 0) {\n\t\t\t\tspeed := e.upload(ctx, client.Client)\n\t\t\t\tif speed > 0 {\n\t\t\t\t\tn.Info.SpeedUp.Update(uint32(speed))\n\t\t\t\t\tlog.Debugf(\"node %s upload speed: %d\", raw[\"name\"], speed)\n\t\t\t\t}\n\t\t\t\tif speed > e.UploadSpeed {\n\t\t\t\t\tuploadCount++\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\twg.Wait()\n\treturn checkModel.Result{\n\t\tMsg:      fmt.Sprintf(\"success, download count: %d, upload count: %d\", downloadCount, uploadCount),\n\t\tLastRun:  time.Now(),\n\t\tDuration: time.Since(startTime).Milliseconds(),\n\t}\n}\nfunc (e *Speed) download(ctx context.Context, client *http.Client) int64 {\n\trequest, err := http.NewRequestWithContext(ctx, \"GET\", e.DownloadUrl, nil)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tresponse, err := client.Do(request)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tdefer response.Body.Close()\n\tstartTime := time.Now()\n\n\tlimitReader := io.LimitReader(response.Body, e.DownloadSize*mbToBytes)\n\tbytes, _ := io.Copy(io.Discard, limitReader)\n\tduration := time.Since(startTime).Milliseconds()\n\tif duration <= 0 || bytes <= 0 {\n\t\treturn 0\n\t}\n\tsystem.AddDownloadBytes(uint64(bytes))\n\treturn bytes / duration\n}\n\nfunc (e *Speed) upload(ctx context.Context, client *http.Client) int64 {\n\tuploadBytes := e.UploadSize * mbToBytes\n\treader := &trackingZeroReader{remaining: uploadBytes}\n\trequest, err := http.NewRequestWithContext(ctx, \"POST\", e.UploadUrl, reader)\n\tif err != nil {\n\t\treturn 0\n\t}\n\trequest.ContentLength = uploadBytes\n\tstartTime := time.Now()\n\tresponse, err := client.Do(request)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tdefer response.Body.Close()\n\tif response.StatusCode < 200 || response.StatusCode >= 300 {\n\t\treturn 0\n\t}\n\tio.Copy(io.Discard, response.Body)\n\tduration := time.Since(startTime).Milliseconds()\n\tif duration <= 0 || reader.bytesRead <= 0 {\n\t\treturn 0\n\t}\n\tsystem.AddUploadBytes(uint64(reader.bytesRead))\n\treturn reader.bytesRead / duration\n}\n\ntype trackingZeroReader struct {\n\tremaining int64\n\tbytesRead int64\n}\n\nfunc (r *trackingZeroReader) Read(p []byte) (n int, err error) {\n\tif r.remaining <= 0 {\n\t\treturn 0, io.EOF\n\t}\n\n\tif int64(len(p)) > r.remaining {\n\t\tn = int(r.remaining)\n\t} else {\n\t\tn = len(p)\n\t}\n\n\tclear(p[:n])\n\n\tr.remaining -= int64(n)\n\tr.bytesRead += int64(n)\n\treturn n, nil\n}\n\nfunc init() {\n\tregister.Check(&Speed{})\n}\n"
  },
  {
    "path": "internal/core/check/checker/tiktok.go",
    "content": "package checker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/task\"\n\t\"github.com/bestruirui/bestsub/internal/models/check\"\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/bestruirui/bestsub/internal/utils/ua\"\n)\n\ntype TikTok struct {\n\tThread  int `json:\"thread\" name:\"线程数\" value:\"200\"`\n\tTimeout int `json:\"timeout\" name:\"超时时间\" value:\"10\" desc:\"单个节点检测的超时时间(s)\"`\n}\n\nfunc (e *TikTok) Init() error {\n\treturn nil\n}\n\nfunc (e *TikTok) Run(ctx context.Context, log *log.Logger, subID []uint16) check.Result {\n\tstartTime := time.Now()\n\tvar nodes []nodeModel.Data\n\n\tif len(subID) == 0 {\n\t\tnodes = node.GetAll()\n\t} else {\n\t\tnodes = *node.GetBySubId(subID)\n\t}\n\n\tthreads := e.Thread\n\tif threads <= 0 || threads > len(nodes) {\n\t\tthreads = len(nodes)\n\t}\n\tif threads > task.MaxThread() {\n\t\tthreads = task.MaxThread()\n\t}\n\tif threads == 0 || len(nodes) == 0 {\n\t\tlog.Warnf(\"tiktok check task failed, no nodes\")\n\t\treturn check.Result{\n\t\t\tMsg:      \"no nodes\",\n\t\t\tLastRun:  time.Now(),\n\t\t\tDuration: time.Since(startTime).Milliseconds(),\n\t\t}\n\t}\n\n\tsem := make(chan struct{}, threads)\n\tdefer close(sem)\n\n\tvar wg sync.WaitGroup\n\tfor _, nd := range nodes {\n\t\tsem <- struct{}{}\n\t\twg.Add(1)\n\t\tn := nd\n\t\ttask.Submit(func() {\n\t\t\tdefer func() {\n\t\t\t\t<-sem\n\t\t\t\twg.Done()\n\t\t\t}()\n\n\t\t\tvar raw map[string]any\n\t\t\tif err := yaml.Unmarshal(n.Raw, &raw); err != nil {\n\t\t\t\tlog.Warnf(\"yaml.Unmarshal failed: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tswitch e.detectTikTok(ctx, raw) {\n\t\t\tcase 1:\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.TikTok, true)\n\t\t\tcase 2:\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.TikTokIDC, true)\n\t\t\tdefault:\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.TikTok, false)\n\t\t\t\tn.Info.SetAliveStatus(nodeModel.TikTokIDC, false)\n\t\t\t}\n\t\t})\n\t}\n\twg.Wait()\n\n\tlog.Debugf(\"tiktok check task end\")\n\treturn check.Result{\n\t\tMsg:      \"success\",\n\t\tLastRun:  time.Now(),\n\t\tDuration: time.Since(startTime).Milliseconds(),\n\t}\n}\n\nfunc (e *TikTok) detectTikTok(ctx context.Context, raw map[string]any) uint8 {\n\tclient := mihomo.Proxy(raw)\n\tif client == nil {\n\t\treturn 0\n\t}\n\tclient.Timeout = time.Duration(e.Timeout) * time.Second\n\tdefer client.Release()\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", \"https://www.tiktok.com/\", nil)\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\tua.SetHeader(req)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tif extractRegion(body) {\n\t\treturn 1\n\t}\n\n\treq, err = http.NewRequestWithContext(ctx, \"GET\", \"https://www.tiktok.com/api/passport/web/region/get/\", nil)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tua.SetHeader(req)\n\treq.Header.Set(\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\")\n\treq.Header.Set(\"Accept-Language\", \"en\")\n\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tdefer resp.Body.Close()\n\tbody, err = io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tif extractRegion(body) {\n\t\treturn 2\n\t}\n\n\treturn 0\n}\n\nfunc extractRegion(html []byte) bool {\n\treturn bytes.Contains(html, []byte(`\"region\":`))\n}\n\nfunc init() {\n\tregister.Check(&TikTok{})\n}\n"
  },
  {
    "path": "internal/core/cron/check.go",
    "content": "package cron\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/check\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tcheckModel \"github.com/bestruirui/bestsub/internal/models/check\"\n\t\"github.com/bestruirui/bestsub/internal/utils/generic\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/robfig/cron/v3\"\n)\n\nvar checkFunc = generic.MapOf[uint16, cronFunc]{}\nvar checkScheduled = generic.MapOf[uint16, cron.EntryID]{}\nvar checkRunning = generic.MapOf[uint16, context.CancelFunc]{}\n\nfunc CheckLoad() {\n\tcheckData, err := op.GetCheckList()\n\tif err != nil {\n\t\tlog.Errorf(\"failed to load sub data: %v\", err)\n\t\treturn\n\t}\n\tfor _, data := range checkData {\n\t\tCheckAdd(&data)\n\t}\n}\nfunc CheckAdd(data *checkModel.Data) error {\n\tvar taskConfig checkModel.Task\n\tif err := json.Unmarshal([]byte(data.Task), &taskConfig); err != nil {\n\t\tlog.Errorf(\"failed to unmarshal task config: %v\", err)\n\t\treturn err\n\t}\n\tcheckFunc.Store(data.ID, cronFunc{\n\t\tfn: func() {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(taskConfig.Timeout)*time.Minute)\n\t\t\tcheckRunning.Store(data.ID, cancel)\n\t\t\tdefer func() {\n\t\t\t\tcancel()\n\t\t\t\tcheckRunning.Delete(data.ID)\n\t\t\t}()\n\t\t\tlogger, err := log.NewTaskLogger(\"check\", data.ID, taskConfig.LogLevel, taskConfig.LogWriteFile)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to create logger: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgo func() {\n\t\t\t\t<-ctx.Done()\n\t\t\t\tlogger.Close()\n\t\t\t}()\n\t\t\tchecker, err := check.Get(taskConfig.Type, data.Config)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to get execer: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Infof(\"%s task %d start\", taskConfig.Type, data.ID)\n\t\t\tvar result checkModel.Result\n\t\t\tif taskConfig.SubIdExclude {\n\t\t\t\tresult = checker.Run(ctx, logger, node.GetBySubIdExclude(taskConfig.SubID))\n\t\t\t} else {\n\t\t\t\tresult = checker.Run(ctx, logger, taskConfig.SubID)\n\t\t\t}\n\t\t\tlog.Infof(\"%s task %d end\", taskConfig.Type, data.ID)\n\t\t\top.UpdateCheckResult(data.ID, result)\n\t\t\tnode.RefreshInfo()\n\t\t},\n\t\tcronExpr: taskConfig.CronExpr,\n\t})\n\tif data.Enable {\n\t\tCheckEnable(data.ID)\n\t}\n\treturn nil\n}\nfunc CheckUpdate(data *checkModel.Data) error {\n\tCheckRemove(data.ID)\n\tCheckAdd(data)\n\treturn nil\n}\n\nfunc CheckRun(id uint16) error {\n\tif ft, ok := checkFunc.Load(id); ok {\n\t\tgo ft.fn()\n\t\treturn nil\n\t} else {\n\t\treturn fmt.Errorf(\"check task %d not found\", id)\n\t}\n}\n\nfunc CheckEnable(id uint16) error {\n\tif _, ok := checkScheduled.Load(id); ok {\n\t\tlog.Warnf(\"check task %d already scheduled\", id)\n\t\treturn nil\n\t}\n\tif ft, ok := checkFunc.Load(id); ok {\n\t\tentryID, err := scheduler.AddFunc(ft.cronExpr, ft.fn)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to add task: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tcheckScheduled.Store(id, entryID)\n\t}\n\treturn nil\n}\nfunc CheckDisable(id uint16) error {\n\tif entryID, ok := checkScheduled.Load(id); ok {\n\t\tscheduler.Remove(entryID)\n\t\tcheckScheduled.Delete(id)\n\t\tif cancel, ok := checkRunning.Load(id); ok {\n\t\t\tcancel()\n\t\t\tcheckRunning.Delete(id)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc CheckRemove(id uint16) error {\n\tif entryID, ok := checkScheduled.Load(id); ok {\n\t\tscheduler.Remove(entryID)\n\t\tcheckScheduled.Delete(id)\n\t\tcheckFunc.Delete(id)\n\t\tif cancel, ok := checkRunning.Load(id); ok {\n\t\t\tcancel()\n\t\t\tcheckRunning.Delete(id)\n\t\t}\n\t}\n\treturn nil\n}\nfunc CheckStop(id uint16) error {\n\tif cancel, ok := checkRunning.Load(id); ok {\n\t\tcancel()\n\t\tcheckRunning.Delete(id)\n\t}\n\treturn nil\n}\nfunc CheckStatus(id uint16) string {\n\tif _, ok := checkRunning.Load(id); ok {\n\t\treturn RunningStatus\n\t}\n\tif _, ok := checkScheduled.Load(id); ok {\n\t\treturn ScheduledStatus\n\t}\n\tif _, ok := checkFunc.Load(id); ok {\n\t\treturn PendingStatus\n\t}\n\treturn DisabledStatus\n}\n"
  },
  {
    "path": "internal/core/cron/cron.go",
    "content": "package cron\n\nimport (\n\t\"time\"\n\n\t\"github.com/robfig/cron/v3\"\n)\n\ntype cronFunc struct {\n\tfn       func()\n\tcronExpr string\n}\n\nvar scheduler = cron.New(cron.WithLocation(time.Local))\n\nconst (\n\tRunningStatus   = \"running\"\n\tScheduledStatus = \"scheduled\"\n\tPendingStatus   = \"pending\"\n\tDisabledStatus  = \"disabled\"\n)\n\nfunc Start() {\n\tscheduler.Start()\n}\n\nfunc Stop() {\n\tscheduler.Stop()\n}\n"
  },
  {
    "path": "internal/core/cron/fetch.go",
    "content": "package cron\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/fetch\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tsubModel \"github.com/bestruirui/bestsub/internal/models/sub\"\n\t\"github.com/bestruirui/bestsub/internal/utils/generic\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/robfig/cron/v3\"\n)\n\nvar fetchFunc = generic.MapOf[uint16, cronFunc]{}\nvar fetchScheduled = generic.MapOf[uint16, cron.EntryID]{}\nvar fetchRunning = generic.MapOf[uint16, context.CancelFunc]{}\n\nfunc FetchLoad() {\n\tsubData, err := op.GetSubList(context.Background())\n\tif err != nil {\n\t\tlog.Errorf(\"failed to load sub data: %v\", err)\n\t\treturn\n\t}\n\tfor _, data := range subData {\n\t\tFetchAdd(&data)\n\t}\n}\n\nfunc FetchAdd(data *subModel.Data) error {\n\tfetchFunc.Store(data.ID, cronFunc{\n\t\tfn: func() {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\t\tfetchRunning.Store(data.ID, cancel)\n\t\t\tdefer func() {\n\t\t\t\tcancel()\n\t\t\t\tfetchRunning.Delete(data.ID)\n\t\t\t}()\n\t\t\tresult := fetch.Do(ctx, data.ID, data.Config)\n\t\t\top.UpdateSubResult(ctx, data.ID, result)\n\t\t\tsub, err := op.GetSubByID(ctx, data.ID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"failed to get sub by id: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !sub.Enable {\n\t\t\t\tFetchDisable(data.ID)\n\t\t\t\tlog.Infof(\"fetch task %d auto disable\", data.ID)\n\t\t\t}\n\t\t},\n\t\tcronExpr: data.CronExpr,\n\t})\n\tif data.Enable {\n\t\tFetchEnable(data.ID)\n\t}\n\treturn nil\n}\n\nfunc FetchRun(subID uint16) subModel.Result {\n\tif ft, ok := fetchFunc.Load(subID); ok {\n\t\tft.fn()\n\t} else {\n\t\tlog.Warnf(\"fetch task %d not found\", subID)\n\t\treturn subModel.Result{\n\t\t\tMsg:     \"fetch task not found\",\n\t\t\tLastRun: time.Now(),\n\t\t}\n\t}\n\tsub, err := op.GetSubByID(context.Background(), subID)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to get sub by id: %v\", err)\n\t\treturn subModel.Result{\n\t\t\tMsg:     \"fetch task not found\",\n\t\t\tLastRun: time.Now(),\n\t\t}\n\t}\n\tvar result subModel.Result\n\tif err := json.Unmarshal([]byte(sub.Result), &result); err != nil {\n\t\tlog.Warnf(\"failed to unmarshal sub result: %v\", err)\n\t\treturn subModel.Result{\n\t\t\tMsg:     \"failed to unmarshal sub result\",\n\t\t\tLastRun: time.Now(),\n\t\t}\n\t}\n\treturn result\n}\n\nfunc FetchEnable(subID uint16) error {\n\tif _, ok := fetchScheduled.Load(subID); ok {\n\t\tlog.Warnf(\"fetch task %d already scheduled\", subID)\n\t\treturn nil\n\t}\n\tif ft, ok := fetchFunc.Load(subID); ok {\n\t\tentryID, err := scheduler.AddFunc(ft.cronExpr,\n\t\t\tfunc() {\n\t\t\t\ttime.Sleep(time.Duration(rand.Intn(100)) * time.Second)\n\t\t\t\tft.fn()\n\t\t\t})\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to add task: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tfetchScheduled.Store(subID, entryID)\n\t}\n\treturn nil\n}\nfunc FetchDisable(subID uint16) error {\n\tif entryID, ok := fetchScheduled.Load(subID); ok {\n\t\tscheduler.Remove(entryID)\n\t\tfetchScheduled.Delete(subID)\n\t\tif cancel, ok := fetchRunning.Load(subID); ok {\n\t\t\tcancel()\n\t\t\tfetchRunning.Delete(subID)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc FetchRemove(subID uint16) error {\n\tif entryID, ok := fetchScheduled.Load(subID); ok {\n\t\tscheduler.Remove(entryID)\n\t\tfetchScheduled.Delete(subID)\n\t\tfetchFunc.Delete(subID)\n\t\tif cancel, ok := fetchRunning.Load(subID); ok {\n\t\t\tcancel()\n\t\t\tfetchRunning.Delete(subID)\n\t\t}\n\t}\n\treturn nil\n}\nfunc FetchStop(subID uint16) error {\n\tif cancel, ok := fetchRunning.Load(subID); ok {\n\t\tcancel()\n\t\tfetchRunning.Delete(subID)\n\t}\n\treturn nil\n}\nfunc FetchUpdate(data *subModel.Data) error {\n\tFetchRemove(data.ID)\n\tFetchAdd(data)\n\treturn nil\n}\nfunc FetchStatus(subID uint16) string {\n\tif _, ok := fetchRunning.Load(subID); ok {\n\t\treturn RunningStatus\n\t}\n\tif _, ok := fetchScheduled.Load(subID); ok {\n\t\treturn ScheduledStatus\n\t}\n\tif _, ok := fetchFunc.Load(subID); ok {\n\t\treturn PendingStatus\n\t}\n\treturn DisabledStatus\n}\n"
  },
  {
    "path": "internal/core/fetch/fetch.go",
    "content": "package fetch\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/subconv\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\tsubModel \"github.com/bestruirui/bestsub/internal/models/sub\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc Do(ctx context.Context, subID uint16, config string) subModel.Result {\n\tstartTime := time.Now()\n\tretry := 0\n\n\tvar subConfig subModel.Config\n\tif err := json.Unmarshal([]byte(config), &subConfig); err != nil {\n\t\tlog.Warnf(\"fetch task %d failed: %v\", subID, err)\n\t\treturn createFailureResult(err.Error(), startTime)\n\t}\n\n\tlog.Debugf(\"fetch task %d started\", subID)\n\n\tclient := mihomo.Default(subConfig.Proxy)\n\tif client == nil {\n\t\tlog.Warnf(\"fetch task %d failed: proxy config error\", subID)\n\t\treturn createFailureResult(\"proxy config error\", startTime)\n\t}\n\tdefer client.Release()\n\tfor retry < 3 {\n\t\ttime.Sleep(time.Duration(retry) * time.Second)\n\t\tretry++\n\t\tclient.Timeout = time.Duration(subConfig.Timeout) * time.Second\n\n\t\treq, err := http.NewRequestWithContext(ctx, \"GET\", subConfig.Url, nil)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"fetch task %d failed: %v\", subID, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tresp, err := client.Do(req)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"fetch task %d failed: %v\", subID, err)\n\t\t\tcontinue\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tcontent, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"fetch task %d failed: %v\", subID, err)\n\t\t\tcontinue\n\t\t}\n\t\tcontentStr := subconv.ConvertData(string(content), \"mihomo\")\n\t\tcontent = []byte(contentStr)\n\n\t\tglobalProtocolFilterEnable := op.GetSettingBool(setting.NODE_PROTOCOL_FILTER_ENABLE)\n\t\tglobalProtocolFilterMode := op.GetSettingBool(setting.NODE_PROTOCOL_FILTER_MODE)\n\t\tglobalProtocolFilter := strings.Split(op.GetSettingStr(setting.NODE_PROTOCOL_FILTER), \",\")\n\n\t\tvar nodes []nodeModel.Base\n\t\tvar unique nodeModel.UniqueKey\n\t\tlines := bytes.Split(content, []byte(\"\\n\"))\n\t\tlines = lines[1:]\n\t\tfor _, line := range lines {\n\t\t\tif len(line) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tline = line[4:]\n\t\t\tif err := yaml.Unmarshal(line, &unique); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif subConfig.ProtocolFilterEnable {\n\t\t\t\tif subConfig.ProtocolFilterMode {\n\t\t\t\t\tif !slices.Contains(subConfig.ProtocolFilter, unique.Type) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif slices.Contains(subConfig.ProtocolFilter, unique.Type) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif globalProtocolFilterEnable {\n\t\t\t\t\tif globalProtocolFilterMode {\n\t\t\t\t\t\tif !slices.Contains(globalProtocolFilter, unique.Type) {\n\t\t\t\t\t\t\tlog.Debugf(\"全局协议过滤启用,协议包含模式 丢弃协议: %v\", unique.Type)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif slices.Contains(globalProtocolFilter, unique.Type) {\n\t\t\t\t\t\t\tlog.Debugf(\"全局协议过滤启用,协议排除模式 丢弃协议: %v\", unique.Type)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tnodes = append(nodes, nodeModel.Base{\n\t\t\t\tRaw:       line,\n\t\t\t\tSubId:     subID,\n\t\t\t\tUniqueKey: unique.Gen(),\n\t\t\t})\n\t\t}\n\n\t\tcount := len(nodes)\n\n\t\tnode.Add(&nodes)\n\n\t\tlog.Infof(\"fetch task %d completed, raw node count: %d,  duration: %dms\",\n\t\t\tsubID, count, uint16(time.Since(startTime).Milliseconds()))\n\n\t\treturn createSuccessResult(uint32(count), startTime, count == 0)\n\t}\n\treturn createFailureResult(\"fetch task failed\", startTime)\n}\nfunc createFailureResult(msg string, startTime time.Time) subModel.Result {\n\treturn subModel.Result{\n\t\tSuccess:  0,\n\t\tFail:     1,\n\t\tMsg:      msg,\n\t\tLastRun:  time.Now(),\n\t\tDuration: uint16(time.Since(startTime).Milliseconds()),\n\t}\n}\n\nfunc createSuccessResult(count uint32, startTime time.Time, nodeNull bool) subModel.Result {\n\tnodeNullCount := uint16(0)\n\tif nodeNull {\n\t\tnodeNullCount = 1\n\t}\n\treturn subModel.Result{\n\t\tSuccess:       1,\n\t\tFail:          0,\n\t\tNodeNullCount: nodeNullCount,\n\t\tMsg:           \"sub updated successfully\",\n\t\tRawCount:      count,\n\t\tLastRun:       time.Now(),\n\t\tDuration:      uint16(time.Since(startTime).Milliseconds()),\n\t}\n}\n"
  },
  {
    "path": "internal/core/mihomo/mihomo.go",
    "content": "package mihomo\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/metacubex/mihomo/adapter\"\n\t\"github.com/metacubex/mihomo/constant\"\n)\n\ntype HC struct {\n\t*http.Client\n\tproxy constant.Proxy\n}\n\nvar clientPool = sync.Pool{\n\tNew: func() interface{} {\n\t\treturn &http.Client{\n\t\t\tTimeout: 300 * time.Second,\n\t\t}\n\t},\n}\n\nvar transportPool = sync.Pool{\n\tNew: func() interface{} {\n\t\treturn &http.Transport{\n\t\t\tDisableKeepAlives:     true,\n\t\t\tTLSHandshakeTimeout:   30 * time.Second,\n\t\t\tExpectContinueTimeout: 10 * time.Second,\n\t\t\tResponseHeaderTimeout: 30 * time.Second,\n\t\t}\n\t},\n}\n\nfunc parsePort(portStr string) (uint16, error) {\n\tport, err := strconv.ParseUint(portStr, 10, 16)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint16(port), nil\n}\n\nfunc Default(useProxy bool) *HC {\n\tif !useProxy || !op.GetSettingBool(setting.PROXY_ENABLE) {\n\t\treturn direct()\n\t}\n\tproxyUrl := op.GetSettingStr(setting.PROXY_URL)\n\tif proxyUrl == \"\" {\n\t\tlog.Warnf(\"proxy url is empty\")\n\t\treturn direct()\n\t}\n\n\tparsed, err := url.Parse(proxyUrl)\n\tif err != nil {\n\t\tlog.Warnf(\"parse proxy url failed: %v\", err)\n\t\treturn direct()\n\t}\n\n\thost, portStr, err := net.SplitHostPort(parsed.Host)\n\tif err != nil {\n\t\tlog.Warnf(\"split host port failed: %v\", err)\n\t\treturn direct()\n\t}\n\n\tportInt, err := parsePort(portStr)\n\tif err != nil {\n\t\tlog.Warnf(\"parse port failed: %v\", err)\n\t\treturn direct()\n\t}\n\n\tproxyConfig := map[string]any{\n\t\t\"name\":     \"proxy\",\n\t\t\"server\":   host,\n\t\t\"port\":     portInt,\n\t\t\"username\": parsed.User.Username(),\n\t}\n\tif password, ok := parsed.User.Password(); ok {\n\t\tproxyConfig[\"password\"] = password\n\t}\n\tswitch parsed.Scheme {\n\tcase \"socks5\":\n\t\tproxyConfig[\"type\"] = \"socks5\"\n\tcase \"http\":\n\t\tproxyConfig[\"type\"] = \"http\"\n\tcase \"https\":\n\t\tproxyConfig[\"type\"] = \"http\"\n\t\tproxyConfig[\"tls\"] = true\n\tdefault:\n\t\tlog.Warnf(\"unsupported proxy scheme: %s\", parsed.Scheme)\n\t\treturn direct()\n\t}\n\treturn Proxy(proxyConfig)\n}\n\nfunc direct() *HC {\n\tvar directProxy = map[string]any{\n\t\t\"name\": \"direct\",\n\t\t\"type\": \"direct\",\n\t}\n\treturn Proxy(directProxy)\n}\n\nfunc Proxy(raw map[string]any) *HC {\n\tif raw == nil {\n\t\tlog.Warnf(\"proxy config is nil\")\n\t\treturn nil\n\t}\n\tproxy, err := adapter.ParseProxy(raw)\n\tif err != nil {\n\t\tif proxy != nil {\n\t\t\tproxy.Close()\n\t\t}\n\t\tlog.Debugf(\"parse proxy failed: %v raw: %v\", err, raw)\n\t\treturn nil\n\t}\n\n\tclient := clientPool.Get().(*http.Client)\n\ttransport := transportPool.Get().(*http.Transport)\n\n\ttransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\thost, portStr, err := net.SplitHostPort(addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tu16Port, err := parsePort(portStr)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"parse port failed, using port 0: %v\", err)\n\t\t\tu16Port = 0\n\t\t}\n\n\t\treturn proxy.DialContext(ctx, &constant.Metadata{\n\t\t\tHost:    host,\n\t\t\tDstPort: u16Port,\n\t\t})\n\t}\n\n\tclient.Transport = transport\n\treturn &HC{Client: client, proxy: proxy}\n}\n\nfunc (h *HC) Release() {\n\tif h.Client == nil {\n\t\treturn\n\t}\n\tif h.proxy != nil {\n\t\th.proxy.Close()\n\t\th.proxy = nil\n\t}\n\tif transport, ok := h.Transport.(*http.Transport); ok {\n\t\ttransport.DialContext = nil\n\t\ttransport.TLSClientConfig = nil\n\t\ttransport.Proxy = nil\n\t\ttransport.CloseIdleConnections()\n\t\ttransportPool.Put(transport)\n\t}\n\th.Transport = nil\n\th.Timeout = 300 * time.Second\n\th.CheckRedirect = nil\n\th.Jar = nil\n\tclientPool.Put(h.Client)\n}\n"
  },
  {
    "path": "internal/core/node/exist.go",
    "content": "package node\n\nimport \"sync\"\n\ntype exist struct {\n\tmu   sync.RWMutex\n\tdata map[uint64]struct{}\n}\n\nfunc NewExist(size int) *exist {\n\treturn &exist{data: make(map[uint64]struct{}, size)}\n}\n\nfunc (k *exist) Exist(key uint64) bool {\n\tk.mu.RLock()\n\t_, exists := k.data[key]\n\tk.mu.RUnlock()\n\treturn exists\n}\n\nfunc (k *exist) Add(key uint64) {\n\tk.mu.Lock()\n\tk.data[key] = struct{}{}\n\tk.mu.Unlock()\n}\nfunc (k *exist) Remove(key uint64) {\n\tk.mu.Lock()\n\tdelete(k.data, key)\n\tk.mu.Unlock()\n}\n"
  },
  {
    "path": "internal/core/node/info.go",
    "content": "package node\n\nimport nodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\nfunc RefreshInfo() {\n\trefreshMutex.Lock()\n\tdefer refreshMutex.Unlock()\n\n\tfor k := range subAggBuf {\n\t\tdelete(subAggBuf, k)\n\t}\n\tfor k := range countryAggBuf {\n\t\tdelete(countryAggBuf, k)\n\t}\n\n\tpoolMutex.RLock()\n\tfor _, n := range pool {\n\t\ts := subAggBuf[n.Base.SubId]\n\t\tif s == nil {\n\t\t\ts = &infoSums{}\n\t\t\tsubAggBuf[n.Base.SubId] = s\n\t\t}\n\t\ts.count++\n\t\ts.sumSpeedUp += uint64(n.Info.SpeedUp.Average())\n\t\ts.sumSpeedDown += uint64(n.Info.SpeedDown.Average())\n\t\ts.sumDelay += uint64(n.Info.Delay.Average())\n\t\ts.sumRisk += uint64(n.Info.Risk)\n\n\t\tc := countryAggBuf[n.Info.Country]\n\t\tif c == nil {\n\t\t\tc = &infoSums{}\n\t\t\tcountryAggBuf[n.Info.Country] = c\n\t\t}\n\t\tc.count++\n\t\tc.sumSpeedUp += uint64(n.Info.SpeedUp.Average())\n\t\tc.sumSpeedDown += uint64(n.Info.SpeedDown.Average())\n\t\tc.sumDelay += uint64(n.Info.Delay.Average())\n\t\tc.sumRisk += uint64(n.Info.Risk)\n\t}\n\tpoolMutex.RUnlock()\n\n\tfor k := range subInfoMap {\n\t\tdelete(subInfoMap, k)\n\t}\n\tfor k := range countryInfoMap {\n\t\tdelete(countryInfoMap, k)\n\t}\n\n\tfor subID, s := range subAggBuf {\n\t\tif s.count == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tsubInfoMap[subID] = nodeModel.SimpleInfo{\n\t\t\tCount:     s.count,\n\t\t\tSpeedUp:   uint32(s.sumSpeedUp / uint64(s.count)),\n\t\t\tSpeedDown: uint32(s.sumSpeedDown / uint64(s.count)),\n\t\t\tDelay:     uint16(s.sumDelay / uint64(s.count)),\n\t\t\tRisk:      uint8(s.sumRisk / uint64(s.count)),\n\t\t}\n\t}\n\tfor country, c := range countryAggBuf {\n\t\tif c.count == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tcountryInfoMap[country] = nodeModel.SimpleInfo{\n\t\t\tCount:     c.count,\n\t\t\tSpeedUp:   uint32(c.sumSpeedUp / uint64(c.count)),\n\t\t\tSpeedDown: uint32(c.sumSpeedDown / uint64(c.count)),\n\t\t\tDelay:     uint16(c.sumDelay / uint64(c.count)),\n\t\t\tRisk:      uint8(c.sumRisk / uint64(c.count)),\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/core/node/node.go",
    "content": "package node\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/gob\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"sort\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/bestruirui/bestsub/internal/config\"\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/core/task\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nfunc InitNodePool(size int) {\n\tpool = make([]nodeModel.Data, 0, size)\n\tnodeExist = NewExist(size)\n\tnodeProcess = NewExist(size)\n\tsessionFile := config.Base().Session.NodePath\n\tif _, err := os.Stat(sessionFile); os.IsNotExist(err) {\n\t\treturn\n\t}\n\n\tfile, err := os.Open(sessionFile)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer file.Close()\n\n\tdecoder := gob.NewDecoder(file)\n\tif err := decoder.Decode(&pool); err != nil {\n\t\tlog.Warnf(\"restore node pool failed: %v\", err)\n\t\tos.Remove(sessionFile)\n\t\treturn\n\t}\n\tfor _, node := range pool {\n\t\tnodeExist.Add(node.Base.UniqueKey)\n\t}\n}\n\nfunc CloseNodePool() error {\n\tvar buf bytes.Buffer\n\tencoder := gob.NewEncoder(&buf)\n\tif err := encoder.Encode(pool); err != nil {\n\t\tlog.Warnf(\"save node pool failed: %v\", err)\n\t\treturn err\n\t}\n\tfilePath := config.Base().Session.NodePath\n\n\tdir := path.Dir(filePath)\n\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\tos.MkdirAll(dir, 0755)\n\t}\n\tif os.WriteFile(filePath, buf.Bytes(), 0600) != nil {\n\t\tlog.Warnf(\"node pool save failed\")\n\t}\n\tlog.Debugf(\"node pool saved\")\n\n\treturn nil\n}\n\ntype nameNode struct {\n\tName string\n}\n\nfunc Add(node *[]nodeModel.Base) int {\n\tvar nodesToProcess []nodeModel.Base\n\n\tfor _, n := range *node {\n\t\tvar nameNode nameNode\n\t\tif err := yaml.Unmarshal(n.Raw, &nameNode); err != nil {\n\t\t\tlog.Warnf(\"yaml.Unmarshal failed: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif !nodeExist.Exist(n.UniqueKey) && !nodeProcess.Exist(n.UniqueKey) {\n\t\t\tnodeProcess.Add(n.UniqueKey)\n\t\t\tnodesToProcess = append(nodesToProcess, n)\n\t\t\tlog.Debugf(\"add process node: %s\", nameNode.Name)\n\t\t} else {\n\t\t\tlog.Debugf(\"node already exist: %s\", nameNode.Name)\n\t\t}\n\t}\n\n\tlog.Debugf(\"add %d nodes to process\", len(nodesToProcess))\n\n\tif len(nodesToProcess) > 0 {\n\t\tgo func() {\n\t\t\tfor _, node := range nodesToProcess {\n\t\t\t\tn := node // capture loop variable\n\t\t\t\twgSync.Add(1)\n\t\t\t\ttask.Submit(func() {\n\t\t\t\t\tdefer wgSync.Done()\n\t\t\t\t\tdefer nodeProcess.Remove(n.UniqueKey)\n\t\t\t\t\tvar raw map[string]any\n\t\t\t\t\tif err := yaml.Unmarshal(n.Raw, &raw); err != nil {\n\t\t\t\t\t\tlog.Warnf(\"yaml.Unmarshal failed: %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tclient := mihomo.Proxy(raw)\n\t\t\t\t\tif client == nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tdefer client.Release()\n\t\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(op.GetSettingInt(setting.NODE_TEST_TIMEOUT))*time.Second)\n\t\t\t\t\tdefer cancel()\n\t\t\t\t\trequest, err := http.NewRequestWithContext(ctx, \"GET\", op.GetSettingStr(setting.NODE_TEST_URL), nil)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tstart := time.Now()\n\t\t\t\t\tresponse, err := client.Do(request)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tdefer response.Body.Close()\n\t\t\t\t\tif response.StatusCode != 204 {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tvar info nodeModel.Info\n\t\t\t\t\tinfo.Delay.Update(uint16(time.Since(start).Milliseconds()))\n\t\t\t\t\tinfo.SetAliveStatus(nodeModel.Alive, true)\n\t\t\t\t\trawCopy := append([]byte(nil), n.Raw...)\n\t\t\t\t\tn.Raw = rawCopy\n\t\t\t\t\tvalidMutex.Lock()\n\t\t\t\t\tvalidNodes = append(validNodes, nodeModel.Data{\n\t\t\t\t\t\tBase: n,\n\t\t\t\t\t\tInfo: &info,\n\t\t\t\t\t})\n\t\t\t\t\tlog.Debugf(\"node: %s test end, Delay: %d\", raw[\"name\"].(string), info.Delay.Average())\n\t\t\t\t\tvalidMutex.Unlock()\n\t\t\t\t})\n\n\t\t\t}\n\t\t}()\n\t\tif !wgStatus {\n\t\t\twgStatus = true\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(time.Second * 5)\n\t\t\t\twgSync.Wait()\n\t\t\t\tmergedNodes := 0\n\t\t\t\tif len(validNodes) > 0 {\n\t\t\t\t\tmergedNodes = mergeNodesToPool(validNodes)\n\t\t\t\t\tRefreshInfo()\n\t\t\t\t}\n\t\t\t\tlog.Infof(\"Receipt successful, %d new nodes added\", mergedNodes)\n\t\t\t\tvalidNodes = validNodes[:0]\n\t\t\t\twgStatus = false\n\t\t\t}()\n\t\t}\n\t}\n\treturn len(nodesToProcess)\n}\n\nfunc ForEach(fn func(node []byte)) {\n\tpoolMutex.RLock()\n\tdefer poolMutex.RUnlock()\n\tfor _, node := range pool {\n\t\tfn(node.Raw)\n\t}\n}\n\nfunc GetAll() []nodeModel.Data {\n\tpoolMutex.RLock()\n\tdefer poolMutex.RUnlock()\n\treturn pool\n}\n\nfunc GetBySubIdExclude(subId []uint16) []uint16 {\n\tpoolMutex.RLock()\n\tdefer poolMutex.RUnlock()\n\tvar result []uint16\n\tfor _, node := range pool {\n\t\tif !slices.Contains(subId, node.Base.SubId) {\n\t\t\tresult = append(result, node.Base.SubId)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc GetBySubId(subId []uint16) *[]nodeModel.Data {\n\tpoolMutex.RLock()\n\tdefer poolMutex.RUnlock()\n\tvar result []nodeModel.Data\n\tfor _, node := range pool {\n\t\tif slices.Contains(subId, node.Base.SubId) {\n\t\t\tresult = append(result, node)\n\t\t}\n\t}\n\treturn &result\n}\n\nfunc GetByFilter(filter nodeModel.Filter) *[]nodeModel.Data {\n\tpoolMutex.RLock()\n\tdefer poolMutex.RUnlock()\n\tvar result []nodeModel.Data\n\tfor _, node := range pool {\n\t\tif len(filter.SubId) > 0 {\n\t\t\tif filter.SubIdExclude && slices.Contains(filter.SubId, node.Base.SubId) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !filter.SubIdExclude && !slices.Contains(filter.SubId, node.Base.SubId) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif filter.AliveStatus != 0 && node.Info.AliveStatus&filter.AliveStatus != filter.AliveStatus {\n\t\t\tcontinue\n\t\t}\n\t\tif len(filter.Country) > 0 {\n\t\t\tif filter.CountryExclude && slices.Contains(filter.Country, node.Info.Country) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !filter.CountryExclude && !slices.Contains(filter.Country, node.Info.Country) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif filter.SpeedUpMore != 0 && node.Info.SpeedUp.Average() < filter.SpeedUpMore {\n\t\t\tcontinue\n\t\t}\n\t\tif filter.SpeedDownMore != 0 && node.Info.SpeedDown.Average() < filter.SpeedDownMore {\n\t\t\tcontinue\n\t\t}\n\t\tif filter.DelayLessThan != 0 && node.Info.Delay.Average() > filter.DelayLessThan {\n\t\t\tcontinue\n\t\t}\n\t\tif filter.RiskLessThan != 0 && node.Info.Risk > filter.RiskLessThan {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, node)\n\t}\n\treturn &result\n}\n\nfunc mergeNodesToPool(newNodes []nodeModel.Data) int {\n\tsort.Slice(newNodes, func(i, j int) bool {\n\t\treturn newNodes[i].Info.Delay.Average() < newNodes[j].Info.Delay.Average()\n\t})\n\n\tpoolMutex.Lock()\n\tdefer poolMutex.Unlock()\n\n\tpoolLen := len(pool)\n\tpoolCap := cap(pool)\n\n\tif poolLen < poolCap {\n\t\tremainingCap := poolCap - poolLen\n\t\tif len(newNodes) < remainingCap {\n\t\t\tpool = append(pool, newNodes...)\n\t\t\tfor _, node := range newNodes {\n\t\t\t\tnodeExist.Add(node.Base.UniqueKey)\n\t\t\t}\n\t\t\treturn len(newNodes)\n\t\t} else {\n\t\t\tpool = append(pool, newNodes[:remainingCap]...)\n\t\t\tfor _, node := range newNodes[:remainingCap] {\n\t\t\t\tnodeExist.Add(node.Base.UniqueKey)\n\t\t\t}\n\t\t\tnewNodes = newNodes[remainingCap:]\n\t\t}\n\t}\n\n\tsort.Slice(pool, func(i, j int) bool {\n\t\treturn pool[i].Info.Delay.Average() < pool[j].Info.Delay.Average()\n\t})\n\n\tnewNodeIndex := 0\n\tfor i := len(pool) - 1; i >= 0 && newNodeIndex < len(newNodes); i-- {\n\t\tif newNodes[newNodeIndex].Info.Delay.Average() < pool[i].Info.Delay.Average() {\n\t\t\tlog.Debugf(\"new node delay %dms < old delay %dms,merge\", newNodes[newNodeIndex].Info.Delay.Average(), pool[i].Info.Delay.Average())\n\t\t\tnodeExist.Remove(pool[i].Base.UniqueKey)\n\t\t\tpool[i] = newNodes[newNodeIndex]\n\t\t\tnodeExist.Add(newNodes[newNodeIndex].Base.UniqueKey)\n\t\t\tnewNodeIndex++\n\t\t} else {\n\t\t\tlog.Debugf(\"new node delay %dms > old delay %dms,not merge\", newNodes[newNodeIndex].Info.Delay.Average(), pool[i].Info.Delay.Average())\n\t\t\treturn newNodeIndex\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc GetSubInfo(subID uint16) nodeModel.SimpleInfo {\n\trefreshMutex.Lock()\n\tdefer refreshMutex.Unlock()\n\treturn subInfoMap[subID]\n}\n\nfunc GetCountryInfo(country string) nodeModel.SimpleInfo {\n\trefreshMutex.Lock()\n\tdefer refreshMutex.Unlock()\n\treturn countryInfoMap[country]\n}\nfunc DeleteBySubId(subID uint16) {\n\tpoolMutex.Lock()\n\tdefer poolMutex.Unlock()\n\t\n\tend := len(pool) - 1\n\tfor i := 0; i <= end; {\n\t\tif pool[i].Base.SubId == subID {\n\t\t\tnodeExist.Remove(pool[i].Base.UniqueKey)\n\t\t\tpool[i] = pool[end]\n\t\t\tend--\n\t\t} else {\n\t\t\ti++\n\t\t}\n\t}\n\t\n\tpool = pool[:end+1]\n}\n"
  },
  {
    "path": "internal/core/node/var.go",
    "content": "package node\n\nimport (\n\t\"sync\"\n\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n)\n\nvar (\n\tpoolMutex   sync.RWMutex\n\tpool        []nodeModel.Data\n\tnodeExist   *exist\n\tnodeProcess *exist\n\n\twgSync     sync.WaitGroup\n\twgStatus   bool\n\tvalidNodes []nodeModel.Data\n\tvalidMutex sync.Mutex\n\n\trefreshMutex   sync.Mutex\n\tsubInfoMap     = make(map[uint16]nodeModel.SimpleInfo)\n\tcountryInfoMap = make(map[string]nodeModel.SimpleInfo)\n\tsubAggBuf      = make(map[uint16]*infoSums)\n\tcountryAggBuf  = make(map[string]*infoSums)\n)\n\ntype infoSums struct {\n\tsumSpeedUp   uint64\n\tsumSpeedDown uint64\n\tsumDelay     uint64\n\tsumRisk      uint64\n\tcount        uint32\n}\n"
  },
  {
    "path": "internal/core/subconv/subconv.go",
    "content": "package subconv\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/enfein/mieru/v3/pkg/log\"\n)\n\nfunc ConvertData(raw string, target string) string {\n\tsubStoreUrl := op.GetSettingStr(setting.SUBCONV_URL)\n\tif subStoreUrl == \"\" {\n\t\tlog.Warnf(\"substore url is not set\")\n\t\treturn \"\"\n\t}\n\tclient := mihomo.Default(op.GetSettingBool(setting.SUBCONV_URL_PROXY))\n\tif client == nil {\n\t\tlog.Warnf(\"failed to create http client\")\n\t\treturn \"\"\n\t}\n\tdefer client.Release()\n\treqBody := struct {\n\t\tData   string `json:\"data\"`\n\t\tClient string `json:\"client\"`\n\t}{\n\t\tData:   raw,\n\t\tClient: target,\n\t}\n\treqBodyBytes, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to marshal request body: %v\", err)\n\t\treturn \"\"\n\t}\n\treq, err := http.NewRequestWithContext(context.Background(), \"POST\", subStoreUrl+\"/api/proxy/parse\", bytes.NewBuffer(reqBodyBytes))\n\tif err != nil {\n\t\tlog.Warnf(\"failed to create request: %v\", err)\n\t\treturn \"\"\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to do request: %v\", err)\n\t\treturn \"\"\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to read response body: %v\", err)\n\t\treturn \"\"\n\t}\n\tvar respBody struct {\n\t\tData struct {\n\t\t\tParres string `json:\"par_res\"`\n\t\t}\n\t}\n\terr = json.Unmarshal(body, &respBody)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to unmarshal response body: %v body: %s\", err, string(body))\n\t\treturn \"\"\n\t}\n\treturn respBody.Data.Parres\n}\n"
  },
  {
    "path": "internal/core/system/monitor.go",
    "content": "package system\n\nimport (\n\t\"os\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/system\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/shirou/gopsutil/v4/process\"\n)\n\nvar (\n\tstartTime     string\n\tuploadBytes   uint64\n\tdownloadBytes uint64\n)\n\nfunc init() {\n\tstartTime = time.Now().Format(time.RFC3339)\n}\n\nfunc AddUploadBytes(bytes uint64) {\n\tatomic.AddUint64(&uploadBytes, bytes)\n}\n\nfunc AddDownloadBytes(bytes uint64) {\n\tatomic.AddUint64(&downloadBytes, bytes)\n}\n\nfunc GetSystemInfo() *system.Info {\n\tproc, err := process.NewProcess(int32(os.Getpid()))\n\tif err != nil {\n\t\tlog.Debugf(\"Failed to create process instance: %v\", err)\n\t\treturn nil\n\t}\n\n\tmemInfo, err := proc.MemoryInfo()\n\tif err != nil {\n\t\tlog.Debugf(\"Failed to get process memory info: %v\", err)\n\t\treturn nil\n\t}\n\n\tcpuPercent, err := proc.CPUPercent()\n\tif err != nil {\n\t\tlog.Debugf(\"Failed to get process CPU percent: %v\", err)\n\t\treturn nil\n\t}\n\n\treturn &system.Info{\n\t\tMemoryUsed:    memInfo.RSS,\n\t\tCPUPercent:    cpuPercent,\n\t\tStartTime:     startTime,\n\t\tUploadBytes:   atomic.LoadUint64(&uploadBytes),\n\t\tDownloadBytes: atomic.LoadUint64(&downloadBytes),\n\t}\n}\n\nfunc Reset() {\n\tatomic.StoreUint64(&uploadBytes, 0)\n\tatomic.StoreUint64(&downloadBytes, 0)\n}\n"
  },
  {
    "path": "internal/core/task/task.go",
    "content": "package task\n\nimport \"github.com/panjf2000/ants/v2\"\n\nvar pool *ants.Pool\nvar thread int\n\nfunc Init(maxThread int) {\n\tpool, _ = ants.NewPool(maxThread)\n\tthread = maxThread\n}\n\nfunc Submit(fn func()) {\n\tpool.Submit(fn)\n}\n\nfunc Release() {\n\tpool.Release()\n}\n\nfunc MaxThread() int {\n\treturn thread\n}\n"
  },
  {
    "path": "internal/core/update/core.go",
    "content": "package update\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"syscall\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/bestruirui/bestsub/internal/utils/shutdown\"\n)\n\nfunc UpdateCore() error {\n\tlog.Infof(\"start update core\")\n\terr := updateCore()\n\tif err != nil {\n\t\tlog.Warnf(\"update core failed, please update manually\", err)\n\t\treturn err\n\t}\n\tlog.Infof(\"update core success\")\n\treturn nil\n}\n\nfunc updateCore() error {\n\tarch := runtime.GOARCH\n\tgoos := runtime.GOOS\n\n\tvar downloadUrl string\n\n\tvar filename string\n\tswitch goos {\n\tcase \"windows\":\n\t\tswitch arch {\n\t\tcase \"386\":\n\t\t\tfilename = \"bestsub-windows-x86.zip\"\n\t\tcase \"amd64\":\n\t\t\tfilename = \"bestsub-windows-x86_64.zip\"\n\t\tdefault:\n\t\t\tlog.Errorf(\"unsupported windows architecture: %s\", arch)\n\t\t\treturn fmt.Errorf(\"unsupported windows architecture: %s\", arch)\n\t\t}\n\tcase \"darwin\":\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\tfilename = \"bestsub-darwin-x86_64.zip\"\n\t\tcase \"arm64\":\n\t\t\tfilename = \"bestsub-darwin-arm64.zip\"\n\t\tdefault:\n\t\t\tlog.Errorf(\"unsupported darwin architecture: %s\", arch)\n\t\t\treturn fmt.Errorf(\"unsupported darwin architecture: %s\", arch)\n\t\t}\n\tcase \"linux\":\n\t\tswitch arch {\n\t\tcase \"386\":\n\t\t\tfilename = \"bestsub-linux-x86.zip\"\n\t\tcase \"amd64\":\n\t\t\tfilename = \"bestsub-linux-x86_64.zip\"\n\t\tcase \"arm\":\n\t\t\tfilename = \"bestsub-linux-armv7.zip\"\n\t\tcase \"arm64\":\n\t\t\tfilename = \"bestsub-linux-arm64.zip\"\n\t\tdefault:\n\t\t\tlog.Errorf(\"unsupported linux architecture: %s\", arch)\n\t\t\treturn fmt.Errorf(\"unsupported linux architecture: %s\", arch)\n\t\t}\n\tdefault:\n\t\tlog.Errorf(\"unsupported operating system: %s\", goos)\n\t\treturn fmt.Errorf(\"unsupported operating system: %s\", goos)\n\t}\n\n\tdownloadUrl = bestsubUpdateUrl + \"/\" + filename\n\n\tbytes, err := download(downloadUrl, op.GetSettingBool(setting.PROXY_ENABLE))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\texecPath, err := os.Executable()\n\tif err != nil {\n\t\treturn err\n\t}\n\texecDir := filepath.Dir(execPath)\n\tif err := unzip(bytes, execDir); err != nil {\n\t\treturn err\n\t}\n\tgo restartExecutable(execPath)\n\treturn nil\n}\n\nfunc restartExecutable(execPath string) {\n\tvar err error\n\tshutdown.All()\n\tif runtime.GOOS == \"windows\" {\n\t\tcmd := exec.Command(execPath, os.Args[1:]...)\n\t\tlog.Infof(\"restarting: %q %q\", execPath, os.Args[1:])\n\t\tcmd.Stdin = os.Stdin\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\terr = cmd.Start()\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"restarting: %s\", err)\n\t\t}\n\n\t\tos.Exit(0)\n\t}\n\n\tlog.Infof(\"restarting: %q %q\", execPath, os.Args[1:])\n\terr = syscall.Exec(execPath, os.Args, os.Environ())\n\tif err != nil {\n\t\tlog.Errorf(\"restarting: %s\", err)\n\t}\n\tlog.Infof(\"restarting success\")\n}\n"
  },
  {
    "path": "internal/core/update/update.go",
    "content": "package update\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/mihomo\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nconst (\n\tbestsubUpdateUrl    = \"https://github.com/bestruirui/bestsub/releases/latest/download\"\n\tbestsubUpdateApiUrl = \"https://api.github.com/repos/bestruirui/BestSub/releases/latest\"\n)\n\ntype LatestInfo struct {\n\tTagName     string `json:\"tag_name\"`\n\tPublishedAt string `json:\"published_at\"`\n\tBody        string `json:\"body\"`\n\tMessage     string `json:\"message\"`\n}\n\nfunc GetLatestBestsubInfo() (*LatestInfo, error) {\n\treturn getLatestInfo(bestsubUpdateApiUrl, op.GetSettingBool(setting.PROXY_ENABLE))\n}\n\nfunc getLatestInfo(url string, proxy bool) (*LatestInfo, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\thc := mihomo.Default(proxy)\n\tif hc == nil {\n\t\treturn nil, fmt.Errorf(\"failed to create http client\")\n\t}\n\tdefer hc.Release()\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\tlog.Debugf(\"new request failed: %v\", err)\n\t\treturn nil, err\n\t}\n\tresp, err := hc.Do(req)\n\tif err != nil {\n\t\tlog.Debugf(\"request failed: %v\", err)\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Debugf(\"read body failed: %v\", err)\n\t\treturn nil, err\n\t}\n\n\tlatestInfo := LatestInfo{}\n\terr = json.Unmarshal(body, &latestInfo)\n\tif err != nil {\n\t\tlog.Debugf(\"unmarshal body failed: %v\", err)\n\t\treturn nil, err\n\t}\n\tif latestInfo.Message != \"\" {\n\t\treturn nil, fmt.Errorf(\"failed to get latest info: %s\", latestInfo.Message)\n\t}\n\treturn &latestInfo, nil\n}\n\nfunc download(url string, proxy bool) ([]byte, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\thc := mihomo.Default(proxy)\n\tif hc == nil {\n\t\treturn nil, fmt.Errorf(\"failed to create http client\")\n\t}\n\tdefer hc.Release()\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\tlog.Debugf(\"new request failed: %v\", err)\n\t\treturn nil, err\n\t}\n\tresp, err := hc.Do(req)\n\tif err != nil {\n\t\tlog.Debugf(\"request failed: %v\", err)\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Debugf(\"read body failed: %v\", err)\n\t\treturn nil, err\n\t}\n\treturn bytes, nil\n}\n\nfunc unzip(data []byte, dest string) error {\n\tr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))\n\tif err != nil {\n\t\tlog.Debugf(\"new zip reader failed: %v\", err)\n\t\treturn err\n\t}\n\n\tfor _, f := range r.File {\n\t\tfpath := filepath.Join(dest, f.Name)\n\n\t\tif !inDest(fpath, dest) {\n\t\t\tlog.Debugf(\"invalid file path: %s\", fpath)\n\t\t\treturn fmt.Errorf(\"invalid file path: %s\", fpath)\n\t\t}\n\t\tinfo := f.FileInfo()\n\t\tif info.IsDir() {\n\t\t\tos.MkdirAll(fpath, os.ModePerm)\n\t\t\tcontinue\n\t\t}\n\t\tif info.Mode()&os.ModeSymlink != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {\n\t\t\tlog.Debugf(\"mkdir all failed: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\toutFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode().Perm())\n\t\tif err != nil {\n\t\t\terr = os.Remove(fpath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"remove file failed: %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\toutFile, err = os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"open file failed: %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tdefer outFile.Close()\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"open file failed: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\t_, err = io.Copy(outFile, rc)\n\t\trc.Close()\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"copy failed: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc inDest(fpath, dest string) bool {\n\tif rel, err := filepath.Rel(dest, fpath); err == nil {\n\t\tif filepath.IsLocal(rel) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/auth.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/auth\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\n// Get 获取认证信息\nfunc (db *DB) Auth() interfaces.AuthRepository {\n\treturn &AuthRepository{db: db}\n}\n\n// AuthRepository 认证数据访问实现\ntype AuthRepository struct {\n\tdb *DB\n}\n\n// Get 获取认证信息\nfunc (db *AuthRepository) Get(ctx context.Context) (*auth.Data, error) {\n\tlog.Debugf(\"Get auth\")\n\tquery := `SELECT id, username, password FROM auth LIMIT 1`\n\n\tvar authData auth.Data\n\terr := db.db.db.QueryRowContext(ctx, query).Scan(\n\t\t&authData.ID,\n\t\t&authData.UserName,\n\t\t&authData.Password,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get auth: %w\", err)\n\t}\n\n\treturn &authData, nil\n}\n\n// UpdateName 更新用户名\nfunc (r *AuthRepository) UpdateName(ctx context.Context, name string) error {\n\tlog.Debugf(\"UpdateName: %s\", name)\n\tquery := `UPDATE auth SET username = ? WHERE id = (SELECT id FROM auth LIMIT 1)`\n\n\tresult, err := r.db.db.ExecContext(ctx, query, name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update username: %w\", err)\n\t}\n\n\trowsAffected, err := result.RowsAffected()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get rows affected: %w\", err)\n\t}\n\n\tif rowsAffected == 0 {\n\t\treturn fmt.Errorf(\"no auth record found to update\")\n\t}\n\n\treturn nil\n}\n\n// UpdatePassword 更新密码\nfunc (r *AuthRepository) UpdatePassword(ctx context.Context, hashPassword string) error {\n\tlog.Debugf(\"UpdatePassword: %s\", hashPassword)\n\tquery := `UPDATE auth SET password = ? WHERE id = (SELECT id FROM auth LIMIT 1)`\n\n\tresult, err := r.db.db.ExecContext(ctx, query, hashPassword)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update password: %w\", err)\n\t}\n\n\trowsAffected, err := result.RowsAffected()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get rows affected: %w\", err)\n\t}\n\n\tif rowsAffected == 0 {\n\t\treturn fmt.Errorf(\"no auth record found to update\")\n\t}\n\n\treturn nil\n}\n\n// Initialize 初始化认证信息\nfunc (r *AuthRepository) Initialize(ctx context.Context, authData *auth.Data) error {\n\tlog.Debugf(\"Initialize: %s\", authData.UserName)\n\tquery := `INSERT INTO auth (username, password) VALUES (?, ?)`\n\t_, err := r.db.db.ExecContext(ctx, query, authData.UserName, authData.Password)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize auth: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// IsInitialized 验证是否已初始化\nfunc (r *AuthRepository) IsInitialized(ctx context.Context) (bool, error) {\n\tlog.Debugf(\"IsInitialized\")\n\tquery := `SELECT EXISTS(SELECT 1 FROM auth LIMIT 1)`\n\n\tvar exists bool\n\terr := r.db.db.QueryRowContext(ctx, query).Scan(&exists)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to check auth initialization: %w\", err)\n\t}\n\n\treturn exists, nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/check.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/check\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype CheckRepository struct {\n\tdb *DB\n}\n\nfunc (db *DB) Check() interfaces.CheckRepository {\n\treturn &CheckRepository{db: db}\n}\n\nfunc (r *CheckRepository) Create(ctx context.Context, t *check.Data) error {\n\tlog.Debugf(\"Create check\")\n\tquery := `INSERT INTO check_task (enable, name, task, config, result)\n\t          VALUES (?, ?, ?, ?, ?)`\n\n\tresult, err := r.db.db.ExecContext(ctx, query,\n\t\tt.Enable,\n\t\tt.Name,\n\t\tt.Task,\n\t\tt.Config,\n\t\tt.Result,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create check: %w\", err)\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get check id: %w\", err)\n\t}\n\tt.ID = uint16(id)\n\treturn nil\n}\n\nfunc (r *CheckRepository) GetByID(ctx context.Context, id uint16) (*check.Data, error) {\n\tlog.Debugf(\"Get check by id\")\n\tquery := `SELECT id, enable, name, task, config, result\n\t          FROM check_task WHERE id = ?`\n\n\tvar t check.Data\n\terr := r.db.db.QueryRowContext(ctx, query, id).Scan(\n\t\t&t.ID,\n\t\t&t.Enable,\n\t\t&t.Name,\n\t\t&t.Task,\n\t\t&t.Config,\n\t\t&t.Result,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get check by id: %w\", err)\n\t}\n\n\treturn &t, nil\n}\n\nfunc (r *CheckRepository) Update(ctx context.Context, t *check.Data) error {\n\tlog.Debugf(\"Update check\")\n\tquery := `UPDATE check_task SET enable = ?, name = ?, task = ?, config = ?, result = ? WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\tt.Enable,\n\t\tt.Name,\n\t\tt.Task,\n\t\tt.Config,\n\t\tt.Result,\n\t\tt.ID,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update check: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *CheckRepository) Delete(ctx context.Context, id uint16) error {\n\tlog.Debugf(\"Delete check\")\n\tquery := `DELETE FROM check_task WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query, id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete check: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *CheckRepository) List(ctx context.Context) (*[]check.Data, error) {\n\tlog.Debugf(\"List check\")\n\tquery := `SELECT id, enable, name, task, config, result\n\t          FROM check_task ORDER BY id DESC`\n\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to list checks: %v\", err)\n\t\treturn nil, fmt.Errorf(\"failed to list checks: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar checks []check.Data\n\tfor rows.Next() {\n\t\tvar t check.Data\n\t\terr := rows.Scan(\n\t\t\t&t.ID,\n\t\t\t&t.Enable,\n\t\t\t&t.Name,\n\t\t\t&t.Task,\n\t\t\t&t.Config,\n\t\t\t&t.Result,\n\t\t)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to scan check: %v\", err)\n\t\t\treturn nil, fmt.Errorf(\"failed to scan check: %w\", err)\n\t\t}\n\t\tchecks = append(checks, t)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\tlog.Errorf(\"failed to iterate checks: %v\", err)\n\t\treturn nil, fmt.Errorf(\"failed to iterate checks: %w\", err)\n\t}\n\n\treturn &checks, nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/migration/001_table.go",
    "content": "package migration\n\nimport \"github.com/bestruirui/bestsub/internal/database/migration\"\n\n// Migration001Table 初始数据库架构\nfunc Migration001Table() string {\n\treturn `\nCREATE TABLE IF NOT EXISTS \"auth\" (\n\t\"id\" INTEGER,\n\t\"username\" TEXT NOT NULL UNIQUE,\n\t\"password\" TEXT NOT NULL,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"setting\" (\n\t\"key\" TEXT NOT NULL UNIQUE,\n\t\"value\" TEXT NOT NULL,\n\tPRIMARY KEY(\"key\")\n);\n\nCREATE TABLE IF NOT EXISTS \"notify_template\" (\n\t\"type\" TEXT NOT NULL,\n\t\"template\" TEXT NOT NULL,\n\tPRIMARY KEY(\"type\")\n);\n\nCREATE TABLE IF NOT EXISTS \"notify\" (\n\t\"id\" INTEGER NOT NULL UNIQUE,\n\t\"name\" TEXT NOT NULL,\n\t\"type\" TEXT NOT NULL,\n\t\"config\" TEXT NOT NULL,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"check_task\" (\n\t\"id\" INTEGER,\n\t\"enable\" BOOLEAN NOT NULL,\n\t\"name\" TEXT,\n\t\"task\" TEXT NOT NULL,\n\t\"config\" TEXT,\n\t\"result\" TEXT,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"storage\" (\n\t\"id\" INTEGER,\n\t\"name\" TEXT,\n\t\"type\" TEXT NOT NULL,\n\t\"config\" TEXT NOT NULL,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"sub_template\" (\n\t\"id\" INTEGER,\n\t\"name\" TEXT,\n\t\"type\" TEXT NOT NULL,\n\t\"template\" TEXT NOT NULL,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"sub\" (\n\t\"id\" INTEGER NOT NULL,\n\t\"enable\" BOOLEAN NOT NULL DEFAULT true,\n\t\"name\" TEXT,\n\t\"cron_expr\" TEXT NOT NULL,\n\t\"config\" TEXT NOT NULL,\n\t\"result\" TEXT DEFAULT '{}',\n\t\"created_at\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\t\"updated_at\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"share\" (\n\t\"id\" INTEGER NOT NULL,\n\t\"enable\" BOOLEAN NOT NULL DEFAULT false,\n\t\"name\" TEXT NOT NULL,\n\t\"gen\" TEXT NOT NULL,\n\t\"token\" TEXT NOT NULL UNIQUE,\n\t\"access_count\" INTEGER DEFAULT 0,\n\t\"max_access_count\" INTEGER DEFAULT 0,\n\t\"expires\" INTEGER DEFAULT 0,\n\tPRIMARY KEY(\"id\")\n);\n\nCREATE TABLE IF NOT EXISTS \"migration\" (\n\t\"date\" INTEGER NOT NULL UNIQUE,\n\t\"version\" TEXT NOT NULL,\n\t\"description\" TEXT,\n\t\"applied_at\" DATETIME NOT NULL,\n\tPRIMARY KEY(\"date\")\n);\n`\n}\n\n// init 自动注册迁移\nfunc init() {\n\tmigration.Register(ClientName, 202507171100, \"dev\", \"Tables\", Migration001Table)\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/migration/002_add_sub_tags.go",
    "content": "package migration\n\nimport \"github.com/bestruirui/bestsub/internal/database/migration\"\n\n// Migration002AddSubTags 为sub表添加列tags\nfunc Migration002AddSubTags() string {\n\treturn `\nALTER TABLE \"sub\"\nADD COLUMN tags TEXT NOT NULL DEFAULT '[]';\n`\n}\n\n// init 自动注册迁移\nfunc init() {\n\tmigration.Register(ClientName, 202511082145, \"dev\", \"Add Sub Tags\", Migration002AddSubTags)\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/migration/migration.go",
    "content": "package migration\n\nimport \"github.com/bestruirui/bestsub/internal/database/migration\"\n\nconst ClientName = \"sqlite\"\n\nfunc Get() []*migration.Info {\n\treturn migration.Get(ClientName)\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/migrator.go",
    "content": "package sqlite\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/client/sqlite/migration\"\n\tmigModel \"github.com/bestruirui/bestsub/internal/database/migration\"\n)\n\nfunc (db *DB) Migrate() error {\n\tmigrations := migration.Get()\n\tif len(migrations) == 0 {\n\t\treturn nil\n\t}\n\tif err := db.ensureMigrationsTable(); err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure migrations table: %w\", err)\n\t}\n\n\tappliedDates, err := db.getAppliedMigrations()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get applied migrations: %w\", err)\n\t}\n\n\tvar pendingMigrations []*migModel.Info\n\tfor _, migration := range migrations {\n\t\tif !appliedDates[migration.Date] {\n\t\t\tpendingMigrations = append(pendingMigrations, migration)\n\t\t}\n\t}\n\n\tif len(pendingMigrations) == 0 {\n\t\treturn nil\n\t}\n\n\treturn db.applyMigrations(pendingMigrations)\n}\n\nfunc (db *DB) ensureMigrationsTable() error {\n\tmigrationTable := `\n\tCREATE TABLE IF NOT EXISTS \"migration\" (\n\t\t\"date\" INTEGER NOT NULL UNIQUE,\n\t\t\"version\" TEXT NOT NULL,\n\t\t\"description\" TEXT NOT NULL,\n\t\t\"applied_at\" DATETIME NOT NULL,\n\t\tPRIMARY KEY(\"date\")\n\t);`\n\n\t_, err := db.db.Exec(migrationTable)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create migration table: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (db *DB) getAppliedMigrations() (map[uint64]bool, error) {\n\tappliedDates := make(map[uint64]bool)\n\n\trows, err := db.db.Query(\"SELECT date FROM migration\")\n\tif err != nil {\n\t\treturn appliedDates, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar date uint64\n\t\tif err := rows.Scan(&date); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan migration date: %w\", err)\n\t\t}\n\t\tappliedDates[date] = true\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate migration rows: %w\", err)\n\t}\n\n\treturn appliedDates, nil\n}\n\nfunc (db *DB) applyMigrations(migrations []*migModel.Info) error {\n\ttx, err := db.db.Begin()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t}\n\tdefer tx.Rollback()\n\n\tinsertStmt, err := tx.Prepare(\"INSERT INTO migration (date, version, description, applied_at) VALUES (?, ?, ?, ?)\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to prepare insert statement: %w\", err)\n\t}\n\tdefer insertStmt.Close()\n\n\tfor _, migration := range migrations {\n\t\t_, err = tx.Exec(migration.Content())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute migration %d SQL: %w\", migration.Date, err)\n\t\t}\n\n\t\t_, err = insertStmt.Exec(migration.Date, migration.Version, migration.Description, time.Now())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to record migration %d: %w\", migration.Date, err)\n\t\t}\n\t}\n\n\tif err = tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit migrations transaction: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/notify.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype NotifyRepository struct {\n\tdb *DB\n}\n\nfunc (db *DB) Notify() interfaces.NotifyRepository {\n\treturn &NotifyRepository{db: db}\n}\n\nfunc (r *NotifyRepository) Create(ctx context.Context, channel *notify.Data) error {\n\tlog.Debugf(\"Create notify\")\n\tquery := `INSERT INTO notify (name, type, config )\n\t          VALUES (?, ?, ?)`\n\n\tresult, err := r.db.db.ExecContext(ctx, query,\n\t\tchannel.Name,\n\t\tchannel.Type,\n\t\tchannel.Config,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create notification channel: %w\", err)\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get notification channel id: %w\", err)\n\t}\n\n\tchannel.ID = uint16(id)\n\n\treturn nil\n}\n\nfunc (r *NotifyRepository) GetByID(ctx context.Context, id uint16) (*notify.Data, error) {\n\tlog.Debugf(\"Get notify by id\")\n\tquery := `SELECT id, name, type, config\n\t          FROM notify WHERE id = ?`\n\n\tvar channel notify.Data\n\terr := r.db.db.QueryRowContext(ctx, query, id).Scan(\n\t\t&channel.ID,\n\t\t&channel.Name,\n\t\t&channel.Type,\n\t\t&channel.Config,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get notification channel by id: %w\", err)\n\t}\n\n\treturn &channel, nil\n}\n\nfunc (r *NotifyRepository) Update(ctx context.Context, channel *notify.Data) error {\n\tlog.Debugf(\"Update notify\")\n\tquery := `UPDATE notify SET name = ?, type = ?, config = ? WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\tchannel.Name,\n\t\tchannel.Type,\n\t\tchannel.Config,\n\t\tchannel.ID,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update notification channel: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *NotifyRepository) Delete(ctx context.Context, id uint16) error {\n\tlog.Debugf(\"Delete notify\")\n\tquery := `DELETE FROM notify WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query, id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete notification channel: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *NotifyRepository) List(ctx context.Context) (*[]notify.Data, error) {\n\tlog.Debugf(\"List notify\")\n\tquery := `SELECT id, name, type, config\n\t          FROM notify ORDER BY id DESC`\n\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list notification channels: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar channels []notify.Data\n\tfor rows.Next() {\n\t\tvar channel notify.Data\n\t\terr := rows.Scan(\n\t\t\t&channel.ID,\n\t\t\t&channel.Name,\n\t\t\t&channel.Type,\n\t\t\t&channel.Config,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan notification channel: %w\", err)\n\t\t}\n\t\tchannels = append(channels, channel)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate notification channels: %w\", err)\n\t}\n\n\treturn &channels, nil\n}\n\ntype NotifyTemplateRepository struct {\n\tdb *DB\n}\n\nfunc (db *DB) NotifyTemplate() interfaces.NotifyTemplateRepository {\n\treturn &NotifyTemplateRepository{db: db}\n}\n\nfunc (r *NotifyTemplateRepository) Create(ctx context.Context, template *notify.Template) error {\n\tlog.Debugf(\"Create notify template\")\n\tquery := `INSERT INTO notify_template (type, template)\n\t          VALUES (?, ?)`\n\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\ttemplate.Type,\n\t\ttemplate.Template,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create notify template: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *NotifyTemplateRepository) GetByType(ctx context.Context, t string) (*notify.Template, error) {\n\tlog.Debugf(\"Get notify template by type\")\n\tquery := `SELECT type, template\n\t          FROM notify_template WHERE type = ?`\n\n\tvar template notify.Template\n\terr := r.db.db.QueryRowContext(ctx, query, t).Scan(\n\t\t&template.Type,\n\t\t&template.Template,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get notify template by type: %w\", err)\n\t}\n\n\treturn &template, nil\n}\nfunc (r *NotifyTemplateRepository) Update(ctx context.Context, template *notify.Template) error {\n\tlog.Debugf(\"Update Notify Template\")\n\tquery := `UPDATE notify_template SET template = ? WHERE type = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\ttemplate.Template,\n\t\ttemplate.Type,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update notify template: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (r *NotifyTemplateRepository) List(ctx context.Context) (*[]notify.Template, error) {\n\tlog.Debugf(\"List notify template\")\n\tquery := `SELECT type, template\n\t          FROM notify_template ORDER BY type DESC`\n\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list notify templates: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar templates []notify.Template\n\tfor rows.Next() {\n\t\tvar template notify.Template\n\t\terr := rows.Scan(\n\t\t\t&template.Type,\n\t\t\t&template.Template,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan notify template: %w\", err)\n\t\t}\n\t\ttemplates = append(templates, template)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate notify templates: %w\", err)\n\t}\n\n\treturn &templates, nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/setting.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nfunc (db *DB) Setting() interfaces.SettingRepository {\n\treturn &SystemConfigRepository{db: db}\n}\n\ntype SystemConfigRepository struct {\n\tdb *DB\n}\n\nfunc (r *SystemConfigRepository) Create(ctx context.Context, configs *[]setting.Setting) error {\n\tif configs == nil || len(*configs) == 0 {\n\t\treturn nil\n\t}\n\ttx, err := r.db.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t}\n\tdefer tx.Rollback()\n\n\tquery := `INSERT INTO setting (key, value)\n\t          VALUES (?, ?)`\n\n\tstmt, err := tx.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to prepare statement: %w\", err)\n\t}\n\tdefer stmt.Close()\n\n\tfor _, config := range *configs {\n\t\t_, err := stmt.ExecContext(ctx,\n\t\t\tconfig.Key,\n\t\t\tconfig.Value,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create system config key '%s': %w\", config.Key, err)\n\t\t}\n\t}\n\n\tif err = tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit transaction: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *SystemConfigRepository) GetAll(ctx context.Context) (*[]setting.Setting, error) {\n\tlog.Debugf(\"GetAll\")\n\tquery := `SELECT key, value\n\t          FROM setting ORDER BY key`\n\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to query all configs: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar configs []setting.Setting\n\tfor rows.Next() {\n\t\tvar config setting.Setting\n\t\tif err := rows.Scan(\n\t\t\t&config.Key,\n\t\t\t&config.Value,\n\t\t); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan config: %w\", err)\n\t\t}\n\t\tconfigs = append(configs, config)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate configs: %w\", err)\n\t}\n\n\treturn &configs, nil\n}\n\nfunc (r *SystemConfigRepository) GetByKey(ctx context.Context, keys []string) (*[]setting.Setting, error) {\n\tlog.Debugf(\"GetByKey: %v\", keys)\n\tif len(keys) == 0 {\n\t\treturn &[]setting.Setting{}, nil\n\t}\n\n\targs := make([]interface{}, len(keys))\n\tinClause := \"\"\n\tfor i, key := range keys {\n\t\tif i > 0 {\n\t\t\tinClause += \",\"\n\t\t}\n\t\tinClause += \"?\"\n\t\targs[i] = key\n\t}\n\tquery := `SELECT key, value\n\t          FROM setting WHERE key IN (` + inClause + `) ORDER BY key`\n\n\trows, err := r.db.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to query configs by keys: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar configs []setting.Setting\n\tfor rows.Next() {\n\t\tvar config setting.Setting\n\t\tif err := rows.Scan(\n\t\t\t&config.Key,\n\t\t\t&config.Value,\n\t\t); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan config: %w\", err)\n\t\t}\n\t\tconfigs = append(configs, config)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate configs: %w\", err)\n\t}\n\n\treturn &configs, nil\n}\n\nfunc (r *SystemConfigRepository) Update(ctx context.Context, data *[]setting.Setting) error {\n\tlog.Debugf(\"Update: %v\", data)\n\tif data == nil || len(*data) == 0 {\n\t\treturn nil\n\t}\n\n\ttx, err := r.db.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t}\n\tdefer tx.Rollback()\n\n\tquery := `UPDATE setting SET value = ? WHERE key = ?`\n\n\tstmt, err := tx.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to prepare statement: %w\", err)\n\t}\n\tdefer stmt.Close()\n\tfor _, updateData := range *data {\n\t\tresult, err := stmt.ExecContext(ctx,\n\t\t\tupdateData.Value,\n\t\t\tupdateData.Key,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update system config key '%s': %w\", updateData.Key, err)\n\t\t}\n\n\t\trowsAffected, err := result.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get rows affected for key '%s': %w\", updateData.Key, err)\n\t\t}\n\n\t\tif rowsAffected == 0 {\n\t\t\treturn fmt.Errorf(\"no config found with key '%s'\", updateData.Key)\n\t\t}\n\t}\n\n\tif err = tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit transaction: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/share.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/share\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype ShareRepository struct {\n\tdb *DB\n}\n\nfunc (db *DB) Share() interfaces.ShareRepository {\n\treturn &ShareRepository{db: db}\n}\n\nfunc (r *ShareRepository) Create(ctx context.Context, shareData *share.Data) error {\n\tlog.Debugf(\"Create share\")\n\tquery := `INSERT INTO share (enable, name, token, gen, max_access_count, expires)\n\t          VALUES (?, ?, ?, ?, ?, ?)`\n\n\tresult, err := r.db.db.ExecContext(ctx, query,\n\t\tshareData.Enable,\n\t\tshareData.Name,\n\t\tshareData.Token,\n\t\tshareData.Gen,\n\t\tshareData.MaxAccessCount,\n\t\tshareData.Expires,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create share link: %w\", err)\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get share link id: %w\", err)\n\t}\n\n\tshareData.ID = uint16(id)\n\n\treturn nil\n}\n\nfunc (r *ShareRepository) GetByID(ctx context.Context, id uint16) (*share.Data, error) {\n\tlog.Debugf(\"Get share by id\")\n\tquery := `SELECT id, enable, name, token, gen, access_count, expires, max_access_count\n\t          FROM share WHERE id = ?`\n\n\tvar shareData share.Data\n\terr := r.db.db.QueryRowContext(ctx, query, id).Scan(\n\t\t&shareData.ID,\n\t\t&shareData.Enable,\n\t\t&shareData.Name,\n\t\t&shareData.Token,\n\t\t&shareData.Gen,\n\t\t&shareData.AccessCount,\n\t\t&shareData.Expires,\n\t\t&shareData.MaxAccessCount,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get share link by id: %w\", err)\n\t}\n\n\treturn &shareData, nil\n}\n\nfunc (r *ShareRepository) Update(ctx context.Context, shareData *share.Data) error {\n\tlog.Debugf(\"Update share\")\n\tquery := `UPDATE share SET enable = ?, name = ?, token = ?, gen = ?, access_count = ?, max_access_count = ?, expires = ? WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\tshareData.Enable,\n\t\tshareData.Name,\n\t\tshareData.Token,\n\t\tshareData.Gen,\n\t\tshareData.AccessCount,\n\t\tshareData.MaxAccessCount,\n\t\tshareData.Expires,\n\t\tshareData.ID,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update share link: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *ShareRepository) UpdateAccessCount(ctx context.Context, shareLinks *[]share.UpdateAccessCountDB) error {\n\tif shareLinks == nil || len(*shareLinks) == 0 {\n\t\treturn nil\n\t}\n\tlog.Debugf(\"Batch update share access count for %d items\", len(*shareLinks))\n\n\ttx, err := r.db.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t}\n\tdefer tx.Rollback()\n\n\tquery := `UPDATE share SET access_count = ? WHERE id = ?`\n\tstmt, err := tx.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to prepare statement: %w\", err)\n\t}\n\tdefer stmt.Close()\n\n\tfor _, shareLink := range *shareLinks {\n\t\t_, err := stmt.ExecContext(ctx, shareLink.AccessCount, shareLink.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update share access count for id %d: %w\", shareLink.ID, err)\n\t\t}\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit transaction: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *ShareRepository) Delete(ctx context.Context, id uint16) error {\n\tlog.Debugf(\"Delete share\")\n\tquery := `DELETE FROM share WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query, id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete share link: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *ShareRepository) List(ctx context.Context) (*[]share.Data, error) {\n\tlog.Debugf(\"List share\")\n\tquery := `SELECT id, enable, name, token, gen, access_count, expires, max_access_count\n\t          FROM share`\n\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list share links: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar shareDatas []share.Data\n\tfor rows.Next() {\n\t\tvar shareData share.Data\n\t\terr := rows.Scan(\n\t\t\t&shareData.ID,\n\t\t\t&shareData.Enable,\n\t\t\t&shareData.Name,\n\t\t\t&shareData.Token,\n\t\t\t&shareData.Gen,\n\t\t\t&shareData.AccessCount,\n\t\t\t&shareData.Expires,\n\t\t\t&shareData.MaxAccessCount,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan share link: %w\", err)\n\t\t}\n\t\tshareDatas = append(shareDatas, shareData)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate share links: %w\", err)\n\t}\n\n\treturn &shareDatas, nil\n}\nfunc (r *ShareRepository) GetGenByToken(ctx context.Context, token string) (string, error) {\n\tlog.Debugf(\"Get config by token\")\n\tquery := `SELECT gen FROM share WHERE token = ?`\n\n\tvar config string\n\terr := r.db.db.QueryRowContext(ctx, query, token).Scan(&config)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get config by token: %w\", err)\n\t}\n\treturn config, nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/sqlite.go",
    "content": "package sqlite\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t_ \"modernc.org/sqlite\"\n)\n\n// DB SQLite数据库连接包装器\ntype DB struct {\n\tdb *sql.DB\n}\n\n// New 创建新的SQLite数据库连接\nfunc New(databasePath string) (interfaces.Repository, error) {\n\tdb, err := sql.Open(\"sqlite\", databasePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open database: %w\", err)\n\t}\n\n\tdb.SetMaxOpenConns(1)\n\tdb.SetMaxIdleConns(1)\n\tdb.SetConnMaxLifetime(time.Hour)\n\n\tif err := enablePragmas(db); err != nil {\n\t\tdb.Close()\n\t\treturn nil, fmt.Errorf(\"failed to set pragmas: %w\", err)\n\t}\n\trepository := DB{db: db}\n\n\treturn &repository, nil\n}\n\n// Close 关闭数据库连接\nfunc (db *DB) Close() error {\n\treturn db.db.Close()\n}\n\n// enablePragmas 启用SQLite优化选项\nfunc enablePragmas(db *sql.DB) error {\n\tpragmas := map[string]string{\n\t\t\"journal_mode\":       \"WAL\",    // 启用WAL模式提高并发性能\n\t\t\"synchronous\":        \"NORMAL\", // 平衡性能和安全性\n\t\t\"cache_size\":         \"-64000\", // 64MB缓存\n\t\t\"foreign_keys\":       \"ON\",     // 启用外键约束\n\t\t\"temp_store\":         \"MEMORY\", // 临时表存储在内存中\n\t\t\"busy_timeout\":       \"5000\",   // 5秒忙等待超时\n\t\t\"wal_autocheckpoint\": \"1000\",   // WAL自动检查点\n\t\t\"optimize\":           \"\",       // 优化数据库\n\t}\n\n\tfor key, value := range pragmas {\n\t\tvar query string\n\t\tif value == \"\" {\n\t\t\tquery = fmt.Sprintf(\"PRAGMA %s\", key)\n\t\t} else {\n\t\t\tquery = fmt.Sprintf(\"PRAGMA %s = %s\", key, value)\n\t\t}\n\n\t\tif _, err := db.Exec(query); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute pragma %s: %w\", query, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/storage.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/storage\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\n// StorageRepository 存储配置数据访问实现\ntype StorageRepository struct {\n\tdb *DB\n}\n\n// newStorageRepository 创建存储配置仓库\nfunc (db *DB) Storage() interfaces.StorageRepository {\n\treturn &StorageRepository{db: db}\n}\n\n// Create 创建存储配置\nfunc (r *StorageRepository) Create(ctx context.Context, config *storage.Data) error {\n\tlog.Debugf(\"Create storage config\")\n\tquery := `INSERT INTO storage (name, type, config)\n\t          VALUES (?, ?, ?)`\n\n\tresult, err := r.db.db.ExecContext(ctx, query,\n\t\tconfig.Name,\n\t\tconfig.Type,\n\t\tconfig.Config,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create storage config: %w\", err)\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get storage config id: %w\", err)\n\t}\n\n\tconfig.ID = uint16(id)\n\n\treturn nil\n}\n\n// GetByID 根据ID获取存储配置\nfunc (r *StorageRepository) GetByID(ctx context.Context, id uint16) (*storage.Data, error) {\n\tlog.Debugf(\"Get storage config by id\")\n\tquery := `SELECT id, name, type, config\n\t          FROM storage WHERE id = ?`\n\n\tvar config storage.Data\n\terr := r.db.db.QueryRowContext(ctx, query, id).Scan(\n\t\t&config.ID,\n\t\t&config.Name,\n\t\t&config.Type,\n\t\t&config.Config,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get storage config by id: %w\", err)\n\t}\n\n\treturn &config, nil\n}\n\n// Update 更新存储配置\nfunc (r *StorageRepository) Update(ctx context.Context, config *storage.Data) error {\n\tlog.Debugf(\"Update storage config\")\n\tquery := `UPDATE storage SET name = ?, type = ?, config = ? WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\tconfig.Name,\n\t\tconfig.Type,\n\t\tconfig.Config,\n\t\tconfig.ID,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update storage config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Delete 删除存储配置\nfunc (r *StorageRepository) Delete(ctx context.Context, id uint16) error {\n\tlog.Debugf(\"Delete storage config\")\n\tquery := `DELETE FROM storage WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query, id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete storage config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// List 获取存储配置列表\nfunc (r *StorageRepository) List(ctx context.Context) (*[]storage.Data, error) {\n\tlog.Debugf(\"List storage configs\")\n\tquery := `SELECT id, name, type, config\n\t          FROM storage`\n\n\tvar configs []storage.Data\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list storage configs: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar config storage.Data\n\t\terr := rows.Scan(\n\t\t\t&config.ID,\n\t\t\t&config.Name,\n\t\t\t&config.Type,\n\t\t\t&config.Config,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan storage config: %w\", err)\n\t\t}\n\t\tconfigs = append(configs, config)\n\t}\n\n\treturn &configs, nil\n}\n"
  },
  {
    "path": "internal/database/client/sqlite/sub.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/sub\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype SubRepository struct {\n\tdb *DB\n}\n\nfunc (db *DB) Sub() interfaces.SubRepository {\n\treturn &SubRepository{db: db}\n}\n\nfunc (r *SubRepository) Create(ctx context.Context, link *sub.Data) error {\n\tlog.Debugf(\"Create sub\")\n\tquery := `INSERT INTO sub (enable, name, tags, cron_expr, config, created_at, updated_at)\n\t          VALUES (?, ?, ?, ?, ?, ?, ?)`\n\n\tnow := time.Now()\n\tresult, err := r.db.db.ExecContext(ctx, query,\n\t\tlink.Enable,\n\t\tlink.Name,\n\t\tlink.Tags,\n\t\tlink.CronExpr,\n\t\tlink.Config,\n\t\tnow,\n\t\tnow,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create sub: %w\", err)\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get sub id: %w\", err)\n\t}\n\n\tlink.ID = uint16(id)\n\tlink.CreatedAt = now\n\tlink.UpdatedAt = now\n\n\treturn nil\n}\n\nfunc (r *SubRepository) GetByID(ctx context.Context, id uint16) (*sub.Data, error) {\n\tlog.Debugf(\"Get sub by id\")\n\tquery := `SELECT id, enable, name, tags, cron_expr, config, result, created_at, updated_at\n\t          FROM sub WHERE id = ?`\n\n\tvar s sub.Data\n\terr := r.db.db.QueryRowContext(ctx, query, id).Scan(\n\t\t&s.ID,\n\t\t&s.Enable,\n\t\t&s.Name,\n\t\t&s.Tags,\n\t\t&s.CronExpr,\n\t\t&s.Config,\n\t\t&s.Result,\n\t\t&s.CreatedAt,\n\t\t&s.UpdatedAt,\n\t)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get sub by id: %w\", err)\n\t}\n\n\treturn &s, nil\n}\n\nfunc (r *SubRepository) Update(ctx context.Context, data *sub.Data) error {\n\tlog.Debugf(\"Update sub\")\n\tquery := `UPDATE sub SET enable = ?, name = ?, tags = ?, cron_expr = ?, config = ?, result = ?, updated_at = ? WHERE id = ?`\n\tdata.UpdatedAt = time.Now()\n\t_, err := r.db.db.ExecContext(ctx, query,\n\t\tdata.Enable,\n\t\tdata.Name,\n\t\tdata.Tags,\n\t\tdata.CronExpr,\n\t\tdata.Config,\n\t\tdata.Result,\n\t\tdata.UpdatedAt,\n\t\tdata.ID,\n\t)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update sub: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *SubRepository) Delete(ctx context.Context, id uint16) error {\n\tlog.Debugf(\"Delete sub\")\n\tquery := `DELETE FROM sub WHERE id = ?`\n\n\t_, err := r.db.db.ExecContext(ctx, query, id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete sub: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *SubRepository) List(ctx context.Context) (*[]sub.Data, error) {\n\tlog.Debugf(\"List sub\")\n\tquery := `SELECT id, enable, name, tags, cron_expr, config, result, created_at, updated_at\n\t          FROM sub ORDER BY id DESC`\n\n\trows, err := r.db.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list sub: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar subs []sub.Data\n\tfor rows.Next() {\n\t\tvar s sub.Data\n\t\terr := rows.Scan(\n\t\t\t&s.ID,\n\t\t\t&s.Enable,\n\t\t\t&s.Name,\n\t\t\t&s.Tags,\n\t\t\t&s.CronExpr,\n\t\t\t&s.Config,\n\t\t\t&s.Result,\n\t\t\t&s.CreatedAt,\n\t\t\t&s.UpdatedAt,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan sub: %w\", err)\n\t\t}\n\t\tsubs = append(subs, s)\n\t}\n\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to iterate subs: %w\", err)\n\t}\n\n\treturn &subs, nil\n}\n\nfunc (r *SubRepository) BatchCreate(ctx context.Context, links []*sub.Data) error {\n\tlog.Debugf(\"Batch create %d subs\", len(links))\n\tif len(links) == 0 {\n\t\treturn nil\n\t}\n\n\ttx, err := r.db.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t}\n\t}()\n\n\tquery := `INSERT INTO sub (enable, name, tags, cron_expr, config, created_at, updated_at)\n\t          VALUES (?, ?, ?, ?, ?, ?, ?)`\n\n\tnow := time.Now()\n\tstmt, err := tx.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to prepare statement: %w\", err)\n\t}\n\tdefer stmt.Close()\n\n\tfor _, link := range links {\n\t\tresult, err := stmt.ExecContext(ctx,\n\t\t\tlink.Enable,\n\t\t\tlink.Name,\n\t\t\tlink.Tags,\n\t\t\tlink.CronExpr,\n\t\t\tlink.Config,\n\t\t\tnow,\n\t\t\tnow,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute batch insert: %w\", err)\n\t\t}\n\n\t\tid, err := result.LastInsertId()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get sub id: %w\", err)\n\t\t}\n\n\t\tlink.ID = uint16(id)\n\t\tlink.CreatedAt = now\n\t\tlink.UpdatedAt = now\n\t}\n\n\tif err = tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit transaction: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/database.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/client/sqlite\"\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nfunc Initialize(sqltype, path string) error {\n\tvar err error\n\tvar repo interfaces.Repository\n\tswitch sqltype {\n\tcase \"sqlite\":\n\t\trepo, err = sqlite.New(path)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to create sqlite database: %v\", err)\n\t\t}\n\tdefault:\n\t\tlog.Fatalf(\"unsupported database type: %s\", sqltype)\n\t}\n\top.SetRepo(repo)\n\tif err := repo.Migrate(); err != nil {\n\t\tlog.Fatalf(\"failed to migrate database: %v\", err)\n\t}\n\tif err := initAuth(context.Background(), op.AuthRepo()); err != nil {\n\t\tlog.Fatalf(\"failed to initialize auth: %v\", err)\n\t}\n\tif err := initSystemSetting(context.Background(), op.SettingRepo()); err != nil {\n\t\tlog.Fatalf(\"failed to initialize system config: %v\", err)\n\t}\n\tif err := initNotifyTemplate(context.Background(), op.NotifyTemplateRepo()); err != nil {\n\t\tlog.Fatalf(\"failed to initialize notify templates: %v\", err)\n\t}\n\treturn nil\n}\nfunc Close() error {\n\tif err := op.Close(); err != nil {\n\t\tlog.Errorf(\"failed to close database: %v\", err)\n\t\treturn err\n\t}\n\tlog.Debugf(\"database closed\")\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/init.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tauthModel \"github.com/bestruirui/bestsub/internal/models/auth\"\n\t\"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc initAuth(ctx context.Context, auth interfaces.AuthRepository) error {\n\tisInitialized, err := auth.IsInitialized(ctx)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to check if database is initialized: %v\", err)\n\t}\n\tif !isInitialized {\n\t\tauthData := authModel.Default()\n\t\thashedBytes, err := bcrypt.GenerateFromPassword([]byte(authData.Password), bcrypt.DefaultCost)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to hash password: %v\", err)\n\t\t}\n\t\tauthData.Password = string(hashedBytes)\n\t\tif err := auth.Initialize(ctx, &authData); err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize auth: %v\", err)\n\t\t}\n\t\tlog.Info(\"初始化默认管理员账号 用户名: admin 密码: admin\")\n\t}\n\treturn nil\n}\nfunc initSystemSetting(ctx context.Context, systemSetting interfaces.SettingRepository) error {\n\tdefaultSystemSetting := setting.DefaultSetting()\n\texistingSystemSetting, err := op.GetAllSetting(ctx)\n\tnotExistSetting := make([]setting.Setting, 0)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to get existing system setting: %v\", err)\n\t}\n\n\texistingSystemSettingMap := make(map[string]bool)\n\tupdateSetting := make([]setting.Setting, 0)\n\tfor _, item := range existingSystemSetting {\n\t\texistingSystemSettingMap[item.Key] = true\n\t}\n\tif len(updateSetting) > 0 {\n\t\tif err := systemSetting.Update(ctx, &updateSetting); err != nil {\n\t\t\tlog.Fatalf(\"failed to update system setting: %v\", err)\n\t\t}\n\t}\n\n\tfor _, s := range defaultSystemSetting {\n\t\tif !existingSystemSettingMap[s.Key] {\n\t\t\tnotExistSetting = append(notExistSetting, s)\n\t\t}\n\t}\n\n\tif len(notExistSetting) > 0 {\n\t\tif err := systemSetting.Create(ctx, &notExistSetting); err != nil {\n\t\t\tlog.Fatalf(\"failed to create missing system setting: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\nfunc initNotifyTemplate(ctx context.Context, notifyTemplateRepo interfaces.NotifyTemplateRepository) error {\n\tdefaultNotifyTemplates := notify.DefaultTemplates()\n\texistingNotifyTemplates, err := notifyTemplateRepo.List(ctx)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to get existing notify templates: %v\", err)\n\t}\n\texistingNotifyTemplatesMap := make(map[string]bool)\n\tfor _, template := range *existingNotifyTemplates {\n\t\texistingNotifyTemplatesMap[template.Type] = true\n\t}\n\tfor _, template := range defaultNotifyTemplates {\n\t\tif !existingNotifyTemplatesMap[template.Type] {\n\t\t\tif err := notifyTemplateRepo.Create(ctx, &template); err != nil {\n\t\t\t\tlog.Fatalf(\"failed to create missing notify template %s: %v\", template.Type, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/interfaces/auth.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/auth\"\n)\n\n// 单用户认证数据访问接口\ntype AuthRepository interface {\n\t// 获取认证信息\n\tGet(ctx context.Context) (*auth.Data, error)\n\n\t// 更新用户名\n\tUpdateName(ctx context.Context, name string) error\n\n\t// 更新密码\n\tUpdatePassword(ctx context.Context, hashPassword string) error\n\n\t// 初始化认证信息（首次创建密码）\n\tInitialize(ctx context.Context, auth *auth.Data) error\n\n\t// 验证是否已初始化\n\tIsInitialized(ctx context.Context) (bool, error)\n}\n"
  },
  {
    "path": "internal/database/interfaces/check.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/check\"\n)\n\ntype CheckRepository interface {\n\tCreate(ctx context.Context, check *check.Data) error\n\tUpdate(ctx context.Context, check *check.Data) error\n\tDelete(ctx context.Context, id uint16) error\n\tGetByID(ctx context.Context, id uint16) (*check.Data, error)\n\tList(ctx context.Context) (*[]check.Data, error)\n}\n"
  },
  {
    "path": "internal/database/interfaces/notify.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/notify\"\n)\n\n// NotificationChannelRepository 通知渠道数据访问接口\ntype NotifyRepository interface {\n\t// Create 创建通知渠道\n\tCreate(ctx context.Context, channel *notify.Data) error\n\n\t// GetByID  根据ID获取通知渠道\n\tGetByID(ctx context.Context, id uint16) (*notify.Data, error)\n\n\t// Update 更新通知渠道\n\tUpdate(ctx context.Context, channel *notify.Data) error\n\n\t// Delete 删除通知渠道\n\tDelete(ctx context.Context, id uint16) error\n\n\t// List 获取通知渠道列表\n\tList(ctx context.Context) (*[]notify.Data, error)\n}\n\n// NotificationTemplateRepository 通知模板数据访问接口\ntype NotifyTemplateRepository interface {\n\t// Create 创建通知模板\n\tCreate(ctx context.Context, template *notify.Template) error\n\n\t// GetByType 根据类型获取通知模板\n\tGetByType(ctx context.Context, t string) (*notify.Template, error)\n\n\t// Update 更新通知模板\n\tUpdate(ctx context.Context, template *notify.Template) error\n\n\t// List 获取通知模板列表\n\tList(ctx context.Context) (*[]notify.Template, error)\n}\n"
  },
  {
    "path": "internal/database/interfaces/repository.go",
    "content": "package interfaces\n\n// Repository 统一的仓库接口\ntype Repository interface {\n\tAuth() AuthRepository\n\n\tSetting() SettingRepository\n\n\tNotify() NotifyRepository\n\tNotifyTemplate() NotifyTemplateRepository\n\n\tCheck() CheckRepository\n\n\tSub() SubRepository\n\tShare() ShareRepository\n\n\tStorage() StorageRepository\n\n\tClose() error\n\tMigrate() error\n}\n"
  },
  {
    "path": "internal/database/interfaces/setting.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n)\n\ntype SettingRepository interface {\n\tCreate(ctx context.Context, setting *[]setting.Setting) error\n\n\tGetAll(ctx context.Context) (*[]setting.Setting, error)\n\n\tGetByKey(ctx context.Context, key []string) (*[]setting.Setting, error)\n\n\tUpdate(ctx context.Context, data *[]setting.Setting) error\n}\n"
  },
  {
    "path": "internal/database/interfaces/share.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/share\"\n)\n\n// 分享链接数据访问接口\ntype ShareRepository interface {\n\t// Create 创建分享链接\n\tCreate(ctx context.Context, shareLink *share.Data) error\n\n\t// GetByID 根据ID获取分享链接\n\tGetByID(ctx context.Context, id uint16) (*share.Data, error)\n\n\t// Update 更新分享链接\n\tUpdate(ctx context.Context, shareLink *share.Data) error\n\n\t// UpdateAccessCount 更新分享链接访问次数\n\tUpdateAccessCount(ctx context.Context, shareLink *[]share.UpdateAccessCountDB) error\n\n\t// GetConfigByToken 根据token获取分享链接配置\n\tGetGenByToken(ctx context.Context, token string) (string, error)\n\n\t// Delete 删除分享链接\n\tDelete(ctx context.Context, id uint16) error\n\n\t// List 获取分享链接列表\n\tList(ctx context.Context) (*[]share.Data, error)\n}\n"
  },
  {
    "path": "internal/database/interfaces/storage.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/storage\"\n)\n\n// 存储配置数据访问接口\ntype StorageRepository interface {\n\t// Create 创建存储配置\n\tCreate(ctx context.Context, config *storage.Data) error\n\n\t// GetByID 根据ID获取存储配置\n\tGetByID(ctx context.Context, id uint16) (*storage.Data, error)\n\n\t// Update 更新存储配置\n\tUpdate(ctx context.Context, config *storage.Data) error\n\n\t// Delete 删除存储配置\n\tDelete(ctx context.Context, id uint16) error\n\n\t// List 获取存储配置列表\n\tList(ctx context.Context) (*[]storage.Data, error)\n}\n"
  },
  {
    "path": "internal/database/interfaces/sub.go",
    "content": "package interfaces\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/sub\"\n)\n\n// SubRepository 订阅链接数据访问接口\ntype SubRepository interface {\n\t// Create 创建链接\n\tCreate(ctx context.Context, link *sub.Data) error\n\n\t// GetByID 根据ID获取链接\n\tGetByID(ctx context.Context, id uint16) (*sub.Data, error)\n\n\t// Update 更新链接\n\tUpdate(ctx context.Context, link *sub.Data) error\n\n\t// Delete 删除链接\n\tDelete(ctx context.Context, id uint16) error\n\n\t// List 获取订阅链接列表\n\tList(ctx context.Context) (*[]sub.Data, error)\n\n\t// BatchCreate 批量创建订阅链接\n\tBatchCreate(ctx context.Context, links []*sub.Data) error\n}\n"
  },
  {
    "path": "internal/database/migration/migration.go",
    "content": "package migration\n\nimport (\n\t\"sort\"\n)\n\ntype Info struct {\n\tDate        uint64\n\tVersion     string\n\tDescription string\n\tContent     func() string\n}\n\nvar clientMigrations = make(map[string][]*Info)\n\nfunc Register(client string, date uint64, version, description string, contentFunc func() string) {\n\tinfo := &Info{\n\t\tDate:        date,\n\t\tVersion:     version,\n\t\tDescription: description,\n\t\tContent:     contentFunc,\n\t}\n\n\tmigrations := clientMigrations[client]\n\n\tindex := sort.Search(len(migrations), func(i int) bool {\n\t\treturn migrations[i].Date > date\n\t})\n\n\tmigrations = append(migrations, nil)\n\tcopy(migrations[index+1:], migrations[index:])\n\tmigrations[index] = info\n\n\tclientMigrations[client] = migrations\n}\n\nfunc Get(client string) []*Info {\n\tif migrations := clientMigrations[client]; migrations != nil {\n\t\tclientMigrations = nil\n\t\treturn migrations\n\t}\n\treturn make([]*Info, 0)\n}\n"
  },
  {
    "path": "internal/database/op/auth.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/auth\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nvar authRepo interfaces.AuthRepository\nvar authData *auth.Data\n\nfunc AuthRepo() interfaces.AuthRepository {\n\tif authRepo == nil {\n\t\tauthRepo = repo.Auth()\n\t}\n\treturn authRepo\n}\n\nfunc AuthGet() (auth.Data, error) {\n\tvar err error\n\tif authData == nil {\n\t\tauthData, err = AuthRepo().Get(context.Background())\n\t}\n\treturn *authData, err\n}\nfunc AuthUpdateName(name string) error {\n\tif authData == nil {\n\t\tAuthGet()\n\t}\n\tauthData.UserName = name\n\terr := AuthRepo().UpdateName(context.Background(), name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\nfunc AuthUpdatePassWord(password string) error {\n\tif authData == nil {\n\t\tAuthGet()\n\t}\n\thashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn err\n\t}\n\tauthData.Password = string(hashedBytes)\n\terr = AuthRepo().UpdatePassword(context.Background(), authData.Password)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\nfunc AuthVerify(username, password string) error {\n\tif authData == nil {\n\t\tAuthGet()\n\t}\n\tif authData.UserName != username {\n\t\treturn fmt.Errorf(\"用户名不匹配\")\n\t}\n\treturn bcrypt.CompareHashAndPassword([]byte(authData.Password), []byte(password))\n}\n"
  },
  {
    "path": "internal/database/op/check.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/check\"\n\t\"github.com/bestruirui/bestsub/internal/utils/cache\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nvar checkRepo interfaces.CheckRepository\nvar checkCache = cache.New[uint16, check.Data](16)\n\nfunc CheckRepo() interfaces.CheckRepository {\n\tif checkRepo == nil {\n\t\tcheckRepo = repo.Check()\n\t}\n\treturn checkRepo\n}\nfunc GetCheckByID(id uint16) (check.Data, error) {\n\tif checkCache.Len() == 0 {\n\t\tif err := refreshCheckCache(); err != nil {\n\t\t\treturn check.Data{}, err\n\t\t}\n\t}\n\tif t, ok := checkCache.Get(id); ok {\n\t\treturn t, nil\n\t}\n\treturn check.Data{}, fmt.Errorf(\"check not found\")\n}\nfunc CreateCheck(ctx context.Context, t *check.Data) error {\n\tif checkCache.Len() == 0 {\n\t\tif err := refreshCheckCache(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := CheckRepo().Create(ctx, t); err != nil {\n\t\treturn err\n\t}\n\tcheckCache.Set(t.ID, *t)\n\treturn nil\n}\nfunc UpdateCheck(ctx context.Context, t *check.Data) error {\n\tif checkCache.Len() == 0 {\n\t\tif err := refreshCheckCache(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\toldCheck, ok := checkCache.Get(t.ID)\n\tif !ok {\n\t\treturn fmt.Errorf(\"task not found\")\n\t}\n\tt.Result = oldCheck.Result\n\tif err := CheckRepo().Update(ctx, t); err != nil {\n\t\treturn err\n\t}\n\tcheckCache.Set(t.ID, *t)\n\treturn nil\n}\nfunc UpdateCheckResult(id uint16, result check.Result) error {\n\tif checkCache.Len() == 0 {\n\t\tif err := refreshCheckCache(); err != nil {\n\t\t\tlog.Errorf(\"failed to refresh check cache: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\toldCheck, ok := checkCache.Get(id)\n\tif !ok {\n\t\tlog.Errorf(\"check not found\")\n\t\treturn fmt.Errorf(\"task not found\")\n\t}\n\tvar oldResult check.Result\n\tif oldCheck.Result != \"\" {\n\t\tif err := json.Unmarshal([]byte(oldCheck.Result), &oldResult); err != nil {\n\t\t\tlog.Errorf(\"failed to unmarshal check result: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\toldResult.Msg = result.Msg\n\toldResult.Extra = result.Extra\n\toldResult.LastRun = time.Now()\n\toldResult.Duration = result.Duration\n\tresultBytes, err := json.Marshal(oldResult)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to marshal check result: %v\", err)\n\t\treturn err\n\t}\n\toldCheck.Result = string(resultBytes)\n\tif err := CheckRepo().Update(context.Background(), &oldCheck); err != nil {\n\t\tlog.Errorf(\"failed to update check result: %v\", err)\n\t\treturn err\n\t}\n\tcheckCache.Set(id, oldCheck)\n\treturn nil\n}\nfunc DeleteCheck(ctx context.Context, id uint16) error {\n\tif checkCache.Len() == 0 {\n\t\tif err := refreshCheckCache(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := CheckRepo().Delete(ctx, id); err != nil {\n\t\treturn err\n\t}\n\tcheckCache.Del(id)\n\treturn nil\n}\nfunc GetCheckList() ([]check.Data, error) {\n\ttaskList := checkCache.GetAll()\n\tif len(taskList) == 0 {\n\t\terr := refreshCheckCache()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttaskList = checkCache.GetAll()\n\t}\n\tvar result = make([]check.Data, 0, len(taskList))\n\tfor _, v := range taskList {\n\t\tresult = append(result, v)\n\t}\n\treturn result, nil\n}\nfunc refreshCheckCache() error {\n\tcheckCache.Clear()\n\tchecks, err := CheckRepo().List(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, check := range *checks {\n\t\tcheckCache.Set(check.ID, check)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/op/notify.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/utils/cache\"\n)\n\nvar nr interfaces.NotifyRepository\nvar ntr interfaces.NotifyTemplateRepository\nvar notifyTemplateCache = cache.New[string, string](4)\nvar notifyCache = cache.New[uint16, notify.Data](4)\n\nfunc notifyRepo() interfaces.NotifyRepository {\n\tif nr == nil {\n\t\tnr = repo.Notify()\n\t}\n\treturn nr\n}\nfunc GetNotifyList() ([]notify.Data, error) {\n\tnotifyList := notifyCache.GetAll()\n\tif len(notifyList) == 0 {\n\t\terr := refreshNotifyCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnotifyList = notifyCache.GetAll()\n\t}\n\tvar result = make([]notify.Data, 0, len(notifyList))\n\tfor _, v := range notifyList {\n\t\tresult = append(result, v)\n\t}\n\treturn result, nil\n}\nfunc GetNotifyByID(id uint16) (notify.Data, error) {\n\tif value, ok := notifyCache.Get(id); ok {\n\t\treturn value, nil\n\t}\n\terr := refreshNotifyCache(context.Background())\n\tif err != nil {\n\t\treturn notify.Data{}, err\n\t}\n\tif value, ok := notifyCache.Get(id); ok {\n\t\treturn value, nil\n\t}\n\treturn notify.Data{}, fmt.Errorf(\"notify not found\")\n}\nfunc UpdateNotify(ctx context.Context, n *notify.Data) error {\n\tif notifyCache.Len() == 0 {\n\t\terr := refreshNotifyCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := notifyRepo().Update(ctx, n); err != nil {\n\t\treturn err\n\t}\n\tnotifyCache.Set(n.ID, *n)\n\treturn nil\n}\nfunc CreateNotify(ctx context.Context, n *notify.Data) error {\n\tif notifyCache.Len() == 0 {\n\t\terr := refreshNotifyCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := notifyRepo().Create(ctx, n); err != nil {\n\t\treturn err\n\t}\n\tnotifyCache.Set(n.ID, *n)\n\treturn nil\n}\nfunc DeleteNotify(ctx context.Context, id uint16) error {\n\tif notifyCache.Len() == 0 {\n\t\terr := refreshNotifyCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := notifyRepo().Delete(ctx, id); err != nil {\n\t\treturn err\n\t}\n\tnotifyCache.Del(id)\n\treturn nil\n}\nfunc refreshNotifyCache(ctx context.Context) error {\n\tnotifyCache.Clear()\n\tnotifyList, err := notifyRepo().List(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, n := range *notifyList {\n\t\tnotifyCache.Set(n.ID, n)\n\t}\n\treturn nil\n}\n\nfunc NotifyTemplateRepo() interfaces.NotifyTemplateRepository {\n\tif ntr == nil {\n\t\tntr = repo.NotifyTemplate()\n\t}\n\treturn ntr\n}\nfunc GetNotifyTemplateList() ([]notify.Template, error) {\n\tnotifyTemplateList := notifyTemplateCache.GetAll()\n\tif len(notifyTemplateList) == 0 {\n\t\terr := refreshNotifyTemplate(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnotifyTemplateList = notifyTemplateCache.GetAll()\n\t}\n\tvar result = make([]notify.Template, 0, len(notifyTemplateList))\n\tfor k, v := range notifyTemplateList {\n\t\tresult = append(result, notify.Template{Type: k, Template: v})\n\t}\n\treturn result, nil\n}\n\nfunc GetNotifyTemplateByType(t string) (string, error) {\n\tif value, ok := notifyTemplateCache.Get(t); ok {\n\t\treturn value, nil\n\t}\n\terr := refreshNotifyTemplate(context.Background())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif value, ok := notifyTemplateCache.Get(t); ok {\n\t\treturn value, nil\n\t}\n\treturn \"\", fmt.Errorf(\"notify template not found\")\n}\nfunc UpdateNotifyTemplate(ctx context.Context, nt *notify.Template) error {\n\tif notifyTemplateCache.Len() == 0 {\n\t\trefreshNotifyTemplate(context.Background())\n\t}\n\tif err := NotifyTemplateRepo().Update(ctx, nt); err != nil {\n\t\treturn err\n\t}\n\tnotifyTemplateCache.Set(nt.Type, nt.Template)\n\treturn nil\n}\nfunc refreshNotifyTemplate(ctx context.Context) error {\n\tnotifyTemplateCache.Clear()\n\tnotifyTemplates, err := NotifyTemplateRepo().List(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, t := range *notifyTemplates {\n\t\tnotifyTemplateCache.Set(t.Type, t.Template)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/op/repo.go",
    "content": "package op\n\nimport (\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n)\n\nvar repo interfaces.Repository\n\nfunc SetRepo(repository interfaces.Repository) {\n\trepo = repository\n}\nfunc Close() error {\n\tupdateAccessCount()\n\treturn repo.Close()\n}\n"
  },
  {
    "path": "internal/database/op/setting.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/utils/cache\"\n)\n\nvar settingRepo interfaces.SettingRepository\nvar settingCache = cache.New[string, string](4)\n\nfunc SettingRepo() interfaces.SettingRepository {\n\tif settingRepo == nil {\n\t\tsettingRepo = repo.Setting()\n\t}\n\treturn settingRepo\n}\nfunc GetAllSettingMap(ctx context.Context) (map[string]string, error) {\n\tsysConfCache := settingCache.GetAll()\n\tif len(sysConfCache) == 0 {\n\t\terr := refreshSettingCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsysConfCache = settingCache.GetAll()\n\t}\n\treturn sysConfCache, nil\n}\nfunc GetAllSetting(ctx context.Context) ([]setting.Setting, error) {\n\tsysConfCache := settingCache.GetAll()\n\tif len(sysConfCache) == 0 {\n\t\terr := refreshSettingCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsysConfCache = settingCache.GetAll()\n\t}\n\tvar result []setting.Setting\n\tfor key, value := range sysConfCache {\n\t\tresult = append(result, setting.Setting{\n\t\t\tKey:   key,\n\t\t\tValue: value,\n\t\t})\n\t}\n\treturn result, nil\n}\nfunc GetSettingByKey(key string) (string, error) {\n\tif value, ok := settingCache.Get(key); ok {\n\t\treturn value, nil\n\t}\n\terr := refreshSettingCache(context.Background())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif value, ok := settingCache.Get(key); ok {\n\t\treturn value, nil\n\t}\n\treturn \"\", fmt.Errorf(\"config not found\")\n}\nfunc UpdateSetting(ctx context.Context, setting *[]setting.Setting) error {\n\tif settingCache.Len() == 0 {\n\t\terr := refreshSettingCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := SettingRepo().Update(ctx, setting); err != nil {\n\t\treturn err\n\t}\n\tfor _, item := range *setting {\n\t\tsettingCache.Set(item.Key, item.Value)\n\t}\n\treturn nil\n\n}\nfunc GetSettingStr(key string) string {\n\tvalue, err := GetSettingByKey(key)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn value\n}\nfunc GetSettingInt(key string) int {\n\tvalue, err := GetSettingByKey(key)\n\tif err != nil {\n\t\treturn 0\n\t}\n\ti, err := strconv.Atoi(value)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn i\n}\nfunc GetSettingBool(key string) bool {\n\tvalue, err := GetSettingByKey(key)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn value == \"true\"\n}\n\nfunc refreshSettingCache(ctx context.Context) error {\n\tsettingCache.Clear()\n\tconfigs, err := SettingRepo().GetAll(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, config := range *configs {\n\t\tsettingCache.Set(config.Key, config.Value)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/op/share.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/share\"\n\t\"github.com/bestruirui/bestsub/internal/utils/cache\"\n\t\"github.com/bestruirui/bestsub/internal/utils/generic\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nvar shareRepo interfaces.ShareRepository\nvar shareCache = cache.New[uint16, share.Data](16)\n\nvar pendingUpdates = &generic.MapOf[uint16, bool]{}\nvar startOnce sync.Once\n\nfunc ShareRepo() interfaces.ShareRepository {\n\tif shareRepo == nil {\n\t\tshareRepo = repo.Share()\n\t}\n\treturn shareRepo\n}\nfunc GetShareList(ctx context.Context) ([]share.Data, error) {\n\tshareList := shareCache.GetAll()\n\tif len(shareList) == 0 {\n\t\terr := refreshShareCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tshareList = shareCache.GetAll()\n\t}\n\tvar result = make([]share.Data, 0, len(shareList))\n\tfor _, v := range shareList {\n\t\tresult = append(result, v)\n\t}\n\treturn result, nil\n}\n\nfunc GetShareByID(ctx context.Context, id uint16) (*share.Data, error) {\n\tif shareCache.Len() == 0 {\n\t\tif err := refreshShareCache(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif s, ok := shareCache.Get(id); ok {\n\t\treturn &s, nil\n\t}\n\treturn nil, fmt.Errorf(\"share not found\")\n}\nfunc GetShareByToken(ctx context.Context, token string) (*share.Data, error) {\n\tif shareCache.Len() == 0 {\n\t\tif err := refreshShareCache(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfor _, s := range shareCache.GetAll() {\n\t\tif s.Token == token {\n\t\t\treturn &s, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"share not found\")\n}\nfunc CreateShare(ctx context.Context, share *share.Data) error {\n\tif shareCache.Len() == 0 {\n\t\tif err := refreshShareCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := ShareRepo().Create(ctx, share); err != nil {\n\t\treturn err\n\t}\n\tshareCache.Set(share.ID, *share)\n\treturn nil\n}\nfunc UpdateShare(ctx context.Context, share *share.Data) error {\n\tif shareCache.Len() == 0 {\n\t\tif err := refreshShareCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\toldShare, ok := shareCache.Get(share.ID)\n\tif !ok {\n\t\treturn fmt.Errorf(\"share not found\")\n\t}\n\tshare.AccessCount = oldShare.AccessCount\n\tif err := ShareRepo().Update(ctx, share); err != nil {\n\t\treturn err\n\t}\n\tshareCache.Set(share.ID, *share)\n\treturn nil\n}\n\nfunc UpdateShareAccessCount(ctx context.Context, id uint16) error {\n\tif shareCache.Len() == 0 {\n\t\tif err := refreshShareCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tshare, ok := shareCache.Get(id)\n\tif !ok {\n\t\treturn fmt.Errorf(\"share not found\")\n\t}\n\tshare.AccessCount++\n\tshareCache.Set(id, share)\n\n\tpendingUpdates.Store(id, true)\n\n\tstartScheduleUpdateAccessCount()\n\n\treturn nil\n}\n\nfunc DeleteShare(ctx context.Context, id uint16) error {\n\tif shareCache.Len() == 0 {\n\t\tif err := refreshShareCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := ShareRepo().Delete(ctx, id); err != nil {\n\t\treturn err\n\t}\n\tshareCache.Del(id)\n\treturn nil\n}\n\nfunc refreshShareCache(ctx context.Context) error {\n\tshareList, err := ShareRepo().List(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, s := range *shareList {\n\t\tshareCache.Set(s.ID, s)\n\t}\n\treturn nil\n}\n\nfunc startScheduleUpdateAccessCount() {\n\tstartOnce.Do(func() {\n\t\tticker := time.NewTicker(60 * time.Second)\n\t\tgo func() {\n\t\t\tdefer ticker.Stop()\n\t\t\tfor range ticker.C {\n\t\t\t\tupdateAccessCount()\n\t\t\t}\n\t\t}()\n\t})\n}\n\nvar updateDataBuffer []share.UpdateAccessCountDB\n\nfunc updateAccessCount() {\n\tupdateDataBuffer = updateDataBuffer[:0]\n\n\tpendingUpdates.Range(func(id uint16, _ bool) bool {\n\t\tif shareData, ok := shareCache.Get(id); ok {\n\t\t\tupdateDataBuffer = append(updateDataBuffer, share.UpdateAccessCountDB{\n\t\t\t\tID:          id,\n\t\t\t\tAccessCount: shareData.AccessCount,\n\t\t\t})\n\t\t}\n\t\treturn true\n\t})\n\tif len(updateDataBuffer) == 0 {\n\t\treturn\n\t}\n\tif err := ShareRepo().UpdateAccessCount(context.Background(), &updateDataBuffer); err != nil {\n\t\tlog.Errorf(\"failed to update share access count: %v\", err)\n\t\treturn\n\t}\n\tfor _, data := range updateDataBuffer {\n\t\tpendingUpdates.Delete(data.ID)\n\t}\n}\n"
  },
  {
    "path": "internal/database/op/storage.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/storage\"\n\t\"github.com/bestruirui/bestsub/internal/utils/cache\"\n)\n\nvar storageRepo interfaces.StorageRepository\nvar storageCache = cache.New[uint16, storage.Data](16)\n\nfunc StorageRepo() interfaces.StorageRepository {\n\tif storageRepo == nil {\n\t\tstorageRepo = repo.Storage()\n\t}\n\treturn storageRepo\n}\nfunc GetStorageList(ctx context.Context) ([]storage.Data, error) {\n\tstorageList := storageCache.GetAll()\n\tif len(storageList) == 0 {\n\t\terr := refreshStorageCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstorageList = storageCache.GetAll()\n\t}\n\tvar result = make([]storage.Data, 0, len(storageList))\n\tfor _, v := range storageList {\n\t\tresult = append(result, v)\n\t}\n\treturn result, nil\n}\n\nfunc GetStorageByID(ctx context.Context, id uint16) (*storage.Data, error) {\n\tif storageCache.Len() == 0 {\n\t\tif err := refreshStorageCache(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif s, ok := storageCache.Get(id); ok {\n\t\treturn &s, nil\n\t}\n\treturn nil, fmt.Errorf(\"storage not found\")\n}\nfunc CreateStorage(ctx context.Context, storage *storage.Data) error {\n\tif storageCache.Len() == 0 {\n\t\tif err := refreshStorageCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := StorageRepo().Create(ctx, storage); err != nil {\n\t\treturn err\n\t}\n\tstorageCache.Set(storage.ID, *storage)\n\treturn nil\n}\nfunc UpdateStorage(ctx context.Context, storage *storage.Data) error {\n\tif storageCache.Len() == 0 {\n\t\tif err := refreshStorageCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := StorageRepo().Update(ctx, storage); err != nil {\n\t\treturn err\n\t}\n\tstorageCache.Set(storage.ID, *storage)\n\treturn nil\n}\n\nfunc DeleteStorage(ctx context.Context, id uint16) error {\n\tif storageCache.Len() == 0 {\n\t\tif err := refreshStorageCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := StorageRepo().Delete(ctx, id); err != nil {\n\t\treturn err\n\t}\n\tstorageCache.Del(id)\n\treturn nil\n}\n\nfunc refreshStorageCache(ctx context.Context) error {\n\tstorageList, err := StorageRepo().List(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, s := range *storageList {\n\t\tstorageCache.Set(s.ID, s)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/database/op/sub.go",
    "content": "package op\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/interfaces\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\tsubModel \"github.com/bestruirui/bestsub/internal/models/sub\"\n\t\"github.com/bestruirui/bestsub/internal/utils/cache\"\n)\n\nvar subRepo interfaces.SubRepository\nvar subCache = cache.New[uint16, subModel.Data](16)\n\nfunc SubRepo() interfaces.SubRepository {\n\tif subRepo == nil {\n\t\tsubRepo = repo.Sub()\n\t}\n\treturn subRepo\n}\nfunc GetSubList(ctx context.Context) ([]subModel.Data, error) {\n\tsubList := subCache.GetAll()\n\tif len(subList) == 0 {\n\t\terr := refreshSubCache(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsubList = subCache.GetAll()\n\t}\n\tvar result = make([]subModel.Data, 0, len(subList))\n\tfor _, v := range subList {\n\t\tresult = append(result, v)\n\t}\n\treturn result, nil\n}\n\nfunc GetSubByID(ctx context.Context, id uint16) (*subModel.Data, error) {\n\tif subCache.Len() == 0 {\n\t\tif err := refreshSubCache(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif s, ok := subCache.Get(id); ok {\n\t\treturn &s, nil\n\t}\n\treturn nil, fmt.Errorf(\"sub not found\")\n}\nfunc GetSubNameByID(ctx context.Context, id uint16) string {\n\tsub, err := GetSubByID(ctx, id)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn sub.Name\n}\nfunc GetSubTagsByID(ctx context.Context, id uint16) []string {\n\tsub, err := GetSubByID(ctx, id)\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\ttags := make([]string, 0)\n\terr = json.Unmarshal([]byte(sub.Tags), &tags)\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\treturn tags\n}\nfunc CreateSub(ctx context.Context, sub *subModel.Data) error {\n\tif subCache.Len() == 0 {\n\t\tif err := refreshSubCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := SubRepo().Create(ctx, sub); err != nil {\n\t\treturn err\n\t}\n\tsubCache.Set(sub.ID, *sub)\n\treturn nil\n}\n\nfunc BatchCreateSub(ctx context.Context, subs []*subModel.Data) error {\n\tif subCache.Len() == 0 {\n\t\tif err := refreshSubCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := SubRepo().BatchCreate(ctx, subs); err != nil {\n\t\treturn err\n\t}\n\tfor _, sub := range subs {\n\t\tsubCache.Set(sub.ID, *sub)\n\t}\n\treturn nil\n}\nfunc UpdateSub(ctx context.Context, sub *subModel.Data) error {\n\tif subCache.Len() == 0 {\n\t\tif err := refreshSubCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\toldSub, ok := subCache.Get(sub.ID)\n\tif !ok {\n\t\treturn fmt.Errorf(\"sub not found\")\n\t}\n\tsub.Result = oldSub.Result\n\tsub.CreatedAt = oldSub.CreatedAt\n\tif err := SubRepo().Update(ctx, sub); err != nil {\n\t\treturn err\n\t}\n\tsubCache.Set(sub.ID, *sub)\n\treturn nil\n}\nfunc UpdateSubResult(ctx context.Context, id uint16, result subModel.Result) error {\n\tif subCache.Len() == 0 {\n\t\tif err := refreshSubCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tsub, ok := subCache.Get(id)\n\tif !ok {\n\t\treturn fmt.Errorf(\"sub not found\")\n\t}\n\tvar oldStatus subModel.Result\n\tjson.Unmarshal([]byte(sub.Result), &oldStatus)\n\n\tresult.Success += oldStatus.Success\n\tresult.Fail += oldStatus.Fail\n\tif result.NodeNullCount != 0 {\n\t\tresult.NodeNullCount += oldStatus.NodeNullCount\n\t}\n\tif (result.NodeNullCount > uint16(GetSettingInt(setting.SUB_DISABLE_AUTO))) && GetSettingInt(setting.SUB_DISABLE_AUTO) != 0 {\n\t\tsub.Enable = false\n\t}\n\tbytes, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsub.Result = string(bytes)\n\tif err := SubRepo().Update(ctx, &sub); err != nil {\n\t\treturn err\n\t}\n\tsubCache.Set(id, sub)\n\treturn nil\n}\nfunc DeleteSub(ctx context.Context, id uint16) error {\n\tif subCache.Len() == 0 {\n\t\tif err := refreshSubCache(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := SubRepo().Delete(ctx, id); err != nil {\n\t\treturn err\n\t}\n\tsubCache.Del(id)\n\treturn nil\n}\nfunc refreshSubCache(ctx context.Context) error {\n\tsubList, err := SubRepo().List(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, s := range *subList {\n\t\tsubCache.Set(s.ID, s)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/models/auth/auth.go",
    "content": "package auth\n\nimport \"time\"\n\ntype Data struct {\n\tID       uint8  `db:\"id\" json:\"-\"`\n\tUserName string `db:\"username\" json:\"username\"`\n\tPassword string `db:\"password\" json:\"-\"`\n}\n\ntype LoginRequest struct {\n\tUsername string `json:\"username\" binding:\"required\" example:\"admin\"`\n\tPassword string `json:\"password\" binding:\"required\" example:\"admin\"`\n}\n\ntype LoginResponse struct {\n\tAccessToken     string    `json:\"access_token\" example:\"access_token_string\"`\n\tAccessExpiresAt time.Time `json:\"access_expires_at\" example:\"2024-01-01T12:00:00Z\"`\n}\n\ntype ChangePasswordRequest struct {\n\tUsername    string `json:\"username\" binding:\"required\" example:\"admin\"`\n\tOldPassword string `json:\"old_password\" binding:\"required\" example:\"old_password\"`\n\tNewPassword string `json:\"new_password\" binding:\"required\" example:\"new_password\"`\n}\n\ntype UpdateUserInfoRequest struct {\n\tUsername string `json:\"username\" binding:\"required\" example:\"admin\"`\n}\ntype LoginNotify struct {\n\tUsername  string `json:\"username\"`\n\tIP        string `json:\"ip\"`\n\tTime      string `json:\"time\"`\n\tMsg       string `json:\"msg\"`\n\tUserAgent string `json:\"user_agent\"`\n}\n"
  },
  {
    "path": "internal/models/auth/default.go",
    "content": "package auth\n\nfunc Default() Data {\n\treturn Data{0, \"admin\", \"admin\"}\n}\n"
  },
  {
    "path": "internal/models/check/check.go",
    "content": "package check\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype Instance interface {\n\tInit() error\n\tRun(ctx context.Context, log *log.Logger, subID []uint16) Result\n}\n\ntype Data struct {\n\tID     uint16 `db:\"id\" json:\"id\"`\n\tName   string `db:\"name\" json:\"name\" description:\"检测任务名称\"`\n\tEnable bool   `db:\"enable\" json:\"enable\" description:\"是否启用\"`\n\tTask   string `db:\"task\" json:\"task\" description:\"任务配置\"`\n\tConfig string `db:\"config\" json:\"config\" description:\"检测器配置\"`\n\tResult string `db:\"result\" json:\"result\" description:\"检测结果\"`\n}\n\ntype Task struct {\n\tSubIdExclude  bool     `json:\"sub_id_exclude\" example:\"false\" description:\"是否排除订阅ID\"`\n\tSubID         []uint16 `json:\"sub_id\" example:\"1\" description:\"订阅ID\"`\n\tCronExpr      string   `json:\"cron_expr\" example:\"0 0 * * *\" description:\"cron表达式\"`\n\tNotify        bool     `json:\"notify\" example:\"true\" description:\"是否通知\"`\n\tNotifyChannel int      `json:\"notify_channel\" example:\"1\" description:\"通知渠道\"`\n\tLogWriteFile  bool     `json:\"log_write_file\" example:\"true\" description:\"是否写入日志文件\"`\n\tLogLevel      string   `json:\"log_level\" example:\"info\" description:\"日志级别\"`\n\tTimeout       int      `json:\"timeout\" example:\"60\" description:\"超时时间 分钟\"`\n\tType          string   `json:\"type\" example:\"test\" description:\"任务类型\"`\n}\n\ntype Result struct {\n\tMsg      string    `json:\"msg\" description:\"消息\"`\n\tExtra    any       `json:\"extra\" description:\"额外信息\"`\n\tLastRun  time.Time `json:\"last_run\" description:\"上次运行时间\"`\n\tDuration int64     `json:\"duration\" description:\"运行时长(单位:毫秒)\"`\n}\n\ntype Request struct {\n\tName   string `db:\"name\" json:\"name\" example:\"测试检测任务\" description:\"检测任务名称\"`\n\tEnable bool   `db:\"enable\" json:\"enable\" description:\"是否启用\"`\n\tTask   Task   `db:\"task\" json:\"task\" description:\"任务配置\"`\n\tConfig any    `db:\"config\" json:\"config\" description:\"检测器配置\"`\n}\n\ntype Response struct {\n\tID     uint16 `db:\"id\" json:\"id\" description:\"检测任务ID\"`\n\tName   string `db:\"name\" json:\"name\" description:\"检测任务名称\"`\n\tEnable bool   `db:\"enable\" json:\"enable\" description:\"是否启用\"`\n\tTask   Task   `db:\"task\" json:\"task\" description:\"任务配置\"`\n\tConfig any    `db:\"config\" json:\"config\" description:\"检测器配置\"`\n\tStatus string `db:\"-\" json:\"status\" description:\"检测状态\"`\n\tResult Result `db:\"result\" json:\"result\" description:\"检测结果\"`\n}\n\nfunc (r *Data) GenResponse(status string) Response {\n\tvar resp Response\n\tresp.ID = r.ID\n\tresp.Name = r.Name\n\tresp.Enable = r.Enable\n\tresp.Status = status\n\tif err := json.Unmarshal([]byte(r.Task), &resp.Task); err != nil {\n\t\treturn resp\n\t}\n\tif err := json.Unmarshal([]byte(r.Config), &resp.Config); err != nil {\n\t\treturn resp\n\t}\n\tif err := json.Unmarshal([]byte(r.Result), &resp.Result); err != nil {\n\t\treturn resp\n\t}\n\treturn resp\n}\n\nfunc (r *Request) GenData() Data {\n\tvar data Data\n\ttaskBytes, err := json.Marshal(r.Task)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to marshal task: %v\", err)\n\t\treturn data\n\t}\n\ttaskStr := string(taskBytes)\n\tconfigBytes, err := json.Marshal(r.Config)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to marshal config: %v\", err)\n\t\treturn data\n\t}\n\tconfigStr := string(configBytes)\n\tdata.Task = taskStr\n\tdata.Config = configStr\n\tdata.Name = r.Name\n\tdata.Enable = r.Enable\n\treturn data\n}\n"
  },
  {
    "path": "internal/models/common/base.go",
    "content": "package common\n\nimport \"time\"\n\n// BaseDbModel 基础模型，包含所有实体的公共字段\ntype BaseDbModel struct {\n\tID          uint16    `db:\"id\" json:\"id\"`\n\tEnable      bool      `db:\"enable\" json:\"enable\"`\n\tName        string    `db:\"name\" json:\"name\"`\n\tDescription string    `db:\"description\" json:\"description\"`\n\tCreatedAt   time.Time `db:\"created_at\" json:\"created_at\"`\n\tUpdatedAt   time.Time `db:\"updated_at\" json:\"updated_at\"`\n}\n\ntype BaseRequestModel struct {\n\tEnable      *bool  `json:\"enable\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n}\ntype BaseUpdateRequestModel struct {\n\tID          uint16 `json:\"id\"`\n\tEnable      *bool  `json:\"enable\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n}\n"
  },
  {
    "path": "internal/models/config/config.go",
    "content": "package config\n\ntype Base struct {\n\tServer   ServerConfig   `json:\"server\"`\n\tDatabase DatabaseConfig `json:\"database\"`\n\tLog      LogConfig      `json:\"log\"`\n\tJWT      JWTConfig      `json:\"jwt\"`\n\tSession  SessionConfig  `json:\"-\"`\n}\n\ntype ServerConfig struct {\n\tPort   int    `json:\"port\"`\n\tHost   string `json:\"host\"`\n\tUIPath string `json:\"-\"`\n}\n\ntype DatabaseConfig struct {\n\tType string `json:\"type\"`\n\tPath string `json:\"-\"`\n}\n\ntype LogConfig struct {\n\tLevel  string `json:\"level\"`\n\tOutput string `json:\"output\"`\n\tPath   string `json:\"-\"`\n}\n\ntype JWTConfig struct {\n\tSecret string `json:\"secret\"`\n}\n\ntype SessionConfig struct {\n\tNodePath string `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/models/config/default.go",
    "content": "package config\n\nfunc DefaultBase() Base {\n\treturn Base{\n\t\tServer: ServerConfig{\n\t\t\tPort: 8080,\n\t\t\tHost: \"0.0.0.0\",\n\t\t},\n\t\tDatabase: DatabaseConfig{\n\t\t\tType: \"sqlite\",\n\t\t},\n\t\tLog: LogConfig{\n\t\t\tLevel:  \"debug\",\n\t\t\tOutput: \"console\",\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/models/node/node.go",
    "content": "package node\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils/generic\"\n\t\"github.com/cespare/xxhash/v2\"\n)\n\nconst (\n\tAlive     uint64 = 1 << 0\n\tCountry   uint64 = 1 << 1\n\tTikTok    uint64 = 1 << 2\n\tTikTokIDC uint64 = 1 << 3\n)\n\ntype Data struct {\n\tBase\n\tInfo *Info\n}\n\ntype Base struct {\n\tRaw       []byte\n\tSubId     uint16\n\tUniqueKey uint64\n}\n\ntype UniqueKey struct {\n\tServer     string `yaml:\"server\"`\n\tServername string `yaml:\"servername\"`\n\tPort       string `yaml:\"port\"`\n\tType       string `yaml:\"type\"`\n\tUuid       string `yaml:\"uuid\"`\n\tUsername   string `yaml:\"username\"`\n\tPassword   string `yaml:\"password\"`\n}\n\ntype Info struct {\n\tSpeedUp     generic.Queue[uint32]\n\tSpeedDown   generic.Queue[uint32]\n\tDelay       generic.Queue[uint16]\n\tRisk        uint8\n\tAliveStatus uint64\n\tIP          uint32\n\tCountry     string\n}\n\ntype SimpleInfo struct {\n\tSpeedUp   uint32 `json:\"speed_up\"`\n\tSpeedDown uint32 `json:\"speed_down\"`\n\tDelay     uint16 `json:\"delay\"`\n\tRisk      uint8  `json:\"risk\"`\n\tCount     uint32 `json:\"count\"`\n}\n\ntype Filter struct {\n\tSubId         []uint16 `json:\"sub_id\"`\n\tSubIdExclude  bool     `json:\"sub_id_exclude\"`\n\tSpeedUpMore   uint32   `json:\"speed_up_more\"`\n\tSpeedDownMore uint32   `json:\"speed_down_more\"`\n\tCountry       []string `json:\"country\"`\n\tCountryExclude bool    `json:\"country_exclude\"`\n\tDelayLessThan uint16   `json:\"delay_less_than\"`\n\tAliveStatus   uint64   `json:\"alive_status\"`\n\tRiskLessThan  uint8    `json:\"risk_less_than\"`\n}\n\nfunc (i *Info) SetAliveStatus(AliveStatus uint64, status bool) {\n\tif status {\n\t\ti.AliveStatus |= AliveStatus\n\t} else {\n\t\ti.AliveStatus &= ^AliveStatus\n\t}\n}\n\nfunc (u *UniqueKey) Gen() uint64 {\n\tbytes, _ := json.Marshal(u)\n\treturn xxhash.Sum64(bytes)\n}\n"
  },
  {
    "path": "internal/models/notify/default.go",
    "content": "package notify\n\nfunc DefaultTemplates() []Template {\n\treturn []Template{\n\t\t// \t{\"login_success\", \"登录成功\", \"{{.Username}}{{.Time}}{{.IP}}{{.UserAgent}}\"},\n\t\t// \t{\"login_failed\", \"登录失败\", \"{{.Username}}{{.Time}}{{.IP}}{{.UserAgent}}\"},\n\t}\n}\n"
  },
  {
    "path": "internal/models/notify/notify.go",
    "content": "package notify\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n)\n\ntype Data struct {\n\tID     uint16 `db:\"id\" json:\"id\"`\n\tName   string `db:\"name\" json:\"name\"`\n\tType   string `db:\"type\" json:\"type\"`\n\tConfig string `db:\"config\" json:\"config\"`\n}\ntype NameAndID struct {\n\tID   uint16 `json:\"id\"`\n\tName string `json:\"name\"`\n}\ntype Request struct {\n\tName   string `json:\"name\"`\n\tType   string `json:\"type\"`\n\tConfig any    `json:\"config\"`\n}\n\ntype Response struct {\n\tID     uint16 `json:\"id\"`\n\tName   string `json:\"name\"`\n\tType   string `json:\"type\"`\n\tConfig any    `json:\"config\"`\n}\n\ntype Template struct {\n\tType     string `db:\"type\" json:\"type\"`\n\tTemplate string `db:\"template\" json:\"template\"`\n}\n\ntype Instance interface {\n\tInit() error\n\tSend(title string, body *bytes.Buffer) error\n}\n\nconst (\n\tTypeLoginSuccess uint16 = 1 << 0 // 登录成功通知\n\tTypeLoginFailed  uint16 = 1 << 1 // 登录失败通知\n)\n\nvar TypeMap = map[uint16]string{\n\tTypeLoginSuccess: \"login_success\",\n\tTypeLoginFailed:  \"login_failed\",\n}\n\nfunc (c *Request) GenData(id uint16) Data {\n\tconfigBytes, err := json.Marshal(c.Config)\n\tif err != nil {\n\t\treturn Data{}\n\t}\n\treturn Data{\n\t\tID:     id,\n\t\tName:   c.Name,\n\t\tType:   c.Type,\n\t\tConfig: string(configBytes),\n\t}\n}\nfunc (d *Data) GenResponse() Response {\n\tvar config any\n\tjson.Unmarshal([]byte(d.Config), &config)\n\treturn Response{\n\t\tID:     d.ID,\n\t\tName:   d.Name,\n\t\tType:   d.Type,\n\t\tConfig: config,\n\t}\n}\n"
  },
  {
    "path": "internal/models/setting/default.go",
    "content": "package setting\n\nfunc DefaultSetting() []Setting {\n\treturn []Setting{\n\n\t\t{\n\t\t\tKey:   PROXY_ENABLE,\n\t\t\tValue: \"false\",\n\t\t},\n\t\t{\n\t\t\tKey:   PROXY_URL,\n\t\t\tValue: \"socks5://user:pass@127.0.0.1:1080\",\n\t\t},\n\t\t{\n\t\t\tKey:   LOG_RETENTION_DAYS,\n\t\t\tValue: \"7\",\n\t\t},\n\t\t{\n\t\t\tKey:   SUBCONV_URL,\n\t\t\tValue: \"\",\n\t\t},\n\t\t{\n\t\t\tKey:   SUBCONV_URL_PROXY,\n\t\t\tValue: \"false\",\n\t\t},\n\t\t{\n\t\t\tKey:   SUB_DISABLE_AUTO,\n\t\t\tValue: \"0\",\n\t\t},\n\t\t{\n\t\t\tKey:   NODE_POOL_SIZE,\n\t\t\tValue: \"1000\",\n\t\t},\n\t\t{\n\t\t\tKey:   NODE_TEST_URL,\n\t\t\tValue: \"https://www.gstatic.com/generate_204\",\n\t\t},\n\t\t{\n\t\t\tKey:   NODE_TEST_TIMEOUT,\n\t\t\tValue: \"5\",\n\t\t},\n\t\t{\n\t\t\tKey:   NODE_PROTOCOL_FILTER_ENABLE,\n\t\t\tValue: \"false\",\n\t\t},\n\t\t{\n\t\t\tKey:   NODE_PROTOCOL_FILTER_MODE,\n\t\t\tValue: \"false\",\n\t\t},\n\t\t{\n\t\t\tKey:   NODE_PROTOCOL_FILTER,\n\t\t\tValue: \"\",\n\t\t},\n\t\t{\n\t\t\tKey:   TASK_MAX_THREAD,\n\t\t\tValue: \"200\",\n\t\t},\n\t\t{\n\t\t\tKey:   TASK_MAX_TIMEOUT,\n\t\t\tValue: \"60\",\n\t\t},\n\t\t{\n\t\t\tKey:   TASK_MAX_RETRY,\n\t\t\tValue: \"3\",\n\t\t},\n\t\t{\n\t\t\tKey:   NOTIFY_OPERATION,\n\t\t\tValue: \"0\",\n\t\t},\n\t\t{\n\t\t\tKey:   NOTIFY_ID,\n\t\t\tValue: \"0\",\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/models/setting/setting.go",
    "content": "package setting\n\ntype Setting struct {\n\tKey   string `json:\"key\"`\n\tValue string `json:\"value\"`\n}\n\nconst (\n\tPROXY_ENABLE = \"proxy_enable\"\n\tPROXY_URL    = \"proxy_url\"\n\n\tLOG_RETENTION_DAYS = \"log_retention_days\"\n\n\tSUBCONV_URL       = \"subconv_url\"\n\tSUBCONV_URL_PROXY = \"subconv_url_proxy\"\n\n\tSUB_DISABLE_AUTO = \"sub_disable_auto\"\n\n\tNODE_POOL_SIZE    = \"node_pool_size\"\n\tNODE_TEST_URL     = \"node_test_url\"\n\tNODE_TEST_TIMEOUT = \"node_test_timeout\"\n\n\tNODE_PROTOCOL_FILTER_ENABLE = \"node_protocol_filter_enable\"\n\tNODE_PROTOCOL_FILTER_MODE   = \"node_protocol_filter_mode\"\n\tNODE_PROTOCOL_FILTER        = \"node_protocol_filter\"\n\n\tTASK_MAX_THREAD  = \"task_max_thread\"\n\tTASK_MAX_TIMEOUT = \"task_max_timeout\"\n\tTASK_MAX_RETRY   = \"task_max_retry\"\n\n\tNOTIFY_OPERATION = \"notify_operation\"\n\tNOTIFY_ID        = \"notify_id\"\n)\n"
  },
  {
    "path": "internal/models/share/share.go",
    "content": "package share\n\nimport (\n\t\"encoding/json\"\n\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n)\n\ntype Data struct {\n\tID             uint16 `db:\"id\" json:\"id\"`\n\tEnable         bool   `db:\"enable\" json:\"enable\"`\n\tName           string `db:\"name\" json:\"name\"`\n\tGen            string `db:\"gen\" json:\"gen\"`\n\tToken          string `db:\"token\" json:\"token\"`\n\tAccessCount    uint32 `db:\"access_count\" json:\"access_count\"`\n\tMaxAccessCount uint32 `db:\"max_access_count\" json:\"max_access_count\"`\n\tExpires        uint64 `db:\"expires\" json:\"expires\"`\n}\n\ntype GenConfig struct {\n\tFilter nodeModel.Filter `json:\"filter\"`\n\tRename string           `json:\"rename\"`\n\tTarget string           `json:\"target\"`\n}\n\ntype Request struct {\n\tEnable         bool      `json:\"enable\"`\n\tName           string    `json:\"name\"`\n\tToken          string    `json:\"token\"`\n\tGen            GenConfig `json:\"gen\"`\n\tMaxAccessCount uint32    `json:\"max_access_count\"`\n\tExpires        uint64    `json:\"expires\"`\n}\n\ntype Response struct {\n\tID             uint16    `json:\"id\"`\n\tName           string    `json:\"name\"`\n\tEnable         bool      `json:\"enable\"`\n\tAccessCount    uint32    `json:\"access_count\"`\n\tMaxAccessCount uint32    `json:\"max_access_count\"`\n\tExpires        uint64    `json:\"expires\"`\n\tToken          string    `json:\"token\"`\n\tGen            GenConfig `json:\"gen\"`\n}\n\ntype UpdateAccessCountDB struct {\n\tID          uint16 `db:\"id\"`\n\tAccessCount uint32 `db:\"access_count\"`\n}\n\nfunc (r *Request) GenData() Data {\n\tconfigBytes, err := json.Marshal(r.Gen)\n\tif err != nil {\n\t\treturn Data{}\n\t}\n\treturn Data{\n\t\tEnable:         r.Enable,\n\t\tName:           r.Name,\n\t\tToken:          r.Token,\n\t\tMaxAccessCount: r.MaxAccessCount,\n\t\tExpires:        r.Expires,\n\t\tGen:            string(configBytes),\n\t}\n}\n\nfunc (r *Data) GenResponse() Response {\n\tvar config GenConfig\n\tif err := json.Unmarshal([]byte(r.Gen), &config); err != nil {\n\t\treturn Response{}\n\t}\n\treturn Response{\n\t\tID:             r.ID,\n\t\tName:           r.Name,\n\t\tEnable:         r.Enable,\n\t\tAccessCount:    r.AccessCount,\n\t\tMaxAccessCount: r.MaxAccessCount,\n\t\tExpires:        r.Expires,\n\t\tToken:          r.Token,\n\t\tGen:            config,\n\t}\n}\n"
  },
  {
    "path": "internal/models/storage/storage.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n)\n\ntype Data struct {\n\tID     uint16 `db:\"id\" json:\"id\"`\n\tName   string `db:\"name\" json:\"name\"`\n\tType   string `db:\"type\" json:\"type\"`\n\tConfig string `db:\"config\" json:\"config\"`\n}\n\ntype Request struct {\n\tName   string `json:\"name\" example:\"webdav\"`\n\tType   string `json:\"type\" example:\"webdav\"`\n\tConfig any    `json:\"config\"`\n}\n\ntype Response struct {\n\tID     uint16 `json:\"id\"`\n\tName   string `json:\"name\"`\n\tType   string `json:\"type\"`\n\tConfig any    `json:\"config\"`\n}\n\ntype Instance interface {\n\tInit() error\n\tUpload(ctx context.Context) error\n}\n\nfunc (r *Request) GenData(id uint16) Data {\n\tconfigBytes, _ := json.Marshal(r.Config)\n\treturn Data{\n\t\tID:     id,\n\t\tName:   r.Name,\n\t\tType:   r.Type,\n\t\tConfig: string(configBytes),\n\t}\n}\n\nfunc (d *Data) GenResponse() Response {\n\tvar config any\n\tjson.Unmarshal([]byte(d.Config), &config)\n\treturn Response{\n\t\tID:     d.ID,\n\t\tName:   d.Name,\n\t\tType:   d.Type,\n\t\tConfig: config,\n\t}\n}\n"
  },
  {
    "path": "internal/models/sub/sub.go",
    "content": "package sub\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\tnodeModel \"github.com/bestruirui/bestsub/internal/models/node\"\n)\n\ntype Data struct {\n\tID        uint16    `db:\"id\" json:\"id\"`\n\tEnable    bool      `db:\"enable\" json:\"enable\"`\n\tName      string    `db:\"name\" json:\"name\"`\n\tTags      string    `db:\"tags\" json:\"tags\"`\n\tCronExpr  string    `db:\"cron_expr\" json:\"cron_expr\"`\n\tConfig    string    `db:\"config\" json:\"config\"`\n\tResult    string    `db:\"result\" json:\"result\"`\n\tCreatedAt time.Time `db:\"created_at\" json:\"created_at\"`\n\tUpdatedAt time.Time `db:\"updated_at\" json:\"updated_at\"`\n}\n\ntype Config struct {\n\tUrl                  string   `json:\"url\"`\n\tProxy                bool     `json:\"proxy\"`\n\tTimeout              int      `json:\"timeout\"`\n\tProtocolFilterEnable bool     `json:\"protocol_filter_enable\"`\n\tProtocolFilterMode   bool     `json:\"protocol_filter_mode\"`\n\tProtocolFilter       []string `json:\"protocol_filter\"`\n}\n\ntype Result struct {\n\tSuccess       uint16    `json:\"success,omitempty\" description:\"成功次数\"`\n\tFail          uint16    `json:\"fail,omitempty\" description:\"失败次数\"`\n\tNodeNullCount uint16    `json:\"node_null_count,omitempty\" description:\"节点为空次数\"`\n\tMsg           string    `json:\"msg,omitempty\" description:\"消息\"`\n\tRawCount      uint32    `json:\"raw_count,omitempty\" description:\"节点数量\"`\n\tLastRun       time.Time `json:\"last_run,omitempty\" description:\"上次运行时间\"`\n\tDuration      uint16    `json:\"duration,omitempty\" description:\"运行时长(单位:毫秒)\"`\n}\n\ntype Request struct {\n\tName     string   `json:\"name\" description:\"订阅任务名称\"`\n\tTags     []string `json:\"tags\" description:\"订阅标签\"`\n\tEnable   bool     `json:\"enable\" description:\"是否启用\"`\n\tCronExpr string   `json:\"cron_expr\" example:\"0 0 * * *\" description:\"cron表达式\"`\n\tConfig   Config   `json:\"config\"`\n}\n\ntype Response struct {\n\tID        uint16               `json:\"id\" description:\"订阅任务ID\"`\n\tName      string               `json:\"name\" description:\"订阅任务名称\"`\n\tTags      []string             `json:\"tags\" description:\"订阅标签\"`\n\tEnable    bool                 `json:\"enable\" description:\"是否启用\"`\n\tCronExpr  string               `json:\"cron_expr\" description:\"cron表达式\"`\n\tConfig    Config               `json:\"config\" description:\"订阅器配置\"`\n\tStatus    string               `json:\"status\" description:\"订阅状态\"`\n\tResult    Result               `json:\"result\" description:\"订阅结果\"`\n\tInfo      nodeModel.SimpleInfo `json:\"info\" description:\"订阅信息\"`\n\tCreatedAt time.Time            `json:\"created_at\" description:\"创建时间\"`\n\tUpdatedAt time.Time            `json:\"updated_at\" description:\"更新时间\"`\n}\n\nfunc (c *Request) GenData(id uint16) Data {\n\tconfigBytes, err := json.Marshal(c.Config)\n\tif err != nil {\n\t\treturn Data{}\n\t}\n\ttags, _ := json.Marshal(c.Tags)\n\treturn Data{\n\t\tID:       id,\n\t\tName:     c.Name,\n\t\tTags:     string(tags),\n\t\tEnable:   c.Enable,\n\t\tCronExpr: c.CronExpr,\n\t\tConfig:   string(configBytes),\n\t}\n}\nfunc (d *Data) GenResponse(status string, subInfo nodeModel.SimpleInfo) Response {\n\tvar config Config\n\tjson.Unmarshal([]byte(d.Config), &config)\n\tvar result Result\n\tjson.Unmarshal([]byte(d.Result), &result)\n\ttags := make([]string, 0)\n\tjson.Unmarshal([]byte(d.Tags), &tags)\n\treturn Response{\n\t\tID:        d.ID,\n\t\tName:      d.Name,\n\t\tTags:      tags,\n\t\tEnable:    d.Enable,\n\t\tCronExpr:  d.CronExpr,\n\t\tConfig:    config,\n\t\tStatus:    status,\n\t\tResult:    result,\n\t\tInfo:      subInfo,\n\t\tCreatedAt: d.CreatedAt,\n\t\tUpdatedAt: d.UpdatedAt,\n\t}\n}\n"
  },
  {
    "path": "internal/models/system/info.go",
    "content": "package system\n\n// HealthResponse 健康检查响应\ntype HealthResponse struct {\n\tStatus    string `json:\"status\" example:\"ok\"`                     // 服务状态\n\tTimestamp string `json:\"timestamp\" example:\"2024-01-01T12:00:00\"` // 检查时间\n\tVersion   string `json:\"version\" example:\"1.0.0\"`                 // 版本信息\n\tDatabase  string `json:\"database\" example:\"connected\"`            // 数据库状态\n}\n\n// 系统信息结构\ntype Info struct {\n\tMemoryUsed    uint64  `json:\"memory_used\"`    // 已使用内存 (bytes)\n\tCPUPercent    float64 `json:\"cpu_percent\"`    // CPU 占用率\n\tStartTime     string  `json:\"start_time\"`     // 启动时间\n\tUploadBytes   uint64  `json:\"upload_bytes\"`   // 上传流量 (bytes)\n\tDownloadBytes uint64  `json:\"download_bytes\"` // 下载流量 (bytes)\n}\n\ntype Version struct {\n\tSubConverterVersion string `json:\"subconverter_version\"`\n\tVersion             string `json:\"version\"`\n\tBuildTime           string `json:\"build_time\"`\n\tCommit              string `json:\"commit\"`\n\tAuthor              string `json:\"author\"`\n\tRepo                string `json:\"repo\"`\n}\n"
  },
  {
    "path": "internal/modules/country/channel/cloudflare.go",
    "content": "package channel\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype CloudflareCDN struct{}\n\nfunc (c *CloudflareCDN) Url() string {\n\treturn \"https://cloudflare.com/cdn-cgi/trace\"\n}\n\nfunc (c *CloudflareCDN) Header(req *http.Request) {\n}\n\nfunc (c *CloudflareCDN) CountryCode(body []byte) string {\n\tprefix := []byte(\"loc=\")\n\tidx := bytes.Index(body, prefix)\n\tif idx == -1 {\n\t\treturn \"\"\n\t}\n\tstart := idx + len(prefix)\n\tendRel := bytes.IndexByte(body[start:], '\\n')\n\tvar v []byte\n\tif endRel == -1 {\n\t\tv = body[start:]\n\t} else {\n\t\tv = body[start : start+endRel]\n\t}\n\tv = bytes.TrimSpace(v)\n\treturn string(v)\n}\n\ntype CloudflareSpeed struct{}\n\nfunc (c *CloudflareSpeed) Url() string {\n\treturn \"https://speed.cloudflare.com/meta\"\n}\n\nfunc (c *CloudflareSpeed) Header(req *http.Request) {\n\tUserAgent(req)\n}\n\nfunc (c *CloudflareSpeed) CountryCode(body []byte) string {\n\tvar speed struct {\n\t\tCountryCode string `json:\"country\"`\n\t}\n\tif err := json.Unmarshal(body, &speed); err != nil {\n\t\treturn \"\"\n\t}\n\treturn speed.CountryCode\n}\n\nfunc init() {\n\tregister(&CloudflareCDN{})\n\tregister(&CloudflareSpeed{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/commen.go",
    "content": "package channel\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils/ua\"\n)\n\ntype Common struct {\n\tCountryCode string `json:\"country_code\"`\n}\n\nfunc UserAgent(req *http.Request) {\n\tua.SetHeader(req)\n}\n"
  },
  {
    "path": "internal/modules/country/channel/freeip.go",
    "content": "package channel\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype FreeIP struct{}\n\nfunc (c *FreeIP) Url() string {\n\treturn \"https://free.freeipapi.com/api/json\"\n}\n\nfunc (c *FreeIP) Header(req *http.Request) {\n\tUserAgent(req)\n}\n\nfunc (c *FreeIP) CountryCode(body []byte) string {\n\tvar freeip struct {\n\t\tCountryCode string `json:\"countryCode\"`\n\t}\n\tif err := json.Unmarshal(body, &freeip); err != nil {\n\t\treturn \"\"\n\t}\n\treturn freeip.CountryCode\n}\n\nfunc init() {\n\tregister(&FreeIP{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/ip_sb.go",
    "content": "package channel\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype IPSB struct{}\n\nfunc (c *IPSB) Url() string {\n\treturn \"https://api.ip.sb/geoip\"\n}\n\nfunc (c *IPSB) Header(req *http.Request) {\n\tUserAgent(req)\n}\n\nfunc (c *IPSB) CountryCode(body []byte) string {\n\tvar ip_sb Common\n\tif err := json.Unmarshal(body, &ip_sb); err != nil {\n\t\treturn \"\"\n\t}\n\treturn ip_sb.CountryCode\n}\n\nfunc init() {\n\tregister(&IPSB{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/ipapi.go",
    "content": "package channel\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype IPAPI struct{}\n\nfunc (c *IPAPI) Url() string {\n\treturn \"https://ipapi.co/json\"\n}\n\nfunc (c *IPAPI) Header(req *http.Request) {\n\n}\n\nfunc (c *IPAPI) CountryCode(body []byte) string {\n\tvar ipapi Common\n\tif err := json.Unmarshal(body, &ipapi); err != nil {\n\t\treturn \"\"\n\t}\n\treturn ipapi.CountryCode\n}\n\nfunc init() {\n\tregister(&IPAPI{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/ipwho.go",
    "content": "package channel\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype IPWho struct{}\n\nfunc (c *IPWho) Url() string {\n\treturn \"https://api.ip.sb/geoip\"\n}\n\nfunc (c *IPWho) Header(req *http.Request) {\n\tUserAgent(req)\n}\n\nfunc (c *IPWho) CountryCode(body []byte) string {\n\tvar ipwho Common\n\tif err := json.Unmarshal(body, &ipwho); err != nil {\n\t\treturn \"\"\n\t}\n\treturn ipwho.CountryCode\n}\n\nfunc init() {\n\tregister(&IPWho{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/myip.go",
    "content": "package channel\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype MYIP struct {\n\tCC string `json:\"cc\"`\n}\n\nfunc (c *MYIP) Url() string {\n\treturn \"https://api.myip.com\"\n}\n\nfunc (c *MYIP) Header(req *http.Request) {\n}\n\nfunc (c *MYIP) CountryCode(body []byte) string {\n\tvar myip MYIP\n\tif err := json.Unmarshal(body, &myip); err != nil {\n\t\treturn \"\"\n\t}\n\treturn myip.CC\n}\n\nfunc init() {\n\tregister(&MYIP{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/reallyfreegeoip.go",
    "content": "package channel\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype ReallyFreeGeoIP struct{}\n\nfunc (c *ReallyFreeGeoIP) Url() string {\n\treturn \"https://reallyfreegeoip.org/json\"\n}\n\nfunc (c *ReallyFreeGeoIP) Header(req *http.Request) {\n\tUserAgent(req)\n}\n\nfunc (c *ReallyFreeGeoIP) CountryCode(body []byte) string {\n\tvar reallyfreegeoip struct {\n\t\tCountryCode string `json:\"country_code\"`\n\t}\n\tif err := json.Unmarshal(body, &reallyfreegeoip); err != nil {\n\t\treturn \"\"\n\t}\n\treturn reallyfreegeoip.CountryCode\n}\n\nfunc init() {\n\tregister(&ReallyFreeGeoIP{})\n}\n"
  },
  {
    "path": "internal/modules/country/channel/register.go",
    "content": "package channel\n\nimport (\n\t\"net/http\"\n)\n\ntype Channel interface {\n\tUrl() string\n\tHeader(req *http.Request)\n\tCountryCode(body []byte) string\n}\n\nvar Channels = make([]Channel, 0)\n\nfunc register(channel Channel) {\n\tChannels = append(Channels, channel)\n}\n"
  },
  {
    "path": "internal/modules/country/country.go",
    "content": "package country\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/modules/country/channel\"\n)\n\nfunc GetCode(ctx context.Context, client *http.Client) string {\n\tfor _, channel := range channel.Channels {\n\t\tctx, cancel := context.WithTimeout(ctx, time.Second*5)\n\t\tdefer cancel()\n\t\trequest, err := http.NewRequestWithContext(ctx, \"GET\", channel.Url(), nil)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tchannel.Header(request)\n\t\tresponse, err := client.Do(request)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tdefer response.Body.Close()\n\t\tbody, err := io.ReadAll(response.Body)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tcountry := channel.CountryCode(body)\n\t\tif country != \"\" {\n\t\t\treturn country\n\t\t}\n\t\tbody = nil\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/modules/notify/channel/email.go",
    "content": "package channel\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/smtp\"\n\t\"strings\"\n\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n)\n\ntype Email struct {\n\tServer   string `json:\"server\" require:\"true\" name:\"SMTP服务器\"`\n\tPort     int    `json:\"port\" require:\"true\" name:\"端口\"`\n\tUsername string `json:\"username\" require:\"true\" name:\"用户名\"`\n\tPassword string `json:\"password\" require:\"true\" name:\"密码\"`\n\tFrom     string `json:\"from\" require:\"true\" name:\"发件人\"`\n\tTo       string `json:\"to\" require:\"true\" name:\"接收人\"`\n\tTLS      bool   `json:\"tls\" require:\"true\" name:\"TLS\"`\n\n\taddr       string\n\tauth       smtp.Auth\n\trecipients []string\n}\n\nfunc (e *Email) Init() error {\n\te.addr = fmt.Sprintf(\"%s:%d\", e.Server, e.Port)\n\te.auth = smtp.PlainAuth(\"\", e.Username, e.Password, e.Server)\n\n\trecipients := strings.Split(e.To, \",\")\n\te.recipients = make([]string, len(recipients))\n\tfor i, recipient := range recipients {\n\t\te.recipients[i] = strings.TrimSpace(recipient)\n\t}\n\n\treturn nil\n}\n\nfunc (e *Email) Send(title string, body *bytes.Buffer) error {\n\tif body == nil {\n\t\treturn fmt.Errorf(\"email body is nil\")\n\t}\n\n\tmessage := e.buildMessage(title, body)\n\n\tif err := e.sendMail(message); err != nil {\n\t\treturn fmt.Errorf(\"send email failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (e *Email) buildMessage(subject string, body *bytes.Buffer) *bytes.Buffer {\n\tvar message bytes.Buffer\n\n\tmessage.WriteString(fmt.Sprintf(\"From: %s\\r\\n\", e.From))\n\tmessage.WriteString(fmt.Sprintf(\"To: %s\\r\\n\", e.To))\n\tmessage.WriteString(fmt.Sprintf(\"Subject: %s\\r\\n\", subject))\n\tmessage.WriteString(\"MIME-Version: 1.0\\r\\n\")\n\tmessage.WriteString(\"Content-Type: text/html; charset=UTF-8\\r\\n\")\n\tmessage.WriteString(\"\\r\\n\")\n\n\tbody.WriteTo(&message)\n\n\treturn &message\n}\nfunc (e *Email) sendMail(message *bytes.Buffer) error {\n\tif e.TLS {\n\t\treturn e.sendMailWithTLS(message)\n\t} else {\n\t\treturn smtp.SendMail(e.addr, e.auth, e.From, e.recipients, message.Bytes())\n\t}\n}\n\nfunc (e *Email) sendMailWithTLS(message *bytes.Buffer) error {\n\ttlsConfig := &tls.Config{\n\t\tServerName: e.Server,\n\t}\n\tconn, err := tls.Dial(\"tcp\", e.addr, tlsConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer conn.Close()\n\n\tclient, err := smtp.NewClient(conn, e.Server)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer client.Quit()\n\n\tif err := client.Auth(e.auth); err != nil {\n\t\treturn err\n\t}\n\n\tif err := client.Mail(e.From); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, recipient := range e.recipients {\n\t\tif err := client.Rcpt(recipient); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\twriter, err := client.Data()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer writer.Close()\n\n\tif _, err := writer.Write(message.Bytes()); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc init() {\n\tregister.Notify(&Email{})\n}\n"
  },
  {
    "path": "internal/modules/notify/channel/webhook.go",
    "content": "package channel\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n)\n\ntype WebHook struct {\n\tUrl string `json:\"url\" name:\"WebHook地址\"`\n}\n\nfunc (e *WebHook) Init() error {\n\treturn nil\n}\n\nfunc (e *WebHook) Send(title string, body *bytes.Buffer) error {\n\treturn nil\n}\n\nfunc init() {\n\tregister.Notify(&WebHook{})\n}\n"
  },
  {
    "path": "internal/modules/notify/notify.go",
    "content": "package notify\n\nimport (\n\t\"bytes\"\n\t\"html/template\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tnotifyModel \"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t_ \"github.com/bestruirui/bestsub/internal/modules/notify/channel\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t\"github.com/bestruirui/bestsub/internal/utils/desc\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\ntype Desc = desc.Data\n\nfunc SendSystemNotify(operation uint16, title string, content any) error {\n\tif operation&uint16(op.GetSettingInt(setting.NOTIFY_OPERATION)) == 0 {\n\t\treturn nil\n\t}\n\n\tnt, err := op.GetNotifyTemplateByType(notifyModel.TypeMap[operation])\n\tif err != nil {\n\t\tlog.Errorf(\"failed to get notify template: %v\", operation)\n\t\treturn err\n\t}\n\n\tt, err := template.New(\"notify\").Parse(nt)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to parse notify template: %v\", err)\n\t\treturn err\n\t}\n\n\tvar buf bytes.Buffer\n\terr = t.Execute(&buf, content)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to execute notify template: %v\", err)\n\t\treturn err\n\t}\n\n\tsysNotifyID := op.GetSettingInt(setting.NOTIFY_ID)\n\tnotifyConfig, err := op.GetNotifyByID(uint16(sysNotifyID))\n\tif err != nil {\n\t\tlog.Errorf(\"failed to get notify config: %v\", sysNotifyID)\n\t\treturn err\n\t}\n\n\tnotify, err := Get(notifyConfig.Type, notifyConfig.Config)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to get notify: %v\", err)\n\t\treturn err\n\t}\n\n\terr = notify.Init()\n\tif err != nil {\n\t\tlog.Errorf(\"failed to init notify: %v\", err)\n\t\treturn err\n\t}\n\n\terr = notify.Send(title, &buf)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to send notify: %v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc Get(m string, c string) (notifyModel.Instance, error) {\n\treturn register.Get[notifyModel.Instance](\"notify\", m, c)\n}\n\nfunc GetChannels() []string {\n\treturn register.GetList(\"notify\")\n}\n\nfunc GetInfoMap() map[string][]desc.Data {\n\treturn register.GetInfoMap(\"notify\")\n}\n"
  },
  {
    "path": "internal/modules/register/category.go",
    "content": "package register\n\nimport (\n\t\"github.com/bestruirui/bestsub/internal/models/check\"\n\t\"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/models/storage\"\n)\n\nfunc Notify(i notify.Instance) {\n\tregister(\"notify\", i)\n}\nfunc Check(i check.Instance) {\n\tregister(\"check\", i)\n}\nfunc Storage(i storage.Instance) {\n\tregister(\"storage\", i)\n}\n"
  },
  {
    "path": "internal/modules/register/register.go",
    "content": "package register\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils/desc\"\n)\n\ntype registerInfo struct {\n\tim  map[string]any\n\taim map[string][]desc.Data\n}\n\nvar registers = map[string]*registerInfo{}\n\nfunc register(t string, i any) {\n\tr, exists := registers[t]\n\tif !exists {\n\t\tr = &registerInfo{\n\t\t\tim:  make(map[string]any),\n\t\t\taim: make(map[string][]desc.Data),\n\t\t}\n\t\tregisters[t] = r\n\t}\n\tm := strings.ToLower(reflect.TypeOf(i).Elem().Name())\n\tr.im[m] = i\n\tr.aim[m] = desc.Gen(i)\n}\n\nfunc Get[T any](t string, m string, c string) (T, error) {\n\tri, exists := registers[t]\n\tif !exists {\n\t\treturn *new(T), errors.New(\"category not found\")\n\t}\n\n\tinfo, exists := ri.im[m]\n\tif !exists {\n\t\treturn *new(T), errors.New(\"item not found\")\n\t}\n\n\tni := reflect.New(reflect.TypeOf(info).Elem()).Interface()\n\n\tif c != \"\" {\n\t\terr := json.Unmarshal([]byte(c), ni)\n\t\tif err != nil {\n\t\t\treturn *new(T), err\n\t\t}\n\t}\n\n\treturn ni.(T), nil\n}\n\nfunc GetList(t string) []string {\n\tri, exists := registers[t]\n\tif !exists {\n\t\treturn nil\n\t}\n\n\tkeys := make([]string, 0, len(ri.im))\n\tfor k := range ri.im {\n\t\tkeys = append(keys, k)\n\t}\n\treturn keys\n}\n\nfunc GetInfoMap(t string) map[string][]desc.Data {\n\tri, exists := registers[t]\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn ri.aim\n}\n"
  },
  {
    "path": "internal/modules/share/share.go",
    "content": "package share\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/core/subconv\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/share\"\n\t\"github.com/bestruirui/bestsub/internal/utils\"\n\t\"github.com/bestruirui/bestsub/internal/utils/country\"\n)\n\nfunc GenSubData(genConfigStr string) []byte {\n\tvar genConfig share.GenConfig\n\tif err := json.Unmarshal([]byte(genConfigStr), &genConfig); err != nil {\n\t\treturn nil\n\t}\n\tnodes := node.GetByFilter(genConfig.Filter)\n\tvar result bytes.Buffer\n\tresult.Write(nodeData)\n\ttmpl, err := renameTemplate.Parse(genConfig.Rename)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tvar newName bytes.Buffer\n\tfor i, node := range *nodes {\n\t\tnewName.Reset()\n\t\tresult.Write(dash)\n\t\tsubTags := op.GetSubTagsByID(context.Background(), node.Base.SubId)\n\t\tsimpleInfo := renameTmpl{\n\t\t\tSpeedUp:       node.Info.SpeedUp.Average(),\n\t\t\tSpeedDown:     node.Info.SpeedDown.Average(),\n\t\t\tDelay:         uint32(node.Info.Delay.Average()),\n\t\t\tRisk:          uint32(node.Info.Risk),\n\t\t\tCount:         uint32(i + 1),\n\t\t\tCountry:       country.GetCountry(node.Info.Country),\n\t\t\tIP:            utils.Uint32ToIP(node.Info.IP),\n\t\t\tSubName:       op.GetSubNameByID(context.Background(), node.Base.SubId),\n\t\t\tSubTags:       fmt.Sprintf(\"<%s>\", strings.Join(subTags, \"|\")),\n\t\t\tSubTagsOrigin: subTags,\n\t\t}\n\t\ttmpl.Execute(&newName, simpleInfo)\n\t\tresult.Write(rename(node.Base.Raw, newName.Bytes()))\n\t\tresult.Write(newLine)\n\t}\n\tresultStr := subconv.ConvertData(result.String(), genConfig.Target)\n\treturn []byte(resultStr)\n}\n\nfunc GenNodeData(config string) []byte {\n\tvar genConfig share.GenConfig\n\tif err := json.Unmarshal([]byte(config), &genConfig); err != nil {\n\t\treturn nil\n\t}\n\tnodes := node.GetByFilter(genConfig.Filter)\n\tvar result bytes.Buffer\n\tresult.Write(nodeData)\n\ttmpl, err := renameTemplate.Parse(genConfig.Rename)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tvar newName bytes.Buffer\n\tfor i, node := range *nodes {\n\t\tnewName.Reset()\n\t\tresult.Write(dash)\n\t\tsubTags := op.GetSubTagsByID(context.Background(), node.Base.SubId)\n\t\tsimpleInfo := renameTmpl{\n\t\t\tSpeedUp:       node.Info.SpeedUp.Average(),\n\t\t\tSpeedDown:     node.Info.SpeedDown.Average(),\n\t\t\tDelay:         uint32(node.Info.Delay.Average()),\n\t\t\tRisk:          uint32(node.Info.Risk),\n\t\t\tCount:         uint32(i + 1),\n\t\t\tCountry:       country.GetCountry(node.Info.Country),\n\t\t\tIP:            utils.Uint32ToIP(node.Info.IP),\n\t\t\tSubName:       op.GetSubNameByID(context.Background(), node.Base.SubId),\n\t\t\tSubTags:       fmt.Sprintf(\"<%s>\", strings.Join(subTags, \"|\")),\n\t\t\tSubTagsOrigin: subTags,\n\t\t}\n\t\ttmpl.Execute(&newName, simpleInfo)\n\t\tresult.Write(rename(node.Base.Raw, newName.Bytes()))\n\t\tresult.Write(newLine)\n\t}\n\treturn result.Bytes()\n}\n\nfunc rename(raw []byte, newName []byte) []byte {\n\tvar node map[string]any\n\tif err := json.Unmarshal(raw, &node); err != nil {\n\t\treturn raw\n\t}\n\tnode[\"name\"] = string(newName)\n\tout, err := json.Marshal(node)\n\tif err != nil {\n\t\treturn raw\n\t}\n\treturn out\n}\n\nvar (\n\tnodeData = []byte(\"proxies:\\n\")\n\tnewLine  = []byte(\"\\n\")\n\tdash     = []byte(\" - \")\n)\n\ntype renameTmpl struct {\n\tSpeedUp       uint32\n\tSpeedDown     uint32\n\tDelay         uint32\n\tRisk          uint32\n\tCountry       country.Country\n\tCount         uint32\n\tIP            string\n\tSubName       string\n\tSubTags       string\n\tSubTagsOrigin []string\n}\n\nvar renameTemplate = template.New(\"node\").Funcs(template.FuncMap{\n\t\"add\": func(x, y uint32) uint32 {\n\t\treturn x + y\n\t},\n\t\"sub\": func(x, y uint32) uint32 {\n\t\treturn x - y\n\t},\n\t\"div\": func(x, y uint32) uint32 {\n\t\tif y == 0 {\n\t\t\treturn 0\n\t\t}\n\t\treturn x / y\n\t},\n\t\"mod\": func(x, y uint32) uint32 {\n\t\tif y == 0 {\n\t\t\treturn 0\n\t\t}\n\t\treturn x % y\n\t},\n})\n"
  },
  {
    "path": "internal/modules/storage/channel/webdav.go",
    "content": "package channel\n\nimport (\n\t\"context\"\n\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n)\n\nfunc init() {\n\tregister.Storage(&WebDAV{})\n}\n\ntype WebDAV struct {\n\turl      string `json:\"url\" type:\"string\" required:\"true\" description:\"WebDAV地址\"`\n\tusername string `json:\"username\" type:\"string\" required:\"true\" description:\"WebDAV用户名\"`\n\tpassword string `json:\"password\" type:\"string\" required:\"true\" description:\"WebDAV密码\"`\n}\n\nfunc (w *WebDAV) Init() error {\n\treturn nil\n}\n\nfunc (w *WebDAV) Upload(ctx context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/modules/storage/storage.go",
    "content": "package storage\n\nimport (\n\tstorageModel \"github.com/bestruirui/bestsub/internal/models/storage\"\n\t\"github.com/bestruirui/bestsub/internal/modules/register\"\n\t_ \"github.com/bestruirui/bestsub/internal/modules/storage/channel\"\n\t\"github.com/bestruirui/bestsub/internal/utils/desc\"\n)\n\ntype Desc = desc.Data\n\nfunc Get(m string, c string) (storageModel.Instance, error) {\n\treturn register.Get[storageModel.Instance](\"storage\", m, c)\n}\n\nfunc GetChannels() []string {\n\treturn register.GetList(\"storage\")\n}\n\nfunc GetInfoMap() map[string][]Desc {\n\treturn register.GetInfoMap(\"storage\")\n}\n"
  },
  {
    "path": "internal/server/auth/auth.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/models/auth\"\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\n// Claims JWT声明结构\ntype Claims struct {\n\tUsername  string `json:\"username\"`\n\tjwt.RegisteredClaims\n}\n\n// GenerateToken 生成访问令牌\nfunc GenerateToken(username, secret string) (*auth.LoginResponse, error) {\n\n\tnow := time.Now()\n\n\taccessExpiresAt := now.Add(7 * 24 * time.Hour)\n\n\tclaims := &Claims{\n\t\tUsername: username,\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tExpiresAt: jwt.NewNumericDate(accessExpiresAt),\n\t\t\tIssuedAt:  jwt.NewNumericDate(now),\n\t\t\tNotBefore: jwt.NewNumericDate(now),\n\t\t\tIssuer:    \"bestsub\",\n\t\t\tSubject:   username,\n\t\t},\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\taccessToken, err := token.SignedString([]byte(secret))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to sign token: %w\", err)\n\t}\n\n\treturn &auth.LoginResponse{\n\t\tAccessToken:     accessToken,\n\t\tAccessExpiresAt: accessExpiresAt,\n\t}, nil\n}\n\n// ValidateToken 验证JWT令牌\nfunc ValidateToken(tokenString, secret string) (*Claims, error) {\n\n\ttoken, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {\n\t\tif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unexpected signing method: %v\", token.Header[\"alg\"])\n\t\t}\n\t\treturn []byte(secret), nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse token: %w\", err)\n\t}\n\n\tif claims, ok := token.Claims.(*Claims); ok && token.Valid {\n\t\tif claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {\n\t\t\treturn nil, fmt.Errorf(\"token has expired\")\n\t\t}\n\t\treturn claims, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"invalid token\")\n}\n"
  },
  {
    "path": "internal/server/handlers/auth.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/config\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tauthModel \"github.com/bestruirui/bestsub/internal/models/auth\"\n\tnotifyModel \"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/modules/notify\"\n\t\"github.com/bestruirui/bestsub/internal/server/auth\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\n\trouter.NewGroupRouter(\"/api/v1/auth\").\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/login\", router.POST).\n\t\t\t\tHandle(login),\n\t\t)\n\n\trouter.NewGroupRouter(\"/api/v1/auth\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/logout\", router.POST).\n\t\t\t\tHandle(logout),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/user/password\", router.POST).\n\t\t\t\tHandle(changePassword),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/user/name\", router.POST).\n\t\t\t\tHandle(updateUsername),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/user\", router.GET).\n\t\t\t\tHandle(getUserInfo),\n\t\t)\n}\n\n// login 用户登录\n// @Summary 用户登录\n// @Description 用户登录接口，验证用户名和密码，返回JWT令牌\n// @Tags 认证\n// @Accept json\n// @Produce json\n// @Param request body authModel.LoginRequest true \"登录请求\"\n// @Success 200 {object} resp.ResponseStruct{data=authModel.LoginResponse} \"登录成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"用户名或密码错误\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/auth/login [post]\nfunc login(c *gin.Context) {\n\tvar req authModel.LoginRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\terr := op.AuthVerify(req.Username, req.Password)\n\tif err != nil {\n\t\tlog.Warnf(\"Login failed for user %s: %v from %s\", req.Username, err, c.ClientIP())\n\t\tgo notify.SendSystemNotify(notifyModel.TypeLoginFailed, \"登录失败\", authModel.LoginNotify{\n\t\t\tUsername:  req.Username,\n\t\t\tIP:        c.ClientIP(),\n\t\t\tTime:      time.Now().Format(\"2006-01-02 15:04:05\"),\n\t\t\tMsg:       \"登录失败，用户名或密码错误\",\n\t\t\tUserAgent: c.GetHeader(\"User-Agent\"),\n\t\t})\n\t\tresp.Error(c, http.StatusUnauthorized, \"username or password error\")\n\t\treturn\n\t}\n\n\ttoken, err := auth.GenerateToken(req.Username, config.Base().JWT.Secret)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to generate token: %v from %s\", err, c.ClientIP())\n\t\tgo notify.SendSystemNotify(notifyModel.TypeLoginFailed, \"登录失败\", authModel.LoginNotify{\n\t\t\tUsername:  req.Username,\n\t\t\tIP:        c.ClientIP(),\n\t\t\tTime:      time.Now().Format(\"2006-01-02 15:04:05\"),\n\t\t\tMsg:       \"登录失败，生成令牌失败\",\n\t\t\tUserAgent: c.GetHeader(\"User-Agent\"),\n\t\t})\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to generate token\")\n\t\treturn\n\t}\n\n\tlog.Infof(\"User %s logged in successfully from %s\", req.Username, c.ClientIP())\n\tgo notify.SendSystemNotify(notifyModel.TypeLoginSuccess, \"登录成功\", authModel.LoginNotify{\n\t\tUsername:  req.Username,\n\t\tIP:        c.ClientIP(),\n\t\tTime:      time.Now().Format(\"2006-01-02 15:04:05\"),\n\t\tMsg:       \"登录成功\",\n\t\tUserAgent: c.GetHeader(\"User-Agent\"),\n\t})\n\n\tresp.Success(c, token)\n}\n\n// logout 用户登出\n// @Summary 用户登出\n// @Description 用户登出接口，客户端清除令牌\n// @Tags 认证\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct \"登出成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/auth/logout [post]\nfunc logout(c *gin.Context) {\n\tlog.Infof(\"User logged out successfully from %s\", c.ClientIP())\n\n\tresp.Success(c, nil)\n}\n\n// changePassword 修改密码\n// @Summary 修改密码\n// @Description 修改当前用户的密码\n// @Tags 认证\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body authModel.ChangePasswordRequest true \"修改密码请求\"\n// @Success 200 {object} resp.ResponseStruct \"密码修改成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权或旧密码错误\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/auth/user/password [post]\nfunc changePassword(c *gin.Context) {\n\tvar req authModel.ChangePasswordRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\terr := op.AuthVerify(req.Username, req.OldPassword)\n\tif err != nil {\n\t\tlog.Warnf(\"Change password failed for user %s: old password verification failed from %s\", req.Username, c.ClientIP())\n\t\tresp.Error(c, http.StatusUnauthorized, \"old password verification failed\")\n\t\treturn\n\t}\n\n\terr = op.AuthUpdatePassWord(req.NewPassword)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to update password: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to update password\")\n\t\treturn\n\t}\n\n\tlog.Infof(\"Password changed successfully for user %s from %s\", req.Username, c.ClientIP())\n\n\tresp.Success(c, nil)\n}\n\n// getUserInfo 获取当前用户信息\n// @Summary 获取用户信息\n// @Description 获取当前登录用户的详细信息\n// @Tags 认证\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=authModel.Data} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/auth/user [get]\nfunc getUserInfo(c *gin.Context) {\n\tauthInfo, err := op.AuthGet()\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get auth info from %s: %v\", c.ClientIP(), err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to get auth info\")\n\t\treturn\n\t}\n\tresp.Success(c, authInfo)\n}\n\n// updateUsername 修改用户名\n// @Summary 修改用户名\n// @Description 修改当前用户的用户名\n// @Tags 认证\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body authModel.UpdateUserInfoRequest true \"修改用户名请求\"\n// @Success 200 {object} resp.ResponseStruct \"用户名修改成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 409 {object} resp.ResponseStruct \"用户名已存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/auth/user/name [post]\nfunc updateUsername(c *gin.Context) {\n\tvar req authModel.UpdateUserInfoRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\tauthInfo, err := op.AuthGet()\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get auth info from %s: %v\", c.ClientIP(), err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to get auth info\")\n\t\treturn\n\t}\n\n\tif authInfo.UserName == req.Username {\n\t\tresp.Error(c, http.StatusBadRequest, \"new username cannot be the same as current username\")\n\t\treturn\n\t}\n\n\tif err := op.AuthUpdateName(req.Username); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to update username\")\n\t\treturn\n\t}\n\n\tlog.Infof(\"Username changed successfully from %s to %s from %s\", authInfo.UserName, req.Username, c.ClientIP())\n\n\tresp.Success(c, nil)\n}\n"
  },
  {
    "path": "internal/server/handlers/check.go",
    "content": "package handlers\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/check\"\n\t\"github.com/bestruirui/bestsub/internal/core/cron\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tcheckModel \"github.com/bestruirui/bestsub/internal/models/check\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\n\trouter.NewGroupRouter(\"/api/v1/check\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/type\", router.GET).\n\t\t\t\tHandle(getCheckTypes),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.POST).\n\t\t\t\tHandle(createCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(getCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.PUT).\n\t\t\t\tHandle(updateCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.DELETE).\n\t\t\t\tHandle(deleteCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id/run\", router.POST).\n\t\t\t\tHandle(runCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id/stop\", router.POST).\n\t\t\t\tHandle(stopCheck),\n\t\t)\n}\n\n// getCheckTypes 获取检测类型\n// @Summary 获取检测类型\n// @Description 获取检测类型\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=map[string][]check.Desc} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check/type [get]\nfunc getCheckTypes(c *gin.Context) {\n\tresp.Success(c, check.GetInfoMap())\n}\n\n// createCheck 创建检测\n// @Summary 创建检测\n// @Description 创建单个检测\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body checkModel.Request true \"创建检测请求\"\n// @Success 200 {object} resp.ResponseStruct{data=checkModel.Response} \"创建成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check [post]\nfunc createCheck(c *gin.Context) {\n\tvar req checkModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tcheckData := req.GenData()\n\tif err := op.CreateCheck(c.Request.Context(), &checkData); err != nil {\n\t\tlog.Errorf(\"failed to create check: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tcron.CheckAdd(&checkData)\n\tresp.Success(c, checkData.GenResponse(cron.CheckStatus(checkData.ID)))\n}\n\n// getCheck 获取检测列表\n// @Summary 获取检测列表\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id query int true \"检测ID\"\n// @Success 200 {object} resp.ResponseStruct{data=[]checkModel.Response} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check [get]\nfunc getCheck(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tif idStr == \"\" {\n\t\tcheckList, err := op.GetCheckList()\n\t\tif err != nil {\n\t\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tvar respCheckList = make([]checkModel.Response, len(checkList))\n\t\tfor i := range checkList {\n\t\t\trespCheckList[i] = checkList[i].GenResponse(cron.CheckStatus(checkList[i].ID))\n\t\t}\n\t\tresp.Success(c, respCheckList)\n\t} else {\n\t\tid, err := strconv.ParseUint(idStr, 10, 16)\n\t\tif err != nil {\n\t\t\tresp.ErrorBadRequest(c)\n\t\t\treturn\n\t\t}\n\t\tcheck, err := op.GetCheckByID(uint16(id))\n\t\tif err != nil {\n\t\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tvar respCheck = make([]checkModel.Response, 1)\n\t\trespCheck[0] = check.GenResponse(cron.CheckStatus(check.ID))\n\t\tresp.Success(c, respCheck)\n\t}\n}\n\n// updateCheck 更新检测\n// @Summary 更新检测\n// @Description 根据请求体中的ID更新检测信息\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"检测ID\"\n// @Param request body checkModel.Request true \"更新检测请求\"\n// @Success 200 {object} resp.ResponseStruct{data=checkModel.Response} \"更新成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"检测不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check/{id} [put]\nfunc updateCheck(c *gin.Context) {\n\tvar req checkModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tidStr := c.Param(\"id\")\n\tif idStr == \"\" {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tid, err := strconv.ParseUint(idStr, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tcheckData := req.GenData()\n\tcheckData.ID = uint16(id)\n\tif err := op.UpdateCheck(c.Request.Context(), &checkData); err != nil {\n\t\tlog.Errorf(\"failed to update check: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif err := cron.CheckUpdate(&checkData); err != nil {\n\t\tlog.Errorf(\"failed to update check: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, checkData.GenResponse(cron.CheckStatus(checkData.ID)))\n}\n\n// deleteCheck 删除检测\n// @Summary 删除检测\n// @Description 根据ID删除单个检测\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"检测ID\"\n// @Success 200 {object} resp.ResponseStruct \"删除成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"检测不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check/{id} [delete]\nfunc deleteCheck(c *gin.Context) {\n\tidParam := c.Param(\"id\")\n\tif idParam == \"\" {\n\t\tresp.Error(c, http.StatusBadRequest, \"check id is required\")\n\t\treturn\n\t}\n\n\tid, err := strconv.ParseUint(idParam, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\tif err := op.DeleteCheck(c.Request.Context(), uint16(id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to delete check\")\n\t\treturn\n\t}\n\tif err := cron.CheckRemove(uint16(id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif err := log.DeleteLog(fmt.Sprintf(\"check/%d\", id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, nil)\n}\n\n// runCheck 手动运行检测\n// @Summary 手动运行检测\n// @Description 手动触发检测执行\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"检测ID\"\n// @Success 200 {object} resp.ResponseStruct \"运行成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"检测不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check/{id}/run [post]\nfunc runCheck(c *gin.Context) {\n\tidParam := c.Param(\"id\")\n\tif idParam == \"\" {\n\t\tresp.Error(c, http.StatusBadRequest, \"check id is required\")\n\t\treturn\n\t}\n\n\tid, err := strconv.ParseUint(idParam, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\tif err := cron.CheckRun(uint16(id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\n\tlog.Debugf(\"Check %d manually run from %s\", id, c.ClientIP())\n\n\tresp.Success(c, nil)\n}\n\n// stopCheck 停止检测\n// @Summary 停止检测\n// @Description 停止正在运行的检测\n// @Tags 检测\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"检测ID\"\n// @Success 200 {object} resp.ResponseStruct \"停止成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"检测不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/check/{id}/stop [post]\nfunc stopCheck(c *gin.Context) {\n\tidParam := c.Param(\"id\")\n\tif idParam == \"\" {\n\t\tresp.Error(c, http.StatusBadRequest, \"task id is required\")\n\t\treturn\n\t}\n\n\tid, err := strconv.ParseUint(idParam, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\tif err := cron.CheckStop(uint16(id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\n\tlog.Infof(\"Check %d stopped from %s\", id, c.ClientIP())\n\n\tresp.Success(c, nil)\n}\n"
  },
  {
    "path": "internal/server/handlers/info.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\tsys \"github.com/bestruirui/bestsub/internal/core/system\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/system\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/info\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/system\").\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/health\", router.GET).\n\t\t\t\tHandle(healthCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/ready\", router.GET).\n\t\t\t\tHandle(readinessCheck),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/live\", router.GET).\n\t\t\t\tHandle(livenessCheck),\n\t\t)\n\n\trouter.NewGroupRouter(\"/api/v1/system\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/info\", router.GET).\n\t\t\t\tHandle(systemInfo),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/version\", router.GET).\n\t\t\t\tHandle(version),\n\t\t)\n}\n\n// healthCheck 健康检查\n// @Summary 健康检查\n// @Description 检查服务健康状态，包括数据库连接状态\n// @Tags 系统\n// @Accept json\n// @Produce json\n// @Success 200 {object} resp.ResponseStruct{data=system.HealthResponse} \"服务正常\"\n// @Failure 503 {object} resp.ResponseStruct \"服务不可用\"\n// @Router /api/v1/system/health [get]\nfunc healthCheck(c *gin.Context) {\n\t// 检查数据库连接状态\n\topStatus := \"connected\"\n\n\t// 尝试执行一个简单的数据库查询来检查连接\n\tauthRepo := op.AuthRepo()\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\t_, err := authRepo.IsInitialized(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"Database health check failed: %v\", err)\n\t\topStatus = \"disconnected\"\n\t}\n\n\tresponse := system.HealthResponse{\n\t\tStatus:    \"ok\",\n\t\tTimestamp: time.Now().Format(time.RFC3339),\n\t\tVersion:   info.Version,\n\t\tDatabase:  opStatus,\n\t}\n\n\t// 如果数据库连接失败，返回503状态码\n\tif opStatus == \"disconnected\" {\n\t\tresponse.Status = \"error\"\n\t\tresp.Error(c, http.StatusServiceUnavailable, \"database connection failed\")\n\t\treturn\n\t}\n\n\tresp.Success(c, response)\n}\n\n// readinessCheck 就绪检查\n// @Summary 就绪检查\n// @Description 检查服务是否准备好接收请求\n// @Tags 系统\n// @Accept json\n// @Produce json\n// @Success 200 {object} resp.ResponseStruct{data=system.HealthResponse} \"服务就绪\"\n// @Failure 503 {object} resp.ResponseStruct \"服务未就绪\"\n// @Router /api/v1/system/ready [get]\nfunc readinessCheck(c *gin.Context) {\n\t// 检查关键组件是否就绪\n\tready := true\n\tvar errorMsg string\n\n\tauthRepo := op.AuthRepo()\n\tctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\n\tdefer cancel()\n\n\tisInitialized, err := authRepo.IsInitialized(ctx)\n\tif err != nil || !isInitialized {\n\t\tready = false\n\t\terrorMsg = \"database not initialized\"\n\t\tlog.Errorf(\"Readiness check failed: op not initialized, error: %v\", err)\n\t}\n\n\tresponse := system.HealthResponse{\n\t\tStatus:    \"ready\",\n\t\tTimestamp: time.Now().Format(time.RFC3339),\n\t\tVersion:   info.Version,\n\t\tDatabase:  \"initialized\",\n\t}\n\n\tif !ready {\n\t\tresponse.Status = \"not ready\"\n\t\tresponse.Database = \"not initialized\"\n\t\tresp.Error(c, http.StatusServiceUnavailable, errorMsg)\n\t\treturn\n\t}\n\n\tresp.Success(c, response)\n}\n\n// livenessCheck 存活检查\n// @Summary 存活检查\n// @Description 检查服务是否存活（简单的ping检查）\n// @Tags 系统\n// @Accept json\n// @Produce json\n// @Success 200 {object} resp.ResponseStruct \"服务存活\"\n// @Router /api/v1/system/live [get]\nfunc livenessCheck(c *gin.Context) {\n\tresp.Success(c, map[string]interface{}{\n\t\t\"status\":    \"alive\",\n\t\t\"timestamp\": time.Now().Format(time.RFC3339),\n\t})\n}\n\n// systemInfo 系统信息\n// @Summary 系统信息\n// @Description 获取程序运行相关信息，包括内存使用、运行时长、网络流量、CPU信息等\n// @Tags 系统\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=system.Info} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/system/info [get]\nfunc systemInfo(c *gin.Context) {\n\tresp.Success(c, sys.GetSystemInfo())\n}\n\n// systemInfo 系统版本\n// @Summary 系统版本\n// @Description 获取程序版本信息\n// @Tags 系统\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=system.Version} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/system/version [get]\nfunc version(c *gin.Context) {\n\tresp.Success(c, system.Version{\n\t\tVersion:   info.Version,\n\t\tBuildTime: info.BuildTime,\n\t\tCommit:    info.Commit,\n\t\tAuthor:    info.Author,\n\t\tRepo:      info.Repo,\n\t})\n}\n"
  },
  {
    "path": "internal/server/handlers/log.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/log\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/list\", router.GET).\n\t\t\t\tHandle(getLogFileList),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/content\", router.GET).\n\t\t\t\tHandle(getLogContent),\n\t\t)\n}\n\n// @Summary 获取日志列表\n// @Description 获取日志列表\n// @Tags 日志\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param path query string true \"日志文件路径\"\n// @Success 200 {object} resp.ResponseStruct{data=[]uint64} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/log/list [get]\nfunc getLogFileList(c *gin.Context) {\n\tpath := c.Query(\"path\")\n\tif path == \"\" {\n\t\tresp.Error(c, http.StatusBadRequest, \"path parameter is required\")\n\t\treturn\n\t}\n\tlogFileList, err := log.GetLogFileList(path)\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, logFileList)\n}\n\n// @Summary 获取日志内容\n// @Description 获取日志内容\n// @Tags 日志\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param path query string true \"日志文件路径\"\n// @Param timestamp query uint64 true \"日志文件时间戳\"\n// @Success 200 {object} resp.ResponseStruct{data=[]object{level=string,time=string,msg=string}} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"文件不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/log/content [get]\nfunc getLogContent(c *gin.Context) {\n\tpath := c.Query(\"path\")\n\tif path == \"\" {\n\t\tresp.Error(c, http.StatusBadRequest, \"path parameter is required\")\n\t\treturn\n\t}\n\ttimestampStr := c.Query(\"timestamp\")\n\n\tif timestampStr == \"\" {\n\t\tresp.Error(c, http.StatusBadRequest, \"timestamp parameter is required\")\n\t\treturn\n\t}\n\n\ttimestamp, err := strconv.ParseUint(timestampStr, 10, 64)\n\tif err != nil {\n\t\tresp.Error(c, http.StatusBadRequest, \"invalid timestamp format\")\n\t\treturn\n\t}\n\n\tc.Header(\"Content-Type\", \"application/json; charset=utf-8\")\n\tc.Header(\"Transfer-Encoding\", \"chunked\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\tc.Header(\"X-Accel-Buffering\", \"no\")\n\n\tc.Status(http.StatusOK)\n\tw := c.Writer\n\n\tw.WriteString(`{\"code\":200,\"message\":\"success\",\"data\":[`)\n\tw.Flush()\n\n\terr = log.StreamLogToHTTP(path, timestamp, w)\n\tif err != nil {\n\t\tw.WriteString(`],\"error\":\"`)\n\t\tw.WriteString(strings.ReplaceAll(err.Error(), `\"`, `\\\"`))\n\t\tw.WriteString(`\"}`)\n\t\tw.Flush()\n\t\treturn\n\t}\n\n\tw.WriteString(`]}`)\n\tw.Flush()\n}\n"
  },
  {
    "path": "internal/server/handlers/notify.go",
    "content": "package handlers\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tnotifyModel \"github.com/bestruirui/bestsub/internal/models/notify\"\n\t\"github.com/bestruirui/bestsub/internal/modules/notify\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/notify\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/channel\", router.GET).\n\t\t\t\tHandle(getNotifyChannel),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/channel/config\", router.GET).\n\t\t\t\tHandle(getNotifyChannelConfig),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/name\", router.GET).\n\t\t\t\tHandle(getNotifyNameAndID),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(getNotifyList),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.POST).\n\t\t\t\tHandle(createNotify),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.PUT).\n\t\t\t\tHandle(updateNotify),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.DELETE).\n\t\t\t\tHandle(deleteNotify),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/test\", router.POST).\n\t\t\t\tHandle(testNotify),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/template\", router.GET).\n\t\t\t\tHandle(getTemplates),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/template\", router.PUT).\n\t\t\t\tHandle(updateTemplate),\n\t\t)\n}\n\n// getNotifyConfig 获取通知渠道\n// @Summary 获取通知渠道\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]string} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify/channel [get]\nfunc getNotifyChannel(c *gin.Context) {\n\tresp.Success(c, notify.GetChannels())\n}\n\n// getNotifyChannelConfig 获取渠道配置\n// @Summary 获取渠道配置\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param channel query string false \"渠道\"\n// @Success 200 {object} resp.ResponseStruct{data=map[string][]notify.Desc} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify/channel/config [get]\nfunc getNotifyChannelConfig(c *gin.Context) {\n\tchannel := c.Query(\"channel\")\n\tif channel == \"\" {\n\t\tresp.Success(c, notify.GetInfoMap())\n\t} else {\n\t\tresp.Success(c, notify.GetInfoMap()[channel])\n\t}\n}\n\n// getNotifyList 获取通知\n// @Summary 获取通知\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]notifyModel.Response} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify [get]\nfunc getNotifyList(c *gin.Context) {\n\tnotifyList, err := op.GetNotifyList()\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, notifyList)\n}\n\n// getNotifyNameAndID 获取通知名称和ID\n// @Summary 获取通知名称和ID\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]notifyModel.NameAndID} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify/name [get]\nfunc getNotifyNameAndID(c *gin.Context) {\n\tnotifyList, err := op.GetNotifyList()\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tnameAndIDList := make([]notifyModel.NameAndID, len(notifyList))\n\tfor i, notify := range notifyList {\n\t\tnameAndIDList[i] = notifyModel.NameAndID{\n\t\t\tID:   notify.ID,\n\t\t\tName: notify.Name,\n\t\t}\n\t}\n\tresp.Success(c, nameAndIDList)\n}\n\n// createNotify 创建通知\n// @Summary 创建通知\n// @Description 创建单个通知\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body notifyModel.Request true \"创建通知请求\"\n// @Success 200 {object} resp.ResponseStruct{data=notifyModel.Response} \"创建成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify [post]\nfunc createNotify(c *gin.Context) {\n\tvar req notifyModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tnotifyData := req.GenData(0)\n\ttypes := notify.GetChannels()\n\tif !slices.Contains(types, req.Type) {\n\t\tresp.Error(c, http.StatusBadRequest, fmt.Sprintf(\"通知类型 %s 不存在\", req.Type))\n\t\treturn\n\t}\n\tif err := op.CreateNotify(c.Request.Context(), &notifyData); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tlog.Infof(\"Notify config %d created by from %s\", notifyData.ID, c.ClientIP())\n\tresp.Success(c, notifyData.GenResponse())\n}\n\n// testNotify 测试通知\n// @Summary 测试通知\n// @Description 测试单个通知\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body notifyModel.Request true \"测试通知请求\"\n// @Success 200 {object} resp.ResponseStruct \"测试成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify/test [post]\nfunc testNotify(c *gin.Context) {\n\tvar req notifyModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\ttypes := notify.GetChannels()\n\tif !slices.Contains(types, req.Type) {\n\t\tresp.Error(c, http.StatusBadRequest, fmt.Sprintf(\"通知类型 %s 不存在\", req.Type))\n\t\treturn\n\t}\n\tnotifyData := req.GenData(0)\n\tnotify, err := notify.Get(notifyData.Type, notifyData.Config)\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\terr = notify.Init()\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tvar buf bytes.Buffer\n\tbuf.WriteString(\"test\")\n\terr = notify.Send(\"test\", &buf)\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tlog.Infof(\"Notify config %s tested by from %s\", notifyData.Type, c.ClientIP())\n\tresp.Success(c, nil)\n}\n\n// updateNotify 更新通知\n// @Summary 更新通知\n// @Description 根据请求体中的ID更新通知信息\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id query int true \"通知ID\"\n// @Param request body notifyModel.Request true \"更新通知请求\"\n// @Success 200 {object} resp.ResponseStruct{data=notifyModel.Response} \"更新成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"通知配置不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify [put]\nfunc updateNotify(c *gin.Context) {\n\tvar req notifyModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tidParam := c.Query(\"id\")\n\tif idParam == \"\" {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tid, err := strconv.ParseUint(idParam, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tnotifyData := req.GenData(uint16(id))\n\tif err := op.UpdateNotify(c.Request.Context(), &notifyData); err != nil {\n\t\tlog.Errorf(\"Update notify config %d failed: %v\", id, err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"update notify config failed\")\n\t\treturn\n\t}\n\tlog.Infof(\"Notify config %d updated by from %s\", id, c.ClientIP())\n\tresp.Success(c, notifyData.GenResponse())\n}\n\n// deleteNotify 删除通知\n// @Summary 删除通知\n// @Description 根据ID删除单个通知\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id query int true \"通知ID\"\n// @Success 200 {object} resp.ResponseStruct \"删除成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"通知不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify [delete]\nfunc deleteNotify(c *gin.Context) {\n\tidParam := c.Query(\"id\")\n\tif idParam == \"\" {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\tid, err := strconv.ParseUint(idParam, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tif err := op.DeleteNotify(c.Request.Context(), uint16(id)); err != nil {\n\t\tlog.Errorf(\"Delete notify config %d failed: %v\", id, err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"delete notify config failed\")\n\t\treturn\n\t}\n\tlog.Infof(\"Notify config %d deleted by from %s\", id, c.ClientIP())\n\n\tresp.Success(c, nil)\n}\n\n// getTemplates 获取通知模板\n// @Summary 获取通知模板\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]notifyModel.Template} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify/template [get]\nfunc getTemplates(c *gin.Context) {\n\tnotifyTemplateList, err := op.GetNotifyTemplateList()\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, notifyTemplateList)\n}\n\n// updateTemplate 更新通知模板\n// @Summary 更新通知模板\n// @Description 根据请求体中的ID更新通知模板信息\n// @Tags 通知\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body notifyModel.Template true \"更新通知模板请求\"\n// @Success 200 {object} resp.ResponseStruct{data=notifyModel.Template} \"更新成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"通知模板不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/notify/template [put]\nfunc updateTemplate(c *gin.Context) {\n\tvar req notifyModel.Template\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.Error(c, http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\tif err := op.UpdateNotifyTemplate(c.Request.Context(), &req); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tlog.Infof(\"Notify template %s updated by from %s\", req.Type, c.ClientIP())\n\tresp.Success(c, req)\n}\n"
  },
  {
    "path": "internal/server/handlers/pprof.go",
    "content": "//go:build debug\n\npackage handlers\n\nimport (\n\t\"net/http/pprof\"\n\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\n\trouter.NewGroupRouter(\"/debug/pprof\").\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/\", router.GET).\n\t\t\t\tHandle(index),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/cmdline\", router.GET).\n\t\t\t\tHandle(cmdline),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/profile\", router.GET).\n\t\t\t\tHandle(profile),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/symbol\", router.GET).\n\t\t\t\tHandle(symbol),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/symbol\", router.POST).\n\t\t\t\tHandle(symbol),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/trace\", router.GET).\n\t\t\t\tHandle(trace),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/allocs\", router.GET).\n\t\t\t\tHandle(allocs),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/block\", router.GET).\n\t\t\t\tHandle(block),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/goroutine\", router.GET).\n\t\t\t\tHandle(goroutine),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/heap\", router.GET).\n\t\t\t\tHandle(heap),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/mutex\", router.GET).\n\t\t\t\tHandle(mutex),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/threadcreate\", router.GET).\n\t\t\t\tHandle(threadcreate),\n\t\t)\n}\n\nfunc index(c *gin.Context) {\n\tpprof.Index(c.Writer, c.Request)\n}\n\nfunc cmdline(c *gin.Context) {\n\tpprof.Cmdline(c.Writer, c.Request)\n}\n\nfunc profile(c *gin.Context) {\n\tpprof.Profile(c.Writer, c.Request)\n}\n\nfunc symbol(c *gin.Context) {\n\tpprof.Symbol(c.Writer, c.Request)\n}\n\nfunc trace(c *gin.Context) {\n\tpprof.Trace(c.Writer, c.Request)\n}\n\nfunc allocs(c *gin.Context) {\n\tpprof.Handler(\"allocs\").ServeHTTP(c.Writer, c.Request)\n}\n\nfunc block(c *gin.Context) {\n\tpprof.Handler(\"block\").ServeHTTP(c.Writer, c.Request)\n}\n\nfunc goroutine(c *gin.Context) {\n\tpprof.Handler(\"goroutine\").ServeHTTP(c.Writer, c.Request)\n}\n\nfunc heap(c *gin.Context) {\n\tpprof.Handler(\"heap\").ServeHTTP(c.Writer, c.Request)\n}\n\nfunc mutex(c *gin.Context) {\n\tpprof.Handler(\"mutex\").ServeHTTP(c.Writer, c.Request)\n}\n\nfunc threadcreate(c *gin.Context) {\n\tpprof.Handler(\"threadcreate\").ServeHTTP(c.Writer, c.Request)\n}\n"
  },
  {
    "path": "internal/server/handlers/scalar.go",
    "content": "//go:build dev\n\npackage handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/scalar\").\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/\", router.GET).\n\t\t\t\tHandle(scalar),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/api.json\", router.GET).\n\t\t\t\tHandle(apidata),\n\t\t)\n}\n\nvar scalarHTML = []byte(`\n<!doctype html>\n<html>\n  <head>\n    <title>BestSub API</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n    <script>Scalar.createApiReference(\n    '#app', \n      {\n        url: '/scalar/api.json',\n        hideModels: true,\n        hideDownloadButton: true,\n        authentication: {\n          preferredSecurityScheme: 'BearerAuth',\n        },\n        hideClientButton: true\n      }\n    )\n    </script>\n  </body>\n</html>\n`)\n\nfunc scalar(c *gin.Context) {\n\tc.Data(http.StatusOK, \"text/html; charset=utf-8\", scalarHTML)\n}\nfunc apidata(c *gin.Context) {\n\tc.File(\"docs/api/swagger.json\")\n}\n"
  },
  {
    "path": "internal/server/handlers/setting.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/setting\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\n\trouter.NewGroupRouter(\"/api/v1/setting\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(getSetting),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.PUT).\n\t\t\t\tHandle(updateSetting),\n\t\t)\n}\n\n// getSetting 获取配置项\n// @Summary 获取配置项\n// @Description 获取系统所有配置项，支持按分组过滤和关键字搜索\n// @Tags 配置\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param group query string false \"分组名称\"\n// @Success 200 {object} resp.ResponseStruct{data=[]setting.Setting} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/setting [get]\nfunc getSetting(c *gin.Context) {\n\tresult, err := op.GetAllSetting(context.Background())\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get all setting: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to get all setting\")\n\t\treturn\n\t}\n\tresp.Success(c, result)\n}\n\n// updateSetting 更新配置项\n// @Summary 更新配置项\n// @Description 根据请求数据中的ID批量更新配置项的值和描述\n// @Tags 配置\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body []setting.Setting  true \"更新配置项请求\"\n// @Success 200 {object} resp.ResponseStruct \"更新成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/setting [put]\nfunc updateSetting(c *gin.Context) {\n\tvar req []setting.Setting\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\terr := op.UpdateSetting(context.Background(), &req)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to update config: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, \"failed to update config\")\n\t\treturn\n\t}\n\n\tresp.Success(c, nil)\n}\n"
  },
  {
    "path": "internal/server/handlers/share.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tshareModel \"github.com/bestruirui/bestsub/internal/models/share\"\n\t\"github.com/bestruirui/bestsub/internal/modules/share\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/share\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.POST).\n\t\t\t\tHandle(createShare),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(getShare),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.PUT).\n\t\t\t\tHandle(updateShare),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.DELETE).\n\t\t\t\tHandle(deleteShare),\n\t\t)\n\trouter.NewGroupRouter(\"/api/v1/share\").\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/node/:token\", router.GET).\n\t\t\t\tHandle(getShareNodeContent),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/sub/:token\", router.GET).\n\t\t\t\tHandle(getShareSubContent),\n\t\t)\n}\n\n// @Summary 创建分享链接\n// @Description 创建分享链接\n// @Tags 分享\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param data body shareModel.Request true \"分享数据\"\n// @Success 200 {object} resp.ResponseStruct{data=shareModel.Response} \"创建成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/share [post]\nfunc createShare(c *gin.Context) {\n\tvar req shareModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Errorf(\"createShare: %v\", err)\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tdata := req.GenData()\n\tif err := op.CreateShare(c.Request.Context(), &data); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, data.GenResponse())\n}\n\n// @Summary 获取分享链接\n// @Description 获取分享链接\n// @Tags 分享\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]shareModel.Response} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/share [get]\nfunc getShare(c *gin.Context) {\n\tshares, err := op.GetShareList(c.Request.Context())\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tvar result = make([]shareModel.Response, 0, len(shares))\n\tfor _, v := range shares {\n\t\tresult = append(result, v.GenResponse())\n\t}\n\tresp.Success(c, result)\n}\n\n// @Summary 更新分享链接\n// @Description 更新分享链接\n// @Tags 分享\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path string true \"分享ID\"\n// @Param data body shareModel.Request true \"分享数据\"\n// @Success 200 {object} resp.ResponseStruct{data=shareModel.Response} \"更新成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/share/{id} [put]\nfunc updateShare(c *gin.Context) {\n\tvar req shareModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tid := c.Param(\"id\")\n\tidUint, err := strconv.ParseUint(id, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tdata := req.GenData()\n\tdata.ID = uint16(idUint)\n\tif err := op.UpdateShare(c.Request.Context(), &data); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, data.GenResponse())\n}\n\n// @Summary 删除分享链接\n// @Description 删除分享链接\n// @Tags 分享\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path string true \"分享ID\"\n// @Success 200 {object} resp.ResponseStruct \"删除成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/share/{id} [delete]\nfunc deleteShare(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tidUint, err := strconv.ParseUint(id, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tif err := op.DeleteShare(c.Request.Context(), uint16(idUint)); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tresp.Success(c, nil)\n}\n\n// @Summary 获取订阅内容 纯Mihomo格式的节点\n// @Description 获取订阅内容 纯Mihomo格式的节点\n// @Tags 分享\n// @Accept json\n// @Produce plain\n// @Param token path string true \"分享token\"\n// @Success 200 {string} string \"获取成功，内容为yaml/plain格式\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/share/node/{token} [get]\nfunc getShareNodeContent(c *gin.Context) {\n\ttoken := c.Param(\"token\")\n\tclientIp := c.ClientIP()\n\tif token == \"\" {\n\t\tresp.Error(c, http.StatusInternalServerError, \"token is required\")\n\t\treturn\n\t}\n\tshareData, err := op.GetShareByToken(c.Request.Context(), token)\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif !shareData.Enable {\n\t\tresp.Error(c, http.StatusInternalServerError, \"share not enable\")\n\t\treturn\n\t}\n\tif shareData.Expires < uint64(time.Now().Unix()) && shareData.Expires > 0 {\n\t\tresp.Error(c, http.StatusInternalServerError, \"share expired\")\n\t\treturn\n\t}\n\tif shareData.MaxAccessCount > 0 && shareData.MaxAccessCount <= shareData.AccessCount {\n\t\tresp.Error(c, http.StatusInternalServerError, \"share access count exceeded\")\n\t\treturn\n\t}\n\tif clientIp != \"127.0.0.1\" {\n\t\top.UpdateShareAccessCount(c.Request.Context(), shareData.ID)\n\t}\n\tc.Data(http.StatusOK, \"text/plain; charset=utf-8\", share.GenNodeData(shareData.Gen))\n}\n\n// @Summary 获取订阅内容 带规则的订阅\n// @Description 获取订阅内容 带规则的订阅\n// @Tags 分享\n// @Accept json\n// @Produce plain\n// @Param token path string true \"分享token\"\n// @Success 200 {string} string \"获取成功，内容为yaml/plain格式\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/share/sub/{token} [get]\nfunc getShareSubContent(c *gin.Context) {\n\ttoken := c.Param(\"token\")\n\tif token == \"\" {\n\t\tresp.Error(c, http.StatusInternalServerError, \"token is required\")\n\t\treturn\n\t}\n\tshareData, err := op.GetShareByToken(c.Request.Context(), token)\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif !shareData.Enable {\n\t\tresp.Error(c, http.StatusInternalServerError, \"share not enable\")\n\t\treturn\n\t}\n\tif shareData.Expires < uint64(time.Now().Unix()) && shareData.Expires > 0 {\n\t\tresp.Error(c, http.StatusInternalServerError, \"share expired\")\n\t\treturn\n\t}\n\tif shareData.MaxAccessCount > 0 && shareData.MaxAccessCount <= shareData.AccessCount {\n\t\tresp.Error(c, http.StatusInternalServerError, \"share access count exceeded\")\n\t\treturn\n\t}\n\top.UpdateShareAccessCount(c.Request.Context(), shareData.ID)\n\tc.Data(http.StatusOK, \"text/plain; charset=utf-8\", share.GenSubData(shareData.Gen))\n}\n"
  },
  {
    "path": "internal/server/handlers/storage.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\tstorageModel \"github.com/bestruirui/bestsub/internal/models/storage\"\n\t\"github.com/bestruirui/bestsub/internal/modules/storage\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/storage\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.POST).\n\t\t\t\tHandle(createStorage),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(getStorage),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.PUT).\n\t\t\t\tHandle(updateStorage),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.DELETE).\n\t\t\t\tHandle(deleteStorage),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/channel\", router.GET).\n\t\t\t\tHandle(getStorageChannel),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/channel/config\", router.GET).\n\t\t\t\tHandle(getStorageChannelConfig),\n\t\t)\n}\n\n// @Summary 创建存储\n// @Description 创建存储\n// @Tags 存储\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param data body storageModel.Request true \"存储配置数据\"\n// @Success 200 {object} resp.ResponseStruct{data=storageModel.Response} \"创建成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/storage [post]\nfunc createStorage(c *gin.Context) {\n\tvar req storageModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tlog.Errorf(\"createStorage: %v\", err)\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tdata := req.GenData(0)\n\tif err := op.CreateStorage(c.Request.Context(), &data); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, data.GenResponse())\n}\n\n// @Summary 获取存储\n// @Description 获取存储\n// @Tags 存储\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]storageModel.Response} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/storage [get]\nfunc getStorage(c *gin.Context) {\n\tstorages, err := op.GetStorageList(c.Request.Context())\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresult := make([]storageModel.Response, len(storages))\n\tfor i, v := range storages {\n\t\tresult[i] = v.GenResponse()\n\t}\n\tresp.Success(c, result)\n}\n\n// getStorageChannel 获取存储渠道\n// @Summary 获取存储渠道\n// @Tags 存储\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=[]string} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/storage/channel [get]\nfunc getStorageChannel(c *gin.Context) {\n\tchannels := make([]string, 0, len(storage.GetInfoMap()))\n\tfor channel := range storage.GetInfoMap() {\n\t\tchannels = append(channels, channel)\n\t}\n\tresp.Success(c, channels)\n}\n\n// getStorageChannelConfig 获取渠道配置\n// @Summary 获取渠道配置\n// @Tags 存储\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param channel query string false \"渠道\"\n// @Success 200 {object} resp.ResponseStruct{data=map[string][]storage.Desc} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/storage/channel/config [get]\nfunc getStorageChannelConfig(c *gin.Context) {\n\tchannel := c.Query(\"channel\")\n\tif channel == \"\" {\n\t\tresp.Success(c, storage.GetInfoMap())\n\t} else {\n\t\tresp.Success(c, storage.GetInfoMap()[channel])\n\t}\n}\n\n// @Summary 更新存储\n// @Description 更新存储\n// @Tags 存储\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path string true \"存储ID\"\n// @Param data body storageModel.Request true \"存储配置数据\"\n// @Success 200 {object} resp.ResponseStruct{data=storageModel.Response} \"更新成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/storage/{id} [put]\nfunc updateStorage(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tidUint, err := strconv.ParseUint(idStr, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tvar req storageModel.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tdata := req.GenData(uint16(idUint))\n\tif err := op.UpdateStorage(c.Request.Context(), &data); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tresp.Success(c, data.GenResponse())\n}\n\n// @Summary 删除存储\n// @Description 删除存储\n// @Tags 存储\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path string true \"存储ID\"\n// @Success 200 {object} resp.ResponseStruct \"删除成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/storage/{id} [delete]\nfunc deleteStorage(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tidUint, err := strconv.ParseUint(id, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tif err := op.DeleteStorage(c.Request.Context(), uint16(idUint)); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tresp.Success(c, nil)\n}\n"
  },
  {
    "path": "internal/server/handlers/sub.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/cron\"\n\t\"github.com/bestruirui/bestsub/internal/core/node\"\n\t\"github.com/bestruirui/bestsub/internal/database/op\"\n\t\"github.com/bestruirui/bestsub/internal/models/sub\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/sub\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.POST).\n\t\t\t\tHandle(createSub),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(getSubs),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.PUT).\n\t\t\t\tHandle(updateSub),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:id\", router.DELETE).\n\t\t\t\tHandle(deleteSub),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/refresh/:id\", router.POST).\n\t\t\t\tHandle(refreshSub),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/batch\", router.POST).\n\t\t\t\tHandle(batchCreateSub),\n\t\t)\n}\n\n// createSub 创建订阅链接\n// @Summary 创建订阅链接\n// @Description 创建单个订阅链接\n// @Tags 订阅\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body sub.Request true \"创建订阅链接请求\"\n// @Success 200 {object} resp.ResponseStruct{data=sub.Response} \"创建成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/sub [post]\nfunc createSub(c *gin.Context) {\n\tvar req sub.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tsubData := req.GenData(0)\n\tif err := op.CreateSub(c.Request.Context(), &subData); err != nil {\n\t\tlog.Errorf(\"failed to create sub: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tcron.FetchAdd(&subData)\n\trespData := subData.GenResponse(cron.FetchStatus(subData.ID), node.GetSubInfo(subData.ID))\n\tresp.Success(c, respData)\n}\n\n// getSubs 获取订阅链接\n// @Summary 获取订阅链接\n// @Tags 订阅\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id query int true \"链接ID\"\n// @Success 200 {object} resp.ResponseStruct{data=[]sub.Response} \"获取成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/sub [get]\nfunc getSubs(c *gin.Context) {\n\tidStr := c.Query(\"id\")\n\tif idStr == \"\" {\n\t\tsubList, err := op.GetSubList(c.Request.Context())\n\t\tif err != nil {\n\t\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tvar respSubList = make([]sub.Response, len(subList))\n\t\tfor i := range subList {\n\t\t\trespSubList[i] = subList[i].GenResponse(cron.FetchStatus(subList[i].ID), node.GetSubInfo(subList[i].ID))\n\t\t}\n\t\tresp.Success(c, respSubList)\n\t} else {\n\t\tid, err := strconv.ParseUint(idStr, 10, 16)\n\t\tif err != nil {\n\t\t\tresp.ErrorBadRequest(c)\n\t\t\treturn\n\t\t}\n\t\tsubData, err := op.GetSubByID(c.Request.Context(), uint16(id))\n\t\tif err != nil {\n\t\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tvar respSub = [1]sub.Response{}\n\t\trespSub[0] = subData.GenResponse(cron.FetchStatus(subData.ID), node.GetSubInfo(subData.ID))\n\t\tresp.Success(c, respSub)\n\t}\n}\n\n// updateSub 更新订阅链接\n// @Summary 更新订阅链接\n// @Description 根据请求体中的ID更新订阅链接信息\n// @Tags 订阅\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"订阅链接ID\"\n// @Param request body sub.Request true \"更新订阅链接请求\"\n// @Success 200 {object} resp.ResponseStruct{data=sub.Response} \"更新成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"订阅链接不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/sub/{id} [put]\nfunc updateSub(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseUint(idStr, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tvar req sub.Request\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tsubData := req.GenData(uint16(id))\n\tif err := op.UpdateSub(c.Request.Context(), &subData); err != nil {\n\t\tlog.Errorf(\"failed to update sub: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif err := cron.FetchUpdate(&subData); err != nil {\n\t\tlog.Errorf(\"failed to update sub: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\trespData := subData.GenResponse(cron.FetchStatus(subData.ID), node.GetSubInfo(subData.ID))\n\tresp.Success(c, respData)\n}\n\n// deleteSub 删除订阅链接\n// @Summary 删除订阅链接\n// @Description 根据ID删除单个订阅链接\n// @Tags 订阅\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"订阅链接ID\"\n// @Success 200 {object} resp.ResponseStruct \"删除成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"订阅链接不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/sub/{id} [delete]\nfunc deleteSub(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseUint(idStr, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tif err := op.DeleteSub(c.Request.Context(), uint16(id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif err := cron.FetchRemove(uint16(id)); err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tnode.DeleteBySubId(uint16(id))\n\tresp.Success(c, nil)\n}\n\n// refreshSub 手动刷新订阅\n// @Summary 手动刷新订阅\n// @Description 根据ID手动刷新单个订阅\n// @Tags 订阅\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param id path int true \"订阅链接ID\"\n// @Success 200 {object} resp.ResponseStruct{data=sub.Result} \"刷新成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 404 {object} resp.ResponseStruct \"订阅链接不存在\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/sub/refresh/{id} [post]\nfunc refreshSub(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.ParseUint(idStr, 10, 16)\n\tif err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tresult := cron.FetchRun(uint16(id))\n\tresp.Success(c, result)\n}\n\n// batchCreateSub 批量创建订阅链接\n// @Summary 批量创建订阅链接\n// @Description 批量创建多个订阅链接\n// @Tags 订阅\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Param request body []sub.Request true \"批量创建订阅链接请求\"\n// @Success 200 {object} resp.ResponseStruct{data=[]sub.Response} \"创建成功\"\n// @Failure 400 {object} resp.ResponseStruct \"请求参数错误\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/sub/batch [post]\nfunc batchCreateSub(c *gin.Context) {\n\tvar reqs []sub.Request\n\tif err := c.ShouldBindJSON(&reqs); err != nil {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\tif len(reqs) == 0 {\n\t\tresp.ErrorBadRequest(c)\n\t\treturn\n\t}\n\n\tsubs := make([]*sub.Data, len(reqs))\n\tfor i, req := range reqs {\n\t\tsubData := req.GenData(0)\n\t\tsubs[i] = &subData\n\t}\n\n\tif err := op.BatchCreateSub(c.Request.Context(), subs); err != nil {\n\t\tlog.Errorf(\"failed to batch create subs: %v\", err)\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\n\tfor _, subData := range subs {\n\t\tcron.FetchAdd(subData)\n\t}\n\n\trespData := make([]sub.Response, len(subs))\n\tfor i, subData := range subs {\n\t\trespData[i] = subData.GenResponse(cron.FetchStatus(subData.ID), node.GetSubInfo(subData.ID))\n\t}\n\tresp.Success(c, respData)\n}\n"
  },
  {
    "path": "internal/server/handlers/update.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/bestruirui/bestsub/internal/core/update\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc init() {\n\trouter.NewGroupRouter(\"/api/v1/update\").\n\t\tUse(middleware.Auth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"\", router.GET).\n\t\t\t\tHandle(latest),\n\t\t).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/:name\", router.POST).\n\t\t\t\tHandle(updateFunc),\n\t\t)\n}\n\n// latest 最新版本\n// @Summary 最新版本\n// @Description 获取程序最新版本信息\n// @Tags 更新\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=map[string]update.LatestInfo} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/update [get]\nfunc latest(c *gin.Context) {\n\tlatestInfo := make(map[string]update.LatestInfo, 1)\n\tbestsub, err := update.GetLatestBestsubInfo()\n\tif err != nil {\n\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\n\tlatestInfo[\"bestsub\"] = *bestsub\n\tresp.Success(c, latestInfo)\n}\n\n// update 更新\n// @Summary 更新\n// @Description 更新程序\n// @Tags 更新\n// @Accept json\n// @Produce json\n// @Security BearerAuth\n// @Success 200 {object} resp.ResponseStruct{data=string} \"获取成功\"\n// @Failure 401 {object} resp.ResponseStruct \"未授权\"\n// @Failure 500 {object} resp.ResponseStruct \"服务器内部错误\"\n// @Router /api/v1/update/:name [post]\nfunc updateFunc(c *gin.Context) {\n\tname := c.Param(\"name\")\n\tswitch name {\n\tcase \"bestsub\":\n\t\terr := update.UpdateCore()\n\t\tif err != nil {\n\t\t\tresp.Error(c, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\tdefault:\n\t\tresp.ErrorBadRequest(c)\n\t}\n\tresp.Success(c, nil)\n}\n"
  },
  {
    "path": "internal/server/handlers/ws.go",
    "content": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gorilla/websocket\"\n)\n\n// 日志等级优先级常量\nvar logLevelPriority = map[string]int{\n\t\"debug\": 0,\n\t\"info\":  1,\n\t\"warn\":  2,\n\t\"error\": 3,\n\t\"fatal\": 4,\n}\n\n// 默认配置\nconst (\n\tWriteTimeout      = 5  // 10秒\n\tPingInterval      = 5  // 5秒\n\tMaxConnections    = 20 // 20个连接\n\tWriteBufferSize   = 1024\n\tChannelBufferSize = 256\n)\n\n// LogFilter 日志过滤器\ntype LogFilter struct {\n\tnameFilter  string\n\tlevelFilter string\n}\n\n// ShouldSend 检查是否应该发送日志\nfunc (f *LogFilter) ShouldSend(logEntry log.LogEntry) bool {\n\tif f.nameFilter != \"\" && !strings.Contains(logEntry.Name, f.nameFilter) {\n\t\treturn false\n\t}\n\n\tif f.levelFilter != \"\" && !shouldSendLogLevel(f.levelFilter, logEntry.Level) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// shouldSendLogLevel 检查日志等级是否应该发送\nfunc shouldSendLogLevel(filterLevel, logLevel string) bool {\n\tfilterPriority, filterExists := logLevelPriority[filterLevel]\n\tlogPriority, logExists := logLevelPriority[logLevel]\n\n\tif !filterExists || !logExists {\n\t\treturn true\n\t}\n\n\treturn logPriority >= filterPriority\n}\n\n// wsHandler WebSocket处理器\ntype wsHandler struct {\n\tupgrader    websocket.Upgrader\n\tclients     map[*websocket.Conn]*Client\n\tmu          sync.RWMutex\n\tclientCount int32\n}\n\n// Client WebSocket客户端信息\ntype Client struct {\n\tconn   *websocket.Conn\n\tfilter LogFilter\n\tsend   chan log.LogEntry\n\tmu     sync.RWMutex\n}\n\n// init 函数用于自动注册路由\nfunc init() {\n\twsHandler := newWSHandler()\n\n\trouter.NewGroupRouter(\"/api/v1/ws\").\n\t\tUse(middleware.WSAuth()).\n\t\tAddRoute(\n\t\t\trouter.NewRoute(\"/logs\", router.GET).\n\t\t\t\tHandle(wsHandler.handleLogWebSocket),\n\t\t)\n}\n\n// newWSHandler 创建WebSocket处理器\nfunc newWSHandler() *wsHandler {\n\n\th := &wsHandler{\n\t\tupgrader: websocket.Upgrader{\n\t\t\tCheckOrigin: func(r *http.Request) bool {\n\t\t\t\tif utils.IsDebug() {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\n\t\t\t\torigin := r.Header.Get(\"Origin\")\n\n\t\t\t\tif origin == \"\" {\n\t\t\t\t\tlog.Debugf(\"WebSocket客户端连接: 没有Origin头\")\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\t// TODO: 添加允许的域名列表\n\n\t\t\t\tlog.Debugf(\"WebSocket客户端连接: Origin=%s\", origin)\n\n\t\t\t\treturn true\n\t\t\t},\n\t\t\tWriteBufferSize: WriteBufferSize,\n\t\t},\n\t\tclients: make(map[*websocket.Conn]*Client),\n\t}\n\tgo h.broadcastLogs()\n\treturn h\n}\n\nfunc (h *wsHandler) handleLogWebSocket(c *gin.Context) {\n\tif atomic.LoadInt32(&h.clientCount) >= MaxConnections {\n\t\tresp.Error(c, http.StatusTooManyRequests, \"connection limit reached\")\n\t\treturn\n\t}\n\n\tconn, err := h.upgrader.Upgrade(c.Writer, c.Request, nil)\n\tif err != nil {\n\t\tlog.Errorf(\"WebSocket升级失败: %v\", err)\n\t\treturn\n\t}\n\n\tnameFilter := c.Query(\"name\")\n\tlevelFilter := c.Query(\"level\")\n\n\tclient := &Client{\n\t\tconn: conn,\n\t\tfilter: LogFilter{\n\t\t\tnameFilter:  nameFilter,\n\t\t\tlevelFilter: levelFilter,\n\t\t},\n\t\tsend: make(chan log.LogEntry, ChannelBufferSize),\n\t}\n\n\th.mu.Lock()\n\th.clients[conn] = client\n\tatomic.AddInt32(&h.clientCount, 1)\n\th.mu.Unlock()\n\n\tusername, _ := c.Get(\"username\")\n\tclientIP := c.ClientIP()\n\tlog.Infof(\"WebSocket客户端连接: 用户=%s, IP=%s, 当前连接数=%d\", username, clientIP, atomic.LoadInt32(&h.clientCount))\n\n\tgo h.handleClient(client)\n}\n\nfunc (h *wsHandler) broadcastLogs() {\n\tlogChannel := log.GetWSChannel()\n\n\tfor logEntry := range logChannel {\n\t\th.broadcastToClients(logEntry)\n\t}\n}\n\nfunc (h *wsHandler) broadcastToClients(logEntry log.LogEntry) {\n\tvar clientsToRemove []*websocket.Conn\n\n\tfor conn, client := range h.clients {\n\t\tif h.shouldSendLog(client, logEntry) {\n\t\t\tselect {\n\t\t\tcase client.send <- logEntry:\n\t\t\tdefault:\n\t\t\t\tclientsToRemove = append(clientsToRemove, conn)\n\t\t\t\tlog.Warnf(\"WebSocket客户端发送缓冲区满，移除客户端: %v\", conn.RemoteAddr())\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(clientsToRemove) > 0 {\n\t\th.mu.Lock()\n\t\tfor _, conn := range clientsToRemove {\n\t\t\tif client, exists := h.clients[conn]; exists {\n\t\t\t\tclose(client.send)\n\t\t\t\tdelete(h.clients, conn)\n\t\t\t\tatomic.AddInt32(&h.clientCount, -1)\n\t\t\t}\n\t\t}\n\t\th.mu.Unlock()\n\n\t\tif len(clientsToRemove) > 0 {\n\t\t\tlog.Warnf(\"移除了 %d 个缓冲区满的WebSocket客户端\", len(clientsToRemove))\n\t\t}\n\t}\n}\n\nfunc (h *wsHandler) shouldSendLog(client *Client, logEntry log.LogEntry) bool {\n\tclient.mu.RLock()\n\tdefer client.mu.RUnlock()\n\n\treturn client.filter.ShouldSend(logEntry)\n}\n\nfunc (h *wsHandler) handleClient(client *Client) {\n\tdefer func() {\n\t\th.removeClient(client)\n\t\tclient.conn.Close()\n\t}()\n\n\tticker := time.NewTicker(time.Duration(PingInterval) * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase logEntry := <-client.send:\n\t\t\tclient.conn.SetWriteDeadline(time.Now().Add(time.Duration(WriteTimeout) * time.Second))\n\t\t\tif err := client.conn.WriteJSON(logEntry); err != nil {\n\t\t\t\tlog.Errorf(\"WebSocket发送消息失败: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-ticker.C:\n\t\t\tclient.conn.SetWriteDeadline(time.Now().Add(time.Duration(WriteTimeout) * time.Second))\n\t\t\tif err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {\n\t\t\t\tlog.Debugf(\"WebSocket ping失败，断开连接: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (h *wsHandler) removeClient(client *Client) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\tif _, exists := h.clients[client.conn]; exists {\n\t\tdelete(h.clients, client.conn)\n\t\tclose(client.send)\n\t\tatomic.AddInt32(&h.clientCount, -1)\n\t\tlog.Debugf(\"WebSocket客户端断开连接, 当前连接数=%d\", atomic.LoadInt32(&h.clientCount))\n\t}\n}\n"
  },
  {
    "path": "internal/server/middleware/auth.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/bestruirui/bestsub/internal/config\"\n\t\"github.com/bestruirui/bestsub/internal/server/auth\"\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Auth JWT认证中间件\nfunc Auth() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tauthHeader := c.GetHeader(\"Authorization\")\n\t\tif authHeader == \"\" {\n\t\t\tresp.Error(c, http.StatusUnauthorized, \"Authorization header is required\")\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tif !strings.HasPrefix(authHeader, \"Bearer \") {\n\t\t\tresp.Error(c, http.StatusUnauthorized, \"Invalid Authorization header\")\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\ttoken := strings.TrimSpace(strings.TrimPrefix(authHeader, \"Bearer \"))\n\t\tif token == \"\" {\n\t\t\tresp.Error(c, http.StatusUnauthorized, \"Invalid Authorization header\")\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\n\t\tclaims, err := auth.ValidateToken(token, config.Base().JWT.Secret)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"JWT validation failed: %v\", err)\n\t\t\tresp.Error(c, http.StatusUnauthorized, \"Invalid or expired token\")\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tc.Set(\"username\", claims.Username)\n\t\tc.Next()\n\t}\n}\n\n// WSAuth WebSocket专用认证中间件\n// WebSocket连接的认证处理与普通HTTP请求不同，需要特殊处理\nfunc WSAuth() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\ttoken := c.Query(\"token\")\n\n\t\tif token == \"\" {\n\t\t\tlog.Warnf(\"WebSocket authentication failed: missing token, IP=%s\", c.ClientIP())\n\t\t\tc.AbortWithStatus(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tclaims, err := auth.ValidateToken(token, config.Base().JWT.Secret)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"WebSocket JWT validation failed: %v, IP=%s\", err, c.ClientIP())\n\t\t\tc.AbortWithStatus(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tc.Set(\"username\", claims.Username)\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "internal/server/middleware/cors.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gin-contrib/cors\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Cors() gin.HandlerFunc {\n\tconfig := cors.DefaultConfig()\n\tconfig.AllowAllOrigins = true\n\tconfig.AllowCredentials = true\n\tconfig.AllowMethods = []string{\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"}\n\tconfig.AllowHeaders = []string{\"*\"}\n\treturn cors.New(config)\n}\n"
  },
  {
    "path": "internal/server/middleware/logging.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// 日志中间件\nfunc Logging() gin.HandlerFunc {\n\treturn gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {\n\n\t\tlog.Debugf(\"%s %d %s %s %s\",\n\t\t\tparam.Method,\n\t\t\tparam.StatusCode,\n\t\t\tparam.ClientIP,\n\t\t\tparam.Latency,\n\t\t\tparam.Path,\n\t\t)\n\t\treturn \"\"\n\t})\n}\n"
  },
  {
    "path": "internal/server/middleware/recovery.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/bestruirui/bestsub/internal/server/resp\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Recovery() gin.HandlerFunc {\n\treturn gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {\n\t\tlog.Warnf(\"Panic recovered: %v\", recovered)\n\t\tresp.Error(c, http.StatusInternalServerError, \"An unexpected error occurred\")\n\t\tc.Abort()\n\t})\n}\n"
  },
  {
    "path": "internal/server/middleware/static.go",
    "content": "package middleware\n\nimport (\n\t\"io/fs\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc StaticEmbed(urlPrefix string, embedFS fs.FS) gin.HandlerFunc {\n\tfs := http.FS(embedFS)\n\treturn static(urlPrefix, fs)\n}\n\nfunc StaticLocal(urlPrefix string, localPath string) gin.HandlerFunc {\n\tfs := http.Dir(localPath)\n\treturn static(urlPrefix, fs)\n}\n\nfunc static(urlPrefix string, fileSystem http.FileSystem) gin.HandlerFunc {\n\tfileserver := http.FileServer(fileSystem)\n\tif urlPrefix != \"\" {\n\t\tfileserver = http.StripPrefix(urlPrefix, fileserver)\n\t}\n\treturn func(c *gin.Context) {\n\t\tif strings.HasPrefix(c.Request.URL.Path, \"/api\") {\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tif _, err := fileSystem.Open(c.Request.URL.Path); err == nil {\n\t\t\tc.Header(\"Cache-Control\", \"public, max-age=31536000, immutable\")\n\t\t\tfileserver.ServeHTTP(c.Writer, c.Request)\n\t\t\tc.Abort()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/server/resp/resp.go",
    "content": "package resp\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype ResponseStruct struct {\n\tCode    int         `json:\"code\" example:\"200\"`\n\tMessage string      `json:\"message\" example:\"success\"`\n\tData    interface{} `json:\"data,omitempty\"`\n}\n\ntype ResponsePaginationStruct struct {\n\tPage     int         `json:\"page\" example:\"1\"`\n\tPageSize int         `json:\"page_size\" example:\"10\"`\n\tTotal    uint16      `json:\"total\" example:\"100\"`\n\tData     interface{} `json:\"data\"`\n}\n\nfunc Success(c *gin.Context, data any) {\n\tc.JSON(http.StatusOK, ResponseStruct{\n\t\tCode:    http.StatusOK,\n\t\tMessage: \"success\",\n\t\tData:    data,\n\t})\n}\n\nfunc Error(c *gin.Context, code int, err string) {\n\tc.JSON(code, ResponseStruct{\n\t\tCode:    code,\n\t\tMessage: err,\n\t})\n\tc.Abort()\n}\nfunc ErrorBadRequest(c *gin.Context) {\n\tc.JSON(http.StatusBadRequest, ResponseStruct{\n\t\tCode:    http.StatusBadRequest,\n\t\tMessage: \"bad request\",\n\t})\n\tc.Abort()\n}\n"
  },
  {
    "path": "internal/server/router/router.go",
    "content": "package router\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Method represents HTTP methods\ntype Method string\n\nconst (\n\tGET     Method = \"GET\"\n\tPOST    Method = \"POST\"\n\tPUT     Method = \"PUT\"\n\tDELETE  Method = \"DELETE\"\n\tHEAD    Method = \"HEAD\"\n\tOPTIONS Method = \"OPTIONS\"\n\tPATCH   Method = \"PATCH\"\n\tANY     Method = \"ANY\"\n)\n\n// GroupRouter represents a group of routes with shared path prefix and middlewares\ntype GroupRouter struct {\n\tPath        string\n\tRoutes      []*Route\n\tMiddlewares []gin.HandlerFunc\n}\n\n// Global registry for route groups\nvar registeredRouters []*GroupRouter\n\n// NewGroupRouter creates a new GroupRouter with the given path and automatically registers it.\nfunc NewGroupRouter(path string) *GroupRouter {\n\trouter := &GroupRouter{\n\t\tPath:   path,\n\t\tRoutes: make([]*Route, 0),\n\t}\n\tregisteredRouters = append(registeredRouters, router)\n\treturn router\n}\n\n// Use adds middlewares to the group.\nfunc (g *GroupRouter) Use(middlewares ...gin.HandlerFunc) *GroupRouter {\n\tg.Middlewares = append(g.Middlewares, middlewares...)\n\treturn g\n}\n\n// AddRoute adds a route to the group.\nfunc (g *GroupRouter) AddRoute(route *Route) *GroupRouter {\n\tg.Routes = append(g.Routes, route)\n\treturn g\n}\n\n// Route defines a single endpoint with its handlers and middlewares.\ntype Route struct {\n\tPath        string\n\tMethod      Method\n\tHandlers    []gin.HandlerFunc\n\tMiddlewares []gin.HandlerFunc\n}\n\n// NewRoute creates a new Route instance with the given path and method.\nfunc NewRoute(path string, method Method) *Route {\n\treturn &Route{\n\t\tPath:     path,\n\t\tMethod:   method,\n\t\tHandlers: make([]gin.HandlerFunc, 0),\n\t}\n}\n\n// Handle adds handler functions to the route.\nfunc (r *Route) Handle(handlers ...gin.HandlerFunc) *Route {\n\tr.Handlers = append(r.Handlers, handlers...)\n\treturn r\n}\n\n// Use adds middlewares to the route.\nfunc (r *Route) Use(middlewares ...gin.HandlerFunc) *Route {\n\tr.Middlewares = append(r.Middlewares, middlewares...)\n\treturn r\n}\n\n// Validate checks if the route is valid\nfunc (r *Route) Validate() error {\n\tif len(r.Handlers) == 0 {\n\t\treturn fmt.Errorf(\"route must have at least one handler\")\n\t}\n\treturn nil\n}\n\n// GetRouterCount returns the total count of registered routes\nfunc GetRouterCount() int {\n\tcount := 0\n\tfor _, router := range registeredRouters {\n\t\tcount += len(router.Routes)\n\t}\n\treturn count\n}\n\n// RegisterAll registers all globally registered route groups to the Gin engine\nfunc RegisterAll(engine *gin.Engine) error {\n\tfor _, router := range registeredRouters {\n\t\t// Validate all routes in the group first\n\t\tfor _, route := range router.Routes {\n\t\t\tif err := route.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid route in group %s: %w\", router.Path, err)\n\t\t\t}\n\t\t}\n\n\t\t// Create the route group\n\t\tgroup := engine.Group(router.Path, router.Middlewares...)\n\n\t\t// Register all routes in the group\n\t\tfor _, route := range router.Routes {\n\t\t\thandlers := make([]gin.HandlerFunc, 0, len(route.Middlewares)+len(route.Handlers))\n\t\t\thandlers = append(handlers, route.Middlewares...)\n\t\t\thandlers = append(handlers, route.Handlers...)\n\n\t\t\tregisterRoute(group, route.Method, route.Path, handlers)\n\t\t}\n\t}\n\tregisteredRouters = nil\n\treturn nil\n}\n\n// registerRoute registers a single route to a Gin route group.\nfunc registerRoute(group *gin.RouterGroup, method Method, path string, handlers []gin.HandlerFunc) {\n\tif len(handlers) == 0 {\n\t\treturn\n\t}\n\n\tif path != \"\" {\n\t\tif !strings.HasPrefix(path, \"/\") {\n\t\t\tpath = \"/\" + path\n\t\t}\n\t}\n\n\tswitch method {\n\tcase GET:\n\t\tgroup.GET(path, handlers...)\n\tcase POST:\n\t\tgroup.POST(path, handlers...)\n\tcase PUT:\n\t\tgroup.PUT(path, handlers...)\n\tcase DELETE:\n\t\tgroup.DELETE(path, handlers...)\n\tcase HEAD:\n\t\tgroup.HEAD(path, handlers...)\n\tcase OPTIONS:\n\t\tgroup.OPTIONS(path, handlers...)\n\tcase PATCH:\n\t\tgroup.PATCH(path, handlers...)\n\tcase ANY:\n\t\tgroup.Any(path, handlers...)\n\tdefault:\n\t\tgroup.GET(path, handlers...)\n\t}\n}\n"
  },
  {
    "path": "internal/server/server/server.go",
    "content": "// Package server 提供 BestSub 应用程序的入口点。\n//\n// @title BestSub API\n// @version 1.0.0\n// @description BestSub -  API 文档\n// @description\n// @description 这是 BestSub 的 API 文档\n// @description\n// @description ## 认证\n// @description 大多数接口需要使用 JWT 令牌进行认证。\n// @description 认证时，请在 Authorization 头中包含 JWT 令牌：\n// @description `Authorization: Bearer <your-jwt-token>`\n// @description\n// @description ## 错误响应\n// @description 所有错误响应都遵循统一格式，包含 code、message 和 error 字段。\n// @description\n// @description ## 成功响应\n// @description 所有成功响应都遵循统一格式，包含 code、message 和 data 字段。\n//\n// @contact.name BestSub API 支持\n// @contact.email support@bestsub.com\n//\n// @license.name GPL-3.0\n// @license.url https://opensource.org/license/gpl-3-0\n//\n// @securityDefinitions.apikey BearerAuth\n// @in header\n// @name Authorization\n// @description 类型为 \"Bearer\"，后跟空格和 JWT 令牌。\n//\n// @tag.name 认证\n// @tag.description 用户认证相关接口\n//\n// @tag.name 系统\n// @tag.description 系统状态和健康检查接口\npackage server\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/config\"\n\t_ \"github.com/bestruirui/bestsub/internal/server/handlers\"\n\t\"github.com/bestruirui/bestsub/internal/server/middleware\"\n\t\"github.com/bestruirui/bestsub/internal/server/router\"\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n\t\"github.com/bestruirui/bestsub/static\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nconst (\n\tdefaultReadTimeout     = 30 * time.Second\n\tdefaultWriteTimeout    = 30 * time.Second\n\tdefaultIdleTimeout     = 60 * time.Second\n\tdefaultShutdownTimeout = 30 * time.Second\n\tdefaultMaxHeaderBytes  = 1 << 20 // 1MB\n)\n\nvar server *Server\n\ntype Server struct {\n\thttpServer *http.Server\n\trouter     *gin.Engine\n}\n\nfunc Initialize() error {\n\n\tr, routerErr := setRouter()\n\tif routerErr != nil {\n\t\treturn fmt.Errorf(\"failed to set router: %w\", routerErr)\n\t}\n\n\tserver = &Server{\n\t\thttpServer: &http.Server{\n\t\t\tAddr:           fmt.Sprintf(\"%s:%d\", config.Base().Server.Host, config.Base().Server.Port),\n\t\t\tHandler:        r,\n\t\t\tReadTimeout:    defaultReadTimeout,\n\t\t\tWriteTimeout:   defaultWriteTimeout,\n\t\t\tIdleTimeout:    defaultIdleTimeout,\n\t\t\tMaxHeaderBytes: defaultMaxHeaderBytes,\n\t\t},\n\t\trouter: r,\n\t}\n\treturn nil\n}\n\nfunc Start() error {\n\tif server == nil {\n\t\treturn fmt.Errorf(\"HTTP server not initialized, please call Initialize() first\")\n\t}\n\n\tlog.Infof(\"Starting HTTP server %s\", server.httpServer.Addr)\n\n\tgo func() {\n\t\tif err := server.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\tlog.Errorf(\"Failed to start HTTP server: %v\", err)\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc Close() error {\n\tif server == nil {\n\t\treturn fmt.Errorf(\"HTTP server not initialized\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultShutdownTimeout)\n\tdefer cancel()\n\n\tif err := server.httpServer.Shutdown(ctx); err != nil {\n\t\tlog.Errorf(\"HTTP server force closed: %v\", err)\n\t\treturn fmt.Errorf(\"HTTP server force closed: %w\", err)\n\t}\n\n\tlog.Debug(\"HTTP server closed\")\n\treturn nil\n}\n\nfunc IsInitialized() bool {\n\treturn server != nil\n}\n\nfunc setRouter() (*gin.Engine, error) {\n\tgin.SetMode(gin.ReleaseMode)\n\tr := gin.New()\n\n\t// r.Use(middleware.Logging())\n\tr.Use(middleware.Recovery())\n\tr.Use(middleware.Cors())\n\tr.Use(middleware.StaticEmbed(\"/\", static.StaticFS))\n\n\tif err := router.RegisterAll(r); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to register routes: %w\", err)\n\t}\n\n\tlog.Debugf(\"successfully registered %d routes\", router.GetRouterCount())\n\treturn r, nil\n}\n"
  },
  {
    "path": "internal/utils/cache/cache.go",
    "content": "// This implementation is based on and modified from https://github.com/fanjindong/go-cache\npackage cache\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/cespare/xxhash/v2\"\n)\n\nfunc keyToString[K comparable](key K) string {\n\treturn fmt.Sprintf(\"%v\", key)\n}\n\ntype Cache[K comparable, V any] interface {\n\tSet(k K, v V)\n\tGet(k K) (V, bool)\n\tGetAll() map[K]V\n\tDel(keys ...K) int\n\tExists(keys ...K) bool\n\tLen() int\n\tClear()\n}\n\nfunc New[K comparable, V any](shards int) Cache[K, V] {\n\tif shards <= 0 {\n\t\tshards = 1024\n\t}\n\n\tc := &cache[K, V]{\n\t\tshards:    make([]*shard[K, V], shards),\n\t\tshardMask: uint64(shards - 1),\n\t}\n\tfor i := 0; i < shards; i++ {\n\t\tc.shards[i] = &shard[K, V]{hashmap: map[K]V{}}\n\t}\n\n\treturn c\n}\n\ntype cache[K comparable, V any] struct {\n\tshards    []*shard[K, V]\n\tshardMask uint64\n}\n\nfunc (c *cache[K, V]) Set(k K, v V) {\n\thashedKey := xxhash.Sum64String(keyToString(k))\n\tshard := c.getShard(hashedKey)\n\tshard.set(k, v)\n}\n\nfunc (c *cache[K, V]) Get(k K) (V, bool) {\n\thashedKey := xxhash.Sum64String(keyToString(k))\n\tshard := c.getShard(hashedKey)\n\treturn shard.get(k)\n}\n\nfunc (c *cache[K, V]) GetAll() map[K]V {\n\tresult := make(map[K]V)\n\tfor _, shard := range c.shards {\n\t\tshardData := shard.getAll()\n\t\tfor k, v := range shardData {\n\t\t\tresult[k] = v\n\t\t}\n\t}\n\treturn result\n}\n\nfunc (c *cache[K, V]) Del(ks ...K) int {\n\tvar count int\n\tfor _, k := range ks {\n\t\thashedKey := xxhash.Sum64String(keyToString(k))\n\t\tshard := c.getShard(hashedKey)\n\t\tcount += shard.del(k)\n\t}\n\treturn count\n}\n\nfunc (c *cache[K, V]) Exists(ks ...K) bool {\n\tfor _, k := range ks {\n\t\tif _, found := c.Get(k); !found {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (c *cache[K, V]) Len() int {\n\tvar count int\n\tfor _, shard := range c.shards {\n\t\tcount += shard.len()\n\t}\n\treturn count\n}\n\nfunc (c *cache[K, V]) getShard(hashedKey uint64) (shard *shard[K, V]) {\n\treturn c.shards[hashedKey&c.shardMask]\n}\n\nfunc (c *cache[K, V]) Clear() {\n\tfor _, s := range c.shards {\n\t\ts.clear()\n\t}\n}\n"
  },
  {
    "path": "internal/utils/cache/shard.go",
    "content": "package cache\n\nimport (\n\t\"sync\"\n)\n\ntype shard[K comparable, V any] struct {\n\thashmap map[K]V\n\tlock    sync.RWMutex\n}\n\nfunc (c *shard[K, V]) set(k K, v V) {\n\tc.lock.Lock()\n\tc.hashmap[k] = v\n\tc.lock.Unlock()\n}\n\nfunc (c *shard[K, V]) get(k K) (V, bool) {\n\tc.lock.RLock()\n\titem, exist := c.hashmap[k]\n\tc.lock.RUnlock()\n\tif !exist {\n\t\tvar zero V\n\t\treturn zero, false\n\t}\n\treturn item, true\n}\n\nfunc (c *shard[K, V]) del(k K) int {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tif _, found := c.hashmap[k]; found {\n\t\tdelete(c.hashmap, k)\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc (c *shard[K, V]) clear() {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.hashmap = map[K]V{}\n}\n\nfunc (c *shard[K, V]) getAll() map[K]V {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tresult := make(map[K]V, len(c.hashmap))\n\tfor k, v := range c.hashmap {\n\t\tresult[k] = v\n\t}\n\treturn result\n}\n\nfunc (c *shard[K, V]) len() int {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn len(c.hashmap)\n}\n"
  },
  {
    "path": "internal/utils/color/color.go",
    "content": "package color\n\n// 定义颜色和格式化字符串常量\nconst (\n\tReset  string = \"\\033[0m\"\n\tRed    string = \"\\033[31m\"\n\tGreen  string = \"\\033[32m\"\n\tYellow string = \"\\033[33m\"\n\tBlue   string = \"\\033[34m\"\n\tPurple string = \"\\033[35m\"\n\tCyan   string = \"\\033[36m\"\n\tWhite  string = \"\\033[37m\"\n\tBold   string = \"\\033[1m\"\n\tDim    string = \"\\033[2m\"\n)\n"
  },
  {
    "path": "internal/utils/country/conutry.go",
    "content": "// 此文件由AI生成。如有错误，请手动修正\n// This file was generated with AI assistance\n// Please correct any errors manually\n\npackage country\n\ntype Country struct {\n\tNameEn string\n\tNameZh string\n\tEmoji  string\n}\n\nfunc GetCountry(code string) Country {\n\tif code == \"\" {\n\t\treturn Country{}\n\t}\n\tif names, ok := namesByNumeric[code]; ok {\n\t\treturn Country{NameEn: code, NameZh: names, Emoji: emoji(code)}\n\t}\n\treturn Country{}\n}\n\nfunc emoji(alpha2 string) string {\n\tif len(alpha2) != 2 {\n\t\treturn \"\"\n\t}\n\tb0 := rune(alpha2[0])\n\tb1 := rune(alpha2[1])\n\tif b0 >= 'a' && b0 <= 'z' {\n\t\tb0 -= 32\n\t}\n\tif b1 >= 'a' && b1 <= 'z' {\n\t\tb1 -= 32\n\t}\n\tif b0 < 'A' || b0 > 'Z' || b1 < 'A' || b1 > 'Z' {\n\t\treturn \"\"\n\t}\n\tr0 := 0x1F1E6 + (b0 - 'A')\n\tr1 := 0x1F1E6 + (b1 - 'A')\n\treturn string([]rune{r0, r1})\n}\n\nvar namesByNumeric = map[string]string{\n\t\"AD\": \"安道尔\",\n\t\"AE\": \"阿联酋\",\n\t\"AF\": \"阿富汗\",\n\t\"AG\": \"安提瓜和巴布达\",\n\t\"AI\": \"安圭拉\",\n\t\"AL\": \"阿尔巴尼亚\",\n\t\"AM\": \"亚美尼亚\",\n\t\"AO\": \"安哥拉\",\n\t\"AQ\": \"南极洲\",\n\t\"AR\": \"阿根廷\",\n\t\"AS\": \"美属萨摩亚\",\n\t\"AT\": \"奥地利\",\n\t\"AU\": \"澳大利亚\",\n\t\"AW\": \"阿鲁巴\",\n\t\"AZ\": \"阿塞拜疆\",\n\t\"BA\": \"波黑\",\n\t\"BB\": \"巴巴多斯\",\n\t\"BD\": \"孟加拉国\",\n\t\"BE\": \"比利时\",\n\t\"BF\": \"布基纳法索\",\n\t\"BG\": \"保加利亚\",\n\t\"BH\": \"巴林\",\n\t\"BI\": \"布隆迪\",\n\t\"BJ\": \"贝宁\",\n\t\"BL\": \"法属圣巴泰勒米\",\n\t\"BM\": \"百慕大\",\n\t\"BN\": \"文莱\",\n\t\"BO\": \"玻利维亚\",\n\t\"BQ\": \"荷属加勒比区\",\n\t\"BR\": \"巴西\",\n\t\"BS\": \"巴哈马\",\n\t\"BT\": \"不丹\",\n\t\"BV\": \"布维岛\",\n\t\"BW\": \"博茨瓦纳\",\n\t\"BY\": \"白俄罗斯\",\n\t\"BZ\": \"伯利兹\",\n\t\"CA\": \"加拿大\",\n\t\"CC\": \"科科斯（基林）群岛\",\n\t\"CD\": \"刚果（金）\",\n\t\"CF\": \"中非共和国\",\n\t\"CG\": \"刚果（布）\",\n\t\"CH\": \"瑞士\",\n\t\"CI\": \"科特迪瓦\",\n\t\"CK\": \"库克群岛\",\n\t\"CL\": \"智利\",\n\t\"CM\": \"喀麦隆\",\n\t\"CN\": \"中国\",\n\t\"CO\": \"哥伦比亚\",\n\t\"CR\": \"哥斯达黎加\",\n\t\"CU\": \"古巴\",\n\t\"CV\": \"佛得角\",\n\t\"CW\": \"库拉索\",\n\t\"CX\": \"圣诞岛\",\n\t\"CY\": \"塞浦路斯\",\n\t\"CZ\": \"捷克\",\n\t\"DE\": \"德国\",\n\t\"DJ\": \"吉布提\",\n\t\"DK\": \"丹麦\",\n\t\"DM\": \"多米尼克\",\n\t\"DO\": \"多米尼加\",\n\t\"DZ\": \"阿尔及利亚\",\n\t\"EC\": \"厄瓜多尔\",\n\t\"EE\": \"爱沙尼亚\",\n\t\"EG\": \"埃及\",\n\t\"EH\": \"西撒哈拉\",\n\t\"ER\": \"厄立特里亚\",\n\t\"ES\": \"西班牙\",\n\t\"ET\": \"埃塞俄比亚\",\n\t\"FI\": \"芬兰\",\n\t\"FJ\": \"斐济\",\n\t\"FK\": \"福克兰群岛（马尔维纳斯）\",\n\t\"FM\": \"密克罗尼西亚联邦\",\n\t\"FO\": \"法罗群岛\",\n\t\"FR\": \"法国\",\n\t\"GA\": \"加蓬\",\n\t\"GB\": \"英国\",\n\t\"GD\": \"格林纳达\",\n\t\"GE\": \"格鲁吉亚\",\n\t\"GF\": \"法属圭亚那\",\n\t\"GG\": \"根西\",\n\t\"GH\": \"加纳\",\n\t\"GI\": \"直布罗陀\",\n\t\"GL\": \"格陵兰\",\n\t\"GM\": \"冈比亚\",\n\t\"GN\": \"几内亚\",\n\t\"GP\": \"瓜德罗普\",\n\t\"GQ\": \"赤道几内亚\",\n\t\"GR\": \"希腊\",\n\t\"GS\": \"南乔治亚岛和南桑威奇群岛\",\n\t\"GT\": \"危地马拉\",\n\t\"GU\": \"关岛\",\n\t\"GW\": \"几内亚比绍\",\n\t\"GY\": \"圭亚那\",\n\t\"HK\": \"中国香港\",\n\t\"HM\": \"赫德岛和麦克唐纳群岛\",\n\t\"HN\": \"洪都拉斯\",\n\t\"HR\": \"克罗地亚\",\n\t\"HT\": \"海地\",\n\t\"HU\": \"匈牙利\",\n\t\"ID\": \"印度尼西亚\",\n\t\"IE\": \"爱尔兰\",\n\t\"IL\": \"以色列\",\n\t\"IM\": \"马恩岛\",\n\t\"IN\": \"印度\",\n\t\"IO\": \"英属印度洋领地\",\n\t\"IQ\": \"伊拉克\",\n\t\"IR\": \"伊朗\",\n\t\"IS\": \"冰岛\",\n\t\"IT\": \"意大利\",\n\t\"JE\": \"泽西\",\n\t\"JM\": \"牙买加\",\n\t\"JO\": \"约旦\",\n\t\"JP\": \"日本\",\n\t\"KE\": \"肯尼亚\",\n\t\"KG\": \"吉尔吉斯斯坦\",\n\t\"KH\": \"柬埔寨\",\n\t\"KI\": \"基里巴斯\",\n\t\"KM\": \"科摩罗\",\n\t\"KN\": \"圣基茨和尼维斯\",\n\t\"KP\": \"朝鲜\",\n\t\"KR\": \"韩国\",\n\t\"KW\": \"科威特\",\n\t\"KY\": \"开曼群岛\",\n\t\"KZ\": \"哈萨克斯坦\",\n\t\"LA\": \"老挝\",\n\t\"LB\": \"黎巴嫩\",\n\t\"LC\": \"圣卢西亚\",\n\t\"LI\": \"列支敦士登\",\n\t\"LK\": \"斯里兰卡\",\n\t\"LR\": \"利比里亚\",\n\t\"LS\": \"莱索托\",\n\t\"LT\": \"立陶宛\",\n\t\"LU\": \"卢森堡\",\n\t\"LV\": \"拉脱维亚\",\n\t\"LY\": \"利比亚\",\n\t\"MA\": \"摩洛哥\",\n\t\"MC\": \"摩纳哥\",\n\t\"MD\": \"摩尔多瓦\",\n\t\"ME\": \"黑山\",\n\t\"MF\": \"法属圣马丁\",\n\t\"MG\": \"马达加斯加\",\n\t\"MH\": \"马绍尔群岛\",\n\t\"MK\": \"北马其顿\",\n\t\"ML\": \"马里\",\n\t\"MM\": \"缅甸\",\n\t\"MN\": \"蒙古\",\n\t\"MO\": \"中国澳门\",\n\t\"MP\": \"北马里亚纳群岛\",\n\t\"MQ\": \"马提尼克\",\n\t\"MR\": \"毛里塔尼亚\",\n\t\"MS\": \"蒙特塞拉特\",\n\t\"MT\": \"马耳他\",\n\t\"MU\": \"毛里求斯\",\n\t\"MV\": \"马尔代夫\",\n\t\"MW\": \"马拉维\",\n\t\"MX\": \"墨西哥\",\n\t\"MY\": \"马来西亚\",\n\t\"MZ\": \"莫桑比克\",\n\t\"NA\": \"纳米比亚\",\n\t\"NC\": \"新喀里多尼亚\",\n\t\"NE\": \"尼日尔\",\n\t\"NF\": \"诺福克岛\",\n\t\"NG\": \"尼日利亚\",\n\t\"NI\": \"尼加拉瓜\",\n\t\"NL\": \"荷兰\",\n\t\"NO\": \"挪威\",\n\t\"NP\": \"尼泊尔\",\n\t\"NR\": \"瑙鲁\",\n\t\"NU\": \"纽埃\",\n\t\"NZ\": \"新西兰\",\n\t\"OM\": \"阿曼\",\n\t\"PA\": \"巴拿马\",\n\t\"PE\": \"秘鲁\",\n\t\"PF\": \"法属波利尼西亚\",\n\t\"PG\": \"巴布亚新几内亚\",\n\t\"PH\": \"菲律宾\",\n\t\"PK\": \"巴基斯坦\",\n\t\"PL\": \"波兰\",\n\t\"PM\": \"圣皮埃尔和密克隆\",\n\t\"PN\": \"皮特凯恩群岛\",\n\t\"PR\": \"波多黎各\",\n\t\"PS\": \"巴勒斯坦国\",\n\t\"PT\": \"葡萄牙\",\n\t\"PW\": \"帕劳\",\n\t\"PY\": \"巴拉圭\",\n\t\"QA\": \"卡塔尔\",\n\t\"RE\": \"留尼汪\",\n\t\"RO\": \"罗马尼亚\",\n\t\"RS\": \"塞尔维亚\",\n\t\"RU\": \"俄罗斯\",\n\t\"RW\": \"卢旺达\",\n\t\"SA\": \"沙特阿拉伯\",\n\t\"SB\": \"所罗门群岛\",\n\t\"SC\": \"塞舌尔\",\n\t\"SD\": \"苏丹\",\n\t\"SE\": \"瑞典\",\n\t\"SG\": \"新加坡\",\n\t\"SH\": \"圣赫勒拿、阿森松和特里斯坦-达库尼亚\",\n\t\"SI\": \"斯洛文尼亚\",\n\t\"SJ\": \"斯瓦尔巴和扬马延\",\n\t\"SK\": \"斯洛伐克\",\n\t\"SL\": \"塞拉利昂\",\n\t\"SM\": \"圣马力诺\",\n\t\"SN\": \"塞内加尔\",\n\t\"SO\": \"索马里\",\n\t\"SR\": \"苏里南\",\n\t\"SS\": \"南苏丹\",\n\t\"ST\": \"圣多美和普林西比\",\n\t\"SV\": \"萨尔瓦多\",\n\t\"SX\": \"荷属圣马丁\",\n\t\"SY\": \"叙利亚\",\n\t\"SZ\": \"埃斯瓦蒂尼\",\n\t\"TC\": \"特克斯和凯科斯群岛\",\n\t\"TD\": \"乍得\",\n\t\"TF\": \"法属南部领地\",\n\t\"TG\": \"多哥\",\n\t\"TH\": \"泰国\",\n\t\"TJ\": \"塔吉克斯坦\",\n\t\"TK\": \"托克劳\",\n\t\"TL\": \"东帝汶\",\n\t\"TM\": \"土库曼斯坦\",\n\t\"TN\": \"突尼斯\",\n\t\"TO\": \"汤加\",\n\t\"TR\": \"土耳其\",\n\t\"TT\": \"特立尼达和多巴哥\",\n\t\"TV\": \"图瓦卢\",\n\t\"TW\": \"中国台湾\",\n\t\"TZ\": \"坦桑尼亚\",\n\t\"UA\": \"乌克兰\",\n\t\"UG\": \"乌干达\",\n\t\"UM\": \"美国本土外小岛屿\",\n\t\"US\": \"美国\",\n\t\"UY\": \"乌拉圭\",\n\t\"UZ\": \"乌兹别克斯坦\",\n\t\"VA\": \"梵蒂冈\",\n\t\"VC\": \"圣文森特和格林纳丁斯\",\n\t\"VE\": \"委内瑞拉\",\n\t\"VG\": \"英属维尔京群岛\",\n\t\"VI\": \"美属维尔京群岛\",\n\t\"VN\": \"越南\",\n\t\"VU\": \"瓦努阿图\",\n\t\"WF\": \"瓦利斯和富图纳\",\n\t\"WS\": \"萨摩亚\",\n\t\"YE\": \"也门\",\n\t\"YT\": \"马约特\",\n\t\"ZA\": \"南非\",\n\t\"ZM\": \"赞比亚\",\n\t\"ZW\": \"津巴布韦\",\n}\n"
  },
  {
    "path": "internal/utils/desc/desc.go",
    "content": "package desc\n\nimport (\n\t\"reflect\"\n)\n\nconst (\n\tTypeBoolean     = \"boolean\"\n\tTypeNumber      = \"number\"\n\tTypeString      = \"string\"\n\tTypeSelect      = \"select\"\n\tTypeMultiSelect = \"multi_select\"\n)\n\ntype Data struct {\n\tName    string `json:\"name,omitempty\"`\n\tKey     string `json:\"key,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n\tValue   string `json:\"value,omitempty\"`\n\tOptions string `json:\"options,omitempty\"`\n\tRequire bool   `json:\"require,omitempty\"`\n\tDesc    string `json:\"desc,omitempty\"`\n}\n\nfunc Gen(v any) []Data {\n\tt := reflect.TypeOf(v)\n\tif t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\treturn gen(t)\n}\n\nfunc gen(t reflect.Type) []Data {\n\tvar items []Data\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tfield := t.Field(i)\n\t\tif field.Type.Kind() == reflect.Struct {\n\t\t\titems = append(items, gen(field.Type)...)\n\t\t\tcontinue\n\t\t}\n\t\ttag := field.Tag\n\t\tkey, ok := tag.Lookup(\"json\")\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\ttypeName := tag.Get(\"type\")\n\t\tif typeName == \"\" {\n\t\t\ttypeName = getType(field.Type.Name())\n\t\t}\n\t\titem := Data{\n\t\t\tName:    tag.Get(\"name\"),\n\t\t\tKey:     key,\n\t\t\tType:    typeName,\n\t\t\tValue:   tag.Get(\"value\"),\n\t\t\tOptions: tag.Get(\"options\"),\n\t\t\tRequire: tag.Get(\"require\") == \"true\",\n\t\t\tDesc:    tag.Get(\"desc\"),\n\t\t}\n\t\titems = append(items, item)\n\t}\n\treturn items\n}\n\nfunc getType(t string) string {\n\tswitch t {\n\tcase \"bool\":\n\t\treturn \"boolean\"\n\tcase \"int\", \"int8\", \"int16\", \"int32\", \"int64\",\n\t\t\"uint\", \"uint8\", \"uint16\", \"uint32\", \"uint64\",\n\t\t\"float32\", \"float64\":\n\t\treturn \"number\"\n\tcase \"string\", \"[]byte\":\n\t\treturn \"string\"\n\tdefault:\n\t\treturn \"object\"\n\t}\n}\n"
  },
  {
    "path": "internal/utils/generic/map.go",
    "content": "// This file is based on the generic sync.Map implementation from:\n// https://github.com/SaveTheRbtz/generic-sync-map-go\n// Licensed under the MIT License\n//\n// Original copyright notice:\n// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage generic\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"unsafe\"\n)\n\n// MapOf is like a Go map[interface{}]interface{} but is safe for concurrent use\n// by multiple goroutines without additional locking or coordination.\n// Loads, stores, and deletes run in amortized constant time.\n//\n// The MapOf type is specialized. Most code should use a plain Go map instead,\n// with separate locking or coordination, for better type safety and to make it\n// easier to maintain other invariants along with the map content.\n//\n// The MapOf type is optimized for two common use cases: (1) when the entry for a given\n// key is only ever written once but read many times, as in caches that only grow,\n// or (2) when multiple goroutines read, write, and overwrite entries for disjoint\n// sets of keys. In these two cases, use of a MapOf may significantly reduce lock\n// contention compared to a Go map paired with a separate Mutex or RWMutex.\n//\n// The zero MapOf is empty and ready for use. A MapOf must not be copied after first use.\ntype MapOf[K comparable, V any] struct {\n\tmu sync.Mutex\n\n\t// read contains the portion of the map's contents that are safe for\n\t// concurrent access (with or without mu held).\n\t//\n\t// The read field itself is always safe to load, but must only be stored with\n\t// mu held.\n\t//\n\t// Entries stored in read may be updated concurrently without mu, but updating\n\t// a previously-expunged entry requires that the entry be copied to the dirty\n\t// map and unexpunged with mu held.\n\tread atomic.Value // readOnly\n\n\t// dirty contains the portion of the map's contents that require mu to be\n\t// held. To ensure that the dirty map can be promoted to the read map quickly,\n\t// it also includes all of the non-expunged entries in the read map.\n\t//\n\t// Expunged entries are not stored in the dirty map. An expunged entry in the\n\t// clean map must be unexpunged and added to the dirty map before a new value\n\t// can be stored to it.\n\t//\n\t// If the dirty map is nil, the next write to the map will initialize it by\n\t// making a shallow copy of the clean map, omitting stale entries.\n\tdirty map[K]*entry[V]\n\n\t// misses counts the number of loads since the read map was last updated that\n\t// needed to lock mu to determine whether the key was present.\n\t//\n\t// Once enough misses have occurred to cover the cost of copying the dirty\n\t// map, the dirty map will be promoted to the read map (in the unamended\n\t// state) and the next store to the map will make a new dirty copy.\n\tmisses int\n}\n\n// readOnly is an immutable struct stored atomically in the MapOf.read field.\ntype readOnly[K comparable, V any] struct {\n\tm       map[K]*entry[V]\n\tamended bool // true if the dirty map contains some key not in m.\n}\n\n// expunged is an arbitrary pointer that marks entries which have been deleted\n// from the dirty map.\nvar expunged = unsafe.Pointer(new(interface{}))\n\n// An entry is a slot in the map corresponding to a particular key.\ntype entry[V any] struct {\n\t// p points to the interface{} value stored for the entry.\n\t//\n\t// If p == nil, the entry has been deleted and m.dirty == nil.\n\t//\n\t// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry\n\t// is missing from m.dirty.\n\t//\n\t// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty\n\t// != nil, in m.dirty[key].\n\t//\n\t// An entry can be deleted by atomic replacement with nil: when m.dirty is\n\t// next created, it will atomically replace nil with expunged and leave\n\t// m.dirty[key] unset.\n\t//\n\t// An entry's associated value can be updated by atomic replacement, provided\n\t// p != expunged. If p == expunged, an entry's associated value can be updated\n\t// only after first setting m.dirty[key] = e so that lookups using the dirty\n\t// map find the entry.\n\tp unsafe.Pointer // *interface{}\n}\n\nfunc newEntry[V any](i V) *entry[V] {\n\treturn &entry[V]{p: unsafe.Pointer(&i)}\n}\n\n// Load returns the value stored in the map for a key, or nil if no\n// value is present.\n// The ok result indicates whether value was found in the map.\nfunc (m *MapOf[K, V]) Load(key K) (value V, ok bool) {\n\tread, _ := m.read.Load().(readOnly[K, V])\n\te, ok := read.m[key]\n\tif !ok && read.amended {\n\t\tm.mu.Lock()\n\t\t// Avoid reporting a spurious miss if m.dirty got promoted while we were\n\t\t// blocked on m.mu. (If further loads of the same key will not miss, it's\n\t\t// not worth copying the dirty map for this key.)\n\t\tread, _ = m.read.Load().(readOnly[K, V])\n\t\te, ok = read.m[key]\n\t\tif !ok && read.amended {\n\t\t\te, ok = m.dirty[key]\n\t\t\t// Regardless of whether the entry was present, record a miss: this key\n\t\t\t// will take the slow path until the dirty map is promoted to the read\n\t\t\t// map.\n\t\t\tm.missLocked()\n\t\t}\n\t\tm.mu.Unlock()\n\t}\n\tif !ok {\n\t\treturn value, false\n\t}\n\treturn e.load()\n}\n\nfunc (e *entry[V]) load() (value V, ok bool) {\n\tp := atomic.LoadPointer(&e.p)\n\tif p == nil || p == expunged {\n\t\treturn value, false\n\t}\n\treturn *(*V)(p), true\n}\n\n// Store sets the value for a key.\nfunc (m *MapOf[K, V]) Store(key K, value V) {\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok && e.tryStore(&value) {\n\t\treturn\n\t}\n\n\tm.mu.Lock()\n\tread, _ = m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok {\n\t\tif e.unexpungeLocked() {\n\t\t\t// The entry was previously expunged, which implies that there is a\n\t\t\t// non-nil dirty map and this entry is not in it.\n\t\t\tm.dirty[key] = e\n\t\t}\n\t\te.storeLocked(&value)\n\t} else if e, ok := m.dirty[key]; ok {\n\t\te.storeLocked(&value)\n\t} else {\n\t\tif !read.amended {\n\t\t\t// We're adding the first new key to the dirty map.\n\t\t\t// Make sure it is allocated and mark the read-only map as incomplete.\n\t\t\tm.dirtyLocked()\n\t\t\tm.read.Store(readOnly[K, V]{m: read.m, amended: true})\n\t\t}\n\t\tm.dirty[key] = newEntry(value)\n\t}\n\tm.mu.Unlock()\n}\n\n// tryStore stores a value if the entry has not been expunged.\n//\n// If the entry is expunged, tryStore returns false and leaves the entry\n// unchanged.\nfunc (e *entry[V]) tryStore(i *V) bool {\n\tfor {\n\t\tp := atomic.LoadPointer(&e.p)\n\t\tif p == expunged {\n\t\t\treturn false\n\t\t}\n\t\tif atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {\n\t\t\treturn true\n\t\t}\n\t}\n}\n\n// unexpungeLocked ensures that the entry is not marked as expunged.\n//\n// If the entry was previously expunged, it must be added to the dirty map\n// before m.mu is unlocked.\nfunc (e *entry[V]) unexpungeLocked() (wasExpunged bool) {\n\treturn atomic.CompareAndSwapPointer(&e.p, expunged, nil)\n}\n\n// storeLocked unconditionally stores a value to the entry.\n//\n// The entry must be known not to be expunged.\nfunc (e *entry[V]) storeLocked(i *V) {\n\tatomic.StorePointer(&e.p, unsafe.Pointer(i))\n}\n\n// LoadOrStore returns the existing value for the key if present.\n// Otherwise, it stores and returns the given value.\n// The loaded result is true if the value was loaded, false if stored.\nfunc (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {\n\t// Avoid locking if it's a clean hit.\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok {\n\t\tactual, loaded, ok := e.tryLoadOrStore(value)\n\t\tif ok {\n\t\t\treturn actual, loaded\n\t\t}\n\t}\n\n\tm.mu.Lock()\n\tread, _ = m.read.Load().(readOnly[K, V])\n\tif e, ok := read.m[key]; ok {\n\t\tif e.unexpungeLocked() {\n\t\t\tm.dirty[key] = e\n\t\t}\n\t\tactual, loaded, _ = e.tryLoadOrStore(value)\n\t} else if e, ok := m.dirty[key]; ok {\n\t\tactual, loaded, _ = e.tryLoadOrStore(value)\n\t\tm.missLocked()\n\t} else {\n\t\tif !read.amended {\n\t\t\t// We're adding the first new key to the dirty map.\n\t\t\t// Make sure it is allocated and mark the read-only map as incomplete.\n\t\t\tm.dirtyLocked()\n\t\t\tm.read.Store(readOnly[K, V]{m: read.m, amended: true})\n\t\t}\n\t\tm.dirty[key] = newEntry(value)\n\t\tactual, loaded = value, false\n\t}\n\tm.mu.Unlock()\n\n\treturn actual, loaded\n}\n\n// tryLoadOrStore atomically loads or stores a value if the entry is not\n// expunged.\n//\n// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and\n// returns with ok==false.\nfunc (e *entry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) {\n\tp := atomic.LoadPointer(&e.p)\n\tif p == expunged {\n\t\treturn actual, false, false\n\t}\n\tif p != nil {\n\t\treturn *(*V)(p), true, true\n\t}\n\n\t// Copy the interface after the first load to make this method more amenable\n\t// to escape analysis: if we hit the \"load\" path or the entry is expunged, we\n\t// shouldn'V bother heap-allocating.\n\tic := i\n\tfor {\n\t\tif atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {\n\t\t\treturn i, false, true\n\t\t}\n\t\tp = atomic.LoadPointer(&e.p)\n\t\tif p == expunged {\n\t\t\treturn actual, false, false\n\t\t}\n\t\tif p != nil {\n\t\t\treturn *(*V)(p), true, true\n\t\t}\n\t}\n}\n\n// Delete deletes the value for a key.\nfunc (m *MapOf[K, V]) Delete(key K) {\n\tread, _ := m.read.Load().(readOnly[K, V])\n\te, ok := read.m[key]\n\tif !ok && read.amended {\n\t\tm.mu.Lock()\n\t\tread, _ = m.read.Load().(readOnly[K, V])\n\t\te, ok = read.m[key]\n\t\tif !ok && read.amended {\n\t\t\tdelete(m.dirty, key)\n\t\t}\n\t\tm.mu.Unlock()\n\t}\n\tif ok {\n\t\te.delete()\n\t}\n}\n\nfunc (e *entry[V]) delete() (hadValue bool) {\n\tfor {\n\t\tp := atomic.LoadPointer(&e.p)\n\t\tif p == nil || p == expunged {\n\t\t\treturn false\n\t\t}\n\t\tif atomic.CompareAndSwapPointer(&e.p, p, nil) {\n\t\t\treturn true\n\t\t}\n\t}\n}\n\n// Range calls f sequentially for each key and value present in the map.\n// If f returns false, range stops the iteration.\n//\n// Range does not necessarily correspond to any consistent snapshot of the MapOf's\n// contents: no key will be visited more than once, but if the value for any key\n// is stored or deleted concurrently, Range may reflect any mapping for that key\n// from any point during the Range call.\n//\n// Range may be O(N) with the number of elements in the map even if f returns\n// false after a constant number of calls.\nfunc (m *MapOf[K, V]) Range(f func(key K, value V) bool) {\n\t// We need to be able to iterate over all of the keys that were already\n\t// present at the start of the call to Range.\n\t// If read.amended is false, then read.m satisfies that property without\n\t// requiring us to hold m.mu for a long time.\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tif read.amended {\n\t\t// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)\n\t\t// (assuming the caller does not break out early), so a call to Range\n\t\t// amortizes an entire copy of the map: we can promote the dirty copy\n\t\t// immediately!\n\t\tm.mu.Lock()\n\t\tread, _ = m.read.Load().(readOnly[K, V])\n\t\tif read.amended {\n\t\t\tread = readOnly[K, V]{m: m.dirty}\n\t\t\tm.read.Store(read)\n\t\t\tm.dirty = nil\n\t\t\tm.misses = 0\n\t\t}\n\t\tm.mu.Unlock()\n\t}\n\n\tfor k, e := range read.m {\n\t\tv, ok := e.load()\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif !f(k, v) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (m *MapOf[K, V]) missLocked() {\n\tm.misses++\n\tif m.misses < len(m.dirty) {\n\t\treturn\n\t}\n\tm.read.Store(readOnly[K, V]{m: m.dirty})\n\tm.dirty = nil\n\tm.misses = 0\n}\n\nfunc (m *MapOf[K, V]) dirtyLocked() {\n\tif m.dirty != nil {\n\t\treturn\n\t}\n\n\tread, _ := m.read.Load().(readOnly[K, V])\n\tm.dirty = make(map[K]*entry[V], len(read.m))\n\tfor k, e := range read.m {\n\t\tif !e.tryExpungeLocked() {\n\t\t\tm.dirty[k] = e\n\t\t}\n\t}\n}\n\nfunc (e *entry[V]) tryExpungeLocked() (isExpunged bool) {\n\tp := atomic.LoadPointer(&e.p)\n\tfor p == nil {\n\t\tif atomic.CompareAndSwapPointer(&e.p, nil, expunged) {\n\t\t\treturn true\n\t\t}\n\t\tp = atomic.LoadPointer(&e.p)\n\t}\n\treturn p == expunged\n}\n"
  },
  {
    "path": "internal/utils/generic/queue.go",
    "content": "package generic\n\ntype Integer interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64 |\n\t\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64\n}\n\ntype Queue[T Integer] struct {\n\tData []T\n\tPtr  int\n\tFull bool\n}\n\nfunc NewQueue[T Integer](capacity int) *Queue[T] {\n\tif capacity <= 0 {\n\t\tpanic(\"queue capacity must be positive\")\n\t}\n\treturn &Queue[T]{\n\t\tData: make([]T, 0, capacity),\n\t\tPtr:  0,\n\t\tFull: false,\n\t}\n}\n\nfunc (q *Queue[T]) Update(value T) {\n\tif q.Full {\n\t\tq.Data[q.Ptr] = value\n\t\tq.Ptr = (q.Ptr + 1) % len(q.Data)\n\t} else {\n\t\tq.Data = append(q.Data, value)\n\t\tif len(q.Data) == cap(q.Data) {\n\t\t\tq.Full = true\n\t\t}\n\t}\n}\n\nfunc (q *Queue[T]) GetAll() []T {\n\tif len(q.Data) == 0 {\n\t\treturn nil\n\t}\n\n\tresult := make([]T, len(q.Data))\n\n\tif q.Full {\n\t\ttailLen := len(q.Data) - q.Ptr\n\t\tcopy(result, q.Data[q.Ptr:])\n\t\tcopy(result[tailLen:], q.Data[:q.Ptr])\n\t} else {\n\t\tcopy(result, q.Data)\n\t}\n\n\treturn result\n}\n\nfunc (q *Queue[T]) Clear() {\n\tq.Data = q.Data[:0]\n\tq.Ptr = 0\n\tq.Full = false\n}\n\nfunc (q *Queue[T]) Average() T {\n\tif len(q.Data) == 0 {\n\t\treturn 0\n\t}\n\n\tvar sum int64\n\tfor _, value := range q.Data {\n\t\tsum += int64(value)\n\t}\n\n\treturn T(sum / int64(len(q.Data)))\n}\n"
  },
  {
    "path": "internal/utils/info/info.go",
    "content": "package info\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils/color\"\n)\n\nvar (\n\tVersion   = \"dev\"\n\tCommit    = \"unknown\"\n\tBuildTime = \"unknown\"\n\tAuthor    = \"bestrui\"\n\tRepo      = \"https://github.com/bestruirui/bestsub\"\n)\n\nfunc Banner() {\n\tlogo := `\n  ██████╗ ███████╗███████╗████████╗███████╗██╗   ██╗██████╗ \n  ██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔════╝██║   ██║██╔══██╗\n  ██████╔╝█████╗  ███████╗   ██║   ███████╗██║   ██║██████╔╝\n  ██╔══██╗██╔══╝  ╚════██║   ██║   ╚════██║██║   ██║██╔══██╗\n  ██████╔╝███████╗███████║   ██║   ███████║╚██████╔╝██████╔╝\n  ╚═════╝ ╚══════╝╚══════╝   ╚═╝   ╚══════╝ ╚═════╝ ╚═════╝ \n`\n\n\tfmt.Print(color.Cyan + color.Bold)\n\tfmt.Println(logo)\n\tfmt.Print(color.Reset)\n\n\tfmt.Print(color.Blue + color.Bold)\n\tfmt.Println(\"          🚀 BestSub - Best Sub For You\")\n\tfmt.Print(color.Reset)\n\n\tfmt.Print(color.Dim)\n\tfmt.Println(\"  \" + strings.Repeat(\"─\", 60))\n\tfmt.Print(color.Reset)\n\n\tprintInfo(\"Version\", Version, color.Green)\n\tprintInfo(\"Commit\", Commit[:min(8, len(Commit))], color.Yellow)\n\tprintInfo(\"Build Time\", formatDate(BuildTime), color.Blue)\n\tprintInfo(\"Built By\", Author, color.Purple)\n\tprintInfo(\"Repo\", Repo, color.Cyan)\n\n\tfmt.Print(color.Dim)\n\tfmt.Println(\"  \" + strings.Repeat(\"═\", 60))\n\tfmt.Print(color.Reset)\n}\n\nfunc printInfo(label, value, print_color string) {\n\tfmt.Printf(\"  %s%-12s%s %s%s%s\\n\",\n\t\tcolor.Dim, label+\":\", color.Reset,\n\t\tprint_color, value, color.Reset)\n}\n\nfunc formatDate(date string) string {\n\tif date == \"unknown\" || date == \"\" {\n\t\treturn \"unknown\"\n\t}\n\n\tlayouts := []string{\n\t\t\"2006-01-02T15:04:05Z\",\n\t\t\"2006-01-02 15:04:05\",\n\t\t\"2006-01-02\",\n\t\ttime.RFC3339,\n\t}\n\n\tfor _, layout := range layouts {\n\t\tif t, err := time.Parse(layout, date); err == nil {\n\t\t\treturn t.Format(\"2006-01-02 15:04\")\n\t\t}\n\t}\n\n\treturn date\n}\n\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n"
  },
  {
    "path": "internal/utils/log/log.go",
    "content": "package log\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils\"\n\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\ntype LogEntry struct {\n\tLevel   string `json:\"level\"`\n\tMessage string `json:\"message\"`\n\tName    string `json:\"-\"`\n}\n\nvar (\n\twsChannel chan LogEntry\n\n\tbasePath string = \"build\"\n\n\tuseConsole bool\n\n\tuseFile bool\n\n\tlogger *Logger\n\n\tseparator = []byte(\",\")\n)\nvar consoleEncoder = zapcore.EncoderConfig{\n\tTimeKey:       \"time\",\n\tLevelKey:      \"level\",\n\tMessageKey:    \"msg\",\n\tCallerKey:     \"caller\",\n\tStacktraceKey: \"stacktrace\",\n\tEncodeLevel:   zapcore.CapitalColorLevelEncoder,\n\tEncodeTime:    zapcore.RFC3339TimeEncoder,\n\tEncodeCaller:  zapcore.ShortCallerEncoder,\n}\nvar fileEncoder = zapcore.EncoderConfig{\n\tTimeKey:     \"time\",\n\tLevelKey:    \"level\",\n\tMessageKey:  \"msg\",\n\tEncodeLevel: zapcore.LowercaseLevelEncoder,\n\tEncodeTime:  zapcore.RFC3339TimeEncoder,\n}\n\ntype Logger struct {\n\t*zap.SugaredLogger\n\tbufferedWriter *zapcore.BufferedWriteSyncer\n}\ntype Config struct {\n\tLevel      string\n\tPath       string\n\tUseConsole bool\n\tUseFile    bool\n\tName       string\n\tCallerSkip int\n}\n\nfunc webSocketHook(entry zapcore.Entry) error {\n\tif wsChannel == nil {\n\t\treturn nil\n\t}\n\n\tlogEntry := LogEntry{\n\t\tLevel:   entry.Level.String(),\n\t\tMessage: entry.Message,\n\t\tName:    entry.LoggerName,\n\t}\n\n\tselect {\n\tcase wsChannel <- logEntry:\n\tdefault:\n\t}\n\n\treturn nil\n}\n\nfunc init() {\n\twsChannel = make(chan LogEntry, 1000)\n\n\tlogger, _ = NewLogger(Config{\n\t\tLevel:      \"debug\",\n\t\tUseConsole: true,\n\t\tCallerSkip: 1,\n\t\tUseFile:    false,\n\t\tName:       \"main\",\n\t})\n}\n\nfunc Initialize(level, path, method string) error {\n\tlogger.Close()\n\n\tbasePath = path\n\n\tif _, err := os.Stat(basePath); os.IsNotExist(err) {\n\t\tif err := os.MkdirAll(basePath, 0755); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create log directory: %w\", err)\n\t\t}\n\t}\n\n\tmainPath := filepath.Join(basePath, \"main\", time.Now().Format(\"20060102150405\")+\".log\")\n\n\tswitch method {\n\tcase \"console\":\n\t\tuseConsole = true\n\t\tuseFile = false\n\tcase \"file\":\n\t\tuseConsole = false\n\t\tuseFile = true\n\tcase \"both\":\n\t\tuseConsole = true\n\t\tuseFile = true\n\tdefault:\n\t\tuseConsole = true\n\t\tuseFile = false\n\t}\n\n\tvar err error\n\tlogger, err = NewLogger(Config{\n\t\tLevel:      level,\n\t\tPath:       mainPath,\n\t\tUseConsole: useConsole,\n\t\tUseFile:    useFile,\n\t\tName:       \"main\",\n\t\tCallerSkip: 1,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\nfunc GetDefaultLogger() *Logger {\n\treturn logger\n}\nfunc NewTaskLogger(name string, taskid uint16, level string, writeFile bool) (*Logger, error) {\n\ttaskidstr := strconv.FormatUint(uint64(taskid), 10)\n\tloggerName := \"task_\" + name + \"_\" + taskidstr\n\tpath := filepath.Join(basePath, name, taskidstr, time.Now().Format(\"20060102150405\")+\".log\")\n\treturn NewLogger(Config{\n\t\tLevel:      level,\n\t\tPath:       path,\n\t\tUseConsole: utils.IsDebug(),\n\t\tUseFile:    writeFile,\n\t\tName:       loggerName,\n\t\tCallerSkip: 1,\n\t})\n}\n\nfunc GetWSChannel() <-chan LogEntry {\n\treturn wsChannel\n}\n\nfunc NewLogger(config Config) (*Logger, error) {\n\tparsedLevel, err := zapcore.ParseLevel(config.Level)\n\tif err != nil {\n\t\tparsedLevel = zapcore.InfoLevel\n\t}\n\n\tvar cores []zapcore.Core\n\tvar bufferedWriter *zapcore.BufferedWriteSyncer\n\n\tif config.UseConsole {\n\t\tconsoleCore := zapcore.NewCore(\n\t\t\tzapcore.NewConsoleEncoder(consoleEncoder),\n\t\t\tzapcore.AddSync(os.Stdout),\n\t\t\tparsedLevel,\n\t\t)\n\t\tcores = append(cores, consoleCore)\n\t}\n\n\tif config.UseFile && config.Path != \"\" {\n\t\tfile, err := createLogFile(config.Path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbufferedWriter = &zapcore.BufferedWriteSyncer{\n\t\t\tWS: zapcore.AddSync(file),\n\t\t}\n\t\tfileCore := zapcore.NewCore(\n\t\t\tzapcore.NewJSONEncoder(fileEncoder),\n\t\t\tbufferedWriter,\n\t\t\tparsedLevel,\n\t\t)\n\t\tcores = append(cores, fileCore)\n\t}\n\n\twsEncoderConfig := zapcore.EncoderConfig{\n\t\tLevelKey:    \"level\",\n\t\tMessageKey:  \"msg\",\n\t\tEncodeLevel: zapcore.LowercaseLevelEncoder,\n\t}\n\n\twsCore := zapcore.NewCore(\n\t\tzapcore.NewJSONEncoder(wsEncoderConfig),\n\t\tzapcore.AddSync(io.Discard),\n\t\tzapcore.DebugLevel,\n\t)\n\tcores = append(cores, wsCore)\n\n\tcore := zapcore.NewTee(cores...)\n\tlogger := zap.New(\n\t\tcore,\n\t\tzap.Hooks(webSocketHook),\n\t\tzap.AddStacktrace(zapcore.ErrorLevel),\n\t\tzap.AddCallerSkip(config.CallerSkip),\n\t\tzap.AddCaller(),\n\t)\n\tlogger.Named(config.Name)\n\n\treturn &Logger{\n\t\tSugaredLogger:  logger.Sugar(),\n\t\tbufferedWriter: bufferedWriter,\n\t}, nil\n}\n\nfunc createLogFile(path string) (*os.File, error) {\n\tif err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create log directory: %w\", err)\n\t}\n\n\tfile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open log file: %w\", err)\n\t}\n\n\treturn file, nil\n}\n\nfunc (l *Logger) Close() error {\n\tl.SugaredLogger.Sync()\n\n\tif l.bufferedWriter != nil {\n\t\tif err := l.bufferedWriter.Sync(); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed to flush buffered writer: %v\\n\", err)\n\t\t}\n\t\tl.bufferedWriter = nil\n\t}\n\n\treturn nil\n}\n\nfunc Debug(args ...interface{}) {\n\tlogger.Debug(args...)\n}\nfunc Info(args ...interface{}) {\n\tlogger.Info(args...)\n}\nfunc Warn(args ...interface{}) {\n\tlogger.Warn(args...)\n}\nfunc Error(args ...interface{}) {\n\tlogger.Error(args...)\n}\nfunc Fatal(args ...interface{}) {\n\tlogger.Fatal(args...)\n}\n\nfunc Debugf(template string, args ...interface{}) {\n\tlogger.Debugf(template, args...)\n}\n\nfunc Infof(template string, args ...interface{}) {\n\tlogger.Infof(template, args...)\n}\n\nfunc Warnf(template string, args ...interface{}) {\n\tlogger.Warnf(template, args...)\n}\n\nfunc Errorf(template string, args ...interface{}) {\n\tlogger.Errorf(template, args...)\n}\n\nfunc Fatalf(template string, args ...interface{}) {\n\tlogger.Fatalf(template, args...)\n}\nfunc Close() error {\n\treturn logger.Close()\n}\n\nfunc CleanupOldLogs(retentionDays int) error {\n\tif retentionDays <= 0 {\n\t\treturn fmt.Errorf(\"retention days must be greater than 0\")\n\t}\n\n\tcutoffTime := time.Now().AddDate(0, 0, -retentionDays)\n\n\treturn filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\tWarnf(\"Error accessing path %s: %v\", path, err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif !strings.HasSuffix(info.Name(), \".log\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tif info.ModTime().Before(cutoffTime) {\n\t\t\tif err := os.Remove(path); err != nil {\n\t\t\t\tWarnf(\"Failed to remove old log file %s: %v\", path, err)\n\t\t\t} else {\n\t\t\t\tInfof(\"Removed old log file: %s\", path)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc GetLogFileList(path string) ([]uint64, error) {\n\tvar timestamps []uint64\n\n\tfullPath := filepath.Join(basePath, path)\n\n\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\treturn timestamps, nil\n\t}\n\n\tentries, err := os.ReadDir(fullPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read directory %s: %w\", fullPath, err)\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tfilename := entry.Name()\n\t\tif !strings.HasSuffix(filename, \".log\") {\n\t\t\tcontinue\n\t\t}\n\n\t\ttimeStr := strings.TrimSuffix(filename, \".log\")\n\n\t\tif len(timeStr) != 14 {\n\t\t\tcontinue\n\t\t}\n\n\t\ttimestamp, err := strconv.ParseUint(timeStr, 10, 64)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := time.Parse(\"20060102150405\", timeStr); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\ttimestamps = append(timestamps, timestamp)\n\t}\n\n\tsort.Slice(timestamps, func(i, j int) bool {\n\t\treturn timestamps[i] > timestamps[j]\n\t})\n\treturn timestamps, nil\n}\nfunc StreamLogToHTTP(path string, timestamp uint64, writer io.Writer) error {\n\ttimeStr := strconv.FormatUint(timestamp, 10)\n\tif len(timeStr) != 14 {\n\t\treturn fmt.Errorf(\"invalid timestamp format: %d\", timestamp)\n\t}\n\n\tif _, err := time.Parse(\"20060102150405\", timeStr); err != nil {\n\t\treturn fmt.Errorf(\"invalid timestamp: %d\", timestamp)\n\t}\n\n\tfilename := timeStr + \".log\"\n\tfullPath := filepath.Join(basePath, path, filename)\n\n\tfile, err := os.Open(fullPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"log file not found: %s\", filename)\n\t\t}\n\t\treturn fmt.Errorf(\"failed to open file: %w\", err)\n\t}\n\tdefer file.Close()\n\n\tscanner := bufio.NewScanner(file)\n\tscanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)\n\n\tflusher, canFlush := writer.(http.Flusher)\n\n\tisFirstLine := true\n\n\tfor scanner.Scan() {\n\t\tlineBytes := scanner.Bytes()\n\t\tif len(lineBytes) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !isFirstLine {\n\t\t\tif _, err := writer.Write(separator); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to write separator: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif _, err := writer.Write(lineBytes); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write log line: %w\", err)\n\t\t}\n\n\t\tif canFlush {\n\t\t\tflusher.Flush()\n\t\t}\n\n\t\tisFirstLine = false\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn fmt.Errorf(\"error reading file: %w\", err)\n\t}\n\n\treturn nil\n}\nfunc DeleteLog(path string) error {\n\tif path == \"\" {\n\t\treturn fmt.Errorf(\"path cannot be empty\")\n\t}\n\n\tfullPath := filepath.Join(basePath, path)\n\n\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\treturn nil\n\t}\n\n\tif err := os.RemoveAll(fullPath); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove directory %s: %w\", path, err)\n\t}\n\n\tDebugf(\"Successfully removed log dir: %s\", path)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/utils/shutdown/shutdown.go",
    "content": "package shutdown\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/bestruirui/bestsub/internal/utils/log\"\n)\n\nvar funcs []func() error\n\nfunc Register(fn func() error) {\n\tfuncs = append(funcs, fn)\n}\n\nfunc Listen() {\n\tquit := make(chan os.Signal, 1)\n\tsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)\n\tlog.Info(\"Program started, press Ctrl+C to exit\")\n\tsig := <-quit\n\tlog.Warnf(\"Received exit signal: %v, starting to close program\", sig)\n\tif len(funcs) == 0 {\n\t\treturn\n\t}\n\tfor _, fn := range funcs {\n\t\tif err := fn(); err != nil {\n\t\t\tlog.Errorf(\"Closing functions execution failed: %v\", err)\n\t\t}\n\t}\n\tlog.Info(\"=== Shutdown completed successfully ===\")\n\tos.Exit(0)\n}\nfunc All() {\n\tif len(funcs) == 0 {\n\t\treturn\n\t}\n\tfor _, fn := range funcs {\n\t\tif err := fn(); err != nil {\n\t\t\tlog.Errorf(\"Closing functions execution failed: %v\", err)\n\t\t}\n\t}\n\tlog.Info(\"Shutdown completed successfully\")\n}\n"
  },
  {
    "path": "internal/utils/ua/ua.go",
    "content": "package ua\n\nimport (\n\t\"math/rand\"\n\t\"net/http\"\n)\n\nfunc SetHeader(req *http.Request) {\n\treq.Header.Set(\"User-Agent\", Random())\n}\n\nfunc Random() string {\n\treturn \"Mozilla/5.0 (\" + platforms[rand.Intn(len(platforms))] + \") AppleWebKit/537.36 (KHTML, like Gecko) Chrome/\" + chromeVersions[rand.Intn(len(chromeVersions))] + \" Safari/537.36\"\n}\n\nvar (\n\tchromeVersions = []string{\n\t\t\"139.0.7258.128\",\n\t\t\"139.0.7258.67\",\n\t\t\"138.0.7204.185\",\n\t\t\"138.0.7204.170\",\n\t\t\"138.0.7204.159\",\n\t\t\"138.0.7204.102\",\n\t\t\"138.0.7204.100\",\n\t\t\"138.0.7204.51\",\n\t\t\"138.0.7204.49\",\n\t\t\"137.0.7151.122\",\n\t\t\"138.0.7204.35\",\n\t\t\"137.0.7151.121\",\n\t\t\"137.0.7151.105\",\n\t\t\"137.0.7151.104\",\n\t\t\"137.0.7151.57\",\n\t\t\"137.0.7151.55\",\n\t\t\"136.0.7103.116\",\n\t\t\"137.0.7151.40\",\n\t\t\"136.0.7103.113\",\n\t\t\"136.0.7103.92\",\n\t\t\"135.0.7049.117\",\n\t\t\"136.0.7103.48\",\n\t\t\"135.0.7049.114\",\n\t\t\"135.0.7049.86\",\n\t\t\"135.0.7049.42\",\n\t\t\"135.0.7049.41\",\n\t\t\"134.0.6998.167\",\n\t\t\"134.0.6998.119\",\n\t\t\"134.0.6998.117\",\n\t\t\"134.0.6998.37\",\n\t\t\"134.0.6998.35\",\n\t\t\"133.0.6943.128\",\n\t\t\"133.0.6943.100\",\n\t\t\"133.0.6943.59\",\n\t\t\"133.0.6943.53\",\n\t\t\"132.0.6834.162\",\n\t\t\"133.0.6943.35\",\n\t\t\"132.0.6834.160\",\n\t\t\"132.0.6834.112\",\n\t\t\"132.0.6834.110\",\n\t\t\"131.0.6778.267\",\n\t\t\"132.0.6834.83\",\n\t\t\"131.0.6778.264\",\n\t\t\"131.0.6778.204\",\n\t\t\"131.0.6778.139\",\n\t\t\"131.0.6778.109\",\n\t\t\"131.0.6778.71\",\n\t\t\"131.0.6778.69\",\n\t\t\"130.0.6723.119\",\n\t\t\"131.0.6778.33\",\n\t\t\"130.0.6723.116\",\n\t\t\"130.0.6723.71\",\n\t\t\"130.0.6723.60\",\n\t\t\"130.0.6723.58\",\n\t\t\"129.0.6668.103\",\n\t\t\"130.0.6723.44\",\n\t\t\"129.0.6668.100\",\n\t\t\"129.0.6668.72\",\n\t\t\"129.0.6668.60\",\n\t\t\"129.0.6668.42\",\n\t\t\"128.0.6613.122\",\n\t\t\"128.0.6613.121\",\n\t\t\"128.0.6613.115\",\n\t\t\"128.0.6613.113\",\n\t\t\"127.0.6533.122\",\n\t\t\"128.0.6613.36\",\n\t\t\"127.0.6533.119\",\n\t\t\"127.0.6533.100\",\n\t\t\"127.0.6533.74\",\n\t\t\"127.0.6533.72\",\n\t\t\"126.0.6478.185\",\n\t\t\"127.0.6533.57\",\n\t\t\"126.0.6478.183\",\n\t\t\"126.0.6478.128\",\n\t\t\"126.0.6478.116\",\n\t\t\"126.0.6478.114\",\n\t\t\"126.0.6478.61\",\n\t\t\"125.0.6422.176\",\n\t\t\"126.0.6478.56\",\n\t\t\"125.0.6422.144\",\n\t\t\"126.0.6478.36\",\n\t\t\"125.0.6422.142\",\n\t\t\"125.0.6422.114\",\n\t\t\"125.0.6422.77\",\n\t\t\"125.0.6422.76\",\n\t\t\"124.0.6367.210\",\n\t\t\"125.0.6422.60\",\n\t\t\"124.0.6367.208\",\n\t\t\"124.0.6367.201\",\n\t\t\"124.0.6367.156\",\n\t\t\"125.0.6422.41\",\n\t\t\"124.0.6367.155\",\n\t\t\"124.0.6367.119\",\n\t\t\"124.0.6367.92\",\n\t\t\"124.0.6367.63\",\n\t\t\"124.0.6367.61\",\n\t\t\"123.0.6312.124\",\n\t\t\"124.0.6367.60\",\n\t\t\"123.0.6312.122\",\n\t\t\"123.0.6312.106\",\n\t\t\"123.0.6312.105\",\n\t\t\"123.0.6312.60\",\n\t\t\"123.0.6312.58\",\n\t\t\"122.0.6261.131\",\n\t\t\"123.0.6312.46\",\n\t\t\"122.0.6261.129\",\n\t\t\"122.0.6261.128\",\n\t\t\"122.0.6261.112\",\n\t\t\"122.0.6261.111\",\n\t\t\"122.0.6261.71\",\n\t\t\"122.0.6261.69\",\n\t\t\"121.0.6167.189\",\n\t\t\"122.0.6261.57\",\n\t\t\"121.0.6167.187\",\n\t\t\"121.0.6167.186\",\n\t\t\"121.0.6167.162\",\n\t\t\"121.0.6167.160\",\n\t\t\"121.0.6167.140\",\n\t\t\"121.0.6167.86\",\n\t\t\"121.0.6167.85\",\n\t\t\"120.0.6099.227\",\n\t\t\"120.0.6099.225\",\n\t\t\"121.0.6167.75\",\n\t\t\"120.0.6099.224\",\n\t\t\"120.0.6099.218\",\n\t\t\"120.0.6099.216\",\n\t\t\"120.0.6099.200\",\n\t\t\"120.0.6099.199\",\n\t\t\"120.0.6099.129\",\n\t\t\"120.0.6099.110\",\n\t\t\"120.0.6099.109\",\n\t\t\"120.0.6099.62\",\n\t\t\"120.0.6099.56\",\n\t}\n\n\tplatforms = []string{\n\t\t\"Windows NT 10.0; Win64; x64\",\n\t\t\"Macintosh; Intel Mac OS X 10_15_7\",\n\t}\n)\n"
  },
  {
    "path": "internal/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\n// 检查目录是否可写\nfunc IsWritableDir(dir string) bool {\n\t// 尝试在目录中创建临时文件\n\ttestFile := filepath.Join(dir, \".write_test\")\n\tfile, err := os.Create(testFile)\n\tif err != nil {\n\t\treturn false\n\t}\n\tfile.Close()\n\tos.Remove(testFile)\n\treturn true\n}\n\n// 检查字符串切片是否包含指定字符串\nfunc Contains(slice []string, item string) bool {\n\tfor _, s := range slice {\n\t\tif s == item {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\nfunc RemoveAllControlCharacters(data *[]byte) {\n\tvar cleanedData []byte\n\toriginal := *data\n\tfor len(original) > 0 {\n\t\tr, size := utf8.DecodeRune(original)\n\t\tif r != utf8.RuneError && (r >= 32 && r <= 126) || r == '\\n' || r == '\\t' || r == '\\r' || unicode.Is(unicode.Han, r) {\n\t\t\tcleanedData = append(cleanedData, original[:size]...)\n\t\t}\n\t\toriginal = original[size:]\n\t}\n\t*data = cleanedData\n}\nfunc IsDebug() bool {\n\tdebug := os.Getenv(\"BESTSUB_DEBUG\")\n\treturn strings.ToLower(debug) == \"true\"\n}\n\n// IPToUint32 将IP地址转换为uint32\nfunc IPToUint32(ip string) uint32 {\n\tip = strings.TrimSpace(ip)\n\tif ip == \"\" {\n\t\treturn 0\n\t}\n\n\tparts := strings.Split(ip, \".\")\n\tif len(parts) != 4 {\n\t\treturn 0\n\t}\n\n\tvar result uint32\n\tfor i, part := range parts {\n\t\tpartInt, err := strconv.Atoi(part)\n\t\tif err != nil || partInt < 0 || partInt > 255 {\n\t\t\treturn 0\n\t\t}\n\t\tresult |= uint32(partInt) << ((3 - i) * 8)\n\t}\n\treturn result\n}\n\n// Uint32ToIP 将uint32转换为IP地址\nfunc Uint32ToIP(ip uint32) string {\n\treturn fmt.Sprintf(\"%d.%d.%d.%d\",\n\t\t(ip>>24)&0xFF,\n\t\t(ip>>16)&0xFF,\n\t\t(ip>>8)&0xFF,\n\t\tip&0xFF)\n}\n"
  },
  {
    "path": "scripts/build.sh",
    "content": "#!/bin/bash\n\n# Exit on any error, but handle errors gracefully\nset -e\n\n# Enable error trapping\ntrap 'handle_error $? $LINENO' ERR\n\n# =============================================================================\n# Configuration\n# =============================================================================\n\n# Project configuration\nreadonly APP_NAME=\"bestsub\"\nreadonly MAIN_DIR=\"./cmd/bestsub\"\nreadonly OUTPUT_DIR=\"build\"\nreadonly TOOLCHAIN_DIR=\"$HOME/.bestsub/toolchains\"\n\n# Build metadata\nreadonly BUILD_TIME=\"$(TZ='Asia/Shanghai' date +'%F %T %z')\"\nreadonly GIT_AUTHOR=\"bestrui\"\nreadonly GIT_VERSION=\"$(git describe --tags --abbrev=0 2>/dev/null || echo 'dev')\"\nreadonly COMMIT_ID=\"$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')\"\n\n# Build flags\nreadonly LDFLAGS=\"-X 'github.com/bestruirui/bestsub/internal/utils/info.Version=${GIT_VERSION}' \\\n                  -X 'github.com/bestruirui/bestsub/internal/utils/info.BuildTime=${BUILD_TIME}' \\\n                  -X 'github.com/bestruirui/bestsub/internal/utils/info.Author=${GIT_AUTHOR}' \\\n                  -X 'github.com/bestruirui/bestsub/internal/utils/info.Commit=${COMMIT_ID}' \\\n                  -s -w\"\n\n# Android NDK configuration\nreadonly ANDROID_NDK_VERSION=\"r27c\"\nreadonly ANDROID_NDK_BASE=\"https://dl.google.com/android/repository/\"\nreadonly ANDROID_NDK_PATH=\"${TOOLCHAIN_DIR}/android-ndk/android-ndk-${ANDROID_NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin\"\n\n# =============================================================================\n# Utility Functions\n# =============================================================================\n\nlog_info() {\n    echo \"ℹ️  $1\"\n}\n\nlog_success() {\n    echo \"✅ $1\"\n}\n\nlog_error() {\n    echo \"❌ $1\" >&2\n}\n\nlog_warning() {\n    echo \"⚠️  $1\" >&2\n}\n\nlog_step() {\n    echo \"\"\n    echo \"🔧 $1\"\n    echo \"────────────────────────────────────────\"\n}\n\n# Error handling function\nhandle_error() {\n    local exit_code=$1\n    local line_number=$2\n    log_error \"Build failed at line ${line_number} with exit code ${exit_code}\"\n    log_error \"Command that failed: $(sed -n \"${line_number}p\" \"$0\" | xargs)\"\n    log_error \"Check the output above for more details\"\n    exit $exit_code\n}\n\n# Check if command exists\ncommand_exists() {\n    command -v \"$1\" >/dev/null 2>&1\n}\n\n# Install command if not exists (Linux/macOS)\ninstall_command() {\n    local cmd=\"$1\"\n    local package=\"$2\"\n\n    if command_exists \"$cmd\"; then\n        log_info \"$cmd is already installed\"\n        return 0\n    fi\n\n    log_info \"Installing $cmd...\"\n\n    # Detect OS and package manager\n    if [[ \"$OSTYPE\" == \"linux-gnu\"* ]]; then\n        if command_exists apt-get; then\n            sudo apt-get update >/dev/null 2>&1 || {\n                log_error \"Failed to update package list\"\n                return 1\n            }\n            sudo apt-get install -y \"$package\" >/dev/null 2>&1 || {\n                log_error \"Failed to install $package using apt-get\"\n                return 1\n            }\n        elif command_exists yum; then\n            sudo yum install -y \"$package\" >/dev/null 2>&1 || {\n                log_error \"Failed to install $package using yum\"\n                return 1\n            }\n        elif command_exists dnf; then\n            sudo dnf install -y \"$package\" >/dev/null 2>&1 || {\n                log_error \"Failed to install $package using dnf\"\n                return 1\n            }\n        elif command_exists pacman; then\n            sudo pacman -S --noconfirm \"$package\" >/dev/null 2>&1 || {\n                log_error \"Failed to install $package using pacman\"\n                return 1\n            }\n        else\n            log_error \"No supported package manager found. Please install $cmd manually\"\n            return 1\n        fi\n    elif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n        if command_exists brew; then\n            brew install \"$package\" >/dev/null 2>&1 || {\n                log_error \"Failed to install $package using brew\"\n                return 1\n            }\n        else\n            log_error \"Homebrew not found. Please install $cmd manually or install Homebrew first\"\n            return 1\n        fi\n    else\n        log_error \"Unsupported OS: $OSTYPE. Please install $cmd manually\"\n        return 1\n    fi\n\n    log_success \"$cmd installed successfully\"\n}\n\n# =============================================================================\n# Setup Functions\n# =============================================================================\n\nprepare_environment() {\n    log_step \"Preparing build environment\"\n\n    # Check and install required commands\n    log_info \"Checking required commands...\"\n\n    # Check Go\n    if ! command_exists go; then\n        log_error \"Go is not installed. Please install Go from https://golang.org/dl/\"\n        return 1\n    fi\n\n    local go_version=$(go version 2>/dev/null | grep -o 'go[0-9]\\+\\.[0-9]\\+' | head -1)\n    log_success \"Go version: $go_version\"\n\n    # Check git\n    if ! command_exists git; then\n        install_command git git || return 1\n    fi\n\n    # Check curl\n    if ! command_exists curl; then\n        install_command curl curl || return 1\n    fi\n\n    # Check unzip\n    if ! command_exists unzip; then\n        install_command unzip unzip || return 1\n    fi\n\n    # Check tar\n    if ! command_exists tar; then\n        install_command tar tar || return 1\n    fi\n\n    # Check zip\n    if ! command_exists zip; then\n        install_command zip zip || return 1\n    fi\n\n    # Check md5sum (or md5 on macOS)\n    if ! command_exists md5sum && ! command_exists md5; then\n        if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n            log_warning \"md5sum not found, but md5 is available on macOS\"\n        else\n            install_command md5sum coreutils || return 1\n        fi\n    fi\n\n    log_success \"All required commands installed\"\n\n    # Create output directory and subdirectories\n    log_info \"Creating output directory structure: ${OUTPUT_DIR}\"\n\n    # Check if OUTPUT_DIR exists (including symlinks)\n    if [ -e \"${OUTPUT_DIR}\" ]; then\n        if [ -d \"${OUTPUT_DIR}\" ]; then\n            log_success \"Output directory already exists: ${OUTPUT_DIR}\"\n        else\n            log_error \"Output path exists but is not a directory: ${OUTPUT_DIR}\"\n            log_error \"Path type: $(ls -la \"${OUTPUT_DIR}\" 2>/dev/null || echo 'Cannot determine type')\"\n            return 1\n        fi\n    else\n        # Try to create the directory\n        if ! mkdir -p \"${OUTPUT_DIR}\"; then\n            log_error \"Failed to create output directory: ${OUTPUT_DIR}\"\n            log_error \"Current working directory: $(pwd)\"\n            log_error \"Directory permissions: $(ls -la . 2>/dev/null || echo 'Cannot list directory')\"\n            return 1\n        fi\n        log_success \"Created output directory: ${OUTPUT_DIR}\"\n    fi\n\n    # Create subdirectories for organized output\n    local subdirs=(\"bin\" \"docker\" \"archives\")\n    for subdir in \"${subdirs[@]}\"; do\n        if ! mkdir -p \"${OUTPUT_DIR}/${subdir}\"; then\n            log_error \"Failed to create subdirectory: ${OUTPUT_DIR}/${subdir}\"\n            return 1\n        fi\n    done\n    log_success \"Created output subdirectories: bin, docker, archives\"\n\n    log_info \"Tidying Go modules...\"\n    if ! go mod tidy >/dev/null 2>&1; then\n        log_error \"Failed to tidy Go modules\"\n        return 1\n    fi\n\n    log_success \"Build environment ready\"\n}\n\n# =============================================================================\n# Build Functions\n# =============================================================================\n\nbuild_frontend() {\n    log_step \"Building frontend\"\n\n    local web_dir=\"web\"\n\n    # Check if web directory exists\n    if [ ! -d \"$web_dir\" ]; then\n        log_error \"Web directory not found: $web_dir\"\n        log_error \"Please run this script from the project root directory\"\n        return 1\n    fi\n\n    # Change to web directory\n    cd \"$web_dir\" || return 1\n\n    # Install dependencies\n    log_info \"Installing frontend dependencies...\"\n    if ! pnpm install; then\n        log_error \"Failed to install frontend dependencies\"\n        cd ..\n        return 1\n    fi\n    log_success \"Frontend dependencies installed\"\n\n    # Build the project\n    log_info \"Building frontend project...\"\n    if ! NEXT_PUBLIC_APP_VERSION=\"$GIT_VERSION\" pnpm run build; then\n        log_error \"Failed to build frontend project\"\n        cd ..\n        return 1\n    fi\n    log_success \"Frontend build completed\"\n\n    # Return to original directory\n    cd ..\n\n    # Move out directory to static directory\n    log_info \"Moving frontend output to static directory...\"\n    \n    # Remove old static/out if exists\n    if [ -d \"static/out\" ]; then\n        rm -rf \"static/out\"\n        log_info \"Removed old static/out directory\"\n    fi\n    \n    # Move web/out to static/out\n    if [ -d \"${web_dir}/out\" ]; then\n        mv \"${web_dir}/out\" \"static/\"\n        log_success \"Moved frontend output to static/out\"\n    else\n        log_error \"Frontend output directory not found: ${web_dir}/out\"\n        return 1\n    fi\n\n    return 0\n}\nsetup_android_ndk() {\n    log_step \"Setting up Android NDK\"\n\n    if [ -d \"${TOOLCHAIN_DIR}/android-ndk\" ]; then\n        log_success \"Android NDK ${ANDROID_NDK_VERSION} already installed\"\n        return 0\n    fi\n\n    local ndk_zip=\"/tmp/android-ndk-${ANDROID_NDK_VERSION}.zip\"\n    local ndk_url=\"${ANDROID_NDK_BASE}android-ndk-${ANDROID_NDK_VERSION}-linux.zip\"\n\n    log_info \"Downloading Android NDK ${ANDROID_NDK_VERSION}...\"\n    if ! curl -L -o \"${ndk_zip}\" \"${ndk_url}\" >/dev/null 2>&1; then\n        log_error \"Failed to download Android NDK from ${ndk_url}\"\n        return 1\n    fi\n\n    log_info \"Extracting Android NDK...\"\n    if ! mkdir -p \"${TOOLCHAIN_DIR}/android-ndk\"; then\n        log_error \"Failed to create NDK directory: ${TOOLCHAIN_DIR}/android-ndk\"\n        log_error \"Toolchain directory: ${TOOLCHAIN_DIR}\"\n        log_error \"Home directory permissions: $(ls -la \"$HOME\" 2>/dev/null | head -5 || echo 'Cannot list home directory')\"\n        return 1\n    fi\n\n    if ! unzip -q \"${ndk_zip}\" -d \"${TOOLCHAIN_DIR}/android-ndk\" 2>/dev/null; then\n        log_error \"Failed to extract Android NDK\"\n        rm -f \"${ndk_zip}\"\n        return 1\n    fi\n\n    rm -f \"${ndk_zip}\"\n\n    log_success \"Android NDK ${ANDROID_NDK_VERSION} installed\"\n}\n\n# =============================================================================\n# Build Functions\n# =============================================================================\n\nget_go_arch() {\n    case \"$1\" in\n    \"x86_64\") echo \"amd64\" ;;\n    \"arm64\") echo \"arm64\" ;;\n    \"x86\") echo \"386\" ;;\n    \"armv7\") echo \"arm\" ;;\n    *)\n        log_error \"Unsupported architecture: $1\"\n        return 1\n        ;;\n    esac\n}\n\nbuild_standard() {\n    local os=\"$1\"\n    local arch=\"$2\"\n    local go_arch\n\n    if ! go_arch=\"$(get_go_arch \"${arch}\")\"; then\n        log_error \"Failed to get Go architecture: ${arch}\"\n        return 1\n    fi\n\n    local output_file=\"${OUTPUT_DIR}/bin/${APP_NAME}-${os}-${arch}\"\n\n    log_info \"Building ${os}/${arch}...\"\n\n    if ! GOOS=\"${os}\" GOARCH=\"${go_arch}\" CGO_ENABLED=0 \\\n        go build -o \"${output_file}\" -ldflags=\"${LDFLAGS}\" -tags=jsoniter \"${MAIN_DIR}\" 2>&1; then\n        log_error \"Failed to build ${os}/${arch}\"\n        log_error \"Build command: GOOS=${os} GOARCH=${go_arch} CGO_ENABLED=0 go build -o ${output_file} -ldflags=\\\"${LDFLAGS}\\\" -tags=jsoniter ${MAIN_DIR}\"\n        return 1\n    fi\n\n    if [ ! -f \"${output_file}\" ]; then\n        log_error \"Build completed but output file not found: ${output_file}\"\n        return 1\n    fi\n\n    log_success \"Built ${os}/${arch} → bin/$(basename \"${output_file}\")\"\n}\n\nget_android_compiler() {\n    case \"$1\" in\n    \"x86_64\") echo \"x86_64-linux-android24-clang\" ;;\n    \"arm64\") echo \"aarch64-linux-android24-clang\" ;;\n    \"armv7\") echo \"armv7a-linux-androideabi24-clang\" ;;\n    \"x86\") echo \"i686-linux-android24-clang\" ;;\n    *)\n        log_error \"Unsupported Android architecture: $1\"\n        return 1\n        ;;\n    esac\n}\n\nbuild_android() {\n    local arch=\"$1\"\n    local go_arch\n    local compiler\n\n    if ! go_arch=\"$(get_go_arch \"${arch}\")\"; then\n        log_error \"Failed to normalize architecture: ${arch}\"\n        return 1\n    fi\n\n    if ! compiler=\"$(get_android_compiler \"${arch}\")\"; then\n        log_error \"Failed to get Android compiler for architecture: ${arch}\"\n        return 1\n    fi\n\n    local compiler_path=\"${ANDROID_NDK_PATH}/${compiler}\"\n    if [ ! -f \"${compiler_path}\" ]; then\n        log_error \"Android compiler not found: ${compiler_path}\"\n        log_error \"Make sure Android NDK is properly installed\"\n        return 1\n    fi\n\n    local output_file=\"${OUTPUT_DIR}/bin/${APP_NAME}-android-${arch}\"\n\n    log_info \"Building android/${arch}...\"\n\n    if ! GOOS=android GOARCH=\"${go_arch}\" CC=\"${compiler_path}\" CGO_ENABLED=1 \\\n        go build -o \"${output_file}\" -ldflags=\"${LDFLAGS}\" -tags=jsoniter \"${MAIN_DIR}\" 2>&1; then\n        log_error \"Failed to build android/${arch}\"\n        log_error \"Build command: GOOS=android GOARCH=${go_arch} CC=${compiler_path} CGO_ENABLED=1 go build -o ${output_file} -ldflags=\\\"${LDFLAGS}\\\" -tags=jsoniter ${MAIN_DIR}\"\n        return 1\n    fi\n\n    if [ ! -f \"${output_file}\" ]; then\n        log_error \"Build completed but output file not found: ${output_file}\"\n        return 1\n    fi\n\n    # Strip binary to reduce size\n    local strip_tool=\"${ANDROID_NDK_PATH}/llvm-strip\"\n    if [ -f \"${strip_tool}\" ]; then\n        if ! \"${strip_tool}\" \"${output_file}\" 2>/dev/null; then\n            log_warning \"Failed to strip binary, but build was successful\"\n        fi\n    else\n        log_warning \"Strip tool not found: ${strip_tool}\"\n    fi\n\n    log_success \"Built android/${arch} → bin/$(basename \"${output_file}\")\"\n}\n\n# =============================================================================\n# Post-build Functions\n# =============================================================================\n\ncreate_archives() {\n    log_step \"Creating distribution archives\"\n\n    local archives_dir=\"${OUTPUT_DIR}/archives\"\n\n    # Copy documentation files to archives directory\n    cp README.md LICENSE \"${archives_dir}/\" 2>/dev/null || log_info \"Documentation files not found, skipping\"\n\n    # Archive all binaries (zip format for all platforms)\n    while IFS= read -r -d '' file; do\n        local basename_file\n        basename_file=$(basename \"$file\")\n        local extension=\"\"\n\n        # Add .exe extension for Windows binaries\n        if [[ \"$basename_file\" == *\"-windows-\"* ]]; then\n            extension=\".exe\"\n        fi\n\n        if ! cp \"$file\" \"${archives_dir}/${APP_NAME}${extension}\" 2>/dev/null; then\n            log_error \"Failed to copy $file to ${archives_dir}/${APP_NAME}${extension}\"\n            continue\n        fi\n\n        if (cd \"${archives_dir}\" && zip -q \"${basename_file}.zip\" \"${APP_NAME}${extension}\" README.md LICENSE 2>/dev/null); then\n            rm -f \"${archives_dir}/${APP_NAME}${extension}\"\n            log_success \"Archived: archives/${basename_file}.zip\"\n        else\n            log_error \"Failed to create archive: ${basename_file}.zip\"\n            rm -f \"${archives_dir}/${APP_NAME}${extension}\"\n        fi\n    done < <(find \"${OUTPUT_DIR}/bin/\" -name \"${APP_NAME}-*\" -type f -print0 2>/dev/null)\n\n    # Cleanup documentation files from archives directory\n    rm -f \"${archives_dir}/README.md\" \"${archives_dir}/LICENSE\"\n\n    if ! cd .. 2>/dev/null; then\n        log_error \"Failed to return to parent directory\"\n        return 1\n    fi\n\n    log_success \"Created archives in ${archives_dir}/\"\n}\n\ngenerate_checksums() {\n    log_step \"Generating checksums\"\n\n    local bin_dir=\"${OUTPUT_DIR}/bin\"\n\n    if ! cd \"${bin_dir}\" 2>/dev/null; then\n        log_error \"Failed to change to bin directory: ${bin_dir}\"\n        return 1\n    fi\n\n    if ! find . -maxdepth 1 -name \"${APP_NAME}-*\" -type f | head -1 | grep -q .; then\n        log_info \"No build artifacts found in bin directory, skipping checksums\"\n        cd ../.. 2>/dev/null || true\n        return 0\n    fi\n\n    # Use appropriate checksum command based on OS\n    local checksum_cmd\n    if command_exists md5sum; then\n        checksum_cmd=\"md5sum\"\n    elif command_exists md5; then\n        checksum_cmd=\"md5 -r\" # -r for BSD md5 to match md5sum format\n    else\n        log_error \"No checksum command available (md5sum or md5)\"\n        cd ../.. 2>/dev/null || true\n        return 1\n    fi\n\n    if find . -maxdepth 1 -name \"${APP_NAME}-*\" -type f -print0 | xargs -0 $checksum_cmd >md5.txt 2>/dev/null; then\n        local checksum_count=$(wc -l <md5.txt 2>/dev/null || echo \"0\")\n        log_success \"Generated checksums for ${checksum_count} files in bin/\"\n    else\n        log_error \"Failed to generate checksums\"\n        cd ../.. 2>/dev/null || true\n        return 1\n    fi\n\n    if ! cd ../.. 2>/dev/null; then\n        log_error \"Failed to return to parent directory\"\n        return 1\n    fi\n}\n\nprepare_docker_binaries() {\n    log_step \"Preparing Docker binaries\"\n\n    local docker_dir=\"${OUTPUT_DIR}/docker\"\n\n    # Create docker directory under OUTPUT_DIR\n    if ! mkdir -p \"${docker_dir}\"; then\n        log_error \"Failed to create docker directory: ${docker_dir}\"\n        log_error \"Current working directory: $(pwd)\"\n        log_error \"Directory permissions: $(ls -la . 2>/dev/null || echo 'Cannot list directory')\"\n        return 1\n    fi\n\n    local platforms=(\n        \"x86_64:linux/amd64\"\n        \"x86:linux/386\"\n        \"armv7:linux/arm/v7\"\n        \"arm64:linux/arm64\"\n    )\n\n    local copied_count=0\n\n    for platform in \"${platforms[@]}\"; do\n        local arch=\"${platform%%:*}\"\n        local docker_platform=\"${platform#*:}\"\n        local binary_name=\"${APP_NAME}-linux-${arch}\"\n        local platform_dir=\"${docker_dir}/${docker_platform}\"\n\n        if ! mkdir -p \"${platform_dir}\"; then\n            log_error \"Failed to create directory: ${platform_dir}\"\n            log_error \"Docker platform: ${docker_platform}\"\n            continue\n        fi\n\n        # Try to copy from binary file first\n        if [ -f \"${OUTPUT_DIR}/bin/${binary_name}\" ]; then\n            if cp \"${OUTPUT_DIR}/bin/${binary_name}\" \"${platform_dir}/${APP_NAME}\" 2>/dev/null; then\n                log_success \"Copied bin/${binary_name} → docker/${docker_platform}/${APP_NAME}\"\n                ((copied_count++))\n            else\n                log_error \"Failed to copy bin/${binary_name} to ${platform_dir}/${APP_NAME}\"\n            fi\n        else\n            log_warning \"Binary not found: bin/${binary_name}\"\n        fi\n    done\n\n    if [ $copied_count -gt 0 ]; then\n        log_success \"Prepared ${copied_count} Docker binaries in ${docker_dir}/\"\n    else\n        log_warning \"No Docker binaries prepared\"\n    fi\n}\n\n# =============================================================================\n# Main Execution\n# =============================================================================\n\nshow_usage() {\n    echo \"Usage: $0 <command> [os] [arch]\"\n    echo \"\"\n    echo \"Commands:\"\n    echo \"  release              Build all platforms and create distribution packages\"\n    echo \"  build <os> <arch>    Build for specific OS and architecture\"\n    echo \"  help                 Show this help message\"\n    echo \"\"\n    echo \"Supported OS:\"\n    echo \"  linux, windows, darwin, android\"\n    echo \"\"\n    echo \"Supported architectures:\"\n    echo \"  x86_64, arm64, armv7, x86\"\n    echo \"\"\n    echo \"Examples:\"\n    echo \"  $0 build windows x86_64\"\n    echo \"  $0 build linux x86_64\"\n    echo \"  $0 build android arm64\"\n    echo \"  $0 release\"\n}\n\nvalidate_os_arch() {\n    local os=\"$1\"\n    local arch=\"$2\"\n\n    # Validate OS\n    case \"$os\" in\n    \"linux\" | \"windows\" | \"darwin\" | \"android\") ;;\n    *)\n        log_error \"Unsupported OS: $os\"\n        log_error \"Supported OS: linux, windows, darwin, android\"\n        return 1\n        ;;\n    esac\n\n    # Validate architecture\n    case \"$arch\" in\n    \"x86_64\" | \"arm64\" | \"armv7\" | \"x86\") ;;\n    *)\n        log_error \"Unsupported architecture: $arch\"\n        log_error \"Supported architectures: x86_64, arm64, armv7, x86\"\n        return 1\n        ;;\n    esac\n\n    return 0\n}\n\nmain() {\n    case \"${1:-}\" in\n    \"build\")\n        if [ $# -ne 3 ]; then\n            log_error \"Build command requires OS and architecture\"\n            log_error \"Usage: $0 build <os> <arch>\"\n            show_usage\n            exit 1\n        fi\n\n        local os=\"$2\"\n        local arch=\"$3\"\n\n        if ! validate_os_arch \"$os\" \"$arch\"; then\n            exit 1\n        fi\n\n        log_step \"Starting single platform build\"\n        echo \"📦 Building ${APP_NAME} ${GIT_VERSION} (${COMMIT_ID}) for ${os}/${arch}\"\n        echo \"\"\n\n        # Setup\n        if ! prepare_environment; then\n            log_error \"Failed to prepare build environment\"\n            exit 1\n        fi\n\n        # Setup Android NDK if building for Android\n        if [ \"$os\" = \"android\" ]; then\n            if ! setup_android_ndk; then\n                log_error \"Failed to setup Android NDK\"\n                exit 1\n            fi\n        fi\n\n        # Build for specified platform\n        log_step \"Building binary\"\n\n        if [ \"$os\" = \"android\" ]; then\n            if ! build_android \"$arch\"; then\n                log_error \"Failed to build ${os}/${arch}\"\n                exit 1\n            fi\n        else\n            if ! build_standard \"$os\" \"$arch\"; then\n                log_error \"Failed to build ${os}/${arch}\"\n                exit 1\n            fi\n        fi\n\n        log_step \"Build completed\"\n        log_success \"Binary ready: ${OUTPUT_DIR}/bin/${APP_NAME}-${os}-${arch}\"\n        ;;\n    \"release\")\n        log_step \"Starting release build\"\n        echo \"📦 Building ${APP_NAME} ${GIT_VERSION} (${COMMIT_ID})\"\n        echo \"\"\n\n        # Setup\n        if ! prepare_environment; then\n            log_error \"Failed to prepare build environment\"\n            exit 1\n        fi\n\n        # Build frontend\n        if ! build_frontend; then\n            log_error \"Failed to build frontend\"\n            exit 1\n        fi\n\n        # if ! setup_android_ndk; then\n        #     log_error \"Failed to setup Android NDK\"\n        #     exit 1\n        # fi\n\n        # Build for different platforms\n        log_step \"Building binaries\"\n\n        # Android builds (requires CGO and NDK)\n        # if ! build_android arm64; then\n        #     log_error \"Failed to build Android arm64\"\n        # fi\n\n        # Standard builds (pure Go, static binaries)\n        if ! build_standard linux x86_64; then\n            log_error \"Failed to build Linux x86_64\"\n        fi\n        if ! build_standard linux arm64; then\n            log_error \"Failed to build Linux arm64\"\n        fi\n        if ! build_standard linux armv7; then\n            log_error \"Failed to build Linux armv7\"\n        fi\n        if ! build_standard linux x86; then\n            log_error \"Failed to build Linux x86\"\n        fi\n        if ! build_standard windows x86_64; then\n            log_error \"Failed to build Windows x86_64\"\n        fi\n        if ! build_standard windows x86; then\n            log_error \"Failed to build Windows x86\"\n        fi\n        if ! build_standard darwin arm64; then\n            log_error \"Failed to build Darwin arm64\"\n        fi\n        if ! build_standard darwin x86_64; then\n            log_error \"Failed to build Darwin arm64\"\n        fi\n\n        # Post-processing\n        if ! prepare_docker_binaries; then\n            log_warning \"Failed to prepare Docker binaries, but continuing...\"\n        fi\n\n        if ! generate_checksums; then\n            log_warning \"Failed to generate checksums, but continuing...\"\n        fi\n\n        if ! create_archives; then\n            log_warning \"Failed to create archives, but continuing...\"\n        fi\n\n        log_step \"Build completed\"\n        log_success \"All artifacts ready in ${OUTPUT_DIR}/\"\n        log_info \"  • Binaries: ${OUTPUT_DIR}/bin/\"\n        log_info \"  • Docker binaries: ${OUTPUT_DIR}/docker/\"\n        log_info \"  • Archives: ${OUTPUT_DIR}/archives/\"\n        ;;\n    \"help\" | \"-h\" | \"--help\")\n        show_usage\n        ;;\n    \"\")\n        log_error \"No command specified\"\n        show_usage\n        exit 1\n        ;;\n    *)\n        log_error \"Unknown command: $1\"\n        show_usage\n        exit 1\n        ;;\n    esac\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "scripts/dockerfiles/Dockerfile.alpine",
    "content": "FROM alpine\n\nARG TARGETPLATFORM\nENV TZ=Asia/Shanghai\n\nRUN apk add --no-cache alpine-conf ca-certificates su-exec && \\\n    /usr/sbin/setup-timezone -z Asia/Shanghai && \\\n    apk del alpine-conf && \\\n    rm -rf /var/cache/apk/* && \\\n    mkdir -p /app\n\nCOPY build/docker/${TARGETPLATFORM}/bestsub /app/bestsub\nCOPY scripts/dockerfiles/entrypoint.sh /entrypoint.sh\n\nRUN chmod +x /entrypoint.sh\n\nCMD [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "scripts/dockerfiles/Dockerfile.debian",
    "content": "FROM debian:bookworm\n\nARG TARGETPLATFORM\nENV TZ=Asia/Shanghai\n\nRUN apt-get update && apt-get install -y ca-certificates tzdata gosu && \\\n    ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    dpkg-reconfigure -f noninteractive tzdata && \\\n    rm -rf /var/cache/apt/*  && \\\n    mkdir -p /app\n\nCOPY build/docker/${TARGETPLATFORM}/bestsub /app/bestsub\nCOPY scripts/dockerfiles/entrypoint.sh /entrypoint.sh\n\nRUN chmod +x /entrypoint.sh\n\nCMD [\"/entrypoint.sh\"]"
  },
  {
    "path": "scripts/dockerfiles/entrypoint.sh",
    "content": "#!/bin/sh\nset -e\n\nPUID=\"${PUID:-0}\"\nPGID=\"${PGID:-0}\"\n\nchmod +x /app/bestsub\n\nif [ \"$PUID\" != \"0\" ] || [ \"$PGID\" != \"0\" ]; then\n    chown -R \"$PUID:$PGID\" /app\nfi\n\ncd /app\n\nif command -v su-exec >/dev/null 2>&1; then\n    exec su-exec \"$PUID:$PGID\" ./bestsub -c data/config.json\nelif command -v gosu >/dev/null 2>&1; then\n    exec gosu \"$PUID:$PGID\" ./bestsub -c data/config.json\nelse\n    if [ \"$PUID\" != \"0\" ] || [ \"$PGID\" != \"0\" ]; then\n        echo \"Warning: neither su-exec nor gosu is available; running as root.\" >&2\n    fi\n    exec ./bestsub -c data/config.json\nfi\n"
  },
  {
    "path": "static/static.go",
    "content": "package static\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n)\n\n//go:embed all:out\nvar staticFS embed.FS\n\n// StaticFS 返回 out 子目录的文件系统\nvar StaticFS, _ = fs.Sub(staticFS, \"out\")"
  },
  {
    "path": "web/.env.example",
    "content": "# API 配置\n# API 基础 URL，如果为空则使用当前域名\n# 例如：https://api.example.com 或 http://localhost:8080\nNEXT_PUBLIC_API_BASEURL=\n"
  },
  {
    "path": "web/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n!.env.example\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "web/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/src/components\",\n    \"utils\": \"@/src/utils\",\n    \"ui\": \"@/src/components/ui\",\n    \"lib\": \"@/src/lib\",\n    \"hooks\": \"@/src/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "web/eslint.config.mjs",
    "content": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [\n  ...compat.extends(\"next/core-web-vitals\", \"next/typescript\"),\n  {\n    rules: {\n      \"@typescript-eslint/no-unused-vars\": [\"error\", {\n        argsIgnorePattern: \"^_\",\n        varsIgnorePattern: \"^_\"\n      }],\n      \"@typescript-eslint/no-explicit-any\": \"warn\",\n      \"react-hooks/exhaustive-deps\": \"warn\",\n      \"react/jsx-key\": \"error\",\n      \"no-console\": [\"warn\", { allow: [\"warn\", \"error\"] }],\n      \"prefer-const\": \"error\",\n      \"no-var\": \"error\",\n    },\n  },\n];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "web/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  output: 'export',\n  trailingSlash: true,\n  skipTrailingSlashRedirect: true,\n\n  images: {\n    unoptimized: true,\n  },\n\n  compress: true,\n\n  assetPrefix: process.env.NODE_ENV === 'production' ? '' : '',\n\n  experimental: {\n    optimizePackageImports: [\n      '@radix-ui/react-icons',\n      '@tabler/icons-react',\n      'lucide-react'\n    ],\n  },\n\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"bestsub\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"NEXT_PUBLIC_APP_VERSION=$(git describe --tags --abbrev=0) next dev --turbopack\",\n    \"build\": \"NEXT_PUBLIC_APP_VERSION=$(git describe --tags --abbrev=0) next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@dnd-kit/core\": \"^6.3.1\",\n    \"@dnd-kit/modifiers\": \"^9.0.0\",\n    \"@dnd-kit/sortable\": \"^10.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"@radix-ui/react-accordion\": \"^1.2.12\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-avatar\": \"^1.1.10\",\n    \"@radix-ui/react-checkbox\": \"^1.3.2\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.15\",\n    \"@radix-ui/react-hover-card\": \"^1.1.14\",\n    \"@radix-ui/react-label\": \"^2.1.7\",\n    \"@radix-ui/react-popover\": \"^1.1.15\",\n    \"@radix-ui/react-select\": \"^2.2.5\",\n    \"@radix-ui/react-separator\": \"^1.1.7\",\n    \"@radix-ui/react-slot\": \"^1.2.3\",\n    \"@radix-ui/react-switch\": \"^1.2.5\",\n    \"@radix-ui/react-tabs\": \"^1.1.12\",\n    \"@radix-ui/react-toggle\": \"^1.1.9\",\n    \"@radix-ui/react-toggle-group\": \"^1.1.10\",\n    \"@radix-ui/react-tooltip\": \"^1.2.7\",\n    \"@tabler/icons-react\": \"^3.34.1\",\n    \"@tanstack/react-query\": \"^5.85.8\",\n    \"@tanstack/react-table\": \"^8.21.3\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"date-fns\": \"^4.1.0\",\n    \"lucide-react\": \"^0.534.0\",\n    \"marked\": \"^16.2.1\",\n    \"next\": \"15.4.5\",\n    \"next-themes\": \"^0.4.6\",\n    \"react\": \"19.1.0\",\n    \"react-day-picker\": \"^9.8.1\",\n    \"react-dom\": \"19.1.0\",\n    \"react-hook-form\": \"^7.62.0\",\n    \"recharts\": \"^2.15.4\",\n    \"sonner\": \"^2.0.7\",\n    \"tailwind-merge\": \"^3.3.1\",\n    \"vaul\": \"^1.1.2\",\n    \"zod\": \"^4.0.14\",\n    \"zustand\": \"^5.0.7\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.4.5\",\n    \"tailwindcss\": \"^4\",\n    \"tw-animate-css\": \"^1.3.6\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "web/postcss.config.mjs",
    "content": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "web/src/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n  --color-sidebar-ring: var(--sidebar-ring);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar: var(--sidebar);\n  --color-chart-5: var(--chart-5);\n  --color-chart-4: var(--chart-4);\n  --color-chart-3: var(--chart-3);\n  --color-chart-2: var(--chart-2);\n  --color-chart-1: var(--chart-1);\n  --color-ring: var(--ring);\n  --color-input: var(--input);\n  --color-border: var(--border);\n  --color-destructive: var(--destructive);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-accent: var(--accent);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-muted: var(--muted);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-secondary: var(--secondary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-primary: var(--primary);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-popover: var(--popover);\n  --color-card-foreground: var(--card-foreground);\n  --color-card: var(--card);\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n/* 隐藏滚动条但保持滚动功能 */\n.scrollbar-hide {\n  -ms-overflow-style: none;\n  /* IE and Edge */\n  scrollbar-width: none;\n  /* Firefox */\n}\n\n.scrollbar-hide::-webkit-scrollbar {\n  display: none;\n  /* Chrome, Safari and Opera */\n}"
  },
  {
    "path": "web/src/app/layout.tsx",
    "content": "import \"./globals.css\";\nimport { ThemeProvider, AuthProvider, AlertProvider } from \"@/src/components/providers\";\nimport { QueryProvider } from \"@/src/components/providers/query-provider\";\nimport { Toaster } from \"sonner\";\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"zh-CN\" suppressHydrationWarning>\n      <head />\n      <body>\n        <QueryProvider>\n          <ThemeProvider\n            attribute=\"class\"\n            defaultTheme=\"system\"\n            enableSystem\n            disableTransitionOnChange\n          >\n            <AuthProvider>\n              <AlertProvider>\n                {children}\n                <Toaster position=\"top-center\" richColors />\n              </AlertProvider>\n            </AuthProvider>\n          </ThemeProvider>\n        </QueryProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "web/src/app/not-found.tsx",
    "content": "\"use client\"\n\nimport { useEffect } from \"react\"\nimport { SPAApp } from \"@/src/components/app\"\n\nexport default function NotFound() {\n    useEffect(() => {\n        const currentPath = window.location.pathname\n        if (currentPath !== '/') {\n            window.location.href = `/#${currentPath}${window.location.search}${window.location.hash}`\n        }\n    }, [])\n    return <SPAApp />\n} "
  },
  {
    "path": "web/src/app/page.tsx",
    "content": "\"use client\"\n\nimport { SPAApp } from \"@/src/components/app\"\n\nexport default function Home() {\n  return <SPAApp />\n}"
  },
  {
    "path": "web/src/components/app/app-layout.tsx",
    "content": "\"use client\"\n\nimport { useRouter } from \"@/src/router/core/context\"\nimport { useAuth } from \"@/src/components/providers\"\nimport { useRouteTitle, useRoutePreloader } from \"@/src/router\"\nimport { AppSidebar, SiteHeader } from \"@/src/components/layout\"\nimport { SidebarInset, SidebarProvider } from \"@/src/components/ui/sidebar\"\nimport { PageLoading } from \"@/src/components/ui/loading\"\nimport { RouterOutlet } from \"@/src/router/core/outlet\"\n\nexport function AppLayout() {\n    const { currentPath, routes } = useRouter()\n    const { isLoading } = useAuth()\n\n    useRoutePreloader()\n    useRouteTitle()\n\n    if (isLoading) {\n        return <PageLoading message=\"应用启动中...\" />\n    }\n\n    const currentRoute = routes.find(route => route.path === currentPath)\n    const isProtectedRoute = currentRoute?.protected || false\n\n    if (isProtectedRoute) {\n        return (\n            <SidebarProvider\n                style={\n                    {\n                        \"--sidebar-width\": \"calc(var(--spacing) * 72)\",\n                        \"--header-height\": \"calc(var(--spacing) * 12)\",\n                    } as React.CSSProperties\n                }\n            >\n                <AppSidebar variant=\"inset\" />\n                <SidebarInset>\n                    <SiteHeader />\n                    <div className=\"flex flex-1 flex-col\">\n                        <div className=\"@container/main flex flex-1 flex-col gap-2\">\n                            <div\n                                className=\"animate-in fade-in slide-in-from-bottom-2 duration-200 ease-out\"\n                                key={currentPath}\n                            >\n                                <RouterOutlet />\n                            </div>\n                        </div>\n                    </div>\n                </SidebarInset>\n            </SidebarProvider>\n        )\n    }\n\n    return (\n        <div\n            className=\"animate-in fade-in slide-in-from-bottom-2 duration-200 ease-out\"\n            key={currentPath}\n        >\n            <RouterOutlet />\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/app/index.ts",
    "content": "export { SPAApp } from './spa-app'\nexport { AppLayout } from './app-layout' "
  },
  {
    "path": "web/src/components/app/spa-app.tsx",
    "content": "\"use client\"\n\nimport { RouterProvider } from \"@/src/router/core/router\"\nimport { AppLayout } from \"./app-layout\"\n\nexport function SPAApp() {\n    return (\n        <RouterProvider>\n            <AppLayout />\n        </RouterProvider>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/check/components/check-form.tsx",
    "content": "import { Button } from \"@/src/components/ui/button\"\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/src/components/ui/dialog\"\nimport { useCheckForm } from \"../hooks\"\nimport { UI_TEXT } from \"../constants\"\nimport {\n  BasicInfoSection,\n  BasicConfigSection,\n  NotifyConfig,\n  LogConfig,\n  ExtraConfigSection\n} from \"./form-sections\"\nimport { SubscriptionSection } from \"@/src/components/shared/subscription-section\"\nimport type { CheckRequest } from \"@/src/types/check\"\n\ninterface CheckFormProps {\n  initialData?: CheckRequest | undefined\n  formTitle: string\n  isOpen: boolean\n  onClose: () => void\n  editingCheckId?: number | undefined\n}\n\nexport function CheckForm({\n  initialData,\n  formTitle,\n  isOpen,\n  onClose,\n  editingCheckId,\n}: CheckFormProps) {\n  const { form, onSubmit, isEditing } = useCheckForm({\n    initialData,\n    editingCheckId,\n    onSuccess: onClose,\n    isOpen,\n  })\n\n  const { control } = form\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent\n        className=\"max-w-2xl max-h-[80vh] overflow-y-auto scrollbar-hide\"\n        aria-describedby={undefined}\n      >\n        <DialogHeader>\n          <DialogTitle>{formTitle}</DialogTitle>\n        </DialogHeader>\n\n        <form onSubmit={onSubmit} className=\"space-y-6\">\n          <BasicInfoSection control={control} />\n\n          <BasicConfigSection control={control} />\n\n          <NotifyConfig control={control} />\n\n          <LogConfig control={control} />\n\n          <SubscriptionSection\n            control={control}\n            subIdField=\"task.sub_id\"\n            subIdExcludeField=\"task.sub_id_exclude\"\n          />\n\n          <ExtraConfigSection control={control} />\n\n          <div className=\"flex gap-2 pt-4\">\n            <Button type=\"submit\" className=\"flex-1\">\n              {isEditing ? UI_TEXT.UPDATE : UI_TEXT.CREATE}\n            </Button>\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              onClick={onClose}\n            >\n              {UI_TEXT.CANCEL}\n            </Button>\n          </div>\n        </form>\n      </DialogContent>\n    </Dialog>\n  )\n}"
  },
  {
    "path": "web/src/components/features/check/components/check-list.tsx",
    "content": "import { useEffect } from \"react\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Badge } from \"@/src/components/ui/badge\"\nimport { Card, CardContent } from \"@/src/components/ui/card\"\nimport { Table, TableBody, TableCell, TableRow } from \"@/src/components/ui/table\"\nimport { InlineLoading } from \"@/src/components/ui/loading\"\nimport { Play, Edit, Trash2 } from \"lucide-react\"\nimport { UI_TEXT } from \"../constants\"\nimport { formatLastRunTime, formatDuration, formatBooleanText } from \"@/src/utils\"\nimport StatusBadge from \"@/src/components/shared/status-badge\"\nimport { useOverflowDetection } from \"@/src/lib/hooks/useOverflowDetection\"\nimport type { CheckResponse } from \"@/src/types/check\"\nimport { api } from \"@/src/lib/api/client\"\nimport { useAlert } from '@/src/components/providers'\nimport { useChecks, useDeleteCheck } from \"@/src/lib/queries/check-queries\"\nimport { toast } from \"sonner\"\n\ninterface CheckListProps {\n    onEdit: (check: CheckResponse) => void\n}\n\nexport function CheckList({ onEdit }: CheckListProps) {\n    const { confirm } = useAlert()\n    const { data: checks = [], isLoading, error } = useChecks()\n    const deleteCheckMutation = useDeleteCheck()\n    const { containerRef, contentRef, isOverflowing, checkOverflow } = useOverflowDetection<HTMLTableElement>()\n\n    const onDelete = async (id: number, name: string) => {\n        const confirmed = await confirm({\n            title: UI_TEXT.CONFIRM_DELETE,\n            description: UI_TEXT.DELETE_CONFIRM_MESSAGE.replace('{name}', name),\n            confirmText: UI_TEXT.DELETE,\n            cancelText: UI_TEXT.CANCEL,\n            variant: 'destructive'\n        })\n\n        if (confirmed) {\n            try {\n                await deleteCheckMutation.mutateAsync(id)\n                toast.success(UI_TEXT.DELETE_SUCCESS)\n            } catch (error) {\n                toast.error(UI_TEXT.DELETE_FAILED)\n                console.error('Failed to delete check:', error)\n            }\n        }\n    }\n\n    useEffect(() => {\n        if (!isLoading) {\n            checkOverflow()\n        }\n    }, [isLoading, checkOverflow])\n\n    if (isLoading) {\n        return (\n            <Card>\n                <CardContent>\n                    <InlineLoading message={UI_TEXT.LOADING + '检测任务...'} />\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (error) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-destructive\">\n                        加载失败: {error.message}\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (checks.length === 0) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-muted-foreground\">\n                        {UI_TEXT.NO_DATA}，点击上方按钮创建第一个检测任务\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    return (\n        <Card>\n            <CardContent>\n                <div className=\"overflow-x-auto\" ref={containerRef}>\n                    <Table ref={contentRef}>\n                        <TableBody>\n                            {checks.sort((a, b) => a.id - b.id).map((check) => (\n                                <TableRow key={check.id}>\n                                    <TableCell className=\"space-y-1\">\n                                        <div className=\"font-medium\">\n                                            {check.name}\n                                        </div>\n                                        <div className=\"text-sm text-muted-foreground\">{check.task?.cron_expr || 'N/A'}</div>\n                                    </TableCell>\n\n                                    <TableCell className=\"space-y-2 flex flex-col\">\n                                        <StatusBadge status={check.enable ? check.status : 'disabled'} />\n                                        <Badge variant=\"outline\" className=\"text-xs w-fit\">\n                                            {check.task.type}\n                                        </Badge>\n                                    </TableCell>\n\n                                    <TableCell className=\"text-xs space-y-1\">\n                                        <div>超时时间: <span className=\"text-muted-foreground\">{check.task?.timeout || 0}分钟</span> </div>\n                                        <div>通知: <span className=\"text-muted-foreground\">{formatBooleanText(check.task?.notify ?? false)}</span></div>\n                                        <div>日志: <span className=\"text-muted-foreground\">{formatBooleanText(check.task?.log_write_file ?? false)}</span></div>\n                                    </TableCell>\n\n                                    <TableCell className=\"text-xs space-y-1\">\n                                        <div>最后运行: <span className=\"text-muted-foreground\">{formatLastRunTime(check.result?.last_run)}</span></div>\n                                        <div>执行时长: <span className=\"text-muted-foreground\">{formatDuration(check.result?.duration)}</span></div>\n                                        <div>状态消息: <span className=\"text-muted-foreground\">{check.result?.msg || '无'}</span></div>\n                                    </TableCell>\n\n                                    <TableCell className={`text-right space-x-2 sticky right-0 bg-background ${isOverflowing ? 'shadow-[-4px_0_8px_-2px_rgba(0,0,0,0.1)]' : ''}`}>\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => {\n                                                api.runCheck(check.id)\n                                                toast.success(UI_TEXT.RUN_SUCCESS)\n                                            }}\n                                            disabled={!check.enable || check.status === 'running'}\n                                        >\n                                            <Play className=\"h-4 w-4\" />\n                                        </Button>\n\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => onEdit(check)}\n                                        >\n                                            <Edit className=\"h-4 w-4\" />\n                                        </Button>\n\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => onDelete(check.id, check.name)}\n                                        >\n                                            <Trash2 className=\"h-4 w-4\" />\n                                        </Button>\n                                    </TableCell>\n                                </TableRow>\n                            ))}\n                        </TableBody>\n                    </Table>\n                </div>\n            </CardContent>\n        </Card>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/check-page.tsx",
    "content": "import { useState, useCallback } from \"react\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Plus } from \"lucide-react\"\nimport { CheckForm } from \"./check-form\"\nimport { CheckList } from \"./check-list\"\nimport { UI_TEXT } from \"../constants\"\nimport { convertCheckResponseToRequest } from \"../utils\"\nimport type { CheckResponse, CheckRequest } from \"@/src/types/check\"\n\nexport function CheckPage() {\n    const [isDialogOpen, setIsDialogOpen] = useState(false)\n    const [editingCheck, setEditingCheck] = useState<CheckResponse | null>(null)\n    const [formData, setFormData] = useState<CheckRequest | undefined>(undefined)\n\n    const openEditDialog = useCallback((check: CheckResponse) => {\n        setEditingCheck(check)\n        setFormData(convertCheckResponseToRequest(check))\n        setIsDialogOpen(true)\n    }, [])\n\n    const openCreateDialog = useCallback(() => {\n        setEditingCheck(null)\n        setFormData(undefined)\n        setIsDialogOpen(true)\n    }, [])\n\n    const closeFormDialog = useCallback(() => {\n        setIsDialogOpen(false)\n        setTimeout(() => {\n            setEditingCheck(null)\n            setFormData(undefined)\n        }, 200)\n    }, [])\n\n    return (\n        <div className=\"flex flex-col gap-4 py-4 md:gap-6 md:py-6\">\n            <div className=\"flex items-center justify-between px-4 lg:px-6\">\n                <div>\n                    <h1 className=\"text-2xl font-bold\">检测任务</h1>\n                </div>\n\n                <Button onClick={openCreateDialog}>\n                    <Plus className=\"h-4 w-4 mr-2\" />\n                    {UI_TEXT.CREATE_CHECK}\n                </Button>\n            </div>\n\n            <CheckForm\n                {...(formData && { initialData: formData })}\n                formTitle={editingCheck ? UI_TEXT.EDIT_CHECK : UI_TEXT.CREATE_CHECK}\n                isOpen={isDialogOpen}\n                onClose={closeFormDialog}\n                editingCheckId={editingCheck?.id}\n            />\n\n            <div className=\"px-4 lg:px-6\">\n                <CheckList onEdit={openEditDialog} />\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/form-sections/basic-config-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { validateCronExpr, validateTimeout } from '@/src/utils'\nimport { CHECK_CONSTANTS } from '../../constants'\nimport type { CheckRequest } from '@/src/types/check'\n\nexport function BasicConfigSection({ control }: { control: Control<CheckRequest> }) {\n    return (\n        <div className=\"grid grid-cols-2 gap-4\">\n            <Controller\n                name=\"task.timeout\"\n                control={control}\n                render={({ field }) => (\n                    <div>\n                        <Label htmlFor=\"timeout\" className=\"mb-2 block\">\n                            超时时间(分钟)\n                        </Label>\n                        <Input\n                            id=\"timeout\"\n                            type=\"number\"\n                            value={field.value}\n                            onChange={(e) => field.onChange(validateTimeout(e.target.value))}\n                            placeholder={CHECK_CONSTANTS.DEFAULT_TIMEOUT.toString()}\n                            min={CHECK_CONSTANTS.MIN_TIMEOUT}\n                            max={CHECK_CONSTANTS.MAX_TIMEOUT}\n                        />\n                    </div>\n                )}\n            />\n\n            <Controller\n                name=\"task.cron_expr\"\n                control={control}\n                render={({ field }) => (\n                    <div>\n                        <Label htmlFor=\"cron\" className=\"mb-2 block\">\n                            检测频率\n                        </Label>\n                        <Input\n                            id=\"cron\"\n                            {...field}\n                            placeholder={CHECK_CONSTANTS.DEFAULT_CRON}\n                            className={!validateCronExpr(field.value) ? 'border-red-500' : ''}\n                        />\n                        {field.value && !validateCronExpr(field.value) && (\n                            <p className=\"text-xs text-red-500 mt-1\">请输入有效的Cron表达式</p>\n                        )}\n                    </div>\n                )}\n            />\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/form-sections/basic-info-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { Switch } from '@/src/components/ui/switch'\nimport type { CheckRequest } from '@/src/types/check'\n\nexport function BasicInfoSection({ control }: { control: Control<CheckRequest> }) {\n    return (\n        <div className=\"space-y-4\">\n            <Controller\n                name=\"name\"\n                control={control}\n                render={({ field }) => (\n                    <div>\n                        <Label htmlFor=\"name\" className=\"mb-2 block\">\n                            任务名称\n                        </Label>\n                        <Input\n                            id=\"name\"\n                            {...field}\n                            placeholder=\"请输入检测任务名称\"\n                            required\n                        />\n                    </div>\n                )}\n            />\n\n            <Controller\n                name=\"enable\"\n                control={control}\n                render={({ field }) => (\n                    <div className=\"flex items-center justify-between\">\n                        <Label htmlFor=\"enable\">启用任务</Label>\n                        <Switch\n                            id=\"enable\"\n                            checked={field.value}\n                            onCheckedChange={field.onChange}\n                        />\n                    </div>\n                )}\n            />\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/form-sections/extra-config-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/src/components/ui/select'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { Switch } from '@/src/components/ui/switch'\nimport { useCheckTypes } from '@/src/lib/queries/check-queries'\nimport { UI_TEXT } from '../../constants'\nimport type { CheckRequest } from '@/src/types/check'\nimport type { DynamicConfigItem } from '@/src/types/common'\n\n\nexport function ExtraConfigSection({ control }: { control: Control<CheckRequest> }) {\n    const { data: checkTypeConfigs = {}, isLoading } = useCheckTypes()\n\n    const checkTypes = Object.keys(checkTypeConfigs)\n\n    const isConfigFieldEmpty = (configType: string, value: unknown): boolean => {\n        if (configType === 'boolean') return false\n        return value === undefined || value === '' || (typeof value === 'string' && value.trim() === '')\n    }\n\n    const renderConfigField = (config: DynamicConfigItem) => {\n        return (\n            <Controller\n                name={`config.${config.key}`}\n                control={control}\n                render={({ field }) => {\n                    if (field.value === undefined || field.value === null || field.value === '') {\n                        if (config.type === 'boolean') {\n                            field.onChange(config.value === 'true')\n                        } else if (config.type === 'number') {\n                            field.onChange(Number(config.value) || 0)\n                        } else {\n                            field.onChange(config.value || '')\n                        }\n                    }\n                    const isEmpty = isConfigFieldEmpty(config.type, field.value)\n                    const showError = config.require && isEmpty\n\n                    switch (config.type) {\n                        case 'string':\n                            if (config.options) {\n                                return (\n                                    <Select\n                                        value={field.value as string || config.value || ''}\n                                        onValueChange={field.onChange}\n                                    >\n                                        <SelectTrigger className={showError ? 'border-red-500' : ''}>\n                                            <SelectValue placeholder={`请选择${config.name}`} />\n                                        </SelectTrigger>\n                                        <SelectContent>\n                                            {config.options.split(',').map((option: string) => (\n                                                <SelectItem key={option.trim()} value={option.trim()}>\n                                                    {option.trim()}\n                                                </SelectItem>\n                                            ))}\n                                        </SelectContent>\n                                    </Select>\n                                )\n                            }\n                            return (\n                                <Input\n                                    type=\"text\"\n                                    placeholder={`请输入${config.name}`}\n                                    value={field.value as string || config.value || ''}\n                                    onChange={(e) => field.onChange(e.target.value)}\n                                    className={showError ? 'border-red-500' : ''}\n                                />\n                            )\n\n                        case 'number':\n                            return (\n                                <Input\n                                    type=\"number\"\n                                    placeholder={`请输入${config.name}`}\n                                    value={field.value as string || config.value || ''}\n                                    onChange={(e) => field.onChange(Number(e.target.value) || 0)}\n                                    className={showError ? 'border-red-500' : ''}\n                                />\n                            )\n\n                        case 'boolean':\n                            return (\n                                <div className=\"flex items-center justify-between w-full\">\n                                    <span className=\"text-sm font-medium\">{config.name}</span>\n                                    <Switch\n                                        checked={field.value as boolean || config.value === 'true'}\n                                        onCheckedChange={field.onChange}\n                                    />\n                                </div>\n                            )\n\n                        default:\n                            return (\n                                <Input\n                                    type=\"text\"\n                                    placeholder={`请输入${config.name}`}\n                                    value={field.value as string || config.value || ''}\n                                    onChange={(e) => field.onChange(e.target.value)}\n                                    className={showError ? 'border-red-500' : ''}\n                                />\n                            )\n                    }\n                }}\n            />\n        )\n    }\n\n    return (\n        <div className=\"space-y-4\">\n            <Controller\n                name=\"task.type\"\n                control={control}\n                render={({ field }) => (\n                    <div className=\"w-full\">\n                        <Label htmlFor=\"type\" className=\"mb-2 block\">\n                            检测类型\n                        </Label>\n                        <Select\n                            value={field.value}\n                            onValueChange={field.onChange}\n                        >\n                            <SelectTrigger className=\"w-full\">\n                                <SelectValue placeholder={isLoading ? UI_TEXT.LOADING + \"...\" : \"选择检测类型\"} />\n                            </SelectTrigger>\n                            <SelectContent>\n                                {checkTypes.map(type => (\n                                    <SelectItem key={type} value={type}>\n                                        {type}\n                                    </SelectItem>\n                                ))}\n                            </SelectContent>\n                        </Select>\n                    </div>\n                )}\n            />\n\n            <Controller\n                name=\"task.type\"\n                control={control}\n                render={({ field }) => {\n                    const selectedType = field.value\n                    const configs = selectedType ? checkTypeConfigs[selectedType] || [] : []\n\n                    if (!selectedType) {\n                        return (\n                            <div className=\"text-center py-4 text-muted-foreground\">\n                                选择检测类型后将显示相关配置项\n                            </div>\n                        )\n                    }\n\n                    if (isLoading) {\n                        return (\n                            <div className=\"text-center py-4 text-muted-foreground\">\n                                加载配置中...\n                            </div>\n                        )\n                    }\n\n                    if (configs.length === 0) {\n                        return <div></div>\n                    }\n\n                    return (\n                        <div className=\"space-y-4\">\n                            {configs.map((config) => (\n                                <div key={config.key} className=\"space-y-2\">\n                                    {config.type !== 'boolean' && (\n                                        <Label htmlFor={config.key} className=\"block\">\n                                            {config.name}\n                                            {config.require && <span className=\"text-red-500 ml-1\">*</span>}\n                                        </Label>\n                                    )}\n                                    {renderConfigField(config)}\n                                    {config.desc && (\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            {config.desc}\n                                        </p>\n                                    )}\n                                    <Controller\n                                        name={`config.${config.key}`}\n                                        control={control}\n                                        render={({ field: configField }) => {\n                                            const isEmpty = isConfigFieldEmpty(config.type, configField.value)\n                                            return config.require && isEmpty ? (\n                                                <p className=\"text-xs text-red-500\">此字段为必填项</p>\n                                            ) : <></>\n                                        }}\n                                    />\n                                </div>\n                            ))}\n                        </div>\n                    )\n                }}\n            />\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/form-sections/index.ts",
    "content": "export { BasicInfoSection } from './basic-info-section'\nexport { BasicConfigSection } from './basic-config-section'\nexport { NotifyConfig } from './notify-config'\nexport { LogConfig } from './log-config'\nexport { ExtraConfigSection } from './extra-config-section'\n"
  },
  {
    "path": "web/src/components/features/check/components/form-sections/log-config.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Label } from '@/src/components/ui/label'\nimport { Switch } from '@/src/components/ui/switch'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/src/components/ui/select'\nimport { LOG_LEVELS } from '../../constants'\nimport type { CheckRequest } from '@/src/types/check'\n\nexport function LogConfig({ control }: { control: Control<CheckRequest> }) {\n\n    return (\n        <div className=\"space-y-4\">\n            <Controller\n                name=\"task.log_write_file\"\n                control={control}\n                render={({ field }) => (\n                    <div className=\"flex items-center justify-between\">\n                        <Label htmlFor=\"log_write_file\">写入日志文件</Label>\n                        <Switch\n                            id=\"log_write_file\"\n                            checked={field.value ?? false}\n                            onCheckedChange={field.onChange}\n                        />\n                    </div>\n                )}\n            />\n\n            <Controller\n                name=\"task.log_level\"\n                control={control}\n                render={({ field }) => (\n                    <div className=\"mt-2\">\n                        <Label htmlFor=\"log_level\" className=\"mb-2 block\">\n                            日志级别\n                        </Label>\n                        <Select\n                            value={field.value}\n                            onValueChange={field.onChange}\n                        >\n                            <SelectTrigger className=\"w-full\">\n                                <SelectValue />\n                            </SelectTrigger>\n                            <SelectContent>\n                                {LOG_LEVELS.map((level) => (\n                                    <SelectItem key={level.value} value={level.value}>\n                                        {level.label}\n                                    </SelectItem>\n                                ))}\n                            </SelectContent>\n                        </Select>\n                    </div>\n                )}\n            />\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/form-sections/notify-config.tsx",
    "content": "import { Controller, Control, useWatch } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { Switch } from '@/src/components/ui/switch'\nimport type { CheckRequest } from '@/src/types/check'\n\nexport function NotifyConfig({ control }: { control: Control<CheckRequest> }) {\n    const notifyEnabled = useWatch({\n        control,\n        name: \"task.notify\",\n        defaultValue: false\n    })\n\n    return (\n        <div className=\"space-y-4\">\n            <Controller\n                name=\"task.notify\"\n                control={control}\n                render={({ field }) => (\n                    <div className=\"flex items-center justify-between\">\n                        <Label htmlFor=\"notify\">启用通知</Label>\n                        <Switch\n                            id=\"notify\"\n                            checked={field.value ?? false}\n                            onCheckedChange={field.onChange}\n                        />\n                    </div>\n                )}\n            />\n\n            {notifyEnabled && (\n                <Controller\n                    name=\"task.notify_channel\"\n                    control={control}\n                    render={({ field }) => (\n                        <div className=\"mt-2\">\n                            <Label htmlFor=\"notify_channel\" className=\"mb-2 block\">\n                                通知渠道\n                            </Label>\n                            <Input\n                                id=\"notify_channel\"\n                                type=\"number\"\n                                value={field.value}\n                                onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}\n                                min=\"1\"\n                            />\n                        </div>\n                    )}\n                />\n            )}\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/check/components/index.ts",
    "content": "export { CheckPage } from './check-page'"
  },
  {
    "path": "web/src/components/features/check/constants/index.ts",
    "content": "export const CHECK_CONSTANTS = {\n    DEFAULT_TIMEOUT: 30,\n    MIN_TIMEOUT: 1,\n    MAX_TIMEOUT: 300,\n    DEFAULT_CRON: \"0 */5 * * *\",\n    DEFAULT_NOTIFY_CHANNEL: 1,\n    DEFAULT_LOG_LEVEL: \"info\",\n} as const\n\nexport const LOG_LEVELS = [\n    { value: 'debug', label: 'Debug' },\n    { value: 'info', label: 'Info' },\n    { value: 'warn', label: 'Warn' },\n    { value: 'error', label: 'Error' },\n] as const\n\nexport const CHECK_STATUS_OPTIONS = [\n    { value: 'idle', label: '空闲' },\n    { value: 'running', label: '运行中' },\n    { value: 'success', label: '成功' },\n    { value: 'failed', label: '失败' },\n] as const\n\nexport const FORM_VALIDATION = {\n    NAME_REQUIRED: '任务名称不能为空',\n    TYPE_REQUIRED: '请选择检测类型',\n    TIMEOUT_RANGE: '超时时间必须在1-300秒之间',\n    CRON_INVALID: '请输入有效的Cron表达式',\n    NOTIFY_CHANNEL_MIN: '通知渠道必须大于0',\n} as const\n\nexport const UI_TEXT = {\n    CREATE_CHECK: '添加检测',\n    EDIT_CHECK: '编辑检测任务',\n    UPDATE: '更新',\n    CREATE: '创建',\n    CANCEL: '取消',\n    DELETE: '删除',\n    RUN: '运行',\n    LOADING: '加载中...',\n    NO_DATA: '暂无数据',\n    CONFIRM_DELETE: '确认删除',\n    DELETE_CONFIRM_MESSAGE: '您确定要删除检测任务 \"{name}\" 吗？此操作无法撤销。',\n    RUN_SUCCESS: '运行成功',\n    RUN_FAILED: '运行失败',\n    CREATE_SUCCESS: '创建成功',\n    UPDATE_SUCCESS: '更新成功',\n    DELETE_SUCCESS: '删除成功',\n    CREATE_FAILED: '创建失败',\n    UPDATE_FAILED: '更新失败',\n    DELETE_FAILED: '删除失败',\n    LOAD_TYPES_FAILED: '加载失败',\n    LOAD_CONFIG_FAILED: '加载失败',\n} as const\n\nexport const CRON_PRESETS = [\n    { label: '每5分钟', value: '0 */5 * * *' },\n    { label: '每10分钟', value: '0 */10 * * *' },\n    { label: '每30分钟', value: '0 */30 * * *' },\n    { label: '每小时', value: '0 0 * * *' },\n    { label: '每天', value: '0 0 0 * *' },\n    { label: '每周', value: '0 0 0 * 0' },\n] as const"
  },
  {
    "path": "web/src/components/features/check/hooks/index.ts",
    "content": "export { useCheckForm } from './useCheckForm'"
  },
  {
    "path": "web/src/components/features/check/hooks/useCheckForm.ts",
    "content": "import { useForm } from 'react-hook-form'\nimport { useEffect, useMemo } from 'react'\nimport { toast } from 'sonner'\nimport { useCheckTypes, useCreateCheck, useUpdateCheck } from '@/src/lib/queries/check-queries'\nimport {\n    createDefaultCheckData,\n    validateCheckForm\n} from '../utils'\nimport { UI_TEXT } from '../constants'\nimport type { CheckRequest } from '@/src/types/check'\n\ninterface UseCheckFormProps {\n    initialData?: CheckRequest | undefined\n    editingCheckId?: number | undefined\n    onSuccess?: () => void\n    isOpen?: boolean\n}\n\nexport function useCheckForm({\n    initialData,\n    editingCheckId,\n    onSuccess,\n    isOpen = true\n}: UseCheckFormProps = {}) {\n    const { data: checkTypeConfigs = {}, isLoading: isLoadingConfigs } = useCheckTypes()\n    const createCheckMutation = useCreateCheck()\n    const updateCheckMutation = useUpdateCheck()\n\n    const defaultData = useMemo(() => createDefaultCheckData(), [])\n\n    const form = useForm<CheckRequest>({\n        defaultValues: initialData || defaultData\n    })\n\n    const { handleSubmit, reset, watch } = form\n\n    useEffect(() => {\n        if (isOpen) {\n            reset(initialData || defaultData)\n        }\n    }, [initialData, reset, defaultData, isOpen])\n\n    const onSubmit = async (data: CheckRequest) => {\n        const validation = validateCheckForm(data)\n        if (!validation.isValid) {\n            validation.errors.forEach(error => toast.error(error))\n            return\n        }\n\n        try {\n            const submitData: CheckRequest = {\n                ...data,\n            }\n\n            if (editingCheckId) {\n                await updateCheckMutation.mutateAsync({ id: editingCheckId, data: submitData })\n                toast.success(UI_TEXT.UPDATE_SUCCESS)\n            } else {\n                await createCheckMutation.mutateAsync(submitData)\n                toast.success(UI_TEXT.CREATE_SUCCESS)\n            }\n\n            onSuccess?.()\n        } catch (error) {\n            const errorMessage = editingCheckId ? UI_TEXT.UPDATE_FAILED : UI_TEXT.CREATE_FAILED\n            toast.error(errorMessage)\n            console.error('Failed to save check:', error)\n        }\n    }\n\n    return {\n        form,\n        onSubmit: handleSubmit(onSubmit),\n        watch,\n        isEditing: !!editingCheckId,\n        checkTypeConfigs,\n        isLoadingConfigs,\n    }\n}\n"
  },
  {
    "path": "web/src/components/features/check/index.ts",
    "content": "export { CheckPage } from './components/check-page'\n"
  },
  {
    "path": "web/src/components/features/check/utils/index.ts",
    "content": "import { CHECK_CONSTANTS, FORM_VALIDATION } from '../constants'\nimport { validateCronExpr } from '@/src/utils'\nimport type { CheckRequest, CheckResponse } from '@/src/types/check'\nexport function createDefaultCheckData(): CheckRequest {\n    return {\n        name: '',\n        enable: true,\n        task: {\n            type: '',\n            timeout: CHECK_CONSTANTS.DEFAULT_TIMEOUT,\n            cron_expr: CHECK_CONSTANTS.DEFAULT_CRON,\n            notify: false,\n            notify_channel: CHECK_CONSTANTS.DEFAULT_NOTIFY_CHANNEL,\n            log_write_file: false,\n            log_level: CHECK_CONSTANTS.DEFAULT_LOG_LEVEL,\n            sub_id: [],\n        },\n        config: {},\n    }\n}\nexport function validateCheckForm(formData: CheckRequest): { isValid: boolean; errors: string[] } {\n    const errors: string[] = []\n\n    if (!formData.name.trim()) {\n        errors.push(FORM_VALIDATION.NAME_REQUIRED)\n    }\n\n    if (!formData.task.type) {\n        errors.push(FORM_VALIDATION.TYPE_REQUIRED)\n    }\n\n    if (formData.task.timeout < CHECK_CONSTANTS.MIN_TIMEOUT || formData.task.timeout > CHECK_CONSTANTS.MAX_TIMEOUT) {\n        errors.push(FORM_VALIDATION.TIMEOUT_RANGE)\n    }\n\n    if (!validateCronExpr(formData.task.cron_expr)) {\n        errors.push(FORM_VALIDATION.CRON_INVALID)\n    }\n\n    if (formData.task.notify_channel < 1) {\n        errors.push(FORM_VALIDATION.NOTIFY_CHANNEL_MIN)\n    }\n\n    return {\n        isValid: errors.length === 0,\n        errors\n    }\n}\n\n\nexport function convertCheckResponseToRequest(check: CheckResponse): CheckRequest {\n    return {\n        name: check.name,\n        enable: check.enable,\n        task: {\n            type: check.task?.type || '',\n            timeout: check.task?.timeout || CHECK_CONSTANTS.DEFAULT_TIMEOUT,\n            cron_expr: check.task?.cron_expr || CHECK_CONSTANTS.DEFAULT_CRON,\n            notify: check.task?.notify ?? true,\n            notify_channel: check.task?.notify_channel || CHECK_CONSTANTS.DEFAULT_NOTIFY_CHANNEL,\n            log_write_file: check.task?.log_write_file ?? true,\n            log_level: check.task?.log_level || CHECK_CONSTANTS.DEFAULT_LOG_LEVEL,\n            sub_id: check.task?.sub_id || [],\n        },\n        config: check.config || {},\n    }\n}\n\n\n\n"
  },
  {
    "path": "web/src/components/features/home/dashboard.tsx",
    "content": "\"use client\"\n\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/src/components/ui/card\"\nimport { Construction } from \"lucide-react\"\n\nexport function DashboardPage() {\n  return (\n    <div className=\"flex items-center justify-center min-h-[60vh] p-4\">\n      <Card className=\"w-full max-w-md\">\n        <CardHeader className=\"text-center\">\n          <div className=\"mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-orange-100\">\n            <Construction className=\"h-8 w-8 text-orange-600\" />\n          </div>\n          <CardTitle className=\"text-xl\">仪表盘正在开发中</CardTitle>\n        </CardHeader>\n        <CardContent className=\"text-center\">\n          <p className=\"text-muted-foreground\">\n            我们正在努力完善仪表盘功能，敬请期待！\n          </p>\n          <p className=\"text-sm text-muted-foreground mt-2\">\n            您可以通过侧边栏访问其他功能模块\n          </p>\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/index.ts",
    "content": "export { SettingsDialog } from './settings/settings'\nexport { DashboardPage } from './home/dashboard'\nexport { StoragePage } from './storage/storage'\nexport { CheckPage } from './check'\nexport { SharePage } from './share/components/share-page'\nexport { NotifyPage } from './notify'\nexport { LoginPage } from './login'\nexport { SubPage } from './sub'\nexport { SystemUpdateDialog } from './system-update'"
  },
  {
    "path": "web/src/components/features/login/index.ts",
    "content": "export { LoginPage } from './login-page'\nexport { LoginForm } from './login-form'"
  },
  {
    "path": "web/src/components/features/login/login-form.tsx",
    "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { cn } from \"@/src/utils\"\nimport { Button } from \"@/src/components/ui/button\"\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from \"@/src/components/ui/card\"\nimport { Input } from \"@/src/components/ui/input\"\nimport { Label } from \"@/src/components/ui/label\"\nimport { ApiError } from \"@/src/lib/api/client\"\nimport { Spinner } from \"@/src/components/ui/loading\"\nimport { useAuth } from \"@/src/components/providers\"\n\nexport function LoginForm({\n    className,\n    ...props\n}: React.ComponentProps<\"div\">) {\n    const [username, setUsername] = useState(\"\")\n    const [password, setPassword] = useState(\"\")\n    const [error, setError] = useState(\"\")\n    const { login, isLoading } = useAuth()\n\n    const getErrorMessage = (error: unknown): string => {\n        if (error instanceof ApiError) {\n            switch (error.code) {\n                case 401:\n                    return \"用户名或密码错误\"\n                case 429:\n                    return \"登录尝试过于频繁，请稍后再试\"\n                case 500:\n                    return \"服务器错误，请稍后再试\"\n                default:\n                    return \"登录失败，请检查网络连接\"\n            }\n        }\n\n        if (error instanceof Error && error.message.includes('fetch')) {\n            return \"网络连接失败，请检查网络设置\"\n        }\n\n        return \"登录失败，请稍后再试\"\n    }\n\n    const handleSubmit = async (e: React.FormEvent) => {\n        e.preventDefault()\n        setError(\"\")\n\n        if (!username.trim() || !password) {\n            setError(\"请输入用户名和密码\")\n            return\n        }\n\n        try {\n            await login(username.trim(), password)\n        } catch (err) {\n            setError(getErrorMessage(err))\n        }\n    }\n\n    return (\n        <div className={cn(\"flex flex-col gap-6\", className)} {...props}>\n            <Card>\n                <CardHeader>\n                    <CardTitle>登录到您的账户</CardTitle>\n                    <CardDescription>\n                        输入您的用户名和密码来登录\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    <form onSubmit={handleSubmit}>\n                        <div className=\"flex flex-col gap-6\">\n                            {error && (\n                                <div className=\"p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md dark:bg-red-950 dark:text-red-400 dark:border-red-800\">\n                                    {error}\n                                </div>\n                            )}\n\n                            <div className=\"grid gap-3\">\n                                <Label htmlFor=\"username\">用户名</Label>\n                                <Input\n                                    id=\"username\"\n                                    type=\"text\"\n                                    placeholder=\"请输入用户名\"\n                                    value={username}\n                                    onChange={(e) => setUsername(e.target.value)}\n                                    disabled={isLoading}\n                                    required\n                                />\n                            </div>\n                            <div className=\"grid gap-3\">\n                                <div className=\"flex items-center\">\n                                    <Label htmlFor=\"password\">密码</Label>\n                                    <a\n                                        href=\"#\"\n                                        className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n                                    >\n                                        忘记密码？\n                                    </a>\n                                </div>\n                                <Input\n                                    id=\"password\"\n                                    type=\"password\"\n                                    placeholder=\"请输入密码\"\n                                    value={password}\n                                    onChange={(e) => setPassword(e.target.value)}\n                                    disabled={isLoading}\n                                    required\n                                />\n                            </div>\n                            <div className=\"flex flex-col gap-3\">\n                                <Button\n                                    type=\"submit\"\n                                    className=\"w-full\"\n                                    disabled={isLoading || !username.trim() || !password}\n                                >\n                                    {isLoading ? (\n                                        <>\n                                            <Spinner size=\"sm\" className=\"mr-2 border-white\" />\n                                            登录中...\n                                        </>\n                                    ) : (\n                                        \"登录\"\n                                    )}\n                                </Button>\n                            </div>\n                        </div>\n                    </form>\n                </CardContent>\n            </Card>\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/login/login-page.tsx",
    "content": "import { LoginForm } from \"./login-form\"\n\nexport function LoginPage() {\n    return (\n        <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n            <div className=\"w-full max-w-sm\">\n                <LoginForm />\n            </div>\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/notify/components/notify-form.tsx",
    "content": "import { Button } from \"@/src/components/ui/button\"\nimport { Input } from \"@/src/components/ui/input\"\nimport { Label } from \"@/src/components/ui/label\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/src/components/ui/select\"\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/src/components/ui/dialog\"\nimport { DynamicConfigForm } from \"@/src/components/shared/dynamic-config-form\"\nimport type { DynamicConfigItem, NotifyResponse, NotifyRequest } from \"@/src/types\"\n\ninterface NotifyFormProps {\n    formData: NotifyRequest\n    editingNotify: NotifyResponse | null\n    isDialogOpen: boolean\n    notifyChannels: string[]\n    channelConfigs: Record<string, DynamicConfigItem[]>\n    isLoadingChannels: boolean\n    isLoadingConfigs: boolean\n    updateFormField: (field: keyof NotifyRequest, value: string) => void\n    updateConfigField: (field: string, value: string | boolean | number) => void\n    handleChannelChange: (channel: string) => void\n    handleSubmit: (e: React.FormEvent) => void\n    onOpenChange: (open: boolean) => void\n}\n\nexport function NotifyForm({\n    formData,\n    editingNotify,\n    isDialogOpen,\n    notifyChannels,\n    channelConfigs,\n    isLoadingChannels,\n    isLoadingConfigs,\n    updateFormField,\n    updateConfigField,\n    handleChannelChange,\n    handleSubmit,\n    onOpenChange\n}: NotifyFormProps) {\n    const currentConfigs = formData.type ? channelConfigs[formData.type] || [] : []\n\n    return (\n        <Dialog open={isDialogOpen} onOpenChange={onOpenChange}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto scrollbar-hide\">\n                <DialogHeader>\n                    <DialogTitle>\n                        {editingNotify ? '编辑通知配置' : '创建通知配置'}\n                    </DialogTitle>\n                </DialogHeader>\n\n                <form onSubmit={handleSubmit} className=\"space-y-6\">\n                    <div>\n                        <Label htmlFor=\"name\" className=\"mb-2 block\">通知名称</Label>\n                        <Input\n                            id=\"name\"\n                            type=\"text\"\n                            value={formData.name}\n                            onChange={(e) => updateFormField('name', e.target.value)}\n                            placeholder=\"请输入通知名称\"\n                            required\n                        />\n                    </div>\n\n                    <div className=\"w-full\">\n                        <Label htmlFor=\"type\" className=\"mb-2 block\">通知渠道</Label>\n                        <Select\n                            value={formData.type}\n                            onValueChange={handleChannelChange}\n                            disabled={isLoadingChannels}\n                        >\n                            <SelectTrigger className=\"w-full\">\n                                <SelectValue placeholder={isLoadingChannels ? \"加载中...\" : \"请选择通知渠道\"} />\n                            </SelectTrigger>\n                            <SelectContent>\n                                {notifyChannels.map((channel) => (\n                                    <SelectItem key={channel} value={channel}>\n                                        {channel}\n                                    </SelectItem>\n                                ))}\n                            </SelectContent>\n                        </Select>\n                    </div>\n\n                    {formData.type && (\n                        <DynamicConfigForm\n                            configs={currentConfigs}\n                            configValues={formData.config}\n                            onConfigChange={updateConfigField}\n                            isLoading={isLoadingConfigs}\n                            typeName=\"通知渠道\"\n                        />\n                    )}\n\n                    <div className=\"flex gap-2 pt-4\">\n                        <Button type=\"submit\" className=\"flex-1\" disabled={isLoadingConfigs}>\n                            {editingNotify ? '更新' : '创建'}\n                        </Button>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={() => onOpenChange(false)}\n                        >\n                            取消\n                        </Button>\n                    </div>\n                </form>\n            </DialogContent>\n        </Dialog>\n    )\n} "
  },
  {
    "path": "web/src/components/features/notify/components/notify-list.tsx",
    "content": "import { Button } from \"@/src/components/ui/button\"\nimport { Table, TableBody, TableCell, TableRow } from \"@/src/components/ui/table\"\nimport { Card, CardContent } from \"@/src/components/ui/card\"\nimport { InlineLoading } from \"@/src/components/ui/loading\"\nimport type { NotifyResponse } from \"@/src/types\"\nimport { Play, Edit, Trash2 } from \"lucide-react\"\nimport { Badge } from \"@/src/components/ui/badge\"\n\ninterface NotifyListProps {\n    notifies: NotifyResponse[]\n    isLoading: boolean\n    deletingId: number | null\n    testingId: number | null\n    onEdit: (notify: NotifyResponse) => void\n    onDelete: (id: number, name: string) => void\n    onTest: (notify: NotifyResponse) => void\n}\n\nexport function NotifyList({\n    notifies,\n    isLoading,\n    deletingId,\n    testingId,\n    onEdit,\n    onDelete,\n    onTest\n}: NotifyListProps) {\n    if (isLoading) {\n        return (\n            <Card>\n                <CardContent>\n                    <InlineLoading message=\"加载通知...\" />\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (notifies.length === 0) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-muted-foreground\">\n                        暂无通知配置，点击上方按钮添加第一个通知配置\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    return (\n        <Card>\n            <CardContent>\n                <Table>\n                    <TableBody>\n                        {notifies.sort((a, b) => a.id - b.id).map((notify) => (\n                            <TableRow key={notify.id}>\n                                <TableCell className=\"font-medium\">{notify.name}</TableCell>\n                                <TableCell className=\"text-center\">\n                                    <Badge variant=\"outline\" className=\"text-xs w-fit\">\n                                        {notify.type?.toUpperCase() || 'N/A'}\n                                    </Badge>\n                                </TableCell>\n                                <TableCell className=\"text-right\">\n                                    <Button\n                                        variant=\"outline\"\n                                        size=\"sm\"\n                                        onClick={() => onTest(notify)}\n                                        disabled={testingId === notify.id}\n                                    >\n                                        <Play className={`h-4 w-4 ${testingId === notify.id ? 'animate-spin' : ''}`} />\n                                    </Button>\n                                    <Button\n                                        variant=\"outline\"\n                                        size=\"sm\"\n                                        onClick={() => onEdit(notify)}\n                                    >\n                                        <Edit className=\"h-4 w-4\" />\n                                    </Button>\n                                    <Button\n                                        variant=\"outline\"\n                                        size=\"sm\"\n                                        onClick={() => onDelete(notify.id, notify.name)}\n                                        className={deletingId === notify.id ? 'opacity-50' : ''}\n                                    >\n                                        {deletingId === notify.id ? (\n                                            <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n                                        ) : (\n                                            <Trash2 className=\"h-4 w-4\" />\n                                        )}\n                                    </Button>\n                                </TableCell>\n                            </TableRow>\n                        ))}\n                    </TableBody>\n                </Table>\n            </CardContent>\n        </Card>\n    )\n} "
  },
  {
    "path": "web/src/components/features/notify/components/notify-page.tsx",
    "content": "import { useState, useCallback, useEffect } from 'react'\nimport { Button } from \"@/src/components/ui/button\"\nimport { Plus } from \"lucide-react\"\nimport { api } from '@/src/lib/api/client'\nimport { NotifyForm } from './notify-form'\nimport { NotifyList } from './notify-list'\nimport { useNotifyForm } from '../hooks/useNotifyForm'\nimport { useNotifyOperations } from '../hooks/useNotifyOperations'\nimport type { NotifyResponse } from '@/src/types'\n\nexport function NotifyPage() {\n    const [notifies, setNotifies] = useState<NotifyResponse[]>([])\n    const [isLoading, setIsLoading] = useState(true)\n\n    const loadNotifies = useCallback(async () => {\n        try {\n            setIsLoading(true)\n            const data = await api.getNotifyList()\n            setNotifies(data)\n        } catch (error) {\n            console.error('Failed to load notifies:', error)\n        } finally {\n            setIsLoading(false)\n        }\n    }, [])\n\n    useEffect(() => {\n        loadNotifies()\n    }, [loadNotifies])\n\n    const {\n        formData,\n        notifyChannels,\n        channelConfigs,\n        isLoadingChannels,\n        isLoadingConfigs,\n        editingNotify,\n        isDialogOpen,\n        updateFormField,\n        updateConfigField,\n        handleChannelChange,\n        handleSubmit,\n        handleEdit,\n        openCreateDialog,\n        closeDialog\n    } = useNotifyForm({ onSuccess: loadNotifies })\n\n    const {\n        deletingId,\n        testingId,\n        handleDelete,\n        handleTest\n    } = useNotifyOperations()\n\n    const handleTestNotify = useCallback((notify: NotifyResponse) => {\n        handleTest({\n            name: notify.name,\n            type: notify.type,\n            config: notify.config\n        }, notify.id)\n    }, [handleTest])\n\n    return (\n        <div className=\"flex flex-col gap-4 py-4 md:gap-6 md:py-6\">\n            <div className=\"flex items-center justify-between px-4 lg:px-6\">\n                <div>\n                    <h1 className=\"text-2xl font-bold\">通知配置</h1>\n                </div>\n                <Button onClick={openCreateDialog}>\n                    <Plus className=\"h-4 w-4 mr-2\" />\n                    添加通知\n                </Button>\n            </div>\n\n            <NotifyForm\n                formData={formData}\n                editingNotify={editingNotify}\n                isDialogOpen={isDialogOpen}\n                notifyChannels={notifyChannels}\n                channelConfigs={channelConfigs}\n                isLoadingChannels={isLoadingChannels}\n                isLoadingConfigs={isLoadingConfigs}\n                updateFormField={updateFormField}\n                updateConfigField={updateConfigField}\n                handleChannelChange={handleChannelChange}\n                handleSubmit={handleSubmit}\n                onOpenChange={closeDialog}\n            />\n\n            <div className=\"px-4 lg:px-6\">\n                <NotifyList\n                    notifies={notifies}\n                    isLoading={isLoading}\n                    deletingId={deletingId}\n                    testingId={testingId}\n                    onEdit={handleEdit}\n                    onDelete={handleDelete}\n                    onTest={handleTestNotify}\n                />\n            </div>\n        </div>\n    )\n} "
  },
  {
    "path": "web/src/components/features/notify/hooks/useNotifyForm.ts",
    "content": "import { useState, useCallback } from 'react'\nimport { toast } from 'sonner'\nimport { api } from '@/src/lib/api/client'\nimport type { NotifyRequest, NotifyResponse, DynamicConfigItem } from '@/src/types'\n\n\nconst DEFAULT_FORM_DATA: NotifyRequest = {\n    name: '',\n    type: '',\n    config: {}\n}\n\ninterface UseNotifyFormProps {\n    onSuccess: () => void\n}\n\nexport function useNotifyForm({ onSuccess }: UseNotifyFormProps) {\n    const [formData, setFormData] = useState<NotifyRequest>(DEFAULT_FORM_DATA)\n    const [notifyChannels, setNotifyChannels] = useState<string[]>([])\n    const [channelConfigs, setChannelConfigs] = useState<Record<string, DynamicConfigItem[]>>({})\n    const [isLoadingChannels, setIsLoadingChannels] = useState(false)\n    const [isLoadingConfigs, setIsLoadingConfigs] = useState(false)\n    const [editingNotify, setEditingNotify] = useState<NotifyResponse | null>(null)\n    const [isDialogOpen, setIsDialogOpen] = useState(false)\n\n\n    const loadNotifyChannels = useCallback(async () => {\n        try {\n            setIsLoadingChannels(true)\n            const channels = await api.getNotifyChannels()\n            setNotifyChannels(channels)\n        } catch (error) {\n            console.error('Failed to load notify channels:', error)\n            toast.error('加载通知渠道失败')\n        } finally {\n            setIsLoadingChannels(false)\n        }\n    }, [])\n\n    const loadChannelConfig = useCallback(async (channel: string) => {\n        try {\n            setIsLoadingConfigs(true)\n            const configs = await api.getNotifyChannelConfig(channel)\n            if (Array.isArray(configs)) {\n                setChannelConfigs(prev => ({ ...prev, [channel]: configs }))\n            }\n        } catch (error) {\n            console.error('Failed to load channel config:', error)\n            toast.error('加载渠道配置失败')\n        } finally {\n            setIsLoadingConfigs(false)\n        }\n    }, [])\n\n    const handleChannelChange = useCallback(async (channel: string) => {\n        setFormData(prev => ({ ...prev, type: channel, config: {} }))\n\n        if (channel && !channelConfigs[channel]) {\n            await loadChannelConfig(channel)\n        }\n    }, [channelConfigs, loadChannelConfig])\n\n    const updateFormField = useCallback((field: keyof NotifyRequest, value: string) => {\n        setFormData(prev => ({ ...prev, [field]: value }))\n    }, [])\n\n    const updateConfigField = useCallback((field: string, value: string | boolean | number) => {\n        setFormData(prev => ({\n            ...prev,\n            config: { ...prev.config, [field]: value }\n        }))\n    }, [])\n\n    const handleSubmit = useCallback(async (e: React.FormEvent) => {\n        e.preventDefault()\n\n        if (!formData.name.trim()) {\n            toast.error('请输入通知名称')\n            return\n        }\n\n        if (!formData.type) {\n            toast.error('请选择通知渠道')\n            return\n        }\n\n        try {\n            // 处理配置数据，将空值替换为 default 值\n            const processedConfig = { ...formData.config }\n            const configs = channelConfigs[formData.type] || []\n\n            configs.forEach(config => {\n                const value = processedConfig[config.key]\n                // 如果值为空且有 default 值，则使用 default 值\n                if ((value === undefined || value === '' || value === null) && config.value) {\n                    processedConfig[config.key] = config.value\n                }\n            })\n\n            const requestData: NotifyRequest = {\n                name: formData.name.trim(),\n                type: formData.type,\n                config: processedConfig\n            }\n\n            if (editingNotify) {\n                await api.updateNotify(editingNotify.id, requestData)\n                toast.success('通知配置更新成功')\n            } else {\n                await api.createNotify(requestData)\n                toast.success('通知配置创建成功')\n            }\n\n            setFormData(DEFAULT_FORM_DATA)\n            setEditingNotify(null)\n            setIsDialogOpen(false)\n            onSuccess()\n        } catch (error) {\n            console.error('Failed to submit notify form:', error)\n            toast.error(editingNotify ? '更新通知配置失败' : '创建通知配置失败')\n        }\n    }, [formData, editingNotify, onSuccess, channelConfigs])\n\n    const handleEdit = useCallback((notify: NotifyResponse) => {\n        setFormData({\n            name: notify.name,\n            type: notify.type,\n            config: notify.config\n        })\n        setEditingNotify(notify)\n        setIsDialogOpen(true)\n\n        // 确保渠道配置已加载\n        if (notify.type && !channelConfigs[notify.type]) {\n            loadChannelConfig(notify.type)\n        }\n    }, [channelConfigs, loadChannelConfig])\n\n    const openCreateDialog = useCallback(async () => {\n        if (notifyChannels.length === 0) {\n            await loadNotifyChannels()\n        }\n        setFormData(DEFAULT_FORM_DATA)\n        setEditingNotify(null)\n        setIsDialogOpen(true)\n    }, [notifyChannels.length, loadNotifyChannels])\n\n    const closeDialog = useCallback(() => {\n        setFormData(DEFAULT_FORM_DATA)\n        setEditingNotify(null)\n        setIsDialogOpen(false)\n    }, [])\n\n    return {\n        formData,\n        notifyChannels,\n        channelConfigs,\n        isLoadingChannels,\n        isLoadingConfigs,\n        editingNotify,\n        isDialogOpen,\n        updateFormField,\n        updateConfigField,\n        handleChannelChange,\n        handleSubmit,\n        handleEdit,\n        openCreateDialog,\n        closeDialog\n    }\n} "
  },
  {
    "path": "web/src/components/features/notify/hooks/useNotifyOperations.ts",
    "content": "import { useState, useCallback } from 'react'\nimport { toast } from 'sonner'\nimport { useAlert } from '@/src/components/providers'\nimport { api } from '@/src/lib/api/client'\nimport type { NotifyRequest } from '@/src/types'\n\nexport function useNotifyOperations() {\n    const { confirm } = useAlert()\n    const [testingId, setTestingId] = useState<number | null>(null)\n    const [deletingId, setDeletingId] = useState<number | null>(null)\n\n    const handleTest = useCallback(async (notify: NotifyRequest, id?: number) => {\n        try {\n            setTestingId(id || 0)\n            await api.testNotify(notify)\n            toast.success('通知测试成功')\n        } catch (error) {\n            console.error('Failed to test notify:', error)\n            toast.error('通知测试失败')\n        } finally {\n            setTestingId(null)\n        }\n    }, [])\n\n    const handleDelete = useCallback(async (id: number, name: string) => {\n        const confirmed = await confirm({\n            title: '删除通知',\n            description: `确定要删除通知配置 \"${name}\" 吗？`,\n            confirmText: '删除',\n            cancelText: '取消',\n            variant: 'destructive'\n        })\n\n        if (confirmed) {\n            try {\n                setDeletingId(id)\n                await api.deleteNotify(id)\n                toast.success('删除成功')\n            } catch (error) {\n                console.error('Failed to delete notify:', error)\n                toast.error('删除失败')\n            } finally {\n                setDeletingId(null)\n            }\n        }\n    }, [confirm])\n\n    return {\n        deletingId,\n        testingId,\n        handleDelete,\n        handleTest\n    }\n} "
  },
  {
    "path": "web/src/components/features/notify/index.ts",
    "content": "export { NotifyPage } from './components/notify-page'\nexport { NotifyForm } from './components/notify-form'\nexport { NotifyList } from './components/notify-list'\nexport { useNotifyForm } from './hooks/useNotifyForm'\nexport { useNotifyOperations } from './hooks/useNotifyOperations' "
  },
  {
    "path": "web/src/components/features/profile/ProfileDesktopNavButton.tsx",
    "content": "import * as React from \"react\"\nimport { cn } from \"@/src/utils\"\n\ninterface ProfileDesktopNavButtonProps {\n  tabId: string\n  activeTab: string\n  onTabChange: (id: string) => void\n  children: React.ReactNode\n}\n\nexport function ProfileDesktopNavButton({ tabId, activeTab, onTabChange, children }: ProfileDesktopNavButtonProps) {\n  const handleClick = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    onTabChange(tabId)\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      className={cn(\n        \"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors\",\n        activeTab === tabId\n          ? \"bg-primary text-primary-foreground shadow-sm\"\n          : \"hover:bg-muted/70 text-muted-foreground hover:text-foreground\"\n      )}\n    >\n      <span>{children}</span>\n    </button>\n  )\n}"
  },
  {
    "path": "web/src/components/features/profile/ProfileDialog.tsx",
    "content": "\"use client\"\n\nimport { useState, useEffect, useCallback } from \"react\"\nimport { useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\n\nimport {\n  Dialog,\n  DialogContent,\n} from \"@/src/components/ui/dialog\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Input } from \"@/src/components/ui/input\"\nimport { Label } from \"@/src/components/ui/label\"\nimport { ProfileLayout } from \"./ProfileLayout\"\nimport { InlineLoading } from \"@/src/components/ui/loading\"\nimport { api } from \"@/src/lib/api/client\"\nimport { useAuth } from \"@/src/components/providers\"\n\ninterface ProfileDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\ninterface FormData {\n  username: string\n  oldPassword: string\n  newPassword: string\n  confirmPassword: string\n}\n\nexport function ProfileDialog({ open, onOpenChange }: ProfileDialogProps) {\n  const { user, logout, updateUser } = useAuth()\n  const [activeTab, setActiveTab] = useState(\"profile\")\n  const [isSubmitting, setIsSubmitting] = useState(false)\n\n  const form = useForm<FormData>({\n    mode: 'onChange',\n    defaultValues: {\n      username: user?.username || \"\",\n      oldPassword: \"\",\n      newPassword: \"\",\n      confirmPassword: \"\"\n    }\n  })\n\n\n  useEffect(() => {\n    if (open && user) {\n      form.reset({\n        username: user.username,\n        oldPassword: \"\",\n        newPassword: \"\",\n        confirmPassword: \"\"\n      })\n    }\n  }, [open, user])\n\n  const handleUpdateUsername = useCallback(async (data: FormData) => {\n    if (!data.username.trim()) {\n      toast.error(\"用户名不能为空\")\n      return\n    }\n\n    if (data.username === user?.username) {\n      toast.error(\"新用户名不能与当前用户名相同\")\n      return\n    }\n\n    setIsSubmitting(true)\n    try {\n      await api.updateUsername({ username: data.username })\n      updateUser({ ...user!, username: data.username })\n      toast.success(\"用户名修改成功\")\n      onOpenChange(false)\n    } catch (error: any) {\n      toast.error(error.message || \"用户名修改失败\")\n    } finally {\n      setIsSubmitting(false)\n    }\n  }, [user, updateUser, onOpenChange])\n\n  const handleChangePassword = useCallback(async (data: FormData) => {\n    if (!data.oldPassword || !data.newPassword) {\n      toast.error(\"请填写完整密码信息\")\n      return\n    }\n\n    if (data.newPassword !== data.confirmPassword) {\n      toast.error(\"两次输入的新密码不一致\")\n      return\n    }\n\n    if (data.newPassword.length < 6) {\n      toast.error(\"新密码长度至少为6位\")\n      return\n    }\n\n    setIsSubmitting(true)\n    try {\n      await api.changePassword({\n        username: user!.username,\n        old_password: data.oldPassword,\n        new_password: data.newPassword\n      })\n      toast.success(\"密码修改成功，请重新登录\")\n      onOpenChange(false)\n      // 密码修改成功后调用登出\n      setTimeout(() => {\n        logout()\n      }, 1000)\n    } catch (error: any) {\n      toast.error(error.message || \"密码修改失败\")\n    } finally {\n      setIsSubmitting(false)\n    }\n  }, [user, logout, onOpenChange])\n\n  const handleSubmit = useCallback(async (e: React.FormEvent) => {\n    e.preventDefault()\n\n    const data = form.getValues()\n\n    if (activeTab === \"profile\") {\n      await handleUpdateUsername(data)\n    } else if (activeTab === \"password\") {\n      await handleChangePassword(data)\n    }\n  }, [activeTab, form, handleUpdateUsername, handleChangePassword])\n\n  const renderContent = () => {\n    switch (activeTab) {\n      case \"profile\":\n        return (\n          <div className=\"space-y-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"username\">用户名</Label>\n              <Input\n                id=\"username\"\n                {...form.register(\"username\", { required: true })}\n                placeholder=\"请输入用户名\"\n              />\n              {form.formState.errors.username && (\n                <p className=\"text-sm text-destructive\">用户名不能为空</p>\n              )}\n            </div>\n          </div>\n        )\n\n      case \"password\":\n        return (\n          <div className=\"space-y-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"oldPassword\">当前密码</Label>\n              <Input\n                id=\"oldPassword\"\n                type=\"password\"\n                {...form.register(\"oldPassword\", { required: true })}\n                placeholder=\"请输入当前密码\"\n              />\n            </div>\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"newPassword\">新密码</Label>\n              <Input\n                id=\"newPassword\"\n                type=\"password\"\n                {...form.register(\"newPassword\", {\n                  required: true,\n                  minLength: 6\n                })}\n                placeholder=\"请输入新密码（至少6位）\"\n              />\n              {form.formState.errors.newPassword && (\n                <p className=\"text-sm text-destructive\">密码长度至少为6位</p>\n              )}\n            </div>\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"confirmPassword\">确认新密码</Label>\n              <Input\n                id=\"confirmPassword\"\n                type=\"password\"\n                {...form.register(\"confirmPassword\", {\n                  required: true,\n                  validate: (value) => value === form.getValues(\"newPassword\") || \"两次输入的密码不一致\"\n                })}\n                placeholder=\"请再次输入新密码\"\n              />\n              {form.formState.errors.confirmPassword && (\n                <p className=\"text-sm text-destructive\">{form.formState.errors.confirmPassword.message}</p>\n              )}\n            </div>\n          </div>\n        )\n\n      default:\n        return null\n    }\n  }\n\n  const renderActions = (isMobile?: boolean) => {\n    const isDirty = form.formState.isDirty\n    const canSubmit = activeTab === \"profile\"\n      ? isDirty && form.getValues(\"username\").trim() !== user?.username\n      : isDirty && form.getValues(\"newPassword\") === form.getValues(\"confirmPassword\") && form.getValues(\"newPassword\").length >= 6\n\n    return (\n      <>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          onClick={() => onOpenChange(false)}\n          className={isMobile ? \"flex-1 h-9 sm:h-10 text-sm\" : \"h-10\"}\n        >\n          取消\n        </Button>\n        <Button\n          type=\"submit\"\n          disabled={!canSubmit || isSubmitting}\n          className={isMobile ? \"flex-1 h-9 sm:h-10 text-sm\" : \"h-10\"}\n        >\n          {isSubmitting ? <InlineLoading size=\"sm\" /> : \"保存\"}\n        </Button>\n      </>\n    )\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"overflow-hidden p-0 md:max-h-[600px] md:max-w-[800px] lg:max-w-[900px] max-h-[90vh] h-full md:h-auto w-[95vw] sm:w-[90vw] md:w-full\">\n        <ProfileLayout\n          activeTab={activeTab}\n          onTabChange={setActiveTab}\n          onSubmit={handleSubmit}\n          renderActions={renderActions}\n        >\n          {renderContent()}\n        </ProfileLayout>\n      </DialogContent>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/profile/ProfileLayout.tsx",
    "content": "import * as React from \"react\"\nimport { ProfileNavButton } from \"./ProfileNavButton\"\nimport { ProfileDesktopNavButton } from \"./ProfileDesktopNavButton\"\n\ninterface ProfileLayoutProps {\n  activeTab: string\n  onTabChange: (id: string) => void\n  children: React.ReactNode\n  renderActions: (isMobile?: boolean) => React.ReactNode\n  onSubmit: (e: React.FormEvent) => void\n}\n\nexport function ProfileLayout({\n  activeTab,\n  onTabChange,\n  children,\n  renderActions,\n  onSubmit\n}: ProfileLayoutProps) {\n\n  return (\n    <form onSubmit={onSubmit} className=\"flex h-full\">\n      <div className=\"md:hidden flex flex-col h-full max-h-[90vh] w-full\">\n        <div className=\"border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex-shrink-0\">\n          <div className=\"p-3 sm:p-4\">\n            <h2 className=\"text-lg sm:text-xl font-semibold mb-3 sm:mb-4\">个人资料</h2>\n            <div className=\"relative\">\n              <div className=\"flex overflow-x-auto gap-1.5 sm:gap-2 scrollbar-hide\">\n                <ProfileNavButton tabId=\"profile\" activeTab={activeTab} onTabChange={onTabChange}>\n                  个人资料\n                </ProfileNavButton>\n                <ProfileNavButton tabId=\"password\" activeTab={activeTab} onTabChange={onTabChange}>\n                  修改密码\n                </ProfileNavButton>\n              </div>\n              <div className=\"absolute right-0 top-0 h-full w-6 sm:w-8 bg-gradient-to-l from-background/95 via-background/80 to-transparent pointer-events-none\"></div>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex-1 overflow-y-auto min-h-0 bg-muted/20\">\n          <div className=\"p-3 sm:p-4 space-y-4\">\n            {children}\n          </div>\n        </div>\n\n        <div className=\"border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 p-3 sm:p-4 flex-shrink-0\">\n          <div className=\"flex gap-2 sm:gap-3\">\n            {renderActions(true)}\n          </div>\n        </div>\n      </div>\n\n      <div className=\"hidden md:flex w-full h-full\">\n        <div className=\"flex w-full\">\n          <div className=\"w-64 flex-shrink-0 border-r bg-muted/5\">\n            <div className=\"p-6 pt-6\">\n              <h2 className=\"text-lg font-semibold mb-4\">个人资料</h2>\n              <nav className=\"space-y-1\">\n                <ProfileDesktopNavButton tabId=\"profile\" activeTab={activeTab} onTabChange={onTabChange}>\n                  个人资料\n                </ProfileDesktopNavButton>\n                <ProfileDesktopNavButton tabId=\"password\" activeTab={activeTab} onTabChange={onTabChange}>\n                  修改密码\n                </ProfileDesktopNavButton>\n              </nav>\n            </div>\n          </div>\n\n          <main className=\"flex h-[600px] flex-1 flex-col overflow-hidden bg-background\">\n            <div className=\"flex-1 overflow-y-auto p-6 pt-12\">\n              {children}\n            </div>\n\n            <div className=\"border-t bg-muted/10 p-6 flex-shrink-0\">\n              <div className=\"flex justify-end gap-3\">\n                {renderActions(false)}\n              </div>\n            </div>\n          </main>\n        </div>\n      </div>\n    </form>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/profile/ProfileNavButton.tsx",
    "content": "import * as React from \"react\"\nimport { cn } from \"@/src/utils\"\n\ninterface ProfileNavButtonProps {\n  tabId: string\n  activeTab: string\n  onTabChange: (id: string) => void\n  children: React.ReactNode\n}\n\nexport function ProfileNavButton({ tabId, activeTab, onTabChange, children }: ProfileNavButtonProps) {\n  const handleClick = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    onTabChange(tabId)\n  }\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      className={cn(\n        \"flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 text-xs sm:text-sm rounded-lg transition-all whitespace-nowrap flex-shrink-0\",\n        activeTab === tabId\n          ? \"bg-primary text-primary-foreground shadow-sm\"\n          : \"hover:bg-muted/70\"\n      )}\n    >\n      <span className=\"truncate max-w-20 sm:max-w-24\">{children}</span>\n    </button>\n  )\n}"
  },
  {
    "path": "web/src/components/features/profile/index.ts",
    "content": "export { ProfileDialog } from './ProfileDialog'"
  },
  {
    "path": "web/src/components/features/settings/SettingsActions.tsx",
    "content": "import * as React from \"react\"\nimport { Button } from \"@/src/components/ui/button\"\n\ninterface SettingsActionsProps {\n  onCancel: () => void\n  isMobile?: boolean\n  hasChanges?: boolean\n}\n\nexport function SettingsActions({ onCancel, isMobile, hasChanges }: SettingsActionsProps) {\n  return (\n    <>\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        onClick={onCancel}\n        className={isMobile ? \"flex-1 h-9 sm:h-10 text-sm\" : \"h-10\"}\n      >\n        取消\n      </Button>\n      <Button\n        type=\"submit\"\n        disabled={!hasChanges}\n        className={isMobile ? \"flex-1 h-9 sm:h-10 text-sm\" : \"h-10\"}\n      >\n        保存设置\n      </Button>\n    </>\n  )\n}"
  },
  {
    "path": "web/src/components/features/settings/SettingsLayout.tsx",
    "content": "import * as React from \"react\"\nimport { cn } from \"@/src/utils\"\n\ninterface SettingsLayoutProps {\n  nav: Array<{ name: string; id: string }>\n  activeTab: string\n  onTabChange: (id: string) => void\n  children: React.ReactNode\n  renderActions: (isMobile: boolean) => React.ReactNode\n  onSubmit: (e: React.FormEvent) => void\n}\n\nexport function SettingsLayout({\n  nav,\n  activeTab,\n  onTabChange,\n  children,\n  renderActions,\n  onSubmit\n}: SettingsLayoutProps) {\n  const handleTabClick = (e: React.MouseEvent, itemId: string) => {\n    e.preventDefault()\n    e.stopPropagation()\n    onTabChange(itemId)\n  }\n\n  return (\n    <form onSubmit={onSubmit} className=\"flex h-full\">\n      <div className=\"md:hidden flex flex-col h-full max-h-[90vh] w-full\">\n        <div className=\"border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex-shrink-0\">\n          <div className=\"p-3 sm:p-4\">\n            <h2 className=\"text-lg sm:text-xl font-semibold mb-3 sm:mb-4\">设置</h2>\n            <div className=\"relative\">\n              <div className=\"flex overflow-x-auto gap-1.5 sm:gap-2 scrollbar-hide\">\n                {nav.map((item) => (\n                  <button\n                    key={item.name}\n                    type=\"button\"\n                    onClick={(e) => handleTabClick(e, item.id)}\n                    className={cn(\n                      \"flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 text-xs sm:text-sm rounded-lg transition-all whitespace-nowrap flex-shrink-0\",\n                      activeTab === item.id\n                        ? \"bg-primary text-primary-foreground shadow-sm\"\n                        : \"hover:bg-muted/70\"\n                    )}\n                  >\n                    <span className=\"truncate max-w-20 sm:max-w-24\">{item.name}</span>\n                  </button>\n                ))}\n              </div>\n              <div className=\"absolute right-0 top-0 h-full w-6 sm:w-8 bg-gradient-to-l from-background/95 via-background/80 to-transparent pointer-events-none\"></div>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex-1 overflow-y-auto min-h-0 bg-muted/20\">\n          <div className=\"p-3 sm:p-4 space-y-4\">\n            {children}\n          </div>\n        </div>\n\n        <div className=\"border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 p-3 sm:p-4 flex-shrink-0\">\n          <div className=\"flex gap-2 sm:gap-3\">\n            {renderActions(true)}\n          </div>\n        </div>\n      </div>\n\n      <div className=\"hidden md:flex w-full h-full\">\n        <div className=\"flex w-full\">\n          <div className=\"w-64 flex-shrink-0 border-r bg-muted/5\">\n            <div className=\"p-6 pt-6\">\n              <h2 className=\"text-lg font-semibold mb-4\">设置</h2>\n              <nav className=\"space-y-1\">\n                {nav.map((item) => (\n                  <button\n                    key={item.name}\n                    type=\"button\"\n                    onClick={(e) => handleTabClick(e, item.id)}\n                    className={cn(\n                      \"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors\",\n                      activeTab === item.id\n                        ? \"bg-primary text-primary-foreground shadow-sm\"\n                        : \"hover:bg-muted/70 text-muted-foreground hover:text-foreground\"\n                    )}\n                  >\n                    <span>{item.name}</span>\n                  </button>\n                ))}\n              </nav>\n            </div>\n          </div>\n\n          <main className=\"flex h-[600px] flex-1 flex-col overflow-hidden bg-background\">\n            <div className=\"flex-1 overflow-y-auto p-6 pt-12\">\n              {children}\n            </div>\n\n            <div className=\"border-t bg-muted/10 p-3 flex-shrink-0\">\n              <div className=\"flex justify-end gap-3\">\n                {renderActions(false)}\n              </div>\n            </div>\n          </main>\n        </div>\n      </div>\n    </form>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/NodeSettingsSection.tsx",
    "content": "import { Controller, useWatch, type Control } from \"react-hook-form\"\nimport type { FormValues } from \"@/src/types/setting\"\nimport { BooleanSettingField } from \"./fields/BooleanSettingField\"\nimport { MultiSelectSettingField } from \"./fields/MultiSelectSettingField\"\nimport { NumberSettingField } from \"./fields/NumberSettingField\"\nimport { TextSettingField } from \"./fields/TextSettingField\"\nimport { PROTOCOL_OPTIONS } from \"@/src/constant/protocols\"\nimport {\n  NODE_POOL_SIZE,\n  NODE_PROTOCOL_FILTER,\n  NODE_PROTOCOL_FILTER_ENABLE,\n  NODE_PROTOCOL_FILTER_MODE,\n  NODE_TEST_TIMEOUT,\n  NODE_TEST_URL,\n} from \"@/src/constant/settings-keys\"\n\nexport function NodeSettingsSection({ control }: { control: Control<FormValues> }) {\n  const protocolFilterEnabled = Boolean(useWatch({ control, name: NODE_PROTOCOL_FILTER_ENABLE }))\n\n  return (\n    <div className=\"space-y-4\">\n      <Controller\n        name={NODE_POOL_SIZE}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"节点池大小\"\n            value={field.value}\n            onChange={field.onChange}\n            min={0}\n          />\n        )}\n      />\n\n      <Controller\n        name={NODE_TEST_URL}\n        control={control}\n        render={({ field }) => (\n          <TextSettingField\n            title=\"默认测试地址\"\n            value={String(field.value ?? \"\")}\n            onChange={field.onChange}\n          />\n        )}\n      />\n\n      <Controller\n        name={NODE_TEST_TIMEOUT}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"默认测试超时时间（秒）\"\n            value={field.value}\n            onChange={field.onChange}\n            min={1}\n          />\n        )}\n      />\n\n      <Controller\n        name={NODE_PROTOCOL_FILTER_ENABLE}\n        control={control}\n        render={({ field }) => (\n          <BooleanSettingField\n            title=\"全局协议过滤启用\"\n            description=\"是否启用全局协议过滤\"\n            checked={Boolean(field.value)}\n            onCheckedChange={field.onChange}\n          />\n        )}\n      />\n\n      {protocolFilterEnabled && (\n        <>\n          <Controller\n            name={NODE_PROTOCOL_FILTER_MODE}\n            control={control}\n            render={({ field }) => (\n          <BooleanSettingField\n            title=\"全局协议过滤模式\"\n            description=\"关闭为排除,打开为包含\"\n            checked={Boolean(field.value)}\n            onCheckedChange={field.onChange}\n          />\n            )}\n          />\n\n          <Controller\n            name={NODE_PROTOCOL_FILTER}\n            control={control}\n            render={({ field }) => (\n              <MultiSelectSettingField\n                title=\"全局协议过滤\"\n                value={Array.isArray(field.value) ? field.value : []}\n                options={PROTOCOL_OPTIONS}\n                onChange={field.onChange}\n              />\n            )}\n          />\n        </>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/NotifySettingsSection.tsx",
    "content": "import { Controller, type Control } from \"react-hook-form\"\nimport type { FormValues } from \"@/src/types/setting\"\nimport { NumberSettingField } from \"./fields/NumberSettingField\"\nimport { NOTIFY_ID, NOTIFY_OPERATION } from \"@/src/constant/settings-keys\"\n\nexport function NotifySettingsSection({ control }: { control: Control<FormValues> }) {\n  return (\n    <div className=\"space-y-4\">\n      <Controller\n        name={NOTIFY_OPERATION}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"需要通知的操作类型\"\n            value={field.value}\n            onChange={field.onChange}\n            min={0}\n          />\n        )}\n      />\n\n      <Controller\n        name={NOTIFY_ID}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"系统默认通知渠道\"\n            value={field.value}\n            onChange={field.onChange}\n            min={0}\n          />\n        )}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/SystemSettingsSection.tsx",
    "content": "import { Controller, useWatch, type Control } from \"react-hook-form\"\nimport type { FormValues } from \"@/src/types/setting\"\nimport {\n  LOG_RETENTION_DAYS,\n  PROXY_ENABLE,\n  PROXY_URL,\n  SUBCONV_URL,\n  SUBCONV_URL_PROXY,\n  SUB_DISABLE_AUTO,\n} from \"@/src/constant/settings-keys\"\nimport { BooleanSettingField } from \"./fields/BooleanSettingField\"\nimport { NumberSettingField } from \"./fields/NumberSettingField\"\nimport { TextSettingField } from \"./fields/TextSettingField\"\n\nexport function SystemSettingsSection({ control }: { control: Control<FormValues> }) {\n  const proxyEnabled = Boolean(useWatch({ control, name: PROXY_ENABLE }))\n\n  return (\n    <div className=\"space-y-4\">\n      <Controller\n        name={PROXY_ENABLE}\n        control={control}\n        render={({ field }) => (\n          <BooleanSettingField\n            title=\"代理\"\n            description=\"是否启用代理\"\n            checked={Boolean(field.value)}\n            onCheckedChange={field.onChange}\n          />\n        )}\n      />\n\n      {proxyEnabled && (\n        <Controller\n          name={PROXY_URL}\n          control={control}\n          render={({ field }) => (\n            <TextSettingField\n              title=\"代理地址\"\n              value={String(field.value ?? \"\")}\n              onChange={field.onChange}\n            />\n          )}\n        />\n      )}\n\n      <Controller\n        name={LOG_RETENTION_DAYS}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"日志保留天数\"\n            value={field.value}\n            onChange={field.onChange}\n            min={0}\n          />\n        )}\n      />\n\n      <Controller\n        name={SUBCONV_URL}\n        control={control}\n        render={({ field }) => (\n          <TextSettingField\n            title=\"外部订阅转换地址\"\n            description={\n              <>\n                使用{\" \"}\n                <a\n                  href=\"https://github.com/bestruirui/SubWorker\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"text-blue-500 hover:underline\"\n                >\n                  SubWorker\n                </a>{\" \"}\n                或已有的 SubStore 地址, 示例: http://ip:port/xxxxxxx\n              </>\n            }\n            value={String(field.value ?? \"\")}\n            onChange={field.onChange}\n          />\n        )}\n      />\n\n      <Controller\n        name={SUBCONV_URL_PROXY}\n        control={control}\n        render={({ field }) => (\n          <BooleanSettingField\n            title=\"外部订阅转换代理\"\n            description=\"是否启用代理访问外部订阅转换\"\n            checked={Boolean(field.value)}\n            onCheckedChange={field.onChange}\n          />\n        )}\n      />\n\n      <Controller\n        name={SUB_DISABLE_AUTO}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"自动禁用订阅\"\n            description=\"当订阅获取节点数量为0的次数大于该值时,自动禁用订阅,0为不自动禁用\"\n            value={field.value}\n            onChange={field.onChange}\n            min={0}\n          />\n        )}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/TaskSettingsSection.tsx",
    "content": "import { Controller, type Control } from \"react-hook-form\"\nimport type { FormValues } from \"@/src/types/setting\"\nimport { NumberSettingField } from \"./fields/NumberSettingField\"\nimport { TASK_MAX_RETRY, TASK_MAX_THREAD, TASK_MAX_TIMEOUT } from \"@/src/constant/settings-keys\"\n\nexport function TaskSettingsSection({ control }: { control: Control<FormValues> }) {\n  return (\n    <div className=\"space-y-4\">\n      <Controller\n        name={TASK_MAX_THREAD}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"最大线程数\"\n            value={field.value}\n            onChange={field.onChange}\n            min={1}\n          />\n        )}\n      />\n\n      <Controller\n        name={TASK_MAX_TIMEOUT}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"任务最大超时时间（秒）\"\n            value={field.value}\n            onChange={field.onChange}\n            min={1}\n          />\n        )}\n      />\n\n      <Controller\n        name={TASK_MAX_RETRY}\n        control={control}\n        render={({ field }) => (\n          <NumberSettingField\n            title=\"任务最大重试次数\"\n            value={field.value}\n            onChange={field.onChange}\n            min={0}\n          />\n        )}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/BooleanSettingField.tsx",
    "content": "import type { BaseSettingProps } from \"./types\"\nimport { SettingCard } from \"./SettingCard\"\nimport { Switch } from \"@/src/components/ui/switch\"\n\ninterface BooleanSettingFieldProps extends BaseSettingProps {\n  checked: boolean\n  onCheckedChange: (checked: boolean) => void\n}\n\nexport function BooleanSettingField({\n  title,\n  description,\n  checked,\n  onCheckedChange,\n}: BooleanSettingFieldProps) {\n  return (\n    <SettingCard\n      title={title}\n      description={description}\n      action={<Switch checked={checked} onCheckedChange={onCheckedChange} />}\n      actionAlignment=\"center\"\n    />\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/MultiSelectSettingField.tsx",
    "content": "import { Badge } from \"@/src/components/ui/badge\"\nimport type { BaseSettingProps } from \"./types\"\nimport { SettingCard } from \"./SettingCard\"\n\ntype MultiSelectSettingFieldProps = BaseSettingProps & {\n  value: string[]\n  options: string[]\n  onChange: (value: string[]) => void\n}\n\nexport function MultiSelectSettingField({\n  title,\n  description,\n  value,\n  options,\n  onChange,\n}: MultiSelectSettingFieldProps) {\n  const toggleValue = (option: string) => {\n    const exists = value.includes(option)\n    const nextValues = exists\n      ? value.filter((item) => item !== option)\n      : [...value, option]\n\n    const ordered = options.filter((item) => nextValues.includes(item))\n    onChange(ordered)\n  }\n\n  return (\n    <SettingCard title={title} description={description}>\n      <div className=\"flex flex-wrap gap-2\">\n        {options.length === 0 && (\n          <p className=\"text-xs text-muted-foreground\">暂无可选项</p>\n        )}\n        {options.map((option) => {\n          const active = value.includes(option)\n\n          return (\n            <Badge\n              key={option}\n              variant={active ? \"default\" : \"outline\"}\n              className={`cursor-pointer transition-colors ${\n                active\n                  ? \"hover:bg-red-100 hover:text-red-700\"\n                  : \"hover:bg-green-100 hover:text-green-700\"\n              }`}\n              onClick={() => toggleValue(option)}\n            >\n              {option} {active ? \"×\" : \"+\"}\n            </Badge>\n          )\n        })}\n      </div>\n    </SettingCard>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/NumberSettingField.tsx",
    "content": "import { Input } from \"@/src/components/ui/input\"\nimport type { BaseSettingProps } from \"./types\"\nimport { SettingCard } from \"./SettingCard\"\n\ninterface NumberSettingFieldProps extends BaseSettingProps {\n  value: unknown\n  onChange: (value: number | string) => void\n  min?: number\n}\n\nexport function NumberSettingField({\n  title,\n  description,\n  value,\n  onChange,\n  min,\n}: NumberSettingFieldProps) {\n  const inputValue = getNumberInputValue(value)\n\n  return (\n    <SettingCard title={title} description={description}>\n      <Input\n        type=\"number\"\n        value={inputValue}\n        min={min}\n        onChange={(event) => {\n          const nextValue = event.target.value\n          if (nextValue === \"\") {\n            onChange(\"\")\n            return\n          }\n          onChange(Number(nextValue))\n        }}\n        className=\"h-10\"\n      />\n    </SettingCard>\n  )\n}\n\nconst getNumberInputValue = (value: unknown): string | number => {\n  if (typeof value === \"number\") {\n    return Number.isNaN(value) ? \"\" : value\n  }\n\n  if (typeof value === \"string\") {\n    return value\n  }\n\n  return \"\"\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/SelectSettingField.tsx",
    "content": "import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/src/components/ui/select\"\nimport type { BaseSettingProps } from \"./types\"\nimport { SettingCard } from \"./SettingCard\"\n\ntype SelectSettingFieldProps = BaseSettingProps & {\n  value: string\n  options: Array<{ label: string; value: string }>\n  onChange: (value: string) => void\n  placeholder?: string\n}\n\nexport function SelectSettingField({\n  title,\n  description,\n  value,\n  options,\n  onChange,\n  placeholder = \"选择选项\",\n}: SelectSettingFieldProps) {\n  return (\n    <SettingCard title={title} description={description}>\n      <Select value={value} onValueChange={onChange}>\n        <SelectTrigger className=\"h-10\">\n          <SelectValue placeholder={placeholder} />\n        </SelectTrigger>\n        <SelectContent>\n          {options.map((option) => (\n            <SelectItem key={option.value} value={option.value}>\n              {option.label}\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n    </SettingCard>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/SettingCard.tsx",
    "content": "import type { ReactNode } from \"react\"\n\ntype SettingCardProps = {\n  title: string\n  description?: ReactNode\n  action?: ReactNode\n  actionAlignment?: 'center' | 'start'\n  children?: ReactNode\n}\n\nexport function SettingCard({ title, description, action, actionAlignment = 'start', children }: SettingCardProps) {\n  const headerAlignment = action ? (actionAlignment === 'center' ? 'items-center' : 'items-start') : 'items-start'\n\n  return (\n    <div className=\"p-4 border rounded-lg bg-card hover:shadow-sm transition-all space-y-3\">\n      <div className={`flex justify-between gap-4 ${headerAlignment}`}>\n        <div className=\"space-y-1\">\n          <p className=\"text-sm font-medium leading-none\">{title}</p>\n          {description && (\n            <p className=\"text-xs text-muted-foreground leading-relaxed\">{description}</p>\n          )}\n        </div>\n        {action ? (\n          <div className=\"flex-shrink-0\">\n            {action}\n          </div>\n        ) : null}\n      </div>\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/TextSettingField.tsx",
    "content": "import { Input } from \"@/src/components/ui/input\"\nimport type { BaseSettingProps } from \"./types\"\nimport { SettingCard } from \"./SettingCard\"\n\ntype TextSettingFieldProps = BaseSettingProps & {\n  value: string\n  onChange: (value: string) => void\n  placeholder?: string\n  inputType?: React.ComponentProps<typeof Input>[\"type\"]\n}\n\nexport function TextSettingField({\n  title,\n  description,\n  value,\n  onChange,\n  placeholder,\n  inputType = \"text\",\n}: TextSettingFieldProps) {\n  return (\n    <SettingCard title={title} description={description}>\n      <Input\n        type={inputType}\n        value={value}\n        onChange={(event) => onChange(event.target.value)}\n        placeholder={placeholder}\n        className=\"h-10\"\n      />\n    </SettingCard>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/fields/types.ts",
    "content": "import type { ReactNode } from \"react\"\n\nexport type BaseSettingProps = {\n  title: string\n  description?: ReactNode\n}\n"
  },
  {
    "path": "web/src/components/features/settings/sections/index.ts",
    "content": "import type { ComponentType } from \"react\"\nimport type { Control } from \"react-hook-form\"\nimport type { FormValues } from \"@/src/types/setting\"\nimport { NotifySettingsSection } from \"./NotifySettingsSection\"\nimport { NodeSettingsSection } from \"./NodeSettingsSection\"\nimport { SystemSettingsSection } from \"./SystemSettingsSection\"\nimport { TaskSettingsSection } from \"./TaskSettingsSection\"\n\ntype SectionComponent = ComponentType<{ control: Control<FormValues> }>\n\ninterface SettingsSectionDefinition {\n  id: string\n  label: string\n  Component: SectionComponent\n}\n\nexport const SETTINGS_SECTIONS: SettingsSectionDefinition[] = [\n  {\n    id: \"system-config\",\n    label: \"系统配置\",\n    Component: SystemSettingsSection,\n  },\n  {\n    id: \"node-config\",\n    label: \"节点配置\",\n    Component: NodeSettingsSection,\n  },\n  {\n    id: \"task-config\",\n    label: \"任务配置\",\n    Component: TaskSettingsSection,\n  },\n  {\n    id: \"notify-config\",\n    label: \"通知配置\",\n    Component: NotifySettingsSection,\n  },\n]\n"
  },
  {
    "path": "web/src/components/features/settings/settings.tsx",
    "content": "\"use client\"\n\nimport { useState, useMemo, useCallback, useEffect } from \"react\"\nimport { useForm } from \"react-hook-form\"\nimport { Dialog, DialogContent, } from \"@/src/components/ui/dialog\"\nimport { InlineLoading } from \"@/src/components/ui/loading\"\nimport { useSettings, useUpdateSettings } from \"@/src/lib/queries/setting-queries\"\nimport { SettingsLayout } from \"./SettingsLayout\"\nimport { SettingsActions } from \"./SettingsActions\"\nimport type { FormValues, Setting } from \"@/src/types/setting\"\nimport { SETTINGS_SECTIONS } from \"./sections\"\nimport { cloneFormValues, mapSettingsToFormValues } from \"./utils/value-mappers\"\n\ninterface SettingsDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n\nexport function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {\n  const [activeTab, setActiveTab] = useState(() => SETTINGS_SECTIONS[0]?.id ?? \"\")\n  const updateSettingsMutation = useUpdateSettings()\n  const { data: backendSettings, isLoading, error } = useSettings()\n\n  const form = useForm<FormValues>({\n    mode: 'onChange',\n    shouldUnregister: false,\n    shouldFocusError: true,\n    defaultValues: {},\n  })\n\n  const { dirtyFields, isDirty } = form.formState\n\n  useEffect(() => {\n    if (backendSettings === undefined || isDirty) {\n      return\n    }\n\n    const mappedValues = mapSettingsToFormValues(backendSettings)\n    const currentValues = form.getValues()\n\n    if (areFormValuesEqual(currentValues, mappedValues)) {\n      return\n    }\n\n    form.reset(mappedValues, {\n      keepDirty: false,\n      keepDirtyValues: false,\n      keepErrors: false,\n      keepTouched: false,\n      keepSubmitCount: false,\n    })\n  }, [backendSettings, form, isDirty])\n\n  const nav = useMemo(\n    () =>\n      SETTINGS_SECTIONS.map((section) => ({\n        id: section.id,\n        name: section.label,\n      })),\n    []\n  )\n\n  const currentSection = useMemo(() => {\n    if (!SETTINGS_SECTIONS.length) return null\n    return (\n      SETTINGS_SECTIONS.find((section) => section.id === activeTab) ??\n      SETTINGS_SECTIONS[0]\n    )\n  }, [activeTab])\n\n  const hasChanges = isDirty\n\n  const handleSubmit = useCallback(async (e: React.FormEvent) => {\n    e.preventDefault()\n\n    if (!hasChanges) {\n      onOpenChange(false)\n      return\n    }\n\n    const changes: Setting[] = []\n    const formValues = form.getValues()\n\n    const dirtyKeys = Object.keys(dirtyFields) as Array<keyof FormValues>\n\n    dirtyKeys.forEach((key) => {\n      if (!dirtyFields[key]) return\n\n      const value = formValues[key]\n      if (value === undefined) return\n\n      let stringValue: string\n\n      if (Array.isArray(value)) {\n        stringValue = value.join(',')\n      } else if (typeof value === 'boolean') {\n        stringValue = value ? 'true' : 'false'\n      } else {\n        stringValue = value === null ? '' : String(value)\n      }\n\n      changes.push({ key: String(key), value: stringValue })\n    })\n\n    if (changes.length > 0) {\n      try {\n        await updateSettingsMutation.mutateAsync(changes)\n        const nextValues = cloneFormValues(formValues)\n\n        form.reset(nextValues, {\n          keepDirty: false,\n          keepDirtyValues: false,\n          keepErrors: false,\n          keepTouched: false,\n          keepSubmitCount: false,\n        })\n        onOpenChange(false)\n      } catch (err) {\n        console.error('Failed to save settings:', err)\n      }\n    } else {\n      onOpenChange(false)\n    }\n  }, [hasChanges, dirtyFields, form, updateSettingsMutation, onOpenChange])\n\n  const renderContent = useMemo(() => {\n    if (isLoading) {\n      return (\n        <div className=\"flex items-center justify-center h-[400px]\">\n          <InlineLoading message=\"正在加载设置...\" size=\"sm\" />\n        </div>\n      )\n    }\n\n    if (error) {\n      return (\n        <div className=\"flex flex-col items-center justify-center h-[400px] space-y-3 text-sm\">\n          <span className=\"text-destructive font-medium\">设置加载失败</span>\n          <span className=\"text-muted-foreground\">\n            {error instanceof Error ? error.message : '请稍后重试'}\n          </span>\n        </div>\n      )\n    }\n\n    if (!currentSection) {\n      return (\n        <div className=\"text-center text-muted-foreground\">\n          暂无可用的设置项\n        </div>\n      )\n    }\n\n    const SectionComponent = currentSection.Component\n    return <SectionComponent control={form.control} />\n  }, [currentSection, form.control, error, isLoading])\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"overflow-hidden p-0 md:max-h-[600px] md:max-w-[800px] lg:max-w-[900px] max-h-[90vh] h-full md:h-auto w-[95vw] sm:w-[90vw] md:w-full\">\n        <SettingsLayout\n          nav={nav}\n          activeTab={activeTab}\n          onTabChange={setActiveTab}\n          onSubmit={handleSubmit}\n          renderActions={(isMobile) => (\n            <SettingsActions\n              onCancel={() => onOpenChange(false)}\n              isMobile={!!isMobile}\n              hasChanges={hasChanges}\n            />\n          )}\n        >\n          {renderContent}\n        </SettingsLayout>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nconst areFormValuesEqual = (a: FormValues, b: FormValues): boolean => {\n  const aKeys = Object.keys(a)\n  const bKeys = Object.keys(b)\n\n  if (aKeys.length !== bKeys.length) {\n    return false\n  }\n\n  return aKeys.every((key) => {\n    const aValue = a[key]\n    const bValue = b[key]\n\n    if (Array.isArray(aValue) && Array.isArray(bValue)) {\n      if (aValue.length !== bValue.length) {\n        return false\n      }\n\n      return aValue.every((item, index) => item === bValue[index])\n    }\n\n    return aValue === bValue\n  })\n}\n"
  },
  {
    "path": "web/src/components/features/settings/utils/value-mappers.ts",
    "content": "import { BOOLEAN_SETTING_KEYS, MULTI_SELECT_SETTING_KEYS, NUMBER_SETTING_KEYS } from \"@/src/constant/settings-keys\"\nimport type { FormValue, FormValues, Setting } from \"@/src/types/setting\"\n\nconst parseBoolean = (value: string | undefined): boolean => {\n  return value?.trim().toLowerCase() === \"true\"\n}\n\nconst parseNumber = (value: string | undefined): number | string => {\n  if (!value?.length) {\n    return \"\"\n  }\n\n  const parsed = Number(value)\n  return Number.isNaN(parsed) ? \"\" : parsed\n}\n\nconst parseMultiSelect = (value: string | undefined): string[] => {\n  if (!value) return []\n\n  return value\n    .split(\",\")\n    .map((item) => item.trim())\n    .filter((item) => item.length > 0)\n}\n\nconst parseValue = (key: string, value: string | undefined): FormValue => {\n  if (BOOLEAN_SETTING_KEYS.has(key)) {\n    return parseBoolean(value)\n  }\n\n  if (NUMBER_SETTING_KEYS.has(key)) {\n    return parseNumber(value)\n  }\n\n  if (MULTI_SELECT_SETTING_KEYS.has(key)) {\n    return parseMultiSelect(value)\n  }\n\n  return value ?? \"\"\n}\n\nexport const mapSettingsToFormValues = (settings: Setting[] | undefined): FormValues => {\n  if (!settings || settings.length === 0) {\n    return {}\n  }\n\n  return settings.reduce<FormValues>((acc, setting) => {\n    if (!setting.key) return acc\n\n    acc[setting.key] = parseValue(setting.key, setting.value)\n    return acc\n  }, {})\n}\n\nexport const cloneFormValues = (values: FormValues): FormValues => {\n  const clonedEntries = Object.entries(values).map(([key, value]) => {\n    if (Array.isArray(value)) {\n      return [key, [...value]] as const\n    }\n\n    return [key, value] as const\n  })\n\n  return Object.fromEntries(clonedEntries) as FormValues\n}\n"
  },
  {
    "path": "web/src/components/features/share/components/form-sections/alive-status-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Label } from '@/src/components/ui/label'\nimport { Badge } from '@/src/components/ui/badge'\n\ninterface AliveStatusSectionProps {\n    control: Control<Record<string, unknown> | any>\n    fieldName: string\n}\n\n// 根据 bestsub/internal/models/node/node.go 中的常量定义\nconst ALIVE_STATUS_FLAGS = [\n    { value: 1, label: '存活', name: 'Alive' },           // 1 << 0\n    { value: 2, label: '国家', name: 'Country' },         // 1 << 1\n    { value: 4, label: 'TikTok', name: 'TikTok' },             // 1 << 2\n    { value: 8, label: 'TikTok IDC', name: 'TikTok IDC' },     // 1 << 3\n] as const\n\nexport function AliveStatusSection({ control, fieldName }: AliveStatusSectionProps) {\n    return (\n        <Controller\n            name={fieldName}\n            control={control}\n            render={({ field }) => (\n                <div className=\"w-full\">\n                    <Label className=\"mb-2 block\">\n                        存活状态\n                    </Label>\n                    <div className=\"space-y-2\">\n                        <div className=\"flex flex-wrap gap-2\">\n                            {ALIVE_STATUS_FLAGS.map(flag => {\n                                const currentValue = (field.value as number) || 0\n                                const isSelected = (currentValue & flag.value) !== 0\n\n                                const handleToggleSelection = (flagValue: number) => {\n                                    let newValue = currentValue\n                                    if (isSelected) {\n                                        // 取消选择：使用异或运算清除该位\n                                        newValue = currentValue & ~flagValue\n                                    } else {\n                                        // 选择：使用或运算设置该位\n                                        newValue = currentValue | flagValue\n                                    }\n                                    field.onChange(newValue)\n                                }\n\n                                return (\n                                    <Badge\n                                        key={flag.value}\n                                        variant={isSelected ? \"default\" : \"outline\"}\n                                        className={`cursor-pointer transition-colors ${isSelected\n                                            ? \"hover:bg-red-100 hover:text-red-700\"\n                                            : \"hover:bg-green-100 hover:text-green-700\"\n                                            }`}\n                                        onClick={() => handleToggleSelection(flag.value)}\n                                    >\n                                        {flag.label} {isSelected ? \"×\" : \"+\"}\n                                    </Badge>\n                                )\n                            })}\n                        </div>\n                    </div>\n                    <p className=\"text-xs text-muted-foreground mt-2\">\n                        点击状态进行选择/取消选择，支持多选组合\n                    </p>\n                </div>\n            )}\n        />\n    )\n} "
  },
  {
    "path": "web/src/components/features/share/components/form-sections/basic-info-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { Switch } from '@/src/components/ui/switch'\nimport { FORM_VALIDATION } from '../../constants'\nimport type { ShareRequest } from '@/src/types'\n\ninterface BasicInfoSectionProps {\n    control: Control<ShareRequest>\n}\n\nexport function BasicInfoSection({ control }: BasicInfoSectionProps) {\n    return (\n        <div className=\"space-y-4\">\n            <div>\n                <Label htmlFor=\"name\" className=\"mb-2 block\">\n                    分享名称\n                </Label>\n                <Controller\n                    name=\"name\"\n                    control={control}\n                    rules={{ required: FORM_VALIDATION.NAME_REQUIRED }}\n                    render={({ field }) => (\n                        <Input\n                            {...field}\n                            value={field.value || ''}\n                            id=\"name\"\n                            placeholder=\"请输入分享名称\"\n                        />\n                    )}\n                />\n            </div>\n\n            <div>\n                <Label htmlFor=\"token\" className=\"mb-2 block\">\n                    访问Token\n                </Label>\n                <Controller\n                    name=\"token\"\n                    control={control}\n                    render={({ field }) => (\n                        <Input\n                            {...field}\n                            value={field.value || ''}\n                            id=\"token\"\n                            placeholder=\"留空自动生成\"\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"flex items-center justify-between\">\n                <Label htmlFor=\"enable\">启用分享</Label>\n                <Controller\n                    name=\"enable\"\n                    control={control}\n                    render={({ field }) => (\n                        <Switch\n                            id=\"enable\"\n                            checked={!!field.value}\n                            onCheckedChange={field.onChange}\n                        />\n                    )}\n                />\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/share/components/form-sections/config-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/src/components/ui/select'\nimport { Calendar22 } from '../share-date-pick'\nimport { SUBSCRIPTION_TARGETS } from '../../constants'\nimport type { ShareRequest } from '@/src/types'\n\ninterface ConfigSectionProps {\n    control: Control<ShareRequest>\n}\n\nexport function ConfigSection({ control }: ConfigSectionProps) {\n    return (\n        <div className=\"space-y-4\">\n            <div>\n                <Label htmlFor=\"template\" className=\"mb-2 block\">\n                    订阅模板\n                </Label>\n                <Controller\n                    name=\"gen.target\"\n                    control={control}\n                    render={({ field }) => (\n                        <Select onValueChange={field.onChange} value={field.value ?? 'auto'}>\n                            <SelectTrigger className=\"w-full\">\n                                <SelectValue placeholder=\"选择订阅模板\" />\n                            </SelectTrigger>\n                            <SelectContent>\n                                {SUBSCRIPTION_TARGETS.map((target) => (\n                                    <SelectItem key={target.value} value={target.value}>\n                                        {target.label}\n                                    </SelectItem>\n                                ))}\n                            </SelectContent>\n                        </Select>\n                    )}\n                />\n            </div>\n\n            <div>\n                <Label htmlFor=\"rename\" className=\"mb-2 block\">\n                    重命名模板\n                </Label>\n                <Controller\n                    name=\"gen.rename\"\n                    control={control}\n                    render={({ field }) => (\n                        <Input\n                            {...field}\n                            value={field.value || ''}\n                            id=\"rename\"\n                        />\n                    )}\n                />\n            </div>\n\n\n            <div className=\"grid grid-cols-2 gap-4\">\n                <div>\n                    <Label htmlFor=\"max_access_count\" className=\"mb-2 block\">\n                        最大访问次数\n                    </Label>\n                    <Controller\n                        name=\"max_access_count\"\n                        control={control}\n                        render={({ field }) => (\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"max_access_count\"\n                                type=\"number\"\n                                placeholder=\"0\"\n                                min=\"0\"\n                                onChange={(e) => field.onChange(parseInt(e.target.value || '0'))}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div>\n                    <Controller\n                        name=\"expires\"\n                        control={control}\n                        render={({ field }) => (\n                            <Calendar22\n                                value={field.value ?? 0}\n                                onChange={(ts: number) => field.onChange(ts || 0)}\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/share/components/form-sections/country-section.tsx",
    "content": "// 此文件由AI生成。如有错误，请手动修正\n// This file was generated with AI assistance\n// Please correct any errors manually\n\nimport { useState } from 'react'\nimport { Controller, Control } from 'react-hook-form'\nimport { Label } from '@/src/components/ui/label'\nimport { Badge } from '@/src/components/ui/badge'\nimport { Input } from '@/src/components/ui/input'\nimport { Switch } from '@/src/components/ui/switch'\n\ninterface CountrySectionProps {\n    control: Control<Record<string, unknown> | any>\n    fieldName: string\n}\n\nconst POPULAR_COUNTRIES = [\n    { code: 'CN', name: '中国' },\n    { code: 'US', name: '美国' },\n    { code: 'JP', name: '日本' },\n    { code: 'KR', name: '韩国' },\n    { code: 'HK', name: '香港' },\n    { code: 'TW', name: '台湾' },\n    { code: 'SG', name: '新加坡' },\n    { code: 'GB', name: '英国' },\n    { code: 'DE', name: '德国' },\n    { code: 'FR', name: '法国' },\n] as const\n\nconst ALL_COUNTRIES = [\n    ...POPULAR_COUNTRIES,\n    { code: 'AD', name: '安道尔' },\n    { code: 'AE', name: '阿联酋' },\n    { code: 'AF', name: '阿富汗' },\n    { code: 'AG', name: '安提瓜和巴布达' },\n    { code: 'AI', name: '安圭拉' },\n    { code: 'AL', name: '阿尔巴尼亚' },\n    { code: 'AM', name: '亚美尼亚' },\n    { code: 'AO', name: '安哥拉' },\n    { code: 'AQ', name: '南极洲' },\n    { code: 'AR', name: '阿根廷' },\n    { code: 'AS', name: '美属萨摩亚' },\n    { code: 'AT', name: '奥地利' },\n    { code: 'AU', name: '澳大利亚' },\n    { code: 'AW', name: '阿鲁巴' },\n    { code: 'AZ', name: '阿塞拜疆' },\n    { code: 'BA', name: '波黑' },\n    { code: 'BB', name: '巴巴多斯' },\n    { code: 'BD', name: '孟加拉国' },\n    { code: 'BE', name: '比利时' },\n    { code: 'BF', name: '布基纳法索' },\n    { code: 'BG', name: '保加利亚' },\n    { code: 'BH', name: '巴林' },\n    { code: 'BI', name: '布隆迪' },\n    { code: 'BJ', name: '贝宁' },\n    { code: 'BL', name: '法属圣巴泰勒米' },\n    { code: 'BM', name: '百慕大' },\n    { code: 'BN', name: '文莱' },\n    { code: 'BO', name: '玻利维亚' },\n    { code: 'BQ', name: '荷属加勒比区' },\n    { code: 'BR', name: '巴西' },\n    { code: 'BS', name: '巴哈马' },\n    { code: 'BT', name: '不丹' },\n    { code: 'BV', name: '布维岛' },\n    { code: 'BW', name: '博茨瓦纳' },\n    { code: 'BY', name: '白俄罗斯' },\n    { code: 'BZ', name: '伯利兹' },\n    { code: 'CA', name: '加拿大' },\n    { code: 'CC', name: '科科斯（基林）群岛' },\n    { code: 'CD', name: '刚果（金）' },\n    { code: 'CF', name: '中非共和国' },\n    { code: 'CG', name: '刚果（布）' },\n    { code: 'CH', name: '瑞士' },\n    { code: 'CI', name: '科特迪瓦' },\n    { code: 'CK', name: '库克群岛' },\n    { code: 'CL', name: '智利' },\n    { code: 'CM', name: '喀麦隆' },\n    { code: 'CO', name: '哥伦比亚' },\n    { code: 'CR', name: '哥斯达黎加' },\n    { code: 'CU', name: '古巴' },\n    { code: 'CV', name: '佛得角' },\n    { code: 'CW', name: '库拉索' },\n    { code: 'CX', name: '圣诞岛' },\n    { code: 'CY', name: '塞浦路斯' },\n    { code: 'CZ', name: '捷克' },\n    { code: 'DJ', name: '吉布提' },\n    { code: 'DK', name: '丹麦' },\n    { code: 'DM', name: '多米尼克' },\n    { code: 'DO', name: '多米尼加' },\n    { code: 'DZ', name: '阿尔及利亚' },\n    { code: 'EC', name: '厄瓜多尔' },\n    { code: 'EE', name: '爱沙尼亚' },\n    { code: 'EG', name: '埃及' },\n    { code: 'EH', name: '西撒哈拉' },\n    { code: 'ER', name: '厄立特里亚' },\n    { code: 'ES', name: '西班牙' },\n    { code: 'ET', name: '埃塞俄比亚' },\n    { code: 'FI', name: '芬兰' },\n    { code: 'FJ', name: '斐济' },\n    { code: 'FK', name: '福克兰群岛（马尔维纳斯）' },\n    { code: 'FM', name: '密克罗尼西亚联邦' },\n    { code: 'FO', name: '法罗群岛' },\n    { code: 'GA', name: '加蓬' },\n    { code: 'GD', name: '格林纳达' },\n    { code: 'GE', name: '格鲁吉亚' },\n    { code: 'GF', name: '法属圭亚那' },\n    { code: 'GG', name: '根西' },\n    { code: 'GH', name: '加纳' },\n    { code: 'GI', name: '直布罗陀' },\n    { code: 'GL', name: '格陵兰' },\n    { code: 'GM', name: '冈比亚' },\n    { code: 'GN', name: '几内亚' },\n    { code: 'GP', name: '瓜德罗普' },\n    { code: 'GQ', name: '赤道几内亚' },\n    { code: 'GR', name: '希腊' },\n    { code: 'GS', name: '南乔治亚岛和南桑威奇群岛' },\n    { code: 'GT', name: '危地马拉' },\n    { code: 'GU', name: '关岛' },\n    { code: 'GW', name: '几内亚比绍' },\n    { code: 'GY', name: '圭亚那' },\n    { code: 'HM', name: '赫德岛和麦克唐纳群岛' },\n    { code: 'HN', name: '洪都拉斯' },\n    { code: 'HR', name: '克罗地亚' },\n    { code: 'HT', name: '海地' },\n    { code: 'HU', name: '匈牙利' },\n    { code: 'ID', name: '印度尼西亚' },\n    { code: 'IE', name: '爱尔兰' },\n    { code: 'IL', name: '以色列' },\n    { code: 'IM', name: '马恩岛' },\n    { code: 'IN', name: '印度' },\n    { code: 'IO', name: '英属印度洋领地' },\n    { code: 'IQ', name: '伊拉克' },\n    { code: 'IR', name: '伊朗' },\n    { code: 'IS', name: '冰岛' },\n    { code: 'IT', name: '意大利' },\n    { code: 'JE', name: '泽西' },\n    { code: 'JM', name: '牙买加' },\n    { code: 'JO', name: '约旦' },\n    { code: 'KE', name: '肯尼亚' },\n    { code: 'KG', name: '吉尔吉斯斯坦' },\n    { code: 'KH', name: '柬埔寨' },\n    { code: 'KI', name: '基里巴斯' },\n    { code: 'KM', name: '科摩罗' },\n    { code: 'KN', name: '圣基茨和尼维斯' },\n    { code: 'KP', name: '朝鲜' },\n    { code: 'KW', name: '科威特' },\n    { code: 'KY', name: '开曼群岛' },\n    { code: 'KZ', name: '哈萨克斯坦' },\n    { code: 'LA', name: '老挝' },\n    { code: 'LB', name: '黎巴嫩' },\n    { code: 'LC', name: '圣卢西亚' },\n    { code: 'LI', name: '列支敦士登' },\n    { code: 'LK', name: '斯里兰卡' },\n    { code: 'LR', name: '利比里亚' },\n    { code: 'LS', name: '莱索托' },\n    { code: 'LT', name: '立陶宛' },\n    { code: 'LU', name: '卢森堡' },\n    { code: 'LV', name: '拉脱维亚' },\n    { code: 'LY', name: '利比亚' },\n    { code: 'MA', name: '摩洛哥' },\n    { code: 'MC', name: '摩纳哥' },\n    { code: 'MD', name: '摩尔多瓦' },\n    { code: 'ME', name: '黑山' },\n    { code: 'MF', name: '法属圣马丁' },\n    { code: 'MG', name: '马达加斯加' },\n    { code: 'MH', name: '马绍尔群岛' },\n    { code: 'MK', name: '北马其顿' },\n    { code: 'ML', name: '马里' },\n    { code: 'MM', name: '缅甸' },\n    { code: 'MN', name: '蒙古' },\n    { code: 'MO', name: '中国澳门' },\n    { code: 'MP', name: '北马里亚纳群岛' },\n    { code: 'MQ', name: '马提尼克' },\n    { code: 'MR', name: '毛里塔尼亚' },\n    { code: 'MS', name: '蒙特塞拉特' },\n    { code: 'MT', name: '马耳他' },\n    { code: 'MU', name: '毛里求斯' },\n    { code: 'MV', name: '马尔代夫' },\n    { code: 'MW', name: '马拉维' },\n    { code: 'MX', name: '墨西哥' },\n    { code: 'MY', name: '马来西亚' },\n    { code: 'MZ', name: '莫桑比克' },\n    { code: 'NA', name: '纳米比亚' },\n    { code: 'NC', name: '新喀里多尼亚' },\n    { code: 'NE', name: '尼日尔' },\n    { code: 'NF', name: '诺福克岛' },\n    { code: 'NG', name: '尼日利亚' },\n    { code: 'NI', name: '尼加拉瓜' },\n    { code: 'NL', name: '荷兰' },\n    { code: 'NO', name: '挪威' },\n    { code: 'NP', name: '尼泊尔' },\n    { code: 'NR', name: '瑙鲁' },\n    { code: 'NU', name: '纽埃' },\n    { code: 'NZ', name: '新西兰' },\n    { code: 'OM', name: '阿曼' },\n    { code: 'PA', name: '巴拿马' },\n    { code: 'PE', name: '秘鲁' },\n    { code: 'PF', name: '法属波利尼西亚' },\n    { code: 'PG', name: '巴布亚新几内亚' },\n    { code: 'PH', name: '菲律宾' },\n    { code: 'PK', name: '巴基斯坦' },\n    { code: 'PL', name: '波兰' },\n    { code: 'PM', name: '圣皮埃尔和密克隆' },\n    { code: 'PN', name: '皮特凯恩群岛' },\n    { code: 'PR', name: '波多黎各' },\n    { code: 'PS', name: '巴勒斯坦国' },\n    { code: 'PT', name: '葡萄牙' },\n    { code: 'PW', name: '帕劳' },\n    { code: 'PY', name: '巴拉圭' },\n    { code: 'QA', name: '卡塔尔' },\n    { code: 'RE', name: '留尼汪' },\n    { code: 'RO', name: '罗马尼亚' },\n    { code: 'RS', name: '塞尔维亚' },\n    { code: 'RW', name: '卢旺达' },\n    { code: 'SA', name: '沙特阿拉伯' },\n    { code: 'SB', name: '所罗门群岛' },\n    { code: 'SC', name: '塞舌尔' },\n    { code: 'SD', name: '苏丹' },\n    { code: 'SE', name: '瑞典' },\n    { code: 'SH', name: '圣赫勒拿、阿森松和特里斯坦-达库尼亚' },\n    { code: 'SI', name: '斯洛文尼亚' },\n    { code: 'SJ', name: '斯瓦尔巴和扬马延' },\n    { code: 'SK', name: '斯洛伐克' },\n    { code: 'SL', name: '塞拉利昂' },\n    { code: 'SM', name: '圣马力诺' },\n    { code: 'SN', name: '塞内加尔' },\n    { code: 'SO', name: '索马里' },\n    { code: 'SR', name: '苏里南' },\n    { code: 'SS', name: '南苏丹' },\n    { code: 'ST', name: '圣多美和普林西比' },\n    { code: 'SV', name: '萨尔瓦多' },\n    { code: 'SX', name: '荷属圣马丁' },\n    { code: 'SY', name: '叙利亚' },\n    { code: 'SZ', name: '埃斯瓦蒂尼' },\n    { code: 'TC', name: '特克斯和凯科斯群岛' },\n    { code: 'TD', name: '乍得' },\n    { code: 'TF', name: '法属南部领地' },\n    { code: 'TG', name: '多哥' },\n    { code: 'TH', name: '泰国' },\n    { code: 'TJ', name: '塔吉克斯坦' },\n    { code: 'TK', name: '托克劳' },\n    { code: 'TL', name: '东帝汶' },\n    { code: 'TM', name: '土库曼斯坦' },\n    { code: 'TN', name: '突尼斯' },\n    { code: 'TO', name: '汤加' },\n    { code: 'TR', name: '土耳其' },\n    { code: 'TT', name: '特立尼达和多巴哥' },\n    { code: 'TV', name: '图瓦卢' },\n    { code: 'TZ', name: '坦桑尼亚' },\n    { code: 'UA', name: '乌克兰' },\n    { code: 'UG', name: '乌干达' },\n    { code: 'UM', name: '美国本土外小岛屿' },\n    { code: 'UY', name: '乌拉圭' },\n    { code: 'UZ', name: '乌兹别克斯坦' },\n    { code: 'VA', name: '梵蒂冈' },\n    { code: 'VC', name: '圣文森特和格林纳丁斯' },\n    { code: 'VE', name: '委内瑞拉' },\n    { code: 'VG', name: '英属维尔京群岛' },\n    { code: 'VI', name: '美属维尔京群岛' },\n    { code: 'VN', name: '越南' },\n    { code: 'VU', name: '瓦努阿图' },\n    { code: 'WF', name: '瓦利斯和富图纳' },\n    { code: 'WS', name: '萨摩亚' },\n    { code: 'YE', name: '也门' },\n    { code: 'YT', name: '马约特' },\n    { code: 'ZA', name: '南非' },\n    { code: 'ZM', name: '赞比亚' },\n    { code: 'ZW', name: '津巴布韦' }\n] as const\n\nexport function CountrySection({ control, fieldName }: CountrySectionProps) {\n    const [searchTerm, setSearchTerm] = useState('')\n    const [showSearchResults, setShowSearchResults] = useState(false)\n\n    return (\n        <Controller\n            name={fieldName}\n            control={control}\n            render={({ field }) => {\n                const selectedCodes = (field.value as string[]) || []\n                const excludeFieldName = fieldName.replace('country', 'country_exclude')\n                \n                const handleAddCountry = (code: string) => {\n                    if (!selectedCodes.includes(code)) {\n                        field.onChange([...selectedCodes, code])\n                    }\n                    setSearchTerm('')\n                    setShowSearchResults(false)\n                }\n\n                const handleRemoveCountry = (code: string) => {\n                    field.onChange(selectedCodes.filter(c => c !== code))\n                }\n\n                const getCountryName = (code: string) => {\n                    const country = ALL_COUNTRIES.find(c => c.code === code)\n                    return country ? country.name : code\n                }\n\n                const filteredCountries = ALL_COUNTRIES\n                    .filter(country =>\n                        !selectedCodes.includes(country.code) &&\n                        (country.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n                            country.code.includes(searchTerm))\n                    )\n                    .slice(0, 10)\n\n                return (\n                    <div className=\"w-full\">\n                        <div className=\"flex items-center justify-between mb-2\">\n                            <Label className=\"block\">\n                                国家\n                            </Label>\n                            <div className=\"flex items-center space-x-2\">\n                                <Label htmlFor={`${fieldName}-exclude`} className=\"text-sm text-muted-foreground\">\n                                    排除模式\n                                </Label>\n                                <Controller\n                                    name={excludeFieldName}\n                                    control={control}\n                                    render={({ field: excludeField }) => (\n                                        <Switch\n                                            id={`${fieldName}-exclude`}\n                                            checked={excludeField.value || false}\n                                            onCheckedChange={excludeField.onChange}\n                                        />\n                                    )}\n                                />\n                            </div>\n                        </div>\n\n                        {selectedCodes.length > 0 && (\n                            <div className=\"mb-3\">\n                                <div className=\"text-xs text-muted-foreground mb-1\">已选择:</div>\n                                <div className=\"flex flex-wrap gap-1\">\n                                    {selectedCodes.map(code => (\n                                        <Badge\n                                            key={code}\n                                            variant=\"secondary\"\n                                            className=\"cursor-pointer hover:bg-red-100 hover:text-red-700\"\n                                            onClick={() => handleRemoveCountry(code)}\n                                        >\n                                            {getCountryName(code)} ({code}) ×\n                                        </Badge>\n                                    ))}\n                                </div>\n                            </div>\n                        )}\n\n                        <div className=\"mb-3\">\n                            <div className=\"text-xs text-muted-foreground mb-1\">常用国家:</div>\n                            <div className=\"flex flex-wrap gap-1\">\n                                {POPULAR_COUNTRIES.map(country => {\n                                    const isSelected = selectedCodes.includes(country.code)\n                                    return (\n                                        <Badge\n                                            key={country.code}\n                                            variant={isSelected ? \"default\" : \"outline\"}\n                                            className={`cursor-pointer transition-colors ${isSelected\n                                                ? \"opacity-50 cursor-not-allowed\"\n                                                : \"hover:bg-green-100 hover:text-green-700\"\n                                                }`}\n                                            onClick={() => !isSelected && handleAddCountry(country.code)}\n                                        >\n                                            {country.name} {isSelected ? '' : '+'}\n                                        </Badge>\n                                    )\n                                })}\n                            </div>\n                        </div>\n\n                        <div className=\"relative\">\n                            <div className=\"text-xs text-muted-foreground mb-1\">搜索其他国家:</div>\n                            <Input\n                                placeholder=\"输入国家名称或二字母代码（如CN、US）...\"\n                                value={searchTerm}\n                                onChange={(e) => {\n                                    setSearchTerm(e.target.value)\n                                    setShowSearchResults(e.target.value.length > 0)\n                                }}\n                                onFocus={() => setShowSearchResults(searchTerm.length > 0)}\n                                onBlur={() => setTimeout(() => setShowSearchResults(false), 200)}\n                            />\n\n                            {showSearchResults && filteredCountries.length > 0 && (\n                                <div className=\"absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg max-h-48 overflow-y-auto\">\n                                    {filteredCountries.map(country => (\n                                        <div\n                                            key={country.code}\n                                            className=\"px-3 py-2 cursor-pointer hover:bg-gray-100\"\n                                            onClick={() => handleAddCountry(country.code)}\n                                        >\n                                            {country.name} ({country.code})\n                                        </div>\n                                    ))}\n                                </div>\n                            )}\n                        </div>\n                    </div>\n                )\n            }}\n        />\n    )\n} "
  },
  {
    "path": "web/src/components/features/share/components/form-sections/filter-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport { safeParseInt, safeParseFloat } from '../../utils'\n\nexport function FilterSection({ control }: { control: Control<Record<string, unknown> | any> }) {\n    return (\n        <div className=\"space-y-4\">\n            <div className=\"grid grid-cols-2 gap-4\">\n                <div>\n                    <Label htmlFor=\"speed_up_more\" className=\"mb-2 block\">\n                        上传速度 {'>'} (KB/s)\n                    </Label>\n                    <Controller\n                        name=\"gen.filter.speed_up_more\"\n                        control={control}\n                        render={({ field }) => (\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"speed_up_more\"\n                                type=\"number\"\n                                step=\"0.1\"\n                                placeholder=\"0\"\n                                min=\"0\"\n                                onChange={(e) => field.onChange(safeParseFloat(e.target.value))}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div>\n                    <Label htmlFor=\"speed_down_more\" className=\"mb-2 block\">\n                        下载速度 {'>'} (KB/s)\n                    </Label>\n                    <Controller\n                        name=\"gen.filter.speed_down_more\"\n                        control={control}\n                        render={({ field }) => (\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"speed_down_more\"\n                                type=\"number\"\n                                step=\"0.1\"\n                                placeholder=\"0\"\n                                min=\"0\"\n                                onChange={(e) => field.onChange(safeParseFloat(e.target.value))}\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n\n            <div className=\"grid grid-cols-2 gap-4\">\n                <div>\n                    <Label htmlFor=\"delay_less_than\" className=\"mb-2 block\">\n                        延迟 {'<'} (ms)\n                    </Label>\n                    <Controller\n                        name=\"gen.filter.delay_less_than\"\n                        control={control}\n                        render={({ field }) => (\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"delay_less_than\"\n                                type=\"number\"\n                                placeholder=\"1000\"\n                                min=\"0\"\n                                onChange={(e) => field.onChange(safeParseInt(e.target.value))}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div>\n                    <Label htmlFor=\"risk_less_than\" className=\"mb-2 block\">\n                        风险值 {'<'}\n                    </Label>\n                    <Controller\n                        name=\"gen.filter.risk_less_than\"\n                        control={control}\n                        render={({ field }) => (\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"risk_less_than\"\n                                type=\"number\"\n                                placeholder=\"5\"\n                                min=\"0\"\n                                onChange={(e) => field.onChange(safeParseInt(e.target.value))}\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/share/components/form-sections/index.ts",
    "content": "export { BasicInfoSection } from './basic-info-section'\nexport { ConfigSection } from './config-section'\nexport { FilterSection } from './filter-section'\nexport { AliveStatusSection } from './alive-status-section'\nexport { CountrySection } from './country-section'"
  },
  {
    "path": "web/src/components/features/share/components/share-copy.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/src/components/ui/dialog'\nimport { Input } from '@/src/components/ui/input'\nimport { Button } from '@/src/components/ui/button'\nimport { Copy, Check } from 'lucide-react'\nimport { copyToClipboard } from '../utils'\nimport { UI_TEXT } from '../constants'\n\ninterface ShareCopyDialogProps {\n    fullUrl: string\n    isOpen: boolean\n    onClose: () => void\n}\n\nexport function ShareCopyDialog({ fullUrl, isOpen, onClose }: ShareCopyDialogProps) {\n    const [copied, setCopied] = useState(false)\n\n    const handleCopy = async () => {\n        const success = await copyToClipboard(fullUrl)\n        if (success) {\n            setCopied(true)\n            setTimeout(() => setCopied(false), 2000) // 2秒后重置状态\n        }\n    }\n\n    const handleClose = () => {\n        setCopied(false)\n        onClose()\n    }\n\n    return (\n        <Dialog open={isOpen} onOpenChange={handleClose}>\n            <DialogContent className=\"sm:max-w-md\">\n                <DialogHeader>\n                    <DialogTitle>订阅链接</DialogTitle>\n                </DialogHeader>\n\n                <div className=\"space-y-4\">\n                    <p className=\"text-sm text-muted-foreground\">\n                        请复制以下订阅链接:\n                    </p>\n\n                    <div className=\"flex items-center space-x-2\">\n                        <Input\n                            readOnly\n                            value={fullUrl || ''}\n                            className=\"flex-1\"\n                            onClick={(e) => e.currentTarget.select()}\n                        />\n                        <Button\n                            type=\"button\"\n                            size=\"sm\"\n                            onClick={handleCopy}\n                            className=\"shrink-0\"\n                            variant={copied ? \"default\" : \"outline\"}\n                        >\n                            {copied ? (\n                                <>\n                                    <Check className=\"h-4 w-4 mr-1\" />\n                                    已复制\n                                </>\n                            ) : (\n                                <>\n                                    <Copy className=\"h-4 w-4 mr-1\" />\n                                    {UI_TEXT.COPY}\n                                </>\n                            )}\n                        </Button>\n                    </div>\n\n                    {copied && (\n                        <p className=\"text-sm text-green-600\">\n                            链接已复制到剪贴板！\n                        </p>\n                    )}\n                </div>\n            </DialogContent>\n        </Dialog>\n    )\n}"
  },
  {
    "path": "web/src/components/features/share/components/share-date-pick.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Calendar } from \"@/src/components/ui/calendar\"\nimport { Label } from \"@/src/components/ui/label\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/src/components/ui/popover\"\n\ninterface Calendar22Props {\n  value?: number\n  onChange?: (timestamp: number) => void\n}\n\nexport function Calendar22({\n  value = 0,\n  onChange,\n}: Calendar22Props) {\n  const [open, setOpen] = React.useState(false)\n\n  // 将时间戳转换为日期对象\n  const selectedDate = React.useMemo(() => {\n    if (!value || value === 0) return undefined\n    return new Date(value * 1000)\n  }, [value])\n\n  // 处理日期选择\n  const handleDateSelect = React.useCallback((date: Date | undefined) => {\n    if (date && onChange) {\n      const timestamp = Math.floor(date.getTime() / 1000)\n      onChange(timestamp)\n    } else if (!date && onChange) {\n      onChange(0)\n    }\n    setOpen(false)\n  }, [onChange])\n\n  // 处理\"永不过期\"按钮点击\n  const handleNeverExpires = React.useCallback(() => {\n    if (onChange) {\n      onChange(0)\n    }\n    setOpen(false)\n  }, [onChange])\n\n  // 显示文本\n  const displayText = React.useMemo(() => {\n    if (!value || value === 0) {\n      return \"永不过期\"\n    }\n    if (selectedDate) {\n      return selectedDate.toLocaleDateString('zh-CN')\n    }\n    return \"选择日期\"\n  }, [selectedDate, value])\n\n  // 禁用过去的日期\n  const isDateDisabled = React.useCallback((date: Date) => {\n    const today = new Date()\n    today.setHours(0, 0, 0, 0)\n    return date < today\n  }, [])\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <Label htmlFor=\"date\" className=\"px-1\">\n        过期时间\n      </Label>\n\n      <Popover open={open} onOpenChange={setOpen} modal={true}>\n        <PopoverTrigger asChild>\n          <Button\n            variant=\"outline\"\n            id=\"date\"\n            className=\"w-full justify-between font-normal\"\n            aria-expanded={open}\n            aria-haspopup=\"dialog\"\n          >\n            <span className={selectedDate ? \"\" : \"text-muted-foreground\"}>\n              {displayText}\n            </span>\n            <ChevronDownIcon className=\"h-4 w-4\" />\n          </Button>\n        </PopoverTrigger>\n\n        <PopoverContent\n          className=\"w-auto overflow-hidden p-0 !z-[60]\"\n          align=\"start\"\n        >\n          <Calendar\n            mode=\"single\"\n            selected={selectedDate}\n            captionLayout=\"dropdown\"\n            onSelect={handleDateSelect}\n            disabled={isDateDisabled}\n          />\n\n          <div className=\"border-t p-3 flex justify-center\">\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              onClick={handleNeverExpires}\n              type=\"button\"\n            >\n              永不过期\n            </Button>\n          </div>\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/share/components/share-form.tsx",
    "content": "import { Button } from \"@/src/components/ui/button\"\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/src/components/ui/dialog\"\nimport { useShareForm } from \"../hooks\"\nimport { UI_TEXT } from \"../constants\"\nimport {\n    BasicInfoSection,\n    ConfigSection,\n    FilterSection,\n    AliveStatusSection,\n    CountrySection\n} from \"./form-sections\"\nimport { SubscriptionSection } from \"@/src/components/shared/subscription-section\"\nimport type { ShareRequest } from \"@/src/types\"\n\ninterface ShareFormProps {\n    initialData?: ShareRequest\n    formTitle: string\n    isOpen: boolean\n    onClose: () => void\n    editingShareId?: number | undefined\n}\n\nexport function ShareForm({\n    initialData,\n    formTitle,\n    isOpen,\n    onClose,\n    editingShareId,\n}: ShareFormProps) {\n    const { form, onSubmit, isEditing, isLoading } = useShareForm({\n        initialData,\n        editingShareId,\n        onSuccess: onClose,\n        isOpen,\n    })\n\n    const { control } = form\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onClose}>\n            <DialogContent\n                className=\"max-w-2xl max-h-[80vh] overflow-y-auto scrollbar-hide\"\n                aria-describedby={undefined}\n            >\n                <DialogHeader>\n                    <DialogTitle>{formTitle}</DialogTitle>\n                </DialogHeader>\n\n                <form onSubmit={onSubmit} className=\"space-y-6\">\n                    {/* 基础信息 */}\n                    <BasicInfoSection control={control} />\n\n                    {/* 配置设置 */}\n                    <ConfigSection control={control} />\n\n                    {/* 订阅选择 */}\n                    <SubscriptionSection\n                        control={control}\n                        subIdField=\"gen.filter.sub_id\"\n                        subIdExcludeField=\"gen.filter.sub_id_exclude\"\n                    />\n\n                    {/* 过滤条件 */}\n                    <FilterSection control={control} />\n\n                    {/* 存活状态选择 */}\n                    <AliveStatusSection control={control} fieldName=\"gen.filter.alive_status\" />\n\n                    <CountrySection control={control} fieldName=\"gen.filter.country\" />\n\n                    {/* 操作按钮 */}\n                    <div className=\"flex gap-2 pt-4\">\n                        <Button type=\"submit\" className=\"flex-1\" disabled={isLoading}>\n                            {isLoading ? (\n                                isEditing ? '更新中...' : '创建中...'\n                            ) : (\n                                isEditing ? UI_TEXT.UPDATE : UI_TEXT.CREATE\n                            )}\n                        </Button>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={onClose}\n                            disabled={isLoading}\n                        >\n                            {UI_TEXT.CANCEL}\n                        </Button>\n                    </div>\n                </form>\n            </DialogContent>\n        </Dialog>\n    )\n}\n\n"
  },
  {
    "path": "web/src/components/features/share/components/share-list.tsx",
    "content": "import { useMemo, useEffect } from 'react'\nimport { Card, CardContent } from \"@/src/components/ui/card\"\nimport { Table, TableBody, TableCell, TableRow } from \"@/src/components/ui/table\"\nimport { InlineLoading } from \"@/src/components/ui/loading\"\nimport StatusBadge from \"@/src/components/shared/status-badge\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Edit, Trash2, Copy } from \"lucide-react\"\nimport { useShares } from \"@/src/lib/queries/share-queries\"\nimport { useShareOperations } from \"../hooks\"\nimport { formatAccessCount, formatExpiresTime } from \"../utils\"\nimport { UI_TEXT } from \"../constants\"\nimport { useOverflowDetection } from \"@/src/lib/hooks/useOverflowDetection\"\nimport type { ShareResponse } from \"@/src/types/share\"\n\ninterface ShareListProps {\n    onEdit: (share: ShareResponse) => void\n    openCopyDialog: (fullUrl: string) => void\n}\n\nexport function ShareList({ onEdit, openCopyDialog }: ShareListProps) {\n    const { data: shares = [], isLoading, error } = useShares()\n    const { handleDelete, handleCopy } = useShareOperations()\n    const { containerRef, contentRef, isOverflowing, checkOverflow } = useOverflowDetection<HTMLTableElement>()\n\n    const sortedShares = useMemo(() =>\n        [...shares].sort((a, b) => a.id - b.id),\n        [shares]\n    )\n\n    const onCopyClick = (token: string) => {\n        handleCopy(token, openCopyDialog)\n    }\n\n    const onDeleteClick = (id: number, name: string) => {\n        handleDelete(id, name)\n    }\n\n    useEffect(() => {\n        if (!isLoading) {\n            checkOverflow()\n        }\n    }, [isLoading, checkOverflow])\n\n    if (isLoading) {\n        return (\n            <Card>\n                <CardContent>\n                    <InlineLoading message={UI_TEXT.LOADING + '分享列表...'} />\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (error) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-destructive\">\n                        加载失败: {error.message}\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (shares.length === 0) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-muted-foreground\">\n                        {UI_TEXT.NO_DATA}，点击上方按钮创建第一个分享\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    return (\n        <Card>\n            <CardContent>\n                <div className=\"overflow-x-auto\" ref={containerRef}>\n                    <Table ref={contentRef}>\n                        <TableBody>\n                            {sortedShares.map((share) => (\n                                <TableRow key={share.id}>\n                                    <TableCell className=\"space-y-1\">\n                                        <div className=\"font-medium\">\n                                            {share.name}\n                                        </div>\n                                    </TableCell>\n\n                                    <TableCell>\n                                        <StatusBadge status={share.enable ? 'enabled' : 'disabled'} />\n                                    </TableCell>\n\n                                    <TableCell>\n                                        <div>\n                                            访问: <span className=\"text-muted-foreground\">\n                                                {formatAccessCount(share.access_count, share.max_access_count)}\n                                            </span>\n                                        </div>\n                                    </TableCell>\n\n                                    <TableCell>\n                                        <div>\n                                            过期日期: <span className=\"text-muted-foreground\">\n                                                {formatExpiresTime(share.expires)}\n                                            </span>\n                                        </div>\n                                    </TableCell>\n\n                                    <TableCell className={`text-right sticky right-0 bg-background ${isOverflowing ? 'shadow-[-4px_0_8px_-2px_rgba(0,0,0,0.1)]' : ''}`}>\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => onCopyClick(share.token)}\n                                            title={UI_TEXT.COPY}\n                                        >\n                                            <Copy className=\"h-4 w-4\" />\n                                        </Button>\n\n                                        <Button\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                            onClick={() => onEdit(share)}\n                                            title=\"编辑\"\n                                        >\n                                            <Edit className=\"h-4 w-4\" />\n                                        </Button>\n\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => onDeleteClick(share.id, share.name)}\n                                            title={UI_TEXT.DELETE}\n                                        >\n                                            <Trash2 className=\"h-4 w-4\" />\n                                        </Button>\n                                    </TableCell>\n                                </TableRow>\n                            ))}\n                        </TableBody>\n                    </Table>\n                </div>\n            </CardContent>\n        </Card>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/share/components/share-page.tsx",
    "content": "import { useState, useCallback } from \"react\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Plus } from \"lucide-react\"\nimport { ShareForm } from \"./share-form\"\nimport { ShareList } from \"./share-list\"\nimport { ShareCopyDialog } from \"./share-copy\"\nimport { UI_TEXT } from \"../constants\"\nimport type { ShareResponse, ShareRequest } from \"@/src/types/share\"\n\nexport function SharePage() {\n    // 表单状态\n    const [isDialogOpen, setIsDialogOpen] = useState(false)\n    const [editingShare, setEditingShare] = useState<ShareResponse | null>(null)\n    const [formData, setFormData] = useState<ShareRequest | undefined>(undefined)\n\n    // 复制对话框状态\n    const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false)\n    const [copyUrl, setCopyUrl] = useState('')\n\n    // 打开编辑对话框\n    const openEditDialog = useCallback((share: ShareResponse) => {\n        setEditingShare(share)\n        // 从 ShareResponse 转换为 ShareRequest，移除只读字段\n        const { id: _id, access_count: _access_count, ...shareRequest } = share\n        setFormData(shareRequest)\n        setIsDialogOpen(true)\n    }, [])\n\n    // 打开创建对话框\n    const openCreateDialog = useCallback(() => {\n        setEditingShare(null)\n        setFormData(undefined)\n        setIsDialogOpen(true)\n    }, [])\n\n    // 关闭表单对话框\n    const closeFormDialog = useCallback(() => {\n        setIsDialogOpen(false)\n        // 延迟清理状态，等待对话框关闭动画完成\n        setTimeout(() => {\n            setEditingShare(null)\n            setFormData(undefined)\n        }, 200) // 对话框关闭动画通常是 150-200ms\n    }, [])\n\n    // 打开复制对话框\n    const openCopyDialog = useCallback((fullUrl: string) => {\n        setCopyUrl(fullUrl)\n        setIsCopyDialogOpen(true)\n    }, [])\n\n    // 关闭复制对话框\n    const closeCopyDialog = useCallback(() => {\n        setIsCopyDialogOpen(false)\n        setCopyUrl('')\n    }, [])\n\n    // 获取表单标题\n    const formTitle = editingShare ? UI_TEXT.EDIT_SHARE : UI_TEXT.CREATE_SHARE\n\n    return (\n        <div className=\"flex flex-col gap-4 py-4 md:gap-6 md:py-6\">\n            {/* 页面头部 */}\n            <div className=\"flex items-center justify-between px-4 lg:px-6\">\n                <div>\n                    <h1 className=\"text-2xl font-bold\">分享管理</h1>\n                </div>\n\n                <Button onClick={openCreateDialog}>\n                    <Plus className=\"h-4 w-4 mr-2\" />\n                    {UI_TEXT.CREATE_SHARE}\n                </Button>\n            </div>\n\n            {/* 分享表单对话框 */}\n            <ShareForm\n                {...(formData && { initialData: formData })}\n                formTitle={formTitle}\n                isOpen={isDialogOpen}\n                onClose={closeFormDialog}\n                editingShareId={editingShare?.id}\n            />\n\n            {/* 分享列表 */}\n            <div className=\"px-4 lg:px-6\">\n                <ShareList\n                    onEdit={openEditDialog}\n                    openCopyDialog={openCopyDialog}\n                />\n            </div>\n\n            {/* 复制链接对话框 */}\n            <ShareCopyDialog\n                fullUrl={copyUrl}\n                isOpen={isCopyDialogOpen}\n                onClose={closeCopyDialog}\n            />\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/share/constants/index.ts",
    "content": "export const SHARE_CONSTANTS = {\n    TOKEN_LENGTH: 32,\n    DEFAULT_EXPIRES_HOURS: 0,\n    DEFAULT_RULE_URL: 'https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_NoAuto.ini',\n    DEFAULT_RENAME_TEMPLATE: '{{.Country.Emoji}}{{.Country.NameZh}} {{.Delay}} {{.Count}}',\n} as const\n\nexport const SUBSCRIPTION_TARGETS = [\n    { value: 'qx', label: 'QX' },\n    { value: 'QuantumultX', label: 'Quantumult X' },\n    { value: 'surge', label: 'Surge' },\n    { value: 'SurgeMac', label: 'SurgeMac' },\n    { value: 'Loon', label: 'Loon' },\n    { value: 'mihomo', label: 'Mihomo' },\n    { value: 'uri', label: 'URI' },\n    { value: 'v2', label: 'V2Ray' },\n    { value: 'json', label: 'JSON' },\n    { value: 'stash', label: 'Stash' },\n    { value: 'shadowrocket', label: 'Shadowrocket' },\n    { value: 'surfboard', label: 'Surfboard' },\n    { value: 'singbox', label: 'Sing-Box' },\n    { value: 'egern', label: 'Egern' },\n] as const\n\nexport const FORM_VALIDATION = {\n    NAME_REQUIRED: '请输入分享名称',\n    POSITIVE_NUMBER: '请输入正数',\n    VALID_COUNTRY_CODE: '请输入有效的国家代码',\n} as const\n\nexport const UI_TEXT = {\n    CREATE_SHARE: '创建分享',\n    EDIT_SHARE: '编辑分享',\n    UPDATE: '更新',\n    CREATE: '创建',\n    CANCEL: '取消',\n    DELETE: '删除',\n    COPY: '复制',\n    LOADING: '加载中...',\n    NO_DATA: '暂无数据',\n    CONFIRM_DELETE: '确认删除',\n    DELETE_CONFIRM_MESSAGE: '您确定要删除分享 \"{name}\" 吗？此操作无法撤销。',\n    COPY_SUCCESS: '复制成功',\n    COPY_FAILED: '复制失败',\n    CREATE_SUCCESS: '分享创建成功',\n    UPDATE_SUCCESS: '分享更新成功',\n    DELETE_SUCCESS: '分享删除成功',\n    CREATE_FAILED: '创建分享失败',\n    UPDATE_FAILED: '更新分享失败',\n    DELETE_FAILED: '删除分享失败',\n} as const"
  },
  {
    "path": "web/src/components/features/share/constants/sub-rules.ts",
    "content": "import type { KeyValue } from \"@/src/types\"\r\n\r\nexport const SUB_RULES: KeyValue[] = [\r\n  {\r\n    \"key\": \"默认\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_NoAuto.ini\"\r\n  },\r\n  {\r\n    \"key\": \"默认（自动测速）\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_AdblockPlus.ini\"\r\n  },\r\n  {\r\n    \"key\": \"默认（索尼电视专用）\",\r\n    \"value\": \"https://raw.githubusercontent.com/youshandefeiyang/webcdn/main/SONY.ini\"\r\n  },\r\n  {\r\n    \"key\": \"默认（附带用于 Clash 的 AdGuard DNS）\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/default_with_clash_adg.yml\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_全分组 Dream修改版\",\r\n    \"value\": \"https://raw.githubusercontent.com/WC-Dream/ACL4SSR/WD/Clash/config/ACL4SSR_Online_Full_Dream.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_精简分组 Dream修改版\",\r\n    \"value\": \"https://raw.githubusercontent.com/WC-Dream/ACL4SSR/WD/Clash/config/ACL4SSR_Mini_Dream.ini\"\r\n  },\r\n  {\r\n    \"key\": \"emby-TikTok-流媒体分组-去广告加强版\",\r\n    \"value\": \"https://raw.githubusercontent.com/justdoiting/ClashRule/main/GeneralClashRule.ini\"\r\n  },\r\n  {\r\n    \"key\": \"流媒体通用分组\",\r\n    \"value\": \"https://raw.githubusercontent.com/cutethotw/ClashRule/main/GeneralClashRule.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_默认版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_无测速版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_NoAuto.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_去广告版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_AdblockPlus.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_多国家版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_MultiCountry.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_无Reject版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_NoReject.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_无测速精简版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Mini_NoAuto.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_全分组版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_全分组谷歌版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_Google.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_全分组多模式版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_MultiMode.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_全分组奈飞版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_Netflix.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_精简版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Mini.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_去广告精简版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Mini_AdblockPlus.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_Fallback精简版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Mini_Fallback.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_多国家精简版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Mini_MultiCountry.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ACL_多模式精简版\",\r\n    \"value\": \"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Mini_MultiMode.ini\"\r\n  },\r\n  {\r\n    \"key\": \"常规规则\",\r\n    \"value\": \"https://raw.githubusercontent.com/flyhigherpi/merlinclash_clash_related/master/Rule_config/ZHANG.ini\"\r\n  },\r\n  {\r\n    \"key\": \"OoHHHHHHH\",\r\n    \"value\": \"https://raw.githubusercontent.com/OoHHHHHHH/ini/master/config.ini\"\r\n  },\r\n  {\r\n    \"key\": \"CFW-TAP\",\r\n    \"value\": \"https://raw.githubusercontent.com/OoHHHHHHH/ini/master/cfw-tap.ini\"\r\n  },\r\n  {\r\n    \"key\": \"lhl77全分组（定期更新）\",\r\n    \"value\": \"https://raw.githubusercontent.com/lhl77/sub-ini/main/tsutsu-full.ini\"\r\n  },\r\n  {\r\n    \"key\": \"lhl77简易版（定期更新）\",\r\n    \"value\": \"https://raw.githubusercontent.com/lhl77/sub-ini/main/tsutsu-mini-gfw.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ConnersHua 神机规则 Outbound\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/connershua_new.ini\"\r\n  },\r\n  {\r\n    \"key\": \"ConnersHua 神机规则 Inbound 回国专用\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/connershua_backtocn.ini\"\r\n  },\r\n  {\r\n    \"key\": \"lhie1 洞主规则（使用 Clash 分组规则）\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/lhie1_clash.ini\"\r\n  },\r\n  {\r\n    \"key\": \"lhie1 洞主规则完整版\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/lhie1_dler.ini\"\r\n  },\r\n  {\r\n    \"key\": \"eHpo1 规则\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/ehpo1_main.ini\"\r\n  },\r\n\r\n\r\n\r\n\r\n  {\r\n    \"key\": \"品云专属配置（仅香港区域分组）\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazeorz/airports/master/Clash/Examine.ini\"\r\n  },\r\n  {\r\n    \"key\": \"品云专属配置（全地域分组）\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazeorz/airports/master/Clash/Examine_Full.ini\"\r\n  },\r\n  {\r\n    \"key\": \"nzw9314 规则\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/nzw9314_custom.ini\"\r\n  },\r\n  {\r\n    \"key\": \"maicoo-l 规则\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/maicoo-l_custom.ini\"\r\n  },\r\n  {\r\n    \"key\": \"DlerCloud Platinum 李哥定制规则\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/dlercloud_lige_platinum.ini\"\r\n  },\r\n  {\r\n    \"key\": \"DlerCloud Gold 李哥定制规则\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/dlercloud_lige_gold.ini\"\r\n  },\r\n  {\r\n    \"key\": \"DlerCloud Silver 李哥定制规则\",\r\n    \"value\": \"https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/dlercloud_lige_silver.ini\"\r\n  },\r\n\r\n  {\r\n    \"key\": \"ShellClash修改版规则 (by UlinoyaPed)\",\r\n    \"value\": \"https://github.com/UlinoyaPed/ShellClash/raw/master/rules/ShellClash.ini\"\r\n  },\r\n\r\n\r\n\r\n  {\r\n    \"key\": \"CNIX\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazeorz/airports/master/Clash/SSRcloud.ini\"\r\n  },\r\n  {\r\n    \"key\": \"Nirvana\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazetsz/ACL4SSR/master/Clash/config/V2rayPro.ini\"\r\n  },\r\n  {\r\n    \"key\": \"V2Pro\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazeorz/airports/master/Clash/V2Pro.ini\"\r\n  },\r\n  {\r\n    \"key\": \"史迪仔-自动测速\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazeorz/airports/master/Clash/Stitch.ini\"\r\n  },\r\n  {\r\n    \"key\": \"史迪仔-负载均衡\",\r\n    \"value\": \"https://raw.githubusercontent.com/Mazeorz/airports/master/Clash/Stitch-Balance.ini\"\r\n  },\r\n  {\r\n    \"key\": \"Maying\",\r\n    \"value\": \"https://raw.githubusercontent.com/SleepyHeeead/subconverter-config/master/remote-config/customized/maying.ini\"\r\n  },\r\n  {\r\n    \"key\": \"Basic\",\r\n    \"value\": \"https://raw.githubusercontent.com/SleepyHeeead/subconverter-config/master/remote-config/special/basic.ini\"\r\n  }\r\n] as const"
  },
  {
    "path": "web/src/components/features/share/hooks/index.ts",
    "content": "export { useShareForm } from './useShareForm'\nexport { useShareOperations } from './useShareOperations'"
  },
  {
    "path": "web/src/components/features/share/hooks/useShareForm.ts",
    "content": "import { useForm } from 'react-hook-form'\nimport { useEffect, useMemo } from 'react'\nimport { toast } from 'sonner'\nimport { useCreateShare, useUpdateShare } from '@/src/lib/queries/share-queries'\nimport { generateToken, createDefaultShareData } from '../utils'\nimport { UI_TEXT } from '../constants'\nimport type { ShareRequest } from '@/src/types'\n\ninterface UseShareFormProps {\n    initialData?: ShareRequest | undefined\n    editingShareId?: number | undefined\n    onSuccess?: () => void\n    isOpen?: boolean\n}\n\nexport function useShareForm({\n    initialData,\n    editingShareId,\n    onSuccess,\n    isOpen = true\n}: UseShareFormProps = {}) {\n    const createShareMutation = useCreateShare()\n    const updateShareMutation = useUpdateShare()\n\n    const defaultData = useMemo(() => createDefaultShareData(), [])\n\n    const form = useForm<ShareRequest>({\n        defaultValues: initialData || defaultData\n    })\n\n    const { handleSubmit, reset, watch } = form\n\n    useEffect(() => {\n        if (isOpen) {\n            reset(initialData || defaultData)\n        }\n    }, [initialData, reset, defaultData, isOpen])\n\n    const onSubmit = async (data: ShareRequest) => {\n        try {\n            const token = data.token || generateToken()\n            const submitData: ShareRequest = {\n                ...data,\n                token,\n            }\n\n            if (editingShareId) {\n                await updateShareMutation.mutateAsync({ id: editingShareId, data: submitData })\n                toast.success(UI_TEXT.UPDATE_SUCCESS)\n            } else {\n                await createShareMutation.mutateAsync(submitData)\n                toast.success(UI_TEXT.CREATE_SUCCESS)\n            }\n\n            onSuccess?.()\n        } catch (error) {\n            const errorMessage = editingShareId ? UI_TEXT.UPDATE_FAILED : UI_TEXT.CREATE_FAILED\n            toast.error(errorMessage)\n            console.error('Failed to save share:', error)\n        }\n    }\n\n    const isLoading = editingShareId\n        ? updateShareMutation.isPending\n        : createShareMutation.isPending\n\n    return {\n        form,\n        onSubmit: handleSubmit(onSubmit),\n        watch,\n        isEditing: !!editingShareId,\n        isLoading,\n    }\n}\n"
  },
  {
    "path": "web/src/components/features/share/hooks/useShareOperations.ts",
    "content": "import { toast } from 'sonner'\nimport { useAlert } from '@/src/components/providers'\nimport { useDeleteShare } from '@/src/lib/queries/share-queries'\nimport { copyToClipboard, buildShareUrl } from '../utils'\nimport { UI_TEXT } from '../constants'\n\nexport function useShareOperations() {\n    const { confirm } = useAlert()\n    const deleteShareMutation = useDeleteShare()\n\n    const handleDelete = async (id: number, name: string) => {\n        const confirmed = await confirm({\n            title: UI_TEXT.CONFIRM_DELETE,\n            description: UI_TEXT.DELETE_CONFIRM_MESSAGE.replace('{name}', name),\n            confirmText: UI_TEXT.DELETE,\n            cancelText: UI_TEXT.CANCEL,\n            variant: 'destructive'\n        })\n\n        if (confirmed) {\n            try {\n                await deleteShareMutation.mutateAsync(id)\n                toast.success(UI_TEXT.DELETE_SUCCESS)\n            } catch (error) {\n                toast.error(UI_TEXT.DELETE_FAILED)\n                console.error('Failed to delete share:', error)\n            }\n        }\n    }\n\n    const handleCopy = async (token: string, onFallback?: (url: string) => void) => {\n        const fullUrl = buildShareUrl(token)\n\n        const success = await copyToClipboard(fullUrl)\n\n        if (success) {\n            toast.success(UI_TEXT.COPY_SUCCESS)\n        } else {\n            if (onFallback) {\n                onFallback(fullUrl)\n            } else {\n                toast.error(UI_TEXT.COPY_FAILED)\n            }\n        }\n    }\n\n    return {\n        handleDelete,\n        handleCopy,\n        isDeleting: deleteShareMutation.isPending,\n    }\n}"
  },
  {
    "path": "web/src/components/features/share/index.ts",
    "content": "export { SharePage } from './components/share-page'"
  },
  {
    "path": "web/src/components/features/share/utils/index.ts",
    "content": "import { SHARE_CONSTANTS } from '../constants'\nimport type { ShareRequest } from '@/src/types'\n\n/**\n * 生成随机 token\n */\nexport function generateToken(): string {\n    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'\n    let token = ''\n    for (let i = 0; i < SHARE_CONSTANTS.TOKEN_LENGTH; i++) {\n        token += chars.charAt(Math.floor(Math.random() * chars.length))\n    }\n    return token\n}\n\n/**\n * 构建分享链接\n */\nexport function buildShareUrl(token: string, baseUrl?: string): string {\n    const origin = baseUrl || (typeof window !== 'undefined' ? window.location.origin : '')\n    return `${origin}/api/v1/share/sub/${token}`\n}\n\n/**\n * 复制文本到剪贴板\n */\nexport async function copyToClipboard(text: string): Promise<boolean> {\n    // 优先使用现代 Clipboard API\n    if (navigator.clipboard && window.isSecureContext) {\n        try {\n            await navigator.clipboard.writeText(text)\n            return true\n        } catch (error) {\n            console.warn('Clipboard API failed:', error)\n            return fallbackCopyToClipboard(text)\n        }\n    }\n\n    // 降级到传统方法\n    return fallbackCopyToClipboard(text)\n}\n\n/**\n * 降级复制方法\n */\nfunction fallbackCopyToClipboard(text: string): boolean {\n    const textArea = document.createElement('textarea')\n    textArea.value = text\n    textArea.style.cssText = `\n    position: fixed;\n    top: 0;\n    left: 0;\n    opacity: 0;\n    pointer-events: none;\n  `\n\n    document.body.appendChild(textArea)\n    textArea.focus()\n    textArea.select()\n\n    try {\n        const successful = document.execCommand('copy')\n        return successful\n    } catch (error) {\n        console.warn('Fallback copy failed:', error)\n        return false\n    } finally {\n        document.body.removeChild(textArea)\n    }\n}\n\n/**\n * 创建默认的分享表单数据\n */\nexport function createDefaultShareData(): ShareRequest {\n    return {\n        name: '',\n        enable: true,\n        token: '',\n        gen: {\n            filter: {\n                sub_id_exclude: false,\n                country_exclude: false,\n                sub_id: [],\n                speed_up_more: 0,\n                speed_down_more: 0,\n                country: [],\n                delay_less_than: 0,\n                alive_status: 0,\n                risk_less_than: 0,\n            },\n            rename: SHARE_CONSTANTS.DEFAULT_RENAME_TEMPLATE,\n            proxy: false,\n            target: 'auto',\n        },\n        max_access_count: 0,\n        expires: SHARE_CONSTANTS.DEFAULT_EXPIRES_HOURS,\n    }\n}\n\n/**\n * 验证国家代码格式\n */\nexport function validateCountryCodes(codes: string): string[] {\n    return codes\n        .split(',')\n        .map(code => code.trim())\n        .filter(Boolean)\n        .filter(code => /^\\d+$/.test(code)) // 只允许数字\n}\n\n/**\n * 格式化访问次数显示\n */\nexport function formatAccessCount(current: number, max: number): string {\n    return `${current}/${max === 0 ? '∞' : max}`\n}\n\n/**\n * 格式化过期时间显示\n */\nexport function formatExpiresTime(expires: number): string {\n    if (expires === 0) {\n        return '永不过期'\n    }\n    const date = new Date(expires * 1000)\n    return date.toLocaleDateString('zh-CN', {\n        year: 'numeric',\n        month: '2-digit',\n        day: '2-digit',\n    })\n}\n\n/**\n * 检查是否为自定义规则配置\n */\nexport function isCustomConfig(config: string, availableRules: Array<{ value: string }>): boolean {\n    return !availableRules.some(rule => rule.value === config)\n}\n\n/**\n * 安全的数字转换\n */\nexport function safeParseInt(value: string, defaultValue = 0): number {\n    const parsed = parseInt(value, 10)\n    return isNaN(parsed) ? defaultValue : parsed\n}\n\n/**\n * 安全的浮点数转换\n */\nexport function safeParseFloat(value: string, defaultValue = 0): number {\n    const parsed = parseFloat(value)\n    return isNaN(parsed) ? defaultValue : parsed\n}"
  },
  {
    "path": "web/src/components/features/storage/storage.tsx",
    "content": "\"use client\"\n\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/src/components/ui/card\"\n\nexport function StoragePage() {\n  return (\n    <div className=\"flex flex-col gap-4 py-4 md:gap-6 md:py-6\">\n      <div className=\"flex items-center justify-between px-4 lg:px-6\">\n        <div>\n          <h1 className=\"text-2xl font-bold\">存储配置</h1>\n          <p className=\"text-muted-foreground\">配置您的存储设置</p>\n        </div>\n      </div>\n\n      <div className=\"px-4 lg:px-6\">\n        <Card>\n          <CardHeader>\n            <CardTitle>存储设置</CardTitle>\n          </CardHeader>\n          <CardContent>\n            <div className=\"text-center py-8 text-muted-foreground\">\n              存储配置功能正在开发中...\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "web/src/components/features/sub/components/batch-sub-form.tsx",
    "content": "import { useState } from 'react'\nimport { useForm, Control } from 'react-hook-form'\nimport { Button } from '@/src/components/ui/button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/src/components/ui/dialog'\nimport { Textarea } from '@/src/components/ui/textarea'\nimport { Label } from '@/src/components/ui/label'\nimport { toast } from 'sonner'\nimport { ConfigSection } from './form-sections'\nimport { useBatchCreateSub } from '@/src/lib/queries/sub-queries'\nimport { generateNameFromUrl } from '../utils'\nimport type { SubRequest } from '@/src/types/sub'\n\ninterface BatchSubFormProps {\n    isOpen: boolean\n    onClose: () => void\n}\n\ninterface BatchFormData extends SubRequest {\n    urls: string\n}\n\nexport function BatchSubForm({ isOpen, onClose }: BatchSubFormProps) {\n    const [isSubmitting, setIsSubmitting] = useState(false)\n    const batchCreateMutation = useBatchCreateSub()\n\n    const form = useForm<BatchFormData>({\n        defaultValues: {\n            urls: '',\n            name: '',\n            tags: [],\n            enable: true,\n            cron_expr: '0 */1 * * *',\n            config: {\n                url: '',\n                proxy: false,\n                timeout: 10,\n            },\n        }\n    })\n\n    const { control, handleSubmit, reset } = form\n\n    const onSubmit = async (data: BatchFormData) => {\n        setIsSubmitting(true)\n\n        try {\n            const urls = data.urls\n                .split('\\n')\n                .map(url => url.trim())\n                .filter(url => url && /^https?:\\/\\/.+/.test(url))\n\n            if (urls.length === 0) {\n                toast.error('请输入至少一个有效的订阅链接')\n                return\n            }\n\n            const subscriptions: SubRequest[] = urls.map(url => ({\n                name: generateNameFromUrl(url) || '未知订阅',\n                tags: data.tags || [],\n                enable: data.enable,\n                cron_expr: data.cron_expr,\n                config: {\n                    url,\n                    proxy: data.config.proxy || false,\n                    timeout: data.config.timeout || 10,\n                },\n            }))\n\n            const results = await batchCreateMutation.mutateAsync(subscriptions)\n\n            const successCount = results.length\n\n            if (successCount > 0) {\n                toast.success(`成功添加 ${successCount} 个订阅`)\n                reset()\n                onClose()\n            }\n\n        } catch (error) {\n            toast.error('批量添加失败')\n            console.error('Failed to batch create subscriptions:', error)\n        } finally {\n            setIsSubmitting(false)\n        }\n    }\n\n    const handleClose = () => {\n        reset()\n        onClose()\n    }\n\n    return (\n        <Dialog open={isOpen} onOpenChange={handleClose}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto scrollbar-hide\">\n                <DialogHeader>\n                    <DialogTitle>批量添加订阅</DialogTitle>\n                </DialogHeader>\n\n                <form onSubmit={handleSubmit(onSubmit)} className=\"space-y-6\">\n                    <div className=\"space-y-2\">\n                        <Label htmlFor=\"urls\" className=\"mb-2 block\">\n                            订阅链接\n                        </Label>\n                        <Textarea\n                            id=\"urls\"\n                            placeholder={`https://example.com/subscription1\nhttps://example.com/subscription2\nhttps://example.com/subscription3`}\n                            className=\"min-h-[120px] resize-none\"\n                            {...form.register('urls', {\n                                required: '请输入至少一个订阅链接'\n                            })}\n                        />\n                        <p className=\"text-xs text-muted-foreground\">\n                            每行输入一个订阅链接，系统会自动为每个链接生成名称\n                        </p>\n                    </div>\n\n                    <div className=\"space-y-4\">\n                        <ConfigSection control={control as unknown as Control<SubRequest>} />\n                    </div>\n\n                    <div className=\"flex gap-2 pt-4\">\n                        <Button\n                            type=\"submit\"\n                            className=\"flex-1\"\n                            disabled={isSubmitting}\n                        >\n                            {isSubmitting ? '添加中...' : '批量添加'}\n                        </Button>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={handleClose}\n                            disabled={isSubmitting}\n                        >\n                            取消\n                        </Button>\n                    </div>\n                </form>\n            </DialogContent>\n        </Dialog>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/sub/components/form-sections/basic-info-section.tsx",
    "content": "import { Controller, Control, useController } from 'react-hook-form'\nimport { Input } from '@/src/components/ui/input'\nimport { Label } from '@/src/components/ui/label'\nimport type { SubRequest } from '@/src/types/sub'\nimport { generateNameFromUrl } from '../../utils'\nimport { Badge } from \"@/src/components/ui/badge\";\nimport { Button } from \"@/src/components/ui/button\";\nimport { useState } from \"react\";\n\nexport function BasicInfoSection({ control }: { control: Control<SubRequest> }) {\n    const { field: nameField } = useController({\n        name: 'name',\n        control\n    })\n\n    const [tag, setTag] = useState(\"\")\n    const [tagError, setTagError] = useState(\"\")\n\n    return (\n        <div className=\"space-y-4\">\n            <div className=\"space-y-2\">\n                <Label htmlFor=\"url\" className=\"mb-2 block\">订阅链接</Label>\n                <Controller\n                    name=\"config.url\"\n                    control={control}\n                    rules={{\n                        required: '请输入有效的订阅链接',\n                        pattern: {\n                            value: /^https?:\\/\\/.+/,\n                            message: '请输入有效的URL (http:// 或 https://)'\n                        }\n                    }}\n                    render={({ field, fieldState }) => (\n                        <>\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"url\"\n                                type=\"url\"\n                                placeholder=\"https://example.com/subscription\"\n                                onChange={(e) => {\n                                    const url = e.target.value\n                                    field.onChange(url)\n\n                                    if (url && /^https?:\\/\\/.+/.test(url)) {\n                                        if (!nameField.value || nameField.value.trim() === '') {\n                                            const generatedName = generateNameFromUrl(url)\n                                            if (generatedName) {\n                                                nameField.onChange(generatedName)\n                                            }\n                                        }\n                                    }\n                                }}\n                            />\n                            {fieldState.error && (\n                                <p className=\"text-xs text-red-500 mt-1\">{fieldState.error.message}</p>\n                            )}\n                        </>\n                    )}\n                />\n            </div>\n            <div className=\"space-y-2\">\n                <Label htmlFor=\"name\" className=\"mb-2 block\">订阅名称</Label>\n                <Controller\n                    name=\"name\"\n                    control={control}\n                    rules={{ required: '请输入订阅名称' }}\n                    render={({ field, fieldState }) => (\n                        <>\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"name\"\n                                placeholder=\"输入订阅名称\"\n                            />\n                            {fieldState.error && (\n                                <p className=\"text-xs text-red-500 mt-1\">{fieldState.error.message}</p>\n                            )}\n                        </>\n                    )}\n                />\n            </div>\n            <div className=\"space-y-2\">\n                <Label htmlFor=\"tags\" className=\"mb-2 block\">订阅标签</Label>\n                <Controller\n                    name=\"tags\"\n                    control={control}\n                    render={({ field }) => (\n                        <>\n                            {\n                                field.value.length > 0 && (\n                                    <div className=\"flex flex-wrap gap-2\">\n                                        {field.value.map(option => {\n                                            return (\n                                                <Badge\n                                                    key={option}\n                                                    variant={'default'}\n                                                    className={`cursor-pointer transition-colors hover:bg-red-100 hover:text-red-700`}\n                                                    onClick={() => {\n                                                        const index = field.value.indexOf(option)\n                                                        field.value.splice(index, 1)\n                                                        field.onChange(field.value)\n                                                    }}\n                                                >\n                                                    {option} {'×'}\n                                                </Badge>\n                                            )\n                                        })}\n                                    </div>\n                                )\n                            }\n                            <div className=\"flex gap-2 pt-4\">\n                                <Input\n                                    placeholder=\"新增订阅标签\"\n                                    className=\"flex-1\"\n                                    disabled={field.disabled}\n                                    value={tag}\n                                    onChange={(e) => setTag(e.target.value)}\n                                />\n                                <Button\n                                    type=\"button\"\n                                    variant=\"outline\"\n                                    disabled={field.disabled}\n                                    onClick={() => {\n                                        if (field.value.indexOf(tag) >= 0) {\n                                            setTagError(\"标签已存在\")\n                                        } else if (tag.trim() === \"\") {\n                                            setTagError(\"请输入标签\")\n                                        } else {\n                                            setTagError(\"\")\n                                            field.value.push(tag)\n                                            setTag(\"\")\n                                        }\n                                    }}\n                                >\n                                    添加\n                                </Button>\n                            </div>\n                            {tagError !== \"\" && (\n                                <p className=\"text-xs text-red-500 mt-1\">{tagError}</p>\n                            )}\n                        </>\n                    )}\n                />\n            </div>\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/sub/components/form-sections/config-section.tsx",
    "content": "import { Controller, Control } from 'react-hook-form'\nimport { Label } from '@/src/components/ui/label'\nimport { Input } from '@/src/components/ui/input'\nimport { Switch } from '@/src/components/ui/switch'\nimport type { SubRequest } from '@/src/types/sub'\n\nexport function ConfigSection({ control }: { control: Control<SubRequest> }) {\n    return (\n        <div className=\"space-y-4\">\n            <div className=\"grid grid-cols-2 gap-4 gap-y-4\">\n                <Label htmlFor=\"cron_expr\">更新频率</Label>\n                <Label htmlFor=\"timeout\">超时时间（秒）</Label>\n\n                <Controller\n                    name=\"cron_expr\"\n                    control={control}\n                    rules={{ required: '请输入有效的Cron表达式' }}\n                    render={({ field, fieldState }) => (\n                        <>\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"cron_expr\"\n                                placeholder=\"0 */6 * * *\"\n                            />\n                            {fieldState.error && (\n                                <p className=\"text-xs text-red-500 mt-1\">{fieldState.error.message}</p>\n                            )}\n                        </>\n                    )}\n                />\n\n                <Controller\n                    name=\"config.timeout\"\n                    control={control}\n                    rules={{\n                        required: '请输入超时时间',\n                        min: { value: 1, message: '超时时间必须大于0' },\n                        max: { value: 300, message: '超时时间不能超过300秒' }\n                    }}\n                    render={({ field, fieldState }) => (\n                        <>\n                            <Input\n                                {...field}\n                                value={field.value || ''}\n                                id=\"timeout\"\n                                type=\"number\"\n                                placeholder=\"10\"\n                                min=\"1\"\n                                max=\"300\"\n                                onChange={(e) => field.onChange(Number(e.target.value))}\n                            />\n                            {fieldState.error && (\n                                <p className=\"text-xs text-red-500 mt-1\">{fieldState.error.message}</p>\n                            )}\n                        </>\n                    )}\n                />\n            </div>\n\n            <div className=\"grid grid-cols-[1fr_auto] items-center gap-4 gap-y-4\">\n                <Label htmlFor=\"proxy\">启用代理</Label>\n                <Controller\n                    name=\"config.proxy\"\n                    control={control}\n                    render={({ field }) => (\n                        <Switch\n                            id=\"proxy\"\n                            checked={field.value || false}\n                            onCheckedChange={field.onChange}\n                        />\n                    )}\n                />\n\n                <Label htmlFor=\"enable\">启用订阅</Label>\n                <Controller\n                    name=\"enable\"\n                    control={control}\n                    render={({ field }) => (\n                        <Switch\n                            id=\"enable\"\n                            checked={field.value || false}\n                            onCheckedChange={field.onChange}\n                        />\n                    )}\n                />\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "web/src/components/features/sub/components/form-sections/index.ts",
    "content": "export { BasicInfoSection } from './basic-info-section'\nexport { ConfigSection } from './config-section'\nexport { ProtocolFilterSection } from './protocol-filter-section'\n"
  },
  {
    "path": "web/src/components/features/sub/components/form-sections/protocol-filter-section.tsx",
    "content": "import { Control, Controller, useWatch } from 'react-hook-form'\nimport { Label } from '@/src/components/ui/label'\nimport { Switch } from '@/src/components/ui/switch'\nimport { Badge } from '@/src/components/ui/badge'\nimport { PROTOCOL_OPTIONS } from '@/src/constant/protocols'\nimport type { SubRequest } from '@/src/types/sub'\n\nexport function ProtocolFilterSection({ control }: { control: Control<SubRequest> }) {\n    const enable = useWatch({ control, name: 'config.protocol_filter_enable' }) as boolean | undefined\n    return (\n        <div className=\"space-y-4\">\n            <div className=\"flex items-start justify-between gap-4\">\n                <Label className=\"font-medium\">协议过滤</Label>\n                <Controller\n                    name=\"config.protocol_filter_enable\"\n                    control={control}\n                    render={({ field }) => (\n                        <Switch\n                            checked={field.value || false}\n                            onCheckedChange={field.onChange}\n                        />\n                    )}\n                />\n            </div>\n\n            {enable && (\n                <div className=\"space-y-4\">\n                    <div className=\"flex items-start justify-between gap-4\">\n                        <div className=\"space-y-1\">\n                            <Label className=\"font-medium\">排除模式</Label>\n                            <p className=\"text-xs text-muted-foreground\">关闭为包含以下协议,打开为排除以下协议</p>\n                        </div>\n\n                        <Controller\n                            name=\"config.protocol_filter_mode\"\n                            control={control}\n                            render={({ field: modeField }) => (\n                                <Switch\n                                    checked={modeField.value || false}\n                                    onCheckedChange={modeField.onChange}\n                                />\n                            )}\n                        />\n                    </div>\n                    <Controller\n                        name=\"config.protocol_filter\"\n                        control={control}\n                        render={({ field: filterField }) => {\n                            const selectedValues = Array.isArray(filterField.value) ? filterField.value : []\n\n                            const handleToggle = (value: string) => {\n                                const isSelected = selectedValues.includes(value)\n                                const nextValues = isSelected\n                                    ? selectedValues.filter(item => item !== value)\n                                    : [...selectedValues, value]\n\n                                filterField.onChange(nextValues)\n                            }\n\n                            return (\n                                <div className=\"space-y-2\">\n                                    <Label className=\"font-medium\">选择协议</Label>\n                                    <div className=\"flex flex-wrap gap-2\">\n                                        {PROTOCOL_OPTIONS.map(option => {\n                                            const isSelected = selectedValues.includes(option)\n\n                                            return (\n                                                <Badge\n                                                    key={option}\n                                                    variant={isSelected ? 'default' : 'outline'}\n                                                    className={`cursor-pointer transition-colors ${isSelected\n                                                        ? 'hover:bg-red-100 hover:text-red-700'\n                                                        : 'hover:bg-green-100 hover:text-green-700'\n                                                        }`}\n                                                    onClick={() => handleToggle(option)}\n                                                >\n                                                    {option} {isSelected ? '×' : '+'}\n                                                </Badge>\n                                            )\n                                        })}\n                                    </div>\n                                </div>\n                            )\n                        }}\n                    />\n                </div>\n            )}\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/sub/components/sub-detail.tsx",
    "content": "import { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/src/components/ui/dialog\"\nimport { formatTime, formatLastRunTime, getNextCronRunTime, formatDuration } from \"@/src/utils\"\nimport StatusBadge from \"@/src/components/shared/status-badge\"\nimport type { SubResponse } from \"@/src/types/sub\"\n\ninterface SubscriptionDetailProps {\n    subscription: SubResponse | null\n    isOpen: boolean\n    onOpenChange: (open: boolean) => void\n}\n\nexport function SubDetail({\n    subscription,\n    isOpen,\n    onOpenChange,\n}: SubscriptionDetailProps) {\n    if (!subscription) return null\n\n    const formatSpeed = (speed: number) => ((speed || 0) / 1024 / 1024).toFixed(2)\n    const getRiskColor = (risk: number) => {\n        if (risk === 0) return 'text-green-600'\n        if (risk <= 3) return 'text-yellow-600'\n        return 'text-red-600'\n    }\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onOpenChange}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto\">\n                <DialogHeader>\n                    <DialogTitle>订阅详情 - {subscription.name}</DialogTitle>\n                </DialogHeader>\n                <div className=\"space-y-6\">\n                    <div>\n                        <h3 className=\"font-semibold mb-2\">基本信息</h3>\n                        <div className=\"grid grid-cols-2 gap-x-8 gap-y-2 text-sm\">\n                            <div className=\"space-y-2\">\n                                <div className=\"text-muted-foreground\"><span>ID:</span> {subscription.id}</div>\n                                <div className=\"text-muted-foreground\"><span>名称:</span> {subscription.name}</div>\n                                <div className=\"text-muted-foreground\"><span>Cron:</span> {subscription.cron_expr}</div>\n                                <div className=\"text-muted-foreground\">\n                                    <span>状态:</span> <StatusBadge status={subscription.status} />\n                                </div>\n                                {subscription.result?.duration && (\n                                    <div className=\"text-muted-foreground\"><span>运行耗时:</span> {formatDuration(subscription.result.duration)}</div>\n                                )}\n                            </div>\n                            <div className=\"space-y-2\">\n                                <div className=\"text-muted-foreground\"><span>创建时间:</span> {formatTime(subscription.created_at) || '未知'}</div>\n                                <div className=\"text-muted-foreground\"><span>更新时间:</span> {formatTime(subscription.updated_at) || '未知'}</div>\n                                <div className=\"text-muted-foreground\"><span>最后运行:</span> {formatLastRunTime(subscription.result?.last_run)}</div>\n                                <div className=\"text-muted-foreground\"><span>下次运行:</span> {getNextCronRunTime(subscription.cron_expr, subscription.enable) || '未启用或无法计算'}</div>\n                            </div>\n                        </div>\n                    </div>\n\n                    <div>\n                        <h3 className=\"font-semibold mb-2\">配置信息</h3>\n                        <div className=\"space-y-3 text-sm\">\n                            <div className=\"text-muted-foreground\"><span>订阅链接:</span> {subscription.config.url}</div>\n                            <div className=\"grid grid-cols-2 gap-4\">\n                                <div className=\"text-muted-foreground\"><span>使用代理:</span> {subscription.config.proxy ? '是' : '否'}</div>\n                                <div className=\"text-muted-foreground\"><span>超时时间:</span> {subscription.config.timeout || 10}秒</div>\n                            </div>\n                        </div>\n                    </div>\n\n                    <div>\n                        <h3 className=\"font-semibold mb-2\">节点信息</h3>\n                        <div className=\"grid grid-cols-3 gap-4 text-sm\">\n                            <div className=\"space-y-2\">\n                                <div className=\"text-muted-foreground\"><span>原始节点:</span> <span className=\"font-medium\">{subscription.result?.raw_count || 0}</span></div>\n                                <div className=\"text-muted-foreground\"><span>入库节点:</span> <span className=\"font-medium text-green-600\">{subscription.info?.count || 0}</span></div>\n                            </div>\n                            <div className=\"space-y-2\">\n                                <div className=\"text-muted-foreground\"><span>平均上行:</span> {formatSpeed(subscription.info?.speed_up)} MB/s</div>\n                                <div className=\"text-muted-foreground\"><span>平均下行:</span> {formatSpeed(subscription.info?.speed_down)} MB/s</div>\n                            </div>\n                            <div className=\"space-y-2\">\n                                <div className=\"text-muted-foreground\"><span>平均延迟:</span> {subscription.info?.delay || 0} ms</div>\n                                <div className=\"text-muted-foreground\"><span>风险等级:</span>\n                                    <span className={`font-medium ml-1 ${getRiskColor(subscription.info?.risk || 0)}`}>\n                                        {subscription.info?.risk || 0}/10\n                                    </span>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n\n                    <div>\n                        <h3 className=\"font-semibold mb-2\">执行结果</h3>\n                        <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                            <div className=\"space-y-2\">\n                                <div className=\"text-muted-foreground\"><span>成功次数:</span> <span className=\"text-green-600 font-medium\">{subscription.result?.success || 0}</span></div>\n                                <div className=\"text-muted-foreground\"><span>失败次数:</span> <span className=\"text-red-600 font-medium\">{subscription.result?.fail || 0}</span></div>\n                            </div>\n                            <div>\n                                <div className=\"text-muted-foreground\"><span>运行消息:</span></div>\n                                <div className=\"text-muted-foreground mt-1 p-2 bg-gray-50 rounded text-xs max-h-20 overflow-y-auto\">\n                                    {subscription.result?.msg || '无消息'}\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </DialogContent>\n        </Dialog >\n    )\n} "
  },
  {
    "path": "web/src/components/features/sub/components/sub-form.tsx",
    "content": "import { useForm } from 'react-hook-form'\nimport { useEffect, useMemo } from 'react'\nimport { toast } from 'sonner'\nimport { Button } from \"@/src/components/ui/button\"\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/src/components/ui/dialog\"\nimport { BasicInfoSection, ConfigSection, ProtocolFilterSection } from \"./form-sections\"\nimport { useCreateSub, useUpdateSub } from '@/src/lib/queries/sub-queries'\nimport type { SubRequest } from \"@/src/types/sub\"\n\ninterface SubFormProps {\n    initialData?: SubRequest | undefined\n    formTitle: string\n    isOpen: boolean\n    onClose: () => void\n    editingSubId?: number | undefined\n}\n\nexport function SubForm({\n    initialData,\n    formTitle,\n    isOpen,\n    onClose,\n    editingSubId,\n}: SubFormProps) {\n    const createSubMutation = useCreateSub()\n    const updateSubMutation = useUpdateSub()\n    const isEditing = !!editingSubId\n\n    const defaultData = useMemo((): SubRequest => ({\n        name: '',\n        tags: [],\n        enable: true,\n        cron_expr: '0 */6 * * *',\n        config: {\n            url: '',\n            proxy: false,\n            timeout: 10,\n            protocol_filter_enable: false,\n            protocol_filter_mode: false,\n            protocol_filter: [],\n        },\n    }), [])\n\n    const form = useForm<SubRequest>({\n        defaultValues: initialData || defaultData\n    })\n\n    const { control, handleSubmit, reset } = form\n\n    useEffect(() => {\n        if (isOpen) {\n            reset(initialData || defaultData)\n        }\n    }, [initialData, reset, defaultData, isOpen])\n\n    const onSubmit = async (data: SubRequest) => {\n        try {\n            if (editingSubId) {\n                await updateSubMutation.mutateAsync({ id: editingSubId, data })\n                toast.success('订阅更新成功')\n            } else {\n                await createSubMutation.mutateAsync(data)\n                toast.success('订阅创建成功')\n            }\n\n            onClose()\n        } catch (error) {\n            const errorMessage = editingSubId ? '更新订阅失败' : '创建订阅失败'\n            toast.error(errorMessage)\n            console.error('Failed to save subscription:', error)\n        }\n    }\n\n    const isSubmitting = createSubMutation.isPending || updateSubMutation.isPending\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onClose}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto scrollbar-hide\">\n                <DialogHeader>\n                    <DialogTitle>{formTitle}</DialogTitle>\n                </DialogHeader>\n\n                <form onSubmit={handleSubmit(onSubmit)} className=\"space-y-6\">\n                    {/* 基础信息 */}\n                    <BasicInfoSection control={control} />\n\n                    {/* 配置设置 */}\n                    <ConfigSection control={control} />\n\n                    {/* 协议过滤 */}\n                    <ProtocolFilterSection control={control} />\n\n                    {/* 操作按钮 */}\n                    <div className=\"flex gap-2 pt-4\">\n                        <Button\n                            type=\"submit\"\n                            className=\"flex-1\"\n                            disabled={isSubmitting}\n                        >\n                            {isSubmitting ? '提交中...' : (isEditing ? \"更新\" : \"创建\")}\n                        </Button>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={onClose}\n                            disabled={isSubmitting}\n                        >\n                            取消\n                        </Button>\n                    </div>\n                </form>\n            </DialogContent>\n        </Dialog>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/sub/components/sub-list.tsx",
    "content": "import { useEffect, useCallback } from \"react\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Card, CardContent } from \"@/src/components/ui/card\"\nimport { Table, TableBody, TableCell, TableRow } from \"@/src/components/ui/table\"\nimport { InlineLoading } from \"@/src/components/ui/loading\"\nimport { RefreshCw, Edit, Trash2 } from \"lucide-react\"\nimport { toast } from \"sonner\"\nimport { formatLastRunTime } from \"@/src/utils\"\nimport { StatusBadge } from \"@/src/components/shared/status-badge\"\nimport { formatSpeed } from \"../utils\"\nimport { useSubs, useDeleteSub, useRefreshSub } from \"@/src/lib/queries/sub-queries\"\nimport { useOverflowDetection } from \"@/src/lib/hooks/useOverflowDetection\"\nimport { useAlert } from \"@/src/components/providers\"\nimport type { SubResponse } from \"@/src/types/sub\"\n\ninterface SubscriptionListProps {\n    onEdit: (subscription: SubResponse) => void\n    onShowDetail: (subscription: SubResponse) => void\n}\n\nexport function SubList({\n    onEdit,\n    onShowDetail,\n}: SubscriptionListProps) {\n    const { data: subs = [], isLoading, error } = useSubs()\n    const deleteSubMutation = useDeleteSub()\n    const refreshSubMutation = useRefreshSub()\n    const { confirm } = useAlert()\n    const { containerRef, contentRef, isOverflowing, checkOverflow } = useOverflowDetection<HTMLTableElement>()\n\n    useEffect(() => {\n        if (!isLoading) {\n            checkOverflow()\n        }\n    }, [isLoading, checkOverflow])\n\n    const handleDelete = useCallback(async (id: number, name: string) => {\n        const confirmed = await confirm({\n            title: '删除订阅',\n            description: `确定要删除订阅 \"${name}\" 吗？`,\n            confirmText: '删除',\n            cancelText: '取消',\n            variant: 'destructive'\n        })\n\n        if (confirmed) {\n            try {\n                await deleteSubMutation.mutateAsync(id)\n                toast.success('删除成功')\n            } catch (error) {\n                console.error('Failed to delete subscription:', error)\n                toast.error('删除失败')\n            }\n        }\n    }, [confirm, deleteSubMutation])\n\n    const handleRefresh = useCallback(async (id: number) => {\n        try {\n            await refreshSubMutation.mutateAsync(id)\n            toast.success('刷新成功')\n        } catch (error) {\n            console.error('Failed to refresh subscription:', error)\n            toast.error('刷新失败')\n        }\n    }, [refreshSubMutation])\n\n    if (isLoading) {\n        return (\n            <Card>\n                <CardContent>\n                    <InlineLoading message=\"加载订阅列表...\" />\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (error) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-destructive\">\n                        加载失败: {error.message}\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    if (subs.length === 0) {\n        return (\n            <Card>\n                <CardContent>\n                    <div className=\"text-center py-8 text-muted-foreground\">\n                        暂无订阅数据，点击上方按钮创建第一个订阅\n                    </div>\n                </CardContent>\n            </Card>\n        )\n    }\n\n    return (\n        <Card>\n            <CardContent>\n                <div className=\"overflow-x-auto\" ref={containerRef}>\n                    <Table ref={contentRef}>\n                        <TableBody>\n                            {subs.sort((a, b) => a.id - b.id).map((sub) => (\n                                <TableRow key={sub.id}>\n                                    <TableCell>\n                                        <div className=\"font-medium cursor-pointer hover:text-blue-600\"\n                                            onClick={() => onShowDetail(sub)}>\n                                            {sub.name}\n                                        </div>\n                                        <div className=\"text-sm text-muted-foreground\">{sub?.cron_expr || 'N/A'}</div>\n                                    </TableCell>\n                                    <TableCell>\n                                        <StatusBadge status={sub.status} />\n                                    </TableCell>\n                                    <TableCell className=\"text-xs space-y-1\">\n                                        <div>平均延迟: <span className=\"text-muted-foreground\">{sub.info?.delay || 0}ms</span></div>\n                                        <div className=\"text-muted-foreground\">↑{formatSpeed(sub.info?.speed_up || 0)} ↓{formatSpeed(sub.info?.speed_down || 0)}</div>\n                                    </TableCell>\n                                    <TableCell className=\"text-xs space-y-1\">\n                                        <div>最后运行: <span className=\"text-muted-foreground\">{formatLastRunTime(sub.result?.last_run)}</span></div>\n                                        <div>执行时长: <span className=\"text-muted-foreground\">{sub.result?.duration || 0}ms</span></div>\n                                    </TableCell>\n                                    <TableCell className={`text-right sticky right-0 bg-background ${isOverflowing ? 'shadow-[-4px_0_8px_-2px_rgba(0,0,0,0.1)]' : ''}`}>\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => handleRefresh(sub.id)}\n                                            disabled={refreshSubMutation.isPending && refreshSubMutation.variables === sub.id}\n                                            className={refreshSubMutation.isPending && refreshSubMutation.variables === sub.id ? 'opacity-50' : ''}\n                                        >\n                                            <RefreshCw className={`h-4 w-4 ${refreshSubMutation.isPending && refreshSubMutation.variables === sub.id ? 'animate-spin' : ''}`} />\n                                        </Button>\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => onEdit(sub)}\n                                        >\n                                            <Edit className=\"h-4 w-4\" />\n                                        </Button>\n                                        <Button\n                                            size=\"sm\"\n                                            variant=\"outline\"\n                                            onClick={() => handleDelete(sub.id, sub.name)}\n                                            disabled={deleteSubMutation.isPending && deleteSubMutation.variables === sub.id}\n                                            className={deleteSubMutation.isPending && deleteSubMutation.variables === sub.id ? 'opacity-50' : ''}\n                                        >\n                                            {deleteSubMutation.isPending && deleteSubMutation.variables === sub.id ? (\n                                                <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n                                            ) : (\n                                                <Trash2 className=\"h-4 w-4\" />\n                                            )}\n                                        </Button>\n                                    </TableCell>\n                                </TableRow>\n                            ))}\n                        </TableBody>\n                    </Table>\n                </div>\n            </CardContent>\n        </Card>\n    )\n} "
  },
  {
    "path": "web/src/components/features/sub/components/sub-page.tsx",
    "content": "import { useState } from \"react\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Plus, Upload } from \"lucide-react\"\nimport { SubForm } from \"./sub-form\"\nimport { SubDetail } from \"./sub-detail\"\nimport { SubList } from \"./sub-list\"\nimport { BatchSubForm } from \"./batch-sub-form\"\nimport type { SubResponse } from \"@/src/types/sub\"\n\nexport function SubPage() {\n    const [detailSubscription, setDetailSubscription] = useState<SubResponse | null>(null)\n    const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false)\n    const [isFormDialogOpen, setIsFormDialogOpen] = useState(false)\n    const [isBatchFormDialogOpen, setIsBatchFormDialogOpen] = useState(false)\n    const [editingSubscription, setEditingSubscription] = useState<SubResponse | null>(null)\n\n\n    const handleEdit = (subscription: SubResponse) => {\n        setEditingSubscription(subscription)\n        setIsFormDialogOpen(true)\n    }\n\n    const handleCreate = () => {\n        setEditingSubscription(null)\n        setIsFormDialogOpen(true)\n    }\n\n    const handleBatchCreate = () => {\n        setIsBatchFormDialogOpen(true)\n    }\n\n    const handleFormSuccess = () => {\n        setIsFormDialogOpen(false)\n        setEditingSubscription(null)\n    }\n\n    const handleBatchFormSuccess = () => {\n        setIsBatchFormDialogOpen(false)\n    }\n\n\n    const showDetail = (subscription: SubResponse) => {\n        setDetailSubscription(subscription)\n        setIsDetailDialogOpen(true)\n    }\n\n    return (\n        <div className=\"flex flex-col gap-4 py-4 md:gap-6 md:py-6\">\n            <div className=\"flex items-center justify-between px-4 lg:px-6\">\n                <div>\n                    <h1 className=\"text-2xl font-bold\">订阅管理</h1>\n                </div>\n\n                <div className=\"flex gap-2\">\n                    <Button variant=\"outline\" onClick={handleBatchCreate}>\n                        <Upload className=\"h-4 w-4 mr-2\" />\n                        批量添加\n                    </Button>\n                    <Button onClick={handleCreate}>\n                        <Plus className=\"h-4 w-4 mr-2\" />\n                        添加订阅\n                    </Button>\n                </div>\n            </div>\n\n            <SubForm\n                initialData={editingSubscription ? {\n                    name: editingSubscription.name,\n                    tags: editingSubscription.tags || [],\n                    enable: editingSubscription.enable,\n                    cron_expr: editingSubscription.cron_expr,\n                    config: {\n                        url: editingSubscription.config.url || '',\n                        proxy: editingSubscription.config.proxy || false,\n                        timeout: editingSubscription.config.timeout || 10,\n                    }\n                } : undefined}\n                formTitle={editingSubscription ? \"编辑订阅\" : \"添加订阅\"}\n                isOpen={isFormDialogOpen}\n                onClose={handleFormSuccess}\n                editingSubId={editingSubscription?.id}\n            />\n\n            <BatchSubForm\n                isOpen={isBatchFormDialogOpen}\n                onClose={handleBatchFormSuccess}\n            />\n\n            <div className=\"px-4 lg:px-6\">\n                <SubList\n                    onEdit={(sub) => handleEdit(sub)}\n                    onShowDetail={showDetail}\n                />\n            </div>\n\n            <SubDetail\n                subscription={detailSubscription}\n                isOpen={isDetailDialogOpen}\n                onOpenChange={setIsDetailDialogOpen}\n            />\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/features/sub/index.ts",
    "content": "export { SubPage } from './components/sub-page'\n\nexport { SubForm } from './components/sub-form'\nexport { SubDetail } from './components/sub-detail'\nexport { SubList } from './components/sub-list'\n"
  },
  {
    "path": "web/src/components/features/sub/utils/index.ts",
    "content": "/**\n * 格式化速度（自动转换单位）\n * 输入单位：KB/s\n */\nexport function formatSpeed(kbPerSec: number): string {\n  if (!kbPerSec || kbPerSec === 0) return '0 KB/s'\n\n  const units = ['KB/s', 'MB/s', 'GB/s', 'TB/s']\n  let size = kbPerSec\n  let unitIndex = 0\n\n  while (size >= 1024 && unitIndex < units.length - 1) {\n    size /= 1024\n    unitIndex++\n  }\n\n  return `${size.toFixed(1)} ${units[unitIndex]}`\n}\n\n/**\n * 从URL自动生成订阅名称\n */\nexport function generateNameFromUrl(url: string): string {\n  try {\n    const urlObj = new URL(url)\n    const hostname = urlObj.hostname\n\n    if (hostname === 'raw.githubusercontent.com') {\n      const parts = urlObj.pathname.split('/').filter(Boolean)\n      if (parts.length >= 2) {\n        return `${parts[0]}/${parts[1]}`\n      }\n    }\n\n    if (hostname === 'gist.githubusercontent.com') {\n      const parts = urlObj.pathname.split('/').filter(Boolean)\n      if (parts.length >= 1) {\n        return parts[0] || ''\n      }\n    }\n\n    const domainParts = hostname.split('.')\n\n    if (domainParts.length >= 3) {\n      return domainParts.slice(0, 2).join('.')\n    }\n\n    return hostname\n  } catch {\n    return ''\n  }\n}"
  },
  {
    "path": "web/src/components/features/system-update/index.tsx",
    "content": "\"use client\"\n\nimport { useState, useEffect } from \"react\"\nimport { toast } from \"sonner\"\nimport { marked } from 'marked'\n\nimport {\n  IconDownload,\n  IconLoader2,\n  IconRefresh,\n  IconAlertCircle,\n  IconCheck,\n} from \"@tabler/icons-react\"\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/src/components/ui/dialog\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/src/components/ui/card\"\nimport { Badge } from \"@/src/components/ui/badge\"\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/src/components/ui/accordion\"\nimport { api } from \"@/src/lib/api/client\"\nimport type { UpdateResponse, UpdateComponent, SystemVersion } from \"@/src/types\"\n\ninterface SystemUpdateDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\ninterface ComponentStatus {\n  name: UpdateComponent\n  displayName: string\n  currentVersion: string\n  latestVersion: string\n  publishedAt: string\n  updateBody: string\n  isUpdating: boolean\n  updateSuccess: boolean | null\n  updateError: string | null\n}\n\nexport function SystemUpdateDialog({ open, onOpenChange }: SystemUpdateDialogProps) {\n  const [isLoading, setIsLoading] = useState(false)\n  const [components, setComponents] = useState<ComponentStatus[]>([\n    {\n      name: 'bestsub',\n      displayName: 'BestSub 后端',\n      currentVersion: '加载中...',\n      latestVersion: '加载中...',\n      publishedAt: '',\n      updateBody: '',\n      isUpdating: false,\n      updateSuccess: null,\n      updateError: null,\n    }\n  ])\n\n  const fetchUpdateInfo = async () => {\n    setIsLoading(true)\n    try {\n      const [info, systemVersion] = await Promise.all([\n        api.getLatestUpdates(),\n        api.getSystemVersion()\n      ]) as [UpdateResponse, SystemVersion]\n\n      setComponents(prev => prev.map(comp => {\n        let currentVersion = comp.currentVersion\n        let latestVersion = comp.latestVersion\n        const latest = info[comp.name]\n\n        if (comp.name === 'bestsub') {\n          currentVersion = systemVersion.version\n          latestVersion = info.bestsub.tag_name\n        }\n\n        return {\n          ...comp,\n          currentVersion,\n          latestVersion,\n          publishedAt: latest.published_at,\n          updateBody: latest.body\n        }\n      }))\n    } catch (error) {\n      toast.error(\"获取更新信息失败\" + (error as Error).message)\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  const handleUpdate = async (componentName: UpdateComponent) => {\n    setComponents(prev =>\n      prev.map(comp =>\n        comp.name === componentName\n          ? { ...comp, isUpdating: true, updateSuccess: null as boolean | null, updateError: null as string | null }\n          : comp\n      )\n    )\n\n    try {\n      await api.updateComponent(componentName)\n      setComponents(prev =>\n        prev.map(comp =>\n          comp.name === componentName\n            ? { ...comp, isUpdating: false, updateSuccess: true, updateError: null }\n            : comp\n        )\n      )\n      toast.success(`${components.find(c => c.name === componentName)?.displayName} 更新成功`)\n    } catch (error) {\n      const errorMessage = error instanceof Error ? error.message : '更新失败'\n      setComponents(prev =>\n        prev.map(comp =>\n          comp.name === componentName\n            ? { ...comp, isUpdating: false, updateSuccess: false, updateError: errorMessage }\n            : comp\n        )\n      )\n      toast.error(`${components.find(c => c.name === componentName)?.displayName} 更新失败`)\n    }\n  }\n\n  useEffect(() => {\n    if (open) {\n      fetchUpdateInfo()\n    }\n  }, [open])\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <div>\n            <DialogTitle className=\"flex items-center gap-2\">\n              <IconRefresh className=\"h-5 w-5\" />\n              系统更新\n            </DialogTitle>\n            <DialogDescription className=\"mt-1\">\n              查看最新版本和更新系统组件\n            </DialogDescription>\n          </div>\n        </DialogHeader>\n\n        <div className=\"space-y-4\">\n          <div className=\"space-y-4 max-h-[60vh] overflow-y-auto scrollbar-hide\">\n            {components.map((component) => (\n              <Card key={component.name}>\n                <CardHeader>\n                  <div className=\"flex items-center justify-between\">\n                    <CardTitle className=\"text-base\">{component.displayName}</CardTitle>\n                    <div className=\"flex items-center gap-2\">\n                      {component.updateSuccess === true && (\n                        <Badge variant=\"default\" className=\"bg-green-600\">\n                          更新成功\n                        </Badge>\n                      )}\n                      {component.updateSuccess === false && (\n                        <Badge variant=\"destructive\">\n                          更新失败\n                        </Badge>\n                      )}\n                    </div>\n                  </div>\n                </CardHeader>\n                <CardContent className=\"text-sm\">\n                  <div>\n                    <span>当前版本: </span>\n                    <span>{component.currentVersion}</span>\n                  </div>\n                  <div>\n                    <span>最新版本: </span>\n                    <span className={`${component.latestVersion !== '加载中...' && component.currentVersion !== component.latestVersion ? 'text-orange-600' : ''}`}>\n                      {component.latestVersion}\n                    </span>\n\n                    {component.updateBody && (\n                      <Accordion type=\"single\" collapsible>\n                        <AccordionItem value=\"update-content\" >\n                          <AccordionTrigger className=\"hover:no-underline\">\n                            <span>查看更新内容</span>\n                          </AccordionTrigger>\n                          <AccordionContent>\n                            <div\n                              className=\" [&_a]:text-blue-600 leading-relaxed [&_ul]:list-inside [&_li]:list-disc [&_li]:ml-4\"\n                              dangerouslySetInnerHTML={{\n                                __html: marked.parse(component.updateBody)\n                              }}\n                            />\n                          </AccordionContent>\n                        </AccordionItem>\n                      </Accordion>\n                    )}\n\n                    {component.updateError && (\n                      <div className=\"flex items-center gap-2 text-destructive bg-destructive/10 p-2 rounded\">\n                        <IconAlertCircle className=\"h-4 w-4\" />\n                        {component.updateError}\n                      </div>\n                    )}\n\n                    <Button\n                      onClick={() => handleUpdate(component.name)}\n                      disabled={component.isUpdating || component.latestVersion === component.currentVersion || component.latestVersion === '加载中...'}\n                      className=\"w-full\"\n                    >\n                      {component.isUpdating ? (\n                        <>\n                          <IconLoader2 className=\"h-4 w-4 animate-spin mr-2\" />\n                          更新中...\n                        </>\n                      ) : component.latestVersion === component.currentVersion ? (\n                        <>\n                          <IconCheck className=\"h-4 w-4 mr-2\" />\n                          已是最新版本\n                        </>\n                      ) : (\n                        <>\n                          <IconDownload className=\"h-4 w-4 mr-2\" />\n                          立即更新\n                        </>\n                      )}\n                    </Button>\n                  </div>\n                </CardContent>\n              </Card>\n            ))}\n          </div>\n\n          <div className=\"flex justify-center pt-2\">\n            <Button\n              variant=\"outline\"\n              onClick={fetchUpdateInfo}\n              disabled={isLoading}\n              className=\"w-full\"\n            >\n              {isLoading ? (\n                <IconLoader2 className=\"h-4 w-4 animate-spin mr-2\" />\n              ) : (\n                <IconRefresh className=\"h-4 w-4 mr-2\" />\n              )}\n              刷新版本\n            </Button>\n          </div>\n        </div>\n      </DialogContent>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "web/src/components/layout/app-sidebar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  IconDashboard,\n  IconLink,\n  IconSearch,\n  IconShare,\n  IconDatabase,\n  IconBell,\n\n  IconHelp,\n  IconInnerShadowTop,\n\n  IconFileText,\n  IconBrandGithub,\n} from \"@tabler/icons-react\"\n\n\nimport { NavMain } from \"./nav-main\"\nimport { NavSecondary } from \"./nav-secondary\"\nimport { NavUser } from \"./nav-user\"\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n} from \"@/src/components/ui/sidebar\"\nimport { APP_ROUTES } from \"@/src/lib/config/config\"\nexport function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {\n\n  const navMain = [\n    {\n      title: APP_ROUTES.DASHBOARD.title,\n      url: APP_ROUTES.DASHBOARD.path,\n      icon: IconDashboard,\n    },\n    {\n      title: APP_ROUTES.SUB.title,\n      url: APP_ROUTES.SUB.path,\n      icon: IconLink,\n    },\n    {\n      title: APP_ROUTES.CHECK.title,\n      url: APP_ROUTES.CHECK.path,\n      icon: IconSearch,\n    },\n    {\n      title: APP_ROUTES.SHARE.title,\n      url: APP_ROUTES.SHARE.path,\n      icon: IconShare,\n    },\n    {\n      title: APP_ROUTES.STORAGE.title,\n      url: APP_ROUTES.STORAGE.path,\n      icon: IconDatabase,\n    },\n    {\n      title: APP_ROUTES.NOTIFY.title,\n      url: APP_ROUTES.NOTIFY.path,\n      icon: IconBell,\n    },\n  ]\n\n  const navSecondary = [\n    {\n      title: APP_ROUTES.LOG.title,\n      url: APP_ROUTES.LOG.path,\n      icon: IconFileText,\n    },\n    {\n      title: APP_ROUTES.HELP.title,\n      url: APP_ROUTES.HELP.path,\n      icon: IconHelp,\n    },\n    {\n      title: APP_ROUTES.GITHUB.title,\n      url: APP_ROUTES.GITHUB.path,\n      icon: IconBrandGithub,\n    },\n  ]\n\n  return (\n    <>\n      <Sidebar collapsible=\"offcanvas\" {...props}>\n        <SidebarHeader>\n          <SidebarMenu>\n            <SidebarMenuItem>\n              <SidebarMenuButton\n                className=\"data-[slot=sidebar-menu-button]:!p-1.5\"\n                onClick={() => window.location.hash = '/dashboard'}\n              >\n                <IconInnerShadowTop className=\"!size-5\" />\n                <span className=\"text-base font-semibold\">BestSub</span>\n              </SidebarMenuButton>\n            </SidebarMenuItem>\n          </SidebarMenu>\n        </SidebarHeader>\n        <SidebarContent>\n          <NavMain items={navMain} />\n          <NavSecondary items={navSecondary} className=\"mt-auto\" />\n        </SidebarContent>\n        <SidebarFooter>\n          <NavUser />\n        </SidebarFooter>\n      </Sidebar>\n    </>\n  )\n}\n"
  },
  {
    "path": "web/src/components/layout/index.ts",
    "content": "// Layout components\nexport { AppSidebar } from './app-sidebar'\nexport { SiteHeader } from './site-header'\nexport { NavMain } from './nav-main'\nexport { NavSecondary } from './nav-secondary'\nexport { NavUser } from './nav-user'\nexport { NavDocuments } from './nav-documents'\n"
  },
  {
    "path": "web/src/components/layout/nav-documents.tsx",
    "content": "\"use client\"\n\nimport {\n  IconDots,\n  IconFolder,\n  IconShare3,\n  IconTrash,\n  type Icon,\n} from \"@tabler/icons-react\"\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/src/components/ui/dropdown-menu\"\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@/src/components/ui/sidebar\"\n\nexport function NavDocuments({\n  items,\n}: {\n  items: {\n    name: string\n    url: string\n    icon: Icon\n  }[]\n}) {\n  const { isMobile } = useSidebar()\n\n  return (\n    <SidebarGroup className=\"group-data-[collapsible=icon]:hidden\">\n      <SidebarGroupLabel>Documents</SidebarGroupLabel>\n      <SidebarMenu>\n        {items.map((item) => (\n          <SidebarMenuItem key={item.name}>\n            <SidebarMenuButton asChild>\n              <a href={item.url}>\n                <item.icon />\n                <span>{item.name}</span>\n              </a>\n            </SidebarMenuButton>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <SidebarMenuAction\n                  showOnHover\n                  className=\"data-[state=open]:bg-accent rounded-sm\"\n                >\n                  <IconDots />\n                  <span className=\"sr-only\">More</span>\n                </SidebarMenuAction>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent\n                className=\"w-24 rounded-lg\"\n                side={isMobile ? \"bottom\" : \"right\"}\n                align={isMobile ? \"end\" : \"start\"}\n              >\n                <DropdownMenuItem>\n                  <IconFolder />\n                  <span>Open</span>\n                </DropdownMenuItem>\n                <DropdownMenuItem>\n                  <IconShare3 />\n                  <span>Share</span>\n                </DropdownMenuItem>\n                <DropdownMenuSeparator />\n                <DropdownMenuItem variant=\"destructive\">\n                  <IconTrash />\n                  <span>Delete</span>\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </SidebarMenuItem>\n        ))}\n        <SidebarMenuItem>\n          <SidebarMenuButton className=\"text-sidebar-foreground/70\">\n            <IconDots className=\"text-sidebar-foreground/70\" />\n            <span>More</span>\n          </SidebarMenuButton>\n        </SidebarMenuItem>\n      </SidebarMenu>\n    </SidebarGroup>\n  )\n}\n"
  },
  {
    "path": "web/src/components/layout/nav-main.tsx",
    "content": "\"use client\"\n\nimport { type Icon } from \"@tabler/icons-react\"\nimport { useRouter, useLinkPreloader } from \"@/src/router\"\n\nimport {\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n} from \"@/src/components/ui/sidebar\"\n\nexport function NavMain({\n  items,\n}: {\n  items: {\n    title: string\n    url: string\n    icon?: Icon\n  }[]\n}) {\n  const { currentPath, navigate } = useRouter()\n  const { handleMouseEnter, handleMouseLeave } = useLinkPreloader()\n\n  return (\n    <SidebarGroup>\n      <SidebarGroupContent className=\"flex flex-col gap-2\">\n        <SidebarMenu>\n          {items.map((item) => {\n            const isActive = currentPath === item.url\n            return (\n              <SidebarMenuItem key={item.title}>\n                <SidebarMenuButton\n                  tooltip={item.title}\n                  isActive={isActive}\n                  onClick={() => navigate(item.url)}\n                  onMouseEnter={() => handleMouseEnter(item.url)}\n                  onMouseLeave={handleMouseLeave}\n                  className=\"transition-all duration-200 hover:bg-sidebar-accent\"\n                >\n                  {item.icon && <item.icon />}\n                  <span>{item.title}</span>\n                </SidebarMenuButton>\n              </SidebarMenuItem>\n            )\n          })}\n        </SidebarMenu>\n      </SidebarGroupContent>\n    </SidebarGroup>\n  )\n}\n"
  },
  {
    "path": "web/src/components/layout/nav-secondary.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport Link from \"next/link\"\nimport { usePathname } from \"next/navigation\"\nimport { type Icon } from \"@tabler/icons-react\"\n\nimport {\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n} from \"@/src/components/ui/sidebar\"\n\nexport function NavSecondary({\n  items,\n  onItemClick: _onItemClick,\n  ...props\n}: {\n  items: {\n    title: string\n    url: string\n    icon: Icon\n    onClick?: () => void\n  }[]\n  onItemClick?: (item: { title: string; url: string; icon: Icon; onClick?: () => void }) => void\n} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {\n  const pathname = usePathname()\n\n  return (\n    <SidebarGroup {...props}>\n      <SidebarGroupContent>\n        <SidebarMenu>\n          {items.map((item) => {\n            const isActive = pathname === item.url\n\n            if (item.onClick) {\n              return (\n                <SidebarMenuItem key={item.title}>\n                  <SidebarMenuButton onClick={item.onClick} isActive={isActive}>\n                    <item.icon />\n                    <span>{item.title}</span>\n                  </SidebarMenuButton>\n                </SidebarMenuItem>\n              )\n            }\n\n            if (item.url.startsWith('http')) {\n              return (\n                <SidebarMenuItem key={item.title}>\n                  <SidebarMenuButton asChild isActive={isActive}>\n                    <a href={item.url} target=\"_blank\" rel=\"noopener noreferrer\">\n                      <item.icon />\n                      <span>{item.title}</span>\n                    </a>\n                  </SidebarMenuButton>\n                </SidebarMenuItem>\n              )\n            }\n\n            return (\n              <SidebarMenuItem key={item.title}>\n                <SidebarMenuButton asChild isActive={isActive}>\n                  <Link href={item.url}>\n                    <item.icon />\n                    <span>{item.title}</span>\n                  </Link>\n                </SidebarMenuButton>\n              </SidebarMenuItem>\n            )\n          })}\n        </SidebarMenu>\n      </SidebarGroupContent>\n    </SidebarGroup>\n  )\n}\n"
  },
  {
    "path": "web/src/components/layout/nav-user.tsx",
    "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport {\n  IconDotsVertical,\n  IconLogout,\n  IconSettings,\n  IconRefresh,\n  IconUserCircle,\n} from \"@tabler/icons-react\"\n\nimport {\n  Avatar,\n  AvatarFallback,\n} from \"@/src/components/ui/avatar\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/src/components/ui/dropdown-menu\"\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@/src/components/ui/sidebar\"\nimport { useAuth } from \"@/src/components/providers\"\nimport { SettingsDialog } from \"@/src/components/features\"\nimport { ProfileDialog } from \"@/src/components/features/profile\"\nimport { SystemUpdateDialog } from \"@/src/components/features/system-update\"\n\nexport function NavUser() {\n  const { isMobile } = useSidebar()\n  const { user, logout } = useAuth()\n  const [isSettingsOpen, setIsSettingsOpen] = useState(false)\n  const [isProfileOpen, setIsProfileOpen] = useState(false)\n  const [isUpdateOpen, setIsUpdateOpen] = useState(false)\n  const [isDropdownOpen, setIsDropdownOpen] = useState(false)\n\n  if (!user) return null\n\n  const handleOpenSettings = () => {\n    setIsDropdownOpen(false)\n    setTimeout(() => {\n      setIsSettingsOpen(true)\n    }, 100)\n  }\n\n  const handleOpenProfile = () => {\n    setIsDropdownOpen(false)\n    setTimeout(() => {\n      setIsProfileOpen(true)\n    }, 100)\n  }\n\n  const handleOpenUpdate = () => {\n    setIsDropdownOpen(false)\n    setTimeout(() => {\n      setIsUpdateOpen(true)\n    }, 100)\n  }\n\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>\n          <DropdownMenuTrigger asChild>\n            <SidebarMenuButton\n              size=\"lg\"\n              className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n            >\n              <Avatar className=\"h-8 w-8 rounded-lg grayscale\">\n                <AvatarFallback className=\"rounded-lg\">\n                  {user.username.charAt(0).toUpperCase()}\n                </AvatarFallback>\n              </Avatar>\n              <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                <span className=\"truncate font-medium\">{user.username}</span>\n                <span className=\"text-muted-foreground truncate text-xs\">\n                  管理员\n                </span>\n              </div>\n              <IconDotsVertical className=\"ml-auto size-4\" />\n            </SidebarMenuButton>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n            side={isMobile ? \"bottom\" : \"right\"}\n            align=\"end\"\n            sideOffset={4}\n          >\n            <DropdownMenuLabel className=\"p-0 font-normal\">\n              <div className=\"flex items-center gap-2 px-1 py-1.5 text-left text-sm\">\n                <Avatar className=\"h-8 w-8 rounded-lg\">\n                  <AvatarFallback className=\"rounded-lg\">\n                    {user.username.charAt(0).toUpperCase()}\n                  </AvatarFallback>\n                </Avatar>\n                <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                  <span className=\"truncate font-medium\">{user.username}</span>\n                  <span className=\"text-muted-foreground truncate text-xs\">\n                    管理员\n                  </span>\n                </div>\n              </div>\n            </DropdownMenuLabel>\n            <DropdownMenuSeparator />\n            <DropdownMenuGroup>\n              <DropdownMenuItem onClick={handleOpenProfile}>\n                <IconUserCircle />\n                个人资料\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={handleOpenSettings}>\n                <IconSettings />\n                系统设置\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={handleOpenUpdate}>\n                <IconRefresh />\n                系统更新\n              </DropdownMenuItem>\n            </DropdownMenuGroup>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem onClick={logout}>\n              <IconLogout />\n              退出登录\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </SidebarMenuItem>\n      <SettingsDialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen} />\n      <ProfileDialog open={isProfileOpen} onOpenChange={setIsProfileOpen} />\n      <SystemUpdateDialog open={isUpdateOpen} onOpenChange={setIsUpdateOpen} />\n    </SidebarMenu>\n  )\n}\n"
  },
  {
    "path": "web/src/components/layout/site-header.tsx",
    "content": "import { Separator } from \"@/src/components/ui/separator\"\nimport { SidebarTrigger } from \"@/src/components/ui/sidebar\"\nimport { ModeToggle } from \"@/src/components/ui/mode-toggle\"\n\nexport function SiteHeader() {\n  return (\n    <header className=\"flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)\">\n      <div className=\"flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6\">\n        <SidebarTrigger className=\"-ml-1\" />\n        <Separator\n          orientation=\"vertical\"\n          className=\"mx-2 data-[orientation=vertical]:h-4\"\n        />\n        <div className=\"ml-auto\">\n          <ModeToggle />\n        </div>\n      </div>\n    </header>\n  )\n}\n"
  },
  {
    "path": "web/src/components/pages/index.ts",
    "content": "export { NotFound } from './not-found'\n"
  },
  {
    "path": "web/src/components/pages/not-found.tsx",
    "content": "\"use client\"\n\nimport { Button } from \"@/src/components/ui/button\"\nimport { Card, CardContent } from \"@/src/components/ui/card\"\nimport { Home, ArrowLeft } from \"lucide-react\"\nimport { useRouter } from \"@/src/router\"\n\ninterface NotFoundProps {\n    path?: string\n}\n\nexport function NotFound({ path }: NotFoundProps) {\n    const { navigate } = useRouter()\n\n    const handleGoHome = () => {\n        navigate('/dashboard')\n    }\n\n    const handleGoBack = () => {\n        if (typeof window !== 'undefined' && window.history.length > 1) {\n            window.history.back()\n        } else {\n            navigate('/dashboard')\n        }\n    }\n\n    return (\n        <div className=\"min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-background to-muted/20\">\n            <Card className=\"w-full max-w-md mx-auto shadow-lg\">\n                <CardContent className=\"p-8 text-center\">\n                    <div className=\"mb-6\">\n                        <div className=\"text-6xl font-bold text-primary/80 mb-2\">404</div>\n                    </div>\n\n                    <div className=\"mb-8\">\n                        <h1 className=\"text-2xl font-semibold text-foreground mb-3\">\n                            页面未找到\n                        </h1>\n                        <p className=\"text-muted-foreground leading-relaxed mb-4\">\n                            抱歉，您访问的页面不存在或已被移动。\n                        </p>\n                        {path && (\n                            <p className=\"text-sm text-muted-foreground\">\n                                路径: <code className=\"bg-muted px-2 py-1 rounded text-xs\">{path}</code>\n                            </p>\n                        )}\n                    </div>\n\n                    <div className=\"flex flex-col sm:flex-row gap-3 justify-center\">\n                        <Button\n                            onClick={handleGoHome}\n                            className=\"flex items-center gap-2\"\n                        >\n                            <Home className=\"w-4 h-4\" />\n                            返回首页\n                        </Button>\n                        <Button\n                            variant=\"outline\"\n                            onClick={handleGoBack}\n                            className=\"flex items-center gap-2\"\n                        >\n                            <ArrowLeft className=\"w-4 h-4\" />\n                            返回上页\n                        </Button>\n                    </div>\n\n                </CardContent>\n            </Card>\n        </div>\n    )\n}\n"
  },
  {
    "path": "web/src/components/providers/alert-provider.tsx",
    "content": "'use client'\n\nimport React, { createContext, useContext, useState, useCallback } from 'react'\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from '@/src/components/ui/alert-dialog'\n\ninterface AlertOptions {\n  title?: string\n  description?: string\n  confirmText?: string\n  cancelText?: string\n  variant?: 'default' | 'destructive'\n}\n\ninterface AlertContextType {\n  confirm: (options?: AlertOptions) => Promise<boolean>\n}\n\nconst AlertContext = createContext<AlertContextType | undefined>(undefined)\n\nexport function useAlert() {\n  const context = useContext(AlertContext)\n  if (!context) {\n    throw new Error('useAlert must be used within an AlertProvider')\n  }\n  return context\n}\n\ninterface AlertState {\n  isOpen: boolean\n  title: string\n  description: string\n  confirmText: string\n  cancelText: string\n  variant: 'default' | 'destructive'\n  resolve: ((value: boolean) => void) | null\n}\n\nexport function AlertProvider({ children }: { children: React.ReactNode }) {\n  const [alertState, setAlertState] = useState<AlertState>({\n    isOpen: false,\n    title: '确认操作',\n    description: '您确定要执行此操作吗？',\n    confirmText: '确认',\n    cancelText: '取消',\n    variant: 'default',\n    resolve: null,\n  })\n\n  const confirm = useCallback((options: AlertOptions = {}): Promise<boolean> => {\n    return new Promise((resolve) => {\n      setAlertState({\n        isOpen: true,\n        title: options.title || '确认操作',\n        description: options.description || '您确定要执行此操作吗？',\n        confirmText: options.confirmText || '确认',\n        cancelText: options.cancelText || '取消',\n        variant: options.variant || 'default',\n        resolve,\n      })\n    })\n  }, [])\n\n  const handleConfirm = useCallback(() => {\n    alertState.resolve?.(true)\n    setAlertState(prev => ({ ...prev, isOpen: false, resolve: null }))\n  }, [alertState])\n\n  const handleCancel = useCallback(() => {\n    alertState.resolve?.(false)\n    setAlertState(prev => ({ ...prev, isOpen: false, resolve: null }))\n  }, [alertState])\n\n  const handleOpenChange = useCallback((open: boolean) => {\n    if (!open) {\n      handleCancel()\n    }\n  }, [handleCancel])\n\n  return (\n    <AlertContext.Provider value={{ confirm }}>\n      {children}\n      <AlertDialog open={alertState.isOpen} onOpenChange={handleOpenChange}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>{alertState.title}</AlertDialogTitle>\n            <AlertDialogDescription>{alertState.description}</AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel onClick={handleCancel}>\n              {alertState.cancelText}\n            </AlertDialogCancel>\n            <AlertDialogAction onClick={handleConfirm}>\n              {alertState.confirmText}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </AlertContext.Provider>\n  )\n}\n"
  },
  {
    "path": "web/src/components/providers/auth-provider.tsx",
    "content": "\"use client\"\n\nimport React, { createContext, useContext, useEffect, useState } from 'react'\nimport { api } from '@/src/lib/api/client'\nimport { tokenManager } from '@/src/lib/api/token-manager'\nimport type { UserInfo } from '@/src/types'\n\n// Auth相关类型定义\ninterface AuthState {\n    user: UserInfo | null\n    isLoading: boolean\n    isAuthenticated: boolean\n}\n\ninterface AuthContextType extends AuthState {\n    login: (username: string, password: string) => Promise<void>\n    logout: () => Promise<void>\n    updateUser: (user: UserInfo) => void\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nexport function useAuth() {\n    const context = useContext(AuthContext)\n    if (context === undefined) {\n        throw new Error('useAuth must be used within an AuthProvider')\n    }\n    return context\n}\n\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n    const [user, setUser] = useState<UserInfo | null>(null)\n    const [isLoading, setIsLoading] = useState(true)\n\n    const isAuthenticated = !!user\n\n    const login = async (username: string, password: string) => {\n        try {\n            const response = await api.login(username, password)\n            tokenManager.setTokens(response)\n            const userInfo = await api.getUserInfo()\n            setUser(userInfo)\n        } catch (error) {\n            console.error('Login failed:', error)\n            throw error\n        }\n    }\n\n    const updateUser = (user: UserInfo) => {\n        setUser(user)\n    }\n\n    const logout = async () => {\n        try {\n            await api.logout()\n        } catch (error) {\n            console.error('Logout error:', error)\n        } finally {\n            tokenManager.clearTokens()\n            setUser(null)\n        }\n    }\n\n\n\n    // 初始化认证状态\n    useEffect(() => {\n        const initAuth = async () => {\n            try {\n                const currentUser = await api.getUserInfo()\n                setUser(currentUser)\n            } catch (error) {\n                console.error('Auth initialization failed:', error)\n                setUser(null)\n            } finally {\n                setIsLoading(false)\n            }\n        }\n\n        initAuth()\n    }, [])\n\n    const value: AuthContextType = {\n        user,\n        isLoading,\n        isAuthenticated,\n        login,\n        logout,\n        updateUser,\n    }\n\n    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>\n}\n"
  },
  {
    "path": "web/src/components/providers/index.ts",
    "content": "// Provider components\nexport { ThemeProvider } from './theme-provider'\nexport { AuthProvider, useAuth } from './auth-provider'\nexport { AlertProvider, useAlert } from './alert-provider'\nexport { QueryProvider } from './query-provider'\n"
  },
  {
    "path": "web/src/components/providers/query-provider.tsx",
    "content": "\"use client\"\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { useState } from 'react'\n\nexport function QueryProvider({ children }: { children: React.ReactNode }) {\n    const [queryClient] = useState(() => new QueryClient({\n        defaultOptions: {\n            queries: {\n                staleTime: 5 * 60 * 1000,\n                gcTime: 10 * 60 * 1000,\n                refetchOnWindowFocus: true,\n                refetchOnReconnect: true,\n                refetchInterval: 5 * 60 * 1000,\n                refetchIntervalInBackground: false,\n                retry: (failureCount, error: any) => {\n                    if (error?.code >= 400 && error?.code < 500) {\n                        return false\n                    }\n                    return failureCount < 3\n                },\n                retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),\n            },\n            mutations: {\n                retry: (failureCount, error: any) => {\n                    if (error?.code >= 400 && error?.code < 500) {\n                        return false\n                    }\n                    return failureCount < 2\n                },\n                retryDelay: 1500,\n            },\n        },\n    }))\n\n    return (\n        <QueryClientProvider client={queryClient}>\n            {children}\n        </QueryClientProvider>\n    )\n} "
  },
  {
    "path": "web/src/components/providers/theme-provider.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\"\n\nexport function ThemeProvider({\n    children,\n    ...props\n}: React.ComponentProps<typeof NextThemesProvider>) {\n    return <NextThemesProvider {...props}>{children}</NextThemesProvider>\n}"
  },
  {
    "path": "web/src/components/shared/dynamic-config-form.tsx",
    "content": "import { Input } from \"@/src/components/ui/input\"\nimport { Label } from \"@/src/components/ui/label\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/src/components/ui/select\"\nimport { Switch } from \"@/src/components/ui/switch\"\nimport { Textarea } from \"@/src/components/ui/textarea\"\nimport type { DynamicConfigItem } from \"@/src/types/common\"\n\ninterface DynamicConfigFormProps {\n    configs: DynamicConfigItem[]\n    configValues: Record<string, unknown>\n    onConfigChange: (field: string, value: string | boolean | number) => void\n    isLoading?: boolean\n    typeName?: string\n}\n\nexport function DynamicConfigForm({\n    configs,\n    configValues,\n    onConfigChange,\n    isLoading = false,\n    typeName = \"配置\"\n}: DynamicConfigFormProps) {\n    // 验证动态配置字段是否为空\n    const isConfigFieldEmpty = (configName: string, configType: string, value: unknown): boolean => {\n        if (configType === 'boolean') return false // 布尔类型不需要验证\n        // 如果值为undefined或空字符串，则认为是空的\n        return value === undefined || value === '' || (typeof value === 'string' && value.trim() === '')\n    }\n\n    const renderConfigField = (config: DynamicConfigItem) => {\n        const value = configValues[config.key]\n        const isEmpty = isConfigFieldEmpty(config.key, config.type, value)\n        const showError = config.require && isEmpty\n\n        switch (config.type) {\n            case 'string':\n                if (config.options) {\n                    return (\n                        <Select\n                            value={value as string || ''}\n                            onValueChange={(val) => {\n                                const finalValue = val === '' && config.value ? config.value : val\n                                onConfigChange(config.key, finalValue)\n                            }}\n                        >\n                            <SelectTrigger className={showError ? 'border-red-500' : ''}>\n                                <SelectValue placeholder={config.value || `请选择${config.name}`} />\n                            </SelectTrigger>\n                            <SelectContent>\n                                {config.options.split(',').map((option: string) => (\n                                    <SelectItem key={option.trim()} value={option.trim()}>\n                                        {option.trim()}\n                                    </SelectItem>\n                                ))}\n                            </SelectContent>\n                        </Select>\n                    )\n                }\n                return (\n                    <Input\n                        type=\"text\"\n                        placeholder={config.value || `请输入${config.name}`}\n                        value={value as string || ''}\n                        onChange={(e) => {\n                            const inputValue = e.target.value\n                            const finalValue = inputValue === '' && config.value ? config.value : inputValue\n                            onConfigChange(config.key, finalValue)\n                        }}\n                        className={showError ? 'border-red-500' : ''}\n                    />\n                )\n\n            case 'number':\n                return (\n                    <Input\n                        type=\"number\"\n                        placeholder={config.value || `请输入${config.name}`}\n                        value={value as number || ''}\n                        onChange={(e) => {\n                            const inputValue = e.target.value\n                            if (inputValue === '') {\n                                const finalValue = config.value || ''\n                                onConfigChange(config.key, finalValue)\n                            } else {\n                                onConfigChange(config.key, Number(inputValue))\n                            }\n                        }}\n                        className={showError ? 'border-red-500' : ''}\n                    />\n                )\n\n            case 'boolean':\n                return (\n                    <div className=\"flex items-center justify-between w-full\">\n                        <span className=\"text-sm font-medium\">{config.name}</span>\n                        <Switch\n                            checked={value as boolean || false}\n                            onCheckedChange={(checked) => onConfigChange(config.key, checked)}\n                        />\n                    </div>\n                )\n\n            case 'textarea':\n                return (\n                    <Textarea\n                        placeholder={config.value || `请输入${config.name}`}\n                        value={value as string || ''}\n                        onChange={(e) => {\n                            const inputValue = e.target.value\n                            const finalValue = inputValue === '' && config.value ? config.value : inputValue\n                            onConfigChange(config.key, finalValue)\n                        }}\n                        className={showError ? 'border-red-500' : ''}\n                        rows={3}\n                    />\n                )\n\n            default:\n                return (\n                    <Input\n                        type=\"text\"\n                        placeholder={config.value || `请输入${config.name}`}\n                        value={value as string || ''}\n                        onChange={(e) => {\n                            const inputValue = e.target.value\n                            const finalValue = inputValue === '' && config.value ? config.value : inputValue\n                            onConfigChange(config.key, finalValue)\n                        }}\n                        className={showError ? 'border-red-500' : ''}\n                    />\n                )\n        }\n    }\n\n    if (isLoading) {\n        return (\n            <div className=\"text-center py-4 text-muted-foreground\">\n                加载配置中...\n            </div>\n        )\n    }\n\n    if (configs.length === 0) {\n        return (\n            <div className=\"text-center py-4 text-muted-foreground\">\n                选择{typeName}后将显示相关配置项\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"space-y-4\">\n            {configs.map((config) => (\n                <div key={config.key} className=\"space-y-2\">\n                    {config.type !== 'boolean' && (\n                        <Label htmlFor={config.key} className=\"block\">\n                            {config.name}\n                            {config.require && <span className=\"text-red-500 ml-1\">*</span>}\n                        </Label>\n                    )}\n                    {renderConfigField(config)}\n                    {config.desc && (\n                        <p className=\"text-sm text-muted-foreground\">\n                            {config.desc}\n                        </p>\n                    )}\n                    {config.require && isConfigFieldEmpty(config.key, config.type, configValues[config.key]) && (\n                        <p className=\"text-xs text-red-500\">此字段为必填项</p>\n                    )}\n                </div>\n            ))}\n        </div>\n    )\n} "
  },
  {
    "path": "web/src/components/shared/status-badge.tsx",
    "content": "import { Badge } from \"@/src/components/ui/badge\"\nimport type { ComponentProps } from \"react\"\n\ntype BadgeVariant = ComponentProps<typeof Badge>[\"variant\"]\n\ninterface StatusConfig {\n    variant: BadgeVariant\n    className: string\n    text: string\n}\n\nconst STATUS_CONFIG: Record<string, StatusConfig> = {\n    running: { variant: 'default', className: 'bg-blue-500 hover:bg-blue-600 text-white', text: '运行中' },\n    scheduled: { variant: 'default', className: 'bg-teal-500 hover:bg-teal-600 text-white', text: '已调度' },\n    pending: { variant: 'default', className: 'bg-yellow-500 hover:bg-yellow-600 text-white', text: '等待中' },\n    disabled: { variant: 'secondary', className: 'bg-gray-500 hover:bg-gray-600 text-white', text: '已停用' },\n    enabled: { variant: 'default', className: 'bg-green-500 hover:bg-green-600 text-white', text: '已启用' },\n} as const\n\nconst getUnknownConfig = (status: string): StatusConfig => ({\n    variant: \"outline\",\n    className: \"\",\n    text: status || \"未知\"\n})\n\nexport function StatusBadge({ status }: { status: string }) {\n    const config = STATUS_CONFIG[status] || getUnknownConfig(status)\n\n    return (\n        <Badge variant={config.variant} className={config.className}>\n            {config.text}\n        </Badge>\n    )\n}\n\nexport default StatusBadge\n"
  },
  {
    "path": "web/src/components/shared/subscription-section.tsx",
    "content": "import { useMemo } from 'react'\nimport { Controller, Control } from 'react-hook-form'\nimport { Label } from '@/src/components/ui/label'\nimport { Badge } from '@/src/components/ui/badge'\nimport { Switch } from '@/src/components/ui/switch'\nimport { useSubs } from '@/src/lib/queries/sub-queries'\n\ninterface SubscriptionSectionProps {\n    control: Control<Record<string, unknown> | any>\n    subIdField: string\n    subIdExcludeField: string\n}\n\nexport function SubscriptionSection({ control, subIdField, subIdExcludeField }: SubscriptionSectionProps) {\n    const { data: subs = [], isLoading, error } = useSubs()\n\n    const subList = useMemo(() =>\n        subs.map(sub => ({ id: sub.id, name: sub.name })),\n        [subs]\n    )\n\n    if (isLoading) {\n        return (\n            <div className=\"w-full\">\n                <Label className=\"mb-2 block\">选择订阅</Label>\n                <div className=\"text-center py-4 text-muted-foreground\">\n                    <p className=\"text-sm\">加载订阅中...</p>\n                </div>\n            </div>\n        )\n    }\n\n    if (error) {\n        return (\n            <div className=\"w-full\">\n                <Label className=\"mb-2 block\">选择订阅</Label>\n                <div className=\"text-center py-4 text-destructive\">\n                    <p className=\"text-sm\">加载失败: {error.message}</p>\n                </div>\n            </div>\n        )\n    }\n\n    if (subList.length === 0) {\n        return (\n            <div className=\"w-full\">\n                <Label className=\"mb-2 block\">选择订阅</Label>\n                <div className=\"text-center py-4 text-muted-foreground\">\n                    <p className=\"text-sm\">暂无可用订阅</p>\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <Controller\n            name={subIdField}\n            control={control}\n            render={({ field }) => {\n\n                return (\n                    <div className=\"w-full\">\n                        <div className=\"flex items-center justify-between mb-2\">\n                            <Label htmlFor=\"sub_id\" className=\"block\">\n                                选择订阅\n                            </Label>\n                            <div className=\"flex items-center space-x-2\">\n                                <Label htmlFor={`${subIdField}-exclude`} className=\"text-sm text-muted-foreground\">\n                                    排除模式\n                                </Label>\n                                <Controller\n                                    name={subIdExcludeField}\n                                    control={control}\n                                    render={({ field: excludeField }) => (\n                                        <Switch\n                                            id={`${subIdField}-exclude`}\n                                            checked={excludeField.value || false}\n                                            onCheckedChange={excludeField.onChange}\n                                        />\n                                    )}\n                                />\n                            </div>\n                        </div>\n                        <div className=\"space-y-2\">\n                            <div className=\"flex flex-wrap gap-2\">\n                                {subList.map(sub => {\n                                    const selectedSubIds = (field.value as number[]) || []\n                                    const isSelected = selectedSubIds.includes(sub.id)\n\n                                    const handleToggleSelection = (subId: number) => {\n                                        if (isSelected) {\n                                            field.onChange(selectedSubIds.filter((id: number) => id !== subId))\n                                        } else {\n                                            field.onChange([...selectedSubIds, subId])\n                                        }\n                                    }\n\n                                    return (\n                                        <Badge\n                                            key={sub.id}\n                                            variant={isSelected ? \"default\" : \"outline\"}\n                                            className={`cursor-pointer transition-colors ${isSelected\n                                                ? \"hover:bg-red-100 hover:text-red-700\"\n                                                : \"hover:bg-green-100 hover:text-green-700\"\n                                                }`}\n                                            onClick={() => handleToggleSelection(sub.id)}\n                                        >\n                                            {sub.name} {isSelected ? \"×\" : \"+\"}\n                                        </Badge>\n                                    )\n                                })}\n                            </div>\n                        </div>\n                        <p className=\"text-xs text-muted-foreground mt-2\">\n                            点击订阅进行选择/取消选择，不选择则视为全选\n                        </p>\n                    </div>\n                )\n            }}\n        />\n    )\n}"
  },
  {
    "path": "web/src/components/ui/accordion.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils/index\"\n\nfunction Accordion({\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  )\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n      {...props}\n    >\n      <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n    </AccordionPrimitive.Content>\n  )\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n"
  },
  {
    "path": "web/src/components/ui/alert-dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from \"@/src/utils/index\"\nimport { buttonVariants } from \"@/src/components/ui/button\"\n\nfunction AlertDialog({\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {\n  return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />\n}\n\nfunction AlertDialogTrigger({\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {\n  return (\n    <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />\n  )\n}\n\nfunction AlertDialogPortal({\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {\n  return (\n    <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />\n  )\n}\n\nfunction AlertDialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {\n  return (\n    <AlertDialogPrimitive.Overlay\n      data-slot=\"alert-dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDialogContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {\n  return (\n    <AlertDialogPortal>\n      <AlertDialogOverlay />\n      <AlertDialogPrimitive.Content\n        data-slot=\"alert-dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg\",\n          className\n        )}\n        {...props}\n      />\n    </AlertDialogPortal>\n  )\n}\n\nfunction AlertDialogHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDialogFooter({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n  return (\n    <AlertDialogPrimitive.Title\n      data-slot=\"alert-dialog-title\"\n      className={cn(\"text-lg font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n  return (\n    <AlertDialogPrimitive.Description\n      data-slot=\"alert-dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDialogAction({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {\n  return (\n    <AlertDialogPrimitive.Action\n      className={cn(buttonVariants(), className)}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDialogCancel({\n  className,\n  ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {\n  return (\n    <AlertDialogPrimitive.Cancel\n      className={cn(buttonVariants({ variant: \"outline\" }), className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n}\n"
  },
  {
    "path": "web/src/components/ui/avatar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"bg-muted flex size-full items-center justify-center rounded-full\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "web/src/components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/src/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"span\"\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "web/src/components/ui/breadcrumb.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Breadcrumb({ ...props }: React.ComponentProps<\"nav\">) {\n  return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" {...props} />\n}\n\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n  return (\n    <ol\n      data-slot=\"breadcrumb-list\"\n      className={cn(\n        \"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"breadcrumb-item\"\n      className={cn(\"inline-flex items-center gap-1.5\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction BreadcrumbLink({\n  asChild,\n  className,\n  ...props\n}: React.ComponentProps<\"a\"> & {\n  asChild?: boolean\n}) {\n  const Comp = asChild ? Slot : \"a\"\n\n  return (\n    <Comp\n      data-slot=\"breadcrumb-link\"\n      className={cn(\"hover:text-foreground transition-colors\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"breadcrumb-page\"\n      role=\"link\"\n      aria-disabled=\"true\"\n      aria-current=\"page\"\n      className={cn(\"text-foreground font-normal\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction BreadcrumbSeparator({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"breadcrumb-separator\"\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(\"[&>svg]:size-3.5\", className)}\n      {...props}\n    >\n      {children ?? <ChevronRight />}\n    </li>\n  )\n}\n\nfunction BreadcrumbEllipsis({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"breadcrumb-ellipsis\"\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(\"flex size-9 items-center justify-center\", className)}\n      {...props}\n    >\n      <MoreHorizontal className=\"size-4\" />\n      <span className=\"sr-only\">More</span>\n    </span>\n  )\n}\n\nexport {\n  Breadcrumb,\n  BreadcrumbList,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  BreadcrumbEllipsis,\n}\n"
  },
  {
    "path": "web/src/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/src/utils/index\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n  }) {\n  const Comp = asChild ? Slot : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "web/src/components/ui/calendar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n} from \"lucide-react\"\nimport { DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\"\n\nimport { cn } from \"@/src/utils/index\"\nimport { Button, buttonVariants } from \"@/src/components/ui/button\"\n\nfunction Calendar({\n  className,\n  classNames,\n  showOutsideDays = true,\n  captionLayout = \"label\",\n  buttonVariant = \"ghost\",\n  formatters,\n  components,\n  ...props\n}: React.ComponentProps<typeof DayPicker> & {\n  buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"]\n}) {\n  const defaultClassNames = getDefaultClassNames()\n\n  return (\n    <DayPicker\n      showOutsideDays={showOutsideDays}\n      className={cn(\n        \"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n        String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n        String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n        className\n      )}\n      captionLayout={captionLayout}\n      formatters={{\n        formatMonthDropdown: (date) =>\n          date.toLocaleString(\"default\", { month: \"short\" }),\n        ...formatters,\n      }}\n      classNames={{\n        root: cn(\"w-fit\", defaultClassNames.root),\n        months: cn(\n          \"flex gap-4 flex-col md:flex-row relative\",\n          defaultClassNames.months\n        ),\n        month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n        nav: cn(\n          \"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\",\n          defaultClassNames.nav\n        ),\n        button_previous: cn(\n          buttonVariants({ variant: buttonVariant }),\n          \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n          defaultClassNames.button_previous\n        ),\n        button_next: cn(\n          buttonVariants({ variant: buttonVariant }),\n          \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n          defaultClassNames.button_next\n        ),\n        month_caption: cn(\n          \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n          defaultClassNames.month_caption\n        ),\n        dropdowns: cn(\n          \"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5\",\n          defaultClassNames.dropdowns\n        ),\n        dropdown_root: cn(\n          \"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md\",\n          defaultClassNames.dropdown_root\n        ),\n        dropdown: cn(\n          \"absolute bg-popover inset-0 opacity-0\",\n          defaultClassNames.dropdown\n        ),\n        caption_label: cn(\n          \"select-none font-medium\",\n          captionLayout === \"label\"\n            ? \"text-sm\"\n            : \"rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5\",\n          defaultClassNames.caption_label\n        ),\n        table: \"w-full border-collapse\",\n        weekdays: cn(\"flex\", defaultClassNames.weekdays),\n        weekday: cn(\n          \"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none\",\n          defaultClassNames.weekday\n        ),\n        week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n        week_number_header: cn(\n          \"select-none w-(--cell-size)\",\n          defaultClassNames.week_number_header\n        ),\n        week_number: cn(\n          \"text-[0.8rem] select-none text-muted-foreground\",\n          defaultClassNames.week_number\n        ),\n        day: cn(\n          \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none\",\n          defaultClassNames.day\n        ),\n        range_start: cn(\n          \"rounded-l-md bg-accent\",\n          defaultClassNames.range_start\n        ),\n        range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n        range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n        today: cn(\n          \"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none\",\n          defaultClassNames.today\n        ),\n        outside: cn(\n          \"text-muted-foreground aria-selected:text-muted-foreground\",\n          defaultClassNames.outside\n        ),\n        disabled: cn(\n          \"text-muted-foreground opacity-50\",\n          defaultClassNames.disabled\n        ),\n        hidden: cn(\"invisible\", defaultClassNames.hidden),\n        ...classNames,\n      }}\n      components={{\n        Root: ({ className, rootRef, ...props }) => {\n          return (\n            <div\n              data-slot=\"calendar\"\n              ref={rootRef}\n              className={cn(className)}\n              {...props}\n            />\n          )\n        },\n        Chevron: ({ className, orientation, ...props }) => {\n          if (orientation === \"left\") {\n            return (\n              <ChevronLeftIcon className={cn(\"size-4\", className)} {...props} />\n            )\n          }\n\n          if (orientation === \"right\") {\n            return (\n              <ChevronRightIcon\n                className={cn(\"size-4\", className)}\n                {...props}\n              />\n            )\n          }\n\n          return (\n            <ChevronDownIcon className={cn(\"size-4\", className)} {...props} />\n          )\n        },\n        DayButton: CalendarDayButton,\n        WeekNumber: ({ children, ...props }) => {\n          return (\n            <td {...props}>\n              <div className=\"flex size-(--cell-size) items-center justify-center text-center\">\n                {children}\n              </div>\n            </td>\n          )\n        },\n        ...components,\n      }}\n      {...props}\n    />\n  )\n}\n\nfunction CalendarDayButton({\n  className,\n  day,\n  modifiers,\n  ...props\n}: React.ComponentProps<typeof DayButton>) {\n  const defaultClassNames = getDefaultClassNames()\n\n  const ref = React.useRef<HTMLButtonElement>(null)\n  React.useEffect(() => {\n    if (modifiers.focused) ref.current?.focus()\n  }, [modifiers.focused])\n\n  return (\n    <Button\n      ref={ref}\n      variant=\"ghost\"\n      size=\"icon\"\n      data-day={day.date.toLocaleDateString()}\n      data-selected-single={\n        modifiers.selected &&\n        !modifiers.range_start &&\n        !modifiers.range_end &&\n        !modifiers.range_middle\n      }\n      data-range-start={modifiers.range_start}\n      data-range-end={modifiers.range_end}\n      data-range-middle={modifiers.range_middle}\n      className={cn(\n        \"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70\",\n        defaultClassNames.day,\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Calendar, CalendarDayButton }\n"
  },
  {
    "path": "web/src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n}\n"
  },
  {
    "path": "web/src/components/ui/chart.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\n\nimport { cn } from \"@/src/utils\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode\n    icon?: React.ComponentType\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  )\n}\n\ntype ChartContextProps = {\n  config: ChartConfig\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null)\n\nfunction useChart() {\n  const context = React.useContext(ChartContext)\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\")\n  }\n\n  return context\n}\n\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  config: ChartConfig\n  children: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"children\"]\n}) {\n  const uniqueId = React.useId()\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          className\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  )\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color\n  )\n\n  if (!colorConfig.length) {\n    return null\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n                .map(([key, itemConfig]) => {\n                  const color =\n                    itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n                    itemConfig.color\n                  return color ? `  --color-${key}: ${color};` : null\n                })\n                .join(\"\\n\")}\n}\n`\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  )\n}\n\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean\n    hideIndicator?: boolean\n    indicator?: \"line\" | \"dot\" | \"dashed\"\n    nameKey?: string\n    labelKey?: string\n  }) {\n  const { config } = useChart()\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null\n    }\n\n    const [item] = payload\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`\n    const itemConfig = getPayloadConfigFromPayload(config, item, key)\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      )\n    }\n\n    if (!value) {\n      return null\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ])\n\n  if (!active || !payload?.length) {\n    return null\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\"\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl\",\n        className\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload.map((item, index) => {\n          const key = `${nameKey || item.name || item.dataKey || \"value\"}`\n          const itemConfig = getPayloadConfigFromPayload(config, item, key)\n          const indicatorColor = color || item.payload.fill || item.color\n\n          return (\n            <div\n              key={item.dataKey}\n              className={cn(\n                \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n                indicator === \"dot\" && \"items-center\"\n              )}\n            >\n              {formatter && item?.value !== undefined && item.name ? (\n                formatter(item.value, item.name, item, index, item.payload)\n              ) : (\n                <>\n                  {itemConfig?.icon ? (\n                    <itemConfig.icon />\n                  ) : (\n                    !hideIndicator && (\n                      <div\n                        className={cn(\n                          \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                          {\n                            \"h-2.5 w-2.5\": indicator === \"dot\",\n                            \"w-1\": indicator === \"line\",\n                            \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                              indicator === \"dashed\",\n                            \"my-0.5\": nestLabel && indicator === \"dashed\",\n                          }\n                        )}\n                        style={\n                          {\n                            \"--color-bg\": indicatorColor,\n                            \"--color-border\": indicatorColor,\n                          } as React.CSSProperties\n                        }\n                      />\n                    )\n                  )}\n                  <div\n                    className={cn(\n                      \"flex flex-1 justify-between leading-none\",\n                      nestLabel ? \"items-end\" : \"items-center\"\n                    )}\n                  >\n                    <div className=\"grid gap-1.5\">\n                      {nestLabel ? tooltipLabel : null}\n                      <span className=\"text-muted-foreground\">\n                        {itemConfig?.label || item.name}\n                      </span>\n                    </div>\n                    {item.value && (\n                      <span className=\"text-foreground font-mono font-medium tabular-nums\">\n                        {item.value.toLocaleString()}\n                      </span>\n                    )}\n                  </div>\n                </>\n              )}\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nconst ChartLegend = RechartsPrimitive.Legend\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean\n    nameKey?: string\n  }) {\n  const { config } = useChart()\n\n  if (!payload?.length) {\n    return null\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className\n      )}\n    >\n      {payload.map((item) => {\n        const key = `${nameKey || item.dataKey || \"value\"}`\n        const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n        return (\n          <div\n            key={item.value}\n            className={cn(\n              \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\"\n            )}\n          >\n            {itemConfig?.icon && !hideIcon ? (\n              <itemConfig.icon />\n            ) : (\n              <div\n                className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                style={{\n                  backgroundColor: item.color,\n                }}\n              />\n            )}\n            {itemConfig?.label}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n      typeof payload.payload === \"object\" &&\n      payload.payload !== null\n      ? payload.payload\n      : undefined\n\n  let configLabelKey: string = key\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config]\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n}\n"
  },
  {
    "path": "web/src/components/ui/checkbox.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { CheckIcon } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Checkbox({\n  className,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator\n        data-slot=\"checkbox-indicator\"\n        className=\"flex items-center justify-center text-current transition-none\"\n      >\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  )\n}\n\nexport { Checkbox }\n"
  },
  {
    "path": "web/src/components/ui/dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean\n}) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close\n            data-slot=\"dialog-close\"\n            className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n          >\n            <XIcon />\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  )\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-lg leading-none font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n}\n"
  },
  {
    "path": "web/src/components/ui/drawer.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Drawer({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n  return <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />\n}\n\nfunction DrawerTrigger({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n  return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />\n}\n\nfunction DrawerPortal({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n  return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />\n}\n\nfunction DrawerClose({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n  return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />\n}\n\nfunction DrawerOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n  return (\n    <DrawerPrimitive.Overlay\n      data-slot=\"drawer-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DrawerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n  return (\n    <DrawerPortal data-slot=\"drawer-portal\">\n      <DrawerOverlay />\n      <DrawerPrimitive.Content\n        data-slot=\"drawer-content\"\n        className={cn(\n          \"group/drawer-content bg-background fixed z-50 flex h-auto flex-col\",\n          \"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b\",\n          \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t\",\n          \"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm\",\n          \"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm\",\n          className\n        )}\n        {...props}\n      >\n        <div className=\"bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n        {children}\n      </DrawerPrimitive.Content>\n    </DrawerPortal>\n  )\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-header\"\n      className={cn(\n        \"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DrawerTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n  return (\n    <DrawerPrimitive.Title\n      data-slot=\"drawer-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DrawerDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n  return (\n    <DrawerPrimitive.Description\n      data-slot=\"drawer-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n}\n"
  },
  {
    "path": "web/src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  )\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  )\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  )\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean\n  variant?: \"default\" | \"destructive\"\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      checked={checked ?? false}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  )\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  )\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  )\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n}\n"
  },
  {
    "path": "web/src/components/ui/hover-card.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"@/src/utils/index\"\n\nfunction HoverCard({\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n  return <HoverCardPrimitive.Root data-slot=\"hover-card\" {...props} />\n}\n\nfunction HoverCardTrigger({\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n  return (\n    <HoverCardPrimitive.Trigger data-slot=\"hover-card-trigger\" {...props} />\n  )\n}\n\nfunction HoverCardContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {\n  return (\n    <HoverCardPrimitive.Portal data-slot=\"hover-card-portal\">\n      <HoverCardPrimitive.Content\n        data-slot=\"hover-card-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n          className\n        )}\n        {...props}\n      />\n    </HoverCardPrimitive.Portal>\n  )\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n"
  },
  {
    "path": "web/src/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Input }\n"
  },
  {
    "path": "web/src/components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Label }\n"
  },
  {
    "path": "web/src/components/ui/loading.tsx",
    "content": "\"use client\"\n\nimport { cn } from \"@/src/utils\"\n\ninterface LoadingProps {\n  message?: string\n  className?: string\n  size?: 'sm' | 'md' | 'lg'\n  variant?: 'fullscreen' | 'inline' | 'overlay'\n  showMessage?: boolean\n}\n\nconst sizeClasses = {\n  sm: 'h-4 w-4',\n  md: 'h-6 w-6',\n  lg: 'h-8 w-8'\n}\n\nconst variantClasses = {\n  fullscreen: 'flex min-h-screen items-center justify-center',\n  inline: 'flex items-center justify-center p-4',\n  overlay: 'absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm'\n}\n\nexport function Loading({\n  message = \"加载中...\",\n  className = \"\",\n  size = 'md',\n  variant = 'fullscreen',\n  showMessage = true\n}: LoadingProps) {\n  return (\n    <div className={cn(variantClasses[variant], className)}>\n      <div className=\"text-center\">\n        <div\n          className={cn(\n            \"animate-spin rounded-full border-b-2 border-primary mx-auto\",\n            sizeClasses[size],\n            showMessage && \"mb-4\"\n          )}\n        />\n        {showMessage && (\n          <p className=\"text-muted-foreground text-sm\">{message}</p>\n        )}\n      </div>\n    </div>\n  )\n}\n\n// 便捷组件\nexport function InlineLoading({ message = \"加载中...\", size = 'sm' as const }) {\n  return (\n    <Loading\n      variant=\"inline\"\n      size={size}\n      message={message}\n      className=\"py-2\"\n    />\n  )\n}\n\nexport function PageLoading({ message = \"页面加载中...\" }) {\n  return (\n    <Loading\n      variant=\"fullscreen\"\n      size=\"lg\"\n      message={message}\n    />\n  )\n}\n\n// 仅显示spinner，不显示文字\nexport function Spinner({ size = 'md', className = \"\" }: { size?: 'sm' | 'md' | 'lg', className?: string }) {\n  return (\n    <Loading\n      variant=\"inline\"\n      size={size}\n      showMessage={false}\n      className={cn(\"p-0\", className)}\n    />\n  )\n}\n"
  },
  {
    "path": "web/src/components/ui/mode-toggle.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Moon, Sun } from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\n\nimport { Button } from \"@/src/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/src/components/ui/dropdown-menu\"\n\nexport function ModeToggle() {\n  const { setTheme } = useTheme()\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <Sun className=\"h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90\" />\n          <Moon className=\"absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n          Light\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n          Dark\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n          System\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "web/src/components/ui/popover.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n          className\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  )\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"
  },
  {
    "path": "web/src/components/ui/select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\"\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  )\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"popper\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md\",\n          position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className\n        )}\n        position={position}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\"\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  )\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className\n      )}\n      {...props}\n    >\n      <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  )\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  )\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  )\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n}\n"
  },
  {
    "path": "web/src/components/ui/separator.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Separator }\n"
  },
  {
    "path": "web/src/components/ui/sheet.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n          \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n          \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" &&\n          \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n          \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n          <XIcon className=\"size-4\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  )\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n}\n"
  },
  {
    "path": "web/src/components/ui/sidebar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, VariantProps } from \"class-variance-authority\"\nimport { PanelLeftIcon } from \"lucide-react\"\n\nimport { useIsMobile } from \"@/src/lib/hooks/use-mobile\"\nimport { cn } from \"@/src/utils\"\nimport { Button } from \"@/src/components/ui/button\"\nimport { Input } from \"@/src/components/ui/input\"\nimport { Separator } from \"@/src/components/ui/separator\"\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n} from \"@/src/components/ui/sheet\"\nimport { Skeleton } from \"@/src/components/ui/skeleton\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/src/components/ui/tooltip\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\ntype SidebarContextProps = {\n  state: \"expanded\" | \"collapsed\"\n  open: boolean\n  setOpen: (open: boolean) => void\n  openMobile: boolean\n  setOpenMobile: (open: boolean) => void\n  isMobile: boolean\n  toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n  const context = React.useContext(SidebarContext)\n  if (!context) {\n    throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n  }\n\n  return context\n}\n\nfunction SidebarProvider({\n  defaultOpen = true,\n  open: openProp,\n  onOpenChange: setOpenProp,\n  className,\n  style,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  defaultOpen?: boolean\n  open?: boolean\n  onOpenChange?: (open: boolean) => void\n}) {\n  const isMobile = useIsMobile()\n  const [openMobile, setOpenMobile] = React.useState(false)\n\n  // This is the internal state of the sidebar.\n  // We use openProp and setOpenProp for control from outside the component.\n  const [_open, _setOpen] = React.useState(defaultOpen)\n  const open = openProp ?? _open\n  const setOpen = React.useCallback(\n    (value: boolean | ((value: boolean) => boolean)) => {\n      const openState = typeof value === \"function\" ? value(open) : value\n      if (setOpenProp) {\n        setOpenProp(openState)\n      } else {\n        _setOpen(openState)\n      }\n\n      // This sets the cookie to keep the sidebar state.\n      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n    },\n    [setOpenProp, open]\n  )\n\n  // Helper to toggle the sidebar.\n  const toggleSidebar = React.useCallback(() => {\n    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)\n  }, [isMobile, setOpen, setOpenMobile])\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault()\n        toggleSidebar()\n      }\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown)\n    return () => window.removeEventListener(\"keydown\", handleKeyDown)\n  }, [toggleSidebar])\n\n  // We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n  // This makes it easier to style the sidebar with Tailwind classes.\n  const state = open ? \"expanded\" : \"collapsed\"\n\n  const contextValue = React.useMemo<SidebarContextProps>(\n    () => ({\n      state,\n      open,\n      setOpen,\n      isMobile,\n      openMobile,\n      setOpenMobile,\n      toggleSidebar,\n    }),\n    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n  )\n\n  return (\n    <SidebarContext.Provider value={contextValue}>\n      <TooltipProvider delayDuration={0}>\n        <div\n          data-slot=\"sidebar-wrapper\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH,\n              \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n              ...style,\n            } as React.CSSProperties\n          }\n          className={cn(\n            \"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full\",\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      </TooltipProvider>\n    </SidebarContext.Provider>\n  )\n}\n\nfunction Sidebar({\n  side = \"left\",\n  variant = \"sidebar\",\n  collapsible = \"offcanvas\",\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  side?: \"left\" | \"right\"\n  variant?: \"sidebar\" | \"floating\" | \"inset\"\n  collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n}) {\n  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()\n\n  if (collapsible === \"none\") {\n    return (\n      <div\n        data-slot=\"sidebar\"\n        className={cn(\n          \"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  }\n\n  if (isMobile) {\n    return (\n      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n        <SheetContent\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar\"\n          data-mobile=\"true\"\n          className=\"bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n            } as React.CSSProperties\n          }\n          side={side}\n        >\n          <SheetHeader className=\"sr-only\">\n            <SheetTitle>Sidebar</SheetTitle>\n            <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n          </SheetHeader>\n          <div className=\"flex h-full w-full flex-col\">{children}</div>\n        </SheetContent>\n      </Sheet>\n    )\n  }\n\n  return (\n    <div\n      className=\"group peer text-sidebar-foreground hidden md:block\"\n      data-state={state}\n      data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n      data-variant={variant}\n      data-side={side}\n      data-slot=\"sidebar\"\n    >\n      {/* This is what handles the sidebar gap on desktop */}\n      <div\n        data-slot=\"sidebar-gap\"\n        className={cn(\n          \"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear\",\n          \"group-data-[collapsible=offcanvas]:w-0\",\n          \"group-data-[side=right]:rotate-180\",\n          variant === \"floating\" || variant === \"inset\"\n            ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\"\n        )}\n      />\n      <div\n        data-slot=\"sidebar-container\"\n        className={cn(\n          \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex\",\n          side === \"left\"\n            ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n            : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n          // Adjust the padding for floating and inset variants.\n          variant === \"floating\" || variant === \"inset\"\n            ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n          className\n        )}\n        {...props}\n      >\n        <div\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar-inner\"\n          className=\"bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm\"\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction SidebarTrigger({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { toggleSidebar } = useSidebar()\n\n  return (\n    <Button\n      data-sidebar=\"trigger\"\n      data-slot=\"sidebar-trigger\"\n      variant=\"ghost\"\n      size=\"icon\"\n      className={cn(\"size-7\", className)}\n      onClick={(event) => {\n        onClick?.(event)\n        toggleSidebar()\n      }}\n      {...props}\n    >\n      <PanelLeftIcon />\n      <span className=\"sr-only\">Toggle Sidebar</span>\n    </Button>\n  )\n}\n\nfunction SidebarRail({ className, ...props }: React.ComponentProps<\"button\">) {\n  const { toggleSidebar } = useSidebar()\n\n  return (\n    <button\n      data-sidebar=\"rail\"\n      data-slot=\"sidebar-rail\"\n      aria-label=\"Toggle Sidebar\"\n      tabIndex={-1}\n      onClick={toggleSidebar}\n      title=\"Toggle Sidebar\"\n      className={cn(\n        \"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex\",\n        \"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize\",\n        \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n        \"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full\",\n        \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n        \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"main\">) {\n  return (\n    <main\n      data-slot=\"sidebar-inset\"\n      className={cn(\n        \"bg-background relative flex w-full flex-1 flex-col\",\n        \"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof Input>) {\n  return (\n    <Input\n      data-slot=\"sidebar-input\"\n      data-sidebar=\"input\"\n      className={cn(\"bg-background h-8 w-full shadow-none\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-header\"\n      data-sidebar=\"header\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-footer\"\n      data-sidebar=\"footer\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"sidebar-separator\"\n      data-sidebar=\"separator\"\n      className={cn(\"bg-sidebar-border mx-2 w-auto\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-content\"\n      data-sidebar=\"content\"\n      className={cn(\n        \"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group\"\n      data-sidebar=\"group\"\n      className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroupLabel({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-label\"\n      data-sidebar=\"group-label\"\n      className={cn(\n        \"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroupAction({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-action\"\n      data-sidebar=\"group-action\"\n      className={cn(\n        \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        // Increases the hit area of the button on mobile.\n        \"after:absolute after:-inset-2 md:after:hidden\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroupContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group-content\"\n      data-sidebar=\"group-content\"\n      className={cn(\"w-full text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu\"\n      data-sidebar=\"menu\"\n      className={cn(\"flex w-full min-w-0 flex-col gap-1\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-item\"\n      data-sidebar=\"menu-item\"\n      className={cn(\"group/menu-item relative\", className)}\n      {...props}\n    />\n  )\n}\n\nconst sidebarMenuButtonVariants = cva(\n  \"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n        outline:\n          \"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]\",\n      },\n      size: {\n        default: \"h-8 text-sm\",\n        sm: \"h-7 text-xs\",\n        lg: \"h-12 text-sm group-data-[collapsible=icon]:p-0!\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction SidebarMenuButton({\n  asChild = false,\n  isActive = false,\n  variant = \"default\",\n  size = \"default\",\n  tooltip,\n  className,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean\n  isActive?: boolean\n  tooltip?: string | React.ComponentProps<typeof TooltipContent>\n} & VariantProps<typeof sidebarMenuButtonVariants>) {\n  const Comp = asChild ? Slot : \"button\"\n  const { isMobile, state } = useSidebar()\n\n  const button = (\n    <Comp\n      data-slot=\"sidebar-menu-button\"\n      data-sidebar=\"menu-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n      {...props}\n    />\n  )\n\n  if (!tooltip) {\n    return button\n  }\n\n  if (typeof tooltip === \"string\") {\n    tooltip = {\n      children: tooltip,\n    }\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent\n        side=\"right\"\n        align=\"center\"\n        hidden={state !== \"collapsed\" || isMobile}\n        {...tooltip}\n      />\n    </Tooltip>\n  )\n}\n\nfunction SidebarMenuAction({\n  className,\n  asChild = false,\n  showOnHover = false,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean\n  showOnHover?: boolean\n}) {\n  const Comp = asChild ? Slot : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-action\"\n      data-sidebar=\"menu-action\"\n      className={cn(\n        \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        // Increases the hit area of the button on mobile.\n        \"after:absolute after:-inset-2 md:after:hidden\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        showOnHover &&\n        \"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuBadge({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-menu-badge\"\n      data-sidebar=\"menu-badge\"\n      className={cn(\n        \"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none\",\n        \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuSkeleton({\n  className,\n  showIcon = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showIcon?: boolean\n}) {\n  // Random width between 50 to 90%.\n  const width = React.useMemo(() => {\n    return `${Math.floor(Math.random() * 40) + 50}%`\n  }, [])\n\n  return (\n    <div\n      data-slot=\"sidebar-menu-skeleton\"\n      data-sidebar=\"menu-skeleton\"\n      className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n      {...props}\n    >\n      {showIcon && (\n        <Skeleton\n          className=\"size-4 rounded-md\"\n          data-sidebar=\"menu-skeleton-icon\"\n        />\n      )}\n      <Skeleton\n        className=\"h-4 max-w-(--skeleton-width) flex-1\"\n        data-sidebar=\"menu-skeleton-text\"\n        style={\n          {\n            \"--skeleton-width\": width,\n          } as React.CSSProperties\n        }\n      />\n    </div>\n  )\n}\n\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu-sub\"\n      data-sidebar=\"menu-sub\"\n      className={cn(\n        \"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuSubItem({\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-sub-item\"\n      data-sidebar=\"menu-sub-item\"\n      className={cn(\"group/menu-sub-item relative\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuSubButton({\n  asChild = false,\n  size = \"md\",\n  isActive = false,\n  className,\n  ...props\n}: React.ComponentProps<\"a\"> & {\n  asChild?: boolean\n  size?: \"sm\" | \"md\"\n  isActive?: boolean\n}) {\n  const Comp = asChild ? Slot : \"a\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-sub-button\"\n      data-sidebar=\"menu-sub-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(\n        \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n        size === \"sm\" && \"text-xs\",\n        size === \"md\" && \"text-sm\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupAction,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInput,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSkeleton,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  SidebarProvider,\n  SidebarRail,\n  SidebarSeparator,\n  SidebarTrigger,\n  useSidebar,\n}\n"
  },
  {
    "path": "web/src/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/src/utils\"\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "web/src/components/ui/sonner.tsx",
    "content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme: currentTheme } = useTheme()\n  const { theme: _ignoredTheme, ...restProps } = props\n\n  const resolvedTheme: ToasterProps[\"theme\"] =\n    currentTheme === \"light\" || currentTheme === \"dark\" || currentTheme === \"system\"\n      ? currentTheme\n      : \"system\"\n\n  return (\n    <Sonner\n      theme={resolvedTheme}\n      className=\"toaster group\"\n      style={\n        {\n          \"--normal-bg\": \"var(--popover)\",\n          \"--normal-text\": \"var(--popover-foreground)\",\n          \"--normal-border\": \"var(--border)\",\n        } as React.CSSProperties\n      }\n      {...restProps}\n    />\n  )\n}\n\nexport { Toaster }\n"
  },
  {
    "path": "web/src/components/ui/switch.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Switch({\n  className,\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\"\n        )}\n      />\n    </SwitchPrimitive.Root>\n  )\n}\n\nexport { Switch }\n"
  },
  {
    "path": "web/src/components/ui/table.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto scrollbar-hide\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  )\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n}\n"
  },
  {
    "path": "web/src/components/ui/tabs.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction Tabs({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction TabsList({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      className={cn(\n        \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
  },
  {
    "path": "web/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/src/utils\"\n\ntype TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "web/src/components/ui/toggle-group.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/src/utils\"\nimport { toggleVariants } from \"@/src/components/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants>\n>({\n  size: \"default\",\n  variant: \"default\",\n})\n\nfunction ToggleGroup({\n  className,\n  variant,\n  size,\n  children,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <ToggleGroupPrimitive.Root\n      data-slot=\"toggle-group\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(\n        \"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs\",\n        className\n      )}\n      {...props}\n    >\n      <ToggleGroupContext.Provider value={{ variant, size }}>\n        {children}\n      </ToggleGroupContext.Provider>\n    </ToggleGroupPrimitive.Root>\n  )\n}\n\nfunction ToggleGroupItem({\n  className,\n  children,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n  VariantProps<typeof toggleVariants>) {\n  const context = React.useContext(ToggleGroupContext)\n\n  return (\n    <ToggleGroupPrimitive.Item\n      data-slot=\"toggle-group-item\"\n      data-variant={context.variant || variant}\n      data-size={context.size || size}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        \"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n"
  },
  {
    "path": "web/src/components/ui/toggle.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/src/utils\"\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-2 min-w-9\",\n        sm: \"h-8 px-1.5 min-w-8\",\n        lg: \"h-10 px-2.5 min-w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Toggle({\n  className,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive.Root\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Toggle, toggleVariants }\n"
  },
  {
    "path": "web/src/components/ui/tooltip.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/src/utils\"\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  )\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  )\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "web/src/constant/protocols.ts",
    "content": "export const PROTOCOL_OPTIONS = [\n    'http',\n    'socks5',\n    'ss',\n    'ssr',\n    'mieru',\n    'snell',\n    'vmess',\n    'vless',\n    'trojan',\n    'anytls',\n    'hysteria',\n    'hysteria2',\n    'tuic',\n    'wireguard',\n    'ssh'\n]\n"
  },
  {
    "path": "web/src/constant/settings-keys.ts",
    "content": "export const PROXY_ENABLE = \"proxy_enable\"\nexport const PROXY_URL = \"proxy_url\"\n\nexport const LOG_RETENTION_DAYS = \"log_retention_days\"\n\nexport const SUBCONV_URL = \"subconv_url\"\nexport const SUBCONV_URL_PROXY = \"subconv_url_proxy\"\n\nexport const SUB_DISABLE_AUTO = \"sub_disable_auto\"\n\nexport const NODE_POOL_SIZE = \"node_pool_size\"\nexport const NODE_TEST_URL = \"node_test_url\"\nexport const NODE_TEST_TIMEOUT = \"node_test_timeout\"\n\nexport const NODE_PROTOCOL_FILTER_ENABLE = \"node_protocol_filter_enable\"\nexport const NODE_PROTOCOL_FILTER_MODE = \"node_protocol_filter_mode\"\nexport const NODE_PROTOCOL_FILTER = \"node_protocol_filter\"\n\nexport const TASK_MAX_THREAD = \"task_max_thread\"\nexport const TASK_MAX_TIMEOUT = \"task_max_timeout\"\nexport const TASK_MAX_RETRY = \"task_max_retry\"\n\nexport const NOTIFY_OPERATION = \"notify_operation\"\nexport const NOTIFY_ID = \"notify_id\"\n\nexport const BOOLEAN_SETTING_KEYS = new Set<string>([\n  PROXY_ENABLE,\n  SUBCONV_URL_PROXY,\n  NODE_PROTOCOL_FILTER_ENABLE,\n  NODE_PROTOCOL_FILTER_MODE,\n])\n\nexport const NUMBER_SETTING_KEYS = new Set<string>([\n  LOG_RETENTION_DAYS,\n  NODE_POOL_SIZE,\n  NODE_TEST_TIMEOUT,\n  TASK_MAX_THREAD,\n  TASK_MAX_TIMEOUT,\n  TASK_MAX_RETRY,\n  SUB_DISABLE_AUTO,\n  NOTIFY_OPERATION,\n  NOTIFY_ID,\n])\n\nexport const MULTI_SELECT_SETTING_KEYS = new Set<string>([\n  NODE_PROTOCOL_FILTER,\n])\n"
  },
  {
    "path": "web/src/lib/api/client.ts",
    "content": "import { API_PATH } from '../config/config'\nimport { tokenManager } from './token-manager'\nimport type { LoginResponse, UserInfo, ApiResponse, SubResponse, CheckResponse, CheckRequest, SubRequest, DynamicConfigItem, SubNameAndID, NotifyResponse, NotifyRequest, NotifyTemplate, NotifyChannel, NotifyChannelConfigResponse, ShareResponse, ShareRequest, Setting, ChangePasswordRequest, UpdateUserInfoRequest, UpdateResponse, UpdateComponent, SystemVersion } from '@/src/types'\n\nconst DEFAULT_REQUEST_HEADERS: Record<string, string> = {}\n\nexport class ApiError extends Error {\n  constructor(\n    public code: number,\n    override message: string\n  ) {\n    super(message)\n    this.name = 'ApiError'\n  }\n}\n\nclass ApiClient {\n  private async request<T>(\n    endpoint: string,\n    options: RequestInit = {},\n    requiresAuth: boolean = true\n  ): Promise<T> {\n    const url = `${API_PATH.base}${endpoint}`\n\n    const hasBody = options.body !== undefined && options.body !== null\n    const mergedHeaders: Record<string, string> = {\n      ...DEFAULT_REQUEST_HEADERS,\n      ...(options.headers as Record<string, string> | undefined),\n    }\n\n    if (requiresAuth) {\n      const token = await tokenManager.getValidToken()\n      if (token) {\n        mergedHeaders.Authorization = `Bearer ${token}`\n      }\n    }\n\n    if (hasBody) {\n      const existingHeaderKeys = Object.keys(mergedHeaders).map((k) => k.toLowerCase())\n      if (!existingHeaderKeys.includes('content-type')) {\n        mergedHeaders['Content-Type'] = 'application/json'\n      }\n    }\n\n    const config: RequestInit = {\n      ...options,\n      headers: mergedHeaders,\n    }\n\n    try {\n      const response = await fetch(url, config)\n\n      if (!response.ok) {\n        const data = await response.json() as ApiResponse\n        throw new ApiError(data.code, data.message)\n      }\n\n      return await response.json() as T\n    } catch (error) {\n      if (error instanceof ApiError) throw error\n      throw new ApiError(-1, error instanceof Error ? error.message : '与后端通信失败')\n    }\n  }\n\n  async get<T>(endpoint: string, requiresAuth: boolean = true): Promise<T> {\n    return this.request<T>(endpoint, { method: 'GET' }, requiresAuth)\n  }\n\n  async post<T>(endpoint: string, data?: unknown, requiresAuth: boolean = true): Promise<T> {\n    return this.request<T>(\n      endpoint,\n      {\n        method: 'POST',\n        ...(data !== undefined ? { body: JSON.stringify(data) } : {}),\n      },\n      requiresAuth\n    )\n  }\n\n  async delete<T>(endpoint: string, requiresAuth: boolean = true): Promise<T> {\n    return this.request<T>(endpoint, { method: 'DELETE' }, requiresAuth)\n  }\n\n  async put<T>(endpoint: string, data?: unknown, requiresAuth: boolean = true): Promise<T> {\n    return this.request<T>(\n      endpoint,\n      {\n        method: 'PUT',\n        ...(data !== undefined ? { body: JSON.stringify(data) } : {}),\n      },\n      requiresAuth\n    )\n  }\n}\n\nconst apiClient = new ApiClient()\nexport const api = {\n  async login(username: string, password: string): Promise<LoginResponse> {\n    const response = await apiClient.post<ApiResponse<LoginResponse>>(\n      API_PATH.auth.login,\n      { username, password },\n      false\n    )\n    return response.data\n  },\n\n  async logout(): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(API_PATH.auth.logout, {})\n  },\n\n  async getUserInfo(): Promise<UserInfo> {\n    const response = await apiClient.get<ApiResponse<UserInfo>>(API_PATH.auth.user)\n    return response.data\n  },\n\n  async changePassword(data: ChangePasswordRequest): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(API_PATH.auth.password, data)\n  },\n\n  async updateUsername(data: UpdateUserInfoRequest): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(API_PATH.auth.name, data)\n  },\n\n  async getSub(id?: number): Promise<SubResponse[]> {\n    const url = id ? `${API_PATH.sub}?id=${id}` : API_PATH.sub\n    const response = await apiClient.get<ApiResponse<SubResponse[]>>(url)\n    return response.data\n  },\n  async getChecks(id?: number): Promise<CheckResponse[]> {\n    const url = id ? `${API_PATH.check}?id=${id}` : API_PATH.check\n    const response = await apiClient.get<ApiResponse<CheckResponse[]>>(url)\n    return response.data\n  },\n  async getCheckTypes(): Promise<Record<string, DynamicConfigItem[]>> {\n    const response = await apiClient.get<ApiResponse<Record<string, DynamicConfigItem[]>>>(`${API_PATH.check}/type`)\n    return response.data\n  },\n  async refreshSubscription(id: number): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(`${API_PATH.sub}/refresh/${id}`, {})\n  },\n  async runCheck(id: number): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(`${API_PATH.check}/${id}/run`, {})\n  },\n  async createSubscription(data: SubRequest): Promise<SubResponse> {\n    const response = await apiClient.post<ApiResponse<SubResponse>>(API_PATH.sub, data)\n    return response.data\n  },\n  async batchCreateSubscriptions(data: SubRequest[]): Promise<SubResponse[]> {\n    const response = await apiClient.post<ApiResponse<SubResponse[]>>(`${API_PATH.sub}/batch`, data)\n    return response.data\n  },\n  async updateSubscription(id: number, data: SubRequest): Promise<SubResponse> {\n    const response = await apiClient.put<ApiResponse<SubResponse>>(`${API_PATH.sub}/${id}`, data)\n    return response.data\n  },\n  async deleteSubscription(id: number): Promise<void> {\n    await apiClient.delete<ApiResponse<void>>(`${API_PATH.sub}/${id}`)\n  },\n  async createCheck(data: CheckRequest): Promise<CheckResponse> {\n    const response = await apiClient.post<ApiResponse<CheckResponse>>(API_PATH.check, data)\n    return response.data\n  },\n  async updateCheck(id: number, data: CheckRequest): Promise<CheckResponse> {\n    const response = await apiClient.put<ApiResponse<CheckResponse>>(`${API_PATH.check}/${id}`, data)\n    return response.data\n  },\n  async deleteCheck(id: number): Promise<void> {\n    await apiClient.delete<ApiResponse<void>>(`${API_PATH.check}/${id}`)\n  },\n  async getSubNameAndID(): Promise<SubNameAndID[]> {\n    const response = await apiClient.get<ApiResponse<SubNameAndID[]>>(`${API_PATH.sub}/name`)\n    return response.data\n  },\n  async getNotifyChannels(): Promise<NotifyChannel[]> {\n    const response = await apiClient.get<ApiResponse<NotifyChannel[]>>(`${API_PATH.notify}/channel`)\n    return response.data\n  },\n  async getNotifyChannelConfig(channel?: string): Promise<NotifyChannelConfigResponse | DynamicConfigItem[]> {\n    const url = channel\n      ? `${API_PATH.notify}/channel/config?channel=${encodeURIComponent(channel)}`\n      : `${API_PATH.notify}/channel/config`\n    const response = await apiClient.get<ApiResponse<NotifyChannelConfigResponse | DynamicConfigItem[]>>(url)\n    return response.data\n  },\n  async getNotifyList(): Promise<NotifyResponse[]> {\n    const response = await apiClient.get<ApiResponse<NotifyResponse[]>>(API_PATH.notify)\n    return response.data\n  },\n  async createNotify(data: NotifyRequest): Promise<NotifyResponse> {\n    const response = await apiClient.post<ApiResponse<NotifyResponse>>(API_PATH.notify, data)\n    return response.data\n  },\n  async updateNotify(id: number, data: NotifyRequest): Promise<NotifyResponse> {\n    const response = await apiClient.put<ApiResponse<NotifyResponse>>(`${API_PATH.notify}?id=${id}`, data)\n    return response.data\n  },\n  async deleteNotify(id: number): Promise<void> {\n    await apiClient.delete<ApiResponse<void>>(`${API_PATH.notify}?id=${id}`)\n  },\n  async testNotify(data: NotifyRequest): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(`${API_PATH.notify}/test`, data)\n  },\n  async getNotifyTemplates(): Promise<NotifyTemplate[]> {\n    const response = await apiClient.get<ApiResponse<NotifyTemplate[]>>(`${API_PATH.notify}/template`)\n    return response.data\n  },\n  async updateNotifyTemplate(data: NotifyTemplate): Promise<NotifyTemplate> {\n    const response = await apiClient.put<ApiResponse<NotifyTemplate>>(`${API_PATH.notify}/template`, data)\n    return response.data\n  },\n  async getShares(id?: number): Promise<ShareResponse[]> {\n    const url = id ? `${API_PATH.share}?id=${id}` : API_PATH.share\n    const response = await apiClient.get<ApiResponse<ShareResponse[]>>(url)\n    return response.data\n  },\n  async createShare(data: ShareRequest): Promise<ShareResponse> {\n    const response = await apiClient.post<ApiResponse<ShareResponse>>(API_PATH.share, data)\n    return response.data\n  },\n  async updateShare(id: number, data: ShareRequest): Promise<ShareResponse> {\n    const response = await apiClient.put<ApiResponse<ShareResponse>>(`${API_PATH.share}/${id}`, data)\n    return response.data\n  },\n  async deleteShare(id: number): Promise<void> {\n    await apiClient.delete<ApiResponse<void>>(`${API_PATH.share}/${id}`)\n  },\n  async getSettings(): Promise<Setting[]> {\n    const response = await apiClient.get<ApiResponse<Setting[]>>(API_PATH.setting)\n    return response.data\n  },\n  async updateSettings(data: Setting[]): Promise<void> {\n    await apiClient.put<ApiResponse<void>>(API_PATH.setting, data)\n  },\n\n  async getLatestUpdates(): Promise<UpdateResponse> {\n    const response = await apiClient.get<ApiResponse<UpdateResponse>>(API_PATH.update.latest)\n    return response.data\n  },\n\n  async updateComponent(component: UpdateComponent): Promise<void> {\n    await apiClient.post<ApiResponse<void>>(`${API_PATH.update.base}/${component}`, {})\n  },\n\n  async getSystemVersion(): Promise<SystemVersion> {\n    const response = await apiClient.get<ApiResponse<SystemVersion>>(API_PATH.system.version)\n    return response.data\n  },\n}\n"
  },
  {
    "path": "web/src/lib/api/token-manager.ts",
    "content": "import type { LoginResponse } from '../../types'\n\nconst TOKEN_KEYS = {\n    ACCESS_TOKEN: 'access_token',\n    ACCESS_EXPIRES: 'access_expires_at',\n} as const\n\nconst LEGACY_KEYS = ['refresh_token', 'refresh_expires_at'] as const\n\nexport class TokenManager {\n    static isServer() {\n        return typeof window === 'undefined'\n    }\n\n    static getTokens() {\n        if (this.isServer()) return null\n\n        const accessToken = localStorage.getItem(TOKEN_KEYS.ACCESS_TOKEN)\n        if (!accessToken) return null\n\n        return {\n            access_token: accessToken,\n            access_expires_at: localStorage.getItem(TOKEN_KEYS.ACCESS_EXPIRES) || '',\n        }\n    }\n\n    static setTokens(tokens: LoginResponse) {\n        if (this.isServer()) return\n\n        localStorage.setItem(TOKEN_KEYS.ACCESS_TOKEN, tokens.access_token)\n        localStorage.setItem(TOKEN_KEYS.ACCESS_EXPIRES, tokens.access_expires_at)\n    }\n\n    static clearTokens() {\n        if (this.isServer()) return\n        Object.values(TOKEN_KEYS).forEach(key => localStorage.removeItem(key))\n        LEGACY_KEYS.forEach(key => localStorage.removeItem(key))\n    }\n\n    static isExpired(expiresAt: string): boolean {\n        if (!expiresAt) return true\n        return new Date(expiresAt) <= new Date()\n    }\n\n    static async getValidToken(): Promise<string | null> {\n        try {\n            const tokens = this.getTokens()\n            if (!tokens) return null\n            if (!this.isExpired(tokens.access_expires_at)) {\n                return tokens.access_token\n            }\n            this.clearTokens()\n            return null\n        } catch (error) {\n            console.error('Token validation failed:', error)\n            this.clearTokens()\n            return null\n        }\n    }\n}\n\n// 导出实例方法给外部使用\nexport const tokenManager = {\n    getTokens: () => TokenManager.getTokens(),\n    setTokens: (tokens: LoginResponse) => TokenManager.setTokens(tokens),\n    clearTokens: () => TokenManager.clearTokens(),\n    isExpired: (expiresAt: string) => TokenManager.isExpired(expiresAt),\n    getValidToken: () => TokenManager.getValidToken(),\n}\n"
  },
  {
    "path": "web/src/lib/config/config.ts",
    "content": "export const API_PATH = {\n  base: process.env.NEXT_PUBLIC_API_BASEURL?.endsWith('/') ? process.env.NEXT_PUBLIC_API_BASEURL.slice(0, -1) : process.env.NEXT_PUBLIC_API_BASEURL || '',\n  auth: {\n    login: '/api/v1/auth/login',\n    logout: '/api/v1/auth/logout',\n    user: '/api/v1/auth/user',\n    password: '/api/v1/auth/user/password',\n    name: '/api/v1/auth/user/name',\n  },\n  sub: '/api/v1/sub',\n  check: '/api/v1/check',\n  notify: '/api/v1/notify',\n  share: '/api/v1/share',\n  setting: '/api/v1/setting',\n  system: {\n    health: '/api/v1/system/health',\n    info: '/api/v1/system/info',\n    version: '/api/v1/system/version',\n  },\n  update: {\n    base: '/api/v1/update',\n    latest: '/api/v1/update',\n  },\n}\n\nexport const APP_CONFIG = {\n  name: 'BestSub',\n  version: '1.0.0',\n  author: 'BestSub',\n}\n\n\nexport const APP_ROUTES = {\n  LOGIN: {\n    title: \"登录\",\n    path: \"/login\",\n  },\n  DASHBOARD: {\n    title: \"仪表盘\",\n    path: \"/dashboard\",\n  },\n  SUB: {\n    title: \"订阅管理\",\n    path: \"/sub\",\n  },\n  CHECK: {\n    title: \"检测任务\",\n    path: \"/check\",\n  },\n  SHARE: {\n    title: \"分享管理\",\n    path: \"/share\",\n  },\n  STORAGE: {\n    title: \"存储配置\",\n    path: \"/storage\",\n  },\n  NOTIFY: {\n    title: \"通知配置\",\n    path: \"/notify\",\n  },\n  LOG: {\n    title: \"日志查看\",\n    path: \"/log\",\n  },\n  HELP: {\n    title: \"帮助文档\",\n    path: \"/help\",\n  },\n  GITHUB: {\n    title: \"GitHub\",\n    path: \"https://github.com/bestruirui/BestSub\",\n  },\n}\n"
  },
  {
    "path": "web/src/lib/config/version.ts",
    "content": "export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || 'unknown';"
  },
  {
    "path": "web/src/lib/hooks/index.ts",
    "content": "export { useIsMobile } from './use-mobile'\nexport { useForm } from './use-form'\nexport { useFormUpdate } from './use-form-update'\n"
  },
  {
    "path": "web/src/lib/hooks/use-form-update.ts",
    "content": "import { useCallback } from 'react'\n\nexport function useFormUpdate<TFormData extends { config: unknown }>(\n    setFormData: React.Dispatch<React.SetStateAction<TFormData>>\n) {\n    const updateFormField = useCallback((\n        field: string,\n        value: unknown\n    ) => {\n        setFormData(prev => {\n            if (field.includes('.')) {\n                const [parentKey, childKey] = field.split('.') as [string, string]\n                const parent = (prev as Record<string, unknown>)[parentKey] as Record<string, unknown> ?? {}\n                return {\n                    ...prev,\n                    [parentKey]: {\n                        ...parent,\n                        [childKey]: value,\n                    },\n                } as TFormData\n            } else {\n                return { ...prev, [field]: value } as TFormData\n            }\n        })\n    }, [setFormData])\n\n    const updateConfigField = useCallback((\n        field: string,\n        value: unknown\n    ) => {\n        setFormData(prev => ({\n            ...prev,\n            config: { ...(prev.config as Record<string, unknown>), [field]: value },\n        }))\n    }, [setFormData])\n\n    return {\n        updateFormField,\n        updateConfigField,\n    }\n}\n"
  },
  {
    "path": "web/src/lib/hooks/use-form.ts",
    "content": "\nimport { useState, useCallback, useRef } from 'react'\n\nexport interface FormState<T> {\n  data: T\n  errors: Partial<Record<keyof T, string>>\n  touched: Partial<Record<keyof T, boolean>>\n  isSubmitting: boolean\n  isDirty: boolean\n}\n\nexport interface FormActions<T> {\n  setValue: <K extends keyof T>(field: K, value: T[K]) => void\n  setValues: (values: Partial<T>) => void\n  setError: <K extends keyof T>(field: K, error: string) => void\n  clearError: <K extends keyof T>(field: K) => void\n  clearErrors: () => void\n  setTouched: <K extends keyof T>(field: K, touched?: boolean) => void\n  reset: (newData?: T) => void\n  submit: (onSubmit: (data: T) => Promise<void>) => Promise<void>\n}\n\nexport interface FormConfig<T> {\n  initialData: T\n  validate?: (data: T) => Partial<Record<keyof T, string>>\n  onSubmitSuccess?: (data: T) => void\n  onSubmitError?: (error: unknown) => void\n}\nexport function useForm<T extends Record<string, unknown>>(\n  config: FormConfig<T>\n): [FormState<T>, FormActions<T>] {\n  const initialDataRef = useRef(config.initialData)\n\n  const [state, setState] = useState<FormState<T>>({\n    data: config.initialData,\n    errors: {},\n    touched: {},\n    isSubmitting: false,\n    isDirty: false,\n  })\n\n  const updateState = useCallback((updates: Partial<FormState<T>>) => {\n    setState(prev => ({ ...prev, ...updates }))\n  }, [])\n\n  const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {\n    setState(prev => ({\n      ...prev,\n      data: { ...prev.data, [field]: value },\n      touched: { ...prev.touched, [field]: true },\n      isDirty: true,\n      errors: { ...prev.errors, [field]: undefined },\n    }))\n  }, [])\n\n  const setValues = useCallback((values: Partial<T>) => {\n    setState(prev => ({\n      ...prev,\n      data: { ...prev.data, ...values },\n      isDirty: true,\n    }))\n  }, [])\n\n  const setError = useCallback(<K extends keyof T>(field: K, error: string) => {\n    setState(prev => ({\n      ...prev,\n      errors: { ...prev.errors, [field]: error },\n    }))\n  }, [])\n\n  const clearError = useCallback(<K extends keyof T>(field: K) => {\n    setState(prev => ({\n      ...prev,\n      errors: { ...prev.errors, [field]: undefined },\n    }))\n  }, [])\n\n  const clearErrors = useCallback(() => {\n    updateState({ errors: {} })\n  }, [updateState])\n\n  const setTouched = useCallback(<K extends keyof T>(field: K, touched = true) => {\n    setState(prev => ({\n      ...prev,\n      touched: { ...prev.touched, [field]: touched },\n    }))\n  }, [])\n\n  const reset = useCallback((newData?: T) => {\n    const resetData = newData || initialDataRef.current\n    initialDataRef.current = resetData\n    setState({\n      data: resetData,\n      errors: {},\n      touched: {},\n      isSubmitting: false,\n      isDirty: false,\n    })\n  }, [])\n\n  const submit = useCallback(async (onSubmit: (data: T) => Promise<void>) => {\n    if (config.validate) {\n      const errors = config.validate(state.data)\n      if (Object.keys(errors).length > 0) {\n        updateState({ errors })\n        return\n      }\n    }\n\n    updateState({ isSubmitting: true, errors: {} })\n\n    try {\n      await onSubmit(state.data)\n      config.onSubmitSuccess?.(state.data)\n      updateState({ isSubmitting: false, isDirty: false })\n    } catch (error) {\n      config.onSubmitError?.(error)\n      updateState({ isSubmitting: false })\n\n      if (error instanceof Error && error.message.includes(':')) {\n        const [field, message] = error.message.split(':', 2)\n        if (field && message && field in state.data) {\n          setError(field as keyof T, message.trim())\n        }\n      }\n    }\n  }, [config, state.data, updateState, setError])\n\n  const actions: FormActions<T> = {\n    setValue,\n    setValues,\n    setError,\n    clearError,\n    clearErrors,\n    setTouched,\n    reset,\n    submit,\n  }\n\n  return [state, actions]\n}\n\n"
  },
  {
    "path": "web/src/lib/hooks/use-mobile.ts",
    "content": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)\n\n  React.useEffect(() => {\n    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n    const onChange = () => {\n      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n    }\n    mql.addEventListener(\"change\", onChange)\n    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n    return () => mql.removeEventListener(\"change\", onChange)\n  }, [])\n\n  return !!isMobile\n}\n"
  },
  {
    "path": "web/src/lib/hooks/useOverflowDetection.ts",
    "content": "import { useState, useEffect, useRef, RefObject, useCallback } from 'react'\n\ninterface UseOverflowDetectionReturn<T extends HTMLElement> {\n    containerRef: RefObject<HTMLDivElement | null>\n    contentRef: RefObject<T | null>\n    isOverflowing: boolean\n    checkOverflow: () => void\n}\n\nexport function useOverflowDetection<T extends HTMLElement = HTMLElement>(): UseOverflowDetectionReturn<T> {\n    const [isOverflowing, setIsOverflowing] = useState(false)\n    const containerRef = useRef<HTMLDivElement>(null)\n    const contentRef = useRef<T>(null)\n\n    const checkOverflow = useCallback(() => {\n        if (containerRef.current && contentRef.current) {\n            const containerWidth = containerRef.current.clientWidth\n            const contentWidth = contentRef.current.scrollWidth\n            const needsScroll = contentWidth > containerWidth\n            setIsOverflowing(needsScroll)\n        }\n    }, [])\n\n    useEffect(() => {\n        const resizeObserver = new ResizeObserver(checkOverflow)\n\n        if (containerRef.current) {\n            resizeObserver.observe(containerRef.current)\n        }\n        if (contentRef.current) {\n            resizeObserver.observe(contentRef.current)\n        }\n\n        window.addEventListener('resize', checkOverflow)\n\n        return () => {\n            resizeObserver.disconnect()\n            window.removeEventListener('resize', checkOverflow)\n        }\n    }, [checkOverflow])\n\n    return {\n        containerRef,\n        contentRef,\n        isOverflowing,\n        checkOverflow\n    }\n} "
  },
  {
    "path": "web/src/lib/queries/check-queries.ts",
    "content": "import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { api } from '@/src/lib/api/client'\nimport type { CheckResponse, CheckRequest } from '@/src/types'\n\nconst checkKeys = {\n    all: ['checks'] as const,\n    lists: () => [...checkKeys.all, 'list'] as const,\n    types: () => [...checkKeys.all, 'types'] as const,\n    details: () => [...checkKeys.all, 'detail'] as const,\n    detail: (id: number) => [...checkKeys.details(), id] as const,\n}\n\nexport function useChecks(id?: number) {\n    return useQuery({\n        queryKey: id ? checkKeys.detail(id) : checkKeys.lists(),\n        queryFn: () => api.getChecks(id),\n        notifyOnChangeProps: ['data', 'error', 'isLoading'],\n        refetchInterval: 60 * 1000,\n    })\n}\n\nexport function useCheckTypes() {\n    return useQuery({\n        queryKey: checkKeys.types(),\n        queryFn: () => api.getCheckTypes(),\n        refetchInterval: 10 * 60 * 1000,\n        notifyOnChangeProps: ['data', 'error', 'isLoading'],\n    })\n}\n\nexport function useCreateCheck() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (data: CheckRequest) => api.createCheck(data),\n\n        onSuccess: (newCheck) => {\n            queryClient.setQueryData<CheckResponse[]>(\n                checkKeys.lists(),\n                (oldData) => oldData ? [...oldData, newCheck] : [newCheck]\n            )\n\n            queryClient.setQueryData(checkKeys.detail(newCheck.id), newCheck)\n\n            queryClient.invalidateQueries({\n                queryKey: checkKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: checkKeys.lists() })\n        },\n    })\n}\n\nexport function useUpdateCheck() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: ({ id, data }: { id: number; data: CheckRequest }) =>\n            api.updateCheck(id, data),\n\n        onSuccess: (updatedCheck, { id }) => {\n            queryClient.setQueryData<CheckResponse[]>(\n                checkKeys.lists(),\n                (oldData) => oldData?.map(check =>\n                    check.id === id ? updatedCheck : check\n                )\n            )\n\n            queryClient.setQueryData(checkKeys.detail(id), updatedCheck)\n\n            queryClient.invalidateQueries({\n                queryKey: checkKeys.detail(id),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: checkKeys.lists() })\n        },\n    })\n}\n\nexport function useDeleteCheck() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (id: number) => api.deleteCheck(id),\n\n        onSuccess: (_, id) => {\n            queryClient.setQueryData<CheckResponse[]>(\n                checkKeys.lists(),\n                (oldData) => oldData?.filter(check => check.id !== id)\n            )\n\n            queryClient.removeQueries({ queryKey: checkKeys.detail(id) })\n\n            queryClient.invalidateQueries({\n                queryKey: checkKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: checkKeys.lists() })\n        },\n    })\n} "
  },
  {
    "path": "web/src/lib/queries/index.ts",
    "content": "export * from './share-queries'\nexport * from './setting-queries'\nexport * from './check-queries'\nexport * from './sub-queries' "
  },
  {
    "path": "web/src/lib/queries/setting-queries.ts",
    "content": "import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { api } from '@/src/lib/api/client'\nimport type { Setting } from '@/src/types/setting'\n\nconst settingKeys = {\n    all: ['settings'] as const,\n    lists: () => [...settingKeys.all, 'list'] as const,\n}\n\nexport function useSettings() {\n    return useQuery({\n        queryKey: settingKeys.lists(),\n        queryFn: () => api.getSettings(),\n        notifyOnChangeProps: ['data', 'error', 'isLoading'],\n        refetchInterval: 5 * 60 * 1000,\n    })\n}\n\nexport function useUpdateSettings() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (data: Setting[]) => api.updateSettings(data),\n\n        onSuccess: (_, data) => {\n            queryClient.setQueryData<Setting[]>(\n                settingKeys.lists(),\n                (oldData) => {\n                    if (!oldData) {\n                        return data\n                    }\n\n                    const updated = [...oldData]\n\n                    data.forEach((change) => {\n                        const index = updated.findIndex((item) => item.key === change.key)\n\n                        if (index >= 0) {\n                            const target = updated[index]\n                            if (target) {\n                                updated[index] = {\n                                    key: target.key,\n                                    value: change.value,\n                                }\n                            }\n                        } else {\n                            updated.push({ key: change.key, value: change.value })\n                        }\n                    })\n\n                    return updated\n                }\n            )\n\n            queryClient.invalidateQueries({\n                queryKey: settingKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: settingKeys.lists() })\n        },\n    })\n}"
  },
  {
    "path": "web/src/lib/queries/share-queries.ts",
    "content": "import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { api } from '@/src/lib/api/client'\nimport type { ShareResponse, ShareRequest } from '@/src/types'\n\nconst shareKeys = {\n    all: ['shares'] as const,\n    lists: () => [...shareKeys.all, 'list'] as const,\n    details: () => [...shareKeys.all, 'detail'] as const,\n    detail: (id: number) => [...shareKeys.details(), id] as const,\n}\n\nexport function useShares() {\n    return useQuery({\n        queryKey: shareKeys.lists(),\n        queryFn: () => api.getShares(),\n        notifyOnChangeProps: ['data', 'error', 'isLoading'],\n        refetchInterval: 5 * 60 * 1000,\n    })\n}\n\nexport function useCreateShare() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (data: ShareRequest) => api.createShare(data),\n\n        onSuccess: (newShare) => {\n            queryClient.setQueryData<ShareResponse[]>(\n                shareKeys.lists(),\n                (oldData) => oldData ? [...oldData, newShare] : [newShare]\n            )\n\n            queryClient.setQueryData(shareKeys.detail(newShare.id), newShare)\n\n            queryClient.invalidateQueries({\n                queryKey: shareKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: shareKeys.lists() })\n        },\n    })\n}\n\nexport function useUpdateShare() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: ({ id, data }: { id: number; data: ShareRequest }) =>\n            api.updateShare(id, data),\n\n        onSuccess: (updatedShare, { id }) => {\n            queryClient.setQueryData<ShareResponse[]>(\n                shareKeys.lists(),\n                (oldData) => oldData?.map(share =>\n                    share.id === id ? updatedShare : share\n                )\n            )\n\n            queryClient.setQueryData(shareKeys.detail(id), updatedShare)\n\n            queryClient.invalidateQueries({\n                queryKey: shareKeys.detail(id),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: shareKeys.lists() })\n        },\n    })\n}\n\nexport function useDeleteShare() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (id: number) => api.deleteShare(id),\n\n        onSuccess: (_, id) => {\n            queryClient.setQueryData<ShareResponse[]>(\n                shareKeys.lists(),\n                (oldData) => oldData?.filter(share => share.id !== id)\n            )\n\n            queryClient.removeQueries({ queryKey: shareKeys.detail(id) })\n\n            queryClient.invalidateQueries({\n                queryKey: shareKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: shareKeys.lists() })\n        },\n    })\n} "
  },
  {
    "path": "web/src/lib/queries/sub-queries.ts",
    "content": "import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { api } from '@/src/lib/api/client'\nimport type { SubResponse, SubRequest } from '@/src/types'\n\nconst subKeys = {\n    all: ['subs'] as const,\n    lists: () => [...subKeys.all, 'list'] as const,\n    details: () => [...subKeys.all, 'detail'] as const,\n    detail: (id: number) => [...subKeys.details(), id] as const,\n}\n\nexport function useSubs() {\n    return useQuery({\n        queryKey: subKeys.lists(),\n        queryFn: () => api.getSub(),\n        refetchInterval: 60 * 1000,\n        notifyOnChangeProps: ['data', 'error', 'isLoading'],\n    })\n}\n\nexport function useCreateSub() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (data: SubRequest) => api.createSubscription(data),\n\n        onSuccess: (newSub) => {\n            queryClient.setQueryData<SubResponse[]>(\n                subKeys.lists(),\n                (oldData) => oldData ? [...oldData, newSub] : [newSub]\n            )\n\n            queryClient.setQueryData(subKeys.detail(newSub.id), newSub)\n\n            queryClient.invalidateQueries({\n                queryKey: subKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: subKeys.lists() })\n        },\n    })\n}\n\nexport function useUpdateSub() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: ({ id, data }: { id: number; data: SubRequest }) =>\n            api.updateSubscription(id, data),\n\n        onSuccess: (updatedSub, { id }) => {\n            queryClient.setQueryData<SubResponse[]>(\n                subKeys.lists(),\n                (oldData) => oldData?.map(sub =>\n                    sub.id === id ? updatedSub : sub\n                )\n            )\n\n            queryClient.setQueryData(subKeys.detail(id), updatedSub)\n\n            queryClient.invalidateQueries({\n                queryKey: subKeys.detail(id),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: subKeys.lists() })\n        },\n    })\n}\n\nexport function useDeleteSub() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (id: number) => api.deleteSubscription(id),\n\n        onSuccess: (_, id) => {\n            queryClient.setQueryData<SubResponse[]>(\n                subKeys.lists(),\n                (oldData) => oldData?.filter(sub => sub.id !== id)\n            )\n\n            queryClient.removeQueries({ queryKey: subKeys.detail(id) })\n\n            queryClient.invalidateQueries({\n                queryKey: subKeys.lists(),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: subKeys.lists() })\n        },\n    })\n}\n\nexport function useRefreshSub() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (id: number) => api.refreshSubscription(id),\n\n        onSuccess: (_, id) => {\n            queryClient.invalidateQueries({\n                queryKey: subKeys.lists(),\n                refetchType: 'active'\n            })\n\n            queryClient.invalidateQueries({\n                queryKey: subKeys.detail(id),\n                refetchType: 'active'\n            })\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: subKeys.lists() })\n        },\n    })\n}\n\nexport function useBatchCreateSub() {\n    const queryClient = useQueryClient()\n\n    return useMutation({\n        mutationFn: (subscriptions: SubRequest[]) => api.batchCreateSubscriptions(subscriptions),\n\n        onSuccess: (results) => {\n            if (results.length > 0) {\n                queryClient.setQueryData<SubResponse[]>(\n                    subKeys.lists(),\n                    (oldData) => oldData ? [...oldData, ...results] : results\n                )\n\n                results.forEach(sub => {\n                    queryClient.setQueryData(subKeys.detail(sub.id), sub)\n                })\n\n                queryClient.invalidateQueries({\n                    queryKey: subKeys.lists(),\n                    refetchType: 'active'\n                })\n            }\n        },\n\n        onError: () => {\n            queryClient.invalidateQueries({ queryKey: subKeys.lists() })\n        },\n    })\n} "
  },
  {
    "path": "web/src/router/core/context.tsx",
    "content": "\"use client\"\n\nimport { createContext, useContext } from 'react'\n\nexport interface Route {\n    path: string\n    component: React.ComponentType\n    title: string\n    protected?: boolean\n    preloadImport?: () => Promise<unknown>\n    priority?: 'critical' | 'normal' | 'low'\n}\n\nexport interface RouteParams {\n    [key: string]: string | undefined\n}\n\nexport interface QueryParams {\n    [key: string]: string | string[] | undefined\n}\n\nexport interface NavigateOptions {\n    replace?: boolean\n    state?: unknown\n}\n\ninterface RouterContextType {\n    currentPath: string\n    params: RouteParams\n    query: QueryParams\n    navigate: (path: string, query?: QueryParams, options?: NavigateOptions) => void\n    routes: Route[]\n}\n\nexport const RouterContext = createContext<RouterContextType | undefined>(undefined)\n\nexport function useRouter() {\n    const context = useContext(RouterContext)\n    if (!context) {\n        throw new Error('useRouter must be used within a RouterProvider')\n    }\n    return context\n}\n"
  },
  {
    "path": "web/src/router/core/outlet.tsx",
    "content": "\"use client\"\n\nimport { useEffect } from 'react'\nimport { useRouter } from './context'\nimport { useAuth } from '@/src/components/providers'\nimport { Loading } from '@/src/components/ui/loading'\nimport { NotFound } from '@/src/components/pages'\n\ninterface RouterOutletProps {\n  fallback?: React.ComponentType\n}\n\nexport function RouterOutlet({ fallback: Fallback }: RouterOutletProps) {\n  const { currentPath, routes, navigate } = useRouter()\n  const { isAuthenticated, isLoading: authLoading } = useAuth()\n\n  const currentRoute = routes.find(route => route.path === currentPath)\n\n  useEffect(() => {\n    if (!authLoading) {\n      if (currentRoute?.protected && !isAuthenticated && currentPath !== '/login') {\n        navigate('/login', undefined, { replace: true })\n        return\n      }\n\n      if (isAuthenticated && currentPath === '/login') {\n        navigate('/dashboard', undefined, { replace: true })\n        return\n      }\n\n      if (!currentPath || currentPath === '' || currentPath === '/') {\n        if (isAuthenticated) {\n          navigate('/dashboard', undefined, { replace: true })\n        } else {\n          navigate('/login', undefined, { replace: true })\n        }\n        return\n      }\n    }\n  }, [isAuthenticated, authLoading, currentPath, navigate, currentRoute])\n\n  if (routes.length === 0) {\n    return <Loading variant=\"fullscreen\" message=\"初始化应用...\" />\n  }\n\n  if (authLoading) {\n    return <Loading variant=\"fullscreen\" message=\"验证身份中...\" />\n  }\n\n  if (currentRoute) {\n    if (currentRoute.protected && !isAuthenticated) {\n      return <Loading variant=\"fullscreen\" message=\"跳转到登录...\" />\n    }\n\n    const Component = currentRoute.component\n    return <Component />\n  }\n\n  if (!currentPath || currentPath === '' || currentPath === '/') {\n    return <Loading variant=\"fullscreen\" message=\"正在跳转...\" />\n  }\n\n  if (Fallback) {\n    return <Fallback />\n  }\n\n  return <NotFound path={currentPath} />\n}\n"
  },
  {
    "path": "web/src/router/core/router.tsx",
    "content": "\"use client\"\n\nimport { useState, useEffect, useCallback, ReactNode } from 'react'\nimport { RouterContext, QueryParams, RouteParams, NavigateOptions } from './context'\nimport { routes } from '../routes'\n\nfunction parseRoute(hash: string): { path: string; params: RouteParams; query: QueryParams } {\n    if (!hash) return { path: '', params: {}, query: {} }\n\n    const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash\n\n    const [pathWithParams, queryString] = cleanHash.split('?')\n\n    const query: QueryParams = {}\n    if (queryString) {\n        const searchParams = new URLSearchParams(queryString)\n        for (const [key, value] of searchParams) {\n            const existing = query[key]\n            if (existing === undefined) {\n                query[key] = value\n            } else if (Array.isArray(existing)) {\n                existing.push(value)\n            } else {\n                query[key] = [existing, value]\n            }\n        }\n    }\n\n    const path = pathWithParams || ''\n    const params: RouteParams = {}\n\n    return { path, params, query }\n}\n\nfunction buildRoute(path: string, query?: QueryParams): string {\n    if (!query || Object.keys(query).length === 0) {\n        return path\n    }\n\n    const searchParams = new URLSearchParams()\n    Object.entries(query).forEach(([key, value]) => {\n        if (Array.isArray(value)) {\n            value.forEach(v => searchParams.append(key, v))\n        } else if (value !== undefined) {\n            searchParams.set(key, value)\n        }\n    })\n\n    const queryString = searchParams.toString()\n    return queryString ? `${path}?${queryString}` : path\n}\n\nexport function RouterProvider({ children }: { children: ReactNode }) {\n    const [routeState, setRouteState] = useState(() => {\n        if (typeof window !== 'undefined') {\n            return parseRoute(window.location.hash)\n        }\n        return { path: '', params: {}, query: {} }\n    })\n\n    useEffect(() => {\n        const handleHashChange = () => {\n            const newState = parseRoute(window.location.hash)\n            setRouteState(newState)\n        }\n\n        window.addEventListener('hashchange', handleHashChange)\n        return () => window.removeEventListener('hashchange', handleHashChange)\n    }, [])\n\n    const navigate = useCallback((\n        path: string,\n        query?: QueryParams,\n        options: NavigateOptions = {}\n    ) => {\n        const route = buildRoute(path, query)\n\n        if (options.replace) {\n            if (options.state !== undefined) {\n                try { window.history.replaceState(options.state, '') } catch { }\n            }\n            window.location.replace(`#${route}`)\n        } else {\n            if (options.state !== undefined) {\n                try { window.history.pushState(options.state, '') } catch { }\n            }\n            window.location.hash = route\n        }\n    }, [])\n\n    const contextValue = {\n        currentPath: routeState.path,\n        params: routeState.params,\n        query: routeState.query,\n        navigate,\n        routes,\n    }\n\n    return (\n        <RouterContext.Provider value={contextValue}>\n            {children}\n        </RouterContext.Provider>\n    )\n}\n"
  },
  {
    "path": "web/src/router/hooks/use-navigation.tsx",
    "content": "\"use client\"\n\nimport { useState, useCallback } from 'react'\n\nexport function useNavigation() {\n    const [isNavigating, setIsNavigating] = useState(false)\n    const [visitedPaths, setVisitedPaths] = useState<Set<string>>(new Set())\n\n    const markPathAsVisited = useCallback((path: string) => {\n        if (!path) return\n        setVisitedPaths(prev => {\n            const next = new Set(prev)\n            next.add(path)\n            return next\n        })\n    }, [])\n\n    const isFirstVisit = useCallback((path: string) => {\n        return !visitedPaths.has(path)\n    }, [visitedPaths])\n\n    const setNavigating = useCallback((navigating: boolean) => {\n        setIsNavigating(navigating)\n    }, [])\n\n    const goBack = useCallback(() => {\n        setIsNavigating(true)\n        window.history.back()\n        setTimeout(() => setIsNavigating(false), 100)\n    }, [])\n\n    const goForward = useCallback(() => {\n        setIsNavigating(true)\n        window.history.forward()\n        setTimeout(() => setIsNavigating(false), 100)\n    }, [])\n\n    return {\n        isNavigating,\n        visitedPaths,\n        isFirstVisit,\n        markPathAsVisited,\n        setNavigating,\n        goBack,\n        goForward,\n    }\n}\n"
  },
  {
    "path": "web/src/router/hooks/use-route-preloader.tsx",
    "content": "\"use client\"\n\nimport { useEffect, useCallback, useRef } from 'react'\nimport { useRouter } from '../core/context'\n\nconst preloadCache = new Map<string, Promise<unknown>>()\nconst preloadingPaths = new Set<string>()\n\nexport function useRoutePreloader() {\n    const { routes } = useRouter()\n    const preloadedCritical = useRef(false)\n\n    const preloadRoute = useCallback(async (path: string): Promise<unknown> => {\n        try {\n            // 在路由配置中查找对应的路由\n            const route = routes.find(r => r.path === path)\n\n            if (!route?.preloadImport) {\n                return Promise.resolve(null)\n            }\n\n            // 检查是否已经在缓存中\n            if (preloadCache.has(path)) {\n                return preloadCache.get(path)!\n            }\n\n            // 检查是否正在预加载\n            if (preloadingPaths.has(path)) {\n                return Promise.resolve(null)\n            }\n\n            preloadingPaths.add(path)\n\n            // 使用路由配置中的预加载函数\n            const importPromise = route.preloadImport()\n            preloadCache.set(path, importPromise)\n\n            importPromise.finally(() => {\n                preloadingPaths.delete(path)\n            })\n\n            return importPromise\n        } catch (error) {\n            preloadingPaths.delete(path)\n            console.warn(`Failed to preload route: ${path}`, error)\n            return Promise.resolve(null)\n        }\n    }, [routes])\n\n    const preloadCriticalRoutes = useCallback(async () => {\n        if (preloadedCritical.current) return\n        preloadedCritical.current = true\n\n        // 从路由配置中自动获取关键路由\n        const criticalRoutes = routes\n            .filter(route => route.priority === 'critical' && route.preloadImport)\n            .map(route => route.path)\n\n        const schedulePreload = () => {\n            criticalRoutes.forEach(path => preloadRoute(path))\n        }\n\n        // 使用空闲时间预加载\n        if ('requestIdleCallback' in window) {\n            (window as typeof window & {\n                requestIdleCallback: (callback: () => void, options?: { timeout: number }) => void\n            }).requestIdleCallback(schedulePreload, { timeout: 2000 })\n        } else {\n            setTimeout(schedulePreload, 1000)\n        }\n    }, [routes, preloadRoute])\n\n    const preloadByPriority = useCallback(async (priority: 'critical' | 'normal' | 'low') => {\n        const routesToPreload = routes\n            .filter(route => route.priority === priority && route.preloadImport)\n            .map(route => route.path)\n\n        routesToPreload.forEach(path => preloadRoute(path))\n    }, [routes, preloadRoute])\n\n    useEffect(() => {\n        preloadCriticalRoutes()\n    }, [preloadCriticalRoutes])\n\n    return {\n        preloadRoute,\n        preloadByPriority,\n        preloadCriticalRoutes,\n    }\n}\n\nexport function useLinkPreloader() {\n    const { preloadRoute } = useRoutePreloader()\n    const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n    const handleMouseEnter = useCallback((path: string) => {\n        if (timeoutRef.current) {\n            clearTimeout(timeoutRef.current)\n        }\n\n        timeoutRef.current = setTimeout(() => {\n            preloadRoute(path)\n        }, 100)\n    }, [preloadRoute])\n\n    const handleMouseLeave = useCallback(() => {\n        if (timeoutRef.current) {\n            clearTimeout(timeoutRef.current)\n            timeoutRef.current = null\n        }\n    }, [])\n\n    return {\n        handleMouseEnter,\n        handleMouseLeave,\n    }\n}\n\n// 高级预加载策略\nexport function useSmartPreloader() {\n    const { preloadRoute, preloadByPriority } = useRoutePreloader()\n    const { routes, currentPath } = useRouter()\n\n    const preloadAdjacentRoutes = useCallback(() => {\n        const currentIndex = routes.findIndex(route => route.path === currentPath)\n        if (currentIndex === -1) return\n\n        // 预加载相邻的路由\n        const adjacentRoutes = [\n            routes[currentIndex - 1],\n            routes[currentIndex + 1],\n        ].filter(Boolean)\n\n        adjacentRoutes.forEach(route => {\n            if (route?.preloadImport) {\n                preloadRoute(route.path)\n            }\n        })\n    }, [routes, currentPath, preloadRoute])\n\n    const preloadAllNormalPriority = useCallback(() => {\n        preloadByPriority('normal')\n    }, [preloadByPriority])\n\n    return {\n        preloadAdjacentRoutes,\n        preloadAllNormalPriority,\n    }\n}\n"
  },
  {
    "path": "web/src/router/hooks/use-route-title.tsx",
    "content": "\"use client\"\n\nimport { useEffect } from 'react'\nimport { useRouter } from '../core/context'\nimport { APP_CONFIG } from '@/src/lib/config/config'\n\nexport function useRouteTitle() {\n    const { currentPath, routes } = useRouter()\n\n    useEffect(() => {\n        const currentRoute = routes.find(route => route.path === currentPath)\n        if (currentRoute) {\n            document.title = `${currentRoute.title} - ${APP_CONFIG.name}`\n        }\n    }, [currentPath, routes])\n}\n"
  },
  {
    "path": "web/src/router/index.ts",
    "content": "export { RouterProvider } from './core/router'\nexport { RouterOutlet } from './core/outlet'\nexport { useRouter } from './core/context'\nexport { useNavigation } from './hooks/use-navigation'\nexport { useRouteTitle } from './hooks/use-route-title'\nexport { useRoutePreloader, useLinkPreloader, useSmartPreloader } from './hooks/use-route-preloader'\nexport { routes } from './routes'\nexport type { Route, RouteParams, QueryParams, NavigateOptions } from './core/context'\n"
  },
  {
    "path": "web/src/router/routes.tsx",
    "content": "import { Route } from './core/context'\n\nimport { DashboardPage } from '@/src/components/features/home/dashboard'\nimport { SubPage, CheckPage, SharePage, StoragePage, NotifyPage, LoginPage } from '@/src/components/features'\nimport { APP_ROUTES } from '@/src/lib/config/config'\nexport const routes: Route[] = [\n  {\n    path: APP_ROUTES.LOGIN.path,\n    component: LoginPage,\n    title: APP_ROUTES.LOGIN.title,\n    protected: false,\n    preloadImport: () => import('@/src/components/features/login'),\n    priority: 'normal',\n  },\n  {\n    path: APP_ROUTES.DASHBOARD.path,\n    component: DashboardPage,\n    title: APP_ROUTES.DASHBOARD.title,\n    protected: true,\n    preloadImport: () => import('@/src/components/features/home/dashboard'),\n    priority: 'critical',\n  },\n  {\n    path: APP_ROUTES.SUB.path,\n    component: SubPage,\n    title: APP_ROUTES.SUB.title,\n    protected: true,\n    preloadImport: () => import('@/src/components/features/sub'),\n    priority: 'critical',\n  },\n  {\n    path: APP_ROUTES.CHECK.path,\n    component: CheckPage,\n    title: APP_ROUTES.CHECK.title,\n    protected: true,\n    preloadImport: () => import('@/src/components/features/check'),\n    priority: 'normal',\n  },\n  {\n    path: APP_ROUTES.SHARE.path,\n    component: SharePage,\n    title: APP_ROUTES.SHARE.title,\n    protected: true,\n    preloadImport: () => import('@/src/components/features/share'),\n    priority: 'normal',\n  },\n  {\n    path: APP_ROUTES.STORAGE.path,\n    component: StoragePage,\n    title: APP_ROUTES.STORAGE.title,\n    protected: true,\n    preloadImport: () => import('@/src/components/features/storage/storage'),\n    priority: 'low',\n  },\n  {\n    path: APP_ROUTES.NOTIFY.path,\n    component: NotifyPage,\n    title: APP_ROUTES.NOTIFY.title,\n    protected: true,\n    preloadImport: () => import('@/src/components/features/notify'),\n    priority: 'normal',\n  },\n]\n"
  },
  {
    "path": "web/src/types/api.ts",
    "content": "export interface ApiResponse<T = unknown> {\n    code: number\n    message: string\n    data: T\n}\n"
  },
  {
    "path": "web/src/types/auth.ts",
    "content": "/**\n * 认证相关类型定义\n */\n\n/**\n * 登录请求类型\n */\nexport interface LoginRequest {\n    username: string\n    password: string\n}\n\n/**\n * 登录响应类型\n */\nexport interface LoginResponse {\n    access_token: string\n    access_expires_at: string\n}\n\n/**\n * 用户信息类型\n */\nexport interface UserInfo {\n    username: string\n    created_at?: string\n    updated_at?: string\n}\n\n/**\n * 修改密码请求类型\n */\nexport interface ChangePasswordRequest {\n    username: string\n    old_password: string\n    new_password: string\n}\n\n/**\n * 更新用户信息请求类型\n */\nexport interface UpdateUserInfoRequest {\n    username: string\n}\n"
  },
  {
    "path": "web/src/types/check.ts",
    "content": "export interface CheckRequest {\n    name: string\n    enable: boolean\n    task: CheckTask\n    config: Record<string, unknown>\n}\nexport interface CheckResponse {\n    id: number\n    name: string\n    enable: boolean\n    config: Record<string, unknown>\n    result: CheckResult\n    task: CheckTask\n    status: string\n}\n\nexport interface CheckResult {\n    duration: number\n    extra: Record<string, unknown>\n    last_run: string\n    msg: string\n}\n\nexport interface CheckTask {\n    cron_expr: string\n    log_level: string\n    log_write_file: boolean\n    notify: boolean\n    notify_channel: number\n    timeout: number\n    type: string\n    sub_id: number[]\n}"
  },
  {
    "path": "web/src/types/common.ts",
    "content": "/**\n * 通用类型定义\n */\n\n/**\n * 基础实体类型\n */\nexport interface BaseEntity {\n    id: number\n    created_at: string\n    updated_at: string\n}\n\n/**\n * 分页请求参数\n */\nexport interface PaginationParams {\n    page?: number\n    page_size?: number\n    sort_by?: string\n    sort_order?: 'asc' | 'desc'\n}\n\n/**\n * 分页响应类型\n */\nexport interface PaginatedResponse<T> {\n    items: T[]\n    total: number\n    page: number\n    page_size: number\n    total_pages: number\n}\n\n/**\n * 动态配置项类型（用于 check 和 notify）\n */\nexport interface DynamicConfigItem {\n    name: string\n    key: string\n    type: string\n    value: string\n    options: string\n    require: boolean\n    desc: string\n}\n\n/**\n * 动态配置值类型\n */\nexport type ConfigValue = string | number | boolean\n\n/**\n * 动态配置对象类型\n */\nexport interface DynamicConfig {\n    [key: string]: ConfigValue\n}\n\nexport interface KeyValue {\n    key: string\n    value: string\n}"
  },
  {
    "path": "web/src/types/index.ts",
    "content": "/**\n * 类型定义统一导出\n */\n\n// 认证相关类型\nexport * from './auth'\n\n// 订阅相关类型\nexport * from './sub'\n\n// 检测相关类型\nexport * from './check'\n\n// 通知相关类型\nexport * from './notify'\n\n// 分享相关类型\nexport * from './share'\n\n// API相关类型\nexport * from './api'\n\n// 通用类型\nexport * from './common'\n\n// 设置相关类型\nexport * from './setting'\n\n// 更新相关类型\nexport * from './update'"
  },
  {
    "path": "web/src/types/notify.ts",
    "content": "import type { DynamicConfigItem } from './common'\n\n/**\n * 通知相关类型定义\n */\n\n/**\n * 通知配置请求类型\n */\nexport interface NotifyRequest {\n    name: string\n    type: string\n    config: Record<string, unknown>\n}\n\n/**\n * 通知配置响应类型\n */\nexport interface NotifyResponse {\n    id: number\n    name: string\n    type: string\n    config: Record<string, unknown>\n}\n\n/**\n * 通知模板类型\n */\nexport interface NotifyTemplate {\n    id: number\n    type: string\n    title: string\n    content: string\n    created_at: string\n    updated_at: string\n}\n\n/**\n * 通知渠道类型\n */\nexport type NotifyChannel = string\n\n/**\n * 通知渠道配置响应\n */\nexport interface NotifyChannelConfigResponse {\n    [channel: string]: DynamicConfigItem[]\n} "
  },
  {
    "path": "web/src/types/setting.ts",
    "content": "\nexport interface Setting {\n    key: string\n    value: string\n}\n\nexport type FormValue = string | number | boolean | string[]\n\nexport interface FormValues {\n    [key: string]: FormValue\n}\n"
  },
  {
    "path": "web/src/types/share.ts",
    "content": "export interface GenConfig {\n    filter: ShareFilter\n    rename: string\n    proxy: boolean\n    target: string\n}\n\nexport interface ShareRequest {\n    enable: boolean\n    name: string\n    token: string\n    gen: GenConfig\n    max_access_count: number\n    expires: number\n}\n\nexport interface ShareResponse {\n    id: number\n    name: string\n    enable: boolean\n    access_count: number\n    max_access_count: number\n    expires: number\n    token: string\n    gen: GenConfig\n}\n\nexport interface ShareFilter {\n    sub_id: number[]\n    sub_id_exclude: boolean\n    speed_up_more: number\n    speed_down_more: number\n    country: string[]\n    country_exclude: boolean\n    delay_less_than: number\n    alive_status: number\n    risk_less_than: number\n}"
  },
  {
    "path": "web/src/types/sub.ts",
    "content": "export interface SubConfig {\n    url: string\n    proxy?: boolean\n    timeout?: number\n    protocol_filter_enable?: boolean\n    protocol_filter_mode?: boolean\n    protocol_filter?: string[]\n}\n\nexport interface SubRequest {\n    name: string\n    tags: string[]\n    enable: boolean\n    cron_expr: string\n    config: SubConfig\n}\n\n\nexport interface SubResult {\n    success: number\n    fail: number\n    msg: string\n    raw_count: number\n    last_run: string\n    duration: number\n}\n\nexport interface SubNodeInfo {\n    speed_up: number\n    speed_down: number\n    delay: number\n    risk: number\n    count: number\n}\n\nexport interface SubResponse {\n    id: number\n    name: string\n    tags: string[]\n    enable: boolean\n    cron_expr: string\n    config: SubConfig\n    status: string\n    result: SubResult\n    info: SubNodeInfo\n    created_at: string\n    updated_at: string\n}\n\nexport interface SubNameAndID {\n    id: number\n    name: string\n}\n"
  },
  {
    "path": "web/src/types/update.ts",
    "content": "/**\n * 更新相关类型定义\n */\n\nexport interface LatestInfo {\n  /** 版本标签 */\n  tag_name: string\n  /** 发布时间 */\n  published_at: string\n  /** 更新内容 */\n  body: string\n}\n\nexport interface UpdateResponse {\n  bestsub: LatestInfo\n}\n\nexport type UpdateComponent = 'bestsub'\n\nexport interface SystemVersion {\n  /** 版本号 */\n  version: string\n  /** 构建时间 */\n  buildTime: string\n  /** Git 提交哈希 */\n  commit: string\n  /** 作者 */\n  author: string\n  /** 仓库地址 */\n  repo: string\n}\n"
  },
  {
    "path": "web/src/utils/cron.ts",
    "content": "/**\n * Cron表达式处理相关工具函数\n */\n\nimport { formatTime } from './time'\n\n/**\n * 获取下一个Cron运行时间\n */\nexport function getNextCronRunTime(cronExpr: string, enabled: boolean): string | null {\n    if (!enabled || !cronExpr.trim()) {\n        return null\n    }\n\n    try {\n        const parts = cronExpr.trim().split(/\\s+/)\n        if (parts.length < 5) return null\n\n        const minute = parts[0]\n        const hour = parts[1]\n        const day = parts[2]\n        const month = parts[3]\n        const weekday = parts[4]\n\n        if (!minute || !hour || !day || !month || !weekday) return null\n\n        const now = new Date()\n        const next = new Date(now)\n\n        // 简单的cron计算逻辑（处理常见模式）\n        if (hour.startsWith('*/')) {\n            // 每N小时模式: 0 */6 * * * 或 * */6 * * *\n            const hourInterval = parseInt(hour.substring(2))\n            if (isNaN(hourInterval)) return null\n\n            let targetMinute = 0\n            if (minute !== '*') {\n                targetMinute = parseInt(minute)\n                if (isNaN(targetMinute)) return null\n            }\n\n            // 计算下一个时间点\n            const currentHour = now.getHours()\n            const nextHourSlot = Math.ceil(currentHour / hourInterval) * hourInterval\n\n            next.setHours(nextHourSlot, targetMinute, 0, 0)\n\n            // 如果计算出的时间已经过了，添加一个间隔\n            if (next <= now) {\n                next.setHours(next.getHours() + hourInterval)\n            }\n\n        } else if (minute !== '*' && hour !== '*') {\n            // 固定时间模式: 30 14 * * * (每天14:30)\n            const targetMinute = parseInt(minute)\n            const targetHour = parseInt(hour)\n            if (isNaN(targetMinute) || isNaN(targetHour)) return null\n\n            next.setHours(targetHour, targetMinute, 0, 0)\n            if (next <= now) {\n                next.setDate(next.getDate() + 1)\n            }\n\n        } else if (minute.startsWith('*/')) {\n            // 每N分钟模式: */30 * * * *\n            const minuteInterval = parseInt(minute.substring(2))\n            if (isNaN(minuteInterval)) return null\n\n            const nextMinute = Math.ceil(now.getMinutes() / minuteInterval) * minuteInterval\n            next.setMinutes(nextMinute, 0, 0)\n            if (next <= now) {\n                next.setHours(next.getHours() + 1)\n                next.setMinutes(0, 0, 0)\n            }\n\n        } else {\n            // 其他复杂模式，返回估算时间\n            next.setHours(next.getHours() + 1)\n            next.setMinutes(0, 0, 0)\n        }\n\n        return formatTime(next.toISOString())\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    } catch (error) {\n        return null\n    }\n} "
  },
  {
    "path": "web/src/utils/format.ts",
    "content": "/**\n * 格式化相关工具函数\n */\n\nimport { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n/**\n * 合并CSS类名\n */\nexport function cn(...inputs: ClassValue[]) {\n    return twMerge(clsx(inputs))\n}\n\n/**\n * 格式化持续时间\n */\nexport function formatDuration(milliseconds: number): string {\n    if (milliseconds < 1000) {\n        return `${milliseconds}ms`\n    } else if (milliseconds < 60000) {\n        return `${(milliseconds / 1000).toFixed(1)}s`\n    } else {\n        const minutes = Math.floor(milliseconds / 60000)\n        const seconds = Math.floor((milliseconds % 60000) / 1000)\n        return `${minutes}m ${seconds}s`\n    }\n}\n\n\n\n/**\n * 格式化最后运行时间（通用）\n */\nexport function formatLastRunTime(lastRun: string | undefined): string {\n    if (!lastRun) return '从未运行'\n\n    // 检查是否为零时间\n    const zeroTimePatterns = [\n        '0001-01-01T00:00:00Z',\n        '0001-01-01T00:00:00.000Z',\n        '1970-01-01T00:00:00Z',\n        '1970-01-01T00:00:00.000Z'\n    ]\n\n    if (zeroTimePatterns.includes(lastRun)) {\n        return '从未运行'\n    }\n\n    try {\n        return new Date(lastRun).toLocaleString('zh-CN')\n    } catch {\n        return '时间格式错误'\n    }\n}\n\n/**\n * 格式化布尔值显示\n */\nexport function formatBooleanText(value: boolean): string {\n    return value ? '启用' : '禁用'\n}\n"
  },
  {
    "path": "web/src/utils/index.ts",
    "content": "/**\n * 工具函数统一导出\n */\n\n// 验证相关工具函数\nexport * from './validation'\n\n// 格式化相关工具函数\nexport * from './format'\n\n// 时间处理相关工具函数\nexport * from './time'\n\n// URL处理相关工具函数\nexport * from './url'\n\n// Cron表达式处理相关工具函数\nexport * from './cron' "
  },
  {
    "path": "web/src/utils/time.ts",
    "content": "/**\n * 时间处理相关工具函数\n */\n\n/**\n * 检查是否为零时间\n */\nexport function isZeroTime(timeString: string | undefined | null): boolean {\n    if (!timeString) return true\n\n    const zeroTimePatterns = [\n        '0001-01-01T00:00:00Z',\n        '0001-01-01T00:00:00.000Z',\n        '1970-01-01T00:00:00Z',\n        '1970-01-01T00:00:00.000Z'\n    ]\n\n    return zeroTimePatterns.includes(timeString)\n}\n\n/**\n * 格式化时间\n */\nexport function formatTime(\n    timeString: string | undefined | null,\n    options?: Intl.DateTimeFormatOptions\n): string | null {\n    if (!timeString || isZeroTime(timeString)) {\n        return null\n    }\n\n    try {\n        return new Date(timeString).toLocaleString('zh-CN', options)\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    } catch (error) {\n        console.warn('Invalid date string:', timeString)\n        return null\n    }\n}\n\n/**\n * 格式化相对时间\n */\nexport function formatRelativeTime(timeString: string | undefined | null): string | null {\n    if (!timeString || isZeroTime(timeString)) {\n        return null\n    }\n\n    try {\n        const now = new Date()\n        const time = new Date(timeString)\n        const diffInSeconds = Math.floor((now.getTime() - time.getTime()) / 1000)\n\n        if (diffInSeconds < 60) return '刚刚'\n        if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}分钟前`\n        if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}小时前`\n        if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)}天前`\n\n        return formatTime(timeString)\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    } catch (error) {\n        console.warn('Invalid date string:', timeString)\n        return null\n    }\n} "
  },
  {
    "path": "web/src/utils/url.ts",
    "content": "/**\n * URL处理相关工具函数\n */\n\n/**\n * 获取API基础URL\n */\nexport function getApiBaseUrl(): string {\n    const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASEURL\n\n    if (!apiBaseUrl || apiBaseUrl.trim() === '') {\n        if (typeof window !== 'undefined') {\n            return window.location.origin\n        }\n        return ''\n    }\n\n    return apiBaseUrl.replace(/\\/$/, '')\n}\n\n/**\n * 构建完整的API URL\n */\nexport function buildApiUrl(endpoint: string): string {\n    const baseUrl = getApiBaseUrl()\n    const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`\n    return `${baseUrl}${normalizedEndpoint}`\n} "
  },
  {
    "path": "web/src/utils/validation.ts",
    "content": "/**\n * 验证相关工具函数\n */\n\n/**\n * 验证超时时间\n */\nexport function validateTimeout(value: string | number): number {\n    const num = typeof value === 'string' ? parseInt(value, 10) : value\n    if (isNaN(num) || num < 1) return 10\n    if (num > 300) return 300\n    return num\n}\n\n/**\n * 验证URL格式\n */\nexport function validateUrl(url: string): boolean {\n    if (!url.trim()) return false\n    try {\n        new URL(url)\n        return url.startsWith('http://') || url.startsWith('https://')\n    } catch {\n        return false\n    }\n}\n\n/**\n * 验证Cron表达式格式\n */\nexport function validateCronExpr(cron: string): boolean {\n    if (!cron.trim()) return false\n    const parts = cron.trim().split(/\\s+/)\n    return parts.length === 5 || parts.length === 6\n}\n\n"
  },
  {
    "path": "web/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitOverride\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \"next-env.d.ts\",\n    \"out/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]