[
  {
    "path": ".github/workflows/main.yml",
    "content": "name: build\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n    paths:\n      - \"backend/package.json\"\n  pull_request:\n    branches:\n      - master\n    paths:\n      - \"backend/package.json\"\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: \"master\"\n      - name: Set up Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: \"20\"\n      - name: Install dependencies\n        run: |\n          npm install -g pnpm\n          cd backend && pnpm i --no-frozen-lockfile\n      # - name: Test\n      #   run: |\n      #     cd backend\n      #     pnpm test\n      # - name: Build\n      #   run: |\n      #     cd backend\n      #     pnpm run build\n      - name: Bundle\n        run: |\n          cd backend\n          pnpm bundle:esbuild\n      - id: tag\n        name: Generate release tag\n        run: |\n          cd backend\n          SUBSTORE_RELEASE=`node --eval=\"process.stdout.write(require('./package.json').version)\"`\n          echo \"release_tag=$SUBSTORE_RELEASE\" >> $GITHUB_OUTPUT\n      - name: Prepare release\n        run: |\n          cd backend\n          pnpm i -D conventional-changelog-cli\n          pnpm run changelog\n      - name: Release\n        uses: softprops/action-gh-release@v1\n        if: ${{ success() }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          body_path: ./backend/CHANGELOG.md\n          tag_name: ${{ steps.tag.outputs.release_tag }}\n          # generate_release_notes: true\n          files: |\n            ./backend/sub-store.min.js\n            ./backend/dist/sub-store-0.min.js\n            ./backend/dist/sub-store-1.min.js\n            ./backend/dist/sub-store-parser.loon.min.js\n            ./backend/dist/cron-sync-artifacts.min.js\n            ./backend/dist/sub-store.bundle.js\n      - name: Git push assets to \"release\" branch\n        run: |\n          cd backend/dist || exit 1\n          git init\n          git config --local user.name \"github-actions[bot]\"\n          git config --local user.email \"github-actions[bot]@users.noreply.github.com\"\n          git checkout -b release\n          git add .\n          git commit -m \"release: ${{ steps.tag.outputs.release_tag }}\"\n          git remote add origin \"https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}\"\n          git push -f -u origin release\n      # - name: Sync to GitLab\n      #   env:\n      #     GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}\n      #   run: |\n      #     curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n# json config\nsub-store.json\nsub-store_*.json\nroot.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\n# dist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# Dist files\nbackend/dist/*\n!backend/dist/.gitkeep\nbackend/sub-store.min.js\n\nCHANGELOG.md\n\n.codeartsdoer\n.github/copilot-instructions.md"
  },
  {
    "path": "LICENSE",
    "content": "                   GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n               Copyright (c) 2015 Ayuntamiento de Madrid\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://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 Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\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,\nour General Public Licenses are 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.\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  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\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 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 work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\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 AGPL, see\n<http://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<br>\n<img width=\"200\" src=\"https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png\" alt=\"Sub-Store\">\n<br>\n<br>\n<h2 align=\"center\">Sub-Store<h2>\n</div>\n\n<p align=\"center\" color=\"#6a737d\">\nAdvanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.\n</p>\n\n[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)\n<a href=\"https://trendshift.io/repositories/4572\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/4572\" alt=\"sub-store-org%2FSub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)\n\n[📚 文档/DOC](https://github.com/sub-store-org/Sub-Store/wiki)\n\nCore functionalities:\n\n1. Conversion among various formats.\n2. Subscription formatting.\n3. Collect multiple subscriptions in one URL.\n\n> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.\n\n## 1. Subscription Conversion\n\n### Supported Input Formats\n\n[本地节点怎么写/How To Write A Local Node](https://t.me/zhetengsha/824)\n\n> ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs. However, we have already supported some very common non-standard URIs (such as VMess, VLESS).\n\n- [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))\n\n  example: `socks5+tls://user:pass@ip:port#name`\n\n- [x] URI(AnyTLS, SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)\n  > Please note, HTTP(s) does not have a standard URI format, so it is not supported. Please use other formats.\n- [x] Clash Proxies YAML\n- [x] Clash Proxy JSON/JSON5/YAML(single line)\n  > [NaiveProxy](https://t.me/zhetengsha/4308)\n- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)\n- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2, AnyTLS)\n- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, AnyTLS, TrustTunnel, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))\n- [x] mihomo(Clash.Meta) Compatible (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, sudoku, AnyTLS, MASQUE)\n\nDeprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):\n\n- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)\n\n### Supported Target Platforms\n\n- [x] Plain JSON\n- [x] Stash\n- [x] Clash.Meta(mihomo)\n- [x] Surfboard\n- [x] Surge\n- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)\n- [x] Loon\n- [x] Egern\n- [x] Shadowrocket\n- [x] QX\n- [x] sing-box\n- [x] V2Ray\n- [x] V2Ray URI\n\nDeprecated:\n\n- [x] Clash\n\n## 2. Subscription Formatting\n\n### Filtering\n\n- [x] **Regex filter**\n- [x] **Discard regex filter**\n- [x] **Region filter**\n- [x] **Type filter**\n- [x] **Useless proxies filter**\n- [x] **Script filter**\n\n### Proxy Operations\n\n- [x] **Set property operator**: set some proxy properties such as `udp`,`tfo`, `skip-cert-verify` etc.\n- [x] **Flag operator**: add flags or remove flags for proxies.\n- [x] **Sort operator**: sort proxies by name.\n- [x] **Regex sort operator**: sort proxies by keywords (fallback to normal sort).\n- [x] **Regex rename operator**: replace by regex in proxy names.\n- [x] **Regex delete operator**: delete by regex in proxy names.\n- [x] **Script operator**: modify proxy by script.\n- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.\n\n### Development\n\nInstall `pnpm`\n\nGo to `backend` directories, install node dependencies:\n\n```\npnpm i\n```\n\n```\nSUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel \"/^dev:.*/\"\n```\n\n### Build\n\n```\npnpm bundle:esbuild\n```\n\n## LICENSE\n\nThis project is under the GPL V3 LICENSE.\n\n[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)\n\n## Acknowledgements\n\n- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!\n- Special thanks to @Orz-3 and @58xinian for their awesome icons.\n\n## Sponsors\n\n[![image](./support.nodeseek.com_page_promotion_id=8.png)](https://yxvm.com)\n\n[NodeSupport](https://github.com/NodeSeekDev/NodeSupport) sponsored this project.\n"
  },
  {
    "path": "backend/.babelrc",
    "content": "{\n    \"presets\": [\n        [\n            \"@babel/preset-env\"\n        ]\n    ],\n    \"env\": {\n        \"test\": {\n            \"presets\": [\n                \"@babel/preset-env\"\n            ]\n        }\n    },\n    \"plugins\": [\n        [\n            \"babel-plugin-relative-path-import\",\n            {\n                \"paths\": [\n                    {\n                        \"rootPathPrefix\": \"@\",\n                        \"rootPathSuffix\": \"src\"\n                    }\n                ]\n            }\n        ]\n    ]\n}"
  },
  {
    "path": "backend/.eslintrc.json",
    "content": "{\n    \"ignorePatterns\": [\"*.min.js\", \"src/vendor/*.js\"],\n    \"env\": {\n        \"browser\": true,\n        \"es2021\": true,\n        \"node\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaVersion\": \"latest\",\n        \"sourceType\": \"module\"\n    },\n    \"rules\": {\n    }\n}\n"
  },
  {
    "path": "backend/.prettierrc.json",
    "content": "{\n    \"singleQuote\": true,\n    \"trailingComma\": \"all\",\n    \"tabWidth\": 4,\n    \"bracketSpacing\": true\n}\n"
  },
  {
    "path": "backend/banner",
    "content": "/**\n *  ███████╗██╗   ██╗██████╗       ███████╗████████╗ ██████╗ ██████╗ ███████╗\n *  ██╔════╝██║   ██║██╔══██╗      ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝\n *  ███████╗██║   ██║██████╔╝█████╗███████╗   ██║   ██║   ██║██████╔╝█████╗\n *  ╚════██║██║   ██║██╔══██╗╚════╝╚════██║   ██║   ██║   ██║██╔══██╗██╔══╝\n *  ███████║╚██████╔╝██████╔╝      ███████║   ██║   ╚██████╔╝██║  ██║███████╗\n *  ╚══════╝ ╚═════╝ ╚═════╝       ╚══════╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚══════╝\n * Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket!\n * @updated: <%= updated %>\n * @version: <%= pkg.version %>\n * @author: Peng-YM\n * @github: https://github.com/sub-store-org/Sub-Store\n * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46\n */\n\n"
  },
  {
    "path": "backend/bundle-esbuild.js",
    "content": "#!/usr/bin/env node\nconst fs = require('fs');\nconst path = require('path');\nconst { build } = require('esbuild');\n\n!(async () => {\n    const version = JSON.parse(\n        fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),\n    ).version.trim();\n\n    const artifacts = [\n        { src: 'src/main.js', dest: 'sub-store.min.js' },\n        {\n            src: 'src/products/resource-parser.loon.js',\n            dest: 'dist/sub-store-parser.loon.min.js',\n        },\n        {\n            src: 'src/products/cron-sync-artifacts.js',\n            dest: 'dist/cron-sync-artifacts.min.js',\n        },\n        { src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' },\n        { src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' },\n    ];\n\n    for await (const artifact of artifacts) {\n        await build({\n            entryPoints: [artifact.src],\n            bundle: true,\n            minify: true,\n            sourcemap: false,\n            platform: 'browser',\n            format: 'iife',\n            outfile: artifact.dest,\n        });\n    }\n\n    let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {\n        encoding: 'utf8',\n    });\n    content = content.replace(\n        /eval\\(('|\")(require\\(('|\").*?('|\")\\))('|\")\\)/g,\n        '$2',\n    );\n    fs.writeFileSync(\n        path.join(__dirname, 'dist/sub-store.no-bundle.js'),\n        content,\n        {\n            encoding: 'utf8',\n        },\n    );\n\n    await build({\n        entryPoints: ['dist/sub-store.no-bundle.js'],\n        bundle: true,\n        minify: true,\n        sourcemap: false,\n        platform: 'node',\n        format: 'cjs',\n        outfile: 'dist/sub-store.bundle.js',\n    });\n    fs.writeFileSync(\n        path.join(__dirname, 'dist/sub-store.bundle.js'),\n        `// SUB_STORE_BACKEND_VERSION: ${version}\n${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {\n    encoding: 'utf8',\n})}`,\n        {\n            encoding: 'utf8',\n        },\n    );\n})()\n    .catch((e) => {\n        console.log(e);\n    })\n    .finally(() => {\n        console.log('done');\n    });\n"
  },
  {
    "path": "backend/bundle.js",
    "content": "#!/usr/bin/env node\nconst fs = require('fs');\nconst path = require('path');\nconst { build } = require('esbuild');\n\n!(async () => {\n    const version = JSON.parse(\n        fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),\n    ).version.trim();\n\n    let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {\n        encoding: 'utf8',\n    });\n    content = content.replace(\n        /eval\\(('|\")(require\\(('|\").*?('|\")\\))('|\")\\)/g,\n        '$2',\n    );\n    fs.writeFileSync(\n        path.join(__dirname, 'dist/sub-store.no-bundle.js'),\n        content,\n        {\n            encoding: 'utf8',\n        },\n    );\n\n    await build({\n        entryPoints: ['dist/sub-store.no-bundle.js'],\n        bundle: true,\n        minify: true,\n        sourcemap: true,\n        platform: 'node',\n        format: 'cjs',\n        outfile: 'dist/sub-store.bundle.js',\n    });\n    fs.writeFileSync(\n        path.join(__dirname, 'dist/sub-store.bundle.js'),\n        `// SUB_STORE_BACKEND_VERSION: ${version}\n${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {\n    encoding: 'utf8',\n})}`,\n        {\n            encoding: 'utf8',\n        },\n    );\n})()\n    .catch((e) => {\n        console.log(e);\n    })\n    .finally(() => {\n        console.log('done');\n    });\n"
  },
  {
    "path": "backend/dev-esbuild.js",
    "content": "#!/usr/bin/env node\nconst { build } = require('esbuild');\n\n!(async () => {\n    const artifacts = [{ src: 'src/main.js', dest: 'sub-store.min.js' }];\n\n    for await (const artifact of artifacts) {\n        await build({\n            entryPoints: [artifact.src],\n            bundle: true,\n            minify: false,\n            sourcemap: false,\n            platform: 'node',\n            format: 'cjs',\n            outfile: artifact.dest,\n        });\n    }\n})()\n    .catch((e) => {\n        console.log(e);\n    })\n    .finally(() => {\n        console.log('done');\n    });\n"
  },
  {
    "path": "backend/dist/.gitkeep",
    "content": ""
  },
  {
    "path": "backend/gulpfile.babel.js",
    "content": "import fs from 'fs';\nimport browserify from 'browserify';\nimport gulp from 'gulp';\nimport prettier from 'gulp-prettier';\nimport header from 'gulp-header';\nimport eslint from 'gulp-eslint-new';\nimport newFile from 'gulp-file';\nimport path from 'path';\nimport tap from 'gulp-tap';\n\nimport pkg from './package.json';\n\nexport function peggy() {\n    return gulp.src('src/**/*.peg').pipe(\n        tap(function (file) {\n            const filename = path.basename(file.path).split('.')[0] + '.js';\n            const raw = fs.readFileSync(file.path, 'utf8');\n            const contents = `import * as peggy from 'peggy';\nconst grammars = String.raw\\`\\n${raw}\\n\\`;\nlet parser;\nexport default function getParser() {\n    if (!parser) {\n        parser = peggy.generate(grammars);\n    }\n    return parser;\n}\\n`;\n            return newFile(filename, contents).pipe(\n                gulp.dest(path.dirname(file.path)),\n            );\n        }),\n    );\n}\n\nexport function lint() {\n    return gulp\n        .src('src/**/*.js')\n        .pipe(eslint({ fix: true }))\n        .pipe(eslint.fix())\n        .pipe(eslint.format())\n        .pipe(eslint.failAfterError());\n}\n\nexport function styles() {\n    return gulp\n        .src('src/**/*.js')\n        .pipe(\n            prettier({\n                singleQuote: true,\n                trailingComma: 'all',\n                tabWidth: 4,\n                bracketSpacing: true,\n            }),\n        )\n        .pipe(gulp.dest((file) => file.base));\n}\n\nfunction scripts(src, dest) {\n    return () => {\n        return browserify(src)\n            .transform('babelify', {\n                presets: [['@babel/preset-env']],\n                plugins: [\n                    [\n                        'babel-plugin-relative-path-import',\n                        {\n                            paths: [\n                                {\n                                    rootPathPrefix: '@',\n                                    rootPathSuffix: 'src',\n                                },\n                            ],\n                        },\n                    ],\n                ],\n            })\n            .plugin('tinyify')\n            .bundle()\n            .pipe(fs.createWriteStream(dest));\n    };\n}\n\nfunction banner(dest) {\n    return () =>\n        gulp\n            .src(dest)\n            .pipe(\n                header(fs.readFileSync('./banner', 'utf-8'), {\n                    pkg,\n                    updated: new Date().toLocaleString('zh-CN'),\n                }),\n            )\n            .pipe(gulp.dest((file) => file.base));\n}\n\nconst artifacts = [\n    { src: 'src/main.js', dest: 'sub-store.min.js' },\n    {\n        src: 'src/products/resource-parser.loon.js',\n        dest: 'dist/sub-store-parser.loon.min.js',\n    },\n    {\n        src: 'src/products/cron-sync-artifacts.js',\n        dest: 'dist/cron-sync-artifacts.min.js',\n    },\n    { src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' },\n    { src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' },\n];\n\nexport const build = gulp.series(\n    gulp.parallel(\n        artifacts.map((artifact) => scripts(artifact.src, artifact.dest)),\n    ),\n    gulp.parallel(artifacts.map((artifact) => banner(artifact.dest))),\n);\n\nconst all = gulp.series(peggy, lint, styles, build);\n\nexport default all;\n"
  },
  {
    "path": "backend/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    }\n  }\n}"
  },
  {
    "path": "backend/package.json",
    "content": "{\n  \"name\": \"sub-store\",\n  \"version\": \"2.21.51\",\n  \"description\": \"Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.\",\n  \"main\": \"src/main.js\",\n  \"scripts\": {\n    \"preinstall\": \"npx only-allow pnpm\",\n    \"test\": \"gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive\",\n    \"serve\": \"node sub-store.min.js\",\n    \"start\": \"nodemon -w src -w package.json --exec babel-node src/main.js\",\n    \"dev:esbuild\": \"nodemon -w src -w package.json dev-esbuild.js\",\n    \"dev:run\": \"nodemon -w sub-store.min.js sub-store.min.js\",\n    \"build\": \"gulp\",\n    \"bundle\": \"node bundle.js\",\n    \"bundle:esbuild\": \"node bundle-esbuild.js\",\n    \"changelog\": \"conventional-changelog -p cli -i CHANGELOG.md -s\"\n  },\n  \"author\": \"Peng-YM\",\n  \"license\": \"GPL-3.0\",\n  \"pnpm\": {\n    \"patchedDependencies\": {\n      \"http-proxy@1.18.1\": \"patches/http-proxy@1.18.1.patch\"\n    }\n  },\n  \"dependencies\": {\n    \"@maxmind/geoip2-node\": \"^5.0.0\",\n    \"automerge\": \"1.0.1-preview.7\",\n    \"body-parser\": \"^1.19.0\",\n    \"buffer\": \"^6.0.3\",\n    \"connect-history-api-fallback\": \"^2.0.0\",\n    \"cron\": \"^3.1.6\",\n    \"dns-packet\": \"^5.6.1\",\n    \"dotenv\": \"^16.4.7\",\n    \"express\": \"^4.17.1\",\n    \"fastestsmallesttextencoderdecoder\": \"^1.0.22\",\n    \"fetch-socks\": \"^1.3.2\",\n    \"http-proxy-middleware\": \"^3.0.3\",\n    \"ip-address\": \"^9.0.5\",\n    \"js-base64\": \"^3.7.2\",\n    \"json5\": \"^2.2.3\",\n    \"jsrsasign\": \"^11.1.0\",\n    \"lodash\": \"^4.17.21\",\n    \"mime-types\": \"^2.1.35\",\n    \"ms\": \"^2.1.3\",\n    \"nanoid\": \"^3.3.3\",\n    \"semver\": \"^7.6.3\",\n    \"static-js-yaml\": \"^1.0.0\",\n    \"undici\": \"^7.4.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.18.0\",\n    \"@babel/node\": \"^7.17.10\",\n    \"@babel/preset-env\": \"^7.18.0\",\n    \"@babel/register\": \"^7.17.7\",\n    \"@types/gulp\": \"^4.0.9\",\n    \"babel-plugin-relative-path-import\": \"^2.0.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browser-pack-flat\": \"^3.4.2\",\n    \"browserify\": \"^17.0.0\",\n    \"chai\": \"^4.3.6\",\n    \"esbuild\": \"^0.19.8\",\n    \"eslint\": \"^8.16.0\",\n    \"gulp\": \"^4.0.2\",\n    \"gulp-babel\": \"^8.0.0\",\n    \"gulp-eslint-new\": \"^1.4.4\",\n    \"gulp-file\": \"^0.4.0\",\n    \"gulp-header\": \"^2.0.9\",\n    \"gulp-prettier\": \"^4.0.0\",\n    \"gulp-tap\": \"^2.0.0\",\n    \"mocha\": \"^10.0.0\",\n    \"nodemon\": \"^2.0.16\",\n    \"peggy\": \"^2.0.1\",\n    \"prettier\": \"2.6.2\",\n    \"prettier-plugin-sort-imports\": \"^1.6.1\",\n    \"tinyify\": \"^3.0.0\"\n  }\n}"
  },
  {
    "path": "backend/patches/http-proxy@1.18.1.patch",
    "content": "diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js\nindex 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644\n--- a/lib/http-proxy/common.js\n+++ b/lib/http-proxy/common.js\n@@ -1,6 +1,5 @@\n var common   = exports,\n     url      = require('url'),\n-    extend   = require('util')._extend,\n     required = require('requires-port');\n \n var upgradeHeader = /(^|,)\\s*upgrade\\s*($|,)/i,\n@@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) {\n   );\n \n   outgoing.method = options.method || req.method;\n-  outgoing.headers = extend({}, req.headers);\n+  outgoing.headers = Object.assign({}, req.headers);\n \n   if (options.headers){\n-    extend(outgoing.headers, options.headers);\n+    Object.assign(outgoing.headers, options.headers);\n   }\n \n   if (options.auth) {\ndiff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js\nindex 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644\n--- a/lib/http-proxy/index.js\n+++ b/lib/http-proxy/index.js\n@@ -1,5 +1,4 @@\n var httpProxy = module.exports,\n-    extend    = require('util')._extend,\n     parse_url = require('url').parse,\n     EE3       = require('eventemitter3'),\n     http      = require('http'),\n@@ -47,9 +46,9 @@ function createRightProxy(type) {\n         args[cntr] !== res\n       ) {\n         //Copy global options\n-        requestOptions = extend({}, options);\n+        requestOptions = Object.assign({}, options);\n         //Overwrite with request options\n-        extend(requestOptions, args[cntr]);\n+        Object.assign(requestOptions, args[cntr]);\n \n         cntr--;\n       }\n"
  },
  {
    "path": "backend/src/constants.js",
    "content": "export const SCHEMA_VERSION_KEY = 'schemaVersion';\nexport const SETTINGS_KEY = 'settings';\nexport const SUBS_KEY = 'subs';\nexport const COLLECTIONS_KEY = 'collections';\nexport const FILES_KEY = 'files';\nexport const MODULES_KEY = 'modules';\nexport const ARTIFACTS_KEY = 'artifacts';\nexport const RULES_KEY = 'rules';\nexport const TOKENS_KEY = 'tokens';\nexport const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';\nexport const GIST_BACKUP_FILE_NAME = 'Sub-Store';\nexport const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';\nexport const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';\nexport const HEADERS_RESOURCE_CACHE_KEY = '#sub-store-cached-headers-resource';\nexport const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource';\nexport const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour\nexport const DEFAULT_HEADERS_CACHE_TTL = 60 * 1000; // 1 min\nexport const DEFAULT_SCRIPT_CACHE_TTL = 48 * 3600 * 1000; // 48 hours\n"
  },
  {
    "path": "backend/src/core/app.js",
    "content": "import 'fastestsmallesttextencoderdecoder';\nimport { OpenAPI } from '@/vendor/open-api';\n\nconst $ = new OpenAPI('sub-store');\nexport default $;\n"
  },
  {
    "path": "backend/src/core/proxy-utils/index.js",
    "content": "import { Base64 } from 'js-base64';\nimport { Buffer } from 'buffer';\nimport rs from '@/utils/rs';\nimport YAML from '@/utils/yaml';\nimport download, { downloadFile } from '@/utils/download';\nimport {\n    isIPv4,\n    isIPv6,\n    isValidPortNumber,\n    isValidUUID,\n    isNotBlank,\n    ipAddress,\n    getRandomPort,\n    numberToString,\n} from '@/utils';\nimport PROXY_PROCESSORS, { ApplyProcessor } from './processors';\nimport PROXY_PREPROCESSORS from './preprocessors';\nimport PROXY_PRODUCERS from './producers';\nimport PROXY_PARSERS from './parsers';\nimport $ from '@/core/app';\nimport { FILES_KEY, MODULES_KEY } from '@/constants';\nimport { findByName } from '@/utils/database';\nimport { produceArtifact } from '@/restful/sync';\nimport { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';\nimport Gist from '@/utils/gist';\nimport { isPresent } from './producers/utils';\nimport { doh } from '@/utils/dns';\nimport JSON5 from 'json5';\n\nfunction preprocess(raw) {\n    for (const processor of PROXY_PREPROCESSORS) {\n        try {\n            if (processor.test(raw)) {\n                $.info(`Pre-processor [${processor.name}] activated`);\n                return processor.parse(raw);\n            }\n        } catch (e) {\n            $.error(`Parser [${processor.name}] failed\\n Reason: ${e}`);\n        }\n    }\n    return raw;\n}\n\nfunction parse(raw) {\n    raw = preprocess(raw);\n    // parse\n    const lines = raw.split('\\n');\n    const proxies = [];\n    let lastParser;\n\n    for (let line of lines) {\n        line = line.trim();\n        if (line.length === 0) continue; // skip empty line\n        let success = false;\n\n        // try to parse with last used parser\n        if (lastParser) {\n            const [proxy, error] = tryParse(lastParser, line);\n            if (!error) {\n                proxies.push(lastParse(proxy));\n                success = true;\n            }\n        }\n\n        if (!success) {\n            // search for a new parser\n            for (const parser of PROXY_PARSERS) {\n                const [proxy, error] = tryParse(parser, line);\n                if (!error) {\n                    proxies.push(lastParse(proxy));\n                    lastParser = parser;\n                    success = true;\n                    $.info(`${parser.name} is activated`);\n                    break;\n                }\n            }\n        }\n\n        if (!success) {\n            $.error(`Failed to parse line: ${line}`);\n        }\n    }\n    return proxies.filter((proxy) => {\n        if (['vless', 'vmess'].includes(proxy.type)) {\n            const isProxyUUIDValid = isValidUUID(proxy.uuid);\n            if (!isProxyUUIDValid) {\n                $.info(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);\n            }\n            // return isProxyUUIDValid;\n        }\n        return true;\n    });\n}\n\nasync function processFn(\n    proxies,\n    operators = [],\n    targetPlatform,\n    source,\n    $options,\n) {\n    let context = {};\n    for (const item of operators) {\n        if (item.disabled) {\n            $.log(\n                `Skipping disabled operator: \"${\n                    item.type\n                }\" with arguments:\\n >>> ${\n                    JSON.stringify(item.args, null, 2) || 'None'\n                }`,\n            );\n            continue;\n        }\n        // process script\n        let script;\n        let $arguments = {};\n        if (item.type.indexOf('Script') !== -1) {\n            const { mode, content } = item.args;\n            if (mode === 'link') {\n                let url = content || '';\n                // extract link arguments\n                const rawArgs = url.split('#');\n                if (rawArgs.length > 1) {\n                    try {\n                        // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n                        $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));\n                    } catch (e) {\n                        for (const pair of rawArgs[1].split('&')) {\n                            const key = pair.split('=')[0];\n                            const value = pair.split('=')[1];\n                            // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                            $arguments[key] =\n                                value == null || value === ''\n                                    ? true\n                                    : decodeURIComponent(value);\n                        }\n                    }\n                }\n                console.log(rawArgs);\n                url = `${url.split('#')[0]}${\n                    rawArgs[2]\n                        ? `#${rawArgs[2]}`\n                        : $arguments?.noCache != null ||\n                          $arguments?.insecure != null\n                        ? `#${rawArgs[1]}`\n                        : ''\n                }`;\n                const downloadUrlMatch = url\n                    .split('#')[0]\n                    .match(/^\\/api\\/(file|module)\\/(.+)/);\n                if (downloadUrlMatch) {\n                    let type = '';\n                    try {\n                        type = downloadUrlMatch?.[1];\n                        let name = downloadUrlMatch?.[2];\n                        if (name == null) {\n                            throw new Error(`本地 ${type} URL 无效: ${url}`);\n                        }\n                        name = decodeURIComponent(name);\n                        const key = type === 'module' ? MODULES_KEY : FILES_KEY;\n                        const item = findByName($.read(key), name);\n                        if (!item) {\n                            throw new Error(`找不到 ${type}: ${name}`);\n                        }\n\n                        if (type === 'module') {\n                            script = item.content;\n                        } else {\n                            script = await produceArtifact({\n                                type: 'file',\n                                name,\n                            });\n                        }\n                    } catch (err) {\n                        $.error(\n                            `Error when loading ${type}: ${item.args.content}.\\n Reason: ${err}`,\n                        );\n                        throw new Error(`无法加载 ${type}: ${url}`);\n                    }\n                } else if (url?.startsWith('/')) {\n                    try {\n                        const fs = eval(`require(\"fs\")`);\n                        script = fs.readFileSync(url.split('#')[0], 'utf8');\n                        // $.info(`Script loaded: >>>\\n ${script}`);\n                    } catch (err) {\n                        $.error(\n                            `Error when reading local script: ${item.args.content}.\\n Reason: ${err}`,\n                        );\n                        throw new Error(`无法从该路径读取脚本文件: ${url}`);\n                    }\n                } else {\n                    // if this is a remote script, download it\n                    try {\n                        script = await download(url);\n                        // $.info(`Script loaded: >>>\\n ${script}`);\n                    } catch (err) {\n                        $.error(\n                            `Error when downloading remote script: ${item.args.content}.\\n Reason: ${err}`,\n                        );\n                        throw new Error(`无法下载脚本: ${url}`);\n                    }\n                }\n            } else {\n                script = content;\n                $arguments = item.args.arguments || {};\n            }\n        }\n\n        if (!PROXY_PROCESSORS[item.type]) {\n            $.error(`Unknown operator: \"${item.type}\"`);\n            continue;\n        }\n\n        $.log(\n            `Applying \"${item.type}\" with arguments:\\n >>> ${\n                JSON.stringify(item.args, null, 2) || 'None'\n            }`,\n        );\n        let processor;\n        if (item.type.indexOf('Script') !== -1) {\n            processor = PROXY_PROCESSORS[item.type](\n                script,\n                targetPlatform,\n                $arguments,\n                source,\n                $options,\n                context,\n            );\n        } else {\n            processor = PROXY_PROCESSORS[item.type](item.args || {});\n        }\n        proxies = await ApplyProcessor(processor, proxies);\n    }\n    return proxies;\n}\n\nfunction produce(proxies, targetPlatform, type, opts = {}) {\n    const producer = PROXY_PRODUCERS[targetPlatform];\n    if (!producer) {\n        throw new Error(`Target platform: ${targetPlatform} is not supported!`);\n    }\n\n    const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(\n        targetPlatform,\n    );\n\n    // filter unsupported proxies\n    proxies = proxies.filter((proxy) => {\n        // 检查代理是否支持目标平台\n        if (proxy.supported && proxy.supported[targetPlatform] === false) {\n            return false;\n        }\n\n        // 对于 vless 和 vmess 代理,需要额外验证 UUID\n        if (['vless', 'vmess'].includes(proxy.type)) {\n            const isProxyUUIDValid = isValidUUID(proxy.uuid);\n            if (!isProxyUUIDValid)\n                $.info(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);\n            // return isProxyUUIDValid;\n        }\n\n        return true;\n    });\n\n    proxies = proxies.map((proxy) => {\n        proxy._resolved = proxy.resolved;\n\n        if (!isNotBlank(proxy.name)) {\n            proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;\n        }\n        if (proxy['disable-sni']) {\n            if (sni_off_supported) {\n                proxy.sni = 'off';\n            } else if (!['tuic'].includes(proxy.type)) {\n                $.error(\n                    `Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`,\n                );\n                proxy.sni = '';\n                proxy['skip-cert-verify'] = true;\n                delete proxy['tls-fingerprint'];\n            }\n        }\n\n        // 处理 端口跳跃\n        if (proxy.ports) {\n            proxy.ports = String(proxy.ports);\n            if (!['ClashMeta'].includes(targetPlatform)) {\n                proxy.ports = proxy.ports.replace(/\\//g, ',');\n            }\n            if (!proxy.port) {\n                proxy.port = getRandomPort(proxy.ports);\n            }\n        }\n\n        return proxy;\n    });\n\n    $.log(`Producing proxies for target: ${targetPlatform}`);\n    if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {\n        let list = proxies\n            .map((proxy) => {\n                try {\n                    return producer.produce(proxy, type, opts);\n                } catch (err) {\n                    $.error(\n                        `Cannot produce proxy: ${JSON.stringify(\n                            proxy,\n                            null,\n                            2,\n                        )}\\nReason: ${err}`,\n                    );\n                    return '';\n                }\n            })\n            .filter((line) => line.length > 0);\n        list = type === 'internal' ? list : list.join('\\n');\n        if (\n            targetPlatform.startsWith('Surge') &&\n            proxies.length > 0 &&\n            proxies.every((p) => p.type === 'wireguard')\n        ) {\n            list = `#!name=${proxies[0]?._subName}\n#!desc=${proxies[0]?._desc ?? ''}\n#!category=${proxies[0]?._category ?? ''}\n${list}`;\n        }\n        return list;\n    } else if (producer.type === 'ALL') {\n        return producer.produce(proxies, type, opts);\n    }\n}\n\nexport const ProxyUtils = {\n    parse,\n    process: processFn,\n    produce,\n    ipAddress,\n    getRandomPort,\n    isIPv4,\n    isIPv6,\n    isIP,\n    yaml: YAML,\n    getFlag,\n    removeFlag,\n    getISO,\n    MMDB,\n    Gist,\n    download,\n    downloadFile,\n    isValidUUID,\n    doh,\n    Buffer,\n    Base64,\n    JSON5,\n};\n\nfunction tryParse(parser, line) {\n    if (!safeMatch(parser, line)) return [null, new Error('Parser mismatch')];\n    try {\n        const proxy = parser.parse(line);\n        return [proxy, null];\n    } catch (err) {\n        return [null, err];\n    }\n}\n\nfunction safeMatch(parser, line) {\n    try {\n        return parser.test(line);\n    } catch (err) {\n        return false;\n    }\n}\n\nfunction formatTransportPath(path) {\n    if (typeof path === 'string' || typeof path === 'number') {\n        path = String(path).trim();\n\n        if (path === '') {\n            return '/';\n        } else if (!path.startsWith('/')) {\n            return '/' + path;\n        }\n    }\n    return path;\n}\n\nfunction lastParse(proxy) {\n    if (typeof proxy.cipher === 'string') {\n        proxy.cipher = proxy.cipher.toLowerCase();\n    }\n    if (typeof proxy.password === 'number') {\n        proxy.password = numberToString(proxy.password);\n    }\n    if (\n        ['ss'].includes(proxy.type) &&\n        proxy.cipher === 'none' &&\n        !proxy.password\n    ) {\n        // https://github.com/MetaCubeX/mihomo/issues/1677\n        proxy.password = '';\n    }\n    if (proxy.interface) {\n        proxy['interface-name'] = proxy.interface;\n        delete proxy.interface;\n    }\n    if (isValidPortNumber(proxy.port)) {\n        proxy.port = parseInt(proxy.port, 10);\n    }\n    if (proxy.server) {\n        proxy.server = `${proxy.server}`\n            .trim()\n            .replace(/^\\[/, '')\n            .replace(/\\]$/, '');\n    }\n    if (proxy.network === 'ws') {\n        if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) {\n            proxy['ws-opts'] = {};\n            if (proxy['ws-path']) {\n                proxy['ws-opts'].path = proxy['ws-path'];\n            }\n            if (proxy['ws-headers']) {\n                proxy['ws-opts'].headers = proxy['ws-headers'];\n            }\n        }\n        delete proxy['ws-path'];\n        delete proxy['ws-headers'];\n    }\n\n    const transportPath = proxy[`${proxy.network}-opts`]?.path;\n\n    if (Array.isArray(transportPath)) {\n        proxy[`${proxy.network}-opts`].path = transportPath.map((item) =>\n            formatTransportPath(item),\n        );\n    } else if (transportPath != null) {\n        proxy[`${proxy.network}-opts`].path =\n            formatTransportPath(transportPath);\n    }\n\n    // network 逻辑有点乱了 可能还牵扯到别的逻辑 以后再优化...\n    // 以 mihomo 为准的话, 其实应该是\n    // network¶\n    // 传输层，支持 ws/grpc，不配置或配置其他值则为 tcp\n    if (proxy.type === 'trojan') {\n        proxy.network = proxy.network || 'tcp';\n    }\n    // network¶\n    // 传输层，支持 ws/http/h2/grpc，不配置或配置其他值则为 tcp\n    if (['vmess'].includes(proxy.type)) {\n        proxy.network = proxy.network || 'tcp';\n\n        proxy.cipher = proxy.cipher || 'none';\n        proxy.alterId = proxy.alterId || 0;\n    }\n    // network¶\n    // 传输层，支持 ws/http/h2/grpc，不配置或配置其他值则为 tcp\n    if (['vless'].includes(proxy.type)) {\n        proxy.network = proxy.network || 'tcp';\n    }\n    if (\n        [\n            'trojan',\n            'tuic',\n            'hysteria',\n            'hysteria2',\n            'juicity',\n            'anytls',\n            'trusttunnel',\n            'naive',\n        ].includes(proxy.type)\n    ) {\n        proxy.tls = true;\n    }\n    if (proxy.network) {\n        let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;\n        let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;\n        if (proxy.network === 'h2') {\n            if (!transporthost && transportHost) {\n                proxy[`${proxy.network}-opts`].headers.host = transportHost;\n                delete proxy[`${proxy.network}-opts`].headers.Host;\n            }\n        } else if (transporthost && !transportHost) {\n            proxy[`${proxy.network}-opts`].headers.Host = transporthost;\n            delete proxy[`${proxy.network}-opts`].headers.host;\n        }\n    }\n    if (proxy.network === 'h2') {\n        const host = proxy['h2-opts']?.headers?.host;\n        const path = proxy['h2-opts']?.path;\n        if (host && !Array.isArray(host)) {\n            proxy['h2-opts'].headers.host = [host];\n        }\n        if (Array.isArray(path)) {\n            proxy['h2-opts'].path = path[0];\n        }\n    }\n\n    // 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)\n    if (\n        !proxy.tls &&\n        ['ws', 'http'].includes(proxy.network) &&\n        !proxy[`${proxy.network}-opts`]?.headers?.Host &&\n        !isIP(proxy.server)\n    ) {\n        proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};\n        proxy[`${proxy.network}-opts`].headers =\n            proxy[`${proxy.network}-opts`].headers || {};\n        proxy[`${proxy.network}-opts`].headers.Host =\n            ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'\n                ? [proxy.server]\n                : proxy.server;\n    }\n    // 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组\n    if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {\n        let transportPath = proxy[`${proxy.network}-opts`]?.path;\n        let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;\n        if (transportHost && !Array.isArray(transportHost)) {\n            proxy[`${proxy.network}-opts`].headers.Host = [transportHost];\n        }\n        if (transportPath && !Array.isArray(transportPath)) {\n            proxy[`${proxy.network}-opts`].path = [transportPath];\n        }\n    }\n    if (proxy.tls && !proxy.sni) {\n        if (!isIP(proxy.server)) {\n            proxy.sni = proxy.server;\n        }\n        if (!proxy.sni && proxy.network) {\n            let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;\n            transportHost = Array.isArray(transportHost)\n                ? transportHost[0]\n                : transportHost;\n            if (transportHost) {\n                proxy.sni = transportHost;\n            }\n        }\n    }\n    // if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) {\n    if (proxy.ports) {\n        proxy.ports = String(proxy.ports).replace(/\\//g, ',');\n    } else {\n        delete proxy.ports;\n    }\n    // }\n    if (\n        ['hysteria2'].includes(proxy.type) &&\n        proxy.obfs &&\n        !['salamander'].includes(proxy.obfs) &&\n        !proxy['obfs-password']\n    ) {\n        proxy['obfs-password'] = proxy.obfs;\n        proxy.obfs = 'salamander';\n    }\n    if (\n        ['hysteria2'].includes(proxy.type) &&\n        !proxy['obfs-password'] &&\n        proxy['obfs_password']\n    ) {\n        proxy['obfs-password'] = proxy['obfs_password'];\n        delete proxy['obfs_password'];\n    }\n    if (['vless'].includes(proxy.type)) {\n        // 删除 reality-opts: {}\n        if (\n            proxy['reality-opts'] &&\n            Object.keys(proxy['reality-opts']).length === 0\n        ) {\n            delete proxy['reality-opts'];\n        }\n        // 删除 grpc-opts: {}\n        if (\n            proxy['grpc-opts'] &&\n            Object.keys(proxy['grpc-opts']).length === 0\n        ) {\n            delete proxy['grpc-opts'];\n        }\n        // 非 reality, 空 flow 没有意义\n        if (\n            (!proxy['reality-opts'] && !proxy.flow) ||\n            ['null', null].includes(proxy.flow)\n        ) {\n            delete proxy.flow;\n        }\n        if (['http'].includes(proxy.network)) {\n            let transportPath = proxy[`${proxy.network}-opts`]?.path;\n            if (!transportPath) {\n                if (!proxy[`${proxy.network}-opts`]) {\n                    proxy[`${proxy.network}-opts`] = {};\n                }\n                proxy[`${proxy.network}-opts`].path = ['/'];\n            }\n        }\n    }\n\n    if (typeof proxy.name !== 'string') {\n        if (/^\\d+$/.test(proxy.name)) {\n            proxy.name = `${proxy.name}`;\n        } else {\n            try {\n                if (proxy.name?.data) {\n                    proxy.name = Buffer.from(proxy.name.data).toString('utf8');\n                } else {\n                    proxy.name = Buffer.from(proxy.name).toString('utf8');\n                }\n            } catch (e) {\n                $.error(`proxy.name decode failed\\nReason: ${e}`);\n                proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;\n            }\n        }\n    }\n    if (['ws', 'http', 'h2'].includes(proxy.network)) {\n        if (\n            ['ws', 'h2'].includes(proxy.network) &&\n            !proxy[`${proxy.network}-opts`]?.path\n        ) {\n            proxy[`${proxy.network}-opts`] =\n                proxy[`${proxy.network}-opts`] || {};\n            proxy[`${proxy.network}-opts`].path = '/';\n        } else if (\n            proxy.network === 'http' &&\n            (!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||\n                proxy[`${proxy.network}-opts`]?.path.every((i) => !i))\n        ) {\n            proxy[`${proxy.network}-opts`] =\n                proxy[`${proxy.network}-opts`] || {};\n            proxy[`${proxy.network}-opts`].path = ['/'];\n        }\n    }\n    if (['', 'off'].includes(proxy.sni)) {\n        proxy['disable-sni'] = true;\n    }\n    let caStr = proxy['ca_str'];\n    if (proxy['ca-str']) {\n        caStr = proxy['ca-str'];\n    } else if (caStr) {\n        delete proxy['ca_str'];\n        proxy['ca-str'] = caStr;\n    }\n    try {\n        if ($.env.isNode && !caStr && proxy['_ca']) {\n            caStr = $.node.fs.readFileSync(proxy['_ca'], {\n                encoding: 'utf8',\n            });\n        }\n    } catch (e) {\n        $.error(`Read ca file failed\\nReason: ${e}`);\n    }\n    if (!proxy['tls-fingerprint'] && caStr) {\n        proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);\n    }\n    if (\n        ['ss'].includes(proxy.type) &&\n        isPresent(proxy, 'shadow-tls-password')\n    ) {\n        proxy.plugin = 'shadow-tls';\n        proxy['plugin-opts'] = {\n            host: proxy['shadow-tls-sni'],\n            password: proxy['shadow-tls-password'],\n            version: proxy['shadow-tls-version'],\n        };\n        delete proxy['shadow-tls-sni'];\n        delete proxy['shadow-tls-password'];\n        delete proxy['shadow-tls-version'];\n    }\n    if (['tuic'].includes(proxy.type)) {\n        proxy.alpn = Array.isArray(proxy.alpn)\n            ? proxy.alpn\n            : [proxy.alpn || 'h3'];\n        proxy['congestion-controller'] =\n            proxy['congestion-controller'] || 'cubic';\n        proxy['udp-relay-mode'] = proxy['udp-relay-mode'] || 'native';\n    }\n    if (['wireguard'].includes(proxy.type)) {\n        if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {\n            const validPeer =\n                proxy.peers.find((peer) => peer.ip && peer.ipv6) ||\n                proxy.peers.find((peer) => peer.ip || peer.ipv6);\n            if (validPeer) {\n                if (!proxy.ip) {\n                    proxy.ip = proxy.peers[0]?.ip;\n                }\n                if (!proxy.ipv6) {\n                    proxy.ipv6 = proxy.peers[0]?.ipv6;\n                }\n            }\n        }\n        if (proxy.ip?.includes('/')) {\n            const [ip] = proxy.ip.split('/');\n            if (isIPv4(ip)) {\n                proxy.ip = ip;\n            }\n        }\n        if (proxy.ipv6?.includes('/')) {\n            const [ip] = proxy.ipv6.split('/');\n            if (isIPv6(ip)) {\n                proxy.ipv6 = ip;\n            }\n        }\n    }\n    return proxy;\n}\n\nfunction isIP(ip) {\n    return isIPv4(ip) || isIPv6(ip);\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/index.js",
    "content": "import {\n    isIPv4,\n    isIPv6,\n    getIfNotBlank,\n    isPresent,\n    isNotBlank,\n    getIfPresent,\n    getRandomPort,\n} from '@/utils';\nimport getSurgeParser from './peggy/surge';\nimport getLoonParser from './peggy/loon';\nimport getQXParser from './peggy/qx';\nimport getTrojanURIParser from './peggy/trojan-uri';\nimport $ from '@/core/app';\nimport JSON5 from 'json5';\nimport YAML from '@/utils/yaml';\nimport _ from 'lodash';\n\nimport { Base64 } from 'js-base64';\n\nfunction surge_port_hopping(raw) {\n    const [parts, port_hopping] =\n        raw.match(\n            /,\\s*?port-hopping\\s*?=\\s*?[\"']?\\s*?((\\d+(-\\d+)?)([,;]\\d+(-\\d+)?)*)\\s*?[\"']?\\s*?/,\n        ) || [];\n    return {\n        port_hopping: port_hopping\n            ? port_hopping.replace(/;/g, ',')\n            : undefined,\n        line: parts ? raw.replace(parts, '') : raw,\n    };\n}\n\nfunction URI_PROXY() {\n    // socks5+tls\n    // socks5\n    // http, https(可以这么写)\n    const name = 'URI PROXY Parser';\n    const test = (line) => {\n        return /^(socks5\\+tls|socks5|http|https):\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        // parse url\n        // eslint-disable-next-line no-unused-vars\n        let [__, type, tls, username, password, server, port, query, name] =\n            line.match(\n                /^(socks5|http|http)(\\+tls|s)?:\\/\\/(?:(.*?):(.*?)@)?(.*?)(?::(\\d+?))?\\/?(\\?.*?)?(?:#(.*?))?$/,\n            );\n        if (port) {\n            port = parseInt(port, 10);\n        } else {\n            if (tls) {\n                port = 443;\n            } else if (type === 'http') {\n                port = 80;\n            } else {\n                $.error(`port is not present in line: ${line}`);\n                throw new Error(`port is not present in line: ${line}`);\n            }\n            $.info(`port is not present in line: ${line}, set to ${port}`);\n        }\n\n        const proxy = {\n            name:\n                name != null\n                    ? decodeURIComponent(name)\n                    : `${type} ${server}:${port}`,\n            type,\n            tls: tls ? true : false,\n            server,\n            port,\n            username:\n                username != null ? decodeURIComponent(username) : undefined,\n            password:\n                password != null ? decodeURIComponent(password) : undefined,\n        };\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction URI_SOCKS() {\n    const name = 'URI SOCKS Parser';\n    const test = (line) => {\n        return /^socks:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        // parse url\n        // eslint-disable-next-line no-unused-vars\n        let [__, type, auth, server, port, query, name] = line.match(\n            /^(socks)?:\\/\\/(?:(.*)@)?(.*?)(?::(\\d+?))?(\\?.*?)?(?:#(.*?))?$/,\n        );\n        if (port) {\n            port = parseInt(port, 10);\n        } else {\n            $.error(`port is not present in line: ${line}`);\n            throw new Error(`port is not present in line: ${line}`);\n        }\n        let username, password;\n        if (auth) {\n            const parsed = Base64.decode(decodeURIComponent(auth)).split(':');\n            username = parsed[0];\n            password = parsed[1];\n        }\n\n        const proxy = {\n            name:\n                name != null\n                    ? decodeURIComponent(name)\n                    : `${type} ${server}:${port}`,\n            type: 'socks5',\n            server,\n            port,\n            username,\n            password,\n        };\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\n// Parse SS URI format (only supports new SIP002, legacy format is depreciated).\n// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme\nfunction URI_SS() {\n    const name = 'URI SS Parser';\n    const test = (line) => {\n        return /^ss:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        // parse url\n        let content = line.split('ss://')[1];\n\n        let name = line.split('#')[1];\n        const proxy = {\n            type: 'ss',\n        };\n        content = content.split('#')[0]; // strip proxy name\n        // handle IPV4 and IPV6\n        let serverAndPortArray = content.match(/@([^/?]*)(\\/|\\?|$)/);\n\n        let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大\n        let userInfoStr;\n        if (rawUserInfoStr?.startsWith('2022-blake3-')) {\n            userInfoStr = rawUserInfoStr;\n        } else {\n            userInfoStr = Base64.decode(rawUserInfoStr);\n        }\n\n        let query = '';\n        if (!serverAndPortArray) {\n            if (content.includes('?')) {\n                const parsed = content.match(/^(.*)(\\?.*)$/);\n                content = parsed[1];\n                query = parsed[2];\n            }\n            content = Base64.decode(content);\n\n            if (query) {\n                if (/(&|\\?)v2ray-plugin=/.test(query)) {\n                    const parsed = query.match(/(&|\\?)v2ray-plugin=(.*?)(&|$)/);\n                    let v2rayPlugin = parsed[2];\n                    if (v2rayPlugin) {\n                        proxy.plugin = 'v2ray-plugin';\n                        proxy['plugin-opts'] = JSON.parse(\n                            Base64.decode(v2rayPlugin),\n                        );\n                    }\n                }\n                content = `${content}${query}`;\n            }\n            userInfoStr = content.match(/(^.*)@/)?.[1];\n            serverAndPortArray = content.match(/@([^/@]*)(\\/|$)/);\n        } else if (content.includes('?')) {\n            const parsed = content.match(/(\\?.*)$/);\n            query = parsed[1];\n        }\n        const params = {};\n        for (const addon of query.replace(/^\\?/, '').split('&')) {\n            if (addon) {\n                const [key, valueRaw] = addon.split('=');\n                let value = valueRaw;\n                value = decodeURIComponent(valueRaw);\n                params[key] = value;\n            }\n        }\n        proxy.tls = params.security && params.security !== 'none';\n        proxy['skip-cert-verify'] = !!params['allowInsecure'];\n        proxy.sni = params['sni'] || params['peer'];\n        proxy['client-fingerprint'] = params.fp;\n        proxy.alpn = params.alpn\n            ? decodeURIComponent(params.alpn).split(',')\n            : undefined;\n\n        if (params['ws']) {\n            proxy.network = 'ws';\n            _.set(proxy, 'ws-opts.path', params['wspath']);\n        }\n\n        if (params['type']) {\n            let httpupgrade;\n            proxy.network = params['type'];\n            if (proxy.network === 'httpupgrade') {\n                proxy.network = 'ws';\n                httpupgrade = true;\n            }\n            if (['grpc'].includes(proxy.network)) {\n                proxy[proxy.network + '-opts'] = {\n                    'grpc-service-name': params['serviceName'],\n                    '_grpc-type': params['mode'],\n                    '_grpc-authority': params['authority'],\n                };\n            } else {\n                if (params['path']) {\n                    _.set(\n                        proxy,\n                        proxy.network + '-opts.path',\n                        decodeURIComponent(params['path']),\n                    );\n                }\n                if (params['host']) {\n                    _.set(\n                        proxy,\n                        proxy.network + '-opts.headers.Host',\n                        decodeURIComponent(params['host']),\n                    );\n                }\n                if (httpupgrade) {\n                    _.set(\n                        proxy,\n                        proxy.network + '-opts.v2ray-http-upgrade',\n                        true,\n                    );\n                    _.set(\n                        proxy,\n                        proxy.network + '-opts.v2ray-http-upgrade-fast-open',\n                        true,\n                    );\n                }\n            }\n            if (['reality'].includes(params.security)) {\n                const opts = {};\n                if (params.pbk) {\n                    opts['public-key'] = params.pbk;\n                }\n                if (params.sid) {\n                    opts['short-id'] = params.sid;\n                }\n                if (params.spx) {\n                    opts['_spider-x'] = params.spx;\n                }\n                if (params.mode) {\n                    proxy._mode = params.mode;\n                }\n                if (params.extra) {\n                    proxy._extra = params.extra;\n                }\n                if (Object.keys(opts).length > 0) {\n                    _.set(proxy, params.security + '-opts', opts);\n                }\n            }\n        }\n\n        proxy.udp = !!params['udp'];\n\n        const serverAndPort = serverAndPortArray[1];\n        const portIdx = serverAndPort.lastIndexOf(':');\n        proxy.server = serverAndPort.substring(0, portIdx);\n        proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(\n            /\\d+/,\n        )?.[0];\n        let userInfo = userInfoStr.match(/(^.*?):(.*$)/);\n        proxy.cipher = userInfo?.[1];\n        proxy.password = userInfo?.[2];\n        // if (!proxy.cipher || !proxy.password) {\n        //     userInfo = rawUserInfoStr.match(/(^.*?):(.*$)/);\n        //     proxy.cipher = userInfo?.[1];\n        //     proxy.password = userInfo?.[2];\n        // }\n\n        // handle obfs\n        const pluginMatch = content.match(/[?&]plugin=([^&]+)/);\n        const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);\n\n        if (pluginMatch) {\n            const pluginInfo = (\n                'plugin=' + decodeURIComponent(pluginMatch[1])\n            ).split(';');\n            const params = {};\n            for (const item of pluginInfo) {\n                const [key, val] = item.split('=');\n                if (key) params[key] = val || true; // some options like \"tls\" will not have value\n            }\n            switch (params.plugin) {\n                case 'obfs-local':\n                case 'simple-obfs':\n                    proxy.plugin = 'obfs';\n                    proxy['plugin-opts'] = {\n                        mode: params.obfs,\n                        host: getIfNotBlank(params['obfs-host']),\n                    };\n                    break;\n                case 'v2ray-plugin':\n                    proxy.plugin = 'v2ray-plugin';\n                    proxy['plugin-opts'] = {\n                        mode: 'websocket',\n                        host:\n                            getIfNotBlank(params['obfs-host']) ||\n                            getIfNotBlank(params['host']),\n                        path: getIfNotBlank(params.path),\n                        tls: getIfPresent(params.tls),\n                    };\n                    break;\n                case 'shadow-tls': {\n                    proxy.plugin = 'shadow-tls';\n                    const version = getIfNotBlank(params['version']);\n                    proxy['plugin-opts'] = {\n                        host: getIfNotBlank(params['host']),\n                        password: getIfNotBlank(params['password']),\n                        version: version ? parseInt(version, 10) : undefined,\n                    };\n                    break;\n                }\n                default:\n                    throw new Error(\n                        `Unsupported plugin option: ${params.plugin}`,\n                    );\n            }\n        }\n        // Shadowrocket\n        if (shadowTlsMatch) {\n            const params = JSON.parse(Base64.decode(shadowTlsMatch[1]));\n            const version = getIfNotBlank(params['version']);\n            const address = getIfNotBlank(params['address']);\n            const port = getIfNotBlank(params['port']);\n            proxy.plugin = 'shadow-tls';\n            proxy['plugin-opts'] = {\n                host: getIfNotBlank(params['host']),\n                password: getIfNotBlank(params['password']),\n                version: version ? parseInt(version, 10) : undefined,\n            };\n            if (address) {\n                proxy.server = address;\n            }\n            if (port) {\n                proxy.port = parseInt(port, 10);\n            }\n        }\n        if (/(&|\\?)uot=(1|true)/i.test(query)) {\n            proxy['udp-over-tcp'] = true;\n        }\n        if (/(&|\\?)tfo=(1|true)/i.test(query)) {\n            proxy.tfo = true;\n        }\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n        proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`;\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\n// Parse URI SSR format, such as ssr://xxx\nfunction URI_SSR() {\n    const name = 'URI SSR Parser';\n    const test = (line) => {\n        return /^ssr:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = Base64.decode(line.split('ssr://')[1]);\n\n        // handle IPV6 & IPV4 format\n        let splitIdx = line.indexOf(':origin');\n        if (splitIdx === -1) {\n            splitIdx = line.indexOf(':auth_');\n        }\n        const serverAndPort = line.substring(0, splitIdx);\n        const server = serverAndPort.substring(\n            0,\n            serverAndPort.lastIndexOf(':'),\n        );\n        const port = serverAndPort.substring(\n            serverAndPort.lastIndexOf(':') + 1,\n        );\n\n        let params = line\n            .substring(splitIdx + 1)\n            .split('/?')[0]\n            .split(':');\n        let proxy = {\n            type: 'ssr',\n            server,\n            port,\n            protocol: params[0],\n            cipher: params[1],\n            obfs: params[2],\n            password: Base64.decode(params[3]),\n        };\n        // get other params\n        const other_params = {};\n        line = line.split('/?')[1].split('&');\n        if (line.length > 1) {\n            for (const item of line) {\n                let [key, val] = item.split('=');\n                val = val.trim();\n                if (val.length > 0 && val !== '(null)') {\n                    other_params[key] = val;\n                }\n            }\n        }\n        proxy = {\n            ...proxy,\n            name: other_params.remarks\n                ? Base64.decode(other_params.remarks)\n                : proxy.server,\n            'protocol-param': getIfNotBlank(\n                Base64.decode(other_params.protoparam || '').replace(/\\s/g, ''),\n            ),\n            'obfs-param': getIfNotBlank(\n                Base64.decode(other_params.obfsparam || '').replace(/\\s/g, ''),\n            ),\n        };\n        return proxy;\n    };\n\n    return { name, test, parse };\n}\n\n// V2rayN URI VMess format\n// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)\n\n// Quantumult VMess format\nfunction URI_VMess() {\n    const name = 'URI VMess Parser';\n    const test = (line) => {\n        return /^vmess:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = line.split('vmess://')[1];\n        let content = Base64.decode(line.replace(/\\?.*?$/, ''));\n        if (/=\\s*vmess/.test(content)) {\n            // Quantumult VMess URI format\n            const partitions = content.split(',').map((p) => p.trim());\n            // get keyword params\n            const params = {};\n            for (const part of partitions) {\n                if (part.indexOf('=') !== -1) {\n                    const [key, val] = part.split('=');\n                    params[key.trim()] = val.trim();\n                }\n            }\n\n            const proxy = {\n                name: partitions[0].split('=')[0].trim(),\n                type: 'vmess',\n                server: partitions[1],\n                port: partitions[2],\n                cipher: getIfNotBlank(partitions[3], 'auto'),\n                uuid: partitions[4].match(/^\"(.*)\"$/)[1],\n                tls: params.obfs === 'wss',\n                udp: getIfPresent(params['udp-relay']),\n                tfo: getIfPresent(params['fast-open']),\n                'skip-cert-verify': isPresent(params['tls-verification'])\n                    ? !params['tls-verification']\n                    : undefined,\n            };\n\n            // handle ws headers\n            if (isPresent(params.obfs)) {\n                if (params.obfs === 'ws' || params.obfs === 'wss') {\n                    proxy.network = 'ws';\n                    proxy['ws-opts'].path = (\n                        getIfNotBlank(params['obfs-path']) || '\"/\"'\n                    ).match(/^\"(.*)\"$/)[1];\n                    let obfs_host = params['obfs-header'];\n                    if (obfs_host && obfs_host.indexOf('Host') !== -1) {\n                        obfs_host = obfs_host.match(\n                            /Host:\\s*([a-zA-Z0-9-.]*)/,\n                        )[1];\n                    }\n                    if (isNotBlank(obfs_host)) {\n                        proxy['ws-opts'].headers = {\n                            Host: obfs_host,\n                        };\n                    }\n                } else {\n                    throw new Error(`Unsupported obfs: ${params.obfs}`);\n                }\n            }\n            return proxy;\n        } else {\n            let params = {};\n\n            try {\n                // V2rayN URI format\n                params = JSON.parse(content);\n            } catch (e) {\n                // Shadowrocket URI format\n                // eslint-disable-next-line no-unused-vars\n                let [__, base64Line, qs] = /(^[^?]+?)\\/?\\?(.*)$/.exec(line);\n                content = Base64.decode(base64Line);\n\n                for (const addon of qs.split('&')) {\n                    const [key, valueRaw] = addon.split('=');\n                    let value = valueRaw;\n                    value = decodeURIComponent(valueRaw);\n                    if (value.indexOf(',') === -1) {\n                        params[key] = value;\n                    } else {\n                        params[key] = value.split(',');\n                    }\n                }\n                // eslint-disable-next-line no-unused-vars\n                let [___, cipher, uuid, server, port] =\n                    /(^[^:]+?):([^:]+?)@(.*):(\\d+)$/.exec(content);\n\n                params.scy = cipher;\n                params.id = uuid;\n                params.port = port;\n                params.add = server;\n            }\n            const server = params.add;\n            const port = parseInt(getIfPresent(params.port), 10);\n            const proxy = {\n                name:\n                    params.ps ??\n                    params.remarks ??\n                    params.remark ??\n                    `VMess ${server}:${port}`,\n                type: 'vmess',\n                server,\n                port,\n                // https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link\n                // https://github.com/XTLS/Xray-core/issues/91\n                cipher: [\n                    'auto',\n                    'aes-128-gcm',\n                    'chacha20-poly1305',\n                    'none',\n                ].includes(params.scy)\n                    ? params.scy\n                    : 'auto',\n                uuid: params.id,\n                alterId: parseInt(\n                    getIfPresent(params.aid ?? params.alterId, 0),\n                    10,\n                ),\n                tls: ['tls', true, 1, '1'].includes(params.tls),\n                'skip-cert-verify': isPresent(params.verify_cert)\n                    ? !params.verify_cert\n                    : undefined,\n            };\n            if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {\n                proxy['skip-cert-verify'] = /(TRUE)|1/i.test(\n                    params.allowInsecure,\n                );\n            }\n            // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)\n            if (proxy.tls) {\n                if (params.sni && params.sni !== '') {\n                    proxy.sni = params.sni;\n                } else if (params.peer && params.peer !== '') {\n                    proxy.sni = params.peer;\n                }\n            }\n            let httpupgrade = false;\n            // handle obfs\n            if (params.net === 'ws' || params.obfs === 'websocket') {\n                proxy.network = 'ws';\n            } else if (\n                ['http'].includes(params.net) ||\n                ['http'].includes(params.obfs) ||\n                ['http'].includes(params.type)\n            ) {\n                proxy.network = 'http';\n            } else if (['grpc', 'kcp', 'quic'].includes(params.net)) {\n                proxy.network = params.net;\n            } else if (\n                params.net === 'httpupgrade' ||\n                proxy.network === 'httpupgrade'\n            ) {\n                proxy.network = 'ws';\n                httpupgrade = true;\n            } else if (params.net === 'h2' || proxy.network === 'h2') {\n                proxy.network = 'h2';\n            }\n            // 暂不支持 tcp + host + path\n            // else if (params.net === 'tcp' || proxy.network === 'tcp') {\n            //     proxy.network = 'tcp';\n            // }\n            if (proxy.network) {\n                let transportHost = params.host ?? params.obfsParam;\n                try {\n                    const parsedObfs = JSON.parse(transportHost);\n                    const parsedHost = parsedObfs?.Host;\n                    if (parsedHost) {\n                        transportHost = parsedHost;\n                    }\n                    // eslint-disable-next-line no-empty\n                } catch (e) {}\n                let transportPath = params.path;\n\n                // 补上默认 path\n                if (['ws'].includes(proxy.network)) {\n                    transportPath = transportPath || '/';\n                }\n\n                if (proxy.network === 'http') {\n                    if (transportHost) {\n                        // 1)http(tcp)->host中间逗号(,)隔开\n                        transportHost = transportHost\n                            .split(',')\n                            .map((i) => i.trim());\n                        transportHost = Array.isArray(transportHost)\n                            ? transportHost[0]\n                            : transportHost;\n                    }\n                    if (transportPath) {\n                        transportPath = Array.isArray(transportPath)\n                            ? transportPath[0]\n                            : transportPath;\n                    } else {\n                        transportPath = '/';\n                    }\n                }\n                // 传输层应该有配置, 暂时不考虑兼容不给配置的节点\n                if (\n                    transportPath ||\n                    transportHost ||\n                    ['kcp', 'quic'].includes(proxy.network)\n                ) {\n                    if (['grpc'].includes(proxy.network)) {\n                        proxy[`${proxy.network}-opts`] = {\n                            'grpc-service-name': getIfNotBlank(transportPath),\n                            '_grpc-type': getIfNotBlank(params.type),\n                            '_grpc-authority': getIfNotBlank(params.authority),\n                        };\n                    } else if (['kcp', 'quic'].includes(proxy.network)) {\n                        proxy[`${proxy.network}-opts`] = {\n                            [`_${proxy.network}-type`]: getIfNotBlank(\n                                params.type,\n                            ),\n                            [`_${proxy.network}-host`]: getIfNotBlank(\n                                getIfNotBlank(transportHost),\n                            ),\n                            [`_${proxy.network}-path`]:\n                                getIfNotBlank(transportPath),\n                        };\n                    } else {\n                        const opts = {\n                            path: getIfNotBlank(transportPath),\n                            headers: { Host: getIfNotBlank(transportHost) },\n                        };\n                        if (httpupgrade) {\n                            opts['v2ray-http-upgrade'] = true;\n                            opts['v2ray-http-upgrade-fast-open'] = true;\n                        }\n                        proxy[`${proxy.network}-opts`] = opts;\n                    }\n                } else {\n                    delete proxy.network;\n                }\n            }\n\n            proxy['client-fingerprint'] = params.fp;\n            proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;\n            // 然而 wiki 和 app 实测中都没有字段表示这个\n            // proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);\n\n            return proxy;\n        }\n    };\n    return { name, test, parse };\n}\n\nfunction URI_VLESS() {\n    const name = 'URI VLESS Parser';\n    const test = (line) => {\n        return /^vless:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = line.split('vless://')[1];\n        let isShadowrocket;\n        let parsed = /^(.*?)@(.*?):(\\d+)\\/?(\\?(.*?))?(?:#(.*?))?$/.exec(line);\n        if (!parsed) {\n            // eslint-disable-next-line no-unused-vars\n            let [_, base64, other] = /^(.*?)(\\?.*?$)/.exec(line);\n            line = `${Base64.decode(base64)}${other}`;\n            parsed = /^(.*?)@(.*?):(\\d+)\\/?(\\?(.*?))?(?:#(.*?))?$/.exec(line);\n            isShadowrocket = true;\n        }\n        // eslint-disable-next-line no-unused-vars\n        let [__, uuid, server, port, ___, addons = '', name] = parsed;\n        if (isShadowrocket) {\n            uuid = uuid.replace(/^.*?:/g, '');\n        }\n\n        port = parseInt(`${port}`, 10);\n        uuid = decodeURIComponent(uuid);\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n\n        const proxy = {\n            type: 'vless',\n            name,\n            server,\n            port,\n            uuid,\n        };\n        const params = {};\n        for (const addon of addons.split('&')) {\n            if (addon) {\n                const [key, valueRaw] = addon.split('=');\n                let value = valueRaw;\n                value = decodeURIComponent(valueRaw);\n                params[key] = value;\n            }\n        }\n\n        proxy.name =\n            name ??\n            params.remarks ??\n            params.remark ??\n            `VLESS ${server}:${port}`;\n\n        proxy.tls = params.security && params.security !== 'none';\n        if (isShadowrocket && /TRUE|1/i.test(params.tls)) {\n            proxy.tls = true;\n            params.security = params.security ?? 'reality';\n        }\n        proxy.sni = params.sni || params.peer;\n        proxy.flow = params.flow;\n        if (!proxy.flow && isShadowrocket && params.xtls) {\n            // \"none\" is undefined\n            const flow = [undefined, 'xtls-rprx-direct', 'xtls-rprx-vision'][\n                params.xtls\n            ];\n            if (flow) {\n                proxy.flow = flow;\n            }\n        }\n        proxy['client-fingerprint'] = params.fp;\n        proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;\n        proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);\n        proxy._echConfigList = getIfPresent(params.ech);\n        proxy._pcs = getIfPresent(params.pcs);\n        proxy._h2 = /(TRUE)|1/i.test(params.h2);\n\n        if (['reality'].includes(params.security)) {\n            const opts = {};\n            if (params.pbk) {\n                opts['public-key'] = params.pbk;\n            }\n            if (params.sid) {\n                opts['short-id'] = params.sid;\n            }\n            if (params.spx) {\n                opts['_spider-x'] = params.spx;\n            }\n            if (Object.keys(opts).length > 0) {\n                // proxy[`${params.security}-opts`] = opts;\n                proxy[`${params.security}-opts`] = opts;\n            }\n        }\n        let httpupgrade = false;\n        proxy.network = params.type;\n        if (proxy.network === 'tcp' && params.headerType === 'http') {\n            proxy.network = 'http';\n        } else if (proxy.network === 'httpupgrade') {\n            proxy.network = 'ws';\n            httpupgrade = true;\n        }\n        if (!proxy.network && isShadowrocket && params.obfs) {\n            proxy.network = params.obfs;\n            if (['none'].includes(proxy.network)) {\n                proxy.network = 'tcp';\n            }\n        }\n        if (['websocket'].includes(proxy.network)) {\n            proxy.network = 'ws';\n        }\n\n        if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {\n            const opts = {};\n            const host = params.host ?? params.obfsParam;\n            if (host) {\n                if (params.obfsParam) {\n                    try {\n                        const parsed = JSON.parse(host);\n                        opts.headers = parsed;\n                    } catch (e) {\n                        opts.headers = { Host: host };\n                    }\n                } else {\n                    opts.headers = { Host: host };\n                }\n            }\n            if (params.serviceName) {\n                opts[`${proxy.network}-service-name`] = params.serviceName;\n                if (['grpc'].includes(proxy.network) && params.authority) {\n                    opts['_grpc-authority'] = params.authority;\n                }\n            } else if (isShadowrocket && params.path) {\n                if (!['ws', 'http', 'h2'].includes(proxy.network)) {\n                    opts[`${proxy.network}-service-name`] = params.path;\n                    delete params.path;\n                }\n            }\n            if (params.path) {\n                opts.path = params.path;\n            }\n            // https://github.com/XTLS/Xray-core/issues/91\n            if (['grpc'].includes(proxy.network)) {\n                opts['_grpc-type'] = params.mode || 'gun';\n            }\n            if (httpupgrade) {\n                opts['v2ray-http-upgrade'] = true;\n                opts['v2ray-http-upgrade-fast-open'] = true;\n            }\n            if (Object.keys(opts).length > 0) {\n                proxy[`${proxy.network}-opts`] = opts;\n            }\n            if (proxy.network === 'kcp') {\n                // mKCP 种子。省略时不使用种子，但不可以为空字符串。建议 mKCP 用户使用 seed。\n                if (params.seed) {\n                    proxy.seed = params.seed;\n                }\n                // mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none，即不使用伪装头部，但不可以为空字符串。\n                proxy.headerType = params.headerType || 'none';\n            }\n\n            if (params.mode) {\n                proxy._mode = params.mode;\n            }\n            if (params.extra) {\n                proxy._extra = params.extra;\n            }\n        }\n        if (params.encryption) {\n            proxy.encryption = params.encryption;\n        }\n        if (params.pqv) {\n            proxy._pqv = params.pqv;\n        }\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction URI_AnyTLS() {\n    const name = 'URI AnyTLS Parser';\n    const test = (line) => {\n        return /^anytls:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        const parsed = URI_VLESS().parse(line.replace('anytls', 'vless'));\n        // 偷个懒\n        line = line.split(/anytls:\\/\\//)[1];\n        // eslint-disable-next-line no-unused-vars\n        let [__, password, server, port, addons = '', name] =\n            /^(.*?)@(.*?)(?::(\\d+))?\\/?(?:\\?(.*?))?(?:#(.*?))?$/.exec(line);\n        password = decodeURIComponent(password);\n        port = parseInt(`${port}`, 10);\n        if (isNaN(port)) {\n            port = 443;\n        }\n        password = decodeURIComponent(password);\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n        name = name ?? `AnyTLS ${server}:${port}`;\n\n        const proxy = {\n            ...parsed,\n            uuid: undefined,\n            type: 'anytls',\n            name,\n            server,\n            port,\n            password,\n        };\n\n        for (const addon of addons.split('&')) {\n            if (addon) {\n                let [key, value] = addon.split('=');\n                key = key.replace(/_/g, '-');\n                value = decodeURIComponent(value);\n                if (['alpn'].includes(key)) {\n                    proxy[key] = value ? value.split(',') : undefined;\n                } else if (['insecure'].includes(key)) {\n                    proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);\n                } else if (['udp'].includes(key)) {\n                    proxy[key] = /(TRUE)|1/i.test(value);\n                } else if (!Object.keys(proxy).includes(key)) {\n                    proxy[key] = value;\n                }\n            }\n        }\n        if (['tcp'].includes(proxy.network) && !proxy['reality-opts']) {\n            delete proxy.network;\n            delete proxy.security;\n        }\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction URI_Hysteria2() {\n    const name = 'URI Hysteria2 Parser';\n    const test = (line) => {\n        return /^(hysteria2|hy2):\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = line.split(/(hysteria2|hy2):\\/\\//)[2];\n        // 端口跳跃有两种写法:\n        // 1. 服务器的地址和可选端口。如果省略端口，则默认为 443。\n        // 端口部分支持 端口跳跃 的「多端口地址格式」。\n        // https://hysteria.network/zh/docs/advanced/Port-Hopping\n        // 2. 参数 mport\n        let ports;\n        /* eslint-disable no-unused-vars */\n        let [\n            __,\n            password,\n            server,\n            ___,\n            port,\n            ____,\n            _____,\n            ______,\n            _______,\n            ________,\n            addons = '',\n            name,\n        ] = /^(.*?)@(.*?)(:((\\d+(-\\d+)?)([,;]\\d+(-\\d+)?)*))?\\/?(\\?(.*?))?(?:#(.*?))?$/.exec(\n            line,\n        );\n\n        /* eslint-enable no-unused-vars */\n        if (/^\\d+$/.test(port)) {\n            port = parseInt(`${port}`, 10);\n            if (isNaN(port)) {\n                port = 443;\n            }\n        } else if (port) {\n            ports = port;\n            port = getRandomPort(ports);\n        } else {\n            port = 443;\n        }\n\n        password = decodeURIComponent(password);\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n        name = name ?? `Hysteria2 ${server}:${port}`;\n\n        const proxy = {\n            type: 'hysteria2',\n            name,\n            server,\n            port,\n            ports,\n            password,\n        };\n\n        const params = {};\n        for (const addon of addons.split('&')) {\n            if (addon) {\n                const [key, valueRaw] = addon.split('=');\n                let value = valueRaw;\n                value = decodeURIComponent(valueRaw);\n                params[key] = value;\n            }\n        }\n\n        proxy.sni = params.sni;\n        if (!proxy.sni && params.peer) {\n            proxy.sni = params.peer;\n        }\n        if (params.obfs && params.obfs !== 'none') {\n            proxy.obfs = params.obfs;\n        }\n        if (params.mport) {\n            proxy.ports = params.mport;\n        }\n        proxy['obfs-password'] = params['obfs-password'];\n        proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);\n        proxy.tfo = /(TRUE)|1/i.test(params.fastopen);\n        proxy['tls-fingerprint'] = params.pinSHA256;\n        let hop_interval = params['hop-interval'] || params['hop_interval'];\n\n        if (/^\\d+$/.test(hop_interval)) {\n            proxy['hop-interval'] = parseInt(`${hop_interval}`, 10);\n        }\n        let keepalive = params['keepalive'];\n\n        if (/^\\d+$/.test(keepalive)) {\n            proxy['keepalive'] = parseInt(`${keepalive}`, 10);\n        }\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction URI_Hysteria() {\n    const name = 'URI Hysteria Parser';\n    const test = (line) => {\n        return /^(hysteria|hy):\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = line.split(/(hysteria|hy):\\/\\//)[2];\n        // eslint-disable-next-line no-unused-vars\n        let [__, server, ___, port, ____, addons = '', name] =\n            /^(.*?)(:(\\d+))?\\/?(\\?(.*?))?(?:#(.*?))?$/.exec(line);\n        port = parseInt(`${port}`, 10);\n        if (isNaN(port)) {\n            port = 443;\n        }\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n        name = name ?? `Hysteria ${server}:${port}`;\n\n        const proxy = {\n            type: 'hysteria',\n            name,\n            server,\n            port,\n        };\n        const params = {};\n        for (const addon of addons.split('&')) {\n            if (addon) {\n                let [key, value] = addon.split('=');\n                key = key.replace(/_/, '-');\n                value = decodeURIComponent(value);\n                if (['alpn'].includes(key)) {\n                    proxy[key] = value ? value.split(',') : undefined;\n                } else if (['insecure'].includes(key)) {\n                    proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);\n                } else if (['auth'].includes(key)) {\n                    proxy['auth-str'] = value;\n                } else if (['mport'].includes(key)) {\n                    proxy['ports'] = value;\n                } else if (['obfsParam'].includes(key)) {\n                    proxy['obfs'] = value;\n                } else if (['upmbps'].includes(key)) {\n                    proxy['up'] = value;\n                } else if (['downmbps'].includes(key)) {\n                    proxy['down'] = value;\n                } else if (['obfs'].includes(key)) {\n                    // obfs: Obfuscation mode (optional, empty or \"xplus\")\n                    proxy['_obfs'] = value || '';\n                } else if (['fast-open', 'peer'].includes(key)) {\n                    params[key] = value;\n                } else if (!Object.keys(proxy).includes(key)) {\n                    proxy[key] = value;\n                }\n            }\n        }\n\n        if (!proxy.sni && params.peer) {\n            proxy.sni = params.peer;\n        }\n        if (!proxy['fast-open'] && params.fastopen) {\n            proxy['fast-open'] = true;\n        }\n        if (!proxy.protocol) {\n            // protocol: protocol to use (\"udp\", \"wechat-video\", \"faketcp\") (optional, default: \"udp\")\n            proxy.protocol = 'udp';\n        }\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction URI_TUIC() {\n    const name = 'URI TUIC Parser';\n    const test = (line) => {\n        return /^tuic:\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = line.split(/tuic:\\/\\//)[1];\n        // eslint-disable-next-line no-unused-vars\n        let [__, auth, server, port, addons = '', name] =\n            /^(.*?)@(.*?)(?::(\\d+))?\\/?(?:\\?(.*?))?(?:#(.*?))?$/.exec(line);\n        auth = decodeURIComponent(auth);\n        let [uuid, ...passwordParts] = auth.split(':');\n        let password = passwordParts.join(':');\n        port = parseInt(`${port}`, 10);\n        if (isNaN(port)) {\n            port = 443;\n        }\n        password = decodeURIComponent(password);\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n        name = name ?? `TUIC ${server}:${port}`;\n\n        const proxy = {\n            type: 'tuic',\n            name,\n            server,\n            port,\n            password,\n            uuid,\n        };\n\n        for (const addon of addons.split('&')) {\n            if (addon) {\n                let [key, value] = addon.split('=');\n                key = key.replace(/_/g, '-');\n                value = decodeURIComponent(value);\n                if (['alpn'].includes(key)) {\n                    proxy[key] = value ? value.split(',') : undefined;\n                } else if (['allow-insecure', 'insecure'].includes(key)) {\n                    proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);\n                } else if (['fast-open'].includes(key)) {\n                    proxy.tfo = true;\n                } else if (['disable-sni', 'reduce-rtt'].includes(key)) {\n                    proxy[key] = /(TRUE)|1/i.test(value);\n                } else if (key === 'congestion-control') {\n                    proxy['congestion-controller'] = value;\n                    delete proxy[key];\n                } else if (!Object.keys(proxy).includes(key)) {\n                    proxy[key] = value;\n                }\n            }\n        }\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction URI_WireGuard() {\n    const name = 'URI WireGuard Parser';\n    const test = (line) => {\n        return /^(wireguard|wg):\\/\\//.test(line);\n    };\n    const parse = (line) => {\n        line = line.split(/(wireguard|wg):\\/\\//)[2];\n        /* eslint-disable no-unused-vars */\n        let [\n            __,\n            ___,\n            privateKey,\n            server,\n            ____,\n            port,\n            _____,\n            addons = '',\n            name,\n        ] = /^((.*?)@)?(.*?)(:(\\d+))?\\/?(\\?(.*?))?(?:#(.*?))?$/.exec(line);\n        /* eslint-enable no-unused-vars */\n\n        port = parseInt(`${port}`, 10);\n        if (isNaN(port)) {\n            port = 51820;\n        }\n        privateKey = decodeURIComponent(privateKey);\n        if (name != null) {\n            name = decodeURIComponent(name);\n        }\n        name = name ?? `WireGuard ${server}:${port}`;\n        const proxy = {\n            type: 'wireguard',\n            name,\n            server,\n            port,\n            'private-key': privateKey,\n            udp: true,\n        };\n        for (const addon of addons.split('&')) {\n            if (addon) {\n                let [key, value] = addon.split('=');\n                key = key.replace(/_/, '-');\n                value = decodeURIComponent(value);\n                if (['reserved'].includes(key)) {\n                    const parsed = value\n                        .split(',')\n                        .map((i) => parseInt(i.trim(), 10))\n                        .filter((i) => Number.isInteger(i));\n                    if (parsed.length === 3) {\n                        proxy[key] = parsed;\n                    }\n                } else if (['address', 'ip'].includes(key)) {\n                    value.split(',').map((i) => {\n                        const ip = i\n                            .trim()\n                            .replace(/\\/\\d+$/, '')\n                            .replace(/^\\[/, '')\n                            .replace(/\\]$/, '');\n                        if (isIPv4(ip)) {\n                            proxy.ip = ip;\n                        } else if (isIPv6(ip)) {\n                            proxy.ipv6 = ip;\n                        }\n                    });\n                } else if (['mtu'].includes(key)) {\n                    const parsed = parseInt(value.trim(), 10);\n                    if (Number.isInteger(parsed)) {\n                        proxy[key] = parsed;\n                    }\n                } else if (/publickey/i.test(key)) {\n                    proxy['public-key'] = value;\n                } else if (/privatekey/i.test(key)) {\n                    proxy['private-key'] = value;\n                } else if (['udp'].includes(key)) {\n                    proxy[key] = /(TRUE)|1/i.test(value);\n                } else if (![...Object.keys(proxy), 'flag'].includes(key)) {\n                    proxy[key] = value;\n                }\n            }\n        }\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\n// Trojan URI format\nfunction URI_Trojan() {\n    const name = 'URI Trojan Parser';\n    const test = (line) => {\n        return /^trojan:\\/\\//.test(line);\n    };\n\n    const parse = (line) => {\n        const matched = /^(trojan:\\/\\/.*?@.*?)(:(\\d+))?\\/?(\\?.*?)?$/.exec(line);\n        const port = matched?.[2];\n        if (!port) {\n            line = line.replace(matched[1], `${matched[1]}:443`);\n        }\n        let [newLine, name] = line.split(/#(.+)/, 2);\n        const parser = getTrojanURIParser();\n        const proxy = parser.parse(newLine);\n        if (isNotBlank(name)) {\n            try {\n                proxy.name = decodeURIComponent(name);\n            } catch (e) {\n                console.log(e);\n            }\n        }\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\nfunction Clash_All() {\n    const name = 'Clash Parser';\n    const test = (line) => {\n        let proxy;\n        try {\n            proxy = JSON5.parse(line);\n        } catch (e) {\n            proxy = YAML.parse(line);\n        }\n        return !!proxy?.type;\n    };\n    const parse = (line) => {\n        let proxy;\n        try {\n            proxy = JSON5.parse(line);\n        } catch (e) {\n            proxy = YAML.parse(line);\n        }\n        if (\n            ![\n                'trusttunnel',\n                'naive',\n                'anytls',\n                'mieru',\n                'masque',\n                'sudoku',\n                'juicity',\n                'ss',\n                'ssr',\n                'vmess',\n                'socks5',\n                'http',\n                'snell',\n                'trojan',\n                'tuic',\n                'vless',\n                'hysteria',\n                'hysteria2',\n                'wireguard',\n                'ssh',\n                'direct',\n            ].includes(proxy.type)\n        ) {\n            throw new Error(\n                `Clash does not support proxy with type: ${proxy.type}`,\n            );\n        }\n\n        // handle vmess sni\n        if (['vmess', 'vless'].includes(proxy.type) && proxy.servername) {\n            proxy.sni = proxy.servername;\n            delete proxy.servername;\n        }\n        if (proxy['server-cert-fingerprint']) {\n            proxy['tls-fingerprint'] = proxy['server-cert-fingerprint'];\n        }\n        if (proxy.fingerprint) {\n            proxy['tls-fingerprint'] = proxy.fingerprint;\n        }\n        if (proxy['dialer-proxy']) {\n            proxy['underlying-proxy'] = proxy['dialer-proxy'];\n        }\n\n        if (proxy['benchmark-url']) {\n            proxy['test-url'] = proxy['benchmark-url'];\n        }\n        if (proxy['benchmark-timeout']) {\n            proxy['test-timeout'] = proxy['benchmark-timeout'];\n        }\n\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\nfunction QX_SS() {\n    const name = 'QX SS Parser';\n    const test = (line) => {\n        return (\n            /^shadowsocks\\s*=/.test(line.split(',')[0].trim()) &&\n            line.indexOf('ssr-protocol') === -1\n        );\n    };\n    const parse = (line) => {\n        const parser = getQXParser();\n        return parser.parse(line);\n    };\n    return { name, test, parse };\n}\n\nfunction QX_SSR() {\n    const name = 'QX SSR Parser';\n    const test = (line) => {\n        return (\n            /^shadowsocks\\s*=/.test(line.split(',')[0].trim()) &&\n            line.indexOf('ssr-protocol') !== -1\n        );\n    };\n    const parse = (line) => getQXParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction QX_VMess() {\n    const name = 'QX VMess Parser';\n    const test = (line) => {\n        return /^vmess\\s*=/.test(line.split(',')[0].trim());\n    };\n    const parse = (line) => getQXParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction QX_VLESS() {\n    const name = 'QX VLESS Parser';\n    const test = (line) => {\n        return /^vless\\s*=/.test(line.split(',')[0].trim());\n    };\n    const parse = (line) => getQXParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction QX_Trojan() {\n    const name = 'QX Trojan Parser';\n    const test = (line) => {\n        return /^trojan\\s*=/.test(line.split(',')[0].trim());\n    };\n    const parse = (line) => getQXParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction QX_Http() {\n    const name = 'QX HTTP Parser';\n    const test = (line) => {\n        return /^http\\s*=/.test(line.split(',')[0].trim());\n    };\n    const parse = (line) => getQXParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction QX_Socks5() {\n    const name = 'QX Socks5 Parser';\n    const test = (line) => {\n        return /^socks5\\s*=/.test(line.split(',')[0].trim());\n    };\n    const parse = (line) => getQXParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_SS() {\n    const name = 'Loon SS Parser';\n    const test = (line) => {\n        return (\n            line.split(',')[0].split('=')[1].trim().toLowerCase() ===\n            'shadowsocks'\n        );\n    };\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_SSR() {\n    const name = 'Loon SSR Parser';\n    const test = (line) => {\n        return (\n            line.split(',')[0].split('=')[1].trim().toLowerCase() ===\n            'shadowsocksr'\n        );\n    };\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_VMess() {\n    const name = 'Loon VMess Parser';\n    const test = (line) => {\n        // distinguish between surge vmess\n        return (\n            /^.*=\\s*vmess/i.test(line.split(',')[0]) &&\n            line.indexOf('username') === -1\n        );\n    };\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_Vless() {\n    const name = 'Loon Vless Parser';\n    const test = (line) => {\n        return /^.*=\\s*vless/i.test(line.split(',')[0]);\n    };\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_Trojan() {\n    const name = 'Loon Trojan Parser';\n    const test = (line) => {\n        return /^.*=\\s*trojan/i.test(line.split(',')[0]);\n    };\n\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\nfunction Loon_AnyTLS() {\n    const name = 'Loon AnyTLS Parser';\n    const test = (line) => {\n        return /^.*=\\s*anytls/i.test(line.split(',')[0]);\n    };\n\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\nfunction Loon_Hysteria2() {\n    const name = 'Loon Hysteria2 Parser';\n    const test = (line) => {\n        return /^.*=\\s*Hysteria2/i.test(line.split(',')[0]);\n    };\n\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_Http() {\n    const name = 'Loon HTTP Parser';\n    const test = (line) => {\n        return /^.*=\\s*http/i.test(line.split(',')[0]);\n    };\n\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\nfunction Loon_Socks5() {\n    const name = 'Loon SOCKS5 Parser';\n    const test = (line) => {\n        return /^.*=\\s*socks5/i.test(line.split(',')[0]);\n    };\n\n    const parse = (line) => getLoonParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Loon_WireGuard() {\n    const name = 'Loon WireGuard Parser';\n    const test = (line) => {\n        return /^.*=\\s*wireguard/i.test(line.split(',')[0]);\n    };\n\n    const parse = (line) => {\n        const name = line.match(\n            /(^.*?)\\s*?=\\s*?wireguard\\s*?,.+?\\s*?=\\s*?.+?/i,\n        )?.[1];\n        line = line.replace(name, '').replace(/^\\s*?=\\s*?wireguard\\s*/i, '');\n        let peers = line.match(\n            /,\\s*?peers\\s*?=\\s*?\\[\\s*?\\{\\s*?(.+?)\\s*?\\}\\s*?\\]/i,\n        )?.[1];\n        let serverPort = peers.match(\n            /(,|^)\\s*?endpoint\\s*?=\\s*?\"?(.+?):(\\d+)\"?\\s*?(,|$)/i,\n        );\n        let server = serverPort?.[2];\n        let port = parseInt(serverPort?.[3], 10);\n        let mtu = line.match(/(,|^)\\s*?mtu\\s*?=\\s*?\"?(\\d+?)\"?\\s*?(,|$)/i)?.[2];\n        if (mtu) {\n            mtu = parseInt(mtu, 10);\n        }\n        let keepalive = line.match(\n            /(,|^)\\s*?keepalive\\s*?=\\s*?\"?(\\d+?)\"?\\s*?(,|$)/i,\n        )?.[2];\n        if (keepalive) {\n            keepalive = parseInt(keepalive, 10);\n        }\n        let reserved = peers.match(\n            /(,|^)\\s*?reserved\\s*?=\\s*?\"?(\\[\\s*?.+?\\s*?\\])\"?\\s*?(,|$)/i,\n        )?.[2];\n        if (reserved) {\n            reserved = JSON.parse(reserved);\n        }\n\n        let dns;\n        let dnsv4 = line.match(/(,|^)\\s*?dns\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i)?.[2];\n        let dnsv6 = line.match(\n            /(,|^)\\s*?dnsv6\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i,\n        )?.[2];\n        if (dnsv4 || dnsv6) {\n            dns = [];\n            if (dnsv4) {\n                dns.push(dnsv4);\n            }\n            if (dnsv6) {\n                dns.push(dnsv6);\n            }\n        }\n        let allowedIps = peers\n            .match(/(,|^)\\s*?allowed-ips\\s*?=\\s*?\"(.+?)\"\\s*?(,|$)/i)?.[2]\n            ?.split(',')\n            .map((i) => i.trim());\n        let preSharedKey = peers.match(\n            /(,|^)\\s*?preshared-key\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i,\n        )?.[2];\n        let ip = line.match(\n            /(,|^)\\s*?interface-ip\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i,\n        )?.[2];\n        let ipv6 = line.match(\n            /(,|^)\\s*?interface-ipv6\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i,\n        )?.[2];\n        let publicKey = peers.match(\n            /(,|^)\\s*?public-key\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i,\n        )?.[2];\n        // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717\n        const proxy = {\n            type: 'wireguard',\n            name,\n            server,\n            port,\n            ip,\n            ipv6,\n            'private-key': line.match(\n                /(,|^)\\s*?private-key\\s*?=\\s*?\"?(.+?)\"?\\s*?(,|$)/i,\n            )?.[2],\n            'public-key': publicKey,\n            mtu,\n            keepalive,\n            reserved,\n            'allowed-ips': allowedIps,\n            'preshared-key': preSharedKey,\n            dns,\n            udp: true,\n            peers: [\n                {\n                    server,\n                    port,\n                    ip,\n                    ipv6,\n                    'public-key': publicKey,\n                    'pre-shared-key': preSharedKey,\n                    'allowed-ips': allowedIps,\n                    reserved,\n                },\n            ],\n        };\n\n        proxy;\n        if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {\n            proxy['remote-dns-resolve'] = true;\n        }\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\nfunction Surge_Direct() {\n    const name = 'Surge Direct Parser';\n    const test = (line) => {\n        return /^.*=\\s*direct/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\nfunction Surge_AnyTLS() {\n    const name = 'Surge AnyTLS Parser';\n    const test = (line) => {\n        return /^.*=\\s*anytls/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\nfunction Surge_TrustTunnel() {\n    const name = 'Surge TrustTunnel Parser';\n    const test = (line) => {\n        return /^.*=\\s*trust-tunnel/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\nfunction Surge_SSH() {\n    const name = 'Surge SSH Parser';\n    const test = (line) => {\n        return /^.*=\\s*ssh/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\nfunction Surge_SS() {\n    const name = 'Surge SS Parser';\n    const test = (line) => {\n        return /^.*=\\s*ss/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_VMess() {\n    const name = 'Surge VMess Parser';\n    const test = (line) => {\n        return (\n            /^.*=\\s*vmess/.test(line.split(',')[0]) &&\n            line.indexOf('username') !== -1\n        );\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_Trojan() {\n    const name = 'Surge Trojan Parser';\n    const test = (line) => {\n        return /^.*=\\s*trojan/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_Http() {\n    const name = 'Surge HTTP Parser';\n    const test = (line) => {\n        return /^.*=\\s*https?/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_Socks5() {\n    const name = 'Surge Socks5 Parser';\n    const test = (line) => {\n        return /^.*=\\s*socks5(-tls)?/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_External() {\n    const name = 'Surge External Parser';\n    const test = (line) => {\n        return /^.*=\\s*external/.test(line.split(',')[0]);\n    };\n    const parse = (line) => {\n        let parsed = /^\\s*(.*?)\\s*?=\\s*?external\\s*?,\\s*(.*?)\\s*$/.exec(line);\n\n        // eslint-disable-next-line no-unused-vars\n        let [_, name, other] = parsed;\n        line = other;\n\n        // exec = \"/usr/bin/ssh\" 或 exec = /usr/bin/ssh\n        let exec = /(,|^)\\s*?exec\\s*?=\\s*\"(.*?)\"\\s*?(,|$)/.exec(line)?.[2];\n        if (!exec) {\n            exec = /(,|^)\\s*?exec\\s*?=\\s*(.*?)\\s*?(,|$)/.exec(line)?.[2];\n        }\n\n        // local-port = \"1080\" 或 local-port = 1080\n        let localPort = /(,|^)\\s*?local-port\\s*?=\\s*\"(.*?)\"\\s*?(,|$)/.exec(\n            line,\n        )?.[2];\n        if (!localPort) {\n            localPort = /(,|^)\\s*?local-port\\s*?=\\s*(.*?)\\s*?(,|$)/.exec(\n                line,\n            )?.[2];\n        }\n        // args = \"-m\", args = \"rc4-md5\"\n        // args = -m, args = rc4-md5\n        const argsRegex = /(,|^)\\s*?args\\s*?=\\s*(\"(.*?)\"|(.*?))(?=\\s*?(,|$))/g;\n        let argsMatch;\n        const args = [];\n        while ((argsMatch = argsRegex.exec(line)) !== null) {\n            if (argsMatch[3] != null) {\n                args.push(argsMatch[3]);\n            } else if (argsMatch[4] != null) {\n                args.push(argsMatch[4]);\n            }\n        }\n        // addresses = \"[ipv6]\",,addresses = \"ipv6\", addresses = \"ipv4\"\n        // addresses = [ipv6], addresses = ipv6, addresses = ipv4\n        const addressesRegex =\n            /(,|^)\\s*?addresses\\s*?=\\s*(\"(.*?)\"|(.*?))(?=\\s*?(,|$))/g;\n        let addressesMatch;\n        const addresses = [];\n        while ((addressesMatch = addressesRegex.exec(line)) !== null) {\n            let ip;\n            if (addressesMatch[3] != null) {\n                ip = addressesMatch[3];\n            } else if (addressesMatch[4] != null) {\n                ip = addressesMatch[4];\n            }\n            if (ip != null) {\n                ip = `${ip}`.trim().replace(/^\\[/, '').replace(/\\]$/, '');\n            }\n            if (isIP(ip)) {\n                addresses.push(ip);\n            }\n        }\n\n        const proxy = {\n            type: 'external',\n            name,\n            exec,\n            'local-port': localPort,\n            args,\n            addresses,\n        };\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\nfunction Surge_Snell() {\n    const name = 'Surge Snell Parser';\n    const test = (line) => {\n        return /^.*=\\s*snell/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_Tuic() {\n    const name = 'Surge Tuic Parser';\n    const test = (line) => {\n        return /^.*=\\s*tuic(-v5)?/.test(line.split(',')[0]);\n    };\n    const parse = (raw) => {\n        const { port_hopping, line } = surge_port_hopping(raw);\n        const proxy = getSurgeParser().parse(line);\n        proxy['ports'] = port_hopping;\n        return proxy;\n    };\n    return { name, test, parse };\n}\nfunction Surge_WireGuard() {\n    const name = 'Surge WireGuard Parser';\n    const test = (line) => {\n        return /^.*=\\s*wireguard/.test(line.split(',')[0]);\n    };\n    const parse = (line) => getSurgeParser().parse(line);\n    return { name, test, parse };\n}\n\nfunction Surge_Hysteria2() {\n    const name = 'Surge Hysteria2 Parser';\n    const test = (line) => {\n        return /^.*=\\s*hysteria2/.test(line.split(',')[0]);\n    };\n    const parse = (raw) => {\n        const { port_hopping, line } = surge_port_hopping(raw);\n        const proxy = getSurgeParser().parse(line);\n        proxy['ports'] = port_hopping;\n        return proxy;\n    };\n    return { name, test, parse };\n}\n\nfunction isIP(ip) {\n    return isIPv4(ip) || isIPv6(ip);\n}\n\nexport default [\n    URI_PROXY(),\n    URI_SOCKS(),\n    URI_SS(),\n    URI_SSR(),\n    URI_VMess(),\n    URI_VLESS(),\n    URI_TUIC(),\n    URI_WireGuard(),\n    URI_Hysteria(),\n    URI_Hysteria2(),\n    URI_Trojan(),\n    URI_AnyTLS(),\n    Clash_All(),\n    Surge_Direct(),\n    Surge_AnyTLS(),\n    Surge_TrustTunnel(),\n    Surge_SSH(),\n    Surge_SS(),\n    Surge_VMess(),\n    Surge_Trojan(),\n    Surge_Http(),\n    Surge_Snell(),\n    Surge_Tuic(),\n    Surge_WireGuard(),\n    Surge_Hysteria2(),\n    Surge_Socks5(),\n    Surge_External(),\n    Loon_SS(),\n    Loon_SSR(),\n    Loon_VMess(),\n    Loon_Vless(),\n    Loon_Hysteria2(),\n    Loon_Trojan(),\n    Loon_AnyTLS(),\n    Loon_Http(),\n    Loon_Socks5(),\n    Loon_WireGuard(),\n    QX_SS(),\n    QX_SSR(),\n    QX_VMess(),\n    QX_VLESS(),\n    QX_Trojan(),\n    QX_Http(),\n    QX_Socks5(),\n];\n"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/loon.js",
    "content": "import * as peggy from 'peggy';\nconst grammars = String.raw`\n// global initializer\n{{\n    function $set(obj, path, value) {\n      if (Object(obj) !== obj) return obj;\n      if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n      path\n        .slice(0, -1)\n        .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n        path[path.length - 1]\n      ] = value;\n      return obj;\n    }\n}}\n\n// per-parser initializer\n{\n    const proxy = {};\n    const obfs = {};\n    const transport = {};\n    const $ = {};\n\n    function handleTransport() {\n        if (transport.type === \"tcp\") { /* do nothing */ }\n        else if (transport.type === \"ws\") {\n            proxy.network = \"ws\";\n            $set(proxy, \"ws-opts.path\", transport.path);\n            $set(proxy, \"ws-opts.headers.Host\", transport.host);\n        } else if (transport.type === \"http\") {\n            proxy.network = \"http\";\n            $set(proxy, \"http-opts.path\", transport.path);\n            $set(proxy, \"http-opts.headers.Host\", transport.host);\n        }\n    }\n}\n\nstart = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2/anytls) {\n    return proxy;\n}\n\nshadowsocksr = tag equals \"shadowsocksr\"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{\n    proxy.type = \"ssr\";\n    // handle ssr obfs\n    proxy.obfs = obfs.type;\n}\nshadowsocks = tag equals \"shadowsocks\"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/udp_over_tcp/others)* {\n    proxy.type = \"ss\";\n    // handle ss obfs\n    if (obfs.type == \"http\" || obfs.type === \"tls\") {\n        proxy.plugin = \"obfs\";\n        $set(proxy, \"plugin-opts.mode\", obfs.type);\n        $set(proxy, \"plugin-opts.host\", obfs.host);\n        $set(proxy, \"plugin-opts.path\", obfs.path);\n    }\n}\nvmess = tag equals \"vmess\"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {\n    proxy.type = \"vmess\";\n    proxy.cipher = proxy.cipher || \"none\";\n    proxy.alterId = proxy.alterId || 0;\n    handleTransport();\n}\nvless = tag equals \"vless\"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {\n    proxy.type = \"vless\";\n    handleTransport();\n}\ntrojan = tag equals \"trojan\"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"trojan\";\n    handleTransport();\n}\nanytls = tag equals \"anytls\"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/idle_session_check_interval/idle_session_timeout/min_idle_session/max_stream_count/others)* {\n    proxy.type = \"anytls\";\n    handleTransport();\n}\nhysteria2 = tag equals \"hysteria2\"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {\n    proxy.type = \"hysteria2\";\n}\nhttps = tag equals \"https\"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"http\";\n    proxy.tls = true;\n}\nhttp = tag equals \"http\"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"http\";\n}\nsocks5 = tag equals \"socks5\"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"socks5\";\n}\n\naddress = comma server:server comma port:port {\n    proxy.server = server;\n    proxy.port = port;\n}\n\nserver = ip/domain\n\nip = & {\n    const start = peg$currPos;\n    let j = start;\n    while (j < input.length) {\n        if (input[j] === \",\") break;\n        j++;\n    }\n    peg$currPos = j;\n    $.ip = input.substring(start, j).trim();\n    return true;\n} { return $.ip; }\n\ndomain = match:[0-9a-zA-z-_.]+ { \n    const domain = match.join(\"\"); \n    if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n        return domain;\n    }\n    throw new Error(\"Invalid domain: \" + domain);\n}\n\nport = digits:[0-9]+ { \n    const port = parseInt(digits.join(\"\"), 10); \n    if (port >= 0 && port <= 65535) {\n    \treturn port;\n    }\n    throw new Error(\"Invalid port number: \" + port);\n}\n\nmethod = comma cipher:cipher { \n    proxy.cipher = cipher;\n}\ncipher = (\"aes-128-cfb\"/\"aes-128-ctr\"/\"aes-128-gcm\"/\"aes-192-cfb\"/\"aes-192-ctr\"/\"aes-192-gcm\"/\"aes-256-cfb\"/\"aes-256-ctr\"/\"aes-256-gcm\"/\"auto\"/\"bf-cfb\"/\"camellia-128-cfb\"/\"camellia-192-cfb\"/\"camellia-256-cfb\"/\"chacha20-ietf-poly1305\"/\"chacha20-ietf\"/\"chacha20-poly1305\"/\"chacha20\"/\"none\"/\"rc4-md5\"/\"rc4\"/\"salsa20\"/\"xchacha20-ietf-poly1305\"/\"2022-blake3-aes-128-gcm\"/\"2022-blake3-aes-256-gcm\");\n\nusername = & {\n    let j = peg$currPos; \n    let start, end;\n    let first = true;\n    while (j < input.length) {\n        if (input[j] === ',') {\n            if (first) {\n                start = j + 1;\n                first = false;\n            } else {\n                end = j;\n                break;\n            }\n        }\n        j++;\n    }\n    const match = input.substring(start, end);\n    if (match.indexOf(\"=\") === -1) {\n        $.username = match;\n        peg$currPos = end;\n        return true;\n    }\n} { proxy.username = $.username; }\npassword = comma '\"' match:[^\"]* '\"' { proxy.password = match.join(\"\"); }\nuuid = comma '\"' match:[^\"]+ '\"' { proxy.uuid = match.join(\"\"); }\n\nobfs_typev = comma type:(\"http\"/\"tls\") { obfs.type = type; }\nobfs_hostv = comma match:[^,]+ { obfs.host = match.join(\"\"); }\n\nobfs_ss = comma \"obfs-name\" equals type:(\"http\"/\"tls\") { obfs.type = type; }\n\nobfs_ssr = comma \"obfs\" equals type:(\"plain\"/\"http_simple\"/\"http_post\"/\"random_head\"/\"tls1.2_ticket_auth\"/\"tls1.2_ticket_fastauth\") { obfs.type = type; }\nobfs_ssr_param = comma \"obfs-param\" equals match:$[^,]+ { proxy[\"obfs-param\"] = match; }\n\nobfs_host = comma \"obfs-host\" equals match:[^,]+ { obfs.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nobfs_uri = comma \"obfs-uri\" equals uri:uri { obfs.path = uri; }\nuri = $[^,]+\n\ntransport = comma \"transport\" equals type:(\"tcp\"/\"ws\"/\"http\") { transport.type = type; }\ntransport_host = comma \"host\" equals match:[^,]+ { transport.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntransport_path = comma \"path\" equals path:uri { transport.path = path; }\n\nssr_protocol = comma \"protocol\" equals protocol:(\"origin\"/\"auth_sha1_v4\"/\"auth_aes128_md5\"/\"auth_aes128_sha1\"/\"auth_chain_a\"/\"auth_chain_b\") { proxy.protocol = protocol; }\nssr_protocol_param = comma \"protocol-param\" equals param:$[^=,]+ { proxy[\"protocol-param\"] = param; }\n\nvmess_alterId = comma \"alterId\" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } \n\nudp_port = comma \"udp-port\" equals match:$[0-9]+ { proxy[\"udp-port\"] = parseInt(match.trim()); }\nshadow_tls_version = comma \"shadow-tls-version\" equals match:$[0-9]+ { proxy[\"shadow-tls-version\"] = parseInt(match.trim()); }\nshadow_tls_sni = comma \"shadow-tls-sni\" equals match:[^,]+ { proxy[\"shadow-tls-sni\"] = match.join(\"\"); }\nshadow_tls_password = comma \"shadow-tls-password\" equals match:[^,]+ { proxy[\"shadow-tls-password\"] = match.join(\"\"); }\n\nover_tls = comma \"over-tls\" equals flag:bool { proxy.tls = flag; }\ntls_name = comma sni:(\"tls-name\") equals match:[^,]+ { proxy.sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nsni = comma \"sni\" equals match:[^,]+ { proxy.sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntls_verification = comma \"skip-cert-verify\" equals flag:bool { proxy[\"skip-cert-verify\"] = flag; }\ntls_cert_sha256 = comma \"tls-cert-sha256\" equals match:[^,]+ { proxy[\"tls-fingerprint\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntls_pubkey_sha256 = comma \"tls-pubkey-sha256\" equals match:[^,]+ { proxy[\"tls-pubkey-sha256\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\n\nflow = comma \"flow\" equals match:[^,]+ { proxy[\"flow\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\npublic_key = comma \"public-key\" equals match:[^,]+ { proxy[\"reality-opts\"] = proxy[\"reality-opts\"] || {}; proxy[\"reality-opts\"][\"public-key\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nshort_id = comma \"short-id\" equals match:[^,]+ { proxy[\"reality-opts\"] = proxy[\"reality-opts\"] || {}; proxy[\"reality-opts\"][\"short-id\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\n\nfast_open = comma \"fast-open\" equals flag:bool { proxy.tfo = flag; }\nudp_relay = comma \"udp\" equals flag:bool { proxy.udp = flag; }\nip_mode = comma \"ip-mode\" equals match:[^,]+ { proxy[\"ip-version\"] = match.join(\"\"); }\n\necn = comma \"ecn\" equals flag:bool { proxy.ecn = flag; }\ndownload_bandwidth = comma \"download-bandwidth\" equals match:[^,]+ { proxy.down = match.join(\"\"); }\nsalamander_password = comma \"salamander-password\" equals match:[^,]+ { proxy['obfs-password'] = match.join(\"\"); proxy.obfs = 'salamander'; }\n\nblock_quic = comma \"block-quic\" equals flag:bool { if(flag) proxy[\"block-quic\"] = \"on\"; else proxy[\"block-quic\"] = \"off\"; }\n\nidle_session_check_interval = comma \"idle-session-check-interval\" equals match:$[0-9]+ { proxy[\"idle-session-check-interval\"] = parseInt(match.trim()); }\nidle_session_timeout = comma \"idle-session-timeout\" equals match:$[0-9]+ { proxy[\"idle-session-timeout\"] = parseInt(match.trim()); }\nmin_idle_session = comma \"min-idle-session\" equals match:$[0-9]+ { proxy[\"min-idle-session\"] = parseInt(match.trim()); }\nmax_stream_count = comma \"max-stream-count\" equals match:$[0-9]+ { proxy[\"max-stream-count\"] = parseInt(match.trim()); }\n\nudp_over_tcp = comma \"udp-over-tcp\" equals flag:bool { proxy[\"udp-over-tcp\"] = true; proxy[\"udp-over-tcp-version\"] = 2; }\n\ntag = match:[^=,]* { proxy.name = match.join(\"\").trim(); }\ncomma = _ \",\" _\nequals = _ \"=\" _\n_ = [ \\r\\t]*\nbool = b:(\"true\"/\"false\") { return b === \"true\" }\nothers = comma [^=,]+ equals [^=,]+\n`;\nlet parser;\nexport default function getParser() {\n    if (!parser) {\n        parser = peggy.generate(grammars);\n    }\n    return parser;\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/loon.peg",
    "content": "// global initializer\n{{\n    function $set(obj, path, value) {\n      if (Object(obj) !== obj) return obj;\n      if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n      path\n        .slice(0, -1)\n        .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n        path[path.length - 1]\n      ] = value;\n      return obj;\n    }\n}}\n\n// per-parser initializer\n{\n    const proxy = {};\n    const obfs = {};\n    const transport = {};\n    const $ = {};\n\n    function handleTransport() {\n        if (transport.type === \"tcp\") { /* do nothing */ }\n        else if (transport.type === \"ws\") {\n            proxy.network = \"ws\";\n            $set(proxy, \"ws-opts.path\", transport.path);\n            $set(proxy, \"ws-opts.headers.Host\", transport.host);\n        } else if (transport.type === \"http\") {\n            proxy.network = \"http\";\n            $set(proxy, \"http-opts.path\", transport.path);\n            $set(proxy, \"http-opts.headers.Host\", transport.host);\n        }\n    }\n}\n\nstart = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2/anytls) {\n    return proxy;\n}\n\nshadowsocksr = tag equals \"shadowsocksr\"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/udp_over_tcp/others)*{\n    proxy.type = \"ssr\";\n    // handle ssr obfs\n    proxy.obfs = obfs.type;\n}\nshadowsocks = tag equals \"shadowsocks\"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)* {\n    proxy.type = \"ss\";\n    // handle ss obfs\n    if (obfs.type == \"http\" || obfs.type === \"tls\") {\n        proxy.plugin = \"obfs\";\n        $set(proxy, \"plugin-opts.mode\", obfs.type);\n        $set(proxy, \"plugin-opts.host\", obfs.host);\n        $set(proxy, \"plugin-opts.path\", obfs.path);\n    }\n}\nvmess = tag equals \"vmess\"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {\n    proxy.type = \"vmess\";\n    proxy.cipher = proxy.cipher || \"none\";\n    proxy.alterId = proxy.alterId || 0;\n    handleTransport();\n}\nvless = tag equals \"vless\"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {\n    proxy.type = \"vless\";\n    handleTransport();\n}\ntrojan = tag equals \"trojan\"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"trojan\";\n    handleTransport();\n}\nanytls = tag equals \"anytls\"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/idle_session_check_interval/idle_session_timeout/min_idle_session/max_stream_count/others)* {\n    proxy.type = \"anytls\";\n    handleTransport();\n}\nhysteria2 = tag equals \"hysteria2\"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {\n    proxy.type = \"hysteria2\";\n}\nhttps = tag equals \"https\"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"http\";\n    proxy.tls = true;\n}\nhttp = tag equals \"http\"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"http\";\n}\nsocks5 = tag equals \"socks5\"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {\n    proxy.type = \"socks5\";\n}\n\naddress = comma server:server comma port:port {\n    proxy.server = server;\n    proxy.port = port;\n}\n\nserver = ip/domain\n\nip = & {\n    const start = peg$currPos;\n    let j = start;\n    while (j < input.length) {\n        if (input[j] === \",\") break;\n        j++;\n    }\n    peg$currPos = j;\n    $.ip = input.substring(start, j).trim();\n    return true;\n} { return $.ip; }\n\ndomain = match:[0-9a-zA-z-_.]+ { \n    const domain = match.join(\"\"); \n    if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n        return domain;\n    }\n    throw new Error(\"Invalid domain: \" + domain);\n}\n\nport = digits:[0-9]+ { \n    const port = parseInt(digits.join(\"\"), 10); \n    if (port >= 0 && port <= 65535) {\n    \treturn port;\n    }\n    throw new Error(\"Invalid port number: \" + port);\n}\n\nmethod = comma cipher:cipher { \n    proxy.cipher = cipher;\n}\ncipher = (\"aes-128-cfb\"/\"aes-128-ctr\"/\"aes-128-gcm\"/\"aes-192-cfb\"/\"aes-192-ctr\"/\"aes-192-gcm\"/\"aes-256-cfb\"/\"aes-256-ctr\"/\"aes-256-gcm\"/\"auto\"/\"bf-cfb\"/\"camellia-128-cfb\"/\"camellia-192-cfb\"/\"camellia-256-cfb\"/\"chacha20-ietf-poly1305\"/\"chacha20-ietf\"/\"chacha20-poly1305\"/\"chacha20\"/\"none\"/\"rc4-md5\"/\"rc4\"/\"salsa20\"/\"xchacha20-ietf-poly1305\"/\"2022-blake3-aes-128-gcm\"/\"2022-blake3-aes-256-gcm\");\n\nusername = & {\n    let j = peg$currPos; \n    let start, end;\n    let first = true;\n    while (j < input.length) {\n        if (input[j] === ',') {\n            if (first) {\n                start = j + 1;\n                first = false;\n            } else {\n                end = j;\n                break;\n            }\n        }\n        j++;\n    }\n    const match = input.substring(start, end);\n    if (match.indexOf(\"=\") === -1) {\n        $.username = match;\n        peg$currPos = end;\n        return true;\n    }\n} { proxy.username = $.username; }\npassword = comma '\"' match:[^\"]* '\"' { proxy.password = match.join(\"\"); }\nuuid = comma '\"' match:[^\"]+ '\"' { proxy.uuid = match.join(\"\"); }\n\nobfs_typev = comma type:(\"http\"/\"tls\") { obfs.type = type; }\nobfs_hostv = comma match:[^,]+ { obfs.host = match.join(\"\"); }\n\nobfs_ss = comma \"obfs-name\" equals type:(\"http\"/\"tls\") { obfs.type = type; }\n\nobfs_ssr = comma \"obfs\" equals type:(\"plain\"/\"http_simple\"/\"http_post\"/\"random_head\"/\"tls1.2_ticket_auth\"/\"tls1.2_ticket_fastauth\") { obfs.type = type; }\nobfs_ssr_param = comma \"obfs-param\" equals match:$[^,]+ { proxy[\"obfs-param\"] = match; }\n\nobfs_host = comma \"obfs-host\" equals match:[^,]+ { obfs.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nobfs_uri = comma \"obfs-uri\" equals uri:uri { obfs.path = uri; }\nuri = $[^,]+\n\ntransport = comma \"transport\" equals type:(\"tcp\"/\"ws\"/\"http\") { transport.type = type; }\ntransport_host = comma \"host\" equals match:[^,]+ { transport.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntransport_path = comma \"path\" equals path:uri { transport.path = path; }\n\nssr_protocol = comma \"protocol\" equals protocol:(\"origin\"/\"auth_sha1_v4\"/\"auth_aes128_md5\"/\"auth_aes128_sha1\"/\"auth_chain_a\"/\"auth_chain_b\") { proxy.protocol = protocol; }\nssr_protocol_param = comma \"protocol-param\" equals param:$[^=,]+ { proxy[\"protocol-param\"] = param; }\n\nvmess_alterId = comma \"alterId\" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } \n\nudp_port = comma \"udp-port\" equals match:$[0-9]+ { proxy[\"udp-port\"] = parseInt(match.trim()); }\nshadow_tls_version = comma \"shadow-tls-version\" equals match:$[0-9]+ { proxy[\"shadow-tls-version\"] = parseInt(match.trim()); }\nshadow_tls_sni = comma \"shadow-tls-sni\" equals match:[^,]+ { proxy[\"shadow-tls-sni\"] = match.join(\"\"); }\nshadow_tls_password = comma \"shadow-tls-password\" equals match:[^,]+ { proxy[\"shadow-tls-password\"] = match.join(\"\"); }\n\nover_tls = comma \"over-tls\" equals flag:bool { proxy.tls = flag; }\ntls_name = comma sni:(\"tls-name\") equals match:[^,]+ { proxy.sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nsni = comma \"sni\" equals match:[^,]+ { proxy.sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntls_verification = comma \"skip-cert-verify\" equals flag:bool { proxy[\"skip-cert-verify\"] = flag; }\ntls_cert_sha256 = comma \"tls-cert-sha256\" equals match:[^,]+ { proxy[\"tls-fingerprint\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntls_pubkey_sha256 = comma \"tls-pubkey-sha256\" equals match:[^,]+ { proxy[\"tls-pubkey-sha256\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\n\nflow = comma \"flow\" equals match:[^,]+ { proxy[\"flow\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\npublic_key = comma \"public-key\" equals match:[^,]+ { proxy[\"reality-opts\"] = proxy[\"reality-opts\"] || {}; proxy[\"reality-opts\"][\"public-key\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nshort_id = comma \"short-id\" equals match:[^,]+ { proxy[\"reality-opts\"] = proxy[\"reality-opts\"] || {}; proxy[\"reality-opts\"][\"short-id\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\n\nfast_open = comma \"fast-open\" equals flag:bool { proxy.tfo = flag; }\nudp_relay = comma \"udp\" equals flag:bool { proxy.udp = flag; }\nip_mode = comma \"ip-mode\" equals match:[^,]+ { proxy[\"ip-version\"] = match.join(\"\"); }\n\necn = comma \"ecn\" equals flag:bool { proxy.ecn = flag; }\ndownload_bandwidth = comma \"download-bandwidth\" equals match:[^,]+ { proxy.down = match.join(\"\"); }\nsalamander_password = comma \"salamander-password\" equals match:[^,]+ { proxy['obfs-password'] = match.join(\"\"); proxy.obfs = 'salamander'; }\n\nblock_quic = comma \"block-quic\" equals flag:bool { if(flag) proxy[\"block-quic\"] = \"on\"; else proxy[\"block-quic\"] = \"off\"; }\n\nidle_session_check_interval = comma \"idle-session-check-interval\" equals match:$[0-9]+ { proxy[\"idle-session-check-interval\"] = parseInt(match.trim()); }\nidle_session_timeout = comma \"idle-session-timeout\" equals match:$[0-9]+ { proxy[\"idle-session-timeout\"] = parseInt(match.trim()); }\nmin_idle_session = comma \"min-idle-session\" equals match:$[0-9]+ { proxy[\"min-idle-session\"] = parseInt(match.trim()); }\nmax_stream_count = comma \"max-stream-count\" equals match:$[0-9]+ { proxy[\"max-stream-count\"] = parseInt(match.trim()); }\n\nudp_over_tcp = comma \"udp-over-tcp\" equals flag:bool { proxy[\"udp-over-tcp\"] = true; proxy[\"udp-over-tcp-version\"] = 2; }\n\ntag = match:[^=,]* { proxy.name = match.join(\"\").trim(); }\ncomma = _ \",\" _\nequals = _ \"=\" _\n_ = [ \\r\\t]*\nbool = b:(\"true\"/\"false\") { return b === \"true\" }\nothers = comma [^=,]+ equals [^=,]+"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/qx.js",
    "content": "import * as peggy from 'peggy';\nconst grammars = String.raw`\n// global initializer\n{{\n    function $set(obj, path, value) {\n      if (Object(obj) !== obj) return obj;\n      if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n      path\n        .slice(0, -1)\n        .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n        path[path.length - 1]\n      ] = value;\n      return obj;\n    }\n}}\n\n// per-parse initializer\n{\n\tconst proxy = {};\n    const obfs = {};\n    const $ = {};\n\n    function handleObfs() {\n        if (obfs.type === \"ws\" || obfs.type === \"wss\") {\n            proxy.network = \"ws\";\n            if (obfs.type === 'wss') {\n                proxy.tls = true;\n            }\n            $set(proxy, \"ws-opts.path\", obfs.path);\n            $set(proxy, \"ws-opts.headers.Host\", obfs.host);\n        } else if (obfs.type === \"over-tls\") {\n            proxy.tls = true;\n        } else if (obfs.type === \"http\") {\n            proxy.network = \"http\";\n            $set(proxy, \"http-opts.path\", obfs.path);\n            $set(proxy, \"http-opts.headers.Host\", obfs.host);\n        }\n    }\n}\n\nstart = (trojan/shadowsocks/vmess/vless/http/socks5) {\n    return proxy\n}\n\ntrojan = \"trojan\" equals address\n    (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    proxy.type = \"trojan\";\n    handleObfs();\n}\n\nshadowsocks = \"shadowsocks\" equals address\n    (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    if (proxy.protocol || proxy.type === \"ssr\") {\n        proxy.type = \"ssr\";\n        if (!proxy.protocol) {\n            proxy.protocol = \"origin\";\n        }\n        // handle ssr obfs\n        if (obfs.host) proxy[\"obfs-param\"] = obfs.host;\n        if (obfs.type) proxy.obfs = obfs.type;\n    } else {\n        proxy.type = \"ss\";\n        // handle ss obfs\n        if (obfs.type == \"http\" || obfs.type === \"tls\") {\n            proxy.plugin = \"obfs\";\n            $set(proxy, \"plugin-opts\", {\n                mode: obfs.type\n            });\n        } else if (obfs.type === \"ws\" || obfs.type === \"wss\") {\n            proxy.plugin = \"v2ray-plugin\";\n            $set(proxy, \"plugin-opts.mode\", \"websocket\");\n            if (obfs.type === \"wss\") {\n                $set(proxy, \"plugin-opts.tls\", true);\n            }\n        } else if (obfs.type === 'over-tls') {\n            throw new Error('ss over-tls is not supported');\n        }\n        if (obfs.type) {\n            $set(proxy, \"plugin-opts.host\", obfs.host);\n            $set(proxy, \"plugin-opts.path\", obfs.path);\n        }\n    }\n}\n\nvmess = \"vmess\" equals address\n    (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    proxy.type = \"vmess\";\n    proxy.cipher = proxy.cipher || \"none\";\n    if (proxy.aead === false) {\n        proxy.alterId = 1;\n    } else {\n        proxy.alterId = 0;\n    }\n    handleObfs();\n}\n\nvless = \"vless\" equals address\n    (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/vless_flow/others)* {\n    proxy.type = \"vless\";\n    proxy.cipher = proxy.cipher || \"none\";\n    handleObfs();\n}\n\nhttp = \"http\" equals address \n    (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)*{\n    proxy.type = \"http\";\n}\n\nsocks5 = \"socks5\" equals address\n    (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    proxy.type = \"socks5\";\n}\n    \naddress = server:server \":\" port:port {\n    proxy.server = server;\n    proxy.port = port;\n}\nserver = ip/domain\n\ndomain = match:[0-9a-zA-z-_.]+ { \n    const domain = match.join(\"\"); \n    if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n        return domain;\n    }\n}\n\nip = & {\n    const start = peg$currPos;\n    let end;\n    let j = start;\n    while (j < input.length) {\n        if (input[j] === \",\") break;\n        if (input[j] === \":\") end = j;\n        j++;\n    }\n    peg$currPos = end || j;\n    $.ip = input.substring(start, end).trim();\n    return true;\n} { return $.ip; }\n\nport = digits:[0-9]+ { \n    const port = parseInt(digits.join(\"\"), 10); \n    if (port >= 0 && port <= 65535) {\n    \treturn port;\n    }\n}\n\nusername = comma \"username\" equals username:[^,]+ { proxy.username = username.join(\"\").trim(); }\npassword = comma \"password\" equals password:[^,]+ { proxy.password = password.join(\"\").trim(); }\nuuid = comma \"password\" equals uuid:[^,]+ { proxy.uuid = uuid.join(\"\").trim(); }\n\nmethod = comma \"method\" equals cipher:cipher { \n    proxy.cipher = cipher;\n};\ncipher = (\"aes-128-cfb\"/\"aes-128-ctr\"/\"aes-128-gcm\"/\"aes-192-cfb\"/\"aes-192-ctr\"/\"aes-192-gcm\"/\"aes-256-cfb\"/\"aes-256-ctr\"/\"aes-256-gcm\"/\"bf-cfb\"/\"cast5-cfb\"/\"chacha20-ietf-poly1305\"/\"chacha20-ietf\"/\"chacha20-poly1305\"/\"chacha20\"/\"des-cfb\"/\"none\"/\"rc2-cfb\"/\"rc4-md5-6\"/\"rc4-md5\"/\"salsa20\"/\"xchacha20-ietf-poly1305\"/\"2022-blake3-aes-128-gcm\"/\"2022-blake3-aes-256-gcm\");\naead = comma \"aead\" equals flag:bool { proxy.aead = flag; }\n\nudp_relay = comma \"udp-relay\" equals flag:bool { proxy.udp = flag; }\nudp_over_tcp = comma \"udp-over-tcp\" equals flag:bool { throw new Error(\"UDP over TCP is not supported\"); }\nudp_over_tcp_new = comma \"udp-over-tcp\" equals param:$[^=,]+ { if (param === \"sp.v1\") { proxy[\"udp-over-tcp\"] = true; proxy[\"udp-over-tcp-version\"] = 1; } else if (param === \"sp.v2\") { proxy[\"udp-over-tcp\"] = true; proxy[\"udp-over-tcp-version\"] = 2; } else if (param === \"true\") { proxy[\"_ssr_python_uot\"] = true; } else { throw new Error(\"Invalid value for udp-over-tcp\"); } }\n\nfast_open = comma \"fast-open\" equals flag:bool { proxy.tfo = flag; }\n\nover_tls = comma \"over-tls\" equals flag:bool { proxy.tls = flag; }\ntls_host = comma sni:(\"tls-host\") equals match:[^,]+ { proxy.sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntls_verification = comma \"tls-verification\" equals flag:bool { \n    proxy[\"skip-cert-verify\"] = !flag;\n}\ntls_fingerprint = comma \"tls-cert-sha256\" equals tls_fingerprint:$[^,]+ { proxy[\"tls-fingerprint\"] = tls_fingerprint.trim(); }\ntls_pubkey_sha256 = comma \"tls-pubkey-sha256\" equals param:$[^=,]+ { proxy[\"tls-pubkey-sha256\"] = param; }\ntls_alpn = comma \"tls-alpn\" equals param:$[^=,]+ { proxy[\"tls-alpn\"] = param; }\ntls_no_session_ticket = comma \"tls-no-session-ticket\" equals flag:bool { \n    proxy[\"tls-no-session-ticket\"] = flag;\n}\ntls_no_session_reuse = comma \"tls-no-session-reuse\" equals flag:bool { \n    proxy[\"tls-no-session-reuse\"] = flag;\n}\n\nobfs_ss = comma \"obfs\" equals type:(\"http\"/\"tls\"/\"wss\"/\"ws\"/\"over-tls\") { obfs.type = type; return type; }\nobfs_ssr = comma \"obfs\" equals type:(\"plain\"/\"http_simple\"/\"http_post\"/\"random_head\"/\"tls1.2_ticket_auth\"/\"tls1.2_ticket_fastauth\") { proxy.type = \"ssr\"; obfs.type = type; return type; }\nobfs = comma \"obfs\" equals type:(\"wss\"/\"ws\"/\"over-tls\"/\"http\") { obfs.type = type; return type; };\n\nobfs_host = comma \"obfs-host\" equals match:[^,]+ { obfs.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nobfs_uri = comma \"obfs-uri\" equals uri:uri { obfs.path = uri; }\n\nssr_protocol = comma \"ssr-protocol\" equals protocol:(\"origin\"/\"auth_sha1_v4\"/\"auth_aes128_md5\"/\"auth_aes128_sha1\"/\"auth_chain_a\"/\"auth_chain_b\") { proxy.protocol = protocol; return protocol; }\nssr_protocol_param = comma \"ssr-protocol-param\" equals param:$[^=,]+ { proxy[\"protocol-param\"] = param; }\n\nreality_base64_pubkey = comma \"reality-base64-pubkey\" equals param:$[^=,]+ {\n    $set(proxy, \"reality-opts.public-key\", param);\n }\nreality_hex_shortid = comma \"reality-hex-shortid\" equals param:$[^=,]+ {\n    $set(proxy, \"reality-opts.short-id\", param);\n}\n\nvless_flow = comma \"vless-flow\" equals param:$[^=,]+ { proxy[\"flow\"] = param; }\nserver_check_url = comma \"server_check_url\" equals param:$[^=,]+ { proxy[\"test-url\"] = param; }\n\nuri = $[^,]+\n\ntag = comma \"tag\" equals tag:[^=,]+ { proxy.name = tag.join(\"\"); }\nothers = comma [^=,]+ equals [^=,]+\ncomma = _ \",\" _\nequals = _ \"=\" _\n_ = [ \\r\\t]*\nbool = b:(\"true\"/\"false\") { return b === \"true\" }\n`;\nlet parser;\nexport default function getParser() {\n    if (!parser) {\n        parser = peggy.generate(grammars);\n    }\n    return parser;\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/qx.peg",
    "content": "// global initializer\n{{\n    function $set(obj, path, value) {\n      if (Object(obj) !== obj) return obj;\n      if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n      path\n        .slice(0, -1)\n        .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n        path[path.length - 1]\n      ] = value;\n      return obj;\n    }\n}}\n\n// per-parse initializer\n{\n\tconst proxy = {};\n    const obfs = {};\n    const $ = {};\n\n    function handleObfs() {\n        if (obfs.type === \"ws\" || obfs.type === \"wss\") {\n            proxy.network = \"ws\";\n            if (obfs.type === 'wss') {\n                proxy.tls = true;\n            }\n            $set(proxy, \"ws-opts.path\", obfs.path);\n            $set(proxy, \"ws-opts.headers.Host\", obfs.host);\n        } else if (obfs.type === \"over-tls\") {\n            proxy.tls = true;\n        } else if (obfs.type === \"http\") {\n            proxy.network = \"http\";\n            $set(proxy, \"http-opts.path\", obfs.path);\n            $set(proxy, \"http-opts.headers.Host\", obfs.host);\n        }\n    }\n}\n\nstart = (trojan/shadowsocks/vmess/vless/http/socks5) {\n    return proxy\n}\n\ntrojan = \"trojan\" equals address\n    (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    proxy.type = \"trojan\";\n    handleObfs();\n}\n\nshadowsocks = \"shadowsocks\" equals address\n    (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    if (proxy.protocol || proxy.type === \"ssr\") {\n        proxy.type = \"ssr\";\n        if (!proxy.protocol) {\n            proxy.protocol = \"origin\";\n        }\n        // handle ssr obfs\n        if (obfs.host) proxy[\"obfs-param\"] = obfs.host;\n        if (obfs.type) proxy.obfs = obfs.type;\n    } else {\n        proxy.type = \"ss\";\n        // handle ss obfs\n        if (obfs.type == \"http\" || obfs.type === \"tls\") {\n            proxy.plugin = \"obfs\";\n            $set(proxy, \"plugin-opts\", {\n                mode: obfs.type\n            });\n        } else if (obfs.type === \"ws\" || obfs.type === \"wss\") {\n            proxy.plugin = \"v2ray-plugin\";\n            $set(proxy, \"plugin-opts.mode\", \"websocket\");\n            if (obfs.type === \"wss\") {\n                $set(proxy, \"plugin-opts.tls\", true);\n            }\n        } else if (obfs.type === 'over-tls') {\n            throw new Error('ss over-tls is not supported');\n        }\n        if (obfs.type) {\n            $set(proxy, \"plugin-opts.host\", obfs.host);\n            $set(proxy, \"plugin-opts.path\", obfs.path);\n        }\n    }\n}\n\nvmess = \"vmess\" equals address\n    (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    proxy.type = \"vmess\";\n    proxy.cipher = proxy.cipher || \"none\";\n    if (proxy.aead === false) {\n        proxy.alterId = 1;\n    } else {\n        proxy.alterId = 0;\n    }\n    handleObfs();\n}\n\nvless = \"vless\" equals address\n    (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/vless_flow/others)* {\n    proxy.type = \"vless\";\n    proxy.cipher = proxy.cipher || \"none\";\n    handleObfs();\n}\n\nhttp = \"http\" equals address \n    (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)*{\n    proxy.type = \"http\";\n}\n\nsocks5 = \"socks5\" equals address\n    (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {\n    proxy.type = \"socks5\";\n}\n    \naddress = server:server \":\" port:port {\n    proxy.server = server;\n    proxy.port = port;\n}\nserver = ip/domain\n\ndomain = match:[0-9a-zA-z-_.]+ { \n    const domain = match.join(\"\"); \n    if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n        return domain;\n    }\n}\n\nip = & {\n    const start = peg$currPos;\n    let end;\n    let j = start;\n    while (j < input.length) {\n        if (input[j] === \",\") break;\n        if (input[j] === \":\") end = j;\n        j++;\n    }\n    peg$currPos = end || j;\n    $.ip = input.substring(start, end).trim();\n    return true;\n} { return $.ip; }\n\nport = digits:[0-9]+ { \n    const port = parseInt(digits.join(\"\"), 10); \n    if (port >= 0 && port <= 65535) {\n    \treturn port;\n    }\n}\n\nusername = comma \"username\" equals username:[^,]+ { proxy.username = username.join(\"\").trim(); }\npassword = comma \"password\" equals password:[^,]+ { proxy.password = password.join(\"\").trim(); }\nuuid = comma \"password\" equals uuid:[^,]+ { proxy.uuid = uuid.join(\"\").trim(); }\n\nmethod = comma \"method\" equals cipher:cipher { \n    proxy.cipher = cipher;\n};\ncipher = (\"aes-128-cfb\"/\"aes-128-ctr\"/\"aes-128-gcm\"/\"aes-192-cfb\"/\"aes-192-ctr\"/\"aes-192-gcm\"/\"aes-256-cfb\"/\"aes-256-ctr\"/\"aes-256-gcm\"/\"bf-cfb\"/\"cast5-cfb\"/\"chacha20-ietf-poly1305\"/\"chacha20-ietf\"/\"chacha20-poly1305\"/\"chacha20\"/\"des-cfb\"/\"none\"/\"rc2-cfb\"/\"rc4-md5-6\"/\"rc4-md5\"/\"salsa20\"/\"xchacha20-ietf-poly1305\"/\"2022-blake3-aes-128-gcm\"/\"2022-blake3-aes-256-gcm\");\naead = comma \"aead\" equals flag:bool { proxy.aead = flag; }\n\nudp_relay = comma \"udp-relay\" equals flag:bool { proxy.udp = flag; }\nudp_over_tcp = comma \"udp-over-tcp\" equals flag:bool { throw new Error(\"UDP over TCP is not supported\"); }\nudp_over_tcp_new = comma \"udp-over-tcp\" equals param:$[^=,]+ { if (param === \"sp.v1\") { proxy[\"udp-over-tcp\"] = true; proxy[\"udp-over-tcp-version\"] = 1; } else if (param === \"sp.v2\") { proxy[\"udp-over-tcp\"] = true; proxy[\"udp-over-tcp-version\"] = 2; } else if (param === \"true\") { proxy[\"_ssr_python_uot\"] = true; } else { throw new Error(\"Invalid value for udp-over-tcp\"); } }\n\nfast_open = comma \"fast-open\" equals flag:bool { proxy.tfo = flag; }\n\nover_tls = comma \"over-tls\" equals flag:bool { proxy.tls = flag; }\ntls_host = comma sni:(\"tls-host\") equals match:[^,]+ { proxy.sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\ntls_verification = comma \"tls-verification\" equals flag:bool { \n    proxy[\"skip-cert-verify\"] = !flag;\n}\ntls_fingerprint = comma \"tls-cert-sha256\" equals tls_fingerprint:$[^,]+ { proxy[\"tls-fingerprint\"] = tls_fingerprint.trim(); }\ntls_pubkey_sha256 = comma \"tls-pubkey-sha256\" equals param:$[^=,]+ { proxy[\"tls-pubkey-sha256\"] = param; }\ntls_alpn = comma \"tls-alpn\" equals param:$[^=,]+ { proxy[\"tls-alpn\"] = param; }\ntls_no_session_ticket = comma \"tls-no-session-ticket\" equals flag:bool { \n    proxy[\"tls-no-session-ticket\"] = flag;\n}\ntls_no_session_reuse = comma \"tls-no-session-reuse\" equals flag:bool { \n    proxy[\"tls-no-session-reuse\"] = flag;\n}\n\nobfs_ss = comma \"obfs\" equals type:(\"http\"/\"tls\"/\"wss\"/\"ws\"/\"over-tls\") { obfs.type = type; return type; }\nobfs_ssr = comma \"obfs\" equals type:(\"plain\"/\"http_simple\"/\"http_post\"/\"random_head\"/\"tls1.2_ticket_auth\"/\"tls1.2_ticket_fastauth\") { proxy.type = \"ssr\"; obfs.type = type; return type; }\nobfs = comma \"obfs\" equals type:(\"wss\"/\"ws\"/\"over-tls\"/\"http\") { obfs.type = type; return type; };\n\nobfs_host = comma \"obfs-host\" equals match:[^,]+ { obfs.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nobfs_uri = comma \"obfs-uri\" equals uri:uri { obfs.path = uri; }\n\nssr_protocol = comma \"ssr-protocol\" equals protocol:(\"origin\"/\"auth_sha1_v4\"/\"auth_aes128_md5\"/\"auth_aes128_sha1\"/\"auth_chain_a\"/\"auth_chain_b\") { proxy.protocol = protocol; return protocol; }\nssr_protocol_param = comma \"ssr-protocol-param\" equals param:$[^=,]+ { proxy[\"protocol-param\"] = param; }\n\nreality_base64_pubkey = comma \"reality-base64-pubkey\" equals param:$[^=,]+ {\n    $set(proxy, \"reality-opts.public-key\", param);\n }\nreality_hex_shortid = comma \"reality-hex-shortid\" equals param:$[^=,]+ {\n    $set(proxy, \"reality-opts.short-id\", param);\n}\nvless_flow = comma \"vless-flow\" equals param:$[^=,]+ { proxy[\"flow\"] = param; }\nserver_check_url = comma \"server_check_url\" equals param:$[^=,]+ { proxy[\"test-url\"] = param; }\n\nuri = $[^,]+\n\ntag = comma \"tag\" equals tag:[^=,]+ { proxy.name = tag.join(\"\"); }\nothers = comma [^=,]+ equals [^=,]+\ncomma = _ \",\" _\nequals = _ \"=\" _\n_ = [ \\r\\t]*\nbool = b:(\"true\"/\"false\") { return b === \"true\" }"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/surge.js",
    "content": "import * as peggy from 'peggy';\nconst grammars = String.raw`\n// global initializer\n{{\n    function $set(obj, path, value) {\n      if (Object(obj) !== obj) return obj;\n      if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n      path\n        .slice(0, -1)\n        .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n        path[path.length - 1]\n      ] = value;\n      return obj;\n    }\n}}\n\n// per-parser initializer\n{\n    const proxy = {};\n    const obfs = {};\n    const $ = {};\n\n    function handleWebsocket() {\n        if (obfs.type === \"ws\") {\n            proxy.network = \"ws\";\n            $set(proxy, \"ws-opts.path\", obfs.path);\n            $set(proxy, \"ws-opts.headers\", obfs['ws-headers']);\n            if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {\n                proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^\"(.*)\"$/, '$1')\n            }\n        }\n    }\n    function handleShadowTLS() {\n        if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {\n            proxy['shadow-tls-version'] = 2;\n        }\n    }\n}\n\nstart = (anytls/shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/trust_tunnel/direct) {\n    return proxy;\n}\n\nshadowsocks = tag equals \"ss\" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {\n    proxy.type = \"ss\";\n    // handle obfs\n    if (obfs.type == \"http\" || obfs.type === \"tls\") {\n        proxy.plugin = \"obfs\";\n        $set(proxy, \"plugin-opts.mode\", obfs.type);\n        $set(proxy, \"plugin-opts.host\", obfs.host);\n        $set(proxy, \"plugin-opts.path\", obfs.path);\n    }\n    handleShadowTLS();\n}\nvmess = tag equals \"vmess\" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"vmess\";\n    proxy.cipher = proxy.cipher || \"none\";\n    // Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess\n    if (proxy.aead) {\n        proxy.alterId = 0;\n    } else {\n        proxy.alterId = 1;\n    }\n    handleWebsocket();\n    handleShadowTLS();\n}\ntrojan = tag equals \"trojan\" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"trojan\";\n    handleWebsocket();\n    handleShadowTLS();\n}\nhttps = tag equals \"https\" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"http\";\n    proxy.tls = true;\n    handleShadowTLS();\n}\nhttp = tag equals \"http\" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"http\";\n    handleShadowTLS();\n}\nssh = tag equals \"ssh\" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"ssh\";\n    handleShadowTLS();\n}\nsnell = tag equals \"snell\" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"snell\";\n    // handle obfs\n    if (obfs.type == \"http\" || obfs.type === \"tls\") {\n        $set(proxy, \"obfs-opts.mode\", obfs.type);\n        $set(proxy, \"obfs-opts.host\", obfs.host);\n        $set(proxy, \"obfs-opts.path\", obfs.path);\n    }\n    handleShadowTLS();\n}\ntuic = tag equals \"tuic\" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {\n    proxy.type = \"tuic\";\n    handleShadowTLS();\n}\ntuic_v5 = tag equals \"tuic-v5\" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {\n    proxy.type = \"tuic\";\n    proxy.version = 5;\n    handleShadowTLS();\n}\nwireguard = tag equals \"wireguard\" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"wireguard-surge\";\n    handleShadowTLS();\n}\nhysteria2 = tag equals \"hysteria2\" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/others)* {\n    proxy.type = \"hysteria2\";\n    handleShadowTLS();\n}\nsocks5 = tag equals \"socks5\" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"socks5\";\n    handleShadowTLS();\n}\nsocks5_tls = tag equals \"socks5-tls\" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"socks5\";\n    proxy.tls = true;\n    handleShadowTLS();\n}\nanytls = tag equals \"anytls\" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* {\n    proxy.type = \"anytls\";\n    proxy.tls = true;\n}\ntrust_tunnel = tag equals \"trust-tunnel\" address (usernamek/passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* {\n    proxy.type = \"trusttunnel\";\n    proxy.tls = true;\n}\n\ndirect = tag equals \"direct\" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {\n    proxy.type = \"direct\";\n}\naddress = comma server:server comma port:port {\n    proxy.server = server;\n    proxy.port = port;\n}\n\nserver = ip/domain\n\nip = & {\n    const start = peg$currPos;\n    let j = start;\n    while (j < input.length) {\n        if (input[j] === \",\") break;\n        j++;\n    }\n    peg$currPos = j;\n    $.ip = input.substring(start, j).trim();\n    return true;\n} { return $.ip; }\n\ndomain = match:[0-9a-zA-z-_.]+ { \n    const domain = match.join(\"\"); \n    if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n        return domain;\n    }\n}\n\nport = digits:[0-9]+ { \n    const port = parseInt(digits.join(\"\"), 10); \n    if (port >= 0 && port <= 65535) {\n    \treturn port;\n    }\n}\n\nport_hopping_interval = comma \"port-hopping-interval\" equals match:$[0-9]+ { proxy[\"hop-interval\"] = parseInt(match.trim()); }\n\nusername = & {\n    let j = peg$currPos; \n    let start, end;\n    let first = true;\n    while (j < input.length) {\n        if (input[j] === ',') {\n            if (first) {\n                start = j + 1;\n                first = false;\n            } else {\n                end = j;\n                break;\n            }\n        }\n        j++;\n    }\n    const match = input.substring(start, end);\n    if (match.indexOf(\"=\") === -1) {\n        $.username = match;\n        peg$currPos = end;\n        return true;\n    }\n} { proxy.username = $.username.trim().replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\npassword = comma match:[^,]+ { proxy.password = match.join(\"\").replace(/^\"(.*)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\n\ntls = comma \"tls\" equals flag:bool { proxy.tls = flag; }\nsni = comma \"sni\" equals match:[^,]+ { \n    const sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1');\n    if (sni === \"off\") {\n        proxy[\"disable-sni\"] = true;\n    } else {\n        proxy.sni = sni;\n    }\n}\ntls_verification = comma \"skip-cert-verify\" equals flag:bool { proxy[\"skip-cert-verify\"] = flag; }\ntls_fingerprint = comma \"server-cert-fingerprint-sha256\" equals tls_fingerprint:$[^,]+ { proxy[\"tls-fingerprint\"] = tls_fingerprint.trim(); }\n\nsnell_psk = comma \"psk\" equals match:[^,]+ { proxy.psk = match.join(\"\"); }\nsnell_version = comma \"version\" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }\n\nusernamek = comma \"username\" equals match:[^,]+ { proxy.username = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\npasswordk = comma \"password\" equals match:[^,]+ { proxy.password = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\nvmess_uuid = comma \"username\" equals match:[^,]+ { proxy.uuid = match.join(\"\"); }\nvmess_aead = comma \"vmess-aead\" equals flag:bool { proxy.aead = flag; }\n\nmethod = comma \"encrypt-method\" equals cipher:cipher {\n    proxy.cipher = cipher;\n}\ncipher = (\"aes-128-cfb\"/\"aes-128-ctr\"/\"aes-128-gcm\"/\"aes-192-cfb\"/\"aes-192-ctr\"/\"aes-192-gcm\"/\"aes-256-cfb\"/\"aes-256-ctr\"/\"aes-256-gcm\"/\"bf-cfb\"/\"camellia-128-cfb\"/\"camellia-192-cfb\"/\"camellia-256-cfb\"/\"cast5-cfb\"/\"chacha20-ietf-poly1305\"/\"chacha20-ietf\"/\"chacha20-poly1305\"/\"chacha20\"/\"des-cfb\"/\"idea-cfb\"/\"none\"/\"rc2-cfb\"/\"rc4-md5\"/\"rc4\"/\"salsa20\"/\"seed-cfb\"/\"xchacha20-ietf-poly1305\"/\"2022-blake3-aes-128-gcm\"/\"2022-blake3-aes-256-gcm\");\n\nws = comma \"ws\" equals flag:bool { obfs.type = \"ws\"; }\nws_headers = comma \"ws-headers\" equals headers:$[^,]+ {\n    const pairs = headers.split(\"|\");\n    const result = {};\n    pairs.forEach(pair => {\n        const [key, value] = pair.trim().split(\":\");\n        result[key.trim()] = value.trim().replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1');\n    })\n    obfs[\"ws-headers\"] = result;\n}\nws_path = comma \"ws-path\" equals path:uri { obfs.path = path.trim().replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\n\nobfs = comma \"obfs\" equals type:(\"http\"/\"tls\") { obfs.type = type; }\nobfs_host = comma \"obfs-host\" equals match:[^,]+ { obfs.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); };\nobfs_uri = comma \"obfs-uri\" equals path:uri { obfs.path = path }\nuri = $[^,]+\n\nudp_relay = comma \"udp-relay\" equals flag:bool { proxy.udp = flag; }\nfast_open = comma \"fast-open\" equals flag:bool { proxy.tfo = flag; }\nreuse = comma \"reuse\" equals flag:bool { proxy.reuse = flag; }\necn = comma \"ecn\" equals flag:bool { proxy.ecn = flag; }\ntfo = comma \"tfo\" equals flag:bool { proxy.tfo = flag; }\nip_version = comma \"ip-version\" equals match:[^,]+ { proxy[\"ip-version\"] = match.join(\"\"); }\nsection_name = comma \"section-name\" equals match:[^,]+ { proxy[\"section-name\"] = match.join(\"\"); }\nno_error_alert = comma \"no-error-alert\" equals match:[^,]+ { proxy[\"no-error-alert\"] = match.join(\"\"); }\nunderlying_proxy = comma \"underlying-proxy\" equals match:[^,]+ { proxy[\"underlying-proxy\"] = match.join(\"\"); }\ndownload_bandwidth = comma \"download-bandwidth\" equals match:[^,]+ { proxy.down = match.join(\"\"); }\ntest_url = comma \"test-url\" equals match:[^,]+ { proxy[\"test-url\"] = match.join(\"\"); }\ntest_udp = comma \"test-udp\" equals match:[^,]+ { proxy[\"test-udp\"] = match.join(\"\"); }\ntest_timeout = comma \"test-timeout\" equals match:$[0-9]+ { proxy[\"test-timeout\"] = parseInt(match.trim()); }\ntos = comma \"tos\" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }\ninterface = comma \"interface\" equals match:[^,]+ { proxy.interface = match.join(\"\"); }\nallow_other_interface = comma \"allow-other-interface\" equals flag:bool { proxy[\"allow-other-interface\"] = flag; }\nhybrid = comma \"hybrid\" equals flag:bool { proxy.hybrid = flag; }\nidle_timeout = comma \"idle-timeout\" equals match:$[0-9]+ { proxy[\"idle-timeout\"] = parseInt(match.trim()); }\nprivate_key = comma \"private-key\" equals match:[^,]+ { proxy[\"keystore-private-key\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nserver_fingerprint = comma \"server-fingerprint\" equals match:[^,]+ { proxy[\"server-fingerprint\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nblock_quic = comma \"block-quic\" equals match:[^,]+ { proxy[\"block-quic\"] = match.join(\"\"); }\nudp_port = comma \"udp-port\" equals match:$[0-9]+ { proxy[\"udp-port\"] = parseInt(match.trim()); }\nshadow_tls_version = comma \"shadow-tls-version\" equals match:$[0-9]+ { proxy[\"shadow-tls-version\"] = parseInt(match.trim()); }\nshadow_tls_sni = comma \"shadow-tls-sni\" equals match:[^,]+ { proxy[\"shadow-tls-sni\"] = match.join(\"\"); }\nshadow_tls_password = comma \"shadow-tls-password\" equals match:[^,]+ { proxy[\"shadow-tls-password\"] = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\ntoken = comma \"token\" equals match:[^,]+ { proxy.token = match.join(\"\"); }\nalpn = comma \"alpn\" equals match:[^,]+ { proxy.alpn = match.join(\"\"); }\nuuidk = comma \"uuid\" equals match:[^,]+ { proxy.uuid = match.join(\"\"); }\nsalamander_password = comma \"salamander-password\" equals match:[^,]+ { proxy['obfs-password'] = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'salamander'; }\n\ntag = match:[^=,]* { proxy.name = match.join(\"\").trim(); }\ncomma = _ \",\" _\nequals = _ \"=\" _\n_ = [ \\r\\t]*\nbool = b:(\"true\"/\"false\") { return b === \"true\" }\nothers = comma [^=,]+ equals [^=,]+\n`;\nlet parser;\nexport default function getParser() {\n    if (!parser) {\n        parser = peggy.generate(grammars);\n    }\n    return parser;\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/surge.peg",
    "content": "// global initializer\n{{\n    function $set(obj, path, value) {\n      if (Object(obj) !== obj) return obj;\n      if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n      path\n        .slice(0, -1)\n        .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n        path[path.length - 1]\n      ] = value;\n      return obj;\n    }\n}}\n\n// per-parser initializer\n{\n    const proxy = {};\n    const obfs = {};\n    const $ = {};\n\n    function handleWebsocket() {\n        if (obfs.type === \"ws\") {\n            proxy.network = \"ws\";\n            $set(proxy, \"ws-opts.path\", obfs.path);\n            $set(proxy, \"ws-opts.headers\", obfs['ws-headers']);\n            if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {\n                proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^\"(.*)\"$/, '$1')\n            }\n        }\n    }\n    function handleShadowTLS() {\n        if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {\n            proxy['shadow-tls-version'] = 2;\n        }\n    }\n}\n\nstart = (anytls/shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/trust_tunnel/direct) {\n    return proxy;\n}\n\nshadowsocks = tag equals \"ss\" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {\n    proxy.type = \"ss\";\n    // handle obfs\n    if (obfs.type == \"http\" || obfs.type === \"tls\") {\n        proxy.plugin = \"obfs\";\n        $set(proxy, \"plugin-opts.mode\", obfs.type);\n        $set(proxy, \"plugin-opts.host\", obfs.host);\n        $set(proxy, \"plugin-opts.path\", obfs.path);\n    }\n    handleShadowTLS();\n}\nvmess = tag equals \"vmess\" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"vmess\";\n    proxy.cipher = proxy.cipher || \"none\";\n    // Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess\n    if (proxy.aead) {\n        proxy.alterId = 0;\n    } else {\n        proxy.alterId = 1;\n    }\n    handleWebsocket();\n    handleShadowTLS();\n}\ntrojan = tag equals \"trojan\" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"trojan\";\n    handleWebsocket();\n    handleShadowTLS();\n}\nhttps = tag equals \"https\" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"http\";\n    proxy.tls = true;\n    handleShadowTLS();\n}\nhttp = tag equals \"http\" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"http\";\n    handleShadowTLS();\n}\nssh = tag equals \"ssh\" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"ssh\";\n    handleShadowTLS();\n}\nsnell = tag equals \"snell\" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"snell\";\n    // handle obfs\n    if (obfs.type == \"http\" || obfs.type === \"tls\") {\n        $set(proxy, \"obfs-opts.mode\", obfs.type);\n        $set(proxy, \"obfs-opts.host\", obfs.host);\n        $set(proxy, \"obfs-opts.path\", obfs.path);\n    }\n    handleShadowTLS();\n}\ntuic = tag equals \"tuic\" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {\n    proxy.type = \"tuic\";\n    handleShadowTLS();\n}\ntuic_v5 = tag equals \"tuic-v5\" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {\n    proxy.type = \"tuic\";\n    proxy.version = 5;\n    handleShadowTLS();\n}\nwireguard = tag equals \"wireguard\" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"wireguard-surge\";\n    handleShadowTLS();\n}\nhysteria2 = tag equals \"hysteria2\" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/others)* {\n    proxy.type = \"hysteria2\";\n    handleShadowTLS();\n}\nsocks5 = tag equals \"socks5\" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"socks5\";\n    handleShadowTLS();\n}\nsocks5_tls = tag equals \"socks5-tls\" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {\n    proxy.type = \"socks5\";\n    proxy.tls = true;\n    handleShadowTLS();\n}\nanytls = tag equals \"anytls\" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* {\n    proxy.type = \"anytls\";\n    proxy.tls = true;\n}\ntrust_tunnel = tag equals \"trust-tunnel\" address (usernamek/passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* {\n    proxy.type = \"trusttunnel\";\n    proxy.tls = true;\n}\ndirect = tag equals \"direct\" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {\n    proxy.type = \"direct\";\n}\naddress = comma server:server comma port:port {\n    proxy.server = server;\n    proxy.port = port;\n}\n\nserver = ip/domain\n\nip = & {\n    const start = peg$currPos;\n    let j = start;\n    while (j < input.length) {\n        if (input[j] === \",\") break;\n        j++;\n    }\n    peg$currPos = j;\n    $.ip = input.substring(start, j).trim();\n    return true;\n} { return $.ip; }\n\ndomain = match:[0-9a-zA-z-_.]+ { \n    const domain = match.join(\"\"); \n    if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n        return domain;\n    }\n}\n\nport = digits:[0-9]+ { \n    const port = parseInt(digits.join(\"\"), 10); \n    if (port >= 0 && port <= 65535) {\n    \treturn port;\n    }\n}\n\nport_hopping_interval = comma \"port-hopping-interval\" equals match:$[0-9]+ { proxy[\"hop-interval\"] = parseInt(match.trim()); }\n\nusername = & {\n    let j = peg$currPos; \n    let start, end;\n    let first = true;\n    while (j < input.length) {\n        if (input[j] === ',') {\n            if (first) {\n                start = j + 1;\n                first = false;\n            } else {\n                end = j;\n                break;\n            }\n        }\n        j++;\n    }\n    const match = input.substring(start, end);\n    if (match.indexOf(\"=\") === -1) {\n        $.username = match;\n        peg$currPos = end;\n        return true;\n    }\n} { proxy.username = $.username.trim().replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\npassword = comma match:[^,]+ { proxy.password = match.join(\"\").replace(/^\"(.*)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\n\ntls = comma \"tls\" equals flag:bool { proxy.tls = flag; }\nsni = comma \"sni\" equals match:[^,]+ { \n    const sni = match.join(\"\").replace(/^\"(.*)\"$/, '$1');\n    if (sni === \"off\") {\n        proxy[\"disable-sni\"] = true;\n    } else {\n        proxy.sni = sni;\n    }\n}\ntls_verification = comma \"skip-cert-verify\" equals flag:bool { proxy[\"skip-cert-verify\"] = flag; }\ntls_fingerprint = comma \"server-cert-fingerprint-sha256\" equals tls_fingerprint:$[^,]+ { proxy[\"tls-fingerprint\"] = tls_fingerprint.trim(); }\n\nsnell_psk = comma \"psk\" equals match:[^,]+ { proxy.psk = match.join(\"\"); }\nsnell_version = comma \"version\" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }\n\nusernamek = comma \"username\" equals match:[^,]+ { proxy.username = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\npasswordk = comma \"password\" equals match:[^,]+ { proxy.password = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\nvmess_uuid = comma \"username\" equals match:[^,]+ { proxy.uuid = match.join(\"\"); }\nvmess_aead = comma \"vmess-aead\" equals flag:bool { proxy.aead = flag; }\n\nmethod = comma \"encrypt-method\" equals cipher:cipher {\n    proxy.cipher = cipher;\n}\ncipher = (\"aes-128-cfb\"/\"aes-128-ctr\"/\"aes-128-gcm\"/\"aes-192-cfb\"/\"aes-192-ctr\"/\"aes-192-gcm\"/\"aes-256-cfb\"/\"aes-256-ctr\"/\"aes-256-gcm\"/\"bf-cfb\"/\"camellia-128-cfb\"/\"camellia-192-cfb\"/\"camellia-256-cfb\"/\"cast5-cfb\"/\"chacha20-ietf-poly1305\"/\"chacha20-ietf\"/\"chacha20-poly1305\"/\"chacha20\"/\"des-cfb\"/\"idea-cfb\"/\"none\"/\"rc2-cfb\"/\"rc4-md5\"/\"rc4\"/\"salsa20\"/\"seed-cfb\"/\"xchacha20-ietf-poly1305\"/\"2022-blake3-aes-128-gcm\"/\"2022-blake3-aes-256-gcm\");\n\nws = comma \"ws\" equals flag:bool { obfs.type = \"ws\"; }\nws_headers = comma \"ws-headers\" equals headers:$[^,]+ {\n    const pairs = headers.split(\"|\");\n    const result = {};\n    pairs.forEach(pair => {\n        const [key, value] = pair.trim().split(\":\");\n        result[key.trim()] = value.trim().replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1');\n    })\n    obfs[\"ws-headers\"] = result;\n}\nws_path = comma \"ws-path\" equals path:uri { obfs.path = path.trim().replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\n\nobfs = comma \"obfs\" equals type:(\"http\"/\"tls\") { obfs.type = type; }\nobfs_host = comma \"obfs-host\" equals match:[^,]+ { obfs.host = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); };\nobfs_uri = comma \"obfs-uri\" equals path:uri { obfs.path = path }\nuri = $[^,]+\n\nudp_relay = comma \"udp-relay\" equals flag:bool { proxy.udp = flag; }\nfast_open = comma \"fast-open\" equals flag:bool { proxy.tfo = flag; }\nreuse = comma \"reuse\" equals flag:bool { proxy.reuse = flag; }\necn = comma \"ecn\" equals flag:bool { proxy.ecn = flag; }\ntfo = comma \"tfo\" equals flag:bool { proxy.tfo = flag; }\nip_version = comma \"ip-version\" equals match:[^,]+ { proxy[\"ip-version\"] = match.join(\"\"); }\nsection_name = comma \"section-name\" equals match:[^,]+ { proxy[\"section-name\"] = match.join(\"\"); }\nno_error_alert = comma \"no-error-alert\" equals match:[^,]+ { proxy[\"no-error-alert\"] = match.join(\"\"); }\nunderlying_proxy = comma \"underlying-proxy\" equals match:[^,]+ { proxy[\"underlying-proxy\"] = match.join(\"\"); }\ndownload_bandwidth = comma \"download-bandwidth\" equals match:[^,]+ { proxy.down = match.join(\"\"); }\ntest_url = comma \"test-url\" equals match:[^,]+ { proxy[\"test-url\"] = match.join(\"\"); }\ntest_udp = comma \"test-udp\" equals match:[^,]+ { proxy[\"test-udp\"] = match.join(\"\"); }\ntest_timeout = comma \"test-timeout\" equals match:$[0-9]+ { proxy[\"test-timeout\"] = parseInt(match.trim()); }\ntos = comma \"tos\" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }\ninterface = comma \"interface\" equals match:[^,]+ { proxy.interface = match.join(\"\"); }\nallow_other_interface = comma \"allow-other-interface\" equals flag:bool { proxy[\"allow-other-interface\"] = flag; }\nhybrid = comma \"hybrid\" equals flag:bool { proxy.hybrid = flag; }\nidle_timeout = comma \"idle-timeout\" equals match:$[0-9]+ { proxy[\"idle-timeout\"] = parseInt(match.trim()); }\nprivate_key = comma \"private-key\" equals match:[^,]+ { proxy[\"keystore-private-key\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nserver_fingerprint = comma \"server-fingerprint\" equals match:[^,]+ { proxy[\"server-fingerprint\"] = match.join(\"\").replace(/^\"(.*)\"$/, '$1'); }\nblock_quic = comma \"block-quic\" equals match:[^,]+ { proxy[\"block-quic\"] = match.join(\"\"); }\nudp_port = comma \"udp-port\" equals match:$[0-9]+ { proxy[\"udp-port\"] = parseInt(match.trim()); }\nshadow_tls_version = comma \"shadow-tls-version\" equals match:$[0-9]+ { proxy[\"shadow-tls-version\"] = parseInt(match.trim()); }\nshadow_tls_sni = comma \"shadow-tls-sni\" equals match:[^,]+ { proxy[\"shadow-tls-sni\"] = match.join(\"\"); }\nshadow_tls_password = comma \"shadow-tls-password\" equals match:[^,]+ { proxy[\"shadow-tls-password\"] = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); }\ntoken = comma \"token\" equals match:[^,]+ { proxy.token = match.join(\"\"); }\nalpn = comma \"alpn\" equals match:[^,]+ { proxy.alpn = match.join(\"\"); }\nuuidk = comma \"uuid\" equals match:[^,]+ { proxy.uuid = match.join(\"\"); }\nsalamander_password = comma \"salamander-password\" equals match:[^,]+ { proxy['obfs-password'] = match.join(\"\").replace(/^\"(.*?)\"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'salamander'; }\n\ntag = match:[^=,]* { proxy.name = match.join(\"\").trim(); }\ncomma = _ \",\" _\nequals = _ \"=\" _\n_ = [ \\r\\t]*\nbool = b:(\"true\"/\"false\") { return b === \"true\" }\nothers = comma [^=,]+ equals [^=,]+"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js",
    "content": "import * as peggy from 'peggy';\nconst grammars = String.raw`\n// global initializer\n{{\n  function $set(obj, path, value) {\n    if (Object(obj) !== obj) return obj;\n    if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n    path\n      .slice(0, -1)\n      .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n      path[path.length - 1]\n    ] = value;\n    return obj;\n  }\n\n  function toBool(str) {\n    if (typeof str === 'undefined' || str === null) return undefined;\n    return /(TRUE)|1/i.test(str);\n  }\n}}\n\n{\n  const proxy = {};\n  const obfs = {};\n  const $ = {};\n  const params = {};\n}\n\nstart = (trojan) {\n  return proxy\n}\n\ntrojan = \"trojan://\" password:password \"@\" server:server \":\" port:port \"/\"? params? name:name?{\n  proxy.type = \"trojan\";\n  proxy.password = password;\n  proxy.server = server;\n  proxy.port = port;\n  proxy.name = name;\n\n  // name may be empty\n  if (!proxy.name) {\n    proxy.name = server + \":\" + port;\n  }\n};\n\npassword = match:$[^@]+ {\n  return decodeURIComponent(match);\n};\n\nserver = ip/domain;\n\ndomain = match:[0-9a-zA-z-_.]+ { \n  const domain = match.join(\"\"); \n  if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n    return domain;\n  }\n}\n\nip = & {\n  const start = peg$currPos;\n  let end;\n  let j = start;\n  while (j < input.length) {\n    if (input[j] === \",\") break;\n    if (input[j] === \":\") end = j;\n    j++;\n  }\n  peg$currPos = end || j;\n  $.ip = input.substring(start, end).trim();\n  return true;\n} { return $.ip; }\n\nport = digits:[0-9]+ { \n  const port = parseInt(digits.join(\"\"), 10); \n  if (port >= 0 && port <= 65535) {\n    return port;\n  } else {\n    throw new Error(\"Invalid port: \" + port);\n  }\n}\n\nparams = \"?\" head:param tail:(\"&\"@param)* {\n  for (const [key, value] of Object.entries(params)) {\n    params[key] = decodeURIComponent(value);\n  }\n  proxy[\"skip-cert-verify\"] = toBool(params[\"allowInsecure\"]);\n  proxy.sni = params[\"sni\"] || params[\"peer\"];\n  proxy['client-fingerprint'] = params.fp;\n  proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;\n\n  if (toBool(params[\"ws\"])) {\n    proxy.network = \"ws\";\n    $set(proxy, \"ws-opts.path\", params[\"wspath\"]);\n  }\n  \n  if (params[\"type\"]) {\n    let httpupgrade\n    proxy.network = params[\"type\"]\n    if(proxy.network === 'httpupgrade') {\n      proxy.network = 'ws'\n      httpupgrade = true\n    }\n    if (['grpc'].includes(proxy.network)) {\n        proxy[proxy.network + '-opts'] = {\n            'grpc-service-name': params[\"serviceName\"],\n            '_grpc-type': params[\"mode\"],\n            '_grpc-authority': params[\"authority\"],\n        };\n    } else {\n      if (params[\"path\"]) {\n        $set(proxy, proxy.network+\"-opts.path\", decodeURIComponent(params[\"path\"]));  \n      }\n      if (params[\"host\"]) {\n        $set(proxy, proxy.network+\"-opts.headers.Host\", decodeURIComponent(params[\"host\"])); \n      }\n      if (httpupgrade) {\n        $set(proxy, proxy.network+\"-opts.v2ray-http-upgrade\", true); \n        $set(proxy, proxy.network+\"-opts.v2ray-http-upgrade-fast-open\", true); \n      }\n    }\n    if (['reality'].includes(params.security)) {\n      const opts = {};\n      if (params.pbk) {\n        opts['public-key'] = params.pbk;\n      }\n      if (params.sid) {\n        opts['short-id'] = params.sid;\n      }\n      if (params.spx) {\n        opts['_spider-x'] = params.spx;\n      }\n      if (params.mode) {\n        proxy._mode = params.mode;\n      }\n      if (params.extra) {\n        proxy._extra = params.extra;\n      }\n      if (Object.keys(opts).length > 0) {\n        $set(proxy, params.security+\"-opts\", opts); \n      }\n    }\n  }\n\n  proxy.udp = toBool(params[\"udp\"]);\n  proxy.tfo = toBool(params[\"tfo\"]);\n}\n\nparam = kv/single;\n\nkv = key:$[a-z]i+ \"=\" value:$[^&#]i* {\n  params[key] = value;\n}\n\nsingle = key:$[a-z]i+ {\n  params[key] = true;\n};\n\nname = \"#\" + match:$.* {\n  return decodeURIComponent(match);\n}\n`;\nlet parser;\nexport default function getParser() {\n    if (!parser) {\n        parser = peggy.generate(grammars);\n    }\n    return parser;\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/parsers/peggy/trojan-uri.peg",
    "content": "// global initializer\n{{\n  function $set(obj, path, value) {\n    if (Object(obj) !== obj) return obj;\n    if (!Array.isArray(path)) path = path.toString().match(/[^.[\\]]+/g) || [];\n    path\n      .slice(0, -1)\n      .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[\n      path[path.length - 1]\n    ] = value;\n    return obj;\n  }\n\n  function toBool(str) {\n    if (typeof str === 'undefined' || str === null) return undefined;\n    return /(TRUE)|1/i.test(str);\n  }\n}}\n\n{\n  const proxy = {};\n  const obfs = {};\n  const $ = {};\n  const params = {};\n}\n\nstart = (trojan) {\n  return proxy\n}\n\ntrojan = \"trojan://\" password:password \"@\" server:server \":\" port:port \"/\"? params? name:name?{\n  proxy.type = \"trojan\";\n  proxy.password = password;\n  proxy.server = server;\n  proxy.port = port;\n  proxy.name = name;\n\n  // name may be empty\n  if (!proxy.name) {\n    proxy.name = server + \":\" + port;\n  }\n};\n\npassword = match:$[^@]+ {\n  return decodeURIComponent(match);\n};\n\nserver = ip/domain;\n\ndomain = match:[0-9a-zA-z-_.]+ { \n  const domain = match.join(\"\"); \n  if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {\n    return domain;\n  }\n}\n\nip = & {\n  const start = peg$currPos;\n  let end;\n  let j = start;\n  while (j < input.length) {\n    if (input[j] === \",\") break;\n    if (input[j] === \":\") end = j;\n    j++;\n  }\n  peg$currPos = end || j;\n  $.ip = input.substring(start, end).trim();\n  return true;\n} { return $.ip; }\n\nport = digits:[0-9]+ { \n  const port = parseInt(digits.join(\"\"), 10); \n  if (port >= 0 && port <= 65535) {\n    return port;\n  } else {\n    throw new Error(\"Invalid port: \" + port);\n  }\n}\n\nparams = \"?\" head:param tail:(\"&\"@param)* {\n  for (const [key, value] of Object.entries(params)) {\n    params[key] = decodeURIComponent(value);\n  }\n  proxy[\"skip-cert-verify\"] = toBool(params[\"allowInsecure\"]);\n  proxy.sni = params[\"sni\"] || params[\"peer\"];\n  proxy['client-fingerprint'] = params.fp;\n  proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;\n\n  if (toBool(params[\"ws\"])) {\n    proxy.network = \"ws\";\n    $set(proxy, \"ws-opts.path\", params[\"wspath\"]);\n  }\n  \n  if (params[\"type\"]) {\n    let httpupgrade\n    proxy.network = params[\"type\"]\n    if(proxy.network === 'httpupgrade') {\n      proxy.network = 'ws'\n      httpupgrade = true\n    }\n    if (['grpc'].includes(proxy.network)) {\n        proxy[proxy.network + '-opts'] = {\n            'grpc-service-name': params[\"serviceName\"],\n            '_grpc-type': params[\"mode\"],\n            '_grpc-authority': params[\"authority\"],\n        };\n    } else {\n      if (params[\"path\"]) {\n        $set(proxy, proxy.network+\"-opts.path\", decodeURIComponent(params[\"path\"]));  \n      }\n      if (params[\"host\"]) {\n        $set(proxy, proxy.network+\"-opts.headers.Host\", decodeURIComponent(params[\"host\"])); \n      }\n      if (httpupgrade) {\n        $set(proxy, proxy.network+\"-opts.v2ray-http-upgrade\", true); \n        $set(proxy, proxy.network+\"-opts.v2ray-http-upgrade-fast-open\", true); \n      }\n    }\n    if (['reality'].includes(params.security)) {\n      const opts = {};\n      if (params.pbk) {\n        opts['public-key'] = params.pbk;\n      }\n      if (params.sid) {\n        opts['short-id'] = params.sid;\n      }\n      if (params.spx) {\n        opts['_spider-x'] = params.spx;\n      }\n      if (params.mode) {\n        proxy._mode = params.mode;\n      }\n      if (params.extra) {\n        proxy._extra = params.extra;\n      }\n      if (Object.keys(opts).length > 0) {\n        $set(proxy, params.security+\"-opts\", opts); \n      }\n    }\n  }\n\n  proxy.udp = toBool(params[\"udp\"]);\n  proxy.tfo = toBool(params[\"tfo\"]);\n}\n\nparam = kv/single;\n\nkv = key:$[a-z]i+ \"=\" value:$[^&#]i* {\n  params[key] = value;\n}\n\nsingle = key:$[a-z]i+ {\n  params[key] = true;\n};\n\nname = \"#\" + match:$.* {\n  return decodeURIComponent(match);\n}"
  },
  {
    "path": "backend/src/core/proxy-utils/preprocessors/index.js",
    "content": "import { safeLoad } from '@/utils/yaml';\nimport { Base64 } from 'js-base64';\nimport $ from '@/core/app';\n\nfunction HTML() {\n    const name = 'HTML';\n    const test = (raw) => /^<!DOCTYPE html>/.test(raw);\n    // simply discard HTML\n    const parse = () => '';\n    return { name, test, parse };\n}\n\nfunction Base64Encoded() {\n    const name = 'Base64 Pre-processor';\n\n    const keys = [\n        'dm1lc3M', // vmess\n        'c3NyOi8v', // ssr://\n        'c29ja3M6Ly', // socks://\n        'dHJvamFu', // trojan\n        'c3M6Ly', // ss:/\n        'c3NkOi8v', // ssd://\n        'c2hhZG93', // shadow\n        'aHR0c', // htt\n        'dmxlc3M=', // vless\n        'aHlzdGVyaWEy', // hysteria2\n        'aHkyOi8v', // hy2://\n        'd2lyZWd1YXJkOi8v', // wireguard://\n        'd2c6Ly8=', // wg://\n        'dHVpYzovLw==', // tuic://\n    ];\n\n    const test = function (raw) {\n        return (\n            !/^\\w+:\\/\\/\\w+/im.test(raw) &&\n            keys.some((k) => raw.indexOf(k) !== -1)\n        );\n    };\n    const parse = function (raw) {\n        const decoded = Base64.decode(raw);\n        if (!/^\\w+(:\\/\\/|\\s*?=\\s*?)\\w+/m.test(decoded)) {\n            $.error(\n                `Base64 Pre-processor error: decoded line does not start with protocol`,\n            );\n            return raw;\n        }\n\n        return decoded;\n    };\n    return { name, test, parse };\n}\n\nfunction fallbackBase64Encoded() {\n    const name = 'Fallback Base64 Pre-processor';\n\n    const test = function (raw) {\n        return true;\n    };\n    const parse = function (raw) {\n        const decoded = Base64.decode(raw);\n        if (!/^\\w+(:\\/\\/|\\s*?=\\s*?)\\w+/m.test(decoded)) {\n            $.error(\n                `Fallback Base64 Pre-processor error: decoded line does not start with protocol`,\n            );\n            return raw;\n        }\n\n        return decoded;\n    };\n    return { name, test, parse };\n}\n\nfunction Clash() {\n    const name = 'Clash Pre-processor';\n    const test = function (raw) {\n        if (!/proxies/.test(raw)) return false;\n        const content = safeLoad(raw);\n        return content.proxies && Array.isArray(content.proxies);\n    };\n    const parse = function (raw, includeProxies) {\n        // Clash YAML format\n\n        // 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity\n        // 匹配 short-id 冒号后面的值(包含空格和引号)\n        const afterReplace = raw.replace(\n            /short-id:([ \\t]*[^#\\n,}]*)/g,\n            (matched, value) => {\n                const afterTrim = value.trim();\n\n                // 为空\n                if (!afterTrim || afterTrim === '') {\n                    return 'short-id: \"\"';\n                }\n\n                // 是否被引号包裹\n                if (/^(['\"]).*\\1$/.test(afterTrim)) {\n                    return `short-id: ${afterTrim}`;\n                } else if (['null'].includes(afterTrim)) {\n                    return `short-id: ${afterTrim}`;\n                } else {\n                    return `short-id: \"${afterTrim}\"`;\n                }\n            },\n        );\n\n        const { proxies } = safeLoad(afterReplace);\n        return (\n            (includeProxies ? 'proxies:\\n' : '') +\n            proxies\n                .map((p) => {\n                    return `${includeProxies ? '  - ' : ''}${JSON.stringify(\n                        p,\n                    )}\\n`;\n                })\n                .join('')\n        );\n    };\n    return { name, test, parse };\n}\n\nfunction SSD() {\n    const name = 'SSD Pre-processor';\n    const test = function (raw) {\n        return raw.indexOf('ssd://') === 0;\n    };\n    const parse = function (raw) {\n        // preprocessing for SSD subscription format\n        const output = [];\n        let ssdinfo = JSON.parse(Base64.decode(raw.split('ssd://')[1]));\n        let port = ssdinfo.port;\n        let method = ssdinfo.encryption;\n        let password = ssdinfo.password;\n        // servers config\n        let servers = ssdinfo.servers;\n        for (let i = 0; i < servers.length; i++) {\n            let server = servers[i];\n            method = server.encryption ? server.encryption : method;\n            password = server.password ? server.password : password;\n            let userinfo = Base64.encode(method + ':' + password);\n            let hostname = server.server;\n            port = server.port ? server.port : port;\n            let tag = server.remarks ? server.remarks : i;\n            let plugin = server.plugin_options\n                ? '/?plugin=' +\n                  encodeURIComponent(\n                      server.plugin + ';' + server.plugin_options,\n                  )\n                : '';\n            output[i] =\n                'ss://' +\n                userinfo +\n                '@' +\n                hostname +\n                ':' +\n                port +\n                plugin +\n                '#' +\n                tag;\n        }\n        return output.join('\\n');\n    };\n    return { name, test, parse };\n}\n\nfunction FullConfig() {\n    const name = 'Full Config Preprocessor';\n    const test = function (raw) {\n        return /^(\\[server_local\\]|\\[Proxy\\])/gm.test(raw);\n    };\n    const parse = function (raw) {\n        const match = raw.match(\n            /^\\[server_local|Proxy\\]([\\s\\S]+?)^\\[.+?\\](\\r?\\n|$)/im,\n        )?.[1];\n        return match || raw;\n    };\n    return { name, test, parse };\n}\n\nexport default [\n    HTML(),\n    Clash(),\n    Base64Encoded(),\n    SSD(),\n    FullConfig(),\n    fallbackBase64Encoded(),\n];\n"
  },
  {
    "path": "backend/src/core/proxy-utils/processors/index.js",
    "content": "import resourceCache from '@/utils/resource-cache';\nimport scriptResourceCache from '@/utils/script-resource-cache';\nimport { isIPv4, isIPv6, ipAddress } from '@/utils';\nimport { FULL } from '@/utils/logical';\nimport { getFlag, removeFlag } from '@/utils/geo';\nimport { doh } from '@/utils/dns';\nimport lodash from 'lodash';\nimport $ from '@/core/app';\nimport { hex_md5 } from '@/vendor/md5';\nimport { ProxyUtils } from '@/core/proxy-utils';\nimport { produceArtifact } from '@/restful/sync';\nimport { SETTINGS_KEY } from '@/constants';\nimport YAML from '@/utils/yaml';\n\nimport env from '@/utils/env';\nimport {\n    getFlowField,\n    getFlowHeaders,\n    parseFlowHeaders,\n    validCheck,\n    flowTransfer,\n    getRmainingDays,\n    normalizeFlowHeader,\n} from '@/utils/flow';\n\nfunction isObject(item) {\n    return item && typeof item === 'object' && !Array.isArray(item);\n}\nfunction trimWrap(str) {\n    if (str.startsWith('<') && str.endsWith('>')) {\n        return str.slice(1, -1);\n    }\n    return str;\n}\nfunction deepMerge(target, _other) {\n    const other = typeof _other === 'string' ? JSON.parse(_other) : _other;\n    for (const key in other) {\n        if (isObject(other[key])) {\n            if (key.endsWith('!')) {\n                const k = trimWrap(key.slice(0, -1));\n                target[k] = other[key];\n            } else {\n                const k = trimWrap(key);\n                if (!target[k]) Object.assign(target, { [k]: {} });\n                deepMerge(target[k], other[k]);\n            }\n        } else if (Array.isArray(other[key])) {\n            if (key.startsWith('+')) {\n                const k = trimWrap(key.slice(1));\n                if (!target[k]) Object.assign(target, { [k]: [] });\n                target[k] = [...other[key], ...target[k]];\n            } else if (key.endsWith('+')) {\n                const k = trimWrap(key.slice(0, -1));\n                if (!target[k]) Object.assign(target, { [k]: [] });\n                target[k] = [...target[k], ...other[key]];\n            } else {\n                const k = trimWrap(key);\n                Object.assign(target, { [k]: other[key] });\n            }\n        } else {\n            Object.assign(target, { [key]: other[key] });\n        }\n    }\n    return target;\n}\n/**\n The rule \"(name CONTAINS \"🇨🇳\") AND (port IN [80, 443])\" can be expressed as follows:\n {\n    operator: \"AND\",\n    child: [\n        {\n            attr: \"name\",\n            proposition: \"CONTAINS\",\n            value: \"🇨🇳\"\n        },\n        {\n            attr: \"port\",\n            proposition: \"IN\",\n            value: [80, 443]\n        }\n    ]\n}\n */\n\nfunction ConditionalFilter({ rule }) {\n    return {\n        name: 'Conditional Filter',\n        func: (proxies) => {\n            return proxies.map((proxy) => isMatch(rule, proxy));\n        },\n    };\n}\n\nfunction isMatch(rule, proxy) {\n    // leaf node\n    if (!rule.operator) {\n        switch (rule.proposition) {\n            case 'IN':\n                return rule.value.indexOf(proxy[rule.attr]) !== -1;\n            case 'CONTAINS':\n                if (typeof proxy[rule.attr] !== 'string') return false;\n                return proxy[rule.attr].indexOf(rule.value) !== -1;\n            case 'EQUALS':\n                return proxy[rule.attr] === rule.value;\n            case 'EXISTS':\n                return (\n                    proxy[rule.attr] !== null ||\n                    typeof proxy[rule.attr] !== 'undefined'\n                );\n            default:\n                throw new Error(`Unknown proposition: ${rule.proposition}`);\n        }\n    }\n\n    // operator nodes\n    switch (rule.operator) {\n        case 'AND':\n            return rule.child.every((child) => isMatch(child, proxy));\n        case 'OR':\n            return rule.child.some((child) => isMatch(child, proxy));\n        case 'NOT':\n            return !isMatch(rule.child, proxy);\n        default:\n            throw new Error(`Unknown operator: ${rule.operator}`);\n    }\n}\n\nfunction QuickSettingOperator(args) {\n    return {\n        name: 'Quick Setting Operator',\n        func: (proxies) => {\n            if (get(args.useless)) {\n                const filter = UselessFilter();\n                const selected = filter.func(proxies);\n                proxies = proxies.filter(\n                    (p, i) => selected[i] && p.port > 0 && p.port <= 65535,\n                );\n            }\n\n            return proxies.map((proxy) => {\n                proxy.udp = get(args.udp, proxy.udp);\n                proxy.tfo = get(args.tfo, proxy.tfo);\n                proxy['fast-open'] = get(args.tfo, proxy['fast-open']);\n                proxy['skip-cert-verify'] = get(\n                    args.scert,\n                    proxy['skip-cert-verify'],\n                );\n                if (proxy.type === 'vmess') {\n                    proxy.aead = get(args['vmess aead'], proxy.aead);\n                }\n                return proxy;\n            });\n        },\n    };\n\n    function get(value, defaultValue) {\n        switch (value) {\n            case 'ENABLED':\n                return true;\n            case 'DISABLED':\n                return false;\n            default:\n                return defaultValue;\n        }\n    }\n}\n\n// add or remove flag for proxies\nfunction FlagOperator({ mode, tw }) {\n    return {\n        name: 'Flag Operator',\n        func: (proxies) => {\n            return proxies.map((proxy) => {\n                if (mode === 'remove') {\n                    // no flag\n                    proxy.name = removeFlag(proxy.name);\n                } else {\n                    // get flag\n                    const newFlag = getFlag(proxy.name);\n                    // remove old flag\n                    proxy.name = removeFlag(proxy.name);\n                    proxy.name = newFlag + ' ' + proxy.name;\n                    if (tw == 'ws') {\n                        proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸');\n                    } else if (tw == 'tw') {\n                        // 不变\n                    } else {\n                        proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');\n                    }\n                }\n                return proxy;\n            });\n        },\n    };\n}\n\n// duplicate handler\nfunction HandleDuplicateOperator(arg) {\n    const { action, template, link, position, field } = {\n        ...{\n            action: 'rename',\n            template: '0 1 2 3 4 5 6 7 8 9',\n            link: '-',\n            position: 'back',\n            field: ['name'],\n        },\n        ...arg,\n    };\n    return {\n        name: 'Handle Duplicate Operator',\n        func: (proxies) => {\n            if (action === 'delete') {\n                const chosen = {};\n                return proxies.filter((p) => {\n                    const key = field\n                        .map((f) => lodash.get(p, f, '-'))\n                        .join('_');\n                    if (chosen[key]) {\n                        return false;\n                    }\n                    chosen[key] = true;\n                    return true;\n                });\n            } else if (action === 'rename') {\n                const numbers = template.split(' ');\n                // count occurrences of each name\n                const counter = {};\n                let maxLen = 0;\n                proxies.forEach((p) => {\n                    const key = field\n                        .map((f) => lodash.get(p, f, '-'))\n                        .join('_');\n                    if (typeof counter[key] === 'undefined') counter[key] = 1;\n                    else counter[key]++;\n                    maxLen = Math.max(counter[key].toString().length, maxLen);\n                });\n                const increment = {};\n                return proxies.map((p) => {\n                    const key = field\n                        .map((f) => lodash.get(p, f, '-'))\n                        .join('_');\n                    if (counter[key] > 1) {\n                        if (typeof increment[key] == 'undefined')\n                            increment[key] = 1;\n                        let num = '';\n                        let cnt = increment[key]++;\n                        let numDigits = 0;\n                        while (cnt > 0) {\n                            num = numbers[cnt % 10] + num;\n                            cnt = parseInt(cnt / 10);\n                            numDigits++;\n                        }\n                        // padding\n                        while (numDigits++ < maxLen) {\n                            num = numbers[0] + num;\n                        }\n                        if (position === 'front') {\n                            p.name = num + link + p.name;\n                        } else if (position === 'back') {\n                            p.name = p.name + link + num;\n                        }\n                    }\n                    return p;\n                });\n            }\n        },\n    };\n}\n\n// sort proxies according to their names\nfunction SortOperator(order = 'asc') {\n    return {\n        name: 'Sort Operator',\n        func: (proxies) => {\n            switch (order) {\n                case 'asc':\n                case 'desc':\n                    return proxies.sort((a, b) => {\n                        let res = a.name > b.name ? 1 : -1;\n                        res *= order === 'desc' ? -1 : 1;\n                        return res;\n                    });\n                case 'random':\n                    return shuffle(proxies);\n                default:\n                    throw new Error('Unknown sort option: ' + order);\n            }\n        },\n    };\n}\n\n// sort by regex\nfunction RegexSortOperator(input) {\n    const order = input.order || 'asc';\n    let expressions = input.expressions;\n    if (Array.isArray(input)) {\n        expressions = input;\n    }\n    if (!Array.isArray(expressions)) {\n        expressions = [];\n    }\n    return {\n        name: 'Regex Sort Operator',\n        func: (proxies) => {\n            expressions = expressions.map((expr) => buildRegex(expr));\n            return proxies.sort((a, b) => {\n                const oA = getRegexOrder(expressions, a.name);\n                const oB = getRegexOrder(expressions, b.name);\n                if (oA && !oB) return -1;\n                if (oB && !oA) return 1;\n                if (oA && oB) return oA < oB ? -1 : 1;\n                if (order === 'original') {\n                    return 0;\n                } else if (order === 'desc') {\n                    return a.name < b.name ? 1 : -1;\n                } else {\n                    return a.name < b.name ? -1 : 1;\n                }\n            });\n        },\n    };\n}\n\nfunction getRegexOrder(expressions, str) {\n    let order = null;\n    for (let i = 0; i < expressions.length; i++) {\n        if (expressions[i].test(str)) {\n            order = i + 1; // plus 1 is important! 0 will be treated as false!!!\n            break;\n        }\n    }\n    return order;\n}\n\n// rename by regex\n// keywords: [{expr: \"string format regex\", now: \"now\"}]\nfunction RegexRenameOperator(regex) {\n    return {\n        name: 'Regex Rename Operator',\n        func: (proxies) => {\n            return proxies.map((proxy) => {\n                for (const { expr, now } of regex) {\n                    proxy.name = proxy.name\n                        .replace(buildRegex(expr, 'g'), now)\n                        .trim();\n                }\n                return proxy;\n            });\n        },\n    };\n}\n\n// delete regex operator\n// regex: ['a', 'b', 'c']\nfunction RegexDeleteOperator(regex) {\n    const regex_ = regex.map((r) => {\n        return {\n            expr: r,\n            now: '',\n        };\n    });\n    return {\n        name: 'Regex Delete Operator',\n        func: RegexRenameOperator(regex_).func,\n    };\n}\n\n/** Script Operator\n function operator(proxies) {\n            const {arg1} = $arguments;\n\n            // do something\n            return proxies;\n         }\n\n WARNING:\n 1. This function name should be `operator`!\n 2. Always declare variables before using them!\n */\nfunction ScriptOperator(\n    script,\n    targetPlatform,\n    $arguments,\n    source,\n    $options,\n    context,\n) {\n    context.source = source;\n    context.env = env;\n    return {\n        name: 'Script Operator',\n        func: async (proxies) => {\n            let output = proxies;\n            if (output?.$file?.type === 'mihomoProfile') {\n                try {\n                    let patch = YAML.safeLoad(script);\n                    let config;\n                    if (output?.$content) {\n                        try {\n                            config = YAML.safeLoad(output?.$content);\n                        } catch (e) {\n                            $.error(e.message ?? e);\n                        }\n                    }\n                    // if (typeof patch !== 'object') patch = {};\n                    if (typeof patch !== 'object')\n                        throw new Error('patch is not an object');\n                    output.$content = ProxyUtils.yaml.safeDump(\n                        deepMerge(\n                            config ||\n                                (output?.$file?.sourceType === 'none'\n                                    ? {}\n                                    : {\n                                          proxies: await produceArtifact({\n                                              type:\n                                                  output?.$file?.sourceType ||\n                                                  'collection',\n                                              name: output?.$file?.sourceName,\n                                              platform: 'mihomo',\n                                              produceType: 'internal',\n                                              produceOpts: {\n                                                  'delete-underscore-fields': true,\n                                              },\n                                          }),\n                                      }),\n                            patch,\n                        ),\n                    );\n                    return output;\n                } catch (e) {\n                    // console.log(e);\n                }\n            }\n            await (async function () {\n                const operator = createDynamicFunction(\n                    'operator',\n                    script,\n                    $arguments,\n                    $options,\n                );\n                output = operator(proxies, targetPlatform, context);\n            })();\n            return output;\n        },\n        nodeFunc: async (proxies) => {\n            let output = proxies;\n            await (async function () {\n                const operator = createDynamicFunction(\n                    'operator',\n                    `async function operator(input = [], targetPlatform, context) {\n                        if (input && (input.$files || input.$content)) {\n                            let { $content, $files, $options, $file } = input\n                            if($file.type === 'mihomoProfile') {\n                                ${script}\n                                if(typeof main === 'function') {\n                                    let config;\n                                    if ($content) {\n                                        try {\n                                            config = ProxyUtils.yaml.safeLoad($content);\n                                        } catch (e) {\n                                            console.log(e.message ?? e);\n                                        }\n                                    }\n                                    $content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {\n                                        proxies: await produceArtifact({\n                                            type: $file.sourceType || 'collection',\n                                            name: $file.sourceName,\n                                            platform: 'mihomo',\n                                            produceType: 'internal',\n                                            produceOpts: {\n                                                'delete-underscore-fields': true\n                                            }\n                                        }),\n                                    })))\n                                }\n                            } else {\n                                ${script}\n                            }\n                            return { $content, $files, $options, $file }\n                        } else {\n                            let proxies = input\n                            let list = []\n                            for await (let $server of proxies) {\n                                ${script}\n                                list.push($server)\n                            }\n                            return list\n                        }\n                      }`,\n                    $arguments,\n                    $options,\n                );\n                output = operator(proxies, targetPlatform, context);\n            })();\n            return output;\n        },\n    };\n}\n\nfunction parseIP4P(IP4P) {\n    let server;\n    let port;\n    try {\n        let array = IP4P.split(':');\n\n        port = parseInt(array[2], 16);\n        let ipab = parseInt(array[3], 16);\n        let ipcd = parseInt(array[4], 16);\n        let ipa = ipab >> 8;\n        let ipb = ipab & 0xff;\n        let ipc = ipcd >> 8;\n        let ipd = ipcd & 0xff;\n        server = `${ipa}.${ipb}.${ipc}.${ipd}`;\n        if (port <= 0 || port > 65535) {\n            throw new Error(`Invalid port number: ${port}`);\n        }\n        if (!isIPv4(server)) {\n            throw new Error(`Invalid IP address: ${server}`);\n        }\n    } catch (e) {\n        // throw new Error(`IP4P 解析失败: ${e}`);\n        $.error(`IP4P 解析失败: ${e}`);\n    }\n    return { server, port };\n}\n\nconst DOMAIN_RESOLVERS = {\n    Custom: async function (domain, type, noCache, timeout, edns, url) {\n        const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);\n        const cached = resourceCache.get(id);\n        if (!noCache && cached) return cached;\n        const answerType = type === 'IPv6' ? 'AAAA' : 'A';\n        const res = await doh({\n            url,\n            domain,\n            type: answerType,\n            timeout,\n            edns,\n        });\n\n        const { answers } = res;\n        if (!Array.isArray(answers) || answers.length === 0) {\n            throw new Error('No answers');\n        }\n        const result = answers\n            .filter((i) => i?.type === answerType)\n            .map((i) => i?.data)\n            .filter((i) => i);\n        if (result.length === 0) {\n            throw new Error('No answers');\n        }\n        resourceCache.set(id, result);\n        return result;\n    },\n    Google: async function (domain, type, noCache, timeout, edns) {\n        const id = hex_md5(`GOOGLE:${domain}:${type}`);\n        const cached = resourceCache.get(id);\n        if (!noCache && cached) return cached;\n        const answerType = type === 'IPv6' ? 'AAAA' : 'A';\n        const res = await doh({\n            url: 'https://8.8.4.4/dns-query',\n            domain,\n            type: answerType,\n            timeout,\n            edns,\n        });\n\n        const { answers } = res;\n        if (!Array.isArray(answers) || answers.length === 0) {\n            throw new Error('No answers');\n        }\n        const result = answers\n            .filter((i) => i?.type === answerType)\n            .map((i) => i?.data)\n            .filter((i) => i);\n        if (result.length === 0) {\n            throw new Error('No answers');\n        }\n        resourceCache.set(id, result);\n        return result;\n    },\n    'IP-API': async function (domain, type, noCache, timeout) {\n        if (['IPv6'].includes(type)) {\n            throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);\n        }\n        const id = hex_md5(`IP-API:${domain}`);\n        const cached = resourceCache.get(id);\n        if (!noCache && cached) return cached;\n        const resp = await $.http.get({\n            url: `http://ip-api.com/json/${encodeURIComponent(\n                domain,\n            )}?lang=zh-CN`,\n            timeout,\n        });\n        const body = JSON.parse(resp.body);\n        if (body['status'] !== 'success') {\n            throw new Error(`Status is ${body['status']}`);\n        }\n        if (!body.query || body.query === 0) {\n            throw new Error('No answers');\n        }\n        const result = [body.query];\n        if (result.length === 0) {\n            throw new Error('No answers');\n        }\n        resourceCache.set(id, result);\n        return result;\n    },\n    Cloudflare: async function (domain, type, noCache, timeout, edns) {\n        const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);\n        const cached = resourceCache.get(id);\n        if (!noCache && cached) return cached;\n        const answerType = type === 'IPv6' ? 'AAAA' : 'A';\n        const res = await doh({\n            url: 'https://1.0.0.1/dns-query',\n            domain,\n            type: answerType,\n            timeout,\n            edns,\n        });\n\n        const { answers } = res;\n        if (!Array.isArray(answers) || answers.length === 0) {\n            throw new Error('No answers');\n        }\n        const result = answers\n            .filter((i) => i?.type === answerType)\n            .map((i) => i?.data)\n            .filter((i) => i);\n        if (result.length === 0) {\n            throw new Error('No answers');\n        }\n        resourceCache.set(id, result);\n        return result;\n    },\n    Ali: async function (domain, type, noCache, timeout, edns) {\n        const id = hex_md5(`ALI:${domain}:${type}`);\n        const cached = resourceCache.get(id);\n        if (!noCache && cached) return cached;\n        const resp = await $.http.get({\n            url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/${\n                isIPv4(edns) ? 24 : 56\n            }&name=${encodeURIComponent(domain)}&type=${\n                type === 'IPv6' ? 'AAAA' : 'A'\n            }&short=1`,\n            headers: {\n                accept: 'application/dns-json',\n            },\n            timeout,\n        });\n        const answers = JSON.parse(resp.body);\n        if (!Array.isArray(answers) || answers.length === 0) {\n            throw new Error('No answers');\n        }\n        const result = answers;\n        if (result.length === 0) {\n            throw new Error('No answers');\n        }\n        resourceCache.set(id, result);\n        return result;\n    },\n    Tencent: async function (domain, type, noCache, timeout, edns) {\n        const id = hex_md5(`TENCENT:${domain}:${type}`);\n        const cached = resourceCache.get(id);\n        if (!noCache && cached) return cached;\n        const resp = await $.http.get({\n            url: `http://119.28.28.28/d?ip=${edns}&type=${\n                type === 'IPv6' ? 'AAAA' : 'A'\n            }&dn=${encodeURIComponent(domain)}`,\n            headers: {\n                accept: 'application/dns-json',\n            },\n            timeout,\n        });\n        const answers = resp.body.split(';').map((i) => i.split(',')[0]);\n        if (answers.length === 0 || String(answers) === '0') {\n            throw new Error('No answers');\n        }\n        const result = answers;\n        if (result.length === 0) {\n            throw new Error('No answers');\n        }\n        resourceCache.set(id, result);\n        return result;\n    },\n};\n\nfunction ResolveDomainOperator({\n    provider,\n    type: _type,\n    filter,\n    cache,\n    url,\n    timeout,\n    edns: _edns,\n}) {\n    if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {\n        throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);\n    }\n    const { defaultTimeout } = $.read(SETTINGS_KEY);\n    const requestTimeout = timeout || defaultTimeout || 8000;\n    let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';\n\n    const resolver = DOMAIN_RESOLVERS[provider];\n    if (!resolver) {\n        throw new Error(`找不到域名解析服务提供方: ${provider}`);\n    }\n    let edns = _edns || '223.6.6.6';\n    if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);\n    $.info(\n        `Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,\n    );\n    return {\n        name: 'Resolve Domain Operator',\n        func: async (proxies) => {\n            proxies.forEach((p, i) => {\n                if (!p['_no-resolve'] && p['no-resolve']) {\n                    proxies[i]['_no-resolve'] = p['no-resolve'];\n                }\n            });\n            const results = {};\n            const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.\n            const totalDomain = [\n                ...new Set(\n                    proxies\n                        .filter((p) => !isIP(p.server) && !p['_no-resolve'])\n                        .map((c) => c.server),\n                ),\n            ];\n            const totalBatch = Math.ceil(totalDomain.length / limit);\n            for (let i = 0; i < totalBatch; i++) {\n                const currentBatch = [];\n                for (let domain of totalDomain.splice(0, limit)) {\n                    currentBatch.push(\n                        resolver(\n                            domain,\n                            type,\n                            cache === 'disabled',\n                            requestTimeout,\n                            edns,\n                            url,\n                        )\n                            .then((ip) => {\n                                results[domain] = ip;\n                                $.info(\n                                    `Successfully resolved domain: ${domain} ➟ ${ip}`,\n                                );\n                            })\n                            .catch((err) => {\n                                $.error(\n                                    `Failed to resolve domain: ${domain} with resolver [${provider}]: ${err}`,\n                                );\n                            }),\n                    );\n                }\n                await Promise.all(currentBatch);\n            }\n            proxies.forEach((p) => {\n                if (!p['_no-resolve']) {\n                    if (results[p.server]) {\n                        p._resolved_ips = results[p.server];\n                        let ip = Array.isArray(results[p.server])\n                            ? results[p.server][\n                                  Math.floor(\n                                      Math.random() * results[p.server].length,\n                                  )\n                              ]\n                            : results[p.server];\n                        if (type === 'IPv6' && isIPv6(ip)) {\n                            try {\n                                ip = new ipAddress.Address6(ip).correctForm();\n                            } catch (e) {\n                                $.error(\n                                    `Failed to parse IPv6 address: ${ip}: ${e}`,\n                                );\n                            }\n                            if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) {\n                                p._IP4P = ip;\n                                const { server, port } = parseIP4P(ip);\n                                if (server && port) {\n                                    p._domain = p.server;\n                                    p.server = server;\n                                    p.port = port;\n                                    p.resolved = true;\n                                    p._IPv4 = p.server;\n                                    if (!isIP(p._IP)) {\n                                        p._IP = p.server;\n                                    }\n                                } else if (!p.resolved) {\n                                    p.resolved = false;\n                                }\n                            } else {\n                                p._domain = p.server;\n                                p.server = ip;\n                                p.resolved = true;\n                                p[`_${type}`] = p.server;\n                                if (!isIP(p._IP)) {\n                                    p._IP = p.server;\n                                }\n                            }\n                        } else {\n                            p._domain = p.server;\n                            p.server = ip;\n                            p.resolved = true;\n                            p[`_${type}`] = p.server;\n                            if (!isIP(p._IP)) {\n                                p._IP = p.server;\n                            }\n                        }\n                    } else if (!p.resolved) {\n                        p.resolved = false;\n                    }\n                }\n            });\n\n            return proxies.filter((p) => {\n                if (filter === 'removeFailed') {\n                    return isIP(p.server) || p['_no-resolve'] || p.resolved;\n                } else if (filter === 'IPOnly') {\n                    return isIP(p.server);\n                } else if (filter === 'IPv4Only') {\n                    return isIPv4(p.server);\n                } else if (filter === 'IPv6Only') {\n                    return isIPv6(p.server);\n                } else {\n                    return true;\n                }\n            });\n        },\n    };\n}\n\nfunction isIP(ip) {\n    return isIPv4(ip) || isIPv6(ip);\n}\n\nResolveDomainOperator.resolver = DOMAIN_RESOLVERS;\n\nfunction isAscii(str) {\n    // eslint-disable-next-line no-control-regex\n    var pattern = /^[\\x00-\\x7F]+$/; // ASCII 范围的 Unicode 编码\n    return pattern.test(str);\n}\n\n/**************************** Filters ***************************************/\n// filter useless proxies\nfunction UselessFilter() {\n    return {\n        name: 'Useless Filter',\n        func: (proxies) => {\n            return proxies.map((proxy) => {\n                if (proxy.cipher && !isAscii(proxy.cipher)) {\n                    return false;\n                } else if (proxy.password && !isAscii(proxy.password)) {\n                    return false;\n                } else {\n                    if (proxy.network) {\n                        let transportHosts =\n                            proxy[`${proxy.network}-opts`]?.headers?.Host ||\n                            proxy[`${proxy.network}-opts`]?.headers?.host;\n                        transportHosts = Array.isArray(transportHosts)\n                            ? transportHosts\n                            : [transportHosts];\n                        if (\n                            transportHosts.some(\n                                (host) => host && !isAscii(host),\n                            )\n                        ) {\n                            return false;\n                        }\n                    }\n                    return !/网址|流量|时间|应急|过期|Bandwidth|expire/.test(\n                        proxy.name,\n                    );\n                }\n            });\n        },\n    };\n}\n\n// filter by regions\nfunction RegionFilter(input) {\n    let regions = input?.value || input;\n    if (!Array.isArray(regions)) {\n        regions = [];\n    }\n    const keep = input?.keep ?? true;\n    const REGION_MAP = {\n        HK: '🇭🇰',\n        TW: '🇹🇼',\n        US: '🇺🇸',\n        SG: '🇸🇬',\n        JP: '🇯🇵',\n        UK: '🇬🇧',\n        DE: '🇩🇪',\n        KR: '🇰🇷',\n    };\n    return {\n        name: 'Region Filter',\n        func: (proxies) => {\n            // this would be high memory usage\n            return proxies.map((proxy) => {\n                const flag = getFlag(proxy.name);\n                const selected = regions.some((r) => REGION_MAP[r] === flag);\n                return keep ? selected : !selected;\n            });\n        },\n    };\n}\n\n// filter by regex\nfunction RegexFilter({ regex = [], keep = true }) {\n    return {\n        name: 'Regex Filter',\n        func: (proxies) => {\n            return proxies.map((proxy) => {\n                const selected = regex.some((r) => {\n                    return buildRegex(r).test(proxy.name);\n                });\n                return keep ? selected : !selected;\n            });\n        },\n    };\n}\n\nfunction buildRegex(str, ...options) {\n    options = options.join('');\n    if (str.startsWith('(?i)')) {\n        str = str.substring(4);\n        return new RegExp(str, 'i' + options);\n    } else {\n        return new RegExp(str, options);\n    }\n}\n\n// filter by proxy types\nfunction TypeFilter(input) {\n    let types = input?.value || input;\n    if (!Array.isArray(types)) {\n        types = [];\n    }\n    const keep = input?.keep ?? true;\n    return {\n        name: 'Type Filter',\n        func: (proxies) => {\n            return proxies.map((proxy) => {\n                const selected = types.some((t) => proxy.type === t);\n                return keep ? selected : !selected;\n            });\n        },\n    };\n}\n\n/**\n Script Example\n\n function filter(proxies) {\n        return proxies.map(p => {\n            return p.name.indexOf('🇭🇰') !== -1;\n        });\n     }\n\n WARNING:\n 1. This function name should be `filter`!\n 2. Always declare variables before using them!\n */\nfunction ScriptFilter(\n    script,\n    targetPlatform,\n    $arguments,\n    source,\n    $options,\n    context,\n) {\n    context.source = source;\n    context.env = env;\n    return {\n        name: 'Script Filter',\n        func: async (proxies) => {\n            let output = FULL(proxies.length, true);\n            await (async function () {\n                const filter = createDynamicFunction(\n                    'filter',\n                    script,\n                    $arguments,\n                    $options,\n                );\n                output = filter(proxies, targetPlatform, context);\n            })();\n            return output;\n        },\n        nodeFunc: async (proxies) => {\n            let output = FULL(proxies.length, true);\n            await (async function () {\n                const filter = createDynamicFunction(\n                    'filter',\n                    `async function filter(input = [], targetPlatform, context) {\n                        let proxies = input\n                        let list = []\n                        const fn = async ($server) => {\n                            ${script}\n                        }\n                        for await (let $server of proxies) {\n                            list.push(await fn($server))\n                        }\n                        return list\n                      }`,\n                    $arguments,\n                    $options,\n                );\n                output = filter(proxies, targetPlatform, context);\n            })();\n            return output;\n        },\n    };\n}\n\nexport default {\n    'Useless Filter': UselessFilter,\n    'Region Filter': RegionFilter,\n    'Regex Filter': RegexFilter,\n    'Type Filter': TypeFilter,\n    'Script Filter': ScriptFilter,\n    'Conditional Filter': ConditionalFilter,\n\n    'Quick Setting Operator': QuickSettingOperator,\n    'Flag Operator': FlagOperator,\n    'Sort Operator': SortOperator,\n    'Regex Sort Operator': RegexSortOperator,\n    'Regex Rename Operator': RegexRenameOperator,\n    'Regex Delete Operator': RegexDeleteOperator,\n    'Script Operator': ScriptOperator,\n    'Handle Duplicate Operator': HandleDuplicateOperator,\n    'Resolve Domain Operator': ResolveDomainOperator,\n};\n\nasync function ApplyFilter(filter, objs) {\n    // select proxies\n    let selected = FULL(objs.length, true);\n    try {\n        selected = await filter.func(objs);\n    } catch (err) {\n        let funcErr = '';\n        let funcErrMsg = `${err.message ?? err}`;\n        if (funcErrMsg.includes('$server is not defined')) {\n            funcErr = '';\n        } else {\n            $.error(\n                `Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,\n            );\n            funcErr = `执行 function filter 失败 ${funcErrMsg}; `;\n        }\n        try {\n            selected = await filter.nodeFunc(objs);\n        } catch (err) {\n            $.error(\n                `Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`,\n            );\n            let nodeErr = '';\n            let nodeErrMsg = `${err.message ?? err}`;\n            if (funcErr && nodeErrMsg === funcErrMsg) {\n                nodeErr = '';\n                funcErr = `执行失败 ${funcErrMsg}`;\n            } else {\n                nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;\n            }\n            throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);\n        }\n    }\n    return objs.filter((_, i) => selected[i]);\n}\n\nasync function ApplyOperator(operator, objs) {\n    let output = clone(objs);\n    try {\n        const output_ = await operator.func(output);\n        if (output_) output = output_;\n    } catch (err) {\n        let funcErr = '';\n        let funcErrMsg = `${err.message ?? err}`;\n        if (\n            funcErrMsg.includes('$server is not defined') ||\n            funcErrMsg.includes('$content is not defined') ||\n            funcErrMsg.includes('$files is not defined') ||\n            output?.$files ||\n            output?.$content\n        ) {\n            funcErr = '';\n        } else {\n            $.error(\n                `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,\n            );\n            funcErr = `执行 function operator 失败 ${funcErrMsg}; `;\n        }\n        try {\n            const output_ = await operator.nodeFunc(output);\n            if (output_) output = output_;\n        } catch (err) {\n            $.error(\n                `Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`,\n            );\n            let nodeErr = '';\n            let nodeErrMsg = `${err.message ?? err}`;\n            if (funcErr && nodeErrMsg === funcErrMsg) {\n                nodeErr = '';\n                funcErr = `执行失败 ${funcErrMsg}`;\n            } else {\n                nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;\n            }\n            throw new Error(`脚本操作 ${funcErr}${nodeErr}`);\n        }\n    }\n    return output;\n}\n\nexport async function ApplyProcessor(processor, objs) {\n    if (processor.name.indexOf('Filter') !== -1) {\n        return ApplyFilter(processor, objs);\n    } else if (processor.name.indexOf('Operator') !== -1) {\n        return ApplyOperator(processor, objs);\n    }\n}\n\n// shuffle array\nfunction shuffle(array) {\n    let currentIndex = array.length,\n        temporaryValue,\n        randomIndex;\n\n    // While there remain elements to shuffle...\n    while (0 !== currentIndex) {\n        // Pick a remaining element...\n        randomIndex = Math.floor(Math.random() * currentIndex);\n        currentIndex -= 1;\n\n        // And swap it with the current element.\n        temporaryValue = array[currentIndex];\n        array[currentIndex] = array[randomIndex];\n        array[randomIndex] = temporaryValue;\n    }\n\n    return array;\n}\n\n// deep clone object\nfunction clone(object) {\n    return JSON.parse(JSON.stringify(object));\n}\n\nfunction createDynamicFunction(name, script, $arguments, $options) {\n    const flowUtils = {\n        getFlowField,\n        getFlowHeaders,\n        parseFlowHeaders,\n        flowTransfer,\n        validCheck,\n        getRmainingDays,\n        normalizeFlowHeader,\n    };\n    if ($.env.isLoon) {\n        return new Function(\n            '$arguments',\n            '$options',\n            '$substore',\n            'lodash',\n            '$persistentStore',\n            '$httpClient',\n            '$notification',\n            'ProxyUtils',\n            'yaml',\n            'Buffer',\n            'b64d',\n            'b64e',\n            'scriptResourceCache',\n            'flowUtils',\n            'produceArtifact',\n            'require',\n            `${script}\\n return ${name}`,\n        )(\n            $arguments,\n            $options,\n            $,\n            lodash,\n            // eslint-disable-next-line no-undef\n            $persistentStore,\n            // eslint-disable-next-line no-undef\n            $httpClient,\n            // eslint-disable-next-line no-undef\n            $notification,\n            ProxyUtils,\n            ProxyUtils.yaml,\n            ProxyUtils.Buffer,\n            ProxyUtils.Base64.decode,\n            ProxyUtils.Base64.encode,\n            scriptResourceCache,\n            flowUtils,\n            produceArtifact,\n            eval(`typeof require !== \"undefined\"`) ? require : undefined,\n        );\n    } else {\n        return new Function(\n            '$arguments',\n            '$options',\n            '$substore',\n            'lodash',\n            'ProxyUtils',\n            'yaml',\n            'Buffer',\n            'b64d',\n            'b64e',\n            'scriptResourceCache',\n            'flowUtils',\n            'produceArtifact',\n            'require',\n            `${script}\\n return ${name}`,\n        )(\n            $arguments,\n            $options,\n            $,\n            lodash,\n            ProxyUtils,\n            ProxyUtils.yaml,\n            ProxyUtils.Buffer,\n            ProxyUtils.Base64.decode,\n            ProxyUtils.Base64.encode,\n            scriptResourceCache,\n            flowUtils,\n            produceArtifact,\n            eval(`typeof require !== \"undefined\"`) ? require : undefined,\n        );\n    }\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/clash.js",
    "content": "import { isPresent } from '@/core/proxy-utils/producers/utils';\nimport $ from '@/core/app';\n\nexport default function Clash_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type, opts = {}) => {\n        // VLESS XTLS is not supported by Clash\n        // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532\n        // github.com/Dreamacro/clash/pull/2891/files\n        // filter unsupported proxies\n        // https://clash.wiki/configuration/outbound.html#shadowsocks\n        const list = proxies\n            .filter((proxy) => {\n                if (opts['include-unsupported-proxy']) return true;\n                if (\n                    ![\n                        'ss',\n                        'ssr',\n                        'vmess',\n                        'vless',\n                        'socks5',\n                        'http',\n                        'snell',\n                        'trojan',\n                        'wireguard',\n                    ].includes(proxy.type) ||\n                    (proxy.type === 'ss' &&\n                        ![\n                            'aes-128-gcm',\n                            'aes-192-gcm',\n                            'aes-256-gcm',\n                            'aes-128-cfb',\n                            'aes-192-cfb',\n                            'aes-256-cfb',\n                            'aes-128-ctr',\n                            'aes-192-ctr',\n                            'aes-256-ctr',\n                            'rc4-md5',\n                            'chacha20-ietf',\n                            'xchacha20',\n                            'chacha20-ietf-poly1305',\n                            'xchacha20-ietf-poly1305',\n                        ].includes(proxy.cipher)) ||\n                    (proxy.type === 'snell' && proxy.version >= 4) ||\n                    (proxy.type === 'vless' &&\n                        (typeof proxy.flow !== 'undefined' ||\n                            proxy['reality-opts']))\n                ) {\n                    return false;\n                } else if (\n                    ['ws'].includes(proxy.network) &&\n                    proxy['ws-opts']?.['v2ray-http-upgrade']\n                ) {\n                    return false;\n                } else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {\n                    $.error(\n                        `Clash 不支持前置代理字段. 已过滤节点 ${proxy.name}`,\n                    );\n                    return false;\n                }\n                return true;\n            })\n            .map((proxy) => {\n                if (proxy.type === 'vmess') {\n                    // handle vmess aead\n                    if (isPresent(proxy, 'aead')) {\n                        if (proxy.aead) {\n                            proxy.alterId = 0;\n                        }\n                        delete proxy.aead;\n                    }\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                    // https://dreamacro.github.io/clash/configuration/outbound.html#vmess\n                    if (\n                        isPresent(proxy, 'cipher') &&\n                        ![\n                            'auto',\n                            'aes-128-gcm',\n                            'chacha20-poly1305',\n                            'none',\n                        ].includes(proxy.cipher)\n                    ) {\n                        proxy.cipher = 'auto';\n                    }\n                } else if (proxy.type === 'wireguard') {\n                    proxy.keepalive =\n                        proxy.keepalive ?? proxy['persistent-keepalive'];\n                    proxy['persistent-keepalive'] = proxy.keepalive;\n                    proxy['preshared-key'] =\n                        proxy['preshared-key'] ?? proxy['pre-shared-key'];\n                    proxy['pre-shared-key'] = proxy['preshared-key'];\n                } else if (proxy.type === 'snell' && proxy.version < 3) {\n                    delete proxy.udp;\n                } else if (proxy.type === 'vless') {\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                }\n\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'http'\n                ) {\n                    let httpPath = proxy['http-opts']?.path;\n                    if (\n                        isPresent(proxy, 'http-opts.path') &&\n                        !Array.isArray(httpPath)\n                    ) {\n                        proxy['http-opts'].path = [httpPath];\n                    }\n                    let httpHost = proxy['http-opts']?.headers?.Host;\n                    if (\n                        isPresent(proxy, 'http-opts.headers.Host') &&\n                        !Array.isArray(httpHost)\n                    ) {\n                        proxy['http-opts'].headers.Host = [httpHost];\n                    }\n                }\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'h2'\n                ) {\n                    let path = proxy['h2-opts']?.path;\n                    if (\n                        isPresent(proxy, 'h2-opts.path') &&\n                        Array.isArray(path)\n                    ) {\n                        proxy['h2-opts'].path = path[0];\n                    }\n                    let host = proxy['h2-opts']?.headers?.host;\n                    if (\n                        isPresent(proxy, 'h2-opts.headers.Host') &&\n                        !Array.isArray(host)\n                    ) {\n                        proxy['h2-opts'].headers.host = [host];\n                    }\n                }\n                if (['ws'].includes(proxy.network)) {\n                    const networkPath = proxy[`${proxy.network}-opts`]?.path;\n                    if (networkPath) {\n                        const reg = /^(.*?)(?:\\?ed=(\\d+))?$/;\n                        // eslint-disable-next-line no-unused-vars\n                        const [_, path = '', ed = ''] = reg.exec(networkPath);\n                        proxy[`${proxy.network}-opts`].path = path;\n                        if (ed !== '') {\n                            proxy['ws-opts']['early-data-header-name'] =\n                                'Sec-WebSocket-Protocol';\n                            proxy['ws-opts']['max-early-data'] = parseInt(\n                                ed,\n                                10,\n                            );\n                        }\n                    } else {\n                        proxy[`${proxy.network}-opts`] =\n                            proxy[`${proxy.network}-opts`] || {};\n                        proxy[`${proxy.network}-opts`].path = '/';\n                    }\n                }\n                if (proxy['plugin-opts']?.tls) {\n                    if (isPresent(proxy, 'skip-cert-verify')) {\n                        proxy['plugin-opts']['skip-cert-verify'] =\n                            proxy['skip-cert-verify'];\n                    }\n                }\n                if (\n                    [\n                        'trojan',\n                        'tuic',\n                        'hysteria',\n                        'hysteria2',\n                        'juicity',\n                        'anytls',\n                        'trusttunnel',\n                        'naive',\n                    ].includes(proxy.type)\n                ) {\n                    delete proxy.tls;\n                }\n\n                if (proxy['tls-fingerprint']) {\n                    proxy.fingerprint = proxy['tls-fingerprint'];\n                }\n                delete proxy['tls-fingerprint'];\n\n                if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {\n                    delete proxy.tls;\n                }\n\n                delete proxy.subName;\n                delete proxy.collectionName;\n                delete proxy.id;\n                delete proxy.resolved;\n                delete proxy['no-resolve'];\n                if (type !== 'internal') {\n                    for (const key in proxy) {\n                        if (proxy[key] == null || /^_/i.test(key)) {\n                            delete proxy[key];\n                        }\n                    }\n                }\n                if (\n                    ['grpc'].includes(proxy.network) &&\n                    proxy[`${proxy.network}-opts`]\n                ) {\n                    delete proxy[`${proxy.network}-opts`]['_grpc-type'];\n                    delete proxy[`${proxy.network}-opts`]['_grpc-authority'];\n                }\n                return proxy;\n            });\n        return type === 'internal'\n            ? list\n            : 'proxies:\\n' +\n                  list\n                      .map((proxy) => '  - ' + JSON.stringify(proxy) + '\\n')\n                      .join('');\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/clashmeta.js",
    "content": "import { isPresent } from '@/core/proxy-utils/producers/utils';\n\nconst ipVersions = {\n    dual: 'dual',\n    'v4-only': 'ipv4',\n    'v6-only': 'ipv6',\n    'prefer-v4': 'ipv4-prefer',\n    'prefer-v6': 'ipv6-prefer',\n};\n\nexport default function ClashMeta_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type, opts = {}) => {\n        const list = proxies\n            .filter((proxy) => {\n                if (opts['include-unsupported-proxy']) return true;\n                if (proxy.type === 'snell' && proxy.version >= 4) {\n                    return false;\n                } else if (['juicity', 'naive'].includes(proxy.type)) {\n                    return false;\n                } else if (\n                    ['ss'].includes(proxy.type) &&\n                    ![\n                        'aes-128-ctr',\n                        'aes-192-ctr',\n                        'aes-256-ctr',\n                        'aes-128-cfb',\n                        'aes-192-cfb',\n                        'aes-256-cfb',\n                        'aes-128-gcm',\n                        'aes-192-gcm',\n                        'aes-256-gcm',\n                        'aes-128-ccm',\n                        'aes-192-ccm',\n                        'aes-256-ccm',\n                        'aes-128-gcm-siv',\n                        'aes-256-gcm-siv',\n                        'chacha20-ietf',\n                        'chacha20',\n                        'xchacha20',\n                        'chacha20-ietf-poly1305',\n                        'xchacha20-ietf-poly1305',\n                        'chacha8-ietf-poly1305',\n                        'xchacha8-ietf-poly1305',\n                        '2022-blake3-aes-128-gcm',\n                        '2022-blake3-aes-256-gcm',\n                        '2022-blake3-chacha20-poly1305',\n                        'lea-128-gcm',\n                        'lea-192-gcm',\n                        'lea-256-gcm',\n                        'rabbit128-poly1305',\n                        'aegis-128l',\n                        'aegis-256',\n                        'aez-384',\n                        'deoxys-ii-256-128',\n                        'rc4-md5',\n                        'none',\n                    ].includes(proxy.cipher)\n                ) {\n                    // https://wiki.metacubex.one/config/proxies/ss/#cipher\n                    return false;\n                } else if (\n                    ['anytls'].includes(proxy.type) &&\n                    proxy.network &&\n                    (!['tcp'].includes(proxy.network) ||\n                        (['tcp'].includes(proxy.network) &&\n                            proxy['reality-opts']))\n                ) {\n                    return false;\n                } else if (['xhttp'].includes(proxy.network)) {\n                    return false;\n                }\n                return true;\n            })\n            .map((proxy) => {\n                if (proxy['reality-opts'] && !proxy['client-fingerprint']) {\n                    proxy['client-fingerprint'] = 'chrome';\n                }\n                if (proxy.type === 'vmess') {\n                    // handle vmess aead\n                    if (isPresent(proxy, 'aead')) {\n                        if (proxy.aead) {\n                            proxy.alterId = 0;\n                        }\n                        delete proxy.aead;\n                    }\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400\n                    // https://stash.wiki/proxy-protocols/proxy-types#vmess\n                    if (\n                        isPresent(proxy, 'cipher') &&\n                        ![\n                            'auto',\n                            'none',\n                            'zero',\n                            'aes-128-gcm',\n                            'chacha20-poly1305',\n                        ].includes(proxy.cipher)\n                    ) {\n                        proxy.cipher = 'auto';\n                    }\n                } else if (proxy.type === 'tuic') {\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    }\n                    //  else {\n                    //     proxy.alpn = ['h3'];\n                    // }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                    }\n                    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197\n                    if (\n                        (!proxy.token || proxy.token.length === 0) &&\n                        !isPresent(proxy, 'version')\n                    ) {\n                        proxy.version = 5;\n                    }\n                } else if (proxy.type === 'hysteria') {\n                    // auth_str 将会在未来某个时候删除 但是有的机场不规范\n                    if (\n                        isPresent(proxy, 'auth_str') &&\n                        !isPresent(proxy, 'auth-str')\n                    ) {\n                        proxy['auth-str'] = proxy['auth_str'];\n                    }\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                    }\n                } else if (proxy.type === 'wireguard') {\n                    proxy.keepalive =\n                        proxy.keepalive ?? proxy['persistent-keepalive'];\n                    proxy['persistent-keepalive'] = proxy.keepalive;\n                    proxy['preshared-key'] =\n                        proxy['preshared-key'] ?? proxy['pre-shared-key'];\n                    proxy['pre-shared-key'] = proxy['preshared-key'];\n                } else if (proxy.type === 'snell' && proxy.version < 3) {\n                    delete proxy.udp;\n                } else if (proxy.type === 'vless') {\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                } else if (proxy.type === 'ss') {\n                    if (\n                        isPresent(proxy, 'shadow-tls-password') &&\n                        !isPresent(proxy, 'plugin')\n                    ) {\n                        proxy.plugin = 'shadow-tls';\n                        proxy['plugin-opts'] = {\n                            host: proxy['shadow-tls-sni'],\n                            password: proxy['shadow-tls-password'],\n                            version: proxy['shadow-tls-version'],\n                        };\n                        delete proxy['shadow-tls-password'];\n                        delete proxy['shadow-tls-sni'];\n                        delete proxy['shadow-tls-version'];\n                    }\n                }\n\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'http'\n                ) {\n                    let httpPath = proxy['http-opts']?.path;\n                    if (\n                        isPresent(proxy, 'http-opts.path') &&\n                        !Array.isArray(httpPath)\n                    ) {\n                        proxy['http-opts'].path = [httpPath];\n                    }\n                    let httpHost = proxy['http-opts']?.headers?.Host;\n                    if (\n                        isPresent(proxy, 'http-opts.headers.Host') &&\n                        !Array.isArray(httpHost)\n                    ) {\n                        proxy['http-opts'].headers.Host = [httpHost];\n                    }\n                }\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'h2'\n                ) {\n                    let path = proxy['h2-opts']?.path;\n                    if (\n                        isPresent(proxy, 'h2-opts.path') &&\n                        Array.isArray(path)\n                    ) {\n                        proxy['h2-opts'].path = path[0];\n                    }\n                    let host = proxy['h2-opts']?.headers?.host;\n                    if (\n                        isPresent(proxy, 'h2-opts.headers.Host') &&\n                        !Array.isArray(host)\n                    ) {\n                        proxy['h2-opts'].headers.host = [host];\n                    }\n                }\n                if (['ws'].includes(proxy.network)) {\n                    const networkPath = proxy[`${proxy.network}-opts`]?.path;\n                    if (networkPath) {\n                        const reg = /^(.*?)(?:\\?ed=(\\d+))?$/;\n                        // eslint-disable-next-line no-unused-vars\n                        const [_, path = '', ed = ''] = reg.exec(networkPath);\n                        proxy[`${proxy.network}-opts`].path = path;\n                        if (ed !== '') {\n                            proxy['ws-opts']['early-data-header-name'] =\n                                'Sec-WebSocket-Protocol';\n                            proxy['ws-opts']['max-early-data'] = parseInt(\n                                ed,\n                                10,\n                            );\n                        }\n                    } else {\n                        proxy[`${proxy.network}-opts`] =\n                            proxy[`${proxy.network}-opts`] || {};\n                        proxy[`${proxy.network}-opts`].path = '/';\n                    }\n                }\n\n                if (proxy['plugin-opts']?.tls) {\n                    if (isPresent(proxy, 'skip-cert-verify')) {\n                        proxy['plugin-opts']['skip-cert-verify'] =\n                            proxy['skip-cert-verify'];\n                    }\n                }\n                if (\n                    [\n                        'trojan',\n                        'tuic',\n                        'hysteria',\n                        'hysteria2',\n                        'juicity',\n                        'anytls',\n                        'trusttunnel',\n                        'naive',\n                    ].includes(proxy.type)\n                ) {\n                    delete proxy.tls;\n                }\n\n                if (proxy['tls-fingerprint']) {\n                    proxy.fingerprint = proxy['tls-fingerprint'];\n                }\n                delete proxy['tls-fingerprint'];\n\n                if (proxy['underlying-proxy']) {\n                    proxy['dialer-proxy'] = proxy['underlying-proxy'];\n                }\n                delete proxy['underlying-proxy'];\n\n                if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {\n                    delete proxy.tls;\n                }\n                delete proxy.subName;\n                delete proxy.collectionName;\n                delete proxy.id;\n                delete proxy.resolved;\n                delete proxy['no-resolve'];\n                if (type !== 'internal' || opts['delete-underscore-fields']) {\n                    for (const key in proxy) {\n                        if (proxy[key] == null || /^_/i.test(key)) {\n                            delete proxy[key];\n                        }\n                    }\n                }\n                if (\n                    ['grpc'].includes(proxy.network) &&\n                    proxy[`${proxy.network}-opts`]\n                ) {\n                    delete proxy[`${proxy.network}-opts`]['_grpc-type'];\n                    delete proxy[`${proxy.network}-opts`]['_grpc-authority'];\n                }\n\n                if (proxy['ip-version']) {\n                    proxy['ip-version'] =\n                        ipVersions[proxy['ip-version']] || proxy['ip-version'];\n                }\n                return proxy;\n            });\n\n        return type === 'internal'\n            ? list\n            : 'proxies:\\n' +\n                  list\n                      .map((proxy) => '  - ' + JSON.stringify(proxy) + '\\n')\n                      .join('');\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/egern.js",
    "content": "import { isPresent } from './utils';\n\nexport default function Egern_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type) => {\n        // https://egernapp.com/zh-CN/docs/configuration/proxies\n        const list = proxies\n            .filter((proxy) => {\n                if (\n                    ![\n                        'http',\n                        'https',\n                        'socks5',\n                        'ss',\n                        'trojan',\n                        'hysteria2',\n                        'vless',\n                        'vmess',\n                        'tuic',\n                        'wireguard',\n                        'anytls',\n                    ].includes(proxy.type) ||\n                    (proxy.type === 'ss' &&\n                        ((proxy.plugin === 'obfs' &&\n                            !['http', 'tls'].includes(\n                                proxy['plugin-opts']?.mode,\n                            )) ||\n                            ![\n                                'chacha20-ietf-poly1305',\n                                'chacha20-poly1305',\n                                'aes-256-gcm',\n                                'aes-128-gcm',\n                                'none',\n                                'tbale',\n                                'rc4',\n                                'rc4-md5',\n                                'aes-128-cfb',\n                                'aes-192-cfb',\n                                'aes-256-cfb',\n                                'aes-128-ctr',\n                                'aes-192-ctr',\n                                'aes-256-ctr',\n                                'bf-cfb',\n                                'camellia-128-cfb',\n                                'camellia-192-cfb',\n                                'camellia-256-cfb',\n                                'cast5-cfb',\n                                'des-cfb',\n                                'idea-cfb',\n                                'rc2-cfb',\n                                'seed-cfb',\n                                'salsa20',\n                                'chacha20',\n                                'chacha20-ietf',\n                                '2022-blake3-aes-128-gcm',\n                                '2022-blake3-aes-256-gcm',\n                            ].includes(proxy.cipher))) ||\n                    (proxy.type === 'vmess' &&\n                        !['http', 'ws', 'tcp'].includes(proxy.network) &&\n                        proxy.network) ||\n                    (proxy.type === 'trojan' &&\n                        !['http', 'ws', 'tcp'].includes(proxy.network) &&\n                        proxy.network) ||\n                    (proxy.type === 'vless' &&\n                        ((!['http', 'ws', 'tcp'].includes(proxy.network) &&\n                            proxy.network) ||\n                            (typeof proxy.flow !== 'undefined' &&\n                                !['xtls-rprx-vision', ''].includes(\n                                    proxy.flow,\n                                )))) ||\n                    (proxy.type === 'tuic' &&\n                        proxy.token &&\n                        proxy.token.length !== 0)\n                ) {\n                    return false;\n                } else if (\n                    ['anytls'].includes(proxy.type) &&\n                    proxy.network &&\n                    (!['tcp'].includes(proxy.network) ||\n                        (['tcp'].includes(proxy.network) &&\n                            proxy['reality-opts']))\n                ) {\n                    return false;\n                } else if (\n                    ['ws'].includes(proxy.network) &&\n                    proxy['ws-opts']?.['v2ray-http-upgrade']\n                ) {\n                    return false;\n                }\n                return true;\n            })\n            .map((proxy) => {\n                const original = { ...proxy };\n                let flow;\n                if (proxy.tls && !proxy.sni) {\n                    proxy.sni = proxy.server;\n                }\n                const prev_hop =\n                    proxy.prev_hop ||\n                    proxy['underlying-proxy'] ||\n                    proxy['dialer-proxy'] ||\n                    proxy.detour;\n\n                if (proxy.type === 'http') {\n                    proxy = {\n                        type: proxy.tls ? 'https' : 'http',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        username: proxy.username,\n                        password: proxy.password,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        next_hop: proxy.next_hop,\n                        ...(proxy.tls\n                            ? {\n                                  sni: proxy.sni,\n                                  skip_tls_verify: proxy['skip-cert-verify'],\n                              }\n                            : {}),\n                    };\n                } else if (proxy.type === 'socks5') {\n                    proxy = {\n                        type: 'socks5',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        username: proxy.username,\n                        password: proxy.password,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                    };\n                } else if (proxy.type === 'ss') {\n                    proxy = {\n                        type: 'shadowsocks',\n                        name: proxy.name,\n                        method:\n                            proxy.cipher === 'chacha20-ietf-poly1305'\n                                ? 'chacha20-poly1305'\n                                : proxy.cipher,\n                        server: proxy.server,\n                        port: proxy.port,\n                        password: proxy.password,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                    };\n                    if (original.plugin === 'obfs') {\n                        proxy.obfs = original['plugin-opts'].mode;\n                        proxy.obfs_host = original['plugin-opts'].host;\n                        proxy.obfs_uri = original['plugin-opts'].path;\n                    }\n                } else if (proxy.type === 'hysteria2') {\n                    proxy = {\n                        type: 'hysteria2',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        auth: proxy.password,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                        sni: proxy.sni,\n                        skip_tls_verify: proxy['skip-cert-verify'],\n                        port_hopping: proxy.ports,\n                        port_hopping_interval: proxy['hop-interval'],\n                    };\n                    if (\n                        original['obfs-password'] &&\n                        original.obfs == 'salamander'\n                    ) {\n                        proxy.obfs = 'salamander';\n                        proxy.obfs_password = original['obfs-password'];\n                    }\n                } else if (proxy.type === 'tuic') {\n                    proxy = {\n                        type: 'tuic',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        uuid: proxy.uuid,\n                        password: proxy.password,\n                        next_hop: proxy.next_hop,\n                        sni: proxy.sni,\n                        alpn: Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn || 'h3'],\n                        skip_tls_verify: proxy['skip-cert-verify'],\n                        port_hopping: proxy.ports,\n                        port_hopping_interval: proxy['hop-interval'],\n                    };\n                } else if (proxy.type === 'trojan') {\n                    if (proxy.network === 'ws') {\n                        proxy.websocket = {\n                            path: proxy['ws-opts']?.path,\n                            host: proxy['ws-opts']?.headers?.Host,\n                        };\n                    }\n                    proxy = {\n                        type: 'trojan',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        password: proxy.password,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                        sni: proxy.sni,\n                        skip_tls_verify: proxy['skip-cert-verify'],\n                        websocket: proxy.websocket,\n                    };\n                } else if (proxy.type === 'anytls') {\n                    proxy = {\n                        type: 'anytls',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        password: proxy.password,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                        sni: proxy.sni,\n                        skip_tls_verify: proxy['skip-cert-verify'],\n                    };\n                } else if (proxy.type === 'vmess') {\n                    // Egern：传输层，支持 ws/wss/http1/http2/tls，不配置则为 tcp\n                    let security = proxy.cipher;\n                    if (\n                        security &&\n                        ![\n                            'auto',\n                            'none',\n                            'zero',\n                            'aes-128-gcm',\n                            'chacha20-poly1305',\n                        ].includes(security)\n                    ) {\n                        security = 'auto';\n                    }\n                    if (proxy.network === 'ws') {\n                        proxy.transport = {\n                            [proxy.tls ? 'wss' : 'ws']: {\n                                path: proxy['ws-opts']?.path,\n                                headers: {\n                                    Host: proxy['ws-opts']?.headers?.Host,\n                                },\n                                sni: proxy.tls ? proxy.sni : undefined,\n                                skip_tls_verify: proxy.tls\n                                    ? proxy['skip-cert-verify']\n                                    : undefined,\n                            },\n                        };\n                    } else if (proxy.network === 'http') {\n                        proxy.transport = {\n                            http1: {\n                                method: proxy['http-opts']?.method,\n                                path: Array.isArray(proxy['http-opts']?.path)\n                                    ? proxy['http-opts']?.path[0]\n                                    : proxy['http-opts']?.path,\n                                headers: {\n                                    Host: Array.isArray(\n                                        proxy['http-opts']?.headers?.Host,\n                                    )\n                                        ? proxy['http-opts']?.headers?.Host[0]\n                                        : proxy['http-opts']?.headers?.Host,\n                                },\n                                skip_tls_verify: proxy['skip-cert-verify'],\n                            },\n                        };\n                    } else if (proxy.network === 'h2') {\n                        proxy.transport = {\n                            http2: {\n                                method: proxy['h2-opts']?.method,\n                                path: Array.isArray(proxy['h2-opts']?.path)\n                                    ? proxy['h2-opts']?.path[0]\n                                    : proxy['h2-opts']?.path,\n                                headers: {\n                                    Host: Array.isArray(\n                                        proxy['h2-opts']?.headers?.Host,\n                                    )\n                                        ? proxy['h2-opts']?.headers?.Host[0]\n                                        : proxy['h2-opts']?.headers?.Host,\n                                },\n                                skip_tls_verify: proxy['skip-cert-verify'],\n                            },\n                        };\n                    } else if (\n                        (proxy.network === 'tcp' || !proxy.network) &&\n                        proxy.tls\n                    ) {\n                        proxy.transport = {\n                            tls: {\n                                sni: proxy.tls ? proxy.sni : undefined,\n                                skip_tls_verify: proxy.tls\n                                    ? proxy['skip-cert-verify']\n                                    : undefined,\n                            },\n                        };\n                    }\n                    let legacy;\n                    if (isPresent(proxy, 'aead') && !proxy.aead) {\n                        legacy = true;\n                    } else if (proxy.alterId !== 0) {\n                        legacy = true;\n                    }\n                    proxy = {\n                        type: 'vmess',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        user_id: proxy.uuid,\n                        security,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        legacy,\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                        transport: proxy.transport,\n                        // sni: proxy.sni,\n                        // skip_tls_verify: proxy['skip-cert-verify'],\n                    };\n                } else if (proxy.type === 'vless') {\n                    if (proxy.encryption && proxy.encryption !== 'none')\n                        throw new Error(`VLESS encryption is not supported`);\n                    if (proxy.network === 'ws') {\n                        proxy.transport = {\n                            [proxy.tls ? 'wss' : 'ws']: {\n                                path: proxy['ws-opts']?.path,\n                                headers: {\n                                    Host: proxy['ws-opts']?.headers?.Host,\n                                },\n                                sni: proxy.tls ? proxy.sni : undefined,\n                                skip_tls_verify: proxy.tls\n                                    ? proxy['skip-cert-verify']\n                                    : undefined,\n                            },\n                        };\n                    } else if (proxy.network === 'http') {\n                        proxy.transport = {\n                            http: {\n                                method: proxy['http-opts']?.method,\n                                path: Array.isArray(proxy['http-opts']?.path)\n                                    ? proxy['http-opts']?.path[0]\n                                    : proxy['http-opts']?.path,\n                                headers: {\n                                    Host: Array.isArray(\n                                        proxy['http-opts']?.headers?.Host,\n                                    )\n                                        ? proxy['http-opts']?.headers?.Host[0]\n                                        : proxy['http-opts']?.headers?.Host,\n                                },\n                                skip_tls_verify: proxy['skip-cert-verify'],\n                            },\n                        };\n                    } else if (proxy.network === 'tcp' || !proxy.network) {\n                        let reality;\n                        if (\n                            proxy['reality-opts']?.['short-id'] ||\n                            proxy['reality-opts']?.['public-key']\n                        ) {\n                            reality = {\n                                short_id: proxy['reality-opts']['short-id'],\n                                public_key: proxy['reality-opts']['public-key'],\n                            };\n                        }\n                        proxy.transport = {\n                            [proxy.tls ? 'tls' : 'tcp']: {\n                                sni: proxy.tls ? proxy.sni : undefined,\n                                skip_tls_verify: proxy.tls\n                                    ? proxy['skip-cert-verify']\n                                    : undefined,\n                                reality,\n                            },\n                        };\n                        flow = proxy.flow;\n                        if (flow === '') flow = undefined;\n                    }\n                    proxy = {\n                        type: 'vless',\n                        name: proxy.name,\n                        server: proxy.server,\n                        port: proxy.port,\n                        user_id: proxy.uuid,\n                        security: proxy.cipher,\n                        tfo: proxy.tfo || proxy['fast-open'],\n                        udp_relay:\n                            proxy.udp || proxy.udp_relay || proxy.udp_relay,\n                        next_hop: proxy.next_hop,\n                        transport: proxy.transport,\n                        flow,\n                        // sni: proxy.sni,\n                        // skip_tls_verify: proxy['skip-cert-verify'],\n                    };\n                } else if (proxy.type === 'wireguard') {\n                    if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {\n                        proxy.server = proxy.peers[0].server;\n                        proxy.port = proxy.peers[0].port;\n                        proxy.ip = proxy.peers[0].ip;\n                        proxy.ipv6 = proxy.peers[0].ipv6;\n                        proxy['public-key'] = proxy.peers[0]['public-key'];\n                        proxy['preshared-key'] =\n                            proxy.peers[0]['pre-shared-key'];\n                        // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717\n                        proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];\n                        proxy.reserved = proxy.peers[0].reserved;\n                    }\n                    proxy = {\n                        type: 'wireguard',\n                        name: proxy.name,\n                        local_ipv4: proxy.ip,\n                        local_ipv6: proxy.ipv6,\n                        server: proxy.server,\n                        port: proxy.port,\n                        private_key: proxy['private-key'],\n                        peer_public_key: proxy['public-key'],\n                        preshared_key: proxy['preshared-key'],\n                        reserved: proxy.reserved\n                            ? Array.isArray(proxy.reserved)\n                                ? proxy.reserved\n                                : proxy.reserved\n                                      .split(/\\s*\\/\\s*/)\n                                      .map((item) => item.trim())\n                                      .filter((item) => item.length > 0)\n                            : undefined,\n                        dns_servers: proxy.dns\n                            ? Array.isArray(proxy.dns)\n                                ? proxy.dns\n                                : proxy.dns\n                                      .split(/\\s*,\\s*/)\n                                      .map((item) => item.trim())\n                                      .filter((item) => item.length > 0)\n                            : undefined,\n                        mtu: proxy.mtu,\n                        keepalive: proxy.keepalive,\n                    };\n                }\n                if (\n                    [\n                        'http',\n                        'https',\n                        'socks5',\n                        'ss',\n                        'trojan',\n                        'vless',\n                        'vmess',\n                        'anytls',\n                    ].includes(original.type)\n                ) {\n                    if (isPresent(original, 'shadow-tls-password')) {\n                        if (original['shadow-tls-version'] != 3)\n                            throw new Error(\n                                `shadow-tls version ${original['shadow-tls-version']} is not supported`,\n                            );\n                        proxy.shadow_tls = {\n                            password: original['shadow-tls-password'],\n                            sni: original['shadow-tls-sni'],\n                        };\n                    } else if (\n                        ['shadow-tls'].includes(original.plugin) &&\n                        original['plugin-opts']\n                    ) {\n                        if (original['plugin-opts'].version != 3)\n                            throw new Error(\n                                `shadow-tls version ${original['plugin-opts'].version} is not supported`,\n                            );\n                        proxy.shadow_tls = {\n                            password: original['plugin-opts'].password,\n                            sni: original['plugin-opts'].host,\n                        };\n                    }\n                }\n                if (\n                    [\n                        'socks5',\n                        'ss',\n                        'trojan',\n                        'vless',\n                        'vmess',\n                        'wireguard',\n                        'tuic',\n                        'hysteria2',\n                        'anytls',\n                    ].includes(original.type)\n                ) {\n                    if (\n                        ['on', 'true', true, '1', 1].includes(\n                            original['block-quic'],\n                        )\n                    ) {\n                        proxy.block_quic = true;\n                    } else if (\n                        ['off', 'false', false, '0', 0].includes(\n                            original['block-quic'],\n                        )\n                    ) {\n                        proxy.block_quic = false;\n                    }\n                }\n                if (\n                    ['ss'].includes(original.type) &&\n                    proxy.shadow_tls &&\n                    original['udp-port'] > 0 &&\n                    original['udp-port'] <= 65535\n                ) {\n                    proxy['udp_port'] = original['udp-port'];\n                }\n\n                delete proxy.subName;\n                delete proxy.collectionName;\n                delete proxy.id;\n                delete proxy.resolved;\n                delete proxy['no-resolve'];\n\n                if (proxy.transport) {\n                    for (const key in proxy.transport) {\n                        if (\n                            Object.keys(proxy.transport[key]).length === 0 ||\n                            Object.values(proxy.transport[key]).every(\n                                (v) => v == null,\n                            )\n                        ) {\n                            delete proxy.transport[key];\n                        }\n                    }\n                    if (Object.keys(proxy.transport).length === 0) {\n                        delete proxy.transport;\n                    }\n                }\n\n                if (type !== 'internal') {\n                    for (const key in proxy) {\n                        if (proxy[key] == null || /^_/i.test(key)) {\n                            delete proxy[key];\n                        }\n                    }\n                }\n                return {\n                    [proxy.type]: {\n                        ...proxy,\n                        type: undefined,\n                        prev_hop,\n                    },\n                };\n            });\n        return type === 'internal'\n            ? list\n            : 'proxies:\\n' +\n                  list\n                      .map((proxy) => '  - ' + JSON.stringify(proxy) + '\\n')\n                      .join('');\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/index.js",
    "content": "import Surge_Producer from './surge';\nimport SurgeMac_Producer from './surgemac';\nimport Clash_Producer from './clash';\nimport ClashMeta_Producer from './clashmeta';\nimport Stash_Producer from './stash';\nimport Loon_Producer from './loon';\nimport URI_Producer from './uri';\nimport V2Ray_Producer from './v2ray';\nimport QX_Producer from './qx';\nimport Shadowrocket_Producer from './shadowrocket';\nimport Surfboard_Producer from './surfboard';\nimport singbox_Producer from './sing-box';\nimport Egern_Producer from './egern';\n\nfunction JSON_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type) =>\n        type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);\n    return { type, produce };\n}\n\nexport default {\n    qx: QX_Producer(),\n    QX: QX_Producer(),\n    QuantumultX: QX_Producer(),\n    surge: Surge_Producer(),\n    Surge: Surge_Producer(),\n    SurgeMac: SurgeMac_Producer(),\n    Loon: Loon_Producer(),\n    Clash: Clash_Producer(),\n    meta: ClashMeta_Producer(),\n    clashmeta: ClashMeta_Producer(),\n    'clash.meta': ClashMeta_Producer(),\n    'Clash.Meta': ClashMeta_Producer(),\n    ClashMeta: ClashMeta_Producer(),\n    mihomo: ClashMeta_Producer(),\n    Mihomo: ClashMeta_Producer(),\n    uri: URI_Producer(),\n    URI: URI_Producer(),\n    v2: V2Ray_Producer(),\n    v2ray: V2Ray_Producer(),\n    V2Ray: V2Ray_Producer(),\n    json: JSON_Producer(),\n    JSON: JSON_Producer(),\n    stash: Stash_Producer(),\n    Stash: Stash_Producer(),\n    shadowrocket: Shadowrocket_Producer(),\n    Shadowrocket: Shadowrocket_Producer(),\n    ShadowRocket: Shadowrocket_Producer(),\n    surfboard: Surfboard_Producer(),\n    Surfboard: Surfboard_Producer(),\n    singbox: singbox_Producer(),\n    'sing-box': singbox_Producer(),\n    egern: Egern_Producer(),\n    Egern: Egern_Producer(),\n};\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/loon.js",
    "content": "/* eslint-disable no-case-declarations */\nconst targetPlatform = 'Loon';\nimport { isPresent, Result } from './utils';\nimport { isIPv4, isIPv6 } from '@/utils';\nimport $ from '@/core/app';\n\nconst ipVersions = {\n    dual: 'dual',\n    ipv4: 'v4-only',\n    ipv6: 'v6-only',\n    'ipv4-prefer': 'prefer-v4',\n    'ipv6-prefer': 'prefer-v6',\n};\n\nexport default function Loon_Producer() {\n    const produce = (proxy, type, opts = {}) => {\n        if (\n            ['ws'].includes(proxy.network) &&\n            proxy['ws-opts']?.['v2ray-http-upgrade']\n        ) {\n            throw new Error(\n                `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,\n            );\n        }\n        switch (proxy.type) {\n            case 'ss':\n                return shadowsocks(proxy);\n            case 'ssr':\n                return shadowsocksr(proxy);\n            case 'trojan':\n                return trojan(proxy);\n            case 'anytls':\n                return anytls(proxy);\n            case 'vmess':\n                return vmess(proxy, opts['include-unsupported-proxy']);\n            case 'vless':\n                return vless(proxy, opts['include-unsupported-proxy']);\n            case 'http':\n                return http(proxy);\n            case 'socks5':\n                return socks5(proxy);\n            case 'wireguard':\n                return wireguard(proxy);\n            case 'hysteria2':\n                return hysteria2(proxy);\n        }\n        throw new Error(\n            `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,\n        );\n    };\n    return { produce };\n}\n\nfunction shadowsocks(proxy) {\n    const result = new Result(proxy);\n    if (\n        ![\n            'rc4',\n            'rc4-md5',\n            'aes-128-cfb',\n            'aes-192-cfb',\n            'aes-256-cfb',\n            'aes-128-ctr',\n            'aes-192-ctr',\n            'aes-256-ctr',\n            'bf-cfb',\n            'camellia-128-cfb',\n            'camellia-192-cfb',\n            'camellia-256-cfb',\n            'salsa20',\n            'chacha20',\n            'chacha20-ietf',\n            'aes-128-gcm',\n            'aes-192-gcm',\n            'aes-256-gcm',\n            'chacha20-ietf-poly1305',\n            'xchacha20-ietf-poly1305',\n            '2022-blake3-aes-128-gcm',\n            '2022-blake3-aes-256-gcm',\n        ].includes(proxy.cipher)\n    ) {\n        throw new Error(`cipher ${proxy.cipher} is not supported`);\n    }\n    result.append(\n        `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},\"${proxy.password}\"`,\n    );\n\n    // obfs\n    if (isPresent(proxy, 'plugin')) {\n        if (proxy.plugin === 'obfs') {\n            if (\n                proxy['plugin-opts']?.mode &&\n                proxy.cipher.startsWith('2022-')\n            ) {\n                throw new Error(\n                    `${proxy.cipher} ${proxy.plugin} is not supported`,\n                );\n            }\n            result.append(`,obfs-name=${proxy['plugin-opts'].mode}`);\n            result.appendIfPresent(\n                `,obfs-host=${proxy['plugin-opts'].host}`,\n                'plugin-opts.host',\n            );\n            result.appendIfPresent(\n                `,obfs-uri=${proxy['plugin-opts'].path}`,\n                'plugin-opts.path',\n            );\n        } else if (!['shadow-tls'].includes(proxy.plugin)) {\n            throw new Error(`plugin ${proxy.plugin} is not supported`);\n        }\n    }\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n        // udp-port\n        result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');\n    } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {\n        const password = proxy['plugin-opts'].password;\n        const host = proxy['plugin-opts'].host;\n        const version = proxy['plugin-opts'].version;\n        if (password) {\n            result.append(`,shadow-tls-password=${password}`);\n            if (host) {\n                result.append(`,shadow-tls-sni=${host}`);\n            }\n            if (version) {\n                if (version < 2) {\n                    throw new Error(\n                        `shadow-tls version ${version} is not supported`,\n                    );\n                }\n                result.append(`,shadow-tls-version=${version}`);\n            }\n            // udp-port\n            result.appendIfPresent(\n                `,udp-port=${proxy['udp-port']}`,\n                'udp-port',\n            );\n        }\n    }\n\n    // udp over tcp\n    if (proxy['udp-over-tcp']) {\n        if (proxy['udp-over-tcp-version'] === 2) {\n            if (proxy.plugin === 'obfs') {\n                $.error(\n                    `Platform ${targetPlatform} shadowsocks udp-over-tcp does not support obfs`,\n                );\n            } else {\n                result.append(`,udp-over-tcp=true`);\n            }\n        } else {\n            $.error(\n                `Platform ${targetPlatform} shadowsocks only supports udp-over-tcp-version 2`,\n            );\n        }\n    }\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\n\nfunction shadowsocksr(proxy) {\n    const result = new Result(proxy);\n    result.append(\n        `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},\"${proxy.password}\"`,\n    );\n\n    // ssr protocol\n    result.append(`,protocol=${proxy.protocol}`);\n    result.appendIfPresent(\n        `,protocol-param=${proxy['protocol-param']}`,\n        'protocol-param',\n    );\n\n    // obfs\n    result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');\n    result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n        // udp-port\n        result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');\n    } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {\n        const password = proxy['plugin-opts'].password;\n        const host = proxy['plugin-opts'].host;\n        const version = proxy['plugin-opts'].version;\n        if (password) {\n            result.append(`,shadow-tls-password=${password}`);\n            if (host) {\n                result.append(`,shadow-tls-sni=${host}`);\n            }\n            if (version) {\n                if (version < 2) {\n                    throw new Error(\n                        `shadow-tls version ${version} is not supported`,\n                    );\n                }\n                result.append(`,shadow-tls-version=${version}`);\n            }\n            // udp-port\n            result.appendIfPresent(\n                `,udp-port=${proxy['udp-port']}`,\n                'udp-port',\n            );\n        }\n    }\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\n\nfunction trojan(proxy) {\n    const result = new Result(proxy);\n    result.append(\n        `${proxy.name}=trojan,${proxy.server},${proxy.port},\"${proxy.password}\"`,\n    );\n    if (proxy.network === 'tcp') {\n        delete proxy.network;\n    }\n    // transport\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            result.append(`,transport=ws`);\n            result.appendIfPresent(\n                `,path=${proxy['ws-opts']?.path}`,\n                'ws-opts.path',\n            );\n            result.appendIfPresent(\n                `,host=${proxy['ws-opts']?.headers?.Host}`,\n                'ws-opts.headers.Host',\n            );\n        } else {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n    }\n\n    // tls verification\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // sni\n    result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');\n    result.appendIfPresent(\n        `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n    result.appendIfPresent(\n        `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n        'tls-pubkey-sha256',\n    );\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\n\nfunction anytls(proxy) {\n    const result = new Result(proxy);\n    result.append(\n        `${proxy.name}=anytls,${proxy.server},${proxy.port},\"${proxy.password}\"`,\n    );\n    // 新版删除idle-session-check-interval和min-idle-session 参数，session 改为主动超时机制，由于 anytls-go 不支持一个tcp 并发多个 stream，max-stream-cout 设置大于 1 时会有阻塞，如果有其他支持多路复用的 anytls 服务器实现，可以设置max-stream-cout 大于 1\n    for (const key of [\n        // 'idle-session-check-interval',\n        'idle-session-timeout',\n        // 'min-idle-session',\n        'max-stream-count',\n    ]) {\n        // 值为整数 才附加\n        if (isPresent(proxy, key) && Number.isInteger(proxy[key])) {\n            result.append(`,${key}=${proxy[key]}`);\n        }\n    }\n\n    // tls verification\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // sni\n    result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');\n    result.appendIfPresent(\n        `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n    result.appendIfPresent(\n        `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n        'tls-pubkey-sha256',\n    );\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\n\nfunction vmess(proxy) {\n    const isReality = !!proxy['reality-opts'];\n\n    const result = new Result(proxy);\n    result.append(\n        `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},\"${proxy.uuid}\"`,\n    );\n    if (proxy.network === 'tcp') {\n        delete proxy.network;\n    }\n    // transport\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            result.append(`,transport=ws`);\n            result.appendIfPresent(\n                `,path=${proxy['ws-opts']?.path}`,\n                'ws-opts.path',\n            );\n            result.appendIfPresent(\n                `,host=${proxy['ws-opts']?.headers?.Host}`,\n                'ws-opts.headers.Host',\n            );\n        } else if (proxy.network === 'http') {\n            result.append(`,transport=http`);\n            let httpPath = proxy['http-opts']?.path;\n            let httpHost = proxy['http-opts']?.headers?.Host;\n            result.appendIfPresent(\n                `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,\n                'http-opts.path',\n            );\n            result.appendIfPresent(\n                `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,\n                'http-opts.headers.Host',\n            );\n        } else {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n    } else {\n        result.append(`,transport=tcp`);\n    }\n\n    // tls\n    result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');\n\n    // tls verification\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    if (isReality) {\n        result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');\n        result.appendIfPresent(\n            `,public-key=\"${proxy['reality-opts']['public-key']}\"`,\n            'reality-opts.public-key',\n        );\n        result.appendIfPresent(\n            `,short-id=${proxy['reality-opts']['short-id']}`,\n            'reality-opts.short-id',\n        );\n    } else {\n        // sni\n        result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');\n        result.appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n        result.appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n    }\n\n    // AEAD\n    if (isPresent(proxy, 'aead')) {\n        result.append(`,alterId=${proxy.aead ? 0 : 1}`);\n    } else {\n        result.append(`,alterId=${proxy.alterId}`);\n    }\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n    return result.toString();\n}\n\nfunction vless(proxy) {\n    if (proxy.encryption && proxy.encryption !== 'none')\n        throw new Error(`VLESS encryption is not supported`);\n    let isXtls = false;\n    const isReality = !!proxy['reality-opts'];\n\n    if (typeof proxy.flow !== 'undefined') {\n        if (['xtls-rprx-vision'].includes(proxy.flow)) {\n            isXtls = true;\n        } else {\n            throw new Error(`VLESS flow(${proxy.flow}) is not supported`);\n        }\n    }\n\n    const result = new Result(proxy);\n    result.append(\n        `${proxy.name}=vless,${proxy.server},${proxy.port},\"${proxy.uuid}\"`,\n    );\n    if (proxy.network === 'tcp') {\n        delete proxy.network;\n    }\n    // transport\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            result.append(`,transport=ws`);\n            result.appendIfPresent(\n                `,path=${proxy['ws-opts']?.path}`,\n                'ws-opts.path',\n            );\n            result.appendIfPresent(\n                `,host=${proxy['ws-opts']?.headers?.Host}`,\n                'ws-opts.headers.Host',\n            );\n        } else if (proxy.network === 'http') {\n            result.append(`,transport=http`);\n            let httpPath = proxy['http-opts']?.path;\n            let httpHost = proxy['http-opts']?.headers?.Host;\n            result.appendIfPresent(\n                `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,\n                'http-opts.path',\n            );\n            result.appendIfPresent(\n                `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,\n                'http-opts.headers.Host',\n            );\n        } else {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n    } else {\n        result.append(`,transport=tcp`);\n    }\n\n    // tls\n    result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');\n\n    // tls verification\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    if (isXtls) {\n        result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');\n    }\n    if (isReality) {\n        result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');\n        result.appendIfPresent(\n            `,public-key=\"${proxy['reality-opts']['public-key']}\"`,\n            'reality-opts.public-key',\n        );\n        result.appendIfPresent(\n            `,short-id=${proxy['reality-opts']['short-id']}`,\n            'reality-opts.short-id',\n        );\n    } else {\n        // sni\n        result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');\n        result.appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n        result.appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n    }\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n    return result.toString();\n}\n\nfunction http(proxy) {\n    const result = new Result(proxy);\n    const type = proxy.tls ? 'https' : 'http';\n    result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,${proxy.username}`, 'username');\n    result.appendIfPresent(`,\"${proxy.password}\"`, 'password');\n\n    // sni\n    result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');\n\n    // tls verification\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\nfunction socks5(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=socks5,${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,${proxy.username}`, 'username');\n    result.appendIfPresent(`,\"${proxy.password}\"`, 'password');\n\n    // tls\n    result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');\n\n    // sni\n    result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');\n\n    // tls verification\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\n\nfunction wireguard(proxy) {\n    if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {\n        proxy.server = proxy.peers[0].server;\n        proxy.port = proxy.peers[0].port;\n        proxy.ip = proxy.peers[0].ip;\n        proxy.ipv6 = proxy.peers[0].ipv6;\n        proxy['public-key'] = proxy.peers[0]['public-key'];\n        proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];\n        // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717\n        proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];\n        proxy.reserved = proxy.peers[0].reserved;\n    }\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=wireguard`);\n\n    result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');\n    result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');\n\n    result.appendIfPresent(\n        `,private-key=\"${proxy['private-key']}\"`,\n        'private-key',\n    );\n    result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');\n\n    if (proxy.dns) {\n        if (Array.isArray(proxy.dns)) {\n            proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));\n            let dns = proxy.dns.find((i) => isIPv4(i));\n            if (!dns) {\n                dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i));\n            }\n            proxy.dns = dns;\n        }\n    }\n    result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');\n    result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');\n    result.appendIfPresent(\n        `,keepalive=${proxy['persistent-keepalive']}`,\n        'persistent-keepalive',\n    );\n    result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');\n    const allowedIps = Array.isArray(proxy['allowed-ips'])\n        ? proxy['allowed-ips'].join(',')\n        : proxy['allowed-ips'];\n    let reserved = Array.isArray(proxy.reserved)\n        ? proxy.reserved.join(',')\n        : proxy.reserved;\n    if (reserved) {\n        reserved = `,reserved=[${reserved}]`;\n    }\n    let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];\n    if (presharedKey) {\n        presharedKey = `,preshared-key=\"${presharedKey}\"`;\n    }\n    result.append(\n        `,peers=[{public-key=\"${proxy['public-key']}\",allowed-ips=\"${\n            allowedIps ?? '0.0.0.0/0,::/0'\n        }\",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${\n            presharedKey ?? ''\n        }}]`,\n    );\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    return result.toString();\n}\n\nfunction hysteria2(proxy) {\n    if (proxy['obfs-password'] && proxy.obfs != 'salamander') {\n        throw new Error(`only salamander obfs is supported`);\n    }\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);\n\n    result.appendIfPresent(`,\"${proxy.password}\"`, 'password');\n\n    // sni\n    result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');\n    result.appendIfPresent(\n        `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n    result.appendIfPresent(\n        `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n        'tls-pubkey-sha256',\n    );\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    if (proxy['obfs-password'] && proxy.obfs == 'salamander') {\n        result.append(`,salamander-password=${proxy['obfs-password']}`);\n    }\n\n    // tfo\n    result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // block-quic\n    if (proxy['block-quic'] === 'on') {\n        result.append(',block-quic=true');\n    } else if (proxy['block-quic'] === 'off') {\n        result.append(',block-quic=false');\n    }\n\n    // udp\n    if (proxy.udp) {\n        result.append(`,udp=true`);\n    }\n\n    // download-bandwidth\n    result.appendIfPresent(\n        `,download-bandwidth=${`${proxy['down']}`.match(/\\d+/)?.[0] || 0}`,\n        'down',\n    );\n\n    result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');\n\n    return result.toString();\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/qx.js",
    "content": "import { isPresent, Result } from './utils';\n\nconst targetPlatform = 'QX';\n\nexport default function QX_Producer() {\n    // eslint-disable-next-line no-unused-vars\n    const produce = (proxy, type, opts = {}) => {\n        if (\n            ['ws'].includes(proxy.network) &&\n            proxy['ws-opts']?.['v2ray-http-upgrade']\n        ) {\n            throw new Error(\n                `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,\n            );\n        }\n        switch (proxy.type) {\n            case 'ss':\n                return shadowsocks(proxy);\n            case 'ssr':\n                return shadowsocksr(proxy);\n            case 'trojan':\n                return trojan(proxy);\n            case 'vmess':\n                return vmess(proxy);\n            case 'http':\n                return http(proxy);\n            case 'socks5':\n                return socks5(proxy);\n            case 'vless':\n                return vless(proxy);\n        }\n        throw new Error(\n            `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,\n        );\n    };\n    return {\n        produce: (proxy, type, opts = {}) => {\n            let result = produce(proxy, type, opts);\n            if (proxy.flow && proxy.flow !== 'xtls-rprx-vision') {\n                throw new Error(\n                    `Platform ${targetPlatform} does not support flow ${proxy.flow}`,\n                );\n            }\n            if (proxy['reality-opts']) {\n                if (proxy['reality-opts']['public-key']) {\n                    result = `${result},reality-base64-pubkey=${proxy['reality-opts']['public-key']}`;\n                }\n                if (proxy['reality-opts']['short-id']) {\n                    result = `${result},reality-hex-shortid=${proxy['reality-opts']['short-id']}`;\n                }\n            }\n            return result;\n        },\n    };\n}\n\nfunction shadowsocks(proxy) {\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n    if (!proxy.cipher) {\n        proxy.cipher = 'none';\n    }\n    if (\n        ![\n            'none',\n            'rc4-md5',\n            'rc4-md5-6',\n            'aes-128-cfb',\n            'aes-192-cfb',\n            'aes-256-cfb',\n            'aes-128-ctr',\n            'aes-192-ctr',\n            'aes-256-ctr',\n            'bf-cfb',\n            'cast5-cfb',\n            'des-cfb',\n            'rc2-cfb',\n            'salsa20',\n            'chacha20',\n            'chacha20-ietf',\n            'aes-128-gcm',\n            'aes-192-gcm',\n            'aes-256-gcm',\n            'chacha20-ietf-poly1305',\n            'xchacha20-ietf-poly1305',\n            '2022-blake3-aes-128-gcm',\n            '2022-blake3-aes-256-gcm',\n        ].includes(proxy.cipher)\n    ) {\n        throw new Error(`cipher ${proxy.cipher} is not supported`);\n    }\n    append(`shadowsocks=${proxy.server}:${proxy.port}`);\n    append(`,method=${proxy.cipher}`);\n    append(`,password=${proxy.password}`);\n\n    // obfs\n    if (needTls(proxy)) {\n        proxy.tls = true;\n    }\n    if (isPresent(proxy, 'plugin')) {\n        if (proxy.plugin === 'obfs') {\n            const opts = proxy['plugin-opts'];\n            append(`,obfs=${opts.mode}`);\n        } else if (\n            proxy.plugin === 'v2ray-plugin' &&\n            proxy['plugin-opts'].mode === 'websocket'\n        ) {\n            const opts = proxy['plugin-opts'];\n            if (opts.tls) append(`,obfs=wss`);\n            else append(`,obfs=ws`);\n        } else {\n            throw new Error(`plugin is not supported`);\n        }\n        appendIfPresent(\n            `,obfs-host=${proxy['plugin-opts'].host}`,\n            'plugin-opts.host',\n        );\n        appendIfPresent(\n            `,obfs-uri=${proxy['plugin-opts'].path}`,\n            'plugin-opts.path',\n        );\n    }\n\n    if (needTls(proxy)) {\n        appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n        appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');\n        appendIfPresent(\n            `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,\n            'tls-no-session-ticket',\n        );\n        appendIfPresent(\n            `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,\n            'tls-no-session-reuse',\n        );\n        // tls fingerprint\n        appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n\n        // tls verification\n        appendIfPresent(\n            `,tls-verification=${!proxy['skip-cert-verify']}`,\n            'skip-cert-verify',\n        );\n        appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');\n    }\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // udp over tcp\n    if (proxy['_ssr_python_uot']) {\n        append(`,udp-over-tcp=true`);\n    } else if (proxy['udp-over-tcp']) {\n        if (\n            !proxy['udp-over-tcp-version'] ||\n            proxy['udp-over-tcp-version'] === 1\n        ) {\n            append(`,udp-over-tcp=sp.v1`);\n        } else if (proxy['udp-over-tcp-version'] === 2) {\n            append(`,udp-over-tcp=sp.v2`);\n        }\n    }\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\n\nfunction shadowsocksr(proxy) {\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n\n    append(`shadowsocks=${proxy.server}:${proxy.port}`);\n    append(`,method=${proxy.cipher}`);\n    append(`,password=${proxy.password}`);\n\n    // ssr protocol\n    append(`,ssr-protocol=${proxy.protocol}`);\n    appendIfPresent(\n        `,ssr-protocol-param=${proxy['protocol-param']}`,\n        'protocol-param',\n    );\n\n    // obfs\n    appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');\n    appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param');\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\n\nfunction trojan(proxy) {\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n\n    append(`trojan=${proxy.server}:${proxy.port}`);\n    append(`,password=${proxy.password}`);\n\n    // obfs ws\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            if (needTls(proxy)) append(`,obfs=wss`);\n            else append(`,obfs=ws`);\n            appendIfPresent(\n                `,obfs-uri=${proxy['ws-opts']?.path}`,\n                'ws-opts.path',\n            );\n            appendIfPresent(\n                `,obfs-host=${proxy['ws-opts']?.headers?.Host}`,\n                'ws-opts.headers.Host',\n            );\n        } else if (!['tcp'].includes(proxy.network)) {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n    }\n\n    // over tls\n    if (proxy.network !== 'ws' && needTls(proxy)) {\n        append(`,over-tls=true`);\n    }\n\n    if (needTls(proxy)) {\n        appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n        appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');\n        appendIfPresent(\n            `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,\n            'tls-no-session-ticket',\n        );\n        appendIfPresent(\n            `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,\n            'tls-no-session-reuse',\n        );\n        // tls fingerprint\n        appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n\n        // tls verification\n        appendIfPresent(\n            `,tls-verification=${!proxy['skip-cert-verify']}`,\n            'skip-cert-verify',\n        );\n        appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');\n    }\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\n\nfunction vmess(proxy) {\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n\n    append(`vmess=${proxy.server}:${proxy.port}`);\n\n    // cipher\n    let cipher;\n    if (proxy.cipher === 'auto') {\n        cipher = 'chacha20-ietf-poly1305';\n    } else {\n        cipher = proxy.cipher;\n    }\n    append(`,method=${cipher}`);\n\n    append(`,password=${proxy.uuid}`);\n\n    // obfs\n    if (needTls(proxy)) {\n        proxy.tls = true;\n    }\n\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            if (proxy.tls) append(`,obfs=wss`);\n            else append(`,obfs=ws`);\n        } else if (proxy.network === 'http') {\n            append(`,obfs=http`);\n        } else if (['tcp'].includes(proxy.network)) {\n            if (proxy.tls) append(`,obfs=over-tls`);\n        } else if (!['tcp'].includes(proxy.network)) {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n        let transportPath = proxy[`${proxy.network}-opts`]?.path;\n        let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;\n        appendIfPresent(\n            `,obfs-uri=${\n                Array.isArray(transportPath) ? transportPath[0] : transportPath\n            }`,\n            `${proxy.network}-opts.path`,\n        );\n        appendIfPresent(\n            `,obfs-host=${\n                Array.isArray(transportHost) ? transportHost[0] : transportHost\n            }`,\n            `${proxy.network}-opts.headers.Host`,\n        );\n    } else {\n        // over-tls\n        if (proxy.tls) append(`,obfs=over-tls`);\n    }\n\n    if (needTls(proxy)) {\n        appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n        appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');\n        appendIfPresent(\n            `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,\n            'tls-no-session-ticket',\n        );\n        appendIfPresent(\n            `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,\n            'tls-no-session-reuse',\n        );\n        // tls fingerprint\n        appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n\n        // tls verification\n        appendIfPresent(\n            `,tls-verification=${!proxy['skip-cert-verify']}`,\n            'skip-cert-verify',\n        );\n        appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');\n    }\n\n    // AEAD\n    if (isPresent(proxy, 'aead')) {\n        append(`,aead=${proxy.aead}`);\n    } else {\n        append(`,aead=${proxy.alterId === 0}`);\n    }\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\nfunction vless(proxy) {\n    if (proxy.encryption && proxy.encryption !== 'none')\n        throw new Error(`VLESS encryption is not supported`);\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n\n    append(`vless=${proxy.server}:${proxy.port}`);\n\n    // The method field for vless should be none.\n    let cipher = 'none';\n    // if (proxy.cipher === 'auto') {\n    //     cipher = 'chacha20-ietf-poly1305';\n    // } else {\n    //     cipher = proxy.cipher;\n    // }\n    append(`,method=${cipher}`);\n\n    append(`,password=${proxy.uuid}`);\n\n    // obfs\n    if (needTls(proxy)) {\n        proxy.tls = true;\n    }\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            if (proxy.tls) append(`,obfs=wss`);\n            else append(`,obfs=ws`);\n        } else if (proxy.network === 'http') {\n            append(`,obfs=http`);\n        } else if (['tcp'].includes(proxy.network)) {\n            if (proxy.tls) append(`,obfs=over-tls`);\n        } else if (!['tcp'].includes(proxy.network)) {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n        let transportPath = proxy[`${proxy.network}-opts`]?.path;\n        let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;\n        appendIfPresent(\n            `,obfs-uri=${\n                Array.isArray(transportPath) ? transportPath[0] : transportPath\n            }`,\n            `${proxy.network}-opts.path`,\n        );\n        appendIfPresent(\n            `,obfs-host=${\n                Array.isArray(transportHost) ? transportHost[0] : transportHost\n            }`,\n            `${proxy.network}-opts.headers.Host`,\n        );\n    } else {\n        // over-tls\n        if (proxy.tls) append(`,obfs=over-tls`);\n    }\n\n    if (needTls(proxy)) {\n        appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n        appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');\n        appendIfPresent(\n            `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,\n            'tls-no-session-ticket',\n        );\n        appendIfPresent(\n            `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,\n            'tls-no-session-reuse',\n        );\n        // tls fingerprint\n        appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n\n        // tls verification\n        appendIfPresent(\n            `,tls-verification=${!proxy['skip-cert-verify']}`,\n            'skip-cert-verify',\n        );\n        appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');\n    }\n\n    appendIfPresent(`,vless-flow=${proxy.flow}`, 'flow');\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\n\nfunction http(proxy) {\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n\n    append(`http=${proxy.server}:${proxy.port}`);\n    appendIfPresent(`,username=${proxy.username}`, 'username');\n    appendIfPresent(`,password=${proxy.password}`, 'password');\n\n    // tls\n    if (needTls(proxy)) {\n        proxy.tls = true;\n    }\n    appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');\n\n    if (needTls(proxy)) {\n        appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n        appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');\n        appendIfPresent(\n            `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,\n            'tls-no-session-ticket',\n        );\n        appendIfPresent(\n            `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,\n            'tls-no-session-reuse',\n        );\n        // tls fingerprint\n        appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n\n        // tls verification\n        appendIfPresent(\n            `,tls-verification=${!proxy['skip-cert-verify']}`,\n            'skip-cert-verify',\n        );\n        appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');\n    }\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\n\nfunction socks5(proxy) {\n    const result = new Result(proxy);\n    const append = result.append.bind(result);\n    const appendIfPresent = result.appendIfPresent.bind(result);\n\n    append(`socks5=${proxy.server}:${proxy.port}`);\n    appendIfPresent(`,username=${proxy.username}`, 'username');\n    appendIfPresent(`,password=${proxy.password}`, 'password');\n\n    // tls\n    if (needTls(proxy)) {\n        proxy.tls = true;\n    }\n    appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');\n\n    if (needTls(proxy)) {\n        appendIfPresent(\n            `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,\n            'tls-pubkey-sha256',\n        );\n        appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');\n        appendIfPresent(\n            `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,\n            'tls-no-session-ticket',\n        );\n        appendIfPresent(\n            `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,\n            'tls-no-session-reuse',\n        );\n        // tls fingerprint\n        appendIfPresent(\n            `,tls-cert-sha256=${proxy['tls-fingerprint']}`,\n            'tls-fingerprint',\n        );\n\n        // tls verification\n        appendIfPresent(\n            `,tls-verification=${!proxy['skip-cert-verify']}`,\n            'skip-cert-verify',\n        );\n        appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');\n    }\n\n    // tfo\n    appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');\n\n    // udp\n    appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // server_check_url\n    result.appendIfPresent(\n        `,server_check_url=${proxy['test-url']}`,\n        'test-url',\n    );\n\n    // tag\n    append(`,tag=${proxy.name}`);\n\n    return result.toString();\n}\n\nfunction needTls(proxy) {\n    return proxy.tls;\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/shadowrocket.js",
    "content": "import { isPresent } from '@/core/proxy-utils/producers/utils';\nimport $ from '@/core/app';\n\nexport default function Shadowrocket_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type, opts = {}) => {\n        const list = proxies\n            .filter((proxy) => {\n                if (opts['include-unsupported-proxy']) return true;\n                if (proxy.type === 'snell' && proxy.version >= 4) {\n                    return false;\n                } else if (\n                    [\n                        'trusttunnel',\n                        'mieru',\n                        'sudoku',\n                        'naive',\n                        'masque',\n                    ].includes(proxy.type)\n                ) {\n                    return false;\n                } else if (\n                    proxy.encryption &&\n                    proxy.encryption !== 'none' &&\n                    ['vless'].includes(proxy.type)\n                ) {\n                    return false;\n                } else if (\n                    ['anytls'].includes(proxy.type) &&\n                    proxy.network &&\n                    (!['tcp'].includes(proxy.network) ||\n                        (['tcp'].includes(proxy.network) &&\n                            proxy['reality-opts']))\n                ) {\n                    return false;\n                } else if (['xhttp'].includes(proxy.network)) {\n                    return false;\n                }\n                return true;\n            })\n            .map((proxy) => {\n                if (proxy.type === 'vmess') {\n                    // handle vmess aead\n                    if (isPresent(proxy, 'aead')) {\n                        if (proxy.aead) {\n                            proxy.alterId = 0;\n                        }\n                        delete proxy.aead;\n                    }\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400\n                    // https://stash.wiki/proxy-protocols/proxy-types#vmess\n                    if (\n                        isPresent(proxy, 'cipher') &&\n                        ![\n                            'auto',\n                            'none',\n                            'zero',\n                            'aes-128-gcm',\n                            'chacha20-poly1305',\n                        ].includes(proxy.cipher)\n                    ) {\n                        proxy.cipher = 'auto';\n                    }\n                } else if (proxy.type === 'tuic') {\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    }\n                    //  else {\n                    //     proxy.alpn = ['h3'];\n                    // }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                    }\n                    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197\n                    if (\n                        (!proxy.token || proxy.token.length === 0) &&\n                        !isPresent(proxy, 'version')\n                    ) {\n                        proxy.version = 5;\n                    }\n                } else if (proxy.type === 'hysteria') {\n                    // auth_str 将会在未来某个时候删除 但是有的机场不规范\n                    if (\n                        isPresent(proxy, 'auth_str') &&\n                        !isPresent(proxy, 'auth-str')\n                    ) {\n                        proxy['auth-str'] = proxy['auth_str'];\n                    }\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                    }\n                } else if (proxy.type === 'hysteria2') {\n                    // 新版已更改\n                    // if (proxy['obfs-password'] && proxy.obfs == 'salamander') {\n                    //     proxy.obfs = proxy['obfs-password'];\n                    //     delete proxy['obfs-password'];\n                    // }\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                    }\n                } else if (proxy.type === 'wireguard') {\n                    proxy.keepalive =\n                        proxy.keepalive ?? proxy['persistent-keepalive'];\n                    proxy['persistent-keepalive'] = proxy.keepalive;\n                    proxy['preshared-key'] =\n                        proxy['preshared-key'] ?? proxy['pre-shared-key'];\n                    proxy['pre-shared-key'] = proxy['preshared-key'];\n                } else if (proxy.type === 'snell' && proxy.version < 3) {\n                    delete proxy.udp;\n                } else if (proxy.type === 'vless') {\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                } else if (proxy.type === 'ss') {\n                    if (\n                        isPresent(proxy, 'shadow-tls-password') &&\n                        !isPresent(proxy, 'plugin')\n                    ) {\n                        proxy.plugin = 'shadow-tls';\n                        proxy['plugin-opts'] = {\n                            host: proxy['shadow-tls-sni'],\n                            password: proxy['shadow-tls-password'],\n                            version: proxy['shadow-tls-version'],\n                        };\n                        delete proxy['shadow-tls-password'];\n                        delete proxy['shadow-tls-sni'];\n                        delete proxy['shadow-tls-version'];\n                    }\n                }\n\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'http'\n                ) {\n                    let httpPath = proxy['http-opts']?.path;\n                    if (\n                        isPresent(proxy, 'http-opts.path') &&\n                        !Array.isArray(httpPath)\n                    ) {\n                        proxy['http-opts'].path = [httpPath];\n                    }\n                    let httpHost = proxy['http-opts']?.headers?.Host;\n                    if (\n                        isPresent(proxy, 'http-opts.headers.Host') &&\n                        !Array.isArray(httpHost)\n                    ) {\n                        proxy['http-opts'].headers.Host = [httpHost];\n                    }\n                }\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'h2'\n                ) {\n                    let path = proxy['h2-opts']?.path;\n                    if (\n                        isPresent(proxy, 'h2-opts.path') &&\n                        Array.isArray(path)\n                    ) {\n                        proxy['h2-opts'].path = path[0];\n                    }\n                    let host = proxy['h2-opts']?.headers?.host;\n                    if (\n                        isPresent(proxy, 'h2-opts.headers.Host') &&\n                        !Array.isArray(host)\n                    ) {\n                        proxy['h2-opts'].headers.host = [host];\n                    }\n                }\n                if (['ws'].includes(proxy.network)) {\n                    const networkPath = proxy[`${proxy.network}-opts`]?.path;\n                    if (networkPath) {\n                        const reg = /^(.*?)(?:\\?ed=(\\d+))?$/;\n                        // eslint-disable-next-line no-unused-vars\n                        const [_, path = '', ed = ''] = reg.exec(networkPath);\n                        proxy[`${proxy.network}-opts`].path = path;\n                        if (ed !== '') {\n                            proxy['ws-opts']['early-data-header-name'] =\n                                'Sec-WebSocket-Protocol';\n                            proxy['ws-opts']['max-early-data'] = parseInt(\n                                ed,\n                                10,\n                            );\n                        }\n                    } else {\n                        proxy[`${proxy.network}-opts`] =\n                            proxy[`${proxy.network}-opts`] || {};\n                        proxy[`${proxy.network}-opts`].path = '/';\n                    }\n                }\n                if (proxy['plugin-opts']?.tls) {\n                    if (isPresent(proxy, 'skip-cert-verify')) {\n                        proxy['plugin-opts']['skip-cert-verify'] =\n                            proxy['skip-cert-verify'];\n                    }\n                }\n                if (\n                    [\n                        'trojan',\n                        'tuic',\n                        'hysteria',\n                        'hysteria2',\n                        'juicity',\n                        'anytls',\n                        'trusttunnel',\n                        'naive',\n                    ].includes(proxy.type)\n                ) {\n                    delete proxy.tls;\n                }\n\n                if (proxy['tls-fingerprint']) {\n                    proxy.fingerprint = proxy['tls-fingerprint'];\n                }\n                delete proxy['tls-fingerprint'];\n\n                if (proxy['underlying-proxy']) {\n                    proxy['dialer-proxy'] = proxy['underlying-proxy'];\n                }\n                delete proxy['underlying-proxy'];\n\n                if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {\n                    delete proxy.tls;\n                }\n                delete proxy.subName;\n                delete proxy.collectionName;\n                delete proxy.id;\n                delete proxy.resolved;\n                delete proxy['no-resolve'];\n                if (type !== 'internal') {\n                    for (const key in proxy) {\n                        if (proxy[key] == null || /^_/i.test(key)) {\n                            delete proxy[key];\n                        }\n                    }\n                }\n                if (\n                    ['grpc'].includes(proxy.network) &&\n                    proxy[`${proxy.network}-opts`]\n                ) {\n                    delete proxy[`${proxy.network}-opts`]['_grpc-type'];\n                    delete proxy[`${proxy.network}-opts`]['_grpc-authority'];\n                }\n                return proxy;\n            });\n        return type === 'internal'\n            ? list\n            : 'proxies:\\n' +\n                  list\n                      .map((proxy) => {\n                          return '  - ' + JSON.stringify(proxy) + '\\n';\n                      })\n                      .join('');\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/sing-box.js",
    "content": "import ClashMeta_Producer from './clashmeta';\nimport $ from '@/core/app';\nimport { isIPv4, isIPv6, isPlainObject } from '@/utils';\n\nconst ipVersions = {\n    ipv4: 'ipv4_only',\n    ipv6: 'ipv6_only',\n    'v4-only': 'ipv4_only',\n    'v6-only': 'ipv6_only',\n    'ipv4-prefer': 'prefer_ipv4',\n    'ipv6-prefer': 'prefer_ipv6',\n    'prefer-v4': 'prefer_ipv4',\n    'prefer-v6': 'prefer_ipv6',\n};\n\nconst ipVersionParser = (proxy, parsedProxy) => {\n    const strategy = ipVersions[proxy['ip-version']];\n    if (proxy._dns_server && strategy) {\n        parsedProxy.domain_resolver = {\n            server: proxy._dns_server,\n            strategy,\n        };\n    }\n};\nconst detourParser = (proxy, parsedProxy) => {\n    parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;\n};\nconst networkParser = (proxy, parsedProxy) => {\n    if (['tcp', 'udp'].includes(proxy._network))\n        parsedProxy.network = proxy._network;\n};\nconst tfoParser = (proxy, parsedProxy) => {\n    parsedProxy.tcp_fast_open = false;\n    if (proxy.tfo) parsedProxy.tcp_fast_open = true;\n    if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;\n    if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;\n    if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;\n};\n\nconst smuxParser = (smux, proxy) => {\n    if (!smux || !smux.enabled) return;\n    proxy.multiplex = { enabled: true };\n    proxy.multiplex.protocol = smux.protocol;\n    if (smux['max-connections'])\n        proxy.multiplex.max_connections = parseInt(\n            `${smux['max-connections']}`,\n            10,\n        );\n    if (smux['max-streams'])\n        proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);\n    if (smux['min-streams'])\n        proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);\n    if (smux.padding) proxy.multiplex.padding = true;\n    if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) {\n        proxy.multiplex.brutal = {\n            enabled: true,\n        };\n        if (smux['brutal-opts']?.up)\n            proxy.multiplex.brutal.up_mbps = parseInt(\n                `${smux['brutal-opts']?.up}`,\n                10,\n            );\n        if (smux['brutal-opts']?.down)\n            proxy.multiplex.brutal.down_mbps = parseInt(\n                `${smux['brutal-opts']?.down}`,\n                10,\n            );\n    }\n};\n\nconst wsParser = (proxy, parsedProxy) => {\n    const transport = { type: 'ws', headers: {} };\n    if (proxy['ws-opts']) {\n        const {\n            path: wsPath = '',\n            headers: wsHeaders = {},\n            'max-early-data': max_early_data,\n            'early-data-header-name': early_data_header_name,\n        } = proxy['ws-opts'];\n        transport.early_data_header_name = early_data_header_name;\n        transport.max_early_data = max_early_data\n            ? parseInt(max_early_data, 10)\n            : undefined;\n        if (wsPath !== '') transport.path = `${wsPath}`;\n        if (Object.keys(wsHeaders).length > 0) {\n            const headers = {};\n            for (const key of Object.keys(wsHeaders)) {\n                let value = wsHeaders[key];\n                if (value === '') continue;\n                if (!Array.isArray(value)) value = [`${value}`];\n                if (value.length > 0) headers[key] = value;\n            }\n            const { Host: wsHost } = headers;\n            if (wsHost.length === 1)\n                for (const item of `Host:${wsHost[0]}`.split('\\n')) {\n                    const [key, value] = item.split(':');\n                    if (value.trim() === '') continue;\n                    headers[key.trim()] = value.trim().split(',');\n                }\n            transport.headers = headers;\n        }\n    }\n    if (proxy['ws-headers']) {\n        const headers = {};\n        for (const key of Object.keys(proxy['ws-headers'])) {\n            let value = proxy['ws-headers'][key];\n            if (value === '') continue;\n            if (!Array.isArray(value)) value = [`${value}`];\n            if (value.length > 0) headers[key] = value;\n        }\n        const { Host: wsHost } = headers;\n        if (wsHost.length === 1)\n            for (const item of `Host:${wsHost[0]}`.split('\\n')) {\n                const [key, value] = item.split(':');\n                if (value.trim() === '') continue;\n                headers[key.trim()] = value.trim().split(',');\n            }\n        for (const key of Object.keys(headers))\n            transport.headers[key] = headers[key];\n    }\n    if (proxy['ws-path'] && proxy['ws-path'] !== '')\n        transport.path = `${proxy['ws-path']}`;\n    if (transport.path) {\n        const reg = /^(.*?)(?:\\?ed=(\\d+))?$/;\n        // eslint-disable-next-line no-unused-vars\n        const [_, path = '', ed = ''] = reg.exec(transport.path);\n        transport.path = path;\n        if (ed !== '') {\n            transport.early_data_header_name = 'Sec-WebSocket-Protocol';\n            transport.max_early_data = parseInt(ed, 10);\n        }\n    }\n\n    if (parsedProxy.tls.insecure)\n        parsedProxy.tls.server_name = transport.headers.Host[0];\n    if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {\n        transport.type = 'httpupgrade';\n        if (transport.headers.Host) {\n            transport.host = transport.headers.Host[0];\n            delete transport.headers.Host;\n        }\n        if (transport.max_early_data) delete transport.max_early_data;\n        if (transport.early_data_header_name)\n            delete transport.early_data_header_name;\n    }\n    for (const key of Object.keys(transport.headers)) {\n        const value = transport.headers[key];\n        if (value.length === 1) transport.headers[key] = value[0];\n    }\n    parsedProxy.transport = transport;\n};\n\nconst h1Parser = (proxy, parsedProxy) => {\n    const transport = { type: 'http', headers: {} };\n    if (proxy['http-opts']) {\n        const {\n            method = '',\n            path: h1Path = '',\n            headers: h1Headers = {},\n        } = proxy['http-opts'];\n        if (method !== '') transport.method = method;\n        if (Array.isArray(h1Path)) {\n            transport.path = `${h1Path[0]}`;\n        } else if (h1Path !== '') transport.path = `${h1Path}`;\n        for (const key of Object.keys(h1Headers)) {\n            let value = h1Headers[key];\n            if (value === '') continue;\n            if (key.toLowerCase() === 'host') {\n                let host = value;\n                if (!Array.isArray(host))\n                    host = `${host}`.split(',').map((i) => i.trim());\n                if (host.length > 0) transport.host = host;\n                continue;\n            }\n            if (!Array.isArray(value))\n                value = `${value}`.split(',').map((i) => i.trim());\n            if (value.length > 0) transport.headers[key] = value;\n        }\n    }\n    if (proxy['http-host'] && proxy['http-host'] !== '') {\n        let host = proxy['http-host'];\n        if (!Array.isArray(host))\n            host = `${host}`.split(',').map((i) => i.trim());\n        if (host.length > 0) transport.host = host;\n    }\n    // if (!transport.host) return;\n    if (proxy['http-path'] && proxy['http-path'] !== '') {\n        const path = proxy['http-path'];\n        if (Array.isArray(path)) {\n            transport.path = `${path[0]}`;\n        } else if (path !== '') transport.path = `${path}`;\n    }\n    if (parsedProxy.tls.insecure)\n        parsedProxy.tls.server_name = transport.host[0];\n    if (transport.host?.length === 1) transport.host = transport.host[0];\n    for (const key of Object.keys(transport.headers)) {\n        const value = transport.headers[key];\n        if (value.length === 1) transport.headers[key] = value[0];\n    }\n    parsedProxy.transport = transport;\n};\n\nconst h2Parser = (proxy, parsedProxy) => {\n    const transport = { type: 'http' };\n    if (proxy['h2-opts']) {\n        let { host = '', path = '' } = proxy['h2-opts'];\n        if (path !== '') transport.path = `${path}`;\n        if (host !== '') {\n            if (!Array.isArray(host))\n                host = `${host}`.split(',').map((i) => i.trim());\n            if (host.length > 0) transport.host = host;\n        }\n    }\n    if (proxy['h2-host'] && proxy['h2-host'] !== '') {\n        let host = proxy['h2-host'];\n        if (!Array.isArray(host))\n            host = `${host}`.split(',').map((i) => i.trim());\n        if (host.length > 0) transport.host = host;\n    }\n    if (proxy['h2-path'] && proxy['h2-path'] !== '')\n        transport.path = `${proxy['h2-path']}`;\n    parsedProxy.tls.enabled = true;\n    if (parsedProxy.tls.insecure)\n        parsedProxy.tls.server_name = transport.host[0];\n    if (transport.host.length === 1) transport.host = transport.host[0];\n    parsedProxy.transport = transport;\n};\n\nconst grpcParser = (proxy, parsedProxy) => {\n    const transport = { type: 'grpc' };\n    if (proxy['grpc-opts']) {\n        const serviceName = proxy['grpc-opts']['grpc-service-name'];\n        if (serviceName != null && serviceName !== '')\n            transport.service_name = `${serviceName}`;\n    }\n    parsedProxy.transport = transport;\n};\n\nconst tlsParser = (proxy, parsedProxy) => {\n    if (proxy.tls) parsedProxy.tls.enabled = true;\n    if (proxy.servername && proxy.servername !== '')\n        parsedProxy.tls.server_name = proxy.servername;\n    if (proxy.peer && proxy.peer !== '')\n        parsedProxy.tls.server_name = proxy.peer;\n    if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;\n    if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;\n    if (proxy.insecure) parsedProxy.tls.insecure = true;\n    if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;\n    if (typeof proxy.alpn === 'string') {\n        parsedProxy.tls.alpn = [proxy.alpn];\n    } else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;\n    if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;\n    if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];\n    if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];\n    if (proxy['reality-opts']) {\n        parsedProxy.tls.reality = { enabled: true };\n        if (proxy['reality-opts']['public-key'])\n            parsedProxy.tls.reality.public_key =\n                proxy['reality-opts']['public-key'];\n        if (proxy['reality-opts']['short-id'])\n            parsedProxy.tls.reality.short_id =\n                proxy['reality-opts']['short-id'];\n        parsedProxy.tls.utls = { enabled: true };\n    }\n    if (\n        !['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&\n        proxy['client-fingerprint'] &&\n        proxy['client-fingerprint'] !== ''\n    )\n        parsedProxy.tls.utls = {\n            enabled: true,\n            fingerprint: proxy['client-fingerprint'],\n        };\n    if (proxy._ech && isPlainObject(proxy._ech)) {\n        parsedProxy.tls.ech = proxy._ech;\n    }\n    if (proxy._curve_preferences && Array.isArray(proxy._curve_preferences)) {\n        parsedProxy.tls.curve_preferences = proxy._curve_preferences;\n    }\n    if (proxy['_fragment']) parsedProxy.tls.fragment = !!proxy['_fragment'];\n    if (proxy['_fragment_fallback_delay'])\n        parsedProxy.tls.fragment_fallback_delay =\n            proxy['_fragment_fallback_delay'];\n    if (proxy['_record_fragment'])\n        parsedProxy.tls.record_fragment = !!proxy['_record_fragment'];\n    if (proxy['_certificate'])\n        parsedProxy.tls.certificate = proxy['_certificate'];\n    if (proxy['_certificate_path'])\n        parsedProxy.tls.certificate_path = proxy['_certificate_path'];\n    if (proxy['_certificate_public_key_sha256'])\n        parsedProxy.tls.certificate_public_key_sha256 =\n            proxy['_certificate_public_key_sha256'];\n    if (proxy['_client_certificate'])\n        parsedProxy.tls.client_certificate = proxy['_client_certificate'];\n    if (proxy['_client_certificate_path'])\n        parsedProxy.tls.client_certificate_path =\n            proxy['_client_certificate_path'];\n    if (proxy['_client_key']) parsedProxy.tls.client_key = proxy['_client_key'];\n    if (proxy['_client_key_path'])\n        parsedProxy.tls.client_key_path = proxy['_client_key_path'];\n    if (!parsedProxy.tls.enabled) delete parsedProxy.tls;\n};\n\nconst sshParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'ssh',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.username) parsedProxy.user = proxy.username;\n    if (proxy.password) parsedProxy.password = proxy.password;\n    // https://wiki.metacubex.one/config/proxies/ssh\n    // https://sing-box.sagernet.org/zh/configuration/outbound/ssh\n    if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];\n    if (proxy['private-key'])\n        parsedProxy.private_key_path = proxy['private-key'];\n    if (proxy['private-key-passphrase'])\n        parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];\n    if (proxy['server-fingerprint']) {\n        parsedProxy.host_key = [proxy['server-fingerprint']];\n        // https://manual.nssurge.com/policy/ssh.html\n        // Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.)\n        // TODO: ?\n        parsedProxy.host_key_algorithms = [\n            proxy['server-fingerprint'].split(' ')[0],\n        ];\n    }\n    if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];\n    if (proxy['host-key-algorithms'])\n        parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\n\nconst httpParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'http',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        tls: { enabled: false, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.username) parsedProxy.username = proxy.username;\n    if (proxy.password) parsedProxy.password = proxy.password;\n    if (proxy.headers) {\n        parsedProxy.headers = {};\n        for (const k of Object.keys(proxy.headers)) {\n            parsedProxy.headers[k] = `${proxy.headers[k]}`;\n        }\n        if (Object.keys(parsedProxy.headers).length === 0)\n            delete parsedProxy.headers;\n    }\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\n\nconst socks5Parser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'socks',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        version: '5',\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.username) parsedProxy.username = proxy.username;\n    if (proxy.password) parsedProxy.password = proxy.password;\n    if (proxy.uot) parsedProxy.udp_over_tcp = true;\n    if (proxy['udp-over-tcp']) {\n        parsedProxy.udp_over_tcp = {\n            enabled: true,\n            version:\n                !proxy['udp-over-tcp-version'] ||\n                proxy['udp-over-tcp-version'] === 1\n                    ? 1\n                    : 2,\n        };\n    }\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\n\nconst shadowTLSParser = (proxy = {}) => {\n    const ssPart = {\n        tag: proxy.name,\n        type: 'shadowsocks',\n        method: proxy.cipher,\n        password: proxy.password,\n        detour: `${proxy.name}_shadowtls`,\n    };\n    if (proxy.uot) ssPart.udp_over_tcp = true;\n    if (proxy['udp-over-tcp']) {\n        ssPart.udp_over_tcp = {\n            enabled: true,\n            version:\n                !proxy['udp-over-tcp-version'] ||\n                proxy['udp-over-tcp-version'] === 1\n                    ? 1\n                    : 2,\n        };\n    }\n    const stPart = {\n        tag: `${proxy.name}_shadowtls`,\n        type: 'shadowtls',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        version: proxy['plugin-opts'].version,\n        password: proxy['plugin-opts'].password,\n        tls: {\n            enabled: true,\n            server_name: proxy['plugin-opts'].host,\n            utls: {\n                enabled: true,\n                fingerprint: proxy['client-fingerprint'],\n            },\n        },\n    };\n    if (stPart.server_port < 0 || stPart.server_port > 65535)\n        throw '端口值非法';\n    if (proxy['fast-open'] === true) stPart.udp_fragment = true;\n    tfoParser(proxy, stPart);\n    detourParser(proxy, stPart);\n    smuxParser(proxy.smux, ssPart);\n    ipVersionParser(proxy, stPart);\n    return { type: 'ss-with-st', ssPart, stPart };\n};\nconst ssParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'shadowsocks',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        method: proxy.cipher,\n        password: proxy.password,\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.uot) parsedProxy.udp_over_tcp = true;\n    if (proxy['udp-over-tcp']) {\n        parsedProxy.udp_over_tcp = {\n            enabled: true,\n            version:\n                !proxy['udp-over-tcp-version'] ||\n                proxy['udp-over-tcp-version'] === 1\n                    ? 1\n                    : 2,\n        };\n    }\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    if (proxy.plugin) {\n        const optArr = [];\n        if (proxy.plugin === 'obfs') {\n            parsedProxy.plugin = 'obfs-local';\n            parsedProxy.plugin_opts = '';\n            if (proxy['obfs-host'])\n                proxy['plugin-opts'].host = proxy['obfs-host'];\n            Object.keys(proxy['plugin-opts']).forEach((k) => {\n                switch (k) {\n                    case 'mode':\n                        optArr.push(`obfs=${proxy['plugin-opts'].mode}`);\n                        break;\n                    case 'host':\n                        optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);\n                        break;\n                    default:\n                        optArr.push(`${k}=${proxy['plugin-opts'][k]}`);\n                        break;\n                }\n            });\n        }\n        if (proxy.plugin === 'v2ray-plugin') {\n            parsedProxy.plugin = 'v2ray-plugin';\n            if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];\n            if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];\n            Object.keys(proxy['plugin-opts']).forEach((k) => {\n                switch (k) {\n                    case 'tls':\n                        if (proxy['plugin-opts'].tls) optArr.push('tls');\n                        break;\n                    case 'host':\n                        optArr.push(`host=${proxy['plugin-opts'].host}`);\n                        break;\n                    case 'path':\n                        optArr.push(`path=${proxy['plugin-opts'].path}`);\n                        break;\n                    case 'headers':\n                        optArr.push(\n                            `headers=${JSON.stringify(\n                                proxy['plugin-opts'].headers,\n                            )}`,\n                        );\n                        break;\n                    case 'mux':\n                        if (proxy['plugin-opts'].mux)\n                            parsedProxy.multiplex = { enabled: true };\n                        break;\n                    default:\n                        optArr.push(`${k}=${proxy['plugin-opts'][k]}`);\n                }\n            });\n        }\n        parsedProxy.plugin_opts = optArr.join(';');\n    }\n\n    return parsedProxy;\n};\n// eslint-disable-next-line no-unused-vars\nconst ssrParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'shadowsocksr',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        method: proxy.cipher,\n        password: proxy.password,\n        obfs: proxy.obfs,\n        protocol: proxy.protocol,\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];\n    if (proxy['protocol-param'] && proxy['protocol-param'] !== '')\n        parsedProxy.protocol_param = proxy['protocol-param'];\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\n\nconst vmessParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'vmess',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        uuid: proxy.uuid,\n        security: proxy.cipher,\n        alter_id: parseInt(`${proxy.alterId}`, 10),\n        tls: { enabled: false, server_name: proxy.server, insecure: false },\n    };\n    if (\n        [\n            'auto',\n            'none',\n            'zero',\n            'aes-128-gcm',\n            'chacha20-poly1305',\n            'aes-128-ctr',\n        ].indexOf(parsedProxy.security) === -1\n    )\n        parsedProxy.security = 'auto';\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    if (proxy.network === 'ws') wsParser(proxy, parsedProxy);\n    if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);\n    if (proxy.network === 'http') h1Parser(proxy, parsedProxy);\n    if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\n\nconst vlessParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'vless',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        uuid: proxy.uuid,\n        tls: { enabled: false, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    // if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow;\n    if (proxy.flow != null) parsedProxy.flow = proxy.flow;\n    if (proxy.network === 'ws') wsParser(proxy, parsedProxy);\n    if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);\n    if (proxy.network === 'http') h1Parser(proxy, parsedProxy);\n    if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\nconst trojanParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'trojan',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        password: proxy.password,\n        tls: { enabled: true, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);\n    if (proxy.network === 'ws') wsParser(proxy, parsedProxy);\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\nconst naiveParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'naive',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        tls: { enabled: true, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy.username) parsedProxy.username = proxy.username;\n    if (proxy.password) parsedProxy.password = proxy.password;\n    if (proxy.uot) parsedProxy.udp_over_tcp = true;\n    if (proxy['udp-over-tcp']) {\n        parsedProxy.udp_over_tcp = {\n            enabled: true,\n            version:\n                !proxy['udp-over-tcp-version'] ||\n                proxy['udp-over-tcp-version'] === 1\n                    ? 1\n                    : 2,\n        };\n    }\n    const insecure_concurrency = parseInt(\n        `${proxy['insecure-concurrency']}`,\n        10,\n    );\n    if (Number.isInteger(insecure_concurrency) && insecure_concurrency >= 0)\n        parsedProxy.insecure_concurrency = insecure_concurrency;\n    if (proxy['extra-headers'])\n        parsedProxy.extra_headers = proxy['extra-headers'];\n    if (proxy.quic) parsedProxy.quic = !!proxy.quic;\n    if (proxy['quic-congestion-control'])\n        parsedProxy.quic_congestion_control = proxy['quic-congestion-control'];\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    if (parsedProxy.tls?.insecure) {\n        $.info(\n            `Platform sing-box: insecure is not supported on naive outbound`,\n        );\n        delete parsedProxy.tls.insecure;\n    }\n\n    return parsedProxy;\n};\nconst hysteriaParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'hysteria',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        disable_mtu_discovery: false,\n        tls: { enabled: true, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy['hop-interval'])\n        parsedProxy.hop_interval = /^\\d+$/.test(proxy['hop-interval'])\n            ? `${proxy['hop-interval']}s`\n            : proxy['hop-interval'];\n    if (proxy['ports'])\n        parsedProxy.server_ports = proxy['ports'].split(/\\s*,\\s*/).map((p) => {\n            const range = p.replace(/\\s*-\\s*/g, ':');\n            return range.includes(':') ? range : `${range}:${range}`;\n        });\n    if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;\n    if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    // eslint-disable-next-line no-control-regex\n    const reg = new RegExp('^[0-9]+[ \\t]*[KMGT]*[Bb]ps$');\n    // sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps\n    if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) {\n        parsedProxy.up = `${proxy.up}`;\n    } else {\n        parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);\n    }\n    if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) {\n        parsedProxy.down = `${proxy.down}`;\n    } else {\n        parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);\n    }\n    if (proxy.obfs) parsedProxy.obfs = proxy.obfs;\n    if (proxy.recv_window_conn)\n        parsedProxy.recv_window_conn = proxy.recv_window_conn;\n    if (proxy['recv-window-conn'])\n        parsedProxy.recv_window_conn = proxy['recv-window-conn'];\n    if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;\n    if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];\n    if (proxy.disable_mtu_discovery) {\n        if (typeof proxy.disable_mtu_discovery === 'boolean') {\n            parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;\n        } else {\n            if (proxy.disable_mtu_discovery === 1)\n                parsedProxy.disable_mtu_discovery = true;\n        }\n    }\n    networkParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\nconst hysteria2Parser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'hysteria2',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        password: proxy.password,\n        obfs: {},\n        tls: { enabled: true, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy['hop-interval'])\n        parsedProxy.hop_interval = /^\\d+$/.test(proxy['hop-interval'])\n            ? `${proxy['hop-interval']}s`\n            : proxy['hop-interval'];\n    if (proxy['ports'])\n        parsedProxy.server_ports = proxy['ports'].split(/\\s*,\\s*/).map((p) => {\n            const range = p.replace(/\\s*-\\s*/g, ':');\n            return range.includes(':') ? range : `${range}:${range}`;\n        });\n    if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);\n    if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);\n    if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';\n    if (proxy['obfs-password'])\n        parsedProxy.obfs.password = proxy['obfs-password'];\n    if (!parsedProxy.obfs.type) delete parsedProxy.obfs;\n    networkParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\nconst tuic5Parser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'tuic',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        uuid: proxy.uuid,\n        password: proxy.password,\n        tls: { enabled: true, server_name: proxy.server, insecure: false },\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    if (\n        proxy['congestion-controller'] &&\n        proxy['congestion-controller'] !== 'cubic'\n    )\n        parsedProxy.congestion_control = proxy['congestion-controller'];\n    if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')\n        parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];\n    if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;\n    if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;\n    if (proxy['heartbeat-interval'])\n        parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\nconst anytlsParser = (proxy = {}) => {\n    const parsedProxy = {\n        tag: proxy.name,\n        type: 'anytls',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        password: proxy.password,\n        tls: { enabled: true, server_name: proxy.server, insecure: false },\n    };\n    if (/^\\d+$/.test(proxy['idle-session-check-interval']))\n        parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;\n    if (/^\\d+$/.test(proxy['idle-session-timeout']))\n        parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;\n    if (/^\\d+$/.test(proxy['min-idle-session']))\n        parsedProxy.min_idle_session = parseInt(\n            `${proxy['min-idle-session']}`,\n            10,\n        );\n    networkParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    tlsParser(proxy, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    return parsedProxy;\n};\n\nconst wireguardParser = (proxy = {}) => {\n    const address = ['ip', 'ipv6']\n        .map((i) => proxy[i])\n        .map((i) => {\n            if (isIPv4(i)) return `${i}/32`;\n            if (isIPv6(i)) return `${i}/128`;\n        })\n        .filter((i) => i);\n    const parsedProxy = {\n        system: !!proxy.system,\n        mtu: proxy.mtu ? parseInt(`${proxy.mtu}`, 10) : undefined,\n        udp_timeout: proxy['udp-timeout']\n            ? parseInt(`${proxy['udp-timeout']}`, 10)\n            : undefined,\n        workers: proxy['workers']\n            ? parseInt(`${proxy['workers']}`, 10)\n            : undefined,\n        tag: proxy.name,\n        type: 'wireguard',\n        server: proxy.server,\n        server_port: parseInt(`${proxy.port}`, 10),\n        address,\n        private_key: proxy['private-key'],\n        peer_public_key: proxy['public-key'],\n        pre_shared_key: proxy['pre-shared-key'],\n        reserved: [],\n    };\n    if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)\n        throw 'invalid port';\n    if (proxy['fast-open']) parsedProxy.udp_fragment = true;\n    if (typeof proxy.reserved === 'string') {\n        parsedProxy.reserved = proxy.reserved;\n    } else if (Array.isArray(proxy.reserved)) {\n        for (const r of proxy.reserved) parsedProxy.reserved.push(r);\n    } else {\n        delete parsedProxy.reserved;\n    }\n    if (!Array.isArray(proxy.peers) || proxy.peers.length === 0) {\n        proxy.peers = [{}];\n    }\n    if (proxy.peers && proxy.peers.length > 0) {\n        parsedProxy.peers = [];\n        for (const p of proxy.peers) {\n            let address;\n            let port;\n            if (p.server && p.port) {\n                address = p.server;\n                port = parseInt(`${p.port}`, 10);\n            } else {\n                address = parsedProxy.server;\n                port = parseInt(`${parsedProxy.server_port}`, 10);\n            }\n            const peer = {\n                address,\n                port,\n                persistent_keepalive_interval: p[\n                    'persistent-keepalive-interval'\n                ]\n                    ? parseInt(`${p['persistent-keepalive-interval']}`, 10)\n                    : undefined,\n                public_key:\n                    p['public-key'] ||\n                    p['public_key'] ||\n                    parsedProxy.peer_public_key,\n                pre_shared_key:\n                    p['pre-shared-key'] ||\n                    p['pre_shared_key'] ||\n                    parsedProxy.pre_shared_key,\n                allowed_ips: p['allowed-ips'] ||\n                    p.allowed_ips || [\n                        '0.0.0.0/0',\n                        ...(proxy.ipv6 ? ['::/0'] : []),\n                    ],\n                reserved: [],\n            };\n            if (typeof p.reserved === 'string') {\n                peer.reserved.push(p.reserved);\n            } else if (Array.isArray(p.reserved)) {\n                for (const r of p.reserved) peer.reserved.push(r);\n            } else {\n                delete peer.reserved;\n            }\n            if (!Array.isArray(peer.reserved) || peer.reserved.length === 0) {\n                peer.reserved = parsedProxy.reserved;\n            }\n            // if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];\n            parsedProxy.peers.push(peer);\n        }\n    }\n    networkParser(proxy, parsedProxy);\n    tfoParser(proxy, parsedProxy);\n    detourParser(proxy, parsedProxy);\n    smuxParser(proxy.smux, parsedProxy);\n    ipVersionParser(proxy, parsedProxy);\n    delete parsedProxy.server;\n    delete parsedProxy.server_port;\n    delete parsedProxy.pre_shared_key;\n    delete parsedProxy.peer_public_key;\n    delete parsedProxy.reserved;\n    return parsedProxy;\n};\n\nexport default function singbox_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type, opts = {}) => {\n        const list = [];\n        ClashMeta_Producer()\n            .produce(proxies, 'internal', { 'include-unsupported-proxy': true })\n            .map((proxy) => {\n                try {\n                    switch (proxy.type) {\n                        case 'ssh':\n                            list.push(sshParser(proxy));\n                            break;\n                        case 'http':\n                            list.push(httpParser(proxy));\n                            break;\n                        case 'socks5':\n                            if (proxy.tls) {\n                                throw new Error(\n                                    `Platform sing-box does not support proxy type: ${proxy.type} with tls`,\n                                );\n                            } else {\n                                list.push(socks5Parser(proxy));\n                            }\n                            break;\n                        case 'ss':\n                            // if (!proxy.cipher) {\n                            //     proxy.cipher = 'none';\n                            // }\n                            // if (\n                            //     ![\n                            //         '2022-blake3-aes-128-gcm',\n                            //         '2022-blake3-aes-256-gcm',\n                            //         '2022-blake3-chacha20-poly1305',\n                            //         'aes-128-cfb',\n                            //         'aes-128-ctr',\n                            //         'aes-128-gcm',\n                            //         'aes-192-cfb',\n                            //         'aes-192-ctr',\n                            //         'aes-192-gcm',\n                            //         'aes-256-cfb',\n                            //         'aes-256-ctr',\n                            //         'aes-256-gcm',\n                            //         'chacha20-ietf',\n                            //         'chacha20-ietf-poly1305',\n                            //         'none',\n                            //         'rc4-md5',\n                            //         'xchacha20',\n                            //         'xchacha20-ietf-poly1305',\n                            //     ].includes(proxy.cipher)\n                            // ) {\n                            //     throw new Error(\n                            //         `cipher ${proxy.cipher} is not supported`,\n                            //     );\n                            // }\n                            if (proxy.plugin === 'shadow-tls') {\n                                const { ssPart, stPart } =\n                                    shadowTLSParser(proxy);\n                                list.push(ssPart);\n                                list.push(stPart);\n                            } else {\n                                list.push(ssParser(proxy));\n                            }\n                            break;\n                        case 'ssr':\n                            if (opts['include-unsupported-proxy']) {\n                                list.push(ssrParser(proxy));\n                            } else {\n                                throw new Error(\n                                    `Platform sing-box does not support proxy type: ${proxy.type}`,\n                                );\n                            }\n                            break;\n                        case 'vmess':\n                            if (\n                                !proxy.network ||\n                                ['tcp', 'ws', 'grpc', 'h2', 'http'].includes(\n                                    proxy.network,\n                                )\n                            ) {\n                                list.push(vmessParser(proxy));\n                            } else {\n                                throw new Error(\n                                    `Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,\n                                );\n                            }\n                            break;\n                        case 'vless':\n                            if (\n                                proxy.encryption &&\n                                proxy.encryption !== 'none'\n                            ) {\n                                throw new Error(\n                                    `VLESS encryption is not supported`,\n                                );\n                            }\n                            if (\n                                !proxy.flow ||\n                                ['xtls-rprx-vision'].includes(proxy.flow)\n                            ) {\n                                list.push(vlessParser(proxy));\n                            } else {\n                                throw new Error(\n                                    `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,\n                                );\n                            }\n                            break;\n                        case 'trojan':\n                            if (!proxy.flow) {\n                                list.push(trojanParser(proxy));\n                            } else {\n                                throw new Error(\n                                    `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,\n                                );\n                            }\n                            break;\n                        case 'naive':\n                            list.push(naiveParser(proxy));\n                            break;\n                        case 'hysteria':\n                            list.push(hysteriaParser(proxy));\n                            break;\n                        case 'hysteria2':\n                            list.push(\n                                hysteria2Parser(\n                                    proxy,\n                                    opts['include-unsupported-proxy'],\n                                ),\n                            );\n                            break;\n                        case 'tuic':\n                            if (!proxy.token || proxy.token.length === 0) {\n                                list.push(tuic5Parser(proxy));\n                            } else {\n                                throw new Error(\n                                    `Platform sing-box does not support proxy type: TUIC v4`,\n                                );\n                            }\n                            break;\n                        case 'wireguard':\n                            list.push(wireguardParser(proxy));\n                            break;\n                        case 'anytls':\n                            list.push(anytlsParser(proxy));\n                            break;\n                        default:\n                            throw new Error(\n                                `Platform sing-box does not support proxy type: ${proxy.type}`,\n                            );\n                    }\n                } catch (e) {\n                    // console.log(e);\n                    $.error(e.message ?? e);\n                }\n            });\n\n        if (type === 'internal') return list;\n\n        const categorized = list.reduce(\n            (result, item) => {\n                if (['wireguard'].includes(item.type)) {\n                    result.endpoints.push(item);\n                } else {\n                    result.outbounds.push(item);\n                }\n                return result;\n            },\n            { outbounds: [], endpoints: [] },\n        );\n\n        return JSON.stringify(categorized, null, 2);\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/stash.js",
    "content": "import { isPresent } from '@/core/proxy-utils/producers/utils';\nimport $ from '@/core/app';\n\nexport default function Stash_Producer() {\n    const type = 'ALL';\n    const produce = (proxies, type, opts = {}) => {\n        // https://stash.wiki/proxy-protocols/proxy-types#shadowsocks\n        const list = proxies\n            .filter((proxy) => {\n                if (\n                    ![\n                        'ss',\n                        'ssr',\n                        'vmess',\n                        'socks5',\n                        'http',\n                        'snell',\n                        'trojan',\n                        'tuic',\n                        'vless',\n                        'wireguard',\n                        'hysteria',\n                        'hysteria2',\n                        'ssh',\n                        'juicity',\n                        'anytls',\n                    ].includes(proxy.type) ||\n                    (proxy.type === 'ss' &&\n                        ![\n                            'aes-128-gcm',\n                            'aes-192-gcm',\n                            'aes-256-gcm',\n                            'aes-128-cfb',\n                            'aes-192-cfb',\n                            'aes-256-cfb',\n                            'aes-128-ctr',\n                            'aes-192-ctr',\n                            'aes-256-ctr',\n                            'rc4-md5',\n                            'chacha20-ietf',\n                            'xchacha20',\n                            'chacha20-ietf-poly1305',\n                            'xchacha20-ietf-poly1305',\n                            '2022-blake3-aes-128-gcm',\n                            '2022-blake3-aes-256-gcm',\n                        ].includes(proxy.cipher)) ||\n                    (proxy.type === 'snell' && proxy.version >= 4) ||\n                    (proxy.type === 'vless' &&\n                        proxy['reality-opts'] &&\n                        !['xtls-rprx-vision'].includes(proxy.flow))\n                ) {\n                    return false;\n                } else if (\n                    ['anytls'].includes(proxy.type) &&\n                    proxy.network &&\n                    (!['tcp'].includes(proxy.network) ||\n                        (['tcp'].includes(proxy.network) &&\n                            proxy['reality-opts']))\n                ) {\n                    return false;\n                } else if (['xhttp'].includes(proxy.network)) {\n                    return false;\n                } else if (\n                    proxy.encryption &&\n                    proxy.encryption !== 'none' &&\n                    ['vless'].includes(proxy.type)\n                ) {\n                    return false;\n                } else if (\n                    ['ws'].includes(proxy.network) &&\n                    proxy['ws-opts']?.['v2ray-http-upgrade']\n                ) {\n                    return false;\n                }\n                return true;\n            })\n            .map((proxy) => {\n                if (proxy.type === 'vmess') {\n                    // handle vmess aead\n                    if (isPresent(proxy, 'aead')) {\n                        if (proxy.aead) {\n                            proxy.alterId = 0;\n                        }\n                        delete proxy.aead;\n                    }\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400\n                    // https://stash.wiki/proxy-protocols/proxy-types#vmess\n                    if (\n                        isPresent(proxy, 'cipher') &&\n                        ![\n                            'auto',\n                            'aes-128-gcm',\n                            'chacha20-poly1305',\n                            'none',\n                        ].includes(proxy.cipher)\n                    ) {\n                        proxy.cipher = 'auto';\n                    }\n                } else if (proxy.type === 'tuic') {\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    } else {\n                        proxy.alpn = ['h3'];\n                    }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                        delete proxy.tfo;\n                    }\n                    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197\n                    if (\n                        (!proxy.token || proxy.token.length === 0) &&\n                        !isPresent(proxy, 'version')\n                    ) {\n                        proxy.version = 5;\n                    }\n                } else if (proxy.type === 'hysteria') {\n                    // auth_str 将会在未来某个时候删除 但是有的机场不规范\n                    if (\n                        isPresent(proxy, 'auth_str') &&\n                        !isPresent(proxy, 'auth-str')\n                    ) {\n                        proxy['auth-str'] = proxy['auth_str'];\n                    }\n                    if (isPresent(proxy, 'alpn')) {\n                        proxy.alpn = Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : [proxy.alpn];\n                    }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                        delete proxy.tfo;\n                    }\n                    if (\n                        isPresent(proxy, 'down') &&\n                        !isPresent(proxy, 'down-speed')\n                    ) {\n                        proxy['down-speed'] = proxy.down;\n                        delete proxy.down;\n                    }\n                    if (\n                        isPresent(proxy, 'up') &&\n                        !isPresent(proxy, 'up-speed')\n                    ) {\n                        proxy['up-speed'] = proxy.up;\n                        delete proxy.up;\n                    }\n                    if (isPresent(proxy, 'down-speed')) {\n                        proxy['down-speed'] =\n                            `${proxy['down-speed']}`.match(/\\d+/)?.[0] || 0;\n                    }\n                    if (isPresent(proxy, 'up-speed')) {\n                        proxy['up-speed'] =\n                            `${proxy['up-speed']}`.match(/\\d+/)?.[0] || 0;\n                    }\n                } else if (proxy.type === 'hysteria2') {\n                    if (\n                        isPresent(proxy, 'password') &&\n                        !isPresent(proxy, 'auth')\n                    ) {\n                        proxy.auth = proxy.password;\n                        delete proxy.password;\n                    }\n                    if (\n                        isPresent(proxy, 'tfo') &&\n                        !isPresent(proxy, 'fast-open')\n                    ) {\n                        proxy['fast-open'] = proxy.tfo;\n                        delete proxy.tfo;\n                    }\n                    if (\n                        isPresent(proxy, 'down') &&\n                        !isPresent(proxy, 'down-speed')\n                    ) {\n                        proxy['down-speed'] = proxy.down;\n                        delete proxy.down;\n                    }\n                    if (\n                        isPresent(proxy, 'up') &&\n                        !isPresent(proxy, 'up-speed')\n                    ) {\n                        proxy['up-speed'] = proxy.up;\n                        delete proxy.up;\n                    }\n                    if (isPresent(proxy, 'down-speed')) {\n                        proxy['down-speed'] =\n                            `${proxy['down-speed']}`.match(/\\d+/)?.[0] || 0;\n                    }\n                    if (isPresent(proxy, 'up-speed')) {\n                        proxy['up-speed'] =\n                            `${proxy['up-speed']}`.match(/\\d+/)?.[0] || 0;\n                    }\n                } else if (proxy.type === 'wireguard') {\n                    proxy.keepalive =\n                        proxy.keepalive ?? proxy['persistent-keepalive'];\n                    proxy['persistent-keepalive'] = proxy.keepalive;\n                    proxy['preshared-key'] =\n                        proxy['preshared-key'] ?? proxy['pre-shared-key'];\n                    proxy['pre-shared-key'] = proxy['preshared-key'];\n                } else if (proxy.type === 'snell' && proxy.version < 3) {\n                    delete proxy.udp;\n                } else if (proxy.type === 'vless') {\n                    if (isPresent(proxy, 'sni')) {\n                        proxy.servername = proxy.sni;\n                        delete proxy.sni;\n                    }\n                }\n\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'http'\n                ) {\n                    let httpPath = proxy['http-opts']?.path;\n                    if (\n                        isPresent(proxy, 'http-opts.path') &&\n                        !Array.isArray(httpPath)\n                    ) {\n                        proxy['http-opts'].path = [httpPath];\n                    }\n                    let httpHost = proxy['http-opts']?.headers?.Host;\n                    if (\n                        isPresent(proxy, 'http-opts.headers.Host') &&\n                        !Array.isArray(httpHost)\n                    ) {\n                        proxy['http-opts'].headers.Host = [httpHost];\n                    }\n                }\n                if (\n                    ['vmess', 'vless'].includes(proxy.type) &&\n                    proxy.network === 'h2'\n                ) {\n                    let path = proxy['h2-opts']?.path;\n                    if (\n                        isPresent(proxy, 'h2-opts.path') &&\n                        Array.isArray(path)\n                    ) {\n                        proxy['h2-opts'].path = path[0];\n                    }\n                    let host = proxy['h2-opts']?.headers?.host;\n                    if (\n                        isPresent(proxy, 'h2-opts.headers.Host') &&\n                        !Array.isArray(host)\n                    ) {\n                        proxy['h2-opts'].headers.host = [host];\n                    }\n                }\n                if (['ws'].includes(proxy.network)) {\n                    const networkPath = proxy[`${proxy.network}-opts`]?.path;\n                    if (networkPath) {\n                        const reg = /^(.*?)(?:\\?ed=(\\d+))?$/;\n                        // eslint-disable-next-line no-unused-vars\n                        const [_, path = '', ed = ''] = reg.exec(networkPath);\n                        proxy[`${proxy.network}-opts`].path = path;\n                        if (ed !== '') {\n                            proxy['ws-opts']['early-data-header-name'] =\n                                'Sec-WebSocket-Protocol';\n                            proxy['ws-opts']['max-early-data'] = parseInt(\n                                ed,\n                                10,\n                            );\n                        }\n                    } else {\n                        proxy[`${proxy.network}-opts`] =\n                            proxy[`${proxy.network}-opts`] || {};\n                        proxy[`${proxy.network}-opts`].path = '/';\n                    }\n                }\n                if (proxy['plugin-opts']?.tls) {\n                    if (isPresent(proxy, 'skip-cert-verify')) {\n                        proxy['plugin-opts']['skip-cert-verify'] =\n                            proxy['skip-cert-verify'];\n                    }\n                }\n                if (\n                    [\n                        'trojan',\n                        'tuic',\n                        'hysteria',\n                        'hysteria2',\n                        'juicity',\n                        'anytls',\n                        'trusttunnel',\n                        'naive',\n                    ].includes(proxy.type)\n                ) {\n                    delete proxy.tls;\n                }\n                if (proxy['tls-fingerprint']) {\n                    proxy['server-cert-fingerprint'] = proxy['tls-fingerprint'];\n                }\n                delete proxy['tls-fingerprint'];\n\n                if (proxy['underlying-proxy']) {\n                    proxy['dialer-proxy'] = proxy['underlying-proxy'];\n                }\n                delete proxy['underlying-proxy'];\n\n                if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {\n                    delete proxy.tls;\n                }\n\n                if (proxy['test-url']) {\n                    proxy['benchmark-url'] = proxy['test-url'];\n                    delete proxy['test-url'];\n                }\n                if (proxy['test-timeout']) {\n                    proxy['benchmark-timeout'] = proxy['test-timeout'];\n                    delete proxy['test-timeout'];\n                }\n\n                delete proxy.subName;\n                delete proxy.collectionName;\n                delete proxy.id;\n                delete proxy.resolved;\n                delete proxy['no-resolve'];\n                if (type !== 'internal') {\n                    for (const key in proxy) {\n                        if (proxy[key] == null || /^_/i.test(key)) {\n                            delete proxy[key];\n                        }\n                    }\n                }\n                if (\n                    ['grpc'].includes(proxy.network) &&\n                    proxy[`${proxy.network}-opts`]\n                ) {\n                    delete proxy[`${proxy.network}-opts`]['_grpc-type'];\n                    delete proxy[`${proxy.network}-opts`]['_grpc-authority'];\n                }\n                return proxy;\n            });\n        return type === 'internal'\n            ? list\n            : 'proxies:\\n' +\n                  list\n                      .map((proxy) => '  - ' + JSON.stringify(proxy) + '\\n')\n                      .join('');\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/surfboard.js",
    "content": "import { Result, isPresent } from './utils';\nimport { isNotBlank } from '@/utils';\n// import $ from '@/core/app';\n\nconst targetPlatform = 'Surfboard';\n\nexport default function Surfboard_Producer() {\n    const produce = (proxy) => {\n        if (\n            ['ws'].includes(proxy.network) &&\n            proxy['ws-opts']?.['v2ray-http-upgrade']\n        ) {\n            throw new Error(\n                `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,\n            );\n        }\n        proxy.name = proxy.name.replace(/=|,/g, '');\n        switch (proxy.type) {\n            case 'ss':\n                return shadowsocks(proxy);\n            case 'trojan':\n                return trojan(proxy);\n            case 'vmess':\n                return vmess(proxy);\n            case 'http':\n                return http(proxy);\n            case 'snell':\n                return snell(proxy);\n            case 'socks5':\n                return socks5(proxy);\n            case 'anytls':\n                return anytls(proxy);\n            case 'wireguard-surge':\n                return wireguard(proxy);\n        }\n        throw new Error(\n            `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,\n        );\n    };\n    return { produce };\n}\nfunction anytls(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // reuse\n    result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');\n\n    return result.toString();\n}\nfunction snell(proxy) {\n    if (proxy.version > 3) {\n        throw new Error(\n            `Platform ${targetPlatform} does not support snell version ${proxy.version}`,\n        );\n    }\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,version=${proxy.version}`, 'version');\n    result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');\n\n    // obfs\n    result.appendIfPresent(\n        `,obfs=${proxy['obfs-opts']?.mode}`,\n        'obfs-opts.mode',\n    );\n    result.appendIfPresent(\n        `,obfs-host=${proxy['obfs-opts']?.host}`,\n        'obfs-opts.host',\n    );\n    result.appendIfPresent(\n        `,obfs-uri=${proxy['obfs-opts']?.path}`,\n        'obfs-opts.path',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    if (proxy.version >= 3) {\n        result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n    }\n\n    return result.toString();\n}\nfunction shadowsocks(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    if (\n        ![\n            'aes-128-gcm',\n            'aes-192-gcm',\n            'aes-256-gcm',\n            'chacha20-ietf-poly1305',\n            'xchacha20-ietf-poly1305',\n            'rc4',\n            'rc4-md5',\n            'aes-128-cfb',\n            'aes-192-cfb',\n            'aes-256-cfb',\n            'aes-128-ctr',\n            'aes-192-ctr',\n            'aes-256-ctr',\n            'bf-cfb',\n            'camellia-128-cfb',\n            'camellia-192-cfb',\n            'camellia-256-cfb',\n            'salsa20',\n            'chacha20',\n            'chacha20-ietf',\n            '2022-blake3-aes-128-gcm',\n            '2022-blake3-aes-256-gcm',\n        ].includes(proxy.cipher)\n    ) {\n        throw new Error(`cipher ${proxy.cipher} is not supported`);\n    }\n    result.append(`,encrypt-method=${proxy.cipher}`);\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    // obfs\n    if (isPresent(proxy, 'plugin')) {\n        if (proxy.plugin === 'obfs') {\n            result.append(`,obfs=${proxy['plugin-opts'].mode}`);\n            result.appendIfPresent(\n                `,obfs-host=${proxy['plugin-opts'].host}`,\n                'plugin-opts.host',\n            );\n            result.appendIfPresent(\n                `,obfs-uri=${proxy['plugin-opts'].path}`,\n                'plugin-opts.path',\n            );\n        } else {\n            throw new Error(`plugin ${proxy.plugin} is not supported`);\n        }\n    }\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    return result.toString();\n}\n\nfunction trojan(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,password=${proxy.password}`, 'password');\n\n    // transport\n    handleTransport(result, proxy);\n\n    // tls\n    result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    return result.toString();\n}\n\nfunction vmess(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');\n\n    // transport\n    handleTransport(result, proxy);\n\n    // AEAD\n    if (isPresent(proxy, 'aead')) {\n        result.append(`,vmess-aead=${proxy.aead}`);\n    } else {\n        result.append(`,vmess-aead=${proxy.alterId === 0}`);\n    }\n\n    // tls\n    result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    return result.toString();\n}\n\nfunction http(proxy) {\n    const result = new Result(proxy);\n    const type = proxy.tls ? 'https' : 'http';\n    result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,${proxy.username}`, 'username');\n    result.appendIfPresent(`,${proxy.password}`, 'password');\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    return result.toString();\n}\n\nfunction socks5(proxy) {\n    const result = new Result(proxy);\n    const type = proxy.tls ? 'socks5-tls' : 'socks5';\n    result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,${proxy.username}`, 'username');\n    result.appendIfPresent(`,${proxy.password}`, 'password');\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    return result.toString();\n}\n\nfunction wireguard(proxy) {\n    const result = new Result(proxy);\n\n    result.append(`${proxy.name}=wireguard`);\n\n    result.appendIfPresent(\n        `,section-name=${proxy['section-name']}`,\n        'section-name',\n    );\n\n    return result.toString();\n}\n\nfunction handleTransport(result, proxy) {\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            result.append(`,ws=true`);\n            if (isPresent(proxy, 'ws-opts')) {\n                result.appendIfPresent(\n                    `,ws-path=${proxy['ws-opts'].path}`,\n                    'ws-opts.path',\n                );\n                if (isPresent(proxy, 'ws-opts.headers')) {\n                    const headers = proxy['ws-opts'].headers;\n                    const value = Object.keys(headers)\n                        .map((k) => {\n                            let v = headers[k];\n                            if (['Host'].includes(k)) {\n                                v = `\"${v}\"`;\n                            }\n                            return `${k}:${v}`;\n                        })\n                        .join('|');\n                    if (isNotBlank(value)) {\n                        result.append(`,ws-headers=${value}`);\n                    }\n                }\n            }\n        } else if (['tcp'].includes(proxy.network) && proxy['reality-opts']) {\n            throw new Error(`reality is unsupported`);\n        } else if (!['tcp'].includes(proxy.network)) {\n            throw new Error(`network ${proxy.network} is unsupported`);\n        }\n    }\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/surge.js",
    "content": "import { Result, isPresent } from './utils';\nimport { isNotBlank, getIfNotBlank } from '@/utils';\nimport $ from '@/core/app';\n\nconst targetPlatform = 'Surge';\n\nconst ipVersions = {\n    dual: 'dual',\n    ipv4: 'v4-only',\n    ipv6: 'v6-only',\n    'ipv4-prefer': 'prefer-v4',\n    'ipv6-prefer': 'prefer-v6',\n};\n\nexport default function Surge_Producer() {\n    const produce = (proxy, type, opts = {}) => {\n        if (\n            ['ws'].includes(proxy.network) &&\n            proxy['ws-opts']?.['v2ray-http-upgrade']\n        ) {\n            throw new Error(\n                `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,\n            );\n        }\n        proxy.name = proxy.name.replace(/=|,/g, '');\n        if (proxy.ports) {\n            proxy.ports = String(proxy.ports);\n        }\n        switch (proxy.type) {\n            case 'ss':\n                return shadowsocks(proxy);\n            case 'trojan':\n                return trojan(proxy);\n            case 'vmess':\n                return vmess(proxy, opts['include-unsupported-proxy']);\n            case 'http':\n                return http(proxy);\n            case 'direct':\n                return direct(proxy);\n            case 'socks5':\n                return socks5(proxy);\n            case 'snell':\n                return snell(proxy);\n            case 'tuic':\n                return tuic(proxy);\n            case 'wireguard-surge':\n                return wireguard_surge(proxy);\n            case 'hysteria2':\n                return hysteria2(proxy, opts['include-unsupported-proxy']);\n            case 'ssh':\n                return ssh(proxy);\n        }\n\n        if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {\n            return wireguard(proxy);\n        }\n        if (opts['include-unsupported-proxy'] && proxy.type === 'anytls') {\n            if (\n                proxy.network &&\n                (!['tcp'].includes(proxy.network) ||\n                    (['tcp'].includes(proxy.network) && proxy['reality-opts']))\n            ) {\n                throw new Error(\n                    `Platform ${targetPlatform} does not support proxy type ${proxy.type} with network or reality`,\n                );\n            }\n\n            return anytls(proxy);\n        }\n        if (opts['include-unsupported-proxy'] && proxy.type === 'trusttunnel') {\n            return trusttunnel(proxy);\n        }\n        throw new Error(\n            `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,\n        );\n    };\n    return { produce };\n}\n\nfunction shadowsocks(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    if (!proxy.cipher) {\n        proxy.cipher = 'none';\n    }\n    if (\n        ![\n            'aes-128-gcm',\n            'aes-192-gcm',\n            'aes-256-gcm',\n            'chacha20-ietf-poly1305',\n            'xchacha20-ietf-poly1305',\n            'rc4',\n            'rc4-md5',\n            'aes-128-cfb',\n            'aes-192-cfb',\n            'aes-256-cfb',\n            'aes-128-ctr',\n            'aes-192-ctr',\n            'aes-256-ctr',\n            'bf-cfb',\n            'camellia-128-cfb',\n            'camellia-192-cfb',\n            'camellia-256-cfb',\n            'cast5-cfb',\n            'des-cfb',\n            'idea-cfb',\n            'rc2-cfb',\n            'seed-cfb',\n            'salsa20',\n            'chacha20',\n            'chacha20-ietf',\n            'none',\n            '2022-blake3-aes-128-gcm',\n            '2022-blake3-aes-256-gcm',\n        ].includes(proxy.cipher)\n    ) {\n        throw new Error(`cipher ${proxy.cipher} is not supported`);\n    }\n    result.append(`,encrypt-method=${proxy.cipher}`);\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // obfs\n    if (isPresent(proxy, 'plugin')) {\n        if (proxy.plugin === 'obfs') {\n            result.append(`,obfs=${proxy['plugin-opts'].mode}`);\n            result.appendIfPresent(\n                `,obfs-host=${proxy['plugin-opts'].host}`,\n                'plugin-opts.host',\n            );\n            result.appendIfPresent(\n                `,obfs-uri=${proxy['plugin-opts'].path}`,\n                'plugin-opts.path',\n            );\n        } else if (!['shadow-tls'].includes(proxy.plugin)) {\n            throw new Error(`plugin ${proxy.plugin} is not supported`);\n        }\n    }\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n        // udp-port\n        result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');\n    } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {\n        const password = proxy['plugin-opts'].password;\n        const host = proxy['plugin-opts'].host;\n        const version = proxy['plugin-opts'].version;\n        if (password) {\n            result.append(`,shadow-tls-password=${password}`);\n            if (host) {\n                result.append(`,shadow-tls-sni=${host}`);\n            }\n            if (version) {\n                if (version < 2) {\n                    throw new Error(\n                        `shadow-tls version ${version} is not supported`,\n                    );\n                }\n                result.append(`,shadow-tls-version=${version}`);\n            }\n            // udp-port\n            result.appendIfPresent(\n                `,udp-port=${proxy['udp-port']}`,\n                'udp-port',\n            );\n        }\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\n\nfunction trojan(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // transport\n    handleTransport(result, proxy);\n\n    // tls\n    result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\n\nfunction anytls(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    // reuse\n    result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');\n\n    return result.toString();\n}\nfunction trusttunnel(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=trust-tunnel,${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,username=\"${proxy.username}\"`, 'username');\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    // reuse\n    result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');\n\n    return result.toString();\n}\n\nfunction vmess(proxy, includeUnsupportedProxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // transport\n    handleTransport(result, proxy, includeUnsupportedProxy);\n\n    // AEAD\n    if (isPresent(proxy, 'aead')) {\n        result.append(`,vmess-aead=${proxy.aead}`);\n    } else {\n        result.append(`,vmess-aead=${proxy.alterId === 0}`);\n    }\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tls\n    result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\n\nfunction ssh(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,username=\"${proxy.username}\"`, 'username');\n    // 所有的类似的字段都有双引号的问题 暂不处理\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    // https://manual.nssurge.com/policy/ssh.html\n    // 需配合 Keystore\n    result.appendIfPresent(\n        `,private-key=${proxy['keystore-private-key']}`,\n        'keystore-private-key',\n    );\n    result.appendIfPresent(\n        `,idle-timeout=${proxy['idle-timeout']}`,\n        'idle-timeout',\n    );\n    result.appendIfPresent(\n        `,server-fingerprint=\"${proxy['server-fingerprint']}\"`,\n        'server-fingerprint',\n    );\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\nfunction http(proxy) {\n    if (proxy.headers && Object.keys(proxy.headers).length > 0) {\n        throw new Error(`headers is unsupported`);\n    }\n    const result = new Result(proxy);\n    const type = proxy.tls ? 'https' : 'http';\n    result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,username=\"${proxy.username}\"`, 'username');\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\nfunction direct(proxy) {\n    const result = new Result(proxy);\n    const type = 'direct';\n    result.append(`${proxy.name}=${type}`);\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\n\nfunction socks5(proxy) {\n    const result = new Result(proxy);\n    const type = proxy.tls ? 'socks5-tls' : 'socks5';\n    result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,username=\"${proxy.username}\"`, 'username');\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tfo\n    if (proxy.tfo) {\n        $.info(`Option tfo is not supported by Surge, thus omitted`);\n    }\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\n\nfunction snell(proxy) {\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);\n    result.appendIfPresent(`,version=${proxy.version}`, 'version');\n    result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // obfs\n    result.appendIfPresent(\n        `,obfs=${proxy['obfs-opts']?.mode}`,\n        'obfs-opts.mode',\n    );\n    result.appendIfPresent(\n        `,obfs-host=${proxy['obfs-opts']?.host}`,\n        'obfs-opts.host',\n    );\n    result.appendIfPresent(\n        `,obfs-uri=${proxy['obfs-opts']?.path}`,\n        'obfs-opts.path',\n    );\n\n    // tfo\n    result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    // reuse\n    result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');\n\n    return result.toString();\n}\n\nfunction tuic(proxy) {\n    const result = new Result(proxy);\n    // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197\n    let type = proxy.type;\n    if (!proxy.token || proxy.token.length === 0) {\n        type = 'tuic-v5';\n    }\n    result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);\n\n    result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n    result.appendIfPresent(`,token=${proxy.token}`, 'token');\n\n    result.appendIfPresent(\n        `,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,\n        'alpn',\n    );\n\n    if (isPresent(proxy, 'ports')) {\n        result.append(`,port-hopping=\"${proxy.ports.replace(/,/g, ';')}\"`);\n    }\n\n    result.appendIfPresent(\n        `,port-hopping-interval=${proxy['hop-interval']}`,\n        'hop-interval',\n    );\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n\n    // tls fingerprint\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tfo\n    if (isPresent(proxy, 'tfo')) {\n        result.append(`,tfo=${proxy['tfo']}`);\n    } else if (isPresent(proxy, 'fast-open')) {\n        result.append(`,tfo=${proxy['fast-open']}`);\n    }\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');\n\n    return result.toString();\n}\n\nfunction wireguard(proxy) {\n    if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {\n        proxy.server = proxy.peers[0].server;\n        proxy.port = proxy.peers[0].port;\n        proxy.ip = proxy.peers[0].ip;\n        proxy.ipv6 = proxy.peers[0].ipv6;\n        proxy['public-key'] = proxy.peers[0]['public-key'];\n        proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];\n        // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717\n        proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];\n        proxy.reserved = proxy.peers[0].reserved;\n    }\n    const result = new Result(proxy);\n\n    result.append(`# > WireGuard Proxy ${proxy.name}\n# ${proxy.name}=wireguard`);\n\n    proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);\n\n    result.appendIfPresent(\n        `,section-name=${proxy['section-name']}`,\n        'section-name',\n    );\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    result.append(`\n# > WireGuard Section ${proxy.name}\n[WireGuard ${proxy['section-name']}]\nprivate-key = ${proxy['private-key']}`);\n\n    result.appendIfPresent(`\\nself-ip = ${proxy.ip}`, 'ip');\n    result.appendIfPresent(`\\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6');\n    if (proxy.dns) {\n        if (Array.isArray(proxy.dns)) {\n            proxy.dns = proxy.dns.join(', ');\n        }\n        result.append(`\\ndns-server = ${proxy.dns}`);\n    }\n    result.appendIfPresent(`\\nmtu = ${proxy.mtu}`, 'mtu');\n\n    if (ip_version === 'prefer-v6') {\n        result.append(`\\nprefer-ipv6 = true`);\n    }\n    const allowedIps = Array.isArray(proxy['allowed-ips'])\n        ? proxy['allowed-ips'].join(',')\n        : proxy['allowed-ips'];\n    let reserved = Array.isArray(proxy.reserved)\n        ? proxy.reserved.join('/')\n        : proxy.reserved;\n    let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];\n\n    const peer = {\n        'public-key': proxy['public-key'],\n        'allowed-ips': allowedIps ? `\"${allowedIps}\"` : undefined,\n        endpoint: `${proxy.server}:${proxy.port}`,\n        keepalive: proxy['persistent-keepalive'] || proxy.keepalive,\n        'client-id': reserved,\n        'preshared-key': presharedKey,\n    };\n    result.append(\n        `\\npeer = (${Object.keys(peer)\n            .filter((k) => peer[k] != null)\n            .map((k) => `${k} = ${peer[k]}`)\n            .join(', ')})`,\n    );\n    return result.toString();\n}\nfunction wireguard_surge(proxy) {\n    const result = new Result(proxy);\n\n    result.append(`${proxy.name}=wireguard`);\n\n    result.appendIfPresent(\n        `,section-name=${proxy['section-name']}`,\n        'section-name',\n    );\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    return result.toString();\n}\n\nfunction hysteria2(proxy, includeUnsupportedProxy) {\n    if (includeUnsupportedProxy) {\n        if (proxy['obfs-password'] && proxy.obfs != 'salamander') {\n            throw new Error(`only salamander obfs is supported`);\n        }\n    } else {\n        if (proxy.obfs || proxy['obfs-password']) {\n            throw new Error(`obfs is unsupported`);\n        }\n    }\n\n    const result = new Result(proxy);\n    result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);\n\n    result.appendIfPresent(`,password=\"${proxy.password}\"`, 'password');\n\n    if (isPresent(proxy, 'ports')) {\n        result.append(`,port-hopping=\"${proxy.ports.replace(/,/g, ';')}\"`);\n    }\n\n    result.appendIfPresent(\n        `,port-hopping-interval=${proxy['hop-interval']}`,\n        'hop-interval',\n    );\n\n    if (proxy['obfs-password'] && proxy.obfs == 'salamander') {\n        result.append(`,salamander-password=\"${proxy['obfs-password']}\"`);\n    }\n\n    const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];\n    result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // tls verification\n    result.appendIfPresent(`,sni=\"${proxy.sni}\"`, 'sni');\n    result.appendIfPresent(\n        `,skip-cert-verify=${proxy['skip-cert-verify']}`,\n        'skip-cert-verify',\n    );\n    result.appendIfPresent(\n        `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,\n        'tls-fingerprint',\n    );\n\n    // tfo\n    if (isPresent(proxy, 'tfo')) {\n        result.append(`,tfo=${proxy['tfo']}`);\n    } else if (isPresent(proxy, 'fast-open')) {\n        result.append(`,tfo=${proxy['fast-open']}`);\n    }\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n    result.appendIfPresent(\n        `,test-timeout=${proxy['test-timeout']}`,\n        'test-timeout',\n    );\n    result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');\n    result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');\n    result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');\n    result.appendIfPresent(\n        `,allow-other-interface=${proxy['allow-other-interface']}`,\n        'allow-other-interface',\n    );\n    result.appendIfPresent(\n        `,interface=${proxy['interface-name']}`,\n        'interface-name',\n    );\n    result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');\n\n    // shadow-tls\n    if (isPresent(proxy, 'shadow-tls-password')) {\n        result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);\n\n        result.appendIfPresent(\n            `,shadow-tls-version=${proxy['shadow-tls-version']}`,\n            'shadow-tls-version',\n        );\n        result.appendIfPresent(\n            `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,\n            'shadow-tls-sni',\n        );\n    }\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    // underlying-proxy\n    result.appendIfPresent(\n        `,underlying-proxy=${proxy['underlying-proxy']}`,\n        'underlying-proxy',\n    );\n\n    // download-bandwidth\n    result.appendIfPresent(\n        `,download-bandwidth=${`${proxy['down']}`.match(/\\d+/)?.[0] || 0}`,\n        'down',\n    );\n\n    result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');\n\n    return result.toString();\n}\n\nfunction handleTransport(result, proxy, includeUnsupportedProxy) {\n    if (isPresent(proxy, 'network')) {\n        if (proxy.network === 'ws') {\n            result.append(`,ws=true`);\n            if (isPresent(proxy, 'ws-opts')) {\n                result.appendIfPresent(\n                    `,ws-path=${proxy['ws-opts'].path}`,\n                    'ws-opts.path',\n                );\n                if (isPresent(proxy, 'ws-opts.headers')) {\n                    const headers = proxy['ws-opts'].headers;\n                    const value = Object.keys(headers)\n                        .map((k) => {\n                            let v = headers[k];\n                            // if (['Host'].includes(k)) {\n                            v = `\"${v}\"`;\n                            // }\n                            return `${k}:${v}`;\n                        })\n                        .join('|');\n                    if (isNotBlank(value)) {\n                        result.append(`,ws-headers=${value}`);\n                    }\n                }\n            }\n        } else {\n            if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {\n                $.info(\n                    `Include Unsupported Proxy: network ${proxy.network} -> tcp`,\n                );\n            } else if (\n                ['tcp'].includes(proxy.network) &&\n                proxy['reality-opts']\n            ) {\n                throw new Error(`reality is unsupported`);\n            } else if (!['tcp'].includes(proxy.network)) {\n                throw new Error(`network ${proxy.network} is unsupported`);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/surgemac.js",
    "content": "import { Base64 } from 'js-base64';\nimport { Result, isPresent } from './utils';\nimport Surge_Producer from './surge';\nimport ClashMeta_Producer from './clashmeta';\nimport { isIPv4, isIPv6 } from '@/utils';\nimport $ from '@/core/app';\n\nconst targetPlatform = 'SurgeMac';\n\nconst surge_Producer = Surge_Producer();\n\nexport default function SurgeMac_Producer() {\n    const produce = (proxy, type, opts = {}) => {\n        switch (proxy.type) {\n            case 'external':\n                return external(proxy);\n            // case 'ssr':\n            //     return shadowsocksr(proxy);\n            default: {\n                try {\n                    return surge_Producer.produce(proxy, type, opts);\n                } catch (e) {\n                    if (opts.useMihomoExternal) {\n                        $.log(\n                            `${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,\n                        );\n                        return mihomo(proxy, type, opts);\n                    } else {\n                        throw new Error(\n                            `Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`,\n                        );\n                    }\n                }\n            }\n        }\n    };\n    return { produce };\n}\nfunction external(proxy) {\n    const result = new Result(proxy);\n    if (!proxy.exec || !proxy['local-port']) {\n        throw new Error(`${proxy.type}: exec and local-port are required`);\n    }\n    result.append(\n        `${proxy.name}=external,exec=\"${proxy.exec}\",local-port=${proxy['local-port']}`,\n    );\n\n    if (Array.isArray(proxy.args)) {\n        proxy.args.map((args) => {\n            result.append(`,args=\"${args}\"`);\n        });\n    }\n    if (Array.isArray(proxy.addresses)) {\n        proxy.addresses.map((addresses) => {\n            result.append(`,addresses=${addresses}`);\n        });\n    }\n\n    result.appendIfPresent(\n        `,no-error-alert=${proxy['no-error-alert']}`,\n        'no-error-alert',\n    );\n\n    // udp\n    result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');\n\n    // tfo\n    if (isPresent(proxy, 'tfo')) {\n        result.append(`,tfo=${proxy['tfo']}`);\n    } else if (isPresent(proxy, 'fast-open')) {\n        result.append(`,tfo=${proxy['fast-open']}`);\n    }\n\n    // test-url\n    result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');\n\n    // block-quic\n    result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');\n\n    return result.toString();\n}\n// eslint-disable-next-line no-unused-vars\nfunction shadowsocksr(proxy) {\n    const external_proxy = {\n        ...proxy,\n        type: 'external',\n        exec: proxy.exec || '/usr/local/bin/ssr-local',\n        'local-port': '__SubStoreLocalPort__',\n        args: [],\n        addresses: [],\n        'local-address':\n            proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1',\n    };\n\n    // https://manual.nssurge.com/policy/external-proxy.html\n    if (isIP(proxy.server)) {\n        external_proxy.addresses.push(proxy.server);\n    } else {\n        $.log(\n            `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,\n        );\n    }\n\n    for (const [key, value] of Object.entries({\n        cipher: '-m',\n        obfs: '-o',\n        'obfs-param': '-g',\n        password: '-k',\n        port: '-p',\n        protocol: '-O',\n        'protocol-param': '-G',\n        server: '-s',\n        'local-port': '-l',\n        'local-address': '-b',\n    })) {\n        if (external_proxy[key] != null) {\n            external_proxy.args.push(value);\n            external_proxy.args.push(external_proxy[key]);\n        }\n    }\n\n    return external(external_proxy);\n}\n// eslint-disable-next-line no-unused-vars\nfunction mihomo(proxy, type, opts) {\n    const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];\n    if (clashProxy) {\n        const localPort = opts?.localPort || proxy._localPort || 65535;\n        const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])\n            ? false\n            : true;\n        const external_proxy = {\n            name: proxy.name,\n            type: 'external',\n            udp: true,\n            exec: proxy._exec || '/usr/local/bin/mihomo',\n            'local-port': localPort,\n            args: [\n                '-config',\n                Base64.encode(\n                    JSON.stringify({\n                        'mixed-port': localPort,\n                        ipv6,\n                        mode: 'global',\n                        dns: {\n                            enable: true,\n                            ipv6,\n                            'default-nameserver': opts?.defaultNameserver ||\n                                proxy._defaultNameserver || [\n                                    '180.76.76.76',\n                                    '52.80.52.52',\n                                    '119.28.28.28',\n                                    '223.6.6.6',\n                                ],\n                            nameserver: opts?.nameserver ||\n                                proxy._nameserver || [\n                                    'https://doh.pub/dns-query',\n                                    'https://dns.alidns.com/dns-query',\n                                    'https://doh-pure.onedns.net/dns-query',\n                                ],\n                        },\n                        proxies: [\n                            {\n                                ...clashProxy,\n                                name: 'proxy',\n                            },\n                        ],\n                        'proxy-groups': [\n                            {\n                                name: 'GLOBAL',\n                                type: 'select',\n                                proxies: ['proxy'],\n                            },\n                        ],\n                    }),\n                ),\n            ],\n            addresses: [],\n        };\n\n        // https://manual.nssurge.com/policy/external-proxy.html\n        if (isIP(proxy.server)) {\n            external_proxy.addresses.push(proxy.server);\n        } else {\n            $.log(\n                `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,\n            );\n        }\n        opts.localPort = localPort - 1;\n        return external(external_proxy);\n    }\n}\n\nfunction isIP(ip) {\n    return isIPv4(ip) || isIPv6(ip);\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/uri.js",
    "content": "/* eslint-disable no-case-declarations */\nimport { Base64 } from 'js-base64';\nimport { isIPv6 } from '@/utils';\n\nfunction vless(proxy) {\n    let security = 'none';\n    const isReality = proxy['reality-opts'];\n    let sid = '';\n    let pbk = '';\n    let spx = '';\n    if (isReality) {\n        security = 'reality';\n        const publicKey = proxy['reality-opts']?.['public-key'];\n        if (publicKey) {\n            pbk = `&pbk=${encodeURIComponent(publicKey)}`;\n        }\n        const shortId = proxy['reality-opts']?.['short-id'];\n        if (shortId) {\n            sid = `&sid=${encodeURIComponent(shortId)}`;\n        }\n        const spiderX = proxy['reality-opts']?.['_spider-x'];\n        if (spiderX) {\n            spx = `&spx=${encodeURIComponent(spiderX)}`;\n        }\n    } else if (proxy.tls) {\n        security = 'tls';\n    }\n    let alpn = '';\n    if (proxy.alpn) {\n        alpn = `&alpn=${encodeURIComponent(\n            Array.isArray(proxy.alpn) ? proxy.alpn : proxy.alpn.join(','),\n        )}`;\n    }\n    let allowInsecure = '';\n    if (proxy['skip-cert-verify']) {\n        allowInsecure = `&allowInsecure=1`;\n    }\n    let h2 = '';\n    if (proxy._h2) {\n        h2 = `&h2=1`;\n    }\n    let pcs = '';\n    if (proxy._pcs) {\n        pcs = `&pcs=${encodeURIComponent(proxy._pcs)}`;\n    }\n    let ech = '';\n    if (proxy._echConfigList) {\n        ech = `&ech=${encodeURIComponent(proxy._echConfigList)}`;\n    }\n    let sni = '';\n    if (proxy.sni) {\n        sni = `&sni=${encodeURIComponent(proxy.sni)}`;\n    }\n    let fp = '';\n    if (proxy['client-fingerprint']) {\n        fp = `&fp=${encodeURIComponent(proxy['client-fingerprint'])}`;\n    }\n    let flow = '';\n    if (proxy.flow) {\n        flow = `&flow=${encodeURIComponent(proxy.flow)}`;\n    }\n    let extra = '';\n    if (proxy._extra) {\n        extra = `&extra=${encodeURIComponent(proxy._extra)}`;\n    }\n    let mode = '';\n    if (proxy._mode) {\n        mode = `&mode=${encodeURIComponent(proxy._mode)}`;\n    }\n    let pqv = '';\n    if (proxy._pqv) {\n        pqv = `&pqv=${encodeURIComponent(proxy._pqv)}`;\n    }\n    let encryption = '';\n    if (proxy.encryption) {\n        encryption = `&encryption=${encodeURIComponent(proxy.encryption)}`;\n    }\n    let vlessType = proxy.network;\n    if (proxy.network === 'ws' && proxy['ws-opts']?.['v2ray-http-upgrade']) {\n        vlessType = 'httpupgrade';\n    }\n\n    let vlessTransport = `&type=${encodeURIComponent(vlessType)}`;\n    if (['grpc'].includes(proxy.network)) {\n        // https://github.com/XTLS/Xray-core/issues/91\n        vlessTransport += `&mode=${encodeURIComponent(\n            proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',\n        )}`;\n        const authority = proxy[`${proxy.network}-opts`]?.['_grpc-authority'];\n        if (authority) {\n            vlessTransport += `&authority=${encodeURIComponent(authority)}`;\n        }\n    }\n\n    let vlessTransportServiceName =\n        proxy[`${proxy.network}-opts`]?.[`${proxy.network}-service-name`];\n    let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;\n    let vlessTransportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;\n    if (vlessTransportPath) {\n        vlessTransport += `&path=${encodeURIComponent(\n            Array.isArray(vlessTransportPath)\n                ? vlessTransportPath[0]\n                : vlessTransportPath,\n        )}`;\n    }\n    if (vlessTransportHost) {\n        vlessTransport += `&host=${encodeURIComponent(\n            Array.isArray(vlessTransportHost)\n                ? vlessTransportHost[0]\n                : vlessTransportHost,\n        )}`;\n    }\n    if (vlessTransportServiceName) {\n        vlessTransport += `&serviceName=${encodeURIComponent(\n            vlessTransportServiceName,\n        )}`;\n    }\n    if (proxy.network === 'kcp') {\n        if (proxy.seed) {\n            vlessTransport += `&seed=${encodeURIComponent(proxy.seed)}`;\n        }\n        if (proxy.headerType) {\n            vlessTransport += `&headerType=${encodeURIComponent(\n                proxy.headerType,\n            )}`;\n        }\n    }\n\n    return `vless://${proxy.uuid}@${proxy.server}:${\n        proxy.port\n    }?security=${encodeURIComponent(\n        security,\n    )}${vlessTransport}${alpn}${allowInsecure}${pcs}${ech}${h2}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}${pqv}${encryption}#${encodeURIComponent(\n        proxy.name,\n    )}`;\n}\n\nexport default function URI_Producer() {\n    const type = 'SINGLE';\n    const produce = (proxy) => {\n        let result = '';\n        delete proxy.subName;\n        delete proxy.collectionName;\n        delete proxy.id;\n        delete proxy.resolved;\n        delete proxy['no-resolve'];\n        for (const key in proxy) {\n            if (proxy[key] == null) {\n                delete proxy[key];\n            }\n        }\n        if (\n            [\n                'tuic',\n                'hysteria',\n                'hysteria2',\n                'juicity',\n                'trusttunnel',\n            ].includes(proxy.type)\n        ) {\n            delete proxy.tls;\n        }\n        if (\n            !['vmess'].includes(proxy.type) &&\n            proxy.server &&\n            isIPv6(proxy.server)\n        ) {\n            proxy.server = `[${proxy.server}]`;\n        }\n        switch (proxy.type) {\n            case 'socks5':\n                result = `socks://${encodeURIComponent(\n                    Base64.encode(\n                        `${proxy.username ?? ''}:${proxy.password ?? ''}`,\n                    ),\n                )}@${proxy.server}:${proxy.port}#${proxy.name}`;\n                break;\n            case 'ss':\n                const userinfo = `${proxy.cipher}:${proxy.password}`;\n                result = `ss://${\n                    proxy.cipher?.startsWith('2022-blake3-')\n                        ? `${encodeURIComponent(\n                              proxy.cipher,\n                          )}:${encodeURIComponent(proxy.password)}`\n                        : Base64.encode(userinfo)\n                }@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;\n                let query = '';\n                if (proxy.plugin) {\n                    query += '&plugin=';\n                    const opts = proxy['plugin-opts'];\n                    switch (proxy.plugin) {\n                        case 'obfs':\n                            query += encodeURIComponent(\n                                `simple-obfs;obfs=${opts.mode}${\n                                    opts.host ? ';obfs-host=' + opts.host : ''\n                                }`,\n                            );\n                            break;\n                        case 'v2ray-plugin':\n                            query += encodeURIComponent(\n                                `v2ray-plugin;obfs=${opts.mode}${\n                                    opts.host ? ';obfs-host=' + opts.host : ''\n                                }${opts.host ? ';host=' + opts.host : ''}${\n                                    opts.path ? ';path=' + opts.path : ''\n                                }${opts.tls ? ';tls' : ''}`,\n                            );\n                            break;\n                        case 'shadow-tls':\n                            query += encodeURIComponent(\n                                `shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,\n                            );\n                            break;\n                        default:\n                            throw new Error(\n                                `Unsupported plugin option: ${proxy.plugin}`,\n                            );\n                    }\n                }\n                if (proxy['udp-over-tcp']) {\n                    query += '&uot=1';\n                }\n                if (proxy.tfo) {\n                    query += '&tfo=1';\n                }\n                let ssTransport = '';\n                if (proxy.network) {\n                    let ssType = proxy.network;\n                    if (\n                        proxy.network === 'ws' &&\n                        proxy['ws-opts']?.['v2ray-http-upgrade']\n                    ) {\n                        ssType = 'httpupgrade';\n                    }\n                    ssTransport = `&type=${encodeURIComponent(ssType)}`;\n                    if (['grpc'].includes(proxy.network)) {\n                        let ssTransportServiceName =\n                            proxy[`${proxy.network}-opts`]?.[\n                                `${proxy.network}-service-name`\n                            ];\n                        let ssTransportAuthority =\n                            proxy[`${proxy.network}-opts`]?.['_grpc-authority'];\n                        if (ssTransportServiceName) {\n                            ssTransport += `&serviceName=${encodeURIComponent(\n                                ssTransportServiceName,\n                            )}`;\n                        }\n                        if (ssTransportAuthority) {\n                            ssTransport += `&authority=${encodeURIComponent(\n                                ssTransportAuthority,\n                            )}`;\n                        }\n                        ssTransport += `&mode=${encodeURIComponent(\n                            proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||\n                                'gun',\n                        )}`;\n                    }\n                    let ssTransportPath = proxy[`${proxy.network}-opts`]?.path;\n                    let ssTransportHost =\n                        proxy[`${proxy.network}-opts`]?.headers?.Host;\n                    if (ssTransportPath) {\n                        ssTransport += `&path=${encodeURIComponent(\n                            Array.isArray(ssTransportPath)\n                                ? ssTransportPath[0]\n                                : ssTransportPath,\n                        )}`;\n                    }\n                    if (ssTransportHost) {\n                        ssTransport += `&host=${encodeURIComponent(\n                            Array.isArray(ssTransportHost)\n                                ? ssTransportHost[0]\n                                : ssTransportHost,\n                        )}`;\n                    }\n                }\n                let ssFp = '';\n                if (proxy['client-fingerprint']) {\n                    ssFp = `&fp=${encodeURIComponent(\n                        proxy['client-fingerprint'],\n                    )}`;\n                }\n                let ssAlpn = '';\n                if (proxy.alpn) {\n                    ssAlpn = `&alpn=${encodeURIComponent(\n                        Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : proxy.alpn.join(','),\n                    )}`;\n                }\n                const ssIsReality = proxy['reality-opts'];\n                let ssSid = '';\n                let ssPbk = '';\n                let ssSpx = '';\n                let ssSecurity = proxy.tls ? '&security=tls' : '';\n                let ssMode = '';\n                let ssExtra = '';\n                if (ssIsReality) {\n                    ssSecurity = `&security=reality`;\n                    const publicKey = proxy['reality-opts']?.['public-key'];\n                    if (publicKey) {\n                        ssPbk = `&pbk=${encodeURIComponent(publicKey)}`;\n                    }\n                    const shortId = proxy['reality-opts']?.['short-id'];\n                    if (shortId) {\n                        ssSid = `&sid=${encodeURIComponent(shortId)}`;\n                    }\n                    const spiderX = proxy['reality-opts']?.['_spider-x'];\n                    if (spiderX) {\n                        ssSpx = `&spx=${encodeURIComponent(spiderX)}`;\n                    }\n                    if (proxy._extra) {\n                        ssExtra = `&extra=${encodeURIComponent(proxy._extra)}`;\n                    }\n                    if (proxy._mode) {\n                        ssMode = `&mode=${encodeURIComponent(proxy._mode)}`;\n                    }\n                }\n                if (proxy.tls) {\n                    query += `&sni=${encodeURIComponent(\n                        proxy.sni || proxy.server,\n                    )}${proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''}`;\n                }\n                query += `${ssTransport}${ssAlpn}${ssFp}${ssSecurity}${ssSid}${ssPbk}${ssSpx}${ssMode}${ssExtra}#${encodeURIComponent(\n                    proxy.name,\n                )}`;\n                result += query.replace(/^&/, '?');\n                break;\n            case 'ssr':\n                result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${\n                    proxy.cipher\n                }:${proxy.obfs}:${Base64.encode(proxy.password)}/`;\n                result += `?remarks=${Base64.encode(proxy.name)}${\n                    proxy['obfs-param']\n                        ? '&obfsparam=' + Base64.encode(proxy['obfs-param'])\n                        : ''\n                }${\n                    proxy['protocol-param']\n                        ? '&protocolparam=' +\n                          Base64.encode(proxy['protocol-param'])\n                        : ''\n                }`;\n                result = 'ssr://' + Base64.encode(result);\n                break;\n            case 'vmess':\n                // V2RayN URI format\n                let type = '';\n                let net = proxy.network || 'tcp';\n                if (proxy.network === 'http') {\n                    net = 'tcp';\n                    type = 'http';\n                } else if (\n                    proxy.network === 'ws' &&\n                    proxy['ws-opts']?.['v2ray-http-upgrade']\n                ) {\n                    net = 'httpupgrade';\n                }\n                result = {\n                    v: '2',\n                    ps: proxy.name,\n                    add: proxy.server,\n                    port: `${proxy.port}`,\n                    id: proxy.uuid,\n                    aid: `${proxy.alterId || 0}`,\n                    scy: proxy.cipher,\n                    net,\n                    type,\n                    tls: proxy.tls ? 'tls' : '',\n                    alpn: Array.isArray(proxy.alpn)\n                        ? proxy.alpn.join(',')\n                        : proxy.alpn,\n                    fp: proxy['client-fingerprint'],\n                };\n                if (proxy.tls && proxy.sni) {\n                    result.sni = proxy.sni;\n                }\n                // obfs\n                if (proxy.network) {\n                    let vmessTransportPath =\n                        proxy[`${proxy.network}-opts`]?.path;\n                    let vmessTransportHost =\n                        proxy[`${proxy.network}-opts`]?.headers?.Host;\n\n                    if (['grpc'].includes(proxy.network)) {\n                        result.path =\n                            proxy[`${proxy.network}-opts`]?.[\n                                'grpc-service-name'\n                            ];\n                        // https://github.com/XTLS/Xray-core/issues/91\n                        result.type =\n                            proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||\n                            'gun';\n                        result.host =\n                            proxy[`${proxy.network}-opts`]?.['_grpc-authority'];\n                    } else if (['kcp', 'quic'].includes(proxy.network)) {\n                        // https://github.com/XTLS/Xray-core/issues/91\n                        result.type =\n                            proxy[`${proxy.network}-opts`]?.[\n                                `_${proxy.network}-type`\n                            ] || 'none';\n                        result.host =\n                            proxy[`${proxy.network}-opts`]?.[\n                                `_${proxy.network}-host`\n                            ];\n                        result.path =\n                            proxy[`${proxy.network}-opts`]?.[\n                                `_${proxy.network}-path`\n                            ];\n                    } else {\n                        if (vmessTransportPath) {\n                            result.path = Array.isArray(vmessTransportPath)\n                                ? vmessTransportPath[0]\n                                : vmessTransportPath;\n                        }\n                        if (vmessTransportHost) {\n                            result.host = Array.isArray(vmessTransportHost)\n                                ? vmessTransportHost[0]\n                                : vmessTransportHost;\n                        }\n                    }\n                }\n                result = 'vmess://' + Base64.encode(JSON.stringify(result));\n                break;\n            case 'vless':\n                result = vless(proxy);\n                break;\n            case 'trojan':\n                let trojanTransport = '';\n                if (proxy.network) {\n                    let trojanType = proxy.network;\n                    if (\n                        proxy.network === 'ws' &&\n                        proxy['ws-opts']?.['v2ray-http-upgrade']\n                    ) {\n                        trojanType = 'httpupgrade';\n                    }\n                    trojanTransport = `&type=${encodeURIComponent(trojanType)}`;\n                    if (['grpc'].includes(proxy.network)) {\n                        let trojanTransportServiceName =\n                            proxy[`${proxy.network}-opts`]?.[\n                                `${proxy.network}-service-name`\n                            ];\n                        let trojanTransportAuthority =\n                            proxy[`${proxy.network}-opts`]?.['_grpc-authority'];\n                        if (trojanTransportServiceName) {\n                            trojanTransport += `&serviceName=${encodeURIComponent(\n                                trojanTransportServiceName,\n                            )}`;\n                        }\n                        if (trojanTransportAuthority) {\n                            trojanTransport += `&authority=${encodeURIComponent(\n                                trojanTransportAuthority,\n                            )}`;\n                        }\n                        trojanTransport += `&mode=${encodeURIComponent(\n                            proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||\n                                'gun',\n                        )}`;\n                    }\n                    let trojanTransportPath =\n                        proxy[`${proxy.network}-opts`]?.path;\n                    let trojanTransportHost =\n                        proxy[`${proxy.network}-opts`]?.headers?.Host;\n                    if (trojanTransportPath) {\n                        trojanTransport += `&path=${encodeURIComponent(\n                            Array.isArray(trojanTransportPath)\n                                ? trojanTransportPath[0]\n                                : trojanTransportPath,\n                        )}`;\n                    }\n                    if (trojanTransportHost) {\n                        trojanTransport += `&host=${encodeURIComponent(\n                            Array.isArray(trojanTransportHost)\n                                ? trojanTransportHost[0]\n                                : trojanTransportHost,\n                        )}`;\n                    }\n                }\n                let trojanFp = '';\n                if (proxy['client-fingerprint']) {\n                    trojanFp = `&fp=${encodeURIComponent(\n                        proxy['client-fingerprint'],\n                    )}`;\n                }\n                let trojanAlpn = '';\n                if (proxy.alpn) {\n                    trojanAlpn = `&alpn=${encodeURIComponent(\n                        Array.isArray(proxy.alpn)\n                            ? proxy.alpn\n                            : proxy.alpn.join(','),\n                    )}`;\n                }\n                const trojanIsReality = proxy['reality-opts'];\n                let trojanSid = '';\n                let trojanPbk = '';\n                let trojanSpx = '';\n                let trojanSecurity = '';\n                let trojanMode = '';\n                let trojanExtra = '';\n                if (trojanIsReality) {\n                    trojanSecurity = `&security=reality`;\n                    const publicKey = proxy['reality-opts']?.['public-key'];\n                    if (publicKey) {\n                        trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;\n                    }\n                    const shortId = proxy['reality-opts']?.['short-id'];\n                    if (shortId) {\n                        trojanSid = `&sid=${encodeURIComponent(shortId)}`;\n                    }\n                    const spiderX = proxy['reality-opts']?.['_spider-x'];\n                    if (spiderX) {\n                        trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;\n                    }\n                    if (proxy._extra) {\n                        trojanExtra = `&extra=${encodeURIComponent(\n                            proxy._extra,\n                        )}`;\n                    }\n                    if (proxy._mode) {\n                        trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;\n                    }\n                }\n                result = `trojan://${proxy.password}@${proxy.server}:${\n                    proxy.port\n                }?sni=${encodeURIComponent(proxy.sni || proxy.server)}${\n                    proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''\n                }${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(\n                    proxy.name,\n                )}`;\n                break;\n            case 'hysteria2':\n                let hysteria2params = [];\n                if (proxy['hop-interval']) {\n                    hysteria2params.push(\n                        `hop-interval=${proxy['hop-interval']}`,\n                    );\n                }\n                if (proxy['keepalive']) {\n                    hysteria2params.push(`keepalive=${proxy['keepalive']}`);\n                }\n                if (proxy['skip-cert-verify']) {\n                    hysteria2params.push(`insecure=1`);\n                }\n                if (proxy.obfs) {\n                    hysteria2params.push(\n                        `obfs=${encodeURIComponent(proxy.obfs)}`,\n                    );\n                    if (proxy['obfs-password']) {\n                        hysteria2params.push(\n                            `obfs-password=${encodeURIComponent(\n                                proxy['obfs-password'],\n                            )}`,\n                        );\n                    }\n                }\n                if (proxy.sni) {\n                    hysteria2params.push(\n                        `sni=${encodeURIComponent(proxy.sni)}`,\n                    );\n                }\n                if (proxy.ports) {\n                    hysteria2params.push(`mport=${proxy.ports}`);\n                }\n                if (proxy['tls-fingerprint']) {\n                    hysteria2params.push(\n                        `pinSHA256=${encodeURIComponent(\n                            proxy['tls-fingerprint'],\n                        )}`,\n                    );\n                }\n                if (proxy.tfo) {\n                    hysteria2params.push(`fastopen=1`);\n                }\n                result = `hysteria2://${encodeURIComponent(proxy.password)}@${\n                    proxy.server\n                }:${proxy.port}?${hysteria2params.join(\n                    '&',\n                )}#${encodeURIComponent(proxy.name)}`;\n                break;\n            case 'hysteria':\n                let hysteriaParams = [];\n                Object.keys(proxy).forEach((key) => {\n                    if (!['name', 'type', 'server', 'port'].includes(key)) {\n                        const i = key.replace(/-/, '_');\n                        if (['alpn'].includes(key)) {\n                            if (proxy[key]) {\n                                hysteriaParams.push(\n                                    `${i}=${encodeURIComponent(\n                                        Array.isArray(proxy[key])\n                                            ? proxy[key][0]\n                                            : proxy[key],\n                                    )}`,\n                                );\n                            }\n                        } else if (['skip-cert-verify'].includes(key)) {\n                            if (proxy[key]) {\n                                hysteriaParams.push(`insecure=1`);\n                            }\n                        } else if (['tfo', 'fast-open'].includes(key)) {\n                            if (\n                                proxy[key] &&\n                                !hysteriaParams.includes('fastopen=1')\n                            ) {\n                                hysteriaParams.push(`fastopen=1`);\n                            }\n                        } else if (['ports'].includes(key)) {\n                            hysteriaParams.push(`mport=${proxy[key]}`);\n                        } else if (['auth-str'].includes(key)) {\n                            hysteriaParams.push(`auth=${proxy[key]}`);\n                        } else if (['up'].includes(key)) {\n                            hysteriaParams.push(`upmbps=${proxy[key]}`);\n                        } else if (['down'].includes(key)) {\n                            hysteriaParams.push(`downmbps=${proxy[key]}`);\n                        } else if (['_obfs'].includes(key)) {\n                            hysteriaParams.push(`obfs=${proxy[key]}`);\n                        } else if (['obfs'].includes(key)) {\n                            hysteriaParams.push(`obfsParam=${proxy[key]}`);\n                        } else if (['sni'].includes(key)) {\n                            hysteriaParams.push(`peer=${proxy[key]}`);\n                        } else if (proxy[key] && !/^_/i.test(key)) {\n                            hysteriaParams.push(\n                                `${i}=${encodeURIComponent(proxy[key])}`,\n                            );\n                        }\n                    }\n                });\n\n                result = `hysteria://${proxy.server}:${\n                    proxy.port\n                }?${hysteriaParams.join('&')}#${encodeURIComponent(\n                    proxy.name,\n                )}`;\n                break;\n\n            case 'tuic':\n                if (!proxy.token || proxy.token.length === 0) {\n                    let tuicParams = [];\n                    Object.keys(proxy).forEach((key) => {\n                        if (\n                            ![\n                                'name',\n                                'type',\n                                'uuid',\n                                'password',\n                                'server',\n                                'port',\n                                'tls',\n                            ].includes(key)\n                        ) {\n                            const i = key.replace(/-/, '_');\n                            if (['alpn'].includes(key)) {\n                                if (proxy[key]) {\n                                    tuicParams.push(\n                                        `${i}=${encodeURIComponent(\n                                            Array.isArray(proxy[key])\n                                                ? proxy[key][0]\n                                                : proxy[key],\n                                        )}`,\n                                    );\n                                }\n                            } else if (['skip-cert-verify'].includes(key)) {\n                                if (proxy[key]) {\n                                    tuicParams.push(`allow_insecure=1`);\n                                }\n                            } else if (['tfo', 'fast-open'].includes(key)) {\n                                if (\n                                    proxy[key] &&\n                                    !tuicParams.includes('fast_open=1')\n                                ) {\n                                    tuicParams.push(`fast_open=1`);\n                                }\n                            } else if (\n                                ['disable-sni', 'reduce-rtt'].includes(key) &&\n                                proxy[key]\n                            ) {\n                                tuicParams.push(`${i.replace(/-/g, '_')}=1`);\n                            } else if (\n                                ['congestion-controller'].includes(key)\n                            ) {\n                                tuicParams.push(\n                                    `congestion_control=${proxy[key]}`,\n                                );\n                            } else if (proxy[key] && !/^_/i.test(key)) {\n                                tuicParams.push(\n                                    `${i.replace(\n                                        /-/g,\n                                        '_',\n                                    )}=${encodeURIComponent(proxy[key])}`,\n                                );\n                            }\n                        }\n                    });\n\n                    result = `tuic://${encodeURIComponent(\n                        proxy.uuid,\n                    )}:${encodeURIComponent(proxy.password)}@${proxy.server}:${\n                        proxy.port\n                    }?${tuicParams.join('&')}#${encodeURIComponent(\n                        proxy.name,\n                    )}`;\n                }\n                break;\n            case 'anytls':\n                result = vless({\n                    ...proxy,\n                    uuid: proxy.password,\n                    network: proxy.network || 'tcp',\n                }).replace('vless', 'anytls');\n                // 偷个懒\n                let anytlsParams = [];\n                Object.keys(proxy).forEach((key) => {\n                    if (\n                        ![\n                            'name',\n                            'type',\n                            'password',\n                            'server',\n                            'port',\n                            'tls',\n                        ].includes(key)\n                    ) {\n                        const i = key.replace(/-/, '_');\n                        if (['alpn'].includes(key)) {\n                            if (proxy[key]) {\n                                anytlsParams.push(\n                                    `${i}=${encodeURIComponent(\n                                        Array.isArray(proxy[key])\n                                            ? proxy[key][0]\n                                            : proxy[key],\n                                    )}`,\n                                );\n                            }\n                        } else if (['skip-cert-verify'].includes(key)) {\n                            if (proxy[key]) {\n                                anytlsParams.push(`insecure=1`);\n                            }\n                        } else if (['udp'].includes(key)) {\n                            if (proxy[key]) {\n                                anytlsParams.push(`udp=1`);\n                            }\n                        } else if (\n                            proxy[key] &&\n                            !/^_|client-fingerprint/i.test(key) &&\n                            ['number', 'string', 'boolean'].includes(\n                                typeof proxy[key],\n                            )\n                        ) {\n                            anytlsParams.push(\n                                `${i.replace(/-/g, '_')}=${encodeURIComponent(\n                                    proxy[key],\n                                )}`,\n                            );\n                        }\n                    }\n                });\n                // Parse existing query parameters from result\n                const urlParts = result.split('?');\n                let baseUrl = urlParts[0];\n                let existingParams = {};\n\n                if (urlParts.length > 1) {\n                    const queryString = urlParts[1].split('#')[0]; // Remove fragment if exists\n                    const pairs = queryString.split('&');\n                    pairs.forEach((pair) => {\n                        const [key, value] = pair.split('=');\n                        if (key) {\n                            existingParams[key] = value;\n                        }\n                    });\n                }\n\n                // Merge anytlsParams with existing parameters\n                anytlsParams.forEach((param) => {\n                    const [key, value] = param.split('=');\n                    if (key) {\n                        existingParams[key] = value;\n                    }\n                });\n\n                // Reconstruct query string\n                const newParams = Object.keys(existingParams)\n                    .map((key) => `${key}=${existingParams[key]}`)\n                    .join('&');\n\n                // Get fragment part if exists\n                const fragmentMatch = result.match(/#(.*)$/);\n                const fragment = fragmentMatch ? `#${fragmentMatch[1]}` : '';\n\n                result = `${baseUrl}?${newParams}${fragment}`;\n                // result = `anytls://${encodeURIComponent(proxy.password)}@${\n                //     proxy.server\n                // }:${proxy.port}/?${anytlsParams.join('&')}#${encodeURIComponent(\n                //     proxy.name,\n                // )}`;\n                break;\n            case 'wireguard':\n                let wireguardParams = [];\n\n                Object.keys(proxy).forEach((key) => {\n                    if (\n                        ![\n                            'name',\n                            'type',\n                            'server',\n                            'port',\n                            'ip',\n                            'ipv6',\n                            'private-key',\n                        ].includes(key)\n                    ) {\n                        if (['public-key'].includes(key)) {\n                            wireguardParams.push(`publickey=${proxy[key]}`);\n                        } else if (['udp'].includes(key)) {\n                            if (proxy[key]) {\n                                wireguardParams.push(`${key}=1`);\n                            }\n                        } else if (proxy[key] && !/^_/i.test(key)) {\n                            wireguardParams.push(\n                                `${key}=${encodeURIComponent(proxy[key])}`,\n                            );\n                        }\n                    }\n                });\n                if (proxy.ip && proxy.ipv6) {\n                    wireguardParams.push(\n                        `address=${proxy.ip}/32,${proxy.ipv6}/128`,\n                    );\n                } else if (proxy.ip) {\n                    wireguardParams.push(`address=${proxy.ip}/32`);\n                } else if (proxy.ipv6) {\n                    wireguardParams.push(`address=${proxy.ipv6}/128`);\n                }\n                result = `wireguard://${encodeURIComponent(\n                    proxy['private-key'],\n                )}@${proxy.server}:${proxy.port}/?${wireguardParams.join(\n                    '&',\n                )}#${encodeURIComponent(proxy.name)}`;\n                break;\n        }\n        return result;\n    };\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/utils.js",
    "content": "import _ from 'lodash';\n\nexport class Result {\n    constructor(proxy) {\n        this.proxy = proxy;\n        this.output = [];\n    }\n\n    append(data) {\n        if (typeof data === 'undefined') {\n            throw new Error('required field is missing');\n        }\n        this.output.push(data);\n    }\n\n    appendIfPresent(data, attr) {\n        if (isPresent(this.proxy, attr)) {\n            this.append(data);\n        }\n    }\n\n    toString() {\n        return this.output.join('');\n    }\n}\n\nexport function isPresent(obj, attr) {\n    const data = _.get(obj, attr);\n    return typeof data !== 'undefined' && data !== null;\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/producers/v2ray.js",
    "content": "/* eslint-disable no-case-declarations */\nimport { Base64 } from 'js-base64';\nimport URI_Producer from './uri';\nimport $ from '@/core/app';\n\nconst URI = URI_Producer();\n\nexport default function V2Ray_Producer() {\n    const type = 'ALL';\n    const produce = (proxies) => {\n        let result = [];\n        proxies.map((proxy) => {\n            try {\n                result.push(URI.produce(proxy));\n            } catch (err) {\n                $.error(\n                    `Cannot produce proxy: ${JSON.stringify(\n                        proxy,\n                        null,\n                        2,\n                    )}\\nReason: ${err}`,\n                );\n            }\n        });\n\n        return Base64.encode(result.join('\\n'));\n    };\n\n    return { type, produce };\n}\n"
  },
  {
    "path": "backend/src/core/proxy-utils/validators/index.js",
    "content": ""
  },
  {
    "path": "backend/src/core/rule-utils/index.js",
    "content": "import RULE_PREPROCESSORS from './preprocessors';\nimport RULE_PRODUCERS from './producers';\nimport RULE_PARSERS from './parsers';\nimport $ from '@/core/app';\n\nexport const RuleUtils = (function () {\n    function preprocess(raw) {\n        for (const processor of RULE_PREPROCESSORS) {\n            try {\n                if (processor.test(raw)) {\n                    $.info(`Pre-processor [${processor.name}] activated`);\n                    return processor.parse(raw);\n                }\n            } catch (e) {\n                $.error(`Parser [${processor.name}] failed\\n Reason: ${e}`);\n            }\n        }\n        return raw;\n    }\n\n    function parse(raw) {\n        raw = preprocess(raw);\n        for (const parser of RULE_PARSERS) {\n            let matched;\n            try {\n                matched = parser.test(raw);\n            } catch (err) {\n                matched = false;\n            }\n            if (matched) {\n                $.info(`Rule parser [${parser.name}] is activated!`);\n                return parser.parse(raw);\n            }\n        }\n    }\n\n    function produce(rules, targetPlatform) {\n        const producer = RULE_PRODUCERS[targetPlatform];\n        if (!producer) {\n            throw new Error(\n                `Target platform: ${targetPlatform} is not supported!`,\n            );\n        }\n        if (\n            typeof producer.type === 'undefined' ||\n            producer.type === 'SINGLE'\n        ) {\n            return rules\n                .map((rule) => {\n                    try {\n                        return producer.func(rule);\n                    } catch (err) {\n                        console.log(\n                            `ERROR: cannot produce rule: ${JSON.stringify(\n                                rule,\n                            )}\\nReason: ${err}`,\n                        );\n                        return '';\n                    }\n                })\n                .filter((line) => line.length > 0)\n                .join('\\n');\n        } else if (producer.type === 'ALL') {\n            return producer.func(rules);\n        }\n    }\n\n    return { parse, produce };\n})();\n"
  },
  {
    "path": "backend/src/core/rule-utils/parsers.js",
    "content": "const RULE_TYPES_MAPPING = [\n    [/^(DOMAIN|host|HOST)$/, 'DOMAIN'],\n    [/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],\n    [/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],\n    [/^USER-AGENT$/i, 'USER-AGENT'],\n    [/^PROCESS-NAME$/, 'PROCESS-NAME'],\n    [/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],\n    [/^SRC-IP(-CIDR)?$/, 'SRC-IP'],\n    [/^(IN|SRC)-PORT$/, 'IN-PORT'],\n    [/^PROTOCOL$/, 'PROTOCOL'],\n    [/^IP-CIDR$/i, 'IP-CIDR'],\n    [/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],\n    [/^GEOIP$/i, 'GEOIP'],\n    [/^GEOSITE$/i, 'GEOSITE'],\n];\n\nfunction AllRuleParser() {\n    const name = 'Universal Rule Parser';\n    const test = () => true;\n    const parse = (raw) => {\n        const lines = raw.split('\\n');\n        const result = [];\n        for (let line of lines) {\n            line = line.trim();\n            // skip empty line\n            if (line.length === 0) continue;\n            // skip comments\n            if (/\\s*#/.test(line)) continue;\n            try {\n                const params = line.split(',').map((w) => w.trim());\n                let rawType = params[0];\n                let matched = false;\n                for (const item of RULE_TYPES_MAPPING) {\n                    const regex = item[0];\n                    if (regex.test(rawType)) {\n                        matched = true;\n                        const rule = {\n                            type: item[1],\n                            content: params[1],\n                        };\n                        if (\n                            ['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)\n                        ) {\n                            rule.options = params.slice(2);\n                        }\n                        result.push(rule);\n                    }\n                }\n                if (!matched) throw new Error('Invalid rule type: ' + rawType);\n            } catch (e) {\n                console.log(`Failed to parse line: ${line}\\n Reason: ${e}`);\n            }\n        }\n        return result;\n    };\n    return { name, test, parse };\n}\n\nexport default [AllRuleParser()];\n"
  },
  {
    "path": "backend/src/core/rule-utils/preprocessors.js",
    "content": "function HTML() {\n    const name = 'HTML';\n    const test = (raw) => /^<!DOCTYPE html>/.test(raw);\n    // simply discard HTML\n    const parse = () => '';\n    return { name, test, parse };\n}\n\nfunction ClashProvider() {\n    const name = 'Clash Provider';\n    const test = (raw) => /^payload:/gm.exec(raw).index >= 0;\n    const parse = (raw) => {\n        return raw.replace('payload:', '').replace(/^\\s*-\\s*/gm, '');\n    };\n    return { name, test, parse };\n}\n\nexport default [HTML(), ClashProvider()];\n"
  },
  {
    "path": "backend/src/core/rule-utils/producers.js",
    "content": "import YAML from '@/utils/yaml';\n\nfunction QXFilter() {\n    const type = 'SINGLE';\n    const func = (rule) => {\n        // skip unsupported rules\n        const UNSUPPORTED = [\n            'URL-REGEX',\n            'DEST-PORT',\n            'SRC-IP',\n            'IN-PORT',\n            'PROTOCOL',\n            'GEOSITE',\n            'GEOIP',\n        ];\n        if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;\n\n        const TRANSFORM = {\n            'DOMAIN-KEYWORD': 'HOST-KEYWORD',\n            'DOMAIN-SUFFIX': 'HOST-SUFFIX',\n            DOMAIN: 'HOST',\n            'IP-CIDR6': 'IP6-CIDR',\n        };\n\n        // QX does not support the no-resolve option\n        return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`;\n    };\n    return { type, func };\n}\n\nfunction SurgeRuleSet() {\n    const type = 'SINGLE';\n    const func = (rule) => {\n        const UNSUPPORTED = ['GEOSITE', 'GEOIP'];\n        if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;\n        let output = `${rule.type},${rule.content}`;\n        if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {\n            output +=\n                rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';\n        }\n        return output;\n    };\n    return { type, func };\n}\n\nfunction LoonRules() {\n    const type = 'SINGLE';\n    const func = (rule) => {\n        // skip unsupported rules\n        const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];\n        if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;\n        if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {\n            // Loon only supports the no-resolve option\n            rule.options = rule.options.filter((option) =>\n                ['no-resolve'].includes(option),\n            );\n        }\n        return SurgeRuleSet().func(rule);\n    };\n    return { type, func };\n}\n\nfunction ClashRuleProvider() {\n    const type = 'ALL';\n    const func = (rules) => {\n        const TRANSFORM = {\n            'DEST-PORT': 'DST-PORT',\n            'SRC-IP': 'SRC-IP-CIDR',\n            'IN-PORT': 'SRC-PORT',\n        };\n        const conf = {\n            payload: rules.map((rule) => {\n                let output = `${TRANSFORM[rule.type] || rule.type},${\n                    rule.content\n                }`;\n                if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {\n                    if (rule.options) {\n                        // Clash only supports the no-resolve option\n                        rule.options = rule.options.filter((option) =>\n                            ['no-resolve'].includes(option),\n                        );\n                    }\n                    output +=\n                        rule.options?.length > 0\n                            ? `,${rule.options.join(',')}`\n                            : '';\n                }\n                return output;\n            }),\n        };\n        return YAML.dump(conf);\n    };\n    return { type, func };\n}\n\nexport default {\n    QX: QXFilter(),\n    Surge: SurgeRuleSet(),\n    Loon: LoonRules(),\n    Clash: ClashRuleProvider(),\n};\n"
  },
  {
    "path": "backend/src/main.js",
    "content": "/**\n *  ███████╗██╗   ██╗██████╗       ███████╗████████╗ ██████╗ ██████╗ ███████╗\n *  ██╔════╝██║   ██║██╔══██╗      ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝\n *  ███████╗██║   ██║██████╔╝█████╗███████╗   ██║   ██║   ██║██████╔╝█████╗\n *  ╚════██║██║   ██║██╔══██╗╚════╝╚════██║   ██║   ██║   ██║██╔══██╗██╔══╝\n *  ███████║╚██████╔╝██████╔╝      ███████║   ██║   ╚██████╔╝██║  ██║███████╗\n *  ╚══════╝ ╚═════╝ ╚═════╝       ╚══════╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚══════╝\n * Advanced Subscription Manager for QX, Loon, Surge and Clash.\n * @author: Peng-YM\n * @github: https://github.com/sub-store-org/Sub-Store\n * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46\n */\nimport { version } from '../package.json';\nimport $ from '@/core/app';\nconsole.log(\n    `\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n     Sub-Store -- v${version}\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n`,\n);\nimport migrate from '@/utils/migration';\nimport serve from '@/restful';\n\nmigrate();\nserve();\n"
  },
  {
    "path": "backend/src/products/cron-sync-artifacts.js",
    "content": "import { version } from '../../package.json';\nimport {\n    SETTINGS_KEY,\n    ARTIFACTS_KEY,\n    SUBS_KEY,\n    COLLECTIONS_KEY,\n} from '@/constants';\nimport $ from '@/core/app';\nimport { produceArtifact } from '@/restful/sync';\nimport { syncToGist } from '@/restful/artifacts';\nimport { findByName } from '@/utils/database';\n\n!(async function () {\n    let arg;\n    if (typeof $argument != 'undefined') {\n        arg = Object.fromEntries(\n            // eslint-disable-next-line no-undef\n            $argument.split('&').map((item) => item.split('=')),\n        );\n    } else {\n        arg = {};\n    }\n    let sub_names = (arg?.subscription ?? arg?.sub ?? '')\n        .split(/,|，/g)\n        .map((i) => i.trim())\n        .filter((i) => i.length > 0)\n        .map((i) => decodeURIComponent(i));\n    let col_names = (arg?.collection ?? arg?.col ?? '')\n        .split(/,|，/g)\n        .map((i) => i.trim())\n        .filter((i) => i.length > 0)\n        .map((i) => decodeURIComponent(i));\n    if (sub_names.length > 0 || col_names.length > 0) {\n        if (sub_names.length > 0)\n            await produceArtifacts(sub_names, 'subscription');\n        if (col_names.length > 0)\n            await produceArtifacts(col_names, 'collection');\n    } else {\n        const settings = $.read(SETTINGS_KEY);\n        // if GitHub token is not configured\n        if (!settings.githubUser || !settings.gistToken) return;\n\n        const artifacts = $.read(ARTIFACTS_KEY);\n        if (!artifacts || artifacts.length === 0) return;\n\n        const shouldSync = artifacts.some((artifact) => artifact.sync);\n        if (shouldSync) await doSync();\n    }\n})().finally(() => $.done());\n\nasync function produceArtifacts(names, type) {\n    try {\n        if (names.length > 0) {\n            $.info(`produceArtifacts ${type} 开始: ${names.join(', ')}`);\n            await Promise.all(\n                names.map(async (name) => {\n                    try {\n                        await produceArtifact({\n                            type,\n                            name,\n                        });\n                    } catch (e) {\n                        $.error(`${type} ${name} error: ${e.message ?? e}`);\n                    }\n                }),\n            );\n            $.info(`produceArtifacts ${type} 完成: ${names.join(', ')}`);\n        }\n    } catch (e) {\n        $.error(`produceArtifacts error: ${e.message ?? e}`);\n    }\n}\nasync function doSync() {\n    console.log(\n        `\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n     Sub-Store Sync -- v${version}\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n`,\n    );\n\n    $.info('开始同步所有远程配置...');\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    const files = {};\n\n    try {\n        const valid = [];\n        const invalid = [];\n        const allSubs = $.read(SUBS_KEY);\n        const allCols = $.read(COLLECTIONS_KEY);\n        const subNames = [];\n        let enabledCount = 0;\n        allArtifacts.map((artifact) => {\n            if (artifact.sync && artifact.source) {\n                enabledCount++;\n                if (artifact.type === 'subscription') {\n                    const subName = artifact.source;\n                    const sub = findByName(allSubs, subName);\n                    if (sub && sub.url && !subNames.includes(subName)) {\n                        subNames.push(subName);\n                    }\n                } else if (artifact.type === 'collection') {\n                    const collection = findByName(allCols, artifact.source);\n                    if (collection && Array.isArray(collection.subscriptions)) {\n                        collection.subscriptions.map((subName) => {\n                            const sub = findByName(allSubs, subName);\n                            if (sub && sub.url && !subNames.includes(subName)) {\n                                subNames.push(subName);\n                            }\n                        });\n                    }\n                }\n            }\n        });\n\n        if (enabledCount === 0) {\n            $.info(\n                `需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,\n            );\n            return;\n        }\n\n        if (subNames.length > 0) {\n            await Promise.all(\n                subNames.map(async (subName) => {\n                    try {\n                        await produceArtifact({\n                            type: 'subscription',\n                            name: subName,\n                            awaitCustomCache: true,\n                        });\n                    } catch (e) {\n                        // $.error(`${e.message ?? e}`);\n                    }\n                }),\n            );\n        }\n        await Promise.all(\n            allArtifacts.map(async (artifact) => {\n                try {\n                    if (artifact.sync && artifact.source) {\n                        $.info(`正在同步云配置：${artifact.name}...`);\n\n                        const useMihomoExternal =\n                            artifact.platform === 'SurgeMac';\n\n                        if (useMihomoExternal) {\n                            $.info(\n                                `手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,\n                            );\n                        }\n                        const output = await produceArtifact({\n                            type: artifact.type,\n                            name: artifact.source,\n                            platform: artifact.platform,\n                            produceOpts: {\n                                'include-unsupported-proxy':\n                                    artifact.includeUnsupportedProxy,\n                                useMihomoExternal,\n                            },\n                        });\n\n                        // if (!output || output.length === 0)\n                        //     throw new Error('该配置的结果为空 不进行上传');\n\n                        files[encodeURIComponent(artifact.name)] = {\n                            content: output,\n                        };\n\n                        valid.push(artifact.name);\n                    }\n                } catch (e) {\n                    $.error(\n                        `生成同步配置 ${artifact.name} 发生错误: ${\n                            e.message ?? e\n                        }`,\n                    );\n                    invalid.push(artifact.name);\n                }\n            }),\n        );\n\n        $.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);\n        $.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);\n\n        if (valid.length === 0) {\n            throw new Error(\n                `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,\n            );\n        }\n\n        const resp = await syncToGist(files);\n        const body = JSON.parse(resp.body);\n        delete body.history;\n        delete body.forks;\n        delete body.owner;\n        Object.values(body.files).forEach((file) => {\n            delete file.content;\n        });\n        $.info('上传配置响应:');\n        $.info(JSON.stringify(body, null, 2));\n\n        for (const artifact of allArtifacts) {\n            if (\n                artifact.sync &&\n                artifact.source &&\n                valid.includes(artifact.name)\n            ) {\n                artifact.updated = new Date().getTime();\n                // extract real url from gist\n                let files = body.files;\n                let isGitLab;\n                if (Array.isArray(files)) {\n                    isGitLab = true;\n                    files = Object.fromEntries(\n                        files.map((item) => [item.path, item]),\n                    );\n                }\n                const raw_url =\n                    files[encodeURIComponent(artifact.name)]?.raw_url;\n                const new_url = isGitLab\n                    ? raw_url\n                    : raw_url?.replace(/\\/raw\\/[^/]*\\/(.*)/, '/raw/$1');\n                $.info(\n                    `上传配置完成\\n文件列表: ${Object.keys(files).join(\n                        ', ',\n                    )}\\n当前文件: ${encodeURIComponent(\n                        artifact.name,\n                    )}\\n响应返回的原始链接: ${raw_url}\\n处理完的新链接: ${new_url}`,\n                );\n                artifact.url = new_url;\n            }\n        }\n\n        $.write(allArtifacts, ARTIFACTS_KEY);\n        $.info('上传配置成功');\n\n        if (invalid.length > 0) {\n            $.notify(\n                '🌍 Sub-Store',\n                `同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,\n            );\n        } else {\n            $.notify('🌍 Sub-Store', '同步配置完成');\n        }\n    } catch (e) {\n        $.notify('🌍 Sub-Store', '同步配置失败', `原因：${e.message ?? e}`);\n        $.error(`无法同步配置到 Gist，原因：${e}`);\n    }\n}\n"
  },
  {
    "path": "backend/src/products/resource-parser.loon.js",
    "content": "/* eslint-disable no-undef */\nimport { ProxyUtils } from '@/core/proxy-utils';\nimport { RuleUtils } from '@/core/rule-utils';\nimport { version } from '../../package.json';\nimport download from '@/utils/download';\n\nlet result = '';\nlet resource = typeof $resource !== 'undefined' ? $resource : '';\nlet resourceType = typeof $resourceType !== 'undefined' ? $resourceType : '';\nlet resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';\n\n!(async () => {\n    console.log(\n        `\n    ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n         Sub-Store -- v${version}\n         Loon -- ${$loon}\n    ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n    `,\n    );\n\n    const build = $loon.match(/\\((\\d+)\\)$/)?.[1];\n    let arg;\n    if (typeof $argument != 'undefined') {\n        arg = Object.fromEntries(\n            $argument.split('&').map((item) => item.split('=')),\n        );\n    } else {\n        arg = {};\n    }\n    console.log(`arg: ${JSON.stringify(arg)}`);\n\n    const RESOURCE_TYPE = {\n        PROXY: 1,\n        RULE: 2,\n    };\n    if (!arg.resourceUrlOnly) {\n        result = resource;\n    }\n\n    if (resourceType === RESOURCE_TYPE.PROXY) {\n        if (!arg.resourceUrlOnly) {\n            try {\n                let proxies = ProxyUtils.parse(resource);\n                result = ProxyUtils.produce(proxies, 'Loon', undefined, {\n                    'include-unsupported-proxy':\n                        arg?.includeUnsupportedProxy || build >= 842,\n                });\n            } catch (e) {\n                console.log('解析器: 使用 resource 出现错误');\n                console.log(e.message ?? e);\n            }\n        }\n        if ((!result || /^\\s*$/.test(result)) && resourceUrl) {\n            console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);\n            try {\n                let raw = await download(\n                    resourceUrl,\n                    arg?.ua,\n                    arg?.timeout,\n                    undefined,\n                    undefined,\n                    undefined,\n                    undefined,\n                    true,\n                );\n                let proxies = ProxyUtils.parse(raw);\n                result = ProxyUtils.produce(proxies, 'Loon', undefined, {\n                    'include-unsupported-proxy':\n                        arg?.includeUnsupportedProxy || build >= 842,\n                });\n            } catch (e) {\n                console.log(e.message ?? e);\n            }\n        }\n    } else if (resourceType === RESOURCE_TYPE.RULE) {\n        if (!arg.resourceUrlOnly) {\n            try {\n                const rules = RuleUtils.parse(resource);\n                result = RuleUtils.produce(rules, 'Loon');\n            } catch (e) {\n                console.log(e.message ?? e);\n            }\n        }\n        if ((!result || /^\\s*$/.test(result)) && resourceUrl) {\n            console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);\n            try {\n                let raw = await download(resourceUrl, arg?.ua, arg?.timeout);\n                let rules = RuleUtils.parse(raw);\n                result = RuleUtils.produce(rules, 'Loon');\n            } catch (e) {\n                console.log(e.message ?? e);\n            }\n        }\n    }\n})()\n    .catch(async (e) => {\n        console.log('解析器: 出现错误');\n        console.log(e.message ?? e);\n    })\n    .finally(() => {\n        $done(result || '');\n    });\n"
  },
  {
    "path": "backend/src/products/sub-store-0.js",
    "content": "/**\n * 路由拆分 - 本文件只包含不涉及到解析器的 RESTFul API\n */\n\nimport { version } from '../../package.json';\nconsole.log(\n    `\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n     Sub-Store -- v${version}\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n`,\n);\n\nimport migrate from '@/utils/migration';\nimport express from '@/vendor/express';\nimport $ from '@/core/app';\nimport registerCollectionRoutes from '@/restful/collections';\nimport registerSubscriptionRoutes from '@/restful/subscriptions';\nimport registerArtifactRoutes from '@/restful/artifacts';\nimport registerSettingRoutes from '@/restful/settings';\nimport registerMiscRoutes from '@/restful/miscs';\nimport registerSortRoutes from '@/restful/sort';\nimport registerFileRoutes from '@/restful/file';\nimport registerTokenRoutes from '@/restful/token';\nimport registerModuleRoutes from '@/restful/module';\n\nmigrate();\nserve();\n\nfunction serve() {\n    const $app = express({ substore: $ });\n\n    // register routes\n    registerCollectionRoutes($app);\n    registerSubscriptionRoutes($app);\n    registerTokenRoutes($app);\n    registerFileRoutes($app);\n    registerModuleRoutes($app);\n    registerArtifactRoutes($app);\n    registerSettingRoutes($app);\n    registerSortRoutes($app);\n    registerMiscRoutes($app);\n\n    $app.start();\n}\n"
  },
  {
    "path": "backend/src/products/sub-store-1.js",
    "content": "/**\n * 路由拆分 - 本文件仅包含使用到解析器的 RESTFul API\n */\n\nimport { version } from '../../package.json';\nimport migrate from '@/utils/migration';\nimport express from '@/vendor/express';\nimport $ from '@/core/app';\nimport registerDownloadRoutes from '@/restful/download';\nimport registerPreviewRoutes from '@/restful/preview';\nimport registerSyncRoutes from '@/restful/sync';\nimport registerNodeInfoRoutes from '@/restful/node-info';\n\nconsole.log(\n    `\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n     Sub-Store -- v${version}\n┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n`,\n);\n\nmigrate();\nserve();\n\nfunction serve() {\n    const $app = express({ substore: $ });\n\n    // register routes\n    registerDownloadRoutes($app);\n    registerPreviewRoutes($app);\n    registerSyncRoutes($app);\n    registerNodeInfoRoutes($app);\n\n    $app.options('/', (req, res) => {\n        res.status(200).end();\n    });\n\n    $app.start();\n}\n"
  },
  {
    "path": "backend/src/restful/artifacts.js",
    "content": "import $ from '@/core/app';\n\nimport {\n    ARTIFACT_REPOSITORY_KEY,\n    ARTIFACTS_KEY,\n    SETTINGS_KEY,\n} from '@/constants';\nimport { deleteByName, findByName, updateByName } from '@/utils/database';\nimport { failed, success } from '@/restful/response';\nimport {\n    InternalServerError,\n    RequestInvalidError,\n    ResourceNotFoundError,\n} from '@/restful/errors';\nimport Gist from '@/utils/gist';\n\nexport default function register($app) {\n    // Initialization\n    if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);\n\n    // RESTful APIs\n    $app.get('/api/artifacts/restore', restoreArtifacts);\n\n    $app.route('/api/artifacts')\n        .get(getAllArtifacts)\n        .post(createArtifact)\n        .put(replaceArtifact);\n\n    $app.route('/api/artifact/:name')\n        .get(getArtifact)\n        .patch(updateArtifact)\n        .delete(deleteArtifact);\n}\n\nasync function restoreArtifacts(_, res) {\n    $.info('开始恢复远程配置...');\n    try {\n        const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);\n        if (!gistToken) {\n            return Promise.reject('未设置 GitHub Token！');\n        }\n        const manager = new Gist({\n            token: gistToken,\n            key: ARTIFACT_REPOSITORY_KEY,\n            syncPlatform,\n        });\n\n        try {\n            const gist = await manager.locate();\n            if (!gist?.files) {\n                throw new Error(`找不到 Sub-Store Gist 文件列表`);\n            }\n            const allArtifacts = $.read(ARTIFACTS_KEY);\n            const failed = [];\n            Object.keys(gist.files).map((key) => {\n                const filename = gist.files[key]?.filename;\n                if (filename) {\n                    if (encodeURIComponent(filename) !== filename) {\n                        $.error(`文件名 ${filename} 未编码 不保存`);\n                        failed.push(filename);\n                    } else {\n                        const artifact = findByName(allArtifacts, filename);\n                        if (artifact) {\n                            updateByName(allArtifacts, filename, {\n                                ...artifact,\n                                url: gist.files[key]?.raw_url.replace(\n                                    /\\/raw\\/[^/]*\\/(.*)/,\n                                    '/raw/$1',\n                                ),\n                            });\n                        } else {\n                            allArtifacts.push({\n                                name: `${filename}`,\n                                url: gist.files[key]?.raw_url.replace(\n                                    /\\/raw\\/[^/]*\\/(.*)/,\n                                    '/raw/$1',\n                                ),\n                            });\n                        }\n                    }\n                }\n            });\n            $.write(allArtifacts, ARTIFACTS_KEY);\n        } catch (err) {\n            $.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);\n            throw err;\n        }\n        success(res);\n    } catch (e) {\n        $.error(`恢复远程配置失败，原因：${e.message ?? e}`);\n        failed(\n            res,\n            new InternalServerError(\n                `FAILED_TO_RESTORE_ARTIFACTS`,\n                `Failed to restore artifacts`,\n                `Reason: ${e.message ?? e}`,\n            ),\n        );\n    }\n}\n\nfunction getAllArtifacts(req, res) {\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    success(res, allArtifacts);\n}\n\nfunction replaceArtifact(req, res) {\n    const allArtifacts = req.body;\n    $.write(allArtifacts, ARTIFACTS_KEY);\n    success(res);\n}\n\nasync function getArtifact(req, res) {\n    let { name } = req.params;\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    const artifact = findByName(allArtifacts, name);\n\n    if (artifact) {\n        success(res, artifact);\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Artifact ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nfunction createArtifact(req, res) {\n    const artifact = req.body;\n    if (!validateArtifactName(artifact.name)) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'INVALID_ARTIFACT_NAME',\n                `Artifact name ${artifact.name} is invalid.`,\n            ),\n        );\n        return;\n    }\n\n    $.info(`正在创建远程配置：${artifact.name}`);\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    if (findByName(allArtifacts, artifact.name)) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'DUPLICATE_KEY',\n                `Artifact ${artifact.name} already exists.`,\n            ),\n        );\n    } else {\n        allArtifacts.push(artifact);\n        $.write(allArtifacts, ARTIFACTS_KEY);\n        success(res, artifact, 201);\n    }\n}\n\nfunction updateArtifact(req, res) {\n    let artifact = req.body;\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    let oldName = req.params.name;\n    const oldArtifact = findByName(allArtifacts, oldName);\n    if (oldArtifact) {\n        if (!artifact.name) artifact.name = oldArtifact.name;\n        $.info(`正在更新远程配置：${oldArtifact.name}`);\n        const newArtifact = {\n            ...oldArtifact,\n            ...artifact,\n        };\n        if (!validateArtifactName(newArtifact.name)) {\n            failed(\n                res,\n                new RequestInvalidError(\n                    'INVALID_ARTIFACT_NAME',\n                    `Artifact name ${newArtifact.name} is invalid.`,\n                ),\n            );\n            return;\n        }\n        updateByName(allArtifacts, oldName, newArtifact);\n        $.write(allArtifacts, ARTIFACTS_KEY);\n        success(res, newArtifact);\n    } else {\n        failed(\n            res,\n            new RequestInvalidError(\n                'DUPLICATE_KEY',\n                `Artifact ${oldName} already exists.`,\n            ),\n        );\n    }\n}\n\nasync function deleteArtifact(req, res) {\n    let { name } = req.params;\n    $.info(`正在删除远程配置：${name}`);\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    try {\n        const artifact = findByName(allArtifacts, name);\n        if (!artifact) throw new Error(`远程配置：${name}不存在！`);\n        if (artifact.updated) {\n            // delete gist\n            const files = {};\n            files[encodeURIComponent(artifact.name)] = {\n                content: '',\n            };\n            if (encodeURIComponent(artifact.name) !== artifact.name) {\n                files[artifact.name] = {\n                    content: '',\n                };\n            }\n\n            // 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug\n            try {\n                await syncToGist(files);\n            } catch (i) {\n                $.error(`Function syncToGist: ${name} : ${i}`);\n            }\n        }\n        // delete local cache\n        deleteByName(allArtifacts, name);\n        $.write(allArtifacts, ARTIFACTS_KEY);\n        success(res);\n    } catch (err) {\n        $.error(`无法删除远程配置：${name}，原因：${err}`);\n        failed(\n            res,\n            new InternalServerError(\n                `FAILED_TO_DELETE_ARTIFACT`,\n                `Failed to delete artifact ${name}`,\n                `Reason: ${err}`,\n            ),\n        );\n    }\n}\n\nfunction validateArtifactName(name) {\n    return /^[a-zA-Z0-9._-]*$/.test(name);\n}\n\nasync function syncToGist(files) {\n    const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);\n    if (!gistToken) {\n        return Promise.reject('未设置 GitHub Token！');\n    }\n    const manager = new Gist({\n        token: gistToken,\n        key: ARTIFACT_REPOSITORY_KEY,\n        syncPlatform,\n    });\n    const res = await manager.upload(files);\n    let body = {};\n    try {\n        body = JSON.parse(res.body);\n        // eslint-disable-next-line no-empty\n    } catch (e) {}\n\n    const url = body?.html_url ?? body?.web_url;\n    const settings = $.read(SETTINGS_KEY);\n    if (url) {\n        $.log(`同步 Gist 后, 找到 Sub-Store Gist: ${url}`);\n        settings.artifactStore = url;\n        settings.artifactStoreStatus = 'VALID';\n    } else {\n        $.error(`同步 Gist 后, 找不到 Sub-Store Gist`);\n        settings.artifactStoreStatus = 'NOT FOUND';\n    }\n    $.write(settings, SETTINGS_KEY);\n    return res;\n}\n\nexport { syncToGist };\n"
  },
  {
    "path": "backend/src/restful/collections.js",
    "content": "import { deleteByName, findByName, updateByName } from '@/utils/database';\nimport { COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY } from '@/constants';\nimport { failed, success } from '@/restful/response';\nimport $ from '@/core/app';\nimport { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';\nimport { formatDateTime } from '@/utils';\n\nexport default function register($app) {\n    if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);\n\n    $app.route('/api/collection/:name')\n        .get(getCollection)\n        .patch(updateCollection)\n        .delete(deleteCollection);\n\n    $app.route('/api/collections')\n        .get(getAllCollections)\n        .post(createCollection)\n        .put(replaceCollection);\n}\n\n// collection API\nfunction createCollection(req, res) {\n    const collection = req.body;\n    $.info(`正在创建组合订阅：${collection.name}`);\n    if (/\\//.test(collection.name)) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'INVALID_NAME',\n                `Collection ${collection.name} is invalid`,\n            ),\n        );\n        return;\n    }\n    const allCols = $.read(COLLECTIONS_KEY);\n    if (findByName(allCols, collection.name)) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'DUPLICATE_KEY',\n                `Collection ${collection.name} already exists.`,\n            ),\n        );\n        return;\n    }\n    allCols.push(collection);\n    $.write(allCols, COLLECTIONS_KEY);\n    success(res, collection, 201);\n}\n\nfunction getCollection(req, res) {\n    let { name } = req.params;\n    let { raw } = req.query;\n    const allCols = $.read(COLLECTIONS_KEY);\n    const collection = findByName(allCols, name);\n    if (collection) {\n        if (raw) {\n            res.set('content-type', 'application/json')\n                .set(\n                    'content-disposition',\n                    `attachment; filename=\"${encodeURIComponent(\n                        `sub-store_collection_${name}_${formatDateTime(\n                            new Date(),\n                        )}.json`,\n                    )}\"`,\n                )\n                .send(JSON.stringify(collection));\n        } else {\n            success(res, collection);\n        }\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                `SUBSCRIPTION_NOT_FOUND`,\n                `Collection ${name} does not exist`,\n                404,\n            ),\n        );\n    }\n}\n\nfunction updateCollection(req, res) {\n    let { name } = req.params;\n    let collection = req.body;\n    const allCols = $.read(COLLECTIONS_KEY);\n    const oldCol = findByName(allCols, name);\n    if (oldCol) {\n        if (!collection.name) collection.name = oldCol.name;\n        const newCol = {\n            ...oldCol,\n            ...collection,\n        };\n        $.info(`正在更新组合订阅：${name}...`);\n\n        if (name !== newCol.name) {\n            // update all artifacts referring this collection\n            const allArtifacts = $.read(ARTIFACTS_KEY) || [];\n            for (const artifact of allArtifacts) {\n                if (\n                    artifact.type === 'collection' &&\n                    artifact.source === oldCol.name\n                ) {\n                    artifact.source = newCol.name;\n                }\n            }\n            // update all files referring this collection\n            const allFiles = $.read(FILES_KEY) || [];\n            for (const file of allFiles) {\n                if (\n                    file.sourceType === 'collection' &&\n                    file.sourceName === oldCol.name\n                ) {\n                    file.sourceName = newCol.name;\n                }\n            }\n            $.write(allArtifacts, ARTIFACTS_KEY);\n            $.write(allFiles, FILES_KEY);\n        }\n\n        updateByName(allCols, name, newCol);\n        $.write(allCols, COLLECTIONS_KEY);\n        success(res, newCol);\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Collection ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nfunction deleteCollection(req, res) {\n    let { name } = req.params;\n    $.info(`正在删除组合订阅：${name}`);\n    let allCols = $.read(COLLECTIONS_KEY);\n    deleteByName(allCols, name);\n    $.write(allCols, COLLECTIONS_KEY);\n    success(res);\n}\n\nfunction getAllCollections(req, res) {\n    const allCols = $.read(COLLECTIONS_KEY);\n    success(res, allCols);\n}\n\nfunction replaceCollection(req, res) {\n    const allCols = req.body;\n    $.write(allCols, COLLECTIONS_KEY);\n    success(res);\n}\n"
  },
  {
    "path": "backend/src/restful/download.js",
    "content": "import {\n    getPlatformFromHeaders,\n    shouldIncludeUnsupportedProxy,\n} from '@/utils/user-agent';\nimport { ProxyUtils } from '@/core/proxy-utils';\nimport { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';\nimport { findByName } from '@/utils/database';\nimport { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';\nimport $ from '@/core/app';\nimport { failed } from '@/restful/response';\nimport { InternalServerError, ResourceNotFoundError } from '@/restful/errors';\nimport { produceArtifact } from '@/restful/sync';\n// eslint-disable-next-line no-unused-vars\nimport { isIPv4, isIPv6 } from '@/utils';\nimport { getISO } from '@/utils/geo';\nimport env from '@/utils/env';\n\nexport default function register($app) {\n    $app.get('/share/col/:name/:target', async (req, res) => {\n        const { target } = req.params;\n        if (target) {\n            req.query.target = target;\n            $.info(`使用路由指定目标: ${target}`);\n        }\n        await downloadCollection(req, res);\n    });\n    $app.get('/share/col/:name', downloadCollection);\n    $app.get('/share/sub/:name/:target', async (req, res) => {\n        const { target } = req.params;\n        if (target) {\n            req.query.target = target;\n            $.info(`使用路由指定目标: ${target}`);\n        }\n        await downloadSubscription(req, res);\n    });\n    $app.get('/share/sub/:name', downloadSubscription);\n\n    $app.get('/download/collection/:name/:target', async (req, res) => {\n        const { target } = req.params;\n        if (target) {\n            req.query.target = target;\n            $.info(`使用路由指定目标: ${target}`);\n        }\n        await downloadCollection(req, res);\n    });\n    $app.get('/download/collection/:name', downloadCollection);\n    $app.get('/download/:name/:target', async (req, res) => {\n        const { target } = req.params;\n        if (target) {\n            req.query.target = target;\n            $.info(`使用路由指定目标: ${target}`);\n        }\n        await downloadSubscription(req, res);\n    });\n    $app.get('/download/:name', downloadSubscription);\n\n    $app.get(\n        '/download/collection/:name/api/v1/server/details',\n        async (req, res) => {\n            req.query.platform = 'JSON';\n            req.query.produceType = 'internal';\n            req.query.resultFormat = 'nezha';\n            await downloadCollection(req, res);\n        },\n    );\n    $app.get('/download/:name/api/v1/server/details', async (req, res) => {\n        req.query.platform = 'JSON';\n        req.query.produceType = 'internal';\n        req.query.resultFormat = 'nezha';\n        await downloadSubscription(req, res);\n    });\n    $app.get(\n        '/download/collection/:name/api/v1/monitor/:nezhaIndex',\n        async (req, res) => {\n            req.query.platform = 'JSON';\n            req.query.produceType = 'internal';\n            req.query.resultFormat = 'nezha-monitor';\n            await downloadCollection(req, res);\n        },\n    );\n    $app.get('/download/:name/api/v1/monitor/:nezhaIndex', async (req, res) => {\n        req.query.platform = 'JSON';\n        req.query.produceType = 'internal';\n        req.query.resultFormat = 'nezha-monitor';\n        await downloadSubscription(req, res);\n    });\n}\n\nasync function downloadSubscription(req, res) {\n    let { name, nezhaIndex } = req.params;\n\n    const useMihomoExternal = req.query.target === 'SurgeMac';\n\n    const platform =\n        req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';\n    const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];\n    $.info(\n        `正在下载订阅：${name}\\n请求 User-Agent: ${reqUA}\\n请求 target: ${req.query.target}\\n实际输出: ${platform}`,\n    );\n    let {\n        url,\n        ua,\n        content,\n        mergeSources,\n        ignoreFailedRemoteSub,\n        produceType,\n        includeUnsupportedProxy,\n        resultFormat,\n        proxy,\n        noCache,\n        _fakeNode,\n    } = req.query;\n\n    let $options = {\n        _req: {\n            method: req.method,\n            url: req.url,\n            path: req.path,\n            query: req.query,\n            params: req.params,\n            headers: req.headers,\n            body: req.body,\n        },\n    };\n    if (req.query.$options) {\n        let options = {};\n        try {\n            // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n            options = JSON.parse(decodeURIComponent(req.query.$options));\n        } catch (e) {\n            for (const pair of req.query.$options.split('&')) {\n                const key = pair.split('=')[0];\n                const value = pair.split('=')[1];\n                // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                options[key] =\n                    value == null || value === ''\n                        ? true\n                        : decodeURIComponent(value);\n            }\n        }\n        $.info(`传入 $options: ${JSON.stringify(options)}`);\n        Object.assign($options, options);\n    }\n    if (url) {\n        $.info(`指定远程订阅 URL: ${url}`);\n        if (!/^https?:\\/\\//.test(url)) {\n            content = url;\n            $.info(`URL 不是链接，视为本地订阅`);\n        }\n    }\n    if (content) {\n        $.info(`指定本地订阅: ${content}`);\n    }\n    if (proxy) {\n        $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);\n    }\n    if (ua) {\n        $.info(`指定远程订阅 User-Agent: ${ua}`);\n    }\n\n    if (mergeSources) {\n        $.info(`指定合并来源: ${mergeSources}`);\n    }\n    if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {\n        $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);\n    }\n    if (produceType) {\n        $.info(`指定生产类型: ${produceType}`);\n    }\n    if (includeUnsupportedProxy) {\n        $.info(\n            `包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,\n        );\n    }\n\n    if (\n        !includeUnsupportedProxy &&\n        shouldIncludeUnsupportedProxy(platform, req.headers)\n    ) {\n        includeUnsupportedProxy = true;\n        $.info(\n            `当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,\n        );\n    }\n\n    if (useMihomoExternal) {\n        $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);\n    }\n\n    if (noCache) {\n        $.info(`指定不使用缓存: ${noCache}`);\n    }\n\n    const allSubs = $.read(SUBS_KEY);\n    const fakeSub = {\n        name: 'fakeNodeInfo',\n        source: 'local',\n        content:\n            'invalid share = ss, 1.0.0.1, 80, encrypt-method=aes-128-gcm, password=password',\n    };\n    const sub = _fakeNode ? fakeSub : findByName(allSubs, name);\n    if (sub) {\n        try {\n            const passThroughUA = sub.passThroughUA;\n            if (passThroughUA) {\n                $.info(\n                    `订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,\n                );\n                ua = reqUA;\n            }\n            const opt = {\n                type: 'subscription',\n                name,\n                platform,\n                url,\n                ua,\n                content,\n                mergeSources,\n                ignoreFailedRemoteSub,\n                produceType,\n                produceOpts: {\n                    'include-unsupported-proxy': includeUnsupportedProxy,\n                    useMihomoExternal,\n                },\n                $options,\n                proxy,\n                noCache,\n            };\n            if (_fakeNode) {\n                $.info(`返回假节点信息`);\n                delete opt.name;\n                opt.subscription = fakeSub;\n            }\n            let output = await produceArtifact(opt);\n            let flowInfo;\n            if (\n                sub.source !== 'local' ||\n                ['localFirst', 'remoteFirst'].includes(sub.mergeSources)\n            ) {\n                try {\n                    url =\n                        `${url || sub.url}`\n                            .split(/[\\r\\n]+/)\n                            .map((i) => i.trim())\n                            .filter((i) => i.length)?.[0] || '';\n\n                    let $arguments = {};\n                    const rawArgs = url.split('#');\n                    url = url.split('#')[0];\n                    if (rawArgs.length > 1) {\n                        try {\n                            // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n                            $arguments = JSON.parse(\n                                decodeURIComponent(rawArgs[1]),\n                            );\n                        } catch (e) {\n                            for (const pair of rawArgs[1].split('&')) {\n                                const key = pair.split('=')[0];\n                                const value = pair.split('=')[1];\n                                // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                                $arguments[key] =\n                                    value == null || value === ''\n                                        ? true\n                                        : decodeURIComponent(value);\n                            }\n                        }\n                    }\n                    if (!$arguments.noFlow && /^https?/.test(url)) {\n                        // forward flow headers\n                        flowInfo = await getFlowHeaders(\n                            $arguments?.insecure ? `${url}#insecure` : url,\n                            $arguments.flowUserAgent,\n                            undefined,\n                            proxy || sub.proxy,\n                            $arguments.flowUrl,\n                        );\n                        if (flowInfo) {\n                            const headers = normalizeFlowHeader(flowInfo, true);\n                            if (headers?.['subscription-userinfo']) {\n                                res.set(\n                                    'subscription-userinfo',\n                                    headers['subscription-userinfo'],\n                                );\n                            }\n                            if (headers?.['profile-web-page-url']) {\n                                res.set(\n                                    'profile-web-page-url',\n                                    headers['profile-web-page-url'],\n                                );\n                            }\n                            if (headers?.['plan-name']) {\n                                res.set('plan-name', headers['plan-name']);\n                            }\n                        }\n                    }\n                } catch (err) {\n                    $.error(\n                        `订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(\n                            err,\n                        )}`,\n                    );\n                }\n            }\n            if (sub.subUserinfo) {\n                let subUserInfo;\n                if (/^https?:\\/\\//.test(sub.subUserinfo)) {\n                    try {\n                        subUserInfo = await getFlowHeaders(\n                            undefined,\n                            undefined,\n                            undefined,\n                            proxy || sub.proxy,\n                            sub.subUserinfo,\n                        );\n                    } catch (e) {\n                        $.error(\n                            `订阅 ${name} 使用自定义流量链接 ${\n                                sub.subUserinfo\n                            } 获取流量信息时发生错误: ${JSON.stringify(e)}`,\n                        );\n                    }\n                } else {\n                    subUserInfo = sub.subUserinfo;\n                }\n\n                const headers = normalizeFlowHeader(\n                    [subUserInfo, flowInfo].filter((i) => i).join(';'),\n                    true,\n                );\n                if (headers?.['subscription-userinfo']) {\n                    res.set(\n                        'subscription-userinfo',\n                        headers['subscription-userinfo'],\n                    );\n                }\n                if (headers?.['profile-web-page-url']) {\n                    res.set(\n                        'profile-web-page-url',\n                        headers['profile-web-page-url'],\n                    );\n                }\n                if (headers?.['plan-name']) {\n                    res.set('plan-name', headers['plan-name']);\n                }\n            }\n\n            if (platform === 'JSON') {\n                if (resultFormat === 'nezha') {\n                    output = nezhaTransform(output);\n                } else if (resultFormat === 'nezha-monitor') {\n                    nezhaIndex = /^\\d+$/.test(nezhaIndex)\n                        ? parseInt(nezhaIndex, 10)\n                        : output.findIndex((i) => i.name === nezhaIndex);\n                    output = await nezhaMonitor(\n                        output[nezhaIndex],\n                        nezhaIndex,\n                        req.query,\n                    );\n                }\n                res.set('Content-Type', 'application/json;charset=utf-8');\n            } else {\n                res.set('Content-Type', 'text/plain; charset=utf-8');\n            }\n            if ($options?._res?.headers) {\n                Object.entries($options._res.headers).forEach(\n                    ([key, value]) => {\n                        if (value == null) {\n                            res.removeHeader(key);\n                        } else {\n                            res.set(key, value);\n                        }\n                    },\n                );\n            }\n            if ($options?._res?.status) {\n                res.status($options._res.status);\n            }\n            res.send(output);\n        } catch (err) {\n            $.notify(\n                `🌍 Sub-Store 下载订阅失败`,\n                `❌ 无法下载订阅：${name}！`,\n                `🤔 原因：${err.message ?? err}`,\n            );\n            $.error(err.message ?? err);\n            failed(\n                res,\n                new InternalServerError(\n                    'INTERNAL_SERVER_ERROR',\n                    `Failed to download subscription: ${name}`,\n                    `Reason: ${err.message ?? err}`,\n                ),\n            );\n        }\n    } else {\n        $.error(`🌍 Sub-Store 下载订阅失败\\n❌ 未找到订阅：${name}！`);\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Subscription ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nasync function downloadCollection(req, res) {\n    let { name, nezhaIndex } = req.params;\n\n    const useMihomoExternal = req.query.target === 'SurgeMac';\n\n    const platform =\n        req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';\n\n    const allCols = $.read(COLLECTIONS_KEY);\n    const collection = findByName(allCols, name);\n    const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];\n    $.info(\n        `正在下载组合订阅：${name}\\n请求 User-Agent: ${reqUA}\\n请求 target: ${req.query.target}\\n实际输出: ${platform}`,\n    );\n\n    let {\n        ignoreFailedRemoteSub,\n        produceType,\n        includeUnsupportedProxy,\n        resultFormat,\n        proxy,\n        noCache,\n    } = req.query;\n\n    let $options = {\n        _req: {\n            method: req.method,\n            url: req.url,\n            path: req.path,\n            query: req.query,\n            params: req.params,\n            headers: req.headers,\n            body: req.body,\n        },\n    };\n    if (req.query.$options) {\n        let options = {};\n        try {\n            // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n            options = JSON.parse(decodeURIComponent(req.query.$options));\n        } catch (e) {\n            for (const pair of req.query.$options.split('&')) {\n                const key = pair.split('=')[0];\n                const value = pair.split('=')[1];\n                // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                options[key] =\n                    value == null || value === ''\n                        ? true\n                        : decodeURIComponent(value);\n            }\n        }\n        $.info(`传入 $options: ${JSON.stringify(options)}`);\n        Object.assign($options, options);\n    }\n\n    if (proxy) {\n        $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);\n    }\n\n    if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {\n        $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);\n    }\n    if (produceType) {\n        $.info(`指定生产类型: ${produceType}`);\n    }\n\n    if (includeUnsupportedProxy) {\n        $.info(\n            `包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,\n        );\n    }\n    if (\n        !includeUnsupportedProxy &&\n        shouldIncludeUnsupportedProxy(platform, req.headers)\n    ) {\n        includeUnsupportedProxy = true;\n        $.info(\n            `当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,\n        );\n    }\n    if (useMihomoExternal) {\n        $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);\n    }\n    if (noCache) {\n        $.info(`指定不使用缓存: ${noCache}`);\n    }\n\n    if (collection) {\n        try {\n            let output = await produceArtifact({\n                type: 'collection',\n                name,\n                platform,\n                ignoreFailedRemoteSub,\n                produceType,\n                produceOpts: {\n                    'include-unsupported-proxy': includeUnsupportedProxy,\n                    useMihomoExternal,\n                },\n                $options,\n                proxy,\n                noCache,\n                ua: reqUA,\n            });\n            let subUserInfoOfSub;\n            // forward flow header from the first subscription in this collection\n            const allSubs = $.read(SUBS_KEY);\n            const subnames = collection.subscriptions;\n            if (subnames.length > 0) {\n                const sub = findByName(allSubs, subnames[0]);\n                if (\n                    sub.source !== 'local' ||\n                    ['localFirst', 'remoteFirst'].includes(sub.mergeSources)\n                ) {\n                    try {\n                        let url =\n                            `${sub.url}`\n                                .split(/[\\r\\n]+/)\n                                .map((i) => i.trim())\n                                .filter((i) => i.length)?.[0] || '';\n\n                        let $arguments = {};\n                        const rawArgs = url.split('#');\n                        url = url.split('#')[0];\n                        if (rawArgs.length > 1) {\n                            try {\n                                // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n                                $arguments = JSON.parse(\n                                    decodeURIComponent(rawArgs[1]),\n                                );\n                            } catch (e) {\n                                for (const pair of rawArgs[1].split('&')) {\n                                    const key = pair.split('=')[0];\n                                    const value = pair.split('=')[1];\n                                    // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                                    $arguments[key] =\n                                        value == null || value === ''\n                                            ? true\n                                            : decodeURIComponent(value);\n                                }\n                            }\n                        }\n                        if (!$arguments.noFlow && /^https?:/.test(url)) {\n                            subUserInfoOfSub = await getFlowHeaders(\n                                $arguments?.insecure ? `${url}#insecure` : url,\n                                $arguments.flowUserAgent,\n                                undefined,\n                                proxy || sub.proxy || collection.proxy,\n                                $arguments.flowUrl,\n                            );\n                        }\n                    } catch (err) {\n                        $.error(\n                            `组合订阅 ${name} 中的子订阅 ${\n                                sub.name\n                            } 获取流量信息时发生错误: ${err.message ?? err}`,\n                        );\n                    }\n                }\n                if (sub.subUserinfo) {\n                    let subUserInfo;\n                    if (/^https?:\\/\\//.test(sub.subUserinfo)) {\n                        try {\n                            subUserInfo = await getFlowHeaders(\n                                undefined,\n                                undefined,\n                                undefined,\n                                proxy || sub.proxy,\n                                sub.subUserinfo,\n                            );\n                        } catch (e) {\n                            $.error(\n                                `组合订阅 ${name} 使用自定义流量链接 ${\n                                    sub.subUserinfo\n                                } 获取流量信息时发生错误: ${JSON.stringify(e)}`,\n                            );\n                        }\n                    } else {\n                        subUserInfo = sub.subUserinfo;\n                    }\n                    subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]\n                        .filter((i) => i)\n                        .join('; ');\n                }\n            }\n\n            $.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`);\n\n            let subUserInfoOfCol;\n            if (/^https?:\\/\\//.test(collection.subUserinfo)) {\n                try {\n                    subUserInfoOfCol = await getFlowHeaders(\n                        undefined,\n                        undefined,\n                        undefined,\n                        proxy || collection.proxy,\n                        collection.subUserinfo,\n                    );\n                } catch (e) {\n                    $.error(\n                        `组合订阅 ${name} 使用自定义流量链接 ${\n                            collection.subUserinfo\n                        } 获取流量信息时发生错误: ${JSON.stringify(e)}`,\n                    );\n                }\n            } else {\n                subUserInfoOfCol = collection.subUserinfo;\n            }\n            const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]\n                .filter((i) => i)\n                .join('; ');\n            if (subUserInfo) {\n                const headers = normalizeFlowHeader(subUserInfo, true);\n                if (headers?.['subscription-userinfo']) {\n                    res.set(\n                        'subscription-userinfo',\n                        headers['subscription-userinfo'],\n                    );\n                }\n                if (headers?.['profile-web-page-url']) {\n                    res.set(\n                        'profile-web-page-url',\n                        headers['profile-web-page-url'],\n                    );\n                }\n                if (headers?.['plan-name']) {\n                    res.set('plan-name', headers['plan-name']);\n                }\n            }\n            if (platform === 'JSON') {\n                if (resultFormat === 'nezha') {\n                    output = nezhaTransform(output);\n                } else if (resultFormat === 'nezha-monitor') {\n                    nezhaIndex = /^\\d+$/.test(nezhaIndex)\n                        ? parseInt(nezhaIndex, 10)\n                        : output.findIndex((i) => i.name === nezhaIndex);\n                    output = await nezhaMonitor(\n                        output[nezhaIndex],\n                        nezhaIndex,\n                        req.query,\n                    );\n                }\n                res.set('Content-Type', 'application/json;charset=utf-8');\n            } else {\n                res.set('Content-Type', 'text/plain; charset=utf-8');\n            }\n            if ($options?._res?.headers) {\n                Object.entries($options._res.headers).forEach(\n                    ([key, value]) => {\n                        if (value == null) {\n                            res.removeHeader(key);\n                        } else {\n                            res.set(key, value);\n                        }\n                    },\n                );\n            }\n            if ($options?._res?.status) {\n                res.status($options._res.status);\n            }\n            res.send(output);\n        } catch (err) {\n            $.notify(\n                `🌍 Sub-Store 下载组合订阅失败`,\n                `❌ 下载组合订阅错误：${name}！`,\n                `🤔 原因：${err}`,\n            );\n            failed(\n                res,\n                new InternalServerError(\n                    'INTERNAL_SERVER_ERROR',\n                    `Failed to download collection: ${name}`,\n                    `Reason: ${err.message ?? err}`,\n                ),\n            );\n        }\n    } else {\n        $.error(\n            `🌍 Sub-Store 下载组合订阅失败`,\n            `❌ 未找到组合订阅：${name}！`,\n        );\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Collection ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nasync function nezhaMonitor(proxy, index, query) {\n    const result = {\n        code: 0,\n        message: 'success',\n        result: [],\n    };\n\n    try {\n        const { isLoon, isSurge } = $.env;\n        if (!isLoon && !isSurge)\n            throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)');\n        const node = ProxyUtils.produce([proxy], isLoon ? 'Loon' : 'Surge');\n        if (!node) throw new Error('当前客户端不兼容此节点');\n        const monitors = proxy._monitors || [\n            {\n                name: 'Cloudflare',\n                url: 'http://cp.cloudflare.com/generate_204',\n                method: 'HEAD',\n                number: 3,\n                timeout: 2000,\n            },\n            {\n                name: 'Google',\n                url: 'http://www.google.com/generate_204',\n                method: 'HEAD',\n                number: 3,\n                timeout: 2000,\n            },\n        ];\n        const number =\n            query.number || Math.max(...monitors.map((i) => i.number)) || 3;\n        for (const monitor of monitors) {\n            const interval = 10 * 60 * 1000;\n            const data = {\n                monitor_id: monitors.indexOf(monitor),\n                server_id: index,\n                monitor_name: monitor.name,\n                server_name: proxy.name,\n                created_at: [],\n                avg_delay: [],\n            };\n            for (let index = 0; index < number; index++) {\n                const startedAt = Date.now();\n                try {\n                    await $.http[(monitor.method || 'HEAD').toLowerCase()]({\n                        timeout: monitor.timeout || 2000,\n                        url: monitor.url,\n                        'policy-descriptor': node,\n                        node,\n                    });\n                    const latency = Date.now() - startedAt;\n                    $.info(`${monitor.name} latency: ${latency}`);\n                    data.avg_delay.push(latency);\n                } catch (e) {\n                    $.error(e);\n                    data.avg_delay.push(0);\n                }\n\n                data.created_at.push(\n                    Date.now() - interval * (monitor.number - index - 1),\n                );\n            }\n\n            result.result.push(data);\n        }\n    } catch (e) {\n        $.error(e);\n        result.result.push({\n            monitor_id: 0,\n            server_id: 0,\n            monitor_name: `❌ ${e.message ?? e}`,\n            server_name: proxy.name,\n            created_at: [Date.now()],\n            avg_delay: [0],\n        });\n    }\n\n    return JSON.stringify(result, null, 2);\n}\nfunction nezhaTransform(output) {\n    const result = {\n        code: 0,\n        message: 'success',\n        result: [],\n    };\n    output.map((proxy, index) => {\n        // 如果节点上有数据 就取节点上的数据\n        let CountryCode = proxy._geo?.countryCode || proxy._geo?.country;\n        // 简单判断下\n        if (!/^[a-z]{2}$/i.test(CountryCode)) {\n            CountryCode = getISO(proxy.name);\n        }\n        // 简单判断下\n        if (/^[a-z]{2}$/i.test(CountryCode)) {\n            // 如果节点上有数据 就取节点上的数据\n            let now = Math.round(new Date().getTime() / 1000);\n            let time = proxy._unavailable ? 0 : now;\n\n            const uptime = parseInt(proxy._uptime || 0, 10);\n\n            result.result.push({\n                id: index,\n                name: proxy.name,\n                tag: `${proxy._tag ?? ''}`,\n                last_active: time,\n                // 暂时不用处理 现在 VPings App 端的接口支持域名查询\n                // 其他场景使用 自己在 Sub-Store 加一步域名解析\n                valid_ip: proxy._IP || proxy.server,\n                ipv4: proxy._IPv4 || proxy.server,\n                ipv6: proxy._IPv6 || (isIPv6(proxy.server) ? proxy.server : ''),\n                host: {\n                    Platform: 'Sub-Store',\n                    PlatformVersion: env.version,\n                    CPU: [],\n                    MemTotal: 1024,\n                    DiskTotal: 1024,\n                    SwapTotal: 1024,\n                    Arch: '',\n                    Virtualization: '',\n                    BootTime: now - uptime,\n                    CountryCode, // 目前需要\n                    Version: '0.0.1',\n                },\n                status: {\n                    CPU: 0,\n                    MemUsed: 0,\n                    SwapUsed: 0,\n                    DiskUsed: 0,\n                    NetInTransfer: 0,\n                    NetOutTransfer: 0,\n                    NetInSpeed: 0,\n                    NetOutSpeed: 0,\n                    Uptime: uptime,\n                    Load1: 0,\n                    Load5: 0,\n                    Load15: 0,\n                    TcpConnCount: 0,\n                    UdpConnCount: 0,\n                    ProcessCount: 0,\n                },\n            });\n        }\n    });\n    return JSON.stringify(result, null, 2);\n}\n"
  },
  {
    "path": "backend/src/restful/errors/index.js",
    "content": "class BaseError {\n    constructor(code, message, details) {\n        this.code = code;\n        this.message = message;\n        this.details = details;\n    }\n}\n\nexport class InternalServerError extends BaseError {\n    constructor(code, message, details) {\n        super(code, message, details);\n        this.type = 'InternalServerError';\n    }\n}\n\nexport class RequestInvalidError extends BaseError {\n    constructor(code, message, details) {\n        super(code, message, details);\n        this.type = 'RequestInvalidError';\n    }\n}\n\nexport class ResourceNotFoundError extends BaseError {\n    constructor(code, message, details) {\n        super(code, message, details);\n        this.type = 'ResourceNotFoundError';\n    }\n}\n\nexport class NetworkError extends BaseError {\n    constructor(code, message, details) {\n        super(code, message, details);\n        this.type = 'NetworkError';\n    }\n}\n"
  },
  {
    "path": "backend/src/restful/file.js",
    "content": "import { deleteByName, findByName, updateByName } from '@/utils/database';\nimport { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';\nimport { FILES_KEY, ARTIFACTS_KEY } from '@/constants';\nimport { failed, success } from '@/restful/response';\nimport $ from '@/core/app';\nimport {\n    RequestInvalidError,\n    ResourceNotFoundError,\n    InternalServerError,\n} from '@/restful/errors';\nimport { produceArtifact } from '@/restful/sync';\nimport { formatDateTime } from '@/utils';\n\nexport default function register($app) {\n    if (!$.read(FILES_KEY)) $.write([], FILES_KEY);\n\n    $app.get('/share/file/:name', getFile);\n\n    $app.route('/api/file/:name')\n        .get(getFile)\n        .patch(updateFile)\n        .delete(deleteFile);\n\n    $app.route('/api/wholeFile/:name').get(getWholeFile);\n\n    $app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);\n    $app.route('/api/wholeFiles').get(getAllWholeFiles);\n}\n\n// file API\nfunction createFile(req, res) {\n    const file = req.body;\n    file.name = `${file.name ?? Date.now()}`;\n    $.info(`正在创建文件：${file.name}`);\n    const allFiles = $.read(FILES_KEY);\n    if (findByName(allFiles, file.name)) {\n        return failed(\n            res,\n            new RequestInvalidError(\n                'DUPLICATE_KEY',\n                req.body.name\n                    ? `已存在 name 为 ${file.name} 的文件`\n                    : `无法同时创建相同的文件 可稍后重试`,\n            ),\n        );\n    }\n    allFiles.push(file);\n    $.write(allFiles, FILES_KEY);\n    success(res, file, 201);\n}\n\nasync function getFile(req, res, next) {\n    let { name } = req.params;\n    const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];\n    $.info(`正在下载文件：${name}\\n请求 User-Agent: ${reqUA}`);\n    let {\n        url,\n        subInfoUrl,\n        subInfoUserAgent,\n        ua,\n        content,\n        mergeSources,\n        ignoreFailedRemoteFile,\n        proxy,\n        noCache,\n        produceType,\n    } = req.query;\n    let $options = {\n        _req: {\n            method: req.method,\n            url: req.url,\n            path: req.path,\n            query: req.query,\n            params: req.params,\n            headers: req.headers,\n            body: req.body,\n        },\n    };\n    if (req.query.$options) {\n        let options = {};\n        try {\n            // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n            options = JSON.parse(decodeURIComponent(req.query.$options));\n        } catch (e) {\n            for (const pair of req.query.$options.split('&')) {\n                const key = pair.split('=')[0];\n                const value = pair.split('=')[1];\n                // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                options[key] =\n                    value == null || value === ''\n                        ? true\n                        : decodeURIComponent(value);\n            }\n        }\n        $.info(`传入 $options: ${JSON.stringify(options)}`);\n        Object.assign($options, options);\n    }\n    if (url) {\n        $.info(`指定远程文件 URL: ${url}`);\n    }\n    if (proxy) {\n        $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);\n    }\n    if (ua) {\n        $.info(`指定远程文件 User-Agent: ${ua}`);\n    }\n    if (subInfoUrl) {\n        $.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);\n    }\n    if (subInfoUserAgent) {\n        $.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);\n    }\n    if (content) {\n        $.info(`指定本地文件: ${content}`);\n    }\n    if (mergeSources) {\n        $.info(`指定合并来源: ${mergeSources}`);\n    }\n    if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {\n        $.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);\n    }\n    if (noCache) {\n        $.info(`指定不使用缓存: ${noCache}`);\n    }\n    if (produceType) {\n        $.info(`指定生产类型: ${produceType}`);\n    }\n\n    const allFiles = $.read(FILES_KEY);\n    const file = findByName(allFiles, name);\n    if (file) {\n        try {\n            const output = await produceArtifact({\n                type: 'file',\n                name,\n                url,\n                ua,\n                content,\n                mergeSources,\n                ignoreFailedRemoteFile,\n                $options,\n                proxy,\n                noCache,\n                produceType,\n                all: true,\n            });\n\n            try {\n                subInfoUrl = subInfoUrl || file.subInfoUrl;\n                if (subInfoUrl) {\n                    // forward flow headers\n                    const flowInfo = await getFlowHeaders(\n                        subInfoUrl,\n                        subInfoUserAgent || file.subInfoUserAgent,\n                        undefined,\n                        proxy || file.proxy,\n                    );\n                    if (flowInfo) {\n                        const headers = normalizeFlowHeader(flowInfo, true);\n                        if (headers?.['subscription-userinfo']) {\n                            res.set(\n                                'subscription-userinfo',\n                                headers['subscription-userinfo'],\n                            );\n                        }\n                        if (headers?.['profile-web-page-url']) {\n                            res.set(\n                                'profile-web-page-url',\n                                headers['profile-web-page-url'],\n                            );\n                        }\n                        if (headers?.['plan-name']) {\n                            res.set('plan-name', headers['plan-name']);\n                        }\n                    }\n                }\n            } catch (err) {\n                $.error(\n                    `文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(\n                        err,\n                    )}`,\n                );\n            }\n            if (file.download) {\n                res.set(\n                    'Content-Disposition',\n                    `attachment; filename*=UTF-8''${encodeURIComponent(\n                        file.displayName || file.name,\n                    )}`,\n                );\n            }\n            res.set('Content-Type', 'text/plain; charset=utf-8');\n            if (output?.$options?._res?.headers) {\n                Object.entries(output.$options._res.headers).forEach(\n                    ([key, value]) => {\n                        if (value == null) {\n                            res.removeHeader(key);\n                        } else {\n                            res.set(key, value);\n                        }\n                    },\n                );\n            }\n            if (output?.$options?._res?.status) {\n                res.status(output.$options._res.status);\n            }\n            res.send(output?.$content ?? '');\n        } catch (err) {\n            $.notify(\n                `🌍 Sub-Store 下载文件失败`,\n                `❌ 无法下载文件：${name}！`,\n                `🤔 原因：${err.message ?? err}`,\n            );\n            $.error(err.message ?? err);\n            failed(\n                res,\n                new InternalServerError(\n                    'INTERNAL_SERVER_ERROR',\n                    `Failed to download file: ${name}`,\n                    `Reason: ${err.message ?? err}`,\n                ),\n            );\n        }\n    } else {\n        $.error(`🌍 Sub-Store 下载文件失败\\n❌ 未找到文件：${name}！`);\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `File ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\nfunction getWholeFile(req, res) {\n    let { name } = req.params;\n    let { raw } = req.query;\n    const allFiles = $.read(FILES_KEY);\n    const file = findByName(allFiles, name);\n    if (file) {\n        if (raw) {\n            res.set('content-type', 'application/json')\n                .set(\n                    'content-disposition',\n                    `attachment; filename=\"${encodeURIComponent(\n                        `sub-store_file_${name}_${formatDateTime(\n                            new Date(),\n                        )}.json`,\n                    )}\"`,\n                )\n                .send(JSON.stringify(file));\n        } else {\n            success(res, file);\n        }\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                `FILE_NOT_FOUND`,\n                `File ${name} does not exist`,\n                404,\n            ),\n        );\n    }\n}\n\nfunction updateFile(req, res) {\n    let { name } = req.params;\n    let file = req.body;\n    const allFiles = $.read(FILES_KEY);\n    const oldFile = findByName(allFiles, name);\n    if (oldFile) {\n        if (!file.name) file.name = oldFile.name;\n        const newFile = {\n            ...oldFile,\n            ...file,\n        };\n        $.info(`正在更新文件：${name}...`);\n\n        if (name !== newFile.name) {\n            // update all artifacts referring this collection\n            const allArtifacts = $.read(ARTIFACTS_KEY) || [];\n            for (const artifact of allArtifacts) {\n                if (\n                    artifact.type === 'file' &&\n                    artifact.source === oldFile.name\n                ) {\n                    artifact.source = newFile.name;\n                }\n            }\n            $.write(allArtifacts, ARTIFACTS_KEY);\n        }\n\n        updateByName(allFiles, name, newFile);\n        $.write(allFiles, FILES_KEY);\n        success(res, newFile);\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `File ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nfunction deleteFile(req, res) {\n    let { name } = req.params;\n    $.info(`正在删除文件：${name}`);\n    let allFiles = $.read(FILES_KEY);\n    deleteByName(allFiles, name);\n    $.write(allFiles, FILES_KEY);\n    success(res);\n}\n\nfunction getAllFiles(req, res) {\n    const allFiles = $.read(FILES_KEY);\n    success(\n        res, // eslint-disable-next-line no-unused-vars\n        allFiles.map(({ content, ...rest }) => rest),\n    );\n}\n\nfunction getAllWholeFiles(req, res) {\n    const allFiles = $.read(FILES_KEY);\n    success(res, allFiles);\n}\n\nfunction replaceFile(req, res) {\n    const allFiles = req.body;\n    $.write(allFiles, FILES_KEY);\n    success(res);\n}\n"
  },
  {
    "path": "backend/src/restful/index.js",
    "content": "import { Base64 } from 'js-base64';\nimport _ from 'lodash';\nimport express from '@/vendor/express';\nimport $ from '@/core/app';\nimport migrate from '@/utils/migration';\nimport download, { downloadFile } from '@/utils/download';\nimport { syncArtifacts, produceArtifact } from '@/restful/sync';\nimport { gistBackupAction } from '@/restful/miscs';\nimport { TOKENS_KEY, SETTINGS_KEY } from '@/constants';\n\nimport registerSubscriptionRoutes from './subscriptions';\nimport registerCollectionRoutes from './collections';\nimport registerArtifactRoutes from './artifacts';\nimport registerFileRoutes from './file';\nimport registerTokenRoutes from './token';\nimport registerModuleRoutes from './module';\nimport registerSyncRoutes from './sync';\nimport registerDownloadRoutes from './download';\nimport registerSettingRoutes from './settings';\nimport registerPreviewRoutes from './preview';\nimport registerSortingRoutes from './sort';\nimport registerMiscRoutes from './miscs';\nimport registerNodeInfoRoutes from './node-info';\nimport registerParserRoutes from './parser';\n\nexport default function serve() {\n    let port;\n    let host;\n    if ($.env.isNode) {\n        port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;\n        host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';\n    }\n    const $app = express({ substore: $, port, host });\n    if ($.env.isNode) {\n        const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');\n        const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');\n        const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');\n        const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');\n        if (be_prefix || be_merge) {\n            if (!fe_be_path.startsWith('/')) {\n                throw new Error(\n                    'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',\n                );\n            }\n            if (be_merge) {\n                $.info(`[BACKEND] MERGE mode is [ON].`);\n                $.info(`[BACKEND && FRONTEND] ${host}:${port}`);\n            }\n            $.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);\n            $app.use((req, res, next) => {\n                if (req.path.startsWith(fe_be_path)) {\n                    req.url = req.url.replace(fe_be_path, '') || '/';\n                    if (be_merge && req.url.startsWith('/api/')) {\n                        req.query['share'] = 'true';\n                    }\n                    next();\n                    return;\n                }\n                const pathname =\n                    decodeURIComponent(req._parsedUrl.pathname) || '/';\n                if (\n                    be_merge &&\n                    req.path.startsWith('/share/') &&\n                    req.query.token\n                ) {\n                    if (req.method.toLowerCase() !== 'get') {\n                        res.status(405).send('Method not allowed');\n                        return;\n                    }\n                    const tokens = $.read(TOKENS_KEY) || [];\n                    const token = tokens.find(\n                        (t) =>\n                            t.token === req.query.token &&\n                            (`/share/${t.type}/${t.name}` === pathname ||\n                                pathname.startsWith(\n                                    `/share/${t.type}/${t.name}/`,\n                                )) &&\n                            (t.exp == null || t.exp > Date.now()),\n                    );\n                    if (token) {\n                        next();\n                        return;\n                    } else {\n                        const settings = $.read(SETTINGS_KEY);\n                        if (settings?.appearanceSetting?.invalidShareFakeNode) {\n                            req.query._fakeNode = true;\n                            req.url = req.url.replace(\n                                /\\/share\\/.*?\\//,\n                                '/share/sub/',\n                            );\n                            next();\n                            return;\n                        }\n                    }\n                }\n                const isBackendRoute = /^\\/(api|download|share)(\\/|$)/.test(\n                    req.path,\n                );\n                if (be_merge && fe_path && !isBackendRoute) {\n                    const express_ = eval(`require(\"express\")`);\n                    const mime_ = eval(`require(\"mime-types\")`);\n                    const path_ = eval(`require(\"path\")`);\n                    const fs_ = eval(`require(\"fs\")`);\n                    // 检查请求的文件是否真实存在，不存在则返回 index.html（SPA 路由）\n                    const filePath = path_.join(fe_path, req.path);\n                    if (!fs_.existsSync(filePath)) {\n                        req.url = '/index.html';\n                    }\n                    const staticFileMiddleware = express_.static(fe_path, {\n                        setHeaders: (res, path) => {\n                            const type = mime_.contentType(path_.extname(path));\n                            if (type) {\n                                res.set('Content-Type', type);\n                            }\n                        },\n                    });\n                    staticFileMiddleware(req, res, next);\n                    return;\n                }\n                res.status(404).end();\n                return;\n            });\n        }\n    }\n    // register routes\n    registerCollectionRoutes($app);\n    registerSubscriptionRoutes($app);\n    registerDownloadRoutes($app);\n    registerPreviewRoutes($app);\n    registerSortingRoutes($app);\n    registerSettingRoutes($app);\n    registerArtifactRoutes($app);\n    registerFileRoutes($app);\n    registerTokenRoutes($app);\n    registerModuleRoutes($app);\n    registerSyncRoutes($app);\n    registerNodeInfoRoutes($app);\n    registerMiscRoutes($app);\n    registerParserRoutes($app);\n\n    $app.start();\n\n    if ($.env.isNode) {\n        // Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON\n        const backend_sync_cron = eval(\n            'process.env.SUB_STORE_BACKEND_SYNC_CRON',\n        );\n\n        if (backend_sync_cron) {\n            $.info(`[SYNC CRON] ${backend_sync_cron} enabled`);\n            const { CronJob } = eval(`require(\"cron\")`);\n            new CronJob(\n                backend_sync_cron,\n                async function () {\n                    try {\n                        $.info(`[SYNC CRON] ${backend_sync_cron} started`);\n                        await syncArtifacts();\n                        $.info(`[SYNC CRON] ${backend_sync_cron} finished`);\n                    } catch (e) {\n                        $.error(\n                            `[SYNC CRON] ${backend_sync_cron} error: ${\n                                e.message ?? e\n                            }`,\n                        );\n                    }\n                }, // onTick\n                null, // onComplete\n                true, // start\n                // 'Asia/Shanghai' // timeZone\n            );\n        } else {\n            if (eval('process.env.SUB_STORE_BACKEND_CRON')) {\n                $.error(\n                    `[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,\n                );\n            }\n            if (eval('process.env.SUB_STORE_CRON')) {\n                $.error(\n                    `[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,\n                );\n            }\n        }\n        // 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b\n        // 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b\n        const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');\n        if (produce_cron) {\n            $.info(`[PRODUCE CRON] ${produce_cron} enabled`);\n            const { CronJob } = eval(`require(\"cron\")`);\n            produce_cron\n                .split(/\\s*;\\s*/)\n                .map((item) => item.trim())\n                .filter((item) => item.length > 0)\n                .forEach((item) => {\n                    const [cron, type, name] = item.split(/\\s*,\\s*/);\n                    $.info(`[PRODUCE CRON] ${type} ${name} ${cron} scheduled`);\n                    new CronJob(\n                        cron.trim(),\n                        async function () {\n                            try {\n                                $.info(\n                                    `[PRODUCE CRON] ${type} ${name} ${cron} started`,\n                                );\n                                await produceArtifact({ type, name });\n                                $.info(\n                                    `[PRODUCE CRON] ${type} ${name} ${cron} finished`,\n                                );\n                            } catch (e) {\n                                $.error(\n                                    `[PRODUCE CRON] ${type} ${name} ${cron} error: ${\n                                        e.message ?? e\n                                    }`,\n                                );\n                            }\n                        }, // onTick\n                        null, // onComplete\n                        true, // start\n                        // 'Asia/Shanghai' // timeZone\n                    );\n                });\n        }\n        const backend_download_cron = eval(\n            'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',\n        );\n        if (backend_download_cron) {\n            $.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`);\n            const { CronJob } = eval(`require(\"cron\")`);\n            new CronJob(\n                backend_download_cron,\n                async function () {\n                    try {\n                        $.info(\n                            `[DOWNLOAD CRON] ${backend_download_cron} started`,\n                        );\n                        await gistBackupAction('download');\n                        $.info(\n                            `[DOWNLOAD CRON] ${backend_download_cron} finished`,\n                        );\n                    } catch (e) {\n                        $.error(\n                            `[DOWNLOAD CRON] ${backend_download_cron} error: ${\n                                e.message ?? e\n                            }`,\n                        );\n                    }\n                }, // onTick\n                null, // onComplete\n                true, // start\n                // 'Asia/Shanghai' // timeZone\n            );\n        }\n        const backend_upload_cron = eval(\n            'process.env.SUB_STORE_BACKEND_UPLOAD_CRON',\n        );\n        if (backend_upload_cron) {\n            $.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`);\n            const { CronJob } = eval(`require(\"cron\")`);\n            new CronJob(\n                backend_upload_cron,\n                async function () {\n                    try {\n                        $.info(`[UPLOAD CRON] ${backend_upload_cron} started`);\n                        await gistBackupAction('upload');\n                        $.info(`[UPLOAD CRON] ${backend_upload_cron} finished`);\n                    } catch (e) {\n                        $.error(\n                            `[UPLOAD CRON] ${backend_upload_cron} error: ${\n                                e.message ?? e\n                            }`,\n                        );\n                    }\n                }, // onTick\n                null, // onComplete\n                true, // start\n                // 'Asia/Shanghai' // timeZone\n            );\n        }\n        const mmdb_cron = eval('process.env.SUB_STORE_MMDB_CRON');\n        const countryFile = eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');\n        const countryUrl = eval('process.env.SUB_STORE_MMDB_COUNTRY_URL');\n        const asnFile = eval('process.env.SUB_STORE_MMDB_ASN_PATH');\n        const asnUrl = eval('process.env.SUB_STORE_MMDB_ASN_URL');\n        if (mmdb_cron && ((countryFile && countryUrl) || (asnFile && asnUrl))) {\n            $.info(`[MMDB CRON] ${mmdb_cron} enabled`);\n            const { CronJob } = eval(`require(\"cron\")`);\n            new CronJob(\n                mmdb_cron,\n                async function () {\n                    try {\n                        $.info(`[MMDB CRON] ${mmdb_cron} started`);\n                        if (countryFile && countryUrl) {\n                            try {\n                                $.info(\n                                    `[MMDB CRON] downloading ${countryUrl} to ${countryFile}`,\n                                );\n                                await downloadFile(countryUrl, countryFile);\n                            } catch (e) {\n                                $.error(\n                                    `[MMDB CRON] ${countryUrl} download failed: ${\n                                        e.message ?? e\n                                    }`,\n                                );\n                            }\n                        }\n                        if (asnFile && asnUrl) {\n                            try {\n                                $.info(\n                                    `[MMDB CRON] downloading ${asnUrl} to ${asnFile}`,\n                                );\n                                await downloadFile(asnUrl, asnFile);\n                            } catch (e) {\n                                $.error(\n                                    `[MMDB CRON] ${asnUrl} download failed: ${\n                                        e.message ?? e\n                                    }`,\n                                );\n                            }\n                        }\n\n                        $.info(`[MMDB CRON] ${mmdb_cron} finished`);\n                    } catch (e) {\n                        $.error(\n                            `[MMDB CRON] ${mmdb_cron} error: ${e.message ?? e}`,\n                        );\n                    }\n                }, // onTick\n                null, // onComplete\n                true, // start\n                // 'Asia/Shanghai' // timeZone\n            );\n        }\n        const path = eval(`require(\"path\")`);\n        const fs = eval(`require(\"fs\")`);\n        const data_url = eval('process.env.SUB_STORE_DATA_URL');\n        const data_url_post = eval('process.env.SUB_STORE_DATA_URL_POST');\n        const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');\n        const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;\n        const fe_host =\n            eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';\n        const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');\n        const fe_abs_path = path.resolve(\n            fe_path || path.join(__dirname, 'frontend'),\n        );\n        const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');\n        if (fe_path && !be_merge) {\n            try {\n                fs.accessSync(path.join(fe_abs_path, 'index.html'));\n            } catch (e) {\n                $.error(\n                    `[FRONTEND] index.html file not found in ${fe_abs_path}`,\n                );\n            }\n\n            const express_ = eval(`require(\"express\")`);\n            const history = eval(`require(\"connect-history-api-fallback\")`);\n            const { createProxyMiddleware } = eval(\n                `require(\"http-proxy-middleware\")`,\n            );\n\n            const app = express_();\n\n            const staticFileMiddleware = express_.static(fe_path);\n\n            let be_api = '/api/';\n            let be_download = '/download/';\n            let be_share = '/share/';\n            let be_download_rewrite = '';\n            let be_api_rewrite = '';\n            let be_share_rewrite = `${be_share}:type/:name`;\n            let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')\n                ? fe_be_path\n                : '';\n            if (fe_be_path) {\n                if (!fe_be_path.startsWith('/')) {\n                    throw new Error(\n                        'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',\n                    );\n                }\n                be_api_rewrite = `${\n                    fe_be_path === '/' ? '' : fe_be_path\n                }${be_api}`;\n                be_download_rewrite = `${\n                    fe_be_path === '/' ? '' : fe_be_path\n                }${be_download}`;\n\n                app.use(\n                    be_share_rewrite,\n                    createProxyMiddleware({\n                        target: `http://127.0.0.1:${port}${prefix}`,\n                        changeOrigin: true,\n                        pathRewrite: async (path, req) => {\n                            if (req.method.toLowerCase() !== 'get')\n                                throw new Error('Method not allowed');\n                            const tokens = $.read(TOKENS_KEY) || [];\n                            const token = tokens.find(\n                                (t) =>\n                                    t.token === req.query.token &&\n                                    t.type === req.params.type &&\n                                    t.name === req.params.name &&\n                                    (t.exp == null || t.exp > Date.now()),\n                            );\n                            if (!token) {\n                                const settings = $.read(SETTINGS_KEY);\n                                if (\n                                    settings?.appearanceSetting\n                                        ?.invalidShareFakeNode\n                                ) {\n                                    return req.originalUrl\n                                        .replace(\n                                            /\\/share\\/.*?\\//,\n                                            '/share/sub/',\n                                        )\n                                        .replace('?', '?_fakeNode=true&');\n                                } else {\n                                    return '/404';\n                                }\n                            }\n                            return req.originalUrl;\n                        },\n                    }),\n                );\n                app.use(\n                    be_api_rewrite,\n                    createProxyMiddleware({\n                        target: `http://127.0.0.1:${port}${prefix}${be_api}`,\n                        pathRewrite: async (path) => {\n                            return path.includes('?')\n                                ? `${path}&share=true`\n                                : `${path}?share=true`;\n                        },\n                    }),\n                );\n                app.use(\n                    be_download_rewrite,\n                    createProxyMiddleware({\n                        target: `http://127.0.0.1:${port}${prefix}${be_download}`,\n                        changeOrigin: true,\n                    }),\n                );\n            }\n\n            app.use(staticFileMiddleware);\n            app.use(\n                history({\n                    disableDotRule: true,\n                    verbose: false,\n                }),\n            );\n            app.use(staticFileMiddleware);\n\n            const listener = app.listen(fe_port, fe_host, () => {\n                const { address: fe_address, port: fe_port } =\n                    listener.address();\n                $.info(`[FRONTEND] ${fe_address}:${fe_port}`);\n                if (fe_be_path) {\n                    $.info(\n                        `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,\n                    );\n                    $.info(\n                        `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${be_download}`,\n                    );\n                    $.info(\n                        `[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,\n                    );\n                }\n            });\n        }\n        if (data_url) {\n            $.info(`[BACKEND] downloading data from ${data_url}`);\n            download(data_url)\n                .then(async (content) => {\n                    try {\n                        content = JSON.parse(Base64.decode(content));\n                        if (!(Object.keys(content.settings).length >= 0)) {\n                            throw new Error(\n                                '备份文件应该至少包含 settings 字段',\n                            );\n                        }\n                    } catch (err) {\n                        try {\n                            content = JSON.parse(content);\n                            if (!(Object.keys(content.settings).length >= 0)) {\n                                throw new Error(\n                                    '备份文件应该至少包含 settings 字段',\n                                );\n                            }\n                        } catch (err) {\n                            $.error(\n                                `Gist 备份文件校验失败, 无法还原\\nReason: ${\n                                    err.message ?? err\n                                }`,\n                            );\n                            throw new Error('Gist 备份文件校验失败, 无法还原');\n                        }\n                    }\n                    if (data_url_post) {\n                        $.info('[BACKEND] executing post-processing script');\n                        eval(data_url_post);\n                    }\n\n                    $.write(JSON.stringify(content, null, `  `), '#sub-store');\n\n                    $.cache = content;\n                    $.persistCache();\n\n                    migrate();\n                    $.info(`[BACKEND] restored data from ${data_url}`);\n                })\n                .catch((e) => {\n                    $.error(`[BACKEND] restore data failed`);\n                    console.error(e);\n                    throw e;\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "backend/src/restful/miscs.js",
    "content": "import { Base64 } from 'js-base64';\nimport _ from 'lodash';\nimport $ from '@/core/app';\nimport { ENV } from '@/vendor/open-api';\nimport { failed, success } from '@/restful/response';\nimport { updateArtifactStore, updateAvatar } from '@/restful/settings';\nimport resourceCache from '@/utils/resource-cache';\nimport scriptResourceCache from '@/utils/script-resource-cache';\nimport headersResourceCache from '@/utils/headers-resource-cache';\nimport {\n    GIST_BACKUP_FILE_NAME,\n    GIST_BACKUP_KEY,\n    SETTINGS_KEY,\n} from '@/constants';\nimport { InternalServerError, RequestInvalidError } from '@/restful/errors';\nimport Gist from '@/utils/gist';\nimport migrate from '@/utils/migration';\nimport env from '@/utils/env';\nimport { formatDateTime } from '@/utils';\n\nexport default function register($app) {\n    // utils\n    $app.get('/api/utils/env', getEnv); // get runtime environment\n    $app.get('/api/utils/backup', gistBackup); // gist backup actions\n    $app.get('/api/utils/refresh', refresh);\n\n    // Storage management\n    $app.route('/api/storage')\n        .get((req, res) => {\n            res.set('content-type', 'application/json')\n                .set(\n                    'content-disposition',\n                    `attachment; filename=\"${encodeURIComponent(\n                        `sub-store_data_${formatDateTime(new Date())}.json`,\n                    )}\"`,\n                )\n                .send(\n                    $.env.isNode\n                        ? JSON.stringify($.cache)\n                        : $.read('#sub-store'),\n                );\n        })\n        .post((req, res) => {\n            let { content } = req.body;\n            try {\n                content = JSON.parse(Base64.decode(content));\n                if (!(Object.keys(content.settings).length >= 0)) {\n                    throw new Error('备份文件应该至少包含 settings 字段');\n                }\n            } catch (err) {\n                try {\n                    content = JSON.parse(content);\n                    if (!(Object.keys(content.settings).length >= 0)) {\n                        throw new Error('备份文件应该至少包含 settings 字段');\n                    }\n                } catch (err) {\n                    $.error(\n                        `备份文件校验失败, 无法还原\\nReason: ${\n                            err.message ?? err\n                        }`,\n                    );\n                    throw new Error('备份文件校验失败, 无法还原');\n                }\n            }\n            $.write(JSON.stringify(content, null, `  `), '#sub-store');\n            if ($.env.isNode) {\n                $.cache = content;\n                $.persistCache();\n            }\n            migrate();\n            success(res);\n        });\n\n    if (ENV().isNode) {\n        $app.get('/', getEnv);\n    } else {\n        // Redirect sub.store to vercel webpage\n        $app.get('/', async (req, res) => {\n            // 302 redirect\n            res.set('location', 'https://sub-store.vercel.app/')\n                .status(302)\n                .end();\n        });\n    }\n\n    // handle preflight request for QX\n    if (ENV().isQX) {\n        $app.options('/', async (req, res) => {\n            res.status(200).end();\n        });\n    }\n\n    $app.all('/', (_, res) => {\n        res.send('Hello from sub-store, made with ❤️ by Peng-YM');\n    });\n}\n\nfunction getEnv(req, res) {\n    if (req.query.share) {\n        env.feature.share = true;\n    }\n    res.set('Content-Type', 'application/json;charset=UTF-8').send(\n        JSON.stringify(\n            {\n                status: 'success',\n                data: {\n                    guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',\n                    ...env,\n                },\n            },\n            null,\n            2,\n        ),\n    );\n}\n\nasync function refresh(_, res) {\n    // 1. get GitHub avatar and artifact store\n    await updateAvatar();\n    await updateArtifactStore();\n\n    // 2. clear resource cache\n    resourceCache.revokeAll();\n    scriptResourceCache.revokeAll();\n    headersResourceCache.revokeAll();\n    success(res);\n}\n\nasync function gistBackupAction(action, keep, encode) {\n    // read token\n    const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);\n    if (!gistToken) throw new Error('GitHub Token is required for backup!');\n\n    const gist = new Gist({\n        token: gistToken,\n        key: GIST_BACKUP_KEY,\n        syncPlatform,\n    });\n    let currentContent = $.read('#sub-store');\n    currentContent = currentContent ? JSON.parse(currentContent) : {};\n    if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache));\n    let content;\n    const settings = $.read(SETTINGS_KEY);\n    const updated = settings.syncTime;\n\n    const encoding = encode || settings.gistUpload || 'base64';\n    $.info(\n        `Gist backup action: ${action}, keep: ${keep}, encode: ${encode}, settings encode: ${settings.gistUpload}, final encoding: ${encoding}`,\n    );\n    switch (action) {\n        case 'upload':\n            try {\n                content = $.read('#sub-store');\n                content = content ? JSON.parse(content) : {};\n                if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));\n                if (encoding === 'plaintext') {\n                    content.settings.gistToken =\n                        '恢复后请重新设置 GitHub Token';\n                    content = JSON.stringify(content, null, `  `);\n                } else {\n                    content = Base64.encode(\n                        JSON.stringify(content, null, `  `),\n                    );\n                }\n\n                $.info(`下载备份, 与本地内容对比...`);\n                const onlineContent = await gist.download(\n                    GIST_BACKUP_FILE_NAME,\n                );\n                if (onlineContent === content) {\n                    $.info(`内容一致, 无需上传备份`);\n                    return;\n                }\n            } catch (error) {\n                $.error(`${error.message ?? error}`);\n            }\n\n            // update syncTime\n            settings.syncTime = new Date().getTime();\n            $.write(settings, SETTINGS_KEY);\n            content = $.read('#sub-store');\n            content = content ? JSON.parse(content) : {};\n            if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));\n            if (encoding === 'plaintext') {\n                content.settings.gistToken = '恢复后请重新设置 GitHub Token';\n                content = JSON.stringify(content, null, `  `);\n            } else {\n                content = Base64.encode(JSON.stringify(content, null, `  `));\n            }\n            $.info(`上传备份中...`);\n            try {\n                await gist.upload({\n                    [GIST_BACKUP_FILE_NAME]: { content },\n                });\n                $.info(`上传备份完成`);\n            } catch (err) {\n                // restore syncTime if upload failed\n                settings.syncTime = updated;\n                $.write(settings, SETTINGS_KEY);\n                throw err;\n            }\n            break;\n        case 'download':\n            $.info(`还原备份中...`);\n            content = await gist.download(GIST_BACKUP_FILE_NAME);\n            try {\n                content = JSON.parse(Base64.decode(content));\n                if (!(Object.keys(content.settings).length >= 0)) {\n                    throw new Error('备份文件应该至少包含 settings 字段');\n                }\n            } catch (err) {\n                try {\n                    content = JSON.parse(content);\n                    if (!(Object.keys(content.settings).length >= 0)) {\n                        throw new Error('备份文件应该至少包含 settings 字段');\n                    }\n                } catch (err) {\n                    $.error(\n                        `Gist 备份文件校验失败, 无法还原\\nReason: ${\n                            err.message ?? err\n                        }`,\n                    );\n                    throw new Error('Gist 备份文件校验失败, 无法还原');\n                }\n            }\n            if (keep) {\n                $.info(`保留原有设置 ${keep}`);\n                keep.split(',').forEach((path) => {\n                    _.set(content, path, _.get(currentContent, path));\n                });\n            }\n            // restore settings\n            $.write(JSON.stringify(content, null, `  `), '#sub-store');\n            if ($.env.isNode) {\n                $.cache = content;\n                $.persistCache();\n            }\n            $.info(`perform migration after restoring from gist...`);\n            migrate();\n            $.info(`migration completed`);\n            $.info(`还原备份完成`);\n            break;\n    }\n}\nasync function gistBackup(req, res) {\n    const { action, keep, encode } = req.query;\n    // read token\n    const { gistToken } = $.read(SETTINGS_KEY);\n    if (!gistToken) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'GIST_TOKEN_NOT_FOUND',\n                `GitHub Token is required for backup!`,\n            ),\n        );\n    } else {\n        try {\n            await gistBackupAction(action, keep, encode);\n            success(res);\n        } catch (err) {\n            $.error(\n                `Failed to ${action} gist data.\\nReason: ${err.message ?? err}`,\n            );\n            failed(\n                res,\n                new InternalServerError(\n                    'BACKUP_FAILED',\n                    `Failed to ${action} gist data!`,\n                    `Reason: ${err.message ?? err}`,\n                ),\n            );\n        }\n    }\n}\n\nexport { gistBackupAction };\n"
  },
  {
    "path": "backend/src/restful/module.js",
    "content": "import { deleteByName, findByName, updateByName } from '@/utils/database';\nimport { MODULES_KEY } from '@/constants';\nimport { failed, success } from '@/restful/response';\nimport $ from '@/core/app';\nimport { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';\nimport { hex_md5 } from '@/vendor/md5';\n\nexport default function register($app) {\n    if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);\n\n    $app.route('/api/module/:name')\n        .get(getModule)\n        .patch(updateModule)\n        .delete(deleteModule);\n\n    $app.route('/api/modules')\n        .get(getAllModules)\n        .post(createModule)\n        .put(replaceModule);\n}\n\n// module API\nfunction createModule(req, res) {\n    const module = req.body;\n    module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;\n    $.info(`正在创建模块：${module.name}`);\n    const allModules = $.read(MODULES_KEY);\n    if (findByName(allModules, module.name)) {\n        return failed(\n            res,\n            new RequestInvalidError(\n                'DUPLICATE_KEY',\n                req.body.name\n                    ? `已存在 name 为 ${module.name} 的模块`\n                    : `已存在相同的模块 请勿重复添加`,\n            ),\n        );\n    }\n    allModules.push(module);\n    $.write(allModules, MODULES_KEY);\n    success(res, module, 201);\n}\n\nfunction getModule(req, res) {\n    let { name } = req.params;\n    const allModules = $.read(MODULES_KEY);\n    const module = findByName(allModules, name);\n    if (module) {\n        res.set('Content-Type', 'text/plain; charset=utf-8').send(\n            module.content,\n        );\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                `MODULE_NOT_FOUND`,\n                `Module ${name} does not exist`,\n                404,\n            ),\n        );\n    }\n}\n\nfunction updateModule(req, res) {\n    let { name } = req.params;\n    let module = req.body;\n    const allModules = $.read(MODULES_KEY);\n    const oldModule = findByName(allModules, name);\n    if (oldModule) {\n        const newModule = {\n            ...oldModule,\n            ...module,\n        };\n        $.info(`正在更新模块：${name}...`);\n\n        updateByName(allModules, name, newModule);\n        $.write(allModules, MODULES_KEY);\n        success(res, newModule);\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Module ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nfunction deleteModule(req, res) {\n    let { name } = req.params;\n    $.info(`正在删除模块：${name}`);\n    let allModules = $.read(MODULES_KEY);\n    deleteByName(allModules, name);\n    $.write(allModules, MODULES_KEY);\n    success(res);\n}\n\nfunction getAllModules(req, res) {\n    const allModules = $.read(MODULES_KEY);\n    success(\n        res,\n        // eslint-disable-next-line no-unused-vars\n        allModules.map(({ content, ...rest }) => rest),\n    );\n}\n\nfunction replaceModule(req, res) {\n    const allModules = req.body;\n    $.write(allModules, MODULES_KEY);\n    success(res);\n}\n"
  },
  {
    "path": "backend/src/restful/node-info.js",
    "content": "import producer from '@/core/proxy-utils/producers';\nimport { HTTP } from '@/vendor/open-api';\nimport { failed, success } from '@/restful/response';\nimport { NetworkError } from '@/restful/errors';\n\nexport default function register($app) {\n    $app.post('/api/utils/node-info', getNodeInfo);\n}\n\nasync function getNodeInfo(req, res) {\n    const proxy = req.body;\n    const lang = req.query.lang || 'zh-CN';\n    let shareUrl;\n    try {\n        shareUrl = producer.URI.produce(proxy);\n    } catch (err) {\n        // do nothing\n    }\n\n    try {\n        const $http = HTTP();\n        const info = await $http\n            .get({\n                url: `http://ip-api.com/json/${encodeURIComponent(\n                    `${proxy.server}`\n                        .trim()\n                        .replace(/^\\[/, '')\n                        .replace(/\\]$/, ''),\n                )}?lang=${lang}`,\n                headers: {\n                    'User-Agent':\n                        'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',\n                },\n            })\n            .then((resp) => {\n                const data = JSON.parse(resp.body);\n                if (data.status !== 'success') {\n                    throw new Error(data.message);\n                }\n\n                // remove unnecessary fields\n                delete data.status;\n                return data;\n            });\n        success(res, {\n            shareUrl,\n            info,\n        });\n    } catch (err) {\n        failed(\n            res,\n            new NetworkError(\n                'FAILED_TO_GET_NODE_INFO',\n                `Failed to get node info`,\n                `Reason: ${err}`,\n            ),\n        );\n    }\n}\n"
  },
  {
    "path": "backend/src/restful/parser.js",
    "content": "import { success, failed } from '@/restful/response';\nimport { ProxyUtils } from '@/core/proxy-utils';\nimport { RuleUtils } from '@/core/rule-utils';\n\nexport default function register($app) {\n    $app.route('/api/proxy/parse').post(proxy_parser);\n    $app.route('/api/rule/parse').post(rule_parser);\n}\n\n/***\n * 感谢 izhangxm 的 PR!\n * 目前没有节点操作, 没有支持完整参数, 以后再完善一下\n */\n\n/***\n * 代理服务器协议转换接口。\n * 请求方法为POST，数据为json。需要提供data和client字段。\n * data: string, 协议数据，每行一个或者是clash\n * client: string, 目标平台名称，见backend/src/core/proxy-utils/producers/index.js\n *\n */\nfunction proxy_parser(req, res) {\n    const { data, client, content, platform } = req.body;\n    var result = {};\n    try {\n        var proxies = ProxyUtils.parse(data ?? content);\n        var par_res = ProxyUtils.produce(proxies, client ?? platform);\n        result['par_res'] = par_res;\n    } catch (err) {\n        failed(res, err);\n        return;\n    }\n    success(res, result);\n}\n/**\n * 规则转换接口。\n * 请求方法为POST，数据为json。需要提供data和client字段。\n * data: string, 多行规则字符串\n * client: string, 目标平台名称，具体见backend/src/core/rule-utils/producers.js\n */\nfunction rule_parser(req, res) {\n    const { data, client, content, platform } = req.body;\n    var result = {};\n    try {\n        const rules = RuleUtils.parse(data ?? content);\n        var par_res = RuleUtils.produce(rules, client ?? platform);\n        result['par_res'] = par_res;\n    } catch (err) {\n        failed(res, err);\n        return;\n    }\n\n    success(res, result);\n}\n"
  },
  {
    "path": "backend/src/restful/preview.js",
    "content": "import { InternalServerError } from './errors';\nimport { ProxyUtils } from '@/core/proxy-utils';\nimport { findByName } from '@/utils/database';\nimport { success, failed } from './response';\nimport download from '@/utils/download';\nimport { SUBS_KEY } from '@/constants';\nimport $ from '@/core/app';\n\nexport default function register($app) {\n    $app.post('/api/preview/sub', compareSub);\n    $app.post('/api/preview/collection', compareCollection);\n    $app.post('/api/preview/file', previewFile);\n}\n\nasync function previewFile(req, res) {\n    try {\n        const file = req.body;\n        let content = '';\n        if (file.type !== 'mihomoProfile') {\n            if (\n                file.source === 'local' &&\n                !['localFirst', 'remoteFirst'].includes(file.mergeSources)\n            ) {\n                content = file.content;\n            } else {\n                const errors = {};\n                content = await Promise.all(\n                    file.url\n                        .split(/[\\r\\n]+/)\n                        .map((i) => i.trim())\n                        .filter((i) => i.length)\n                        .map(async (url) => {\n                            try {\n                                return await download(\n                                    url,\n                                    file.ua,\n                                    undefined,\n                                    file.proxy,\n                                );\n                            } catch (err) {\n                                errors[url] = err;\n                                $.error(\n                                    `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,\n                                );\n                                return '';\n                            }\n                        }),\n                );\n\n                if (Object.keys(errors).length > 0) {\n                    if (!file.ignoreFailedRemoteFile) {\n                        throw new Error(\n                            `文件 ${file.name} 的远程文件 ${Object.keys(\n                                errors,\n                            ).join(', ')} 发生错误, 请查看日志`,\n                        );\n                    } else if (file.ignoreFailedRemoteFile === 'enabled') {\n                        $.notify(\n                            `🌍 Sub-Store 预览文件失败`,\n                            `❌ ${file.name}`,\n                            `远程文件 ${Object.keys(errors).join(\n                                ', ',\n                            )} 发生错误, 请查看日志`,\n                        );\n                    }\n                }\n                if (file.mergeSources === 'localFirst') {\n                    content.unshift(file.content);\n                } else if (file.mergeSources === 'remoteFirst') {\n                    content.push(file.content);\n                }\n            }\n        }\n        // parse proxies\n        const files = (Array.isArray(content) ? content : [content]).flat();\n        let filesContent = files\n            .filter((i) => i != null && i !== '')\n            .join('\\n');\n\n        // apply processors\n        const processed =\n            Array.isArray(file.process) && file.process.length > 0\n                ? await ProxyUtils.process(\n                      { $files: files, $content: filesContent, $file: file },\n                      file.process,\n                  )\n                : { $content: filesContent, $files: files };\n\n        // produce\n        success(res, {\n            original: filesContent,\n            processed: processed?.$content ?? '',\n        });\n    } catch (err) {\n        $.error(err.message ?? err);\n        failed(\n            res,\n            new InternalServerError(\n                `INTERNAL_SERVER_ERROR`,\n                `Failed to preview file`,\n                `Reason: ${err.message ?? err}`,\n            ),\n        );\n    }\n}\n\nasync function compareSub(req, res) {\n    try {\n        const sub = req.body;\n        const target = req.query.target || 'JSON';\n        let content;\n        if (\n            sub.source === 'local' &&\n            !['localFirst', 'remoteFirst'].includes(sub.mergeSources)\n        ) {\n            content = sub.content;\n        } else {\n            const errors = {};\n            content = await Promise.all(\n                sub.url\n                    .split(/[\\r\\n]+/)\n                    .map((i) => i.trim())\n                    .filter((i) => i.length)\n                    .map(async (url) => {\n                        try {\n                            return await download(\n                                url,\n                                sub.ua,\n                                undefined,\n                                sub.proxy,\n                                undefined,\n                                undefined,\n                                undefined,\n                                true,\n                            );\n                        } catch (err) {\n                            errors[url] = err;\n                            $.error(\n                                `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,\n                            );\n                            return '';\n                        }\n                    }),\n            );\n\n            if (Object.keys(errors).length > 0) {\n                if (!sub.ignoreFailedRemoteSub) {\n                    throw new Error(\n                        `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(\n                            ', ',\n                        )} 发生错误, 请查看日志`,\n                    );\n                } else if (sub.ignoreFailedRemoteSub === 'enabled') {\n                    $.notify(\n                        `🌍 Sub-Store 预览订阅失败`,\n                        `❌ ${sub.name}`,\n                        `远程订阅 ${Object.keys(errors).join(\n                            ', ',\n                        )} 发生错误, 请查看日志`,\n                    );\n                }\n            }\n            if (sub.mergeSources === 'localFirst') {\n                content.unshift(sub.content);\n            } else if (sub.mergeSources === 'remoteFirst') {\n                content.push(sub.content);\n            }\n        }\n        // parse proxies\n        const original = (Array.isArray(content) ? content : [content])\n            .map((i) => ProxyUtils.parse(i))\n            .flat();\n\n        // add id\n        original.forEach((proxy, i) => {\n            proxy.id = i;\n            proxy._subName = sub.name;\n            proxy._subDisplayName = sub.displayName;\n        });\n\n        // apply processors\n        const processed = await ProxyUtils.process(\n            original,\n            sub.process || [],\n            target,\n            { [sub.name]: sub },\n        );\n\n        // produce\n        success(res, { original, processed });\n    } catch (err) {\n        $.error(err.message ?? err);\n        failed(\n            res,\n            new InternalServerError(\n                `INTERNAL_SERVER_ERROR`,\n                `Failed to preview subscription`,\n                `Reason: ${err.message ?? err}`,\n            ),\n        );\n    }\n}\n\nasync function compareCollection(req, res) {\n    try {\n        const allSubs = $.read(SUBS_KEY);\n        const collection = req.body;\n        const subnames = [...collection.subscriptions];\n        let subscriptionTags = collection.subscriptionTags;\n        if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {\n            allSubs.forEach((sub) => {\n                if (\n                    Array.isArray(sub.tag) &&\n                    sub.tag.length > 0 &&\n                    !subnames.includes(sub.name) &&\n                    sub.tag.some((tag) => subscriptionTags.includes(tag))\n                ) {\n                    subnames.push(sub.name);\n                }\n            });\n        }\n        const results = {};\n        const errors = {};\n        await Promise.all(\n            subnames.map(async (name) => {\n                const sub = findByName(allSubs, name);\n                try {\n                    let raw;\n                    if (\n                        sub.source === 'local' &&\n                        !['localFirst', 'remoteFirst'].includes(\n                            sub.mergeSources,\n                        )\n                    ) {\n                        raw = sub.content;\n                    } else {\n                        const errors = {};\n                        raw = await Promise.all(\n                            sub.url\n                                .split(/[\\r\\n]+/)\n                                .map((i) => i.trim())\n                                .filter((i) => i.length)\n                                .map(async (url) => {\n                                    try {\n                                        return await download(\n                                            url,\n                                            sub.ua,\n                                            undefined,\n                                            sub.proxy,\n                                            undefined,\n                                            undefined,\n                                            undefined,\n                                            true,\n                                        );\n                                    } catch (err) {\n                                        errors[url] = err;\n                                        $.error(\n                                            `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,\n                                        );\n                                        return '';\n                                    }\n                                }),\n                        );\n\n                        if (Object.keys(errors).length > 0) {\n                            if (!sub.ignoreFailedRemoteSub) {\n                                throw new Error(\n                                    `订阅 ${sub.name} 的远程订阅 ${Object.keys(\n                                        errors,\n                                    ).join(', ')} 发生错误, 请查看日志`,\n                                );\n                            } else if (\n                                sub.ignoreFailedRemoteSub === 'enabled'\n                            ) {\n                                $.notify(\n                                    `🌍 Sub-Store 预览订阅失败`,\n                                    `❌ ${sub.name}`,\n                                    `远程订阅 ${Object.keys(errors).join(\n                                        ', ',\n                                    )} 发生错误, 请查看日志`,\n                                );\n                            }\n                        }\n                        if (sub.mergeSources === 'localFirst') {\n                            raw.unshift(sub.content);\n                        } else if (sub.mergeSources === 'remoteFirst') {\n                            raw.push(sub.content);\n                        }\n                    }\n                    // parse proxies\n                    let currentProxies = (Array.isArray(raw) ? raw : [raw])\n                        .map((i) => ProxyUtils.parse(i))\n                        .flat();\n\n                    currentProxies.forEach((proxy) => {\n                        proxy._subName = sub.name;\n                        proxy._subDisplayName = sub.displayName;\n                        proxy._collectionName = collection.name;\n                        proxy._collectionDisplayName = collection.displayName;\n                    });\n\n                    // apply processors\n                    currentProxies = await ProxyUtils.process(\n                        currentProxies,\n                        sub.process || [],\n                        'JSON',\n                        { [sub.name]: sub, _collection: collection },\n                    );\n                    results[name] = currentProxies;\n                } catch (err) {\n                    errors[name] = err;\n\n                    $.error(\n                        `❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name} 时出现错误：${err}！`,\n                    );\n                }\n            }),\n        );\n\n        if (Object.keys(errors).length > 0) {\n            if (!collection.ignoreFailedRemoteSub) {\n                throw new Error(\n                    `组合订阅 ${collection.name} 的子订阅 ${Object.keys(\n                        errors,\n                    ).join(', ')} 发生错误, 请查看日志`,\n                );\n            } else if (collection.ignoreFailedRemoteSub === 'enabled') {\n                $.notify(\n                    `🌍 Sub-Store 预览组合订阅失败`,\n                    `❌ ${collection.name}`,\n                    `子订阅 ${Object.keys(errors).join(\n                        ', ',\n                    )} 发生错误, 请查看日志`,\n                );\n            }\n        }\n        // merge proxies with the original order\n        const original = Array.prototype.concat.apply(\n            [],\n            subnames.map((name) => results[name] || []),\n        );\n\n        original.forEach((proxy, i) => {\n            proxy.id = i;\n            proxy._collectionName = collection.name;\n            proxy._collectionDisplayName = collection.displayName;\n        });\n\n        const processed = await ProxyUtils.process(\n            original,\n            collection.process || [],\n            'JSON',\n            { _collection: collection },\n        );\n\n        success(res, { original, processed });\n    } catch (err) {\n        $.error(err.message ?? err);\n        failed(\n            res,\n            new InternalServerError(\n                `INTERNAL_SERVER_ERROR`,\n                `Failed to preview collection`,\n                `Reason: ${err.message ?? err}`,\n            ),\n        );\n    }\n}\n"
  },
  {
    "path": "backend/src/restful/response.js",
    "content": "export function success(resp, data, statusCode) {\n    resp.status(statusCode || 200).json({\n        status: 'success',\n        data,\n    });\n}\n\nexport function failed(resp, error, statusCode) {\n    resp.status(statusCode || 500).json({\n        status: 'failed',\n        error: {\n            code: error.code,\n            type: error.type,\n            message: error.message,\n            details: resp.req?.route?.path?.startsWith('/share/')\n                ? '详情请查看日志'\n                : error.details,\n        },\n    });\n}\n"
  },
  {
    "path": "backend/src/restful/settings.js",
    "content": "import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';\nimport { success, failed } from './response';\nimport { InternalServerError } from '@/restful/errors';\nimport $ from '@/core/app';\nimport Gist from '@/utils/gist';\n\nexport default function register($app) {\n    const settings = $.read(SETTINGS_KEY);\n    if (!settings) $.write({}, SETTINGS_KEY);\n    $app.route('/api/settings').get(getSettings).patch(updateSettings);\n}\n\nasync function getSettings(req, res) {\n    try {\n        let settings = $.read(SETTINGS_KEY);\n        if (!settings) {\n            settings = {};\n            $.write(settings, SETTINGS_KEY);\n        }\n\n        if (!settings.avatarUrl) await updateAvatar();\n        if (!settings.artifactStore) await updateArtifactStore();\n\n        success(res, settings);\n    } catch (e) {\n        $.error(`Failed to get settings: ${e.message ?? e}`);\n        failed(\n            res,\n            new InternalServerError(\n                `FAILED_TO_GET_SETTINGS`,\n                `Failed to get settings`,\n                `Reason: ${e.message ?? e}`,\n            ),\n        );\n    }\n}\n\nasync function updateSettings(req, res) {\n    try {\n        const settings = $.read(SETTINGS_KEY);\n        const newSettings = {\n            ...settings,\n            ...req.body,\n        };\n        [\n            'defaultTimeout',\n            'cacheThreshold',\n            'resourceCacheTtl',\n            'headersCacheTtl',\n            'scriptCacheTtl',\n        ].map((key) => {\n            let value = Number(newSettings[key]);\n            if (!isFinite(value) || value <= 0) {\n                delete newSettings[key];\n            }\n        });\n        $.write(newSettings, SETTINGS_KEY);\n        if (\n            req.body.githubUser ||\n            req.body.gistToken ||\n            req.body.githubProxy ||\n            req.body.defaultProxy\n        ) {\n            await updateAvatar();\n            await updateArtifactStore();\n        }\n        success(res, newSettings);\n    } catch (e) {\n        $.error(`Failed to update settings: ${e.message ?? e}`);\n        failed(\n            res,\n            new InternalServerError(\n                `FAILED_TO_UPDATE_SETTINGS`,\n                `Failed to update settings`,\n                `Reason: ${e.message ?? e}`,\n            ),\n        );\n    }\n}\n\nexport async function updateAvatar() {\n    const settings = $.read(SETTINGS_KEY);\n    const { githubUser: username, syncPlatform, githubProxy } = settings;\n    if (username) {\n        if (syncPlatform === 'gitlab') {\n            try {\n                const data = await $.http\n                    .get({\n                        url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(\n                            username,\n                        )}`,\n                        headers: {\n                            'User-Agent':\n                                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',\n                        },\n                    })\n                    .then((resp) => JSON.parse(resp.body));\n                settings.avatarUrl = data[0]['avatar_url'].replace(\n                    /(\\?|&)s=\\d+(&|$)/,\n                    '$1s=160$2',\n                );\n                $.write(settings, SETTINGS_KEY);\n            } catch (err) {\n                $.error(\n                    `Failed to fetch GitLab avatar for User: ${username}. Reason: ${\n                        err.message ?? err\n                    }`,\n                );\n            }\n        } else {\n            try {\n                const data = await $.http\n                    .get({\n                        url: `${\n                            githubProxy ? `${githubProxy}/` : ''\n                        }https://api.github.com/users/${encodeURIComponent(\n                            username,\n                        )}`,\n                        headers: {\n                            'User-Agent':\n                                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',\n                        },\n                    })\n                    .then((resp) => JSON.parse(resp.body));\n                settings.avatarUrl = data['avatar_url'];\n                $.write(settings, SETTINGS_KEY);\n            } catch (err) {\n                $.error(\n                    `Failed to fetch GitHub avatar for User: ${username}. Reason: ${\n                        err.message ?? err\n                    }`,\n                );\n            }\n        }\n    }\n}\n\nexport async function updateArtifactStore() {\n    $.log('Updating artifact store');\n    const settings = $.read(SETTINGS_KEY);\n    const { gistToken, syncPlatform } = settings;\n    if (gistToken) {\n        const manager = new Gist({\n            token: gistToken,\n            key: ARTIFACT_REPOSITORY_KEY,\n            syncPlatform,\n        });\n\n        try {\n            const gist = await manager.locate();\n            const url = gist?.html_url ?? gist?.web_url;\n            if (url) {\n                $.log(`找到 Sub-Store Gist: ${url}`);\n                // 只需要保证 token 是对的, 现在 username 错误只会导致头像错误\n                settings.artifactStore = url;\n                settings.artifactStoreStatus = 'VALID';\n            } else {\n                $.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);\n                settings.artifactStoreStatus = 'NOT FOUND';\n            }\n        } catch (err) {\n            $.error(\n                `查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${\n                    err.message ?? err\n                }`,\n            );\n            settings.artifactStoreStatus = 'ERROR';\n        }\n        $.write(settings, SETTINGS_KEY);\n    }\n}\n"
  },
  {
    "path": "backend/src/restful/sort.js",
    "content": "import {\n    ARTIFACTS_KEY,\n    COLLECTIONS_KEY,\n    SUBS_KEY,\n    FILES_KEY,\n    TOKENS_KEY,\n} from '@/constants';\nimport $ from '@/core/app';\nimport { success } from '@/restful/response';\n\nexport default function register($app) {\n    $app.post('/api/sort/subs', sortSubs);\n    $app.post('/api/sort/collections', sortCollections);\n    $app.post('/api/sort/artifacts', sortArtifacts);\n    $app.post('/api/sort/files', sortFiles);\n    $app.post('/api/sort/tokens', sortTokens);\n}\n\nfunction sortSubs(req, res) {\n    const orders = req.body;\n    const allSubs = $.read(SUBS_KEY);\n    allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));\n    $.write(allSubs, SUBS_KEY);\n    success(res, allSubs);\n}\n\nfunction sortCollections(req, res) {\n    const orders = req.body;\n    const allCols = $.read(COLLECTIONS_KEY);\n    allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));\n    $.write(allCols, COLLECTIONS_KEY);\n    success(res, allCols);\n}\n\nfunction sortArtifacts(req, res) {\n    const orders = req.body;\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    allArtifacts.sort(\n        (a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),\n    );\n    $.write(allArtifacts, ARTIFACTS_KEY);\n    success(res, allArtifacts);\n}\n\nfunction sortFiles(req, res) {\n    const orders = req.body;\n    const allFiles = $.read(FILES_KEY);\n    allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));\n    $.write(allFiles, FILES_KEY);\n    success(res, allFiles);\n}\n\nfunction sortTokens(req, res) {\n    const orders = req.body;\n    const allTokens = $.read(TOKENS_KEY);\n    allTokens.sort(\n        (a, b) =>\n            orders.indexOf(`${a.type}-${a.name}-${a.token}`) -\n            orders.indexOf(`${b.type}-${b.name}-${b.token}`),\n    );\n    $.write(allTokens, TOKENS_KEY);\n    success(res, allTokens);\n}\n"
  },
  {
    "path": "backend/src/restful/subscriptions.js",
    "content": "import {\n    NetworkError,\n    InternalServerError,\n    ResourceNotFoundError,\n    RequestInvalidError,\n} from './errors';\nimport { deleteByName, findByName, updateByName } from '@/utils/database';\nimport {\n    SUBS_KEY,\n    COLLECTIONS_KEY,\n    ARTIFACTS_KEY,\n    FILES_KEY,\n} from '@/constants';\nimport {\n    getFlowHeaders,\n    parseFlowHeaders,\n    getRmainingDays,\n} from '@/utils/flow';\nimport { success, failed } from './response';\nimport $ from '@/core/app';\nimport { formatDateTime } from '@/utils';\n\nif (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);\n\nexport default function register($app) {\n    $app.get('/api/sub/flow/:name', getFlowInfo);\n\n    $app.route('/api/sub/:name')\n        .get(getSubscription)\n        .patch(updateSubscription)\n        .delete(deleteSubscription);\n\n    $app.route('/api/subs')\n        .get(getAllSubscriptions)\n        .post(createSubscription)\n        .put(replaceSubscriptions);\n}\n\n// subscriptions API\nasync function getFlowInfo(req, res) {\n    let { name } = req.params;\n    let { url } = req.query;\n    if (url) {\n        $.info(`指定远程订阅 URL: ${url}`);\n    }\n    const allSubs = $.read(SUBS_KEY);\n    const sub = findByName(allSubs, name);\n    if (!sub) {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Subscription ${name} does not exist!`,\n            ),\n            404,\n        );\n        return;\n    }\n    if (\n        sub.source === 'local' &&\n        !['localFirst', 'remoteFirst'].includes(sub.mergeSources)\n    ) {\n        if (sub.subUserinfo) {\n            let subUserInfo;\n            if (/^https?:\\/\\//.test(sub.subUserinfo)) {\n                try {\n                    subUserInfo = await getFlowHeaders(\n                        undefined,\n                        undefined,\n                        undefined,\n                        sub.proxy,\n                        sub.subUserinfo,\n                    );\n                } catch (e) {\n                    $.error(\n                        `订阅 ${name} 使用自定义流量链接 ${\n                            sub.subUserinfo\n                        } 获取流量信息时发生错误: ${JSON.stringify(e)}`,\n                    );\n                }\n            } else {\n                subUserInfo = sub.subUserinfo;\n            }\n            try {\n                success(res, {\n                    ...parseFlowHeaders(subUserInfo),\n                });\n            } catch (e) {\n                $.error(\n                    `Failed to parse flow info for local subscription ${name}: ${\n                        e.message ?? e\n                    }`,\n                );\n                failed(\n                    res,\n                    new RequestInvalidError(\n                        'NO_FLOW_INFO',\n                        'N/A',\n                        `Failed to parse flow info`,\n                    ),\n                );\n            }\n        } else {\n            failed(\n                res,\n                new RequestInvalidError(\n                    'NO_FLOW_INFO',\n                    'N/A',\n                    `Local subscription ${name} has no flow information!`,\n                ),\n            );\n        }\n        return;\n    }\n    try {\n        url =\n            `${url || sub.url}`\n                .split(/[\\r\\n]+/)\n                .map((i) => i.trim())\n                .filter((i) => i.length)?.[0] || '';\n\n        let $arguments = {};\n        const rawArgs = url.split('#');\n        url = url.split('#')[0];\n        if (rawArgs.length > 1) {\n            try {\n                // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n                $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));\n            } catch (e) {\n                for (const pair of rawArgs[1].split('&')) {\n                    const key = pair.split('=')[0];\n                    const value = pair.split('=')[1];\n                    // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                    $arguments[key] =\n                        value == null || value === ''\n                            ? true\n                            : decodeURIComponent(value);\n                }\n            }\n        }\n        if ($arguments.noFlow || !/^https?/.test(url)) {\n            failed(\n                res,\n                new RequestInvalidError(\n                    'NO_FLOW_INFO',\n                    'N/A',\n                    `Subscription ${name}: noFlow`,\n                ),\n            );\n            return;\n        }\n        const flowHeaders = await getFlowHeaders(\n            $arguments?.insecure ? `${url}#insecure` : url,\n            $arguments.flowUserAgent,\n            undefined,\n            sub.proxy,\n            $arguments.flowUrl,\n        );\n        if (!flowHeaders && !sub.subUserinfo) {\n            failed(\n                res,\n                new InternalServerError(\n                    'NO_FLOW_INFO',\n                    'No flow info',\n                    `Failed to fetch flow headers`,\n                ),\n            );\n            return;\n        }\n        try {\n            const remainingDays = getRmainingDays({\n                resetDay: $arguments.resetDay,\n                startDate: $arguments.startDate,\n                cycleDays: $arguments.cycleDays,\n            });\n            let subUserInfo;\n            if (/^https?:\\/\\//.test(sub.subUserinfo)) {\n                try {\n                    subUserInfo = await getFlowHeaders(\n                        undefined,\n                        undefined,\n                        undefined,\n                        sub.proxy,\n                        sub.subUserinfo,\n                    );\n                } catch (e) {\n                    $.error(\n                        `订阅 ${name} 使用自定义流量链接 ${\n                            sub.subUserinfo\n                        } 获取流量信息时发生错误: ${JSON.stringify(e)}`,\n                    );\n                }\n            } else {\n                subUserInfo = sub.subUserinfo;\n            }\n            const result = {\n                ...parseFlowHeaders(\n                    [subUserInfo, flowHeaders].filter((i) => i).join('; '),\n                ),\n            };\n            if (remainingDays != null) {\n                result.remainingDays = remainingDays;\n            }\n            success(res, result);\n        } catch (e) {\n            $.error(\n                `Failed to parse flow info for local subscription ${name}: ${\n                    e.message ?? e\n                }`,\n            );\n            failed(\n                res,\n                new RequestInvalidError(\n                    'NO_FLOW_INFO',\n                    'N/A',\n                    `Failed to parse flow info`,\n                ),\n            );\n        }\n    } catch (err) {\n        failed(\n            res,\n            new NetworkError(\n                `URL_NOT_ACCESSIBLE`,\n                `The URL for subscription ${name} is inaccessible.`,\n            ),\n        );\n    }\n}\n\nfunction createSubscription(req, res) {\n    const sub = req.body;\n    delete sub.subscriptions;\n    $.info(`正在创建订阅： ${sub.name}`);\n    if (/\\//.test(sub.name)) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'INVALID_NAME',\n                `Subscription ${sub.name} is invalid`,\n            ),\n        );\n        return;\n    }\n    const allSubs = $.read(SUBS_KEY);\n    if (findByName(allSubs, sub.name)) {\n        failed(\n            res,\n            new RequestInvalidError(\n                'DUPLICATE_KEY',\n                `Subscription ${sub.name} already exists.`,\n            ),\n        );\n        return;\n    }\n    allSubs.push(sub);\n    $.write(allSubs, SUBS_KEY);\n    success(res, sub, 201);\n}\n\nfunction getSubscription(req, res) {\n    let { name } = req.params;\n    let { raw } = req.query;\n    const allSubs = $.read(SUBS_KEY);\n    const sub = findByName(allSubs, name);\n    delete sub.subscriptions;\n    if (sub) {\n        if (raw) {\n            res.set('content-type', 'application/json')\n                .set(\n                    'content-disposition',\n                    `attachment; filename=\"${encodeURIComponent(\n                        `sub-store_subscription_${name}_${formatDateTime(\n                            new Date(),\n                        )}.json`,\n                    )}\"`,\n                )\n                .send(JSON.stringify(sub));\n        } else {\n            success(res, sub);\n        }\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                `SUBSCRIPTION_NOT_FOUND`,\n                `Subscription ${name} does not exist`,\n                404,\n            ),\n        );\n    }\n}\n\nfunction updateSubscription(req, res) {\n    let { name } = req.params;\n    let sub = req.body;\n    delete sub.subscriptions;\n    const allSubs = $.read(SUBS_KEY);\n    const oldSub = findByName(allSubs, name);\n    if (oldSub) {\n        if (!sub.name) sub.name = oldSub.name;\n        const newSub = {\n            ...oldSub,\n            ...sub,\n        };\n        $.info(`正在更新订阅： ${name}`);\n        // allow users to update the subscription name\n        if (name !== sub.name) {\n            // update all collections refer to this name\n            const allCols = $.read(COLLECTIONS_KEY) || [];\n            for (const collection of allCols) {\n                const idx = collection.subscriptions.indexOf(name);\n                if (idx !== -1) {\n                    collection.subscriptions[idx] = sub.name;\n                }\n            }\n\n            // update all artifacts referring this subscription\n            const allArtifacts = $.read(ARTIFACTS_KEY) || [];\n            for (const artifact of allArtifacts) {\n                if (\n                    artifact.type === 'subscription' &&\n                    artifact.source == name\n                ) {\n                    artifact.source = sub.name;\n                }\n            }\n            // update all files referring this subscription\n            const allFiles = $.read(FILES_KEY) || [];\n            for (const file of allFiles) {\n                if (\n                    file.sourceType === 'subscription' &&\n                    file.sourceName == name\n                ) {\n                    file.sourceName = sub.name;\n                }\n            }\n\n            $.write(allCols, COLLECTIONS_KEY);\n            $.write(allArtifacts, ARTIFACTS_KEY);\n            $.write(allFiles, FILES_KEY);\n        }\n        updateByName(allSubs, name, newSub);\n        $.write(allSubs, SUBS_KEY);\n        success(res, newSub);\n    } else {\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `Subscription ${name} does not exist!`,\n            ),\n            404,\n        );\n    }\n}\n\nfunction deleteSubscription(req, res) {\n    let { name } = req.params;\n    $.info(`删除订阅：${name}...`);\n    // delete from subscriptions\n    let allSubs = $.read(SUBS_KEY);\n    deleteByName(allSubs, name);\n    $.write(allSubs, SUBS_KEY);\n    // delete from collections\n    const allCols = $.read(COLLECTIONS_KEY);\n    for (const collection of allCols) {\n        collection.subscriptions = collection.subscriptions.filter(\n            (s) => s !== name,\n        );\n    }\n    $.write(allCols, COLLECTIONS_KEY);\n    success(res);\n}\n\nfunction getAllSubscriptions(req, res) {\n    const allSubs = $.read(SUBS_KEY);\n    success(res, allSubs);\n}\n\nfunction replaceSubscriptions(req, res) {\n    const allSubs = req.body;\n    $.write(allSubs, SUBS_KEY);\n    success(res);\n}\n"
  },
  {
    "path": "backend/src/restful/sync.js",
    "content": "import $ from '@/core/app';\nimport {\n    ARTIFACTS_KEY,\n    COLLECTIONS_KEY,\n    RULES_KEY,\n    SUBS_KEY,\n    FILES_KEY,\n} from '@/constants';\nimport { failed, success } from '@/restful/response';\nimport { InternalServerError, ResourceNotFoundError } from '@/restful/errors';\nimport { findByName } from '@/utils/database';\nimport download from '@/utils/download';\nimport { ProxyUtils } from '@/core/proxy-utils';\nimport { RuleUtils } from '@/core/rule-utils';\nimport { syncToGist } from '@/restful/artifacts';\n\nexport default function register($app) {\n    // Initialization\n    if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);\n\n    // sync all artifacts\n    $app.get('/api/sync/artifacts', syncAllArtifacts);\n    $app.get('/api/sync/artifact/:name', syncArtifact);\n}\n\nasync function produceArtifact({\n    type,\n    name,\n    platform,\n    url,\n    ua,\n    content,\n    mergeSources,\n    ignoreFailedRemoteSub,\n    ignoreFailedRemoteFile,\n    produceType,\n    produceOpts = {},\n    subscription,\n    awaitCustomCache,\n    $options,\n    proxy,\n    noCache,\n    all,\n}) {\n    platform = platform || 'JSON';\n\n    if (['subscription', 'sub'].includes(type)) {\n        let sub;\n        if (name) {\n            const allSubs = $.read(SUBS_KEY);\n            sub = findByName(allSubs, name);\n            if (!sub) throw new Error(`找不到订阅 ${name}`);\n        } else if (subscription) {\n            sub = subscription;\n        } else {\n            throw new Error('未提供订阅名称或订阅数据');\n        }\n        let raw;\n        if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {\n            raw = content;\n        } else if (url) {\n            const errors = {};\n            raw = await Promise.all(\n                url\n                    .split(/[\\r\\n]+/)\n                    .map((i) => i.trim())\n                    .filter((i) => i.length)\n                    .map(async (url) => {\n                        try {\n                            return await download(\n                                url,\n                                ua || sub.ua,\n                                undefined,\n                                proxy || sub.proxy,\n                                undefined,\n                                awaitCustomCache,\n                                noCache || sub.noCache,\n                                true,\n                            );\n                        } catch (err) {\n                            errors[url] = err;\n                            $.error(\n                                `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,\n                            );\n                            return '';\n                        }\n                    }),\n            );\n            let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;\n            if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {\n                subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;\n            }\n\n            if (Object.keys(errors).length > 0) {\n                if (!subIgnoreFailedRemoteSub) {\n                    throw new Error(\n                        `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(\n                            ', ',\n                        )} 发生错误, 请查看日志`,\n                    );\n                } else if (subIgnoreFailedRemoteSub === 'enabled') {\n                    $.notify(\n                        `🌍 Sub-Store 处理订阅失败`,\n                        `❌ ${sub.name}`,\n                        `远程订阅 ${Object.keys(errors).join(\n                            ', ',\n                        )} 发生错误, 请查看日志`,\n                    );\n                }\n            }\n            if (mergeSources === 'localFirst') {\n                raw.unshift(content);\n            } else if (mergeSources === 'remoteFirst') {\n                raw.push(content);\n            }\n        } else if (\n            sub.source === 'local' &&\n            !['localFirst', 'remoteFirst'].includes(sub.mergeSources)\n        ) {\n            raw = sub.content;\n        } else {\n            const errors = {};\n            raw = await Promise.all(\n                sub.url\n                    .split(/[\\r\\n]+/)\n                    .map((i) => i.trim())\n                    .filter((i) => i.length)\n                    .map(async (url) => {\n                        try {\n                            return await download(\n                                url,\n                                ua || sub.ua,\n                                undefined,\n                                proxy || sub.proxy,\n                                undefined,\n                                awaitCustomCache,\n                                noCache || sub.noCache,\n                                true,\n                            );\n                        } catch (err) {\n                            errors[url] = err;\n                            $.error(\n                                `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,\n                            );\n                            return '';\n                        }\n                    }),\n            );\n            let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;\n            if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {\n                subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;\n            }\n\n            if (Object.keys(errors).length > 0) {\n                if (!subIgnoreFailedRemoteSub) {\n                    throw new Error(\n                        `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(\n                            ', ',\n                        )} 发生错误, 请查看日志`,\n                    );\n                } else if (subIgnoreFailedRemoteSub === 'enabled') {\n                    $.notify(\n                        `🌍 Sub-Store 处理订阅失败`,\n                        `❌ ${sub.name}`,\n                        `远程订阅 ${Object.keys(errors).join(\n                            ', ',\n                        )} 发生错误, 请查看日志`,\n                    );\n                }\n            }\n            if (sub.mergeSources === 'localFirst') {\n                raw.unshift(sub.content);\n            } else if (sub.mergeSources === 'remoteFirst') {\n                raw.push(sub.content);\n            }\n        }\n        if (produceType === 'raw') {\n            return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());\n        }\n        // parse proxies\n        let proxies = (Array.isArray(raw) ? raw : [raw])\n            .map((i) => ProxyUtils.parse(i))\n            .flat();\n\n        proxies.forEach((proxy) => {\n            proxy._subName = sub.name;\n            proxy._subDisplayName = sub.displayName;\n        });\n        // apply processors\n        proxies = await ProxyUtils.process(\n            proxies,\n            sub.process || [],\n            platform,\n            { [sub.name]: sub },\n            $options,\n        );\n        if (proxies.length === 0) {\n            throw new Error(`订阅 ${name} 中不含有效节点`);\n        }\n        // check duplicate\n        const exist = {};\n        for (const proxy of proxies) {\n            if (exist[proxy.name]) {\n                $.notify(\n                    '🌍 Sub-Store',\n                    `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}！`,\n                    '请仔细检测配置！',\n                    {\n                        'media-url':\n                            'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',\n                    },\n                );\n                break;\n            }\n            exist[proxy.name] = true;\n        }\n        // produce\n        return ProxyUtils.produce(proxies, platform, produceType, produceOpts);\n    } else if (['collection', 'col'].includes(type)) {\n        const allSubs = $.read(SUBS_KEY);\n        const allCols = $.read(COLLECTIONS_KEY);\n        const collection = findByName(allCols, name);\n        if (!collection) throw new Error(`找不到组合订阅 ${name}`);\n        const subnames = [...collection.subscriptions];\n        let subscriptionTags = collection.subscriptionTags;\n        if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {\n            allSubs.forEach((sub) => {\n                if (\n                    Array.isArray(sub.tag) &&\n                    sub.tag.length > 0 &&\n                    !subnames.includes(sub.name) &&\n                    sub.tag.some((tag) => subscriptionTags.includes(tag))\n                ) {\n                    subnames.push(sub.name);\n                }\n            });\n        }\n        const results = {};\n        const errors = {};\n        let processed = 0;\n\n        await Promise.all(\n            subnames.map(async (name) => {\n                const sub = findByName(allSubs, name);\n                const passThroughUA = sub.passThroughUA;\n                let reqUA = sub.ua;\n                if (passThroughUA) {\n                    $.info(\n                        `订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,\n                    );\n                    reqUA = ua;\n                }\n                try {\n                    $.info(`正在处理子订阅：${sub.name}...`);\n                    let raw;\n                    if (\n                        sub.source === 'local' &&\n                        !['localFirst', 'remoteFirst'].includes(\n                            sub.mergeSources,\n                        )\n                    ) {\n                        raw = sub.content;\n                    } else {\n                        const errors = {};\n                        raw = await await Promise.all(\n                            sub.url\n                                .split(/[\\r\\n]+/)\n                                .map((i) => i.trim())\n                                .filter((i) => i.length)\n                                .map(async (url) => {\n                                    try {\n                                        return await download(\n                                            url,\n                                            reqUA,\n                                            undefined,\n                                            proxy ||\n                                                sub.proxy ||\n                                                collection.proxy,\n                                            undefined,\n                                            undefined,\n                                            noCache || sub.noCache,\n                                            true,\n                                        );\n                                    } catch (err) {\n                                        errors[url] = err;\n                                        $.error(\n                                            `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,\n                                        );\n                                        return '';\n                                    }\n                                }),\n                        );\n\n                        if (Object.keys(errors).length > 0) {\n                            if (!sub.ignoreFailedRemoteSub) {\n                                throw new Error(\n                                    `订阅 ${sub.name} 的远程订阅 ${Object.keys(\n                                        errors,\n                                    ).join(', ')} 发生错误, 请查看日志`,\n                                );\n                            } else if (\n                                sub.ignoreFailedRemoteSub === 'enabled'\n                            ) {\n                                $.notify(\n                                    `🌍 Sub-Store 处理订阅失败`,\n                                    `❌ ${sub.name}`,\n                                    `远程订阅 ${Object.keys(errors).join(\n                                        ', ',\n                                    )} 发生错误, 请查看日志`,\n                                );\n                            }\n                        }\n                        if (sub.mergeSources === 'localFirst') {\n                            raw.unshift(sub.content);\n                        } else if (sub.mergeSources === 'remoteFirst') {\n                            raw.push(sub.content);\n                        }\n                    }\n                    // parse proxies\n                    let currentProxies = (Array.isArray(raw) ? raw : [raw])\n                        .map((i) => ProxyUtils.parse(i))\n                        .flat();\n\n                    currentProxies.forEach((proxy) => {\n                        proxy._subName = sub.name;\n                        proxy._subDisplayName = sub.displayName;\n                        proxy._collectionName = collection.name;\n                        proxy._collectionDisplayName = collection.displayName;\n                    });\n\n                    // apply processors\n                    currentProxies = await ProxyUtils.process(\n                        currentProxies,\n                        sub.process || [],\n                        platform,\n                        {\n                            [sub.name]: sub,\n                            _collection: collection,\n                            $options,\n                        },\n                    );\n                    results[name] = currentProxies;\n                    processed++;\n                    $.info(\n                        `✅ 子订阅：${sub.name}加载成功，进度--${\n                            100 * (processed / subnames.length).toFixed(1)\n                        }% `,\n                    );\n                } catch (err) {\n                    processed++;\n                    errors[name] = err;\n                    $.error(\n                        `❌ 处理组合订阅中的子订阅: ${\n                            sub.name\n                        }时出现错误：${err}！进度--${\n                            100 * (processed / subnames.length).toFixed(1)\n                        }%`,\n                    );\n                }\n            }),\n        );\n        let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;\n        if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {\n            collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;\n        }\n\n        if (Object.keys(errors).length > 0) {\n            if (!collectionIgnoreFailedRemoteSub) {\n                throw new Error(\n                    `组合订阅 ${collection.name} 的子订阅 ${Object.keys(\n                        errors,\n                    ).join(', ')} 发生错误, 请查看日志`,\n                );\n            } else if (collectionIgnoreFailedRemoteSub === 'enabled') {\n                $.notify(\n                    `🌍 Sub-Store 处理组合订阅失败`,\n                    `❌ ${collection.name}`,\n                    `子订阅 ${Object.keys(errors).join(\n                        ', ',\n                    )} 发生错误, 请查看日志`,\n                );\n            }\n        }\n\n        // merge proxies with the original order\n        let proxies = Array.prototype.concat.apply(\n            [],\n            subnames.map((name) => results[name] || []),\n        );\n\n        proxies.forEach((proxy) => {\n            proxy._collectionName = collection.name;\n            proxy._collectionDisplayName = collection.displayName;\n        });\n\n        // apply own processors\n        proxies = await ProxyUtils.process(\n            proxies,\n            collection.process || [],\n            platform,\n            { _collection: collection },\n            $options,\n        );\n        if (proxies.length === 0) {\n            throw new Error(`组合订阅 ${name} 中不含有效节点`);\n        }\n        // check duplicate\n        const exist = {};\n        for (const proxy of proxies) {\n            if (exist[proxy.name]) {\n                $.notify(\n                    '🌍 Sub-Store',\n                    `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}！`,\n                    '请仔细检测配置！',\n                    {\n                        'media-url':\n                            'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',\n                    },\n                );\n                break;\n            }\n            exist[proxy.name] = true;\n        }\n        return ProxyUtils.produce(proxies, platform, produceType, produceOpts);\n    } else if (type === 'rule') {\n        const allRules = $.read(RULES_KEY);\n        const rule = findByName(allRules, name);\n        if (!rule) throw new Error(`找不到规则 ${name}`);\n        let rules = [];\n        for (let i = 0; i < rule.urls.length; i++) {\n            const url = rule.urls[i];\n            $.info(\n                `正在处理URL：${url}，进度--${\n                    100 * ((i + 1) / rule.urls.length).toFixed(1)\n                }% `,\n            );\n            try {\n                const { body } = await download(url);\n                const currentRules = RuleUtils.parse(body);\n                rules = rules.concat(currentRules);\n            } catch (err) {\n                $.error(\n                    `处理分流订阅中的URL: ${url}时出现错误：${err}! 该订阅已被跳过。`,\n                );\n            }\n        }\n        // remove duplicates\n        rules = await RuleUtils.process(rules, [\n            { type: 'Remove Duplicate Filter' },\n        ]);\n        // produce output\n        return RuleUtils.produce(rules, platform);\n    } else if (type === 'file') {\n        const allFiles = $.read(FILES_KEY);\n        const file = findByName(allFiles, name);\n        if (!file) throw new Error(`找不到文件 ${name}`);\n        let raw = '';\n        if (file.type !== 'mihomoProfile') {\n            if (\n                content &&\n                !['localFirst', 'remoteFirst'].includes(mergeSources)\n            ) {\n                raw = content;\n            } else if (url) {\n                const errors = {};\n                raw = await Promise.all(\n                    url\n                        .split(/[\\r\\n]+/)\n                        .map((i) => i.trim())\n                        .filter((i) => i.length)\n                        .map(async (url) => {\n                            try {\n                                return await download(\n                                    url,\n                                    ua || file.ua,\n                                    undefined,\n                                    file.proxy || proxy,\n                                    undefined,\n                                    undefined,\n                                    noCache,\n                                );\n                            } catch (err) {\n                                errors[url] = err;\n                                $.error(\n                                    `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,\n                                );\n                                return '';\n                            }\n                        }),\n                );\n                let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;\n                if (\n                    ignoreFailedRemoteFile != null &&\n                    ignoreFailedRemoteFile !== ''\n                ) {\n                    fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;\n                }\n                if (\n                    !fileIgnoreFailedRemoteFile &&\n                    Object.keys(errors).length > 0\n                ) {\n                    throw new Error(\n                        `文件 ${file.name} 的远程文件 ${Object.keys(\n                            errors,\n                        ).join(', ')} 发生错误, 请查看日志`,\n                    );\n                }\n                if (mergeSources === 'localFirst') {\n                    raw.unshift(content);\n                } else if (mergeSources === 'remoteFirst') {\n                    raw.push(content);\n                }\n            } else if (\n                file.source === 'local' &&\n                !['localFirst', 'remoteFirst'].includes(file.mergeSources)\n            ) {\n                raw = file.content;\n            } else {\n                const errors = {};\n                raw = await Promise.all(\n                    file.url\n                        .split(/[\\r\\n]+/)\n                        .map((i) => i.trim())\n                        .filter((i) => i.length)\n                        .map(async (url) => {\n                            try {\n                                return await download(\n                                    url,\n                                    ua || file.ua,\n                                    undefined,\n                                    file.proxy || proxy,\n                                    undefined,\n                                    undefined,\n                                    noCache,\n                                );\n                            } catch (err) {\n                                errors[url] = err;\n                                $.error(\n                                    `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,\n                                );\n                                return '';\n                            }\n                        }),\n                );\n                let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;\n                if (\n                    ignoreFailedRemoteFile != null &&\n                    ignoreFailedRemoteFile !== ''\n                ) {\n                    fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;\n                }\n\n                if (Object.keys(errors).length > 0) {\n                    if (!fileIgnoreFailedRemoteFile) {\n                        throw new Error(\n                            `文件 ${file.name} 的远程文件 ${Object.keys(\n                                errors,\n                            ).join(', ')} 发生错误, 请查看日志`,\n                        );\n                    } else if (fileIgnoreFailedRemoteFile === 'enabled') {\n                        $.notify(\n                            `🌍 Sub-Store 处理文件失败`,\n                            `❌ ${file.name}`,\n                            `远程文件 ${Object.keys(errors).join(\n                                ', ',\n                            )} 发生错误, 请查看日志`,\n                        );\n                    }\n                }\n                if (file.mergeSources === 'localFirst') {\n                    raw.unshift(file.content);\n                } else if (file.mergeSources === 'remoteFirst') {\n                    raw.push(file.content);\n                }\n            }\n        }\n        if (produceType === 'raw') {\n            return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());\n        }\n        const files = (Array.isArray(raw) ? raw : [raw]).flat();\n        let filesContent = files\n            .filter((i) => i != null && i !== '')\n            .join('\\n');\n\n        // apply processors\n        const processed =\n            Array.isArray(file.process) && file.process.length > 0\n                ? await ProxyUtils.process(\n                      {\n                          $files: files,\n                          $content: filesContent,\n                          $options,\n                          $file: file,\n                      },\n                      file.process,\n                  )\n                : { $content: filesContent, $files: files, $options };\n\n        return (all ? processed : processed?.$content) ?? '';\n    }\n}\n\nasync function syncArtifacts() {\n    $.info('开始同步所有远程配置...');\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    const files = {};\n\n    try {\n        const valid = [];\n        const invalid = [];\n        const allSubs = $.read(SUBS_KEY);\n        const allCols = $.read(COLLECTIONS_KEY);\n        const subNames = [];\n        let enabledCount = 0;\n        allArtifacts.map((artifact) => {\n            if (artifact.sync && artifact.source) {\n                enabledCount++;\n                if (artifact.type === 'subscription') {\n                    const subName = artifact.source;\n                    const sub = findByName(allSubs, subName);\n                    if (sub && sub.url && !subNames.includes(subName)) {\n                        subNames.push(subName);\n                    }\n                } else if (artifact.type === 'collection') {\n                    const collection = findByName(allCols, artifact.source);\n                    if (collection && Array.isArray(collection.subscriptions)) {\n                        collection.subscriptions.map((subName) => {\n                            const sub = findByName(allSubs, subName);\n                            if (sub && sub.url && !subNames.includes(subName)) {\n                                subNames.push(subName);\n                            }\n                        });\n                    }\n                }\n            }\n        });\n\n        if (enabledCount === 0) {\n            $.info(\n                `需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,\n            );\n            return;\n        }\n\n        if (subNames.length > 0) {\n            await Promise.all(\n                subNames.map(async (subName) => {\n                    try {\n                        await produceArtifact({\n                            type: 'subscription',\n                            name: subName,\n                            awaitCustomCache: true,\n                        });\n                    } catch (e) {\n                        // $.error(`${e.message ?? e}`);\n                    }\n                }),\n            );\n        }\n\n        await Promise.all(\n            allArtifacts.map(async (artifact) => {\n                try {\n                    if (artifact.sync && artifact.source) {\n                        $.info(`正在同步云配置：${artifact.name}...`);\n\n                        const useMihomoExternal =\n                            artifact.platform === 'SurgeMac';\n\n                        if (useMihomoExternal) {\n                            $.info(\n                                `手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,\n                            );\n                        }\n\n                        const output = await produceArtifact({\n                            type: artifact.type,\n                            name: artifact.source,\n                            platform: artifact.platform,\n                            produceOpts: {\n                                'include-unsupported-proxy':\n                                    artifact.includeUnsupportedProxy,\n                                useMihomoExternal,\n                            },\n                        });\n\n                        // if (!output || output.length === 0)\n                        //     throw new Error('该配置的结果为空 不进行上传');\n\n                        files[encodeURIComponent(artifact.name)] = {\n                            content: output,\n                        };\n\n                        valid.push(artifact.name);\n                    }\n                } catch (e) {\n                    $.error(\n                        `生成同步配置 ${artifact.name} 发生错误: ${\n                            e.message ?? e\n                        }`,\n                    );\n                    invalid.push(artifact.name);\n                }\n            }),\n        );\n\n        $.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);\n        $.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);\n\n        if (valid.length === 0) {\n            throw new Error(\n                `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,\n            );\n        }\n\n        const resp = await syncToGist(files);\n        const body = JSON.parse(resp.body);\n\n        delete body.history;\n        delete body.forks;\n        delete body.owner;\n        Object.values(body.files).forEach((file) => {\n            delete file.content;\n        });\n        $.info('上传配置响应:');\n        $.info(JSON.stringify(body, null, 2));\n\n        for (const artifact of allArtifacts) {\n            if (\n                artifact.sync &&\n                artifact.source &&\n                valid.includes(artifact.name)\n            ) {\n                artifact.updated = new Date().getTime();\n                // extract real url from gist\n                let files = body.files;\n                let isGitLab;\n                if (Array.isArray(files)) {\n                    isGitLab = true;\n                    files = Object.fromEntries(\n                        files.map((item) => [item.path, item]),\n                    );\n                }\n                const raw_url =\n                    files[encodeURIComponent(artifact.name)]?.raw_url;\n                const new_url = isGitLab\n                    ? raw_url\n                    : raw_url?.replace(/\\/raw\\/[^/]*\\/(.*)/, '/raw/$1');\n                $.info(\n                    `上传配置完成\\n文件列表: ${Object.keys(files).join(\n                        ', ',\n                    )}\\n当前文件: ${encodeURIComponent(\n                        artifact.name,\n                    )}\\n响应返回的原始链接: ${raw_url}\\n处理完的新链接: ${new_url}`,\n                );\n                artifact.url = new_url;\n            }\n        }\n\n        $.write(allArtifacts, ARTIFACTS_KEY);\n        $.info('上传配置成功');\n\n        if (invalid.length > 0) {\n            throw new Error(\n                `同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,\n            );\n        } else {\n            $.info(`同步配置成功 ${valid.length} 个`);\n        }\n    } catch (e) {\n        $.error(`同步配置失败，原因：${e.message ?? e}`);\n        throw e;\n    }\n}\nasync function syncAllArtifacts(_, res) {\n    $.info('开始同步所有远程配置...');\n    try {\n        await syncArtifacts();\n        success(res);\n    } catch (e) {\n        $.error(`同步配置失败，原因：${e.message ?? e}`);\n        failed(\n            res,\n            new InternalServerError(\n                `FAILED_TO_SYNC_ARTIFACTS`,\n                `Failed to sync all artifacts`,\n                `Reason: ${e.message ?? e}`,\n            ),\n        );\n    }\n}\n\nasync function syncArtifact(req, res) {\n    let { name } = req.params;\n    $.info(`开始同步远程配置 ${name}...`);\n    const allArtifacts = $.read(ARTIFACTS_KEY);\n    const artifact = findByName(allArtifacts, name);\n\n    if (!artifact) {\n        $.error(`找不到远程配置 ${name}`);\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_NOT_FOUND',\n                `找不到远程配置 ${name}`,\n            ),\n            404,\n        );\n        return;\n    }\n\n    if (!artifact.source) {\n        $.error(`远程配置 ${name} 未设置来源`);\n        failed(\n            res,\n            new ResourceNotFoundError(\n                'RESOURCE_HAS_NO_SOURCE',\n                `远程配置 ${name} 未设置来源`,\n            ),\n            404,\n        );\n        return;\n    }\n\n    try {\n        const useMihomoExternal = artifact.platform === 'SurgeMac';\n\n        if (useMihomoExternal) {\n            $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);\n        }\n        const output = await produceArtifact({\n            type: artifact.type,\n            name: artifact.source,\n            platform: artifact.platform,\n            produceOpts: {\n                'include-unsupported-proxy': artifact.includeUnsupportedProxy,\n                useMihomoExternal,\n            },\n        });\n\n        $.info(\n            `正在上传配置：${artifact.name}\\n>>>${JSON.stringify(\n                artifact,\n                null,\n                2,\n            )}`,\n        );\n        // if (!output || output.length === 0)\n        //     throw new Error('该配置的结果为空 不进行上传');\n        const resp = await syncToGist({\n            [encodeURIComponent(artifact.name)]: {\n                content: output,\n            },\n        });\n        artifact.updated = new Date().getTime();\n        const body = JSON.parse(resp.body);\n\n        delete body.history;\n        delete body.forks;\n        delete body.owner;\n        Object.values(body.files).forEach((file) => {\n            delete file.content;\n        });\n        $.info('上传配置响应:');\n        $.info(JSON.stringify(body, null, 2));\n\n        let files = body.files;\n        let isGitLab;\n        if (Array.isArray(files)) {\n            isGitLab = true;\n            files = Object.fromEntries(files.map((item) => [item.path, item]));\n        }\n        const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;\n        const new_url = isGitLab\n            ? raw_url\n            : raw_url?.replace(/\\/raw\\/[^/]*\\/(.*)/, '/raw/$1');\n        $.info(\n            `上传配置完成\\n文件列表: ${Object.keys(files).join(\n                ', ',\n            )}\\n当前文件: ${encodeURIComponent(\n                artifact.name,\n            )}\\n响应返回的原始链接: ${raw_url}\\n处理完的新链接: ${new_url}`,\n        );\n        artifact.url = new_url;\n        $.write(allArtifacts, ARTIFACTS_KEY);\n        success(res, artifact);\n    } catch (err) {\n        $.error(`远程配置 ${artifact.name} 发生错误: ${err.message ?? err}`);\n        failed(\n            res,\n            new InternalServerError(\n                `FAILED_TO_SYNC_ARTIFACT`,\n                `Failed to sync artifact ${name}`,\n                `Reason: ${err}`,\n            ),\n        );\n    }\n}\n\nexport { produceArtifact, syncArtifacts };\n"
  },
  {
    "path": "backend/src/restful/token.js",
    "content": "import { ENV } from '@/vendor/open-api';\nimport { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants';\nimport { failed, success } from '@/restful/response';\nimport $ from '@/core/app';\nimport { RequestInvalidError, InternalServerError } from '@/restful/errors';\n\nexport default function register($app) {\n    if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY);\n\n    $app.post('/api/token', signToken);\n\n    $app.route('/api/token/:token').delete(deleteToken);\n\n    $app.route('/api/tokens').get(getAllTokens);\n}\n\nfunction deleteToken(req, res) {\n    let { token } = req.params;\n    const { type, name } = req.query;\n    if (!type || !name)\n        return failed(\n            res,\n            new RequestInvalidError(\n                'INVALID_PAYLOAD',\n                `Payload type and name are required. Please update your front-end(version >= 2.15.76)`,\n            ),\n        );\n    $.info(`正在删除...\\ntoken: ${token}, 类型：${type}, 名称：${name}`);\n    let allTokens = $.read(TOKENS_KEY);\n    allTokens = allTokens.filter(\n        (t) => !(t.token === token && t.type === type && t.name === name),\n    );\n    $.write(allTokens, TOKENS_KEY);\n    success(res);\n}\n\nfunction getAllTokens(req, res) {\n    const { type, name } = req.query;\n    const allTokens = $.read(TOKENS_KEY) || [];\n    success(\n        res,\n        type || name\n            ? allTokens.filter(\n                  (item) =>\n                      (type ? item.type === type : true) &&\n                      (name ? item.name === name : true),\n              )\n            : allTokens,\n    );\n}\n\nasync function signToken(req, res) {\n    if (!ENV().isNode) {\n        return failed(\n            res,\n            new RequestInvalidError(\n                'INVALID_ENV',\n                `This endpoint is only available in Node.js environment`,\n            ),\n        );\n    }\n    try {\n        const { payload, options } = req.body;\n        const ms = eval(`require(\"ms\")`);\n        const type = payload?.type;\n        const name = payload?.name;\n        if (!type || !name)\n            return failed(\n                res,\n                new RequestInvalidError(\n                    'INVALID_PAYLOAD',\n                    `payload type and name are required`,\n                ),\n            );\n        let token = payload?.token;\n        if (token != null) {\n            if (typeof token !== 'string' || token.length < 1) {\n                return failed(\n                    res,\n                    new RequestInvalidError(\n                        'INVALID_CUSTOM_TOKEN',\n                        `Invalid custom token: ${token}`,\n                    ),\n                );\n            }\n            const tokens = $.read(TOKENS_KEY) || [];\n            if (\n                tokens.find(\n                    (t) =>\n                        t.token === token && t.type === type && t.name === name,\n                )\n            ) {\n                return failed(\n                    res,\n                    new RequestInvalidError(\n                        'DUPLICATE_TOKEN',\n                        `Token ${token} already exists`,\n                    ),\n                );\n            }\n        }\n\n        if (type === 'col') {\n            const collections = $.read(COLLECTIONS_KEY) || [];\n            const collection = collections.find((c) => c.name === name);\n            if (!collection)\n                return failed(\n                    res,\n                    new RequestInvalidError(\n                        'INVALID_COLLECTION',\n                        `collection ${name} not found`,\n                    ),\n                );\n        } else if (type === 'file') {\n            const files = $.read(FILES_KEY) || [];\n            const file = files.find((f) => f.name === name);\n            if (!file)\n                return failed(\n                    res,\n                    new RequestInvalidError(\n                        'INVALID_FILE',\n                        `file ${name} not found`,\n                    ),\n                );\n        } else if (type === 'sub') {\n            const subs = $.read(SUBS_KEY) || [];\n            const sub = subs.find((s) => s.name === name);\n            if (!sub)\n                return failed(\n                    res,\n                    new RequestInvalidError(\n                        'INVALID_SUB',\n                        `sub ${name} not found`,\n                    ),\n                );\n        } else {\n            return failed(\n                res,\n                new RequestInvalidError(\n                    'INVALID_TYPE',\n                    `type ${name} not supported`,\n                ),\n            );\n        }\n        let expiresIn = options?.expiresIn;\n        if (options?.expiresIn != null) {\n            expiresIn = ms(options.expiresIn);\n            if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {\n                return failed(\n                    res,\n                    new RequestInvalidError(\n                        'INVALID_EXPIRES_IN',\n                        `Invalid expiresIn option: ${options.expiresIn}`,\n                    ),\n                );\n            }\n        }\n        // const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');\n        const nanoid = eval(`require(\"nanoid\")`);\n        const tokens = $.read(TOKENS_KEY) || [];\n        // const now = Date.now();\n        // for (const key in tokens) {\n        //     const token = tokens[key];\n        //     if (token.exp != null || token.exp < now) {\n        //         delete tokens[key];\n        //     }\n        // }\n        if (!token) {\n            do {\n                token = nanoid.customAlphabet(nanoid.urlAlphabet)();\n            } while (\n                tokens.find(\n                    (t) =>\n                        t.token === token && t.type === type && t.name === name,\n                )\n            );\n        }\n        tokens.push({\n            ...payload,\n            token,\n            createdAt: Date.now(),\n            expiresIn: expiresIn > 0 ? options?.expiresIn : undefined,\n            exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,\n        });\n\n        $.write(tokens, TOKENS_KEY);\n        return success(res, {\n            token,\n            // secret,\n        });\n    } catch (e) {\n        return failed(\n            res,\n            new InternalServerError(\n                'TOKEN_SIGN_FAILED',\n                `Failed to sign token`,\n                `Reason: ${e.message ?? e}`,\n            ),\n        );\n    }\n}\n"
  },
  {
    "path": "backend/src/test/proxy-parsers/loon.spec.js",
    "content": "import getLoonParser from '@/core/proxy-utils/parsers/peggy/loon';\nimport { describe, it } from 'mocha';\nimport testcases from './testcases';\nimport { expect } from 'chai';\n\nconst parser = getLoonParser();\n\ndescribe('Loon', function () {\n    describe('shadowsocks', function () {\n        it('test shadowsocks simple', function () {\n            const { input, expected } = testcases.SS.SIMPLE;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks obfs + tls', function () {\n            const { input, expected } = testcases.SS.OBFS_TLS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks obfs + http', function () {\n            const { input, expected } = testcases.SS.OBFS_HTTP;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('shadowsocksr', function () {\n        it('test shadowsocksr simple', function () {\n            const { input, expected } = testcases.SSR.SIMPLE;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('trojan', function () {\n        it('test trojan simple', function () {\n            const { input, expected } = testcases.TROJAN.SIMPLE;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + ws', function () {\n            const { input, expected } = testcases.TROJAN.WS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + wss', function () {\n            const { input, expected } = testcases.TROJAN.WSS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('vmess', function () {\n        it('test vmess simple', function () {\n            const { input, expected } = testcases.VMESS.SIMPLE;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vmess + aead', function () {\n            const { input, expected } = testcases.VMESS.AEAD;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vmess + ws', function () {\n            const { input, expected } = testcases.VMESS.WS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vmess + wss', function () {\n            const { input, expected } = testcases.VMESS.WSS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vmess + http', function () {\n            const { input, expected } = testcases.VMESS.HTTP;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vmess + http + tls', function () {\n            const { input, expected } = testcases.VMESS.HTTP_TLS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n    });\n\n    describe('vless', function () {\n        it('test vless simple', function () {\n            const { input, expected } = testcases.VLESS.SIMPLE;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vless + ws', function () {\n            const { input, expected } = testcases.VLESS.WS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vless + wss', function () {\n            const { input, expected } = testcases.VLESS.WSS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vless + http', function () {\n            const { input, expected } = testcases.VLESS.HTTP;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n\n        it('test vless + http + tls', function () {\n            const { input, expected } = testcases.VLESS.HTTP_TLS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected.Loon);\n        });\n    });\n\n    describe('http(s)', function () {\n        it('test http simple', function () {\n            const { input, expected } = testcases.HTTP.SIMPLE;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n\n        it('test http with authentication', function () {\n            const { input, expected } = testcases.HTTP.AUTH;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n\n        it('test https', function () {\n            const { input, expected } = testcases.HTTP.TLS;\n            const proxy = parser.parse(input.Loon);\n            expect(proxy).eql(expected);\n        });\n    });\n});\n"
  },
  {
    "path": "backend/src/test/proxy-parsers/qx.spec.js",
    "content": "import getQXParser from '@/core/proxy-utils/parsers/peggy/qx';\nimport { describe, it } from 'mocha';\nimport testcases from './testcases';\nimport { expect } from 'chai';\n\nconst parser = getQXParser();\n\ndescribe('QX', function () {\n    describe('shadowsocks', function () {\n        it('test shadowsocks simple', function () {\n            const { input, expected } = testcases.SS.SIMPLE;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks obfs + tls', function () {\n            const { input, expected } = testcases.SS.OBFS_TLS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks obfs + http', function () {\n            const { input, expected } = testcases.SS.OBFS_HTTP;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks v2ray-plugin + ws', function () {\n            const { input, expected } = testcases.SS.V2RAY_PLUGIN_WS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks v2ray-plugin + wss', function () {\n            const { input, expected } = testcases.SS.V2RAY_PLUGIN_WSS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('shadowsocksr', function () {\n        it('test shadowsocksr simple', function () {\n            const { input, expected } = testcases.SSR.SIMPLE;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('trojan', function () {\n        it('test trojan simple', function () {\n            const { input, expected } = testcases.TROJAN.SIMPLE;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + ws', function () {\n            const { input, expected } = testcases.TROJAN.WS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + wss', function () {\n            const { input, expected } = testcases.TROJAN.WSS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + tls fingerprint', function () {\n            const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('vmess', function () {\n        it('test vmess simple', function () {\n            const { input, expected } = testcases.VMESS.SIMPLE;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected.QX);\n        });\n\n        it('test vmess aead', function () {\n            const { input, expected } = testcases.VMESS.AEAD;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected.QX);\n        });\n\n        it('test vmess + ws', function () {\n            const { input, expected } = testcases.VMESS.WS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected.QX);\n        });\n\n        it('test vmess + wss', function () {\n            const { input, expected } = testcases.VMESS.WSS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected.QX);\n        });\n\n        it('test vmess + http', function () {\n            const { input, expected } = testcases.VMESS.HTTP;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected.QX);\n        });\n    });\n\n    describe('http', function () {\n        it('test http simple', function () {\n            const { input, expected } = testcases.HTTP.SIMPLE;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test http with authentication', function () {\n            const { input, expected } = testcases.HTTP.AUTH;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test https', function () {\n            const { input, expected } = testcases.HTTP.TLS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('socks5', function () {\n        it('test socks5 simple', function () {\n            const { input, expected } = testcases.SOCKS5.SIMPLE;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test socks5 with authentication', function () {\n            const { input, expected } = testcases.SOCKS5.AUTH;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n\n        it('test socks5 + tls', function () {\n            const { input, expected } = testcases.SOCKS5.TLS;\n            const proxy = parser.parse(input.QX);\n            expect(proxy).eql(expected);\n        });\n    });\n});\n"
  },
  {
    "path": "backend/src/test/proxy-parsers/surge.spec.js",
    "content": "import getSurgeParser from '@/core/proxy-utils/parsers/peggy/surge';\nimport { describe, it } from 'mocha';\nimport testcases from './testcases';\nimport { expect } from 'chai';\n\nconst parser = getSurgeParser();\n\ndescribe('Surge', function () {\n    describe('shadowsocks', function () {\n        it('test shadowsocks simple', function () {\n            const { input, expected } = testcases.SS.SIMPLE;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks obfs + tls', function () {\n            const { input, expected } = testcases.SS.OBFS_TLS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n        it('test shadowsocks obfs + http', function () {\n            const { input, expected } = testcases.SS.OBFS_HTTP;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('trojan', function () {\n        it('test trojan simple', function () {\n            const { input, expected } = testcases.TROJAN.SIMPLE;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + ws', function () {\n            const { input, expected } = testcases.TROJAN.WS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + wss', function () {\n            const { input, expected } = testcases.TROJAN.WSS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test trojan + tls fingerprint', function () {\n            const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('vmess', function () {\n        it('test vmess simple', function () {\n            const { input, expected } = testcases.VMESS.SIMPLE;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected.Surge);\n        });\n\n        it('test vmess aead', function () {\n            const { input, expected } = testcases.VMESS.AEAD;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected.Surge);\n        });\n\n        it('test vmess + ws', function () {\n            const { input, expected } = testcases.VMESS.WS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected.Surge);\n        });\n\n        it('test vmess + wss', function () {\n            const { input, expected } = testcases.VMESS.WSS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected.Surge);\n        });\n    });\n\n    describe('http', function () {\n        it('test http simple', function () {\n            const { input, expected } = testcases.HTTP.SIMPLE;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test http with authentication', function () {\n            const { input, expected } = testcases.HTTP.AUTH;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test https', function () {\n            const { input, expected } = testcases.HTTP.TLS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('socks5', function () {\n        it('test socks5 simple', function () {\n            const { input, expected } = testcases.SOCKS5.SIMPLE;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test socks5 with authentication', function () {\n            const { input, expected } = testcases.SOCKS5.AUTH;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test socks5 + tls', function () {\n            const { input, expected } = testcases.SOCKS5.TLS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n    });\n\n    describe('snell', function () {\n        it('test snell simple', function () {\n            const { input, expected } = testcases.SNELL.SIMPLE;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test snell obfs + http', function () {\n            const { input, expected } = testcases.SNELL.OBFS_HTTP;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n\n        it('test snell obfs + tls', function () {\n            const { input, expected } = testcases.SNELL.OBFS_TLS;\n            const proxy = parser.parse(input.Surge);\n            expect(proxy).eql(expected);\n        });\n    });\n});\n"
  },
  {
    "path": "backend/src/test/proxy-parsers/testcases.js",
    "content": "function createTestCases() {\n    const name = 'name';\n    const server = 'example.com';\n    const port = 10086;\n\n    const cipher = 'chacha20';\n\n    const username = 'username';\n    const password = 'password';\n\n    const obfs_host = 'obfs.com';\n    const obfs_path = '/resource/file';\n\n    const ssr_protocol = 'auth_chain_b';\n    const ssr_protocol_param = 'def';\n    const ssr_obfs = 'tls1.2_ticket_fastauth';\n    const ssr_obfs_param = 'obfs.com';\n\n    const uuid = '23ad6b10-8d1a-40f7-8ad0-e3e35cd32291';\n\n    const sni = 'sni.com';\n\n    const tls_fingerprint =\n        '67:1B:C8:F2:D4:60:DD:A7:EE:60:DA:BB:A3:F9:A4:D7:C8:29:0F:3E:2F:75:B6:A9:46:88:48:7D:D3:97:7E:98';\n\n    const SS = {\n        SIMPLE: {\n            input: {\n                Loon: `${name}=shadowsocks,${server},${port},${cipher},\"${password}\"`,\n                QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},tag=${name}`,\n                Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password}`,\n            },\n            expected: {\n                type: 'ss',\n                name,\n                server,\n                port,\n                cipher,\n                password,\n            },\n        },\n        OBFS_TLS: {\n            input: {\n                Loon: `${name}=shadowsocks,${server},${port},${cipher},\"${password}\",obfs-name=tls,obfs-uri=${obfs_path},obfs-host=${obfs_host}`,\n                QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n                Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password},obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,\n            },\n            expected: {\n                type: 'ss',\n                name,\n                server,\n                port,\n                cipher,\n                password,\n                plugin: 'obfs',\n                'plugin-opts': {\n                    mode: 'tls',\n                    path: obfs_path,\n                    host: obfs_host,\n                },\n            },\n        },\n        OBFS_HTTP: {\n            input: {\n                Loon: `${name}=shadowsocks,${server},${port},${cipher},\"${password}\",obfs-name=http,obfs-uri=${obfs_path},obfs-host=${obfs_host}`,\n                QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n                Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,\n            },\n            expected: {\n                type: 'ss',\n                name,\n                server,\n                port,\n                cipher,\n                password,\n                plugin: 'obfs',\n                'plugin-opts': {\n                    mode: 'http',\n                    path: obfs_path,\n                    host: obfs_host,\n                },\n            },\n        },\n        V2RAY_PLUGIN_WS: {\n            input: {\n                QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n            },\n            expected: {\n                type: 'ss',\n                name,\n                server,\n                port,\n                cipher,\n                password,\n                plugin: 'v2ray-plugin',\n                'plugin-opts': {\n                    mode: 'websocket',\n                    path: obfs_path,\n                    host: obfs_host,\n                },\n            },\n        },\n        V2RAY_PLUGIN_WSS: {\n            input: {\n                QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n            },\n            expected: {\n                type: 'ss',\n                name,\n                server,\n                port,\n                cipher,\n                password,\n                plugin: 'v2ray-plugin',\n                'plugin-opts': {\n                    mode: 'websocket',\n                    path: obfs_path,\n                    host: obfs_host,\n                    tls: true,\n                },\n            },\n        },\n    };\n    const SSR = {\n        SIMPLE: {\n            input: {\n                QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},ssr-protocol=${ssr_protocol},ssr-protocol-param=${ssr_protocol_param},obfs=${ssr_obfs},obfs-host=${ssr_obfs_param},tag=${name}`,\n                Loon: `${name}=shadowsocksr,${server},${port},${cipher},\"${password}\",protocol=${ssr_protocol},protocol-param=${ssr_protocol_param},obfs=${ssr_obfs},obfs-param=${ssr_obfs_param}`,\n            },\n            expected: {\n                type: 'ssr',\n                name,\n                server,\n                port,\n                cipher,\n                password,\n                obfs: ssr_obfs,\n                protocol: ssr_protocol,\n                'obfs-param': ssr_obfs_param,\n                'protocol-param': ssr_protocol_param,\n            },\n        },\n    };\n    const TROJAN = {\n        SIMPLE: {\n            input: {\n                QX: `trojan=${server}:${port},password=${password},tag=${name}`,\n                Loon: `${name}=trojan,${server},${port},\"${password}\"`,\n                Surge: `${name}=trojan,${server},${port},password=${password}`,\n            },\n            expected: {\n                type: 'trojan',\n                name,\n                server,\n                port,\n                password,\n            },\n        },\n        WS: {\n            input: {\n                QX: `trojan=${server}:${port},password=${password},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n                Loon: `${name}=trojan,${server},${port},\"${password}\",transport=ws,path=${obfs_path},host=${obfs_host}`,\n                Surge: `${name}=trojan,${server},${port},password=${password},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host}`,\n            },\n            expected: {\n                type: 'trojan',\n                name,\n                server,\n                port,\n                password,\n                network: 'ws',\n                'ws-opts': {\n                    path: obfs_path,\n                    headers: {\n                        Host: obfs_host,\n                    },\n                },\n            },\n        },\n        WSS: {\n            input: {\n                QX: `trojan=${server}:${port},password=${password},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tls-verification=false,tls-host=${sni},tag=${name}`,\n                Loon: `${name}=trojan,${server},${port},\"${password}\",transport=ws,path=${obfs_path},host=${obfs_host},over-tls=true,tls-name=${sni},skip-cert-verify=true`,\n                Surge: `${name}=trojan,${server},${port},password=${password},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host},skip-cert-verify=true,sni=${sni},tls=true`,\n            },\n            expected: {\n                type: 'trojan',\n                name,\n                server,\n                port,\n                password,\n                network: 'ws',\n                tls: true,\n                'ws-opts': {\n                    path: obfs_path,\n                    headers: {\n                        Host: obfs_host,\n                    },\n                },\n                'skip-cert-verify': true,\n                sni,\n            },\n        },\n        TLS_FINGERPRINT: {\n            input: {\n                QX: `trojan=${server}:${port},password=${password},tls-verification=false,tls-host=${sni},tls-cert-sha256=${tls_fingerprint},tag=${name},over-tls=true`,\n                Surge: `${name}=trojan,${server},${port},password=${password},skip-cert-verify=true,sni=${sni},tls=true,server-cert-fingerprint-sha256=${tls_fingerprint}`,\n            },\n            expected: {\n                type: 'trojan',\n                name,\n                server,\n                port,\n                password,\n                tls: true,\n                'skip-cert-verify': true,\n                sni,\n                'tls-fingerprint': tls_fingerprint,\n            },\n        },\n    };\n    const VMESS = {\n        SIMPLE: {\n            input: {\n                QX: `vmess=${server}:${port},method=${cipher},password=${uuid},tag=${name}`,\n                Loon: `${name}=vmess,${server},${port},${cipher},\"${uuid}\"`,\n                Surge: `${name}=vmess,${server},${port},username=${uuid}`,\n            },\n            expected: {\n                QX: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    alterId: 0,\n                },\n                Loon: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    alterId: 0,\n                },\n                Surge: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!\n                    alterId: 0,\n                },\n            },\n        },\n        AEAD: {\n            input: {\n                QX: `vmess=${server}:${port},method=${cipher},password=${uuid},aead=true,tag=${name}`,\n                Loon: `${name}=vmess,${server},${port},${cipher},\"${uuid}\",alterId=0`,\n                Surge: `${name}=vmess,${server},${port},username=${uuid},vmess-aead=true`,\n            },\n            expected: {\n                QX: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    aead: true,\n                    alterId: 0,\n                },\n                Loon: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    alterId: 0,\n                },\n                Surge: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!\n                    alterId: 0,\n                    aead: true,\n                },\n            },\n        },\n        WS: {\n            input: {\n                QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n                Loon: `${name}=vmess,${server},${port},${cipher},\"${uuid}\",transport=ws,host=${obfs_host},path=${obfs_path}`,\n                Surge: `${name}=vmess,${server},${port},username=${uuid},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host}`,\n            },\n            expected: {\n                QX: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    alterId: 0,\n                },\n                Loon: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    alterId: 0,\n                },\n                Surge: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    alterId: 0,\n                },\n            },\n        },\n        WSS: {\n            input: {\n                QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tls-verification=false,tls-host=${sni},tag=${name}`,\n                Loon: `${name}=vmess,${server},${port},${cipher},\"${uuid}\",transport=ws,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,\n                Surge: `${name}=vmess,${server},${port},username=${uuid},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host},skip-cert-verify=true,sni=${sni},tls=true`,\n            },\n            expected: {\n                QX: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    tls: true,\n                    'skip-cert-verify': true,\n                    sni,\n                    alterId: 0,\n                },\n                Loon: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    tls: true,\n                    'skip-cert-verify': true,\n                    sni,\n                    alterId: 0,\n                },\n                Surge: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    tls: true,\n                    'skip-cert-verify': true,\n                    sni,\n                    alterId: 0,\n                },\n            },\n        },\n        HTTP: {\n            input: {\n                QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,\n                Loon: `${name}=vmess,${server},${port},${cipher},\"${uuid}\",transport=http,host=${obfs_host},path=${obfs_path}`,\n            },\n            expected: {\n                QX: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'http',\n                    'http-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    alterId: 0,\n                },\n                Loon: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'http',\n                    'http-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    alterId: 0,\n                },\n            },\n        },\n        HTTP_TLS: {\n            input: {\n                Loon: `${name}=vmess,${server},${port},${cipher},\"${uuid}\",transport=http,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,\n            },\n            expected: {\n                Loon: {\n                    type: 'vmess',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    cipher,\n                    network: 'http',\n                    'http-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    tls: true,\n                    'skip-cert-verify': true,\n                    sni,\n                    alterId: 0,\n                },\n            },\n        },\n    };\n    const VLESS = {\n        SIMPLE: {\n            input: {\n                Loon: `${name}=vless,${server},${port},\"${uuid}\"`,\n            },\n            expected: {\n                Loon: {\n                    type: 'vless',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                },\n            },\n        },\n        WS: {\n            input: {\n                Loon: `${name}=vless,${server},${port},\"${uuid}\",transport=ws,host=${obfs_host},path=${obfs_path}`,\n            },\n            expected: {\n                Loon: {\n                    type: 'vless',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                },\n            },\n        },\n        WSS: {\n            input: {\n                Loon: `${name}=vless,${server},${port},\"${uuid}\",transport=ws,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,\n            },\n            expected: {\n                Loon: {\n                    type: 'vless',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    network: 'ws',\n                    'ws-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    tls: true,\n                    'skip-cert-verify': true,\n                    sni,\n                },\n            },\n        },\n        HTTP: {\n            input: {\n                Loon: `${name}=vless,${server},${port},\"${uuid}\",transport=http,host=${obfs_host},path=${obfs_path}`,\n            },\n            expected: {\n                Loon: {\n                    type: 'vless',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    network: 'http',\n                    'http-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                },\n            },\n        },\n        HTTP_TLS: {\n            input: {\n                Loon: `${name}=vless,${server},${port},\"${uuid}\",transport=http,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,\n            },\n            expected: {\n                Loon: {\n                    type: 'vless',\n                    name,\n                    server,\n                    port,\n                    uuid,\n                    network: 'http',\n                    'http-opts': {\n                        path: obfs_path,\n                        headers: {\n                            Host: obfs_host,\n                        },\n                    },\n                    tls: true,\n                    'skip-cert-verify': true,\n                    sni,\n                },\n            },\n        },\n    };\n    const HTTP = {\n        SIMPLE: {\n            input: {\n                Loon: `${name}=http,${server},${port}`,\n                QX: `http=${server}:${port},tag=${name}`,\n                Surge: `${name}=http,${server},${port}`,\n            },\n            expected: {\n                type: 'http',\n                name,\n                server,\n                port,\n            },\n        },\n        AUTH: {\n            input: {\n                Loon: `${name}=http,${server},${port},${username},\"${password}\"`,\n                QX: `http=${server}:${port},tag=${name},username=${username},password=${password}`,\n                Surge: `${name}=http,${server},${port},${username},${password}`,\n            },\n            expected: {\n                type: 'http',\n                name,\n                server,\n                port,\n                username,\n                password,\n            },\n        },\n        TLS: {\n            input: {\n                Loon: `${name}=https,${server},${port},${username},\"${password}\",tls-name=${sni},skip-cert-verify=true`,\n                QX: `http=${server}:${port},username=${username},password=${password},over-tls=true,tls-host=${sni},tls-verification=false,tag=${name}`,\n                Surge: `${name}=https,${server},${port},${username},${password},sni=${sni},skip-cert-verify=true`,\n            },\n            expected: {\n                type: 'http',\n                name,\n                server,\n                port,\n                username,\n                password,\n                sni,\n                'skip-cert-verify': true,\n                tls: true,\n            },\n        },\n    };\n    const SOCKS5 = {\n        SIMPLE: {\n            input: {\n                QX: `socks5=${server}:${port},tag=${name}`,\n                Surge: `${name}=socks5,${server},${port}`,\n            },\n            expected: {\n                type: 'socks5',\n                name,\n                server,\n                port,\n            },\n        },\n        AUTH: {\n            input: {\n                QX: `socks5=${server}:${port},tag=${name},username=${username},password=${password}`,\n                Surge: `${name}=socks5,${server},${port},${username},${password}`,\n            },\n            expected: {\n                type: 'socks5',\n                name,\n                server,\n                port,\n                username,\n                password,\n            },\n        },\n        TLS: {\n            input: {\n                QX: `socks5=${server}:${port},username=${username},password=${password},over-tls=true,tls-host=${sni},tls-verification=false,tag=${name}`,\n                Surge: `${name}=socks5-tls,${server},${port},${username},${password},sni=${sni},skip-cert-verify=true`,\n            },\n            expected: {\n                type: 'socks5',\n                name,\n                server,\n                port,\n                username,\n                password,\n                sni,\n                'skip-cert-verify': true,\n                tls: true,\n            },\n        },\n    };\n    const SNELL = {\n        SIMPLE: {\n            input: {\n                Surge: `${name}=snell,${server},${port},psk=${password},version=3`,\n            },\n            expected: {\n                type: 'snell',\n                name,\n                server,\n                port,\n                psk: password,\n                version: 3,\n            },\n        },\n        OBFS_HTTP: {\n            input: {\n                Surge: `${name}=snell,${server},${port},psk=${password},version=3,obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,\n            },\n            expected: {\n                type: 'snell',\n                name,\n                server,\n                port,\n                psk: password,\n                version: 3,\n                'obfs-opts': {\n                    mode: 'http',\n                    host: obfs_host,\n                    path: obfs_path,\n                },\n            },\n        },\n        OBFS_TLS: {\n            input: {\n                Surge: `${name}=snell,${server},${port},psk=${password},version=3,obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,\n            },\n            expected: {\n                type: 'snell',\n                name,\n                server,\n                port,\n                psk: password,\n                version: 3,\n                'obfs-opts': {\n                    mode: 'tls',\n                    host: obfs_host,\n                    path: obfs_path,\n                },\n            },\n        },\n    };\n    return {\n        SS,\n        SSR,\n        VMESS,\n        VLESS,\n        TROJAN,\n        HTTP,\n        SOCKS5,\n        SNELL,\n    };\n}\n\nexport default createTestCases();\n"
  },
  {
    "path": "backend/src/utils/database.js",
    "content": "export function findByName(list, name, field = 'name') {\n    return list.find((item) => item[field] === name);\n}\n\nexport function findIndexByName(list, name, field = 'name') {\n    return list.findIndex((item) => item[field] === name);\n}\n\nexport function deleteByName(list, name, field = 'name') {\n    const idx = findIndexByName(list, name, field);\n    list.splice(idx, 1);\n}\n\nexport function updateByName(list, name, newItem, field = 'name') {\n    const idx = findIndexByName(list, name, field);\n    list[idx] = newItem;\n}\n"
  },
  {
    "path": "backend/src/utils/dns.js",
    "content": "import $ from '@/core/app';\nimport dnsPacket from 'dns-packet';\nimport { Buffer } from 'buffer';\nimport { isIPv4 } from '@/utils';\n\nexport async function doh({ url, domain, type = 'A', timeout, edns }) {\n    const buf = dnsPacket.encode({\n        type: 'query',\n        id: 0,\n        flags: dnsPacket.RECURSION_DESIRED,\n        questions: [\n            {\n                type,\n                name: domain,\n            },\n        ],\n        additionals: [\n            {\n                type: 'OPT',\n                name: '.',\n                udpPayloadSize: 4096,\n                flags: 0,\n                options: [\n                    {\n                        code: 'CLIENT_SUBNET',\n                        ip: edns,\n                        sourcePrefixLength: isIPv4(edns) ? 24 : 56,\n                        scopePrefixLength: 0,\n                    },\n                ],\n            },\n        ],\n    });\n\n    const b64 = Buffer.from(buf).toString('base64');\n    const b64url = b64\n        .replace(/\\+/g, '-')\n        .replace(/\\//g, '_')\n        .replace(/=+$/, '');\n\n    const res = await $.http.get({\n        url: `${url}?dns=${encodeURIComponent(b64url)}`,\n        headers: {\n            Accept: 'application/dns-message',\n            // 'Content-Type': 'application/dns-message',\n        },\n        // body: buf,\n        'binary-mode': true,\n        encoding: null, // 使用 null 编码以确保响应是原始二进制数据\n        timeout,\n    });\n\n    return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body));\n}\n"
  },
  {
    "path": "backend/src/utils/download.js",
    "content": "import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';\nimport { HTTP, ENV } from '@/vendor/open-api';\nimport { hex_md5 } from '@/vendor/md5';\nimport { getPolicyDescriptor } from '@/utils';\nimport resourceCache from '@/utils/resource-cache';\nimport headersResourceCache from '@/utils/headers-resource-cache';\nimport {\n    getFlowField,\n    getFlowHeaders,\n    parseFlowHeaders,\n    validCheck,\n} from '@/utils/flow';\nimport $ from '@/core/app';\nimport { findByName } from '@/utils/database';\nimport { produceArtifact } from '@/restful/sync';\nimport PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';\nimport { ProxyUtils } from '@/core/proxy-utils';\n\nconst clashPreprocessor = PROXY_PREPROCESSORS.find(\n    (processor) => processor.name === 'Clash Pre-processor',\n);\n\nconst tasks = new Map();\n\nexport default async function download(\n    rawUrl = '',\n    ua,\n    timeout,\n    customProxy,\n    skipCustomCache,\n    awaitCustomCache,\n    noCache,\n    preprocess,\n) {\n    let $arguments = {};\n    let url = rawUrl.replace(/#noFlow$/, '');\n    const rawArgs = url.split('#');\n    url = url.split('#')[0];\n    if (rawArgs.length > 1) {\n        try {\n            // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n            $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));\n        } catch (e) {\n            for (const pair of rawArgs[1].split('&')) {\n                const key = pair.split('=')[0];\n                const value = pair.split('=')[1];\n                // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                $arguments[key] =\n                    value == null || value === ''\n                        ? true\n                        : decodeURIComponent(value);\n            }\n        }\n    }\n    const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();\n    const {\n        defaultProxy,\n        defaultUserAgent,\n        defaultTimeout,\n        cacheThreshold: defaultCacheThreshold,\n    } = $.read(SETTINGS_KEY);\n    const cacheThreshold = defaultCacheThreshold || 1024;\n    let proxy = customProxy || defaultProxy;\n    if ($.env.isNode) {\n        proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');\n    }\n    const userAgent = ua || defaultUserAgent || 'clash.meta';\n    let customHeaders;\n    if ($arguments?.headers) {\n        try {\n            const parsed = JSON.parse($arguments?.headers);\n            if (\n                parsed &&\n                typeof parsed === 'object' &&\n                !Array.isArray(parsed) &&\n                Object.keys(parsed).length > 0\n            ) {\n                const lowerCaseHeaders = { 'user-agent': userAgent };\n                for (const key in parsed) {\n                    lowerCaseHeaders[key.toLowerCase()] = parsed[key];\n                }\n                customHeaders = lowerCaseHeaders;\n            }\n        } catch (e) {\n            $.error(`解析自定义 headers 失败: ${e}`);\n        }\n    }\n\n    const requestTimeout = timeout || defaultTimeout || 8000;\n    const id = hex_md5(\n        `${customHeaders ? JSON.stringify(customHeaders) : userAgent}${url}`,\n    );\n\n    if ($arguments?.cacheKey === true) {\n        $.error(`使用自定义缓存时 cacheKey 的值不能为空`);\n        $arguments.cacheKey = undefined;\n    }\n\n    const customCacheKey = $arguments?.cacheKey\n        ? `#sub-store-cached-custom-${$arguments?.cacheKey}`\n        : undefined;\n\n    if (customCacheKey && !skipCustomCache) {\n        const customCached = $.read(customCacheKey);\n        const cached = resourceCache.get(id);\n        if (!noCache && !$arguments?.noCache && cached) {\n            $.info(\n                `乐观缓存: URL ${url}\\n存在有效的常规缓存\\n使用常规缓存以避免重复请求`,\n            );\n            return cached;\n        }\n        if (customCached) {\n            if (awaitCustomCache) {\n                $.info(`乐观缓存: URL ${url}\\n本次进行请求 尝试更新缓存`);\n                try {\n                    await download(\n                        rawUrl.replace(/(\\?|&)cacheKey=.*?(&|$)/, ''),\n                        ua,\n                        timeout,\n                        proxy,\n                        true,\n                        undefined,\n                        undefined,\n                        preprocess,\n                    );\n                } catch (e) {\n                    $.error(\n                        `乐观缓存: URL ${url} 更新缓存发生错误 ${\n                            e.message ?? e\n                        }`,\n                    );\n                    $.info('使用乐观缓存的数据刷新缓存, 防止后续请求');\n                    resourceCache.set(id, customCached);\n                }\n            } else {\n                $.info(\n                    `乐观缓存: URL ${url}\\n本次返回自定义缓存 ${$arguments?.cacheKey}\\n并进行请求 尝试异步更新缓存`,\n                );\n                download(\n                    rawUrl.replace(/(\\?|&)cacheKey=.*?(&|$)/, ''),\n                    ua,\n                    timeout,\n                    proxy,\n                    true,\n                    undefined,\n                    undefined,\n                    preprocess,\n                ).catch((e) => {\n                    $.error(\n                        `乐观缓存: URL ${url} 异步更新缓存发生错误 ${\n                            e.message ?? e\n                        }`,\n                    );\n                });\n            }\n            return customCached;\n        }\n    }\n\n    const downloadUrlMatch = url\n        .split('#')[0]\n        .match(/^\\/api\\/(file|module)\\/(.+)/);\n    if (downloadUrlMatch) {\n        let type = '';\n        try {\n            type = downloadUrlMatch?.[1];\n            let name = downloadUrlMatch?.[2];\n            if (name == null) {\n                throw new Error(`本地 ${type} URL 无效: ${url}`);\n            }\n            name = decodeURIComponent(name);\n            const key = type === 'module' ? MODULES_KEY : FILES_KEY;\n            const item = findByName($.read(key), name);\n            if (!item) {\n                throw new Error(`找不到 ${type}: ${name}`);\n            }\n\n            if (type === 'module') {\n                return item.content;\n            } else {\n                return await produceArtifact({\n                    type: 'file',\n                    name,\n                });\n            }\n        } catch (err) {\n            $.error(\n                `Error when loading ${type}: ${\n                    url.split('#')[0]\n                }.\\n Reason: ${err}`,\n            );\n            throw new Error(`无法加载 ${type}: ${url}`);\n        }\n    } else if (url?.startsWith('/')) {\n        try {\n            const fs = eval(`require(\"fs\")`);\n            return fs.readFileSync(url.split('#')[0], 'utf8');\n        } catch (err) {\n            $.error(\n                `Error when reading local file: ${\n                    url.split('#')[0]\n                }.\\n Reason: ${err}`,\n            );\n            throw new Error(`无法从该路径读取文本内容: ${url}`);\n        }\n    }\n\n    if (!isNode && tasks.has(id)) {\n        return tasks.get(id);\n    }\n\n    const http = HTTP({\n        headers: {\n            ...(customHeaders || { 'User-Agent': userAgent }),\n            ...(isStash && proxy\n                ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }\n                : {}),\n            ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),\n        },\n        timeout: requestTimeout,\n    });\n\n    let result;\n\n    // try to find in app cache\n    const cached = resourceCache.get(id);\n    if (!noCache && !$arguments?.noCache && cached) {\n        $.info(\n            `使用缓存: ${url}, ${\n                customHeaders ? JSON.stringify(customHeaders) : userAgent\n            }`,\n        );\n        result = cached;\n        if (customCacheKey) {\n            $.info(`URL ${url}\\n写入自定义缓存 ${$arguments?.cacheKey}`);\n            $.write(cached, customCacheKey);\n        }\n    } else {\n        const insecure = $arguments?.insecure\n            ? isNode\n                ? { strictSSL: false }\n                : { insecure: true }\n            : undefined;\n        $.info(\n            `Downloading...\\n${\n                customHeaders\n                    ? JSON.stringify(customHeaders)\n                    : `User-Agent: ${userAgent}`\n            }\\nTimeout: ${requestTimeout}\\nProxy: ${proxy}\\nInsecure: ${!!insecure}\\nPreprocess: ${preprocess}\\nURL: ${url}`,\n        );\n        try {\n            let { body, headers, statusCode } = await http.get({\n                url,\n                ...(proxy ? { proxy } : {}),\n                ...(isLoon && proxy ? { node: proxy } : {}),\n                ...(isQX && proxy ? { opts: { policy: proxy } } : {}),\n                ...(proxy ? getPolicyDescriptor(proxy) : {}),\n                ...(insecure ? insecure : {}),\n            });\n            $.info(`statusCode: ${statusCode}`);\n            if (statusCode < 200 || statusCode >= 400) {\n                throw new Error(`statusCode: ${statusCode}`);\n            }\n\n            if (headers) {\n                const flowInfo = getFlowField(headers);\n                if (flowInfo) {\n                    headersResourceCache.set(id, flowInfo);\n                }\n            }\n            if (body.replace(/\\s/g, '').length === 0)\n                throw new Error(new Error('远程资源内容为空'));\n            if (preprocess) {\n                try {\n                    if (clashPreprocessor.test(body)) {\n                        body = clashPreprocessor.parse(body, true);\n                    }\n                } catch (e) {\n                    $.error(`Clash Pre-processor error: ${e}`);\n                }\n            }\n            let shouldCache = true;\n            if (cacheThreshold) {\n                const size = body.length / 1024;\n                if (size > cacheThreshold) {\n                    $.info(\n                        `资源大小 ${size.toFixed(\n                            2,\n                        )} KB 超过了 ${cacheThreshold} KB, 不缓存`,\n                    );\n                    shouldCache = false;\n                }\n            }\n            if (preprocess) {\n                try {\n                    const proxies = ProxyUtils.parse(body);\n                    if (!Array.isArray(proxies) || proxies.length === 0) {\n                        $.error(`URL ${url} 不包含有效节点, 不缓存`);\n                        shouldCache = false;\n                    }\n                } catch (e) {\n                    $.error(\n                        `URL ${url} 尝试解析节点失败 ${e.message ?? e}, 不缓存`,\n                    );\n                    shouldCache = false;\n                }\n            }\n            if (shouldCache) {\n                resourceCache.set(id, body);\n                if (customCacheKey) {\n                    $.info(\n                        `URL ${url}\\n写入自定义缓存 ${$arguments?.cacheKey}`,\n                    );\n                    $.write(body, customCacheKey);\n                }\n            }\n\n            result = body;\n        } catch (e) {\n            if (customCacheKey) {\n                const cached = $.read(customCacheKey);\n                if (cached) {\n                    $.info(\n                        `无法下载 URL ${url}: ${\n                            e.message ?? e\n                        }\\n使用自定义缓存 ${$arguments?.cacheKey}`,\n                    );\n                    return cached;\n                }\n            }\n            throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);\n        }\n    }\n\n    // 检查订阅有效性\n\n    if ($arguments?.validCheck) {\n        await validCheck(\n            parseFlowHeaders(\n                await getFlowHeaders(\n                    url,\n                    $arguments.flowUserAgent,\n                    undefined,\n                    proxy,\n                    $arguments.flowUrl,\n                ),\n            ),\n        );\n    }\n\n    if (!isNode) {\n        tasks.set(id, result);\n    }\n    return result;\n}\n\nexport async function downloadFile(url, file) {\n    const undici = eval(\"require('undici')\");\n    const fs = eval(\"require('fs')\");\n    const { pipeline } = eval(\"require('stream/promises')\");\n    const { Agent, interceptors, request } = undici;\n    $.info(`Downloading file...\\nURL: ${url}\\nFile: ${file}`);\n    const { body, statusCode } = await request(url, {\n        dispatcher: new Agent().compose(\n            interceptors.redirect({\n                maxRedirections: 3,\n                throwOnRedirect: true,\n            }),\n        ),\n    });\n    if (statusCode !== 200)\n        throw new Error(`Failed to download file from ${url}`);\n    const fileStream = fs.createWriteStream(file);\n    await pipeline(body, fileStream);\n    $.info(`File downloaded from ${url} to ${file}`);\n    return file;\n}\n"
  },
  {
    "path": "backend/src/utils/env.js",
    "content": "import { version as substoreVersion } from '../../package.json';\nimport { ENV } from '@/vendor/open-api';\n\nconst {\n    isNode,\n    isQX,\n    isLoon,\n    isSurge,\n    isStash,\n    isShadowRocket,\n    isLanceX,\n    isEgern,\n    isGUIforCores,\n} = ENV();\nlet backend = 'Node';\nif (isNode) {\n    backend = 'Node';\n} else if (isQX) {\n    backend = 'QX';\n} else if (isLoon) {\n    backend = 'Loon';\n} else if (isStash) {\n    backend = 'Stash';\n} else if (isShadowRocket) {\n    backend = 'Shadowrocket';\n} else if (isEgern) {\n    backend = 'Egern';\n} else if (isSurge) {\n    backend = 'Surge';\n} else if (isLanceX) {\n    backend = 'LanceX';\n} else if (isGUIforCores) {\n    backend = 'GUI.for.Cores';\n}\n\nlet meta = {};\nlet feature = {};\n\ntry {\n    if (typeof $environment !== 'undefined') {\n        // eslint-disable-next-line no-undef\n        meta.env = $environment;\n    }\n    if (typeof $loon !== 'undefined') {\n        // eslint-disable-next-line no-undef\n        meta.loon = $loon;\n    }\n    if (typeof $script !== 'undefined') {\n        // eslint-disable-next-line no-undef\n        meta.script = $script;\n    }\n    if (typeof $Plugin !== 'undefined') {\n        // eslint-disable-next-line no-undef\n        meta.plugin = $Plugin;\n    }\n    if (isNode) {\n        meta.node = {\n            version: eval('process.version'),\n            argv: eval('process.argv'),\n            filename: eval('__filename'),\n            dirname: eval('__dirname'),\n            env: {},\n        };\n        const env = eval('process.env');\n        for (const key in env) {\n            if (/^SUB_STORE_/.test(key)) {\n                meta.node.env[key] = env[key];\n            }\n        }\n    }\n    // eslint-disable-next-line no-empty\n} catch (e) {}\n\nexport default {\n    backend,\n    version: substoreVersion,\n    feature,\n    meta,\n};\n"
  },
  {
    "path": "backend/src/utils/flow.js",
    "content": "import { SETTINGS_KEY } from '@/constants';\nimport { HTTP, ENV } from '@/vendor/open-api';\nimport { hex_md5 } from '@/vendor/md5';\nimport { getPolicyDescriptor } from '@/utils';\nimport $ from '@/core/app';\nimport headersResourceCache from '@/utils/headers-resource-cache';\n\nexport function getFlowField(headers) {\n    const keys = Object.keys(headers);\n    let sub = '';\n    let webPage = '';\n    let planName = '';\n    for (let k of keys) {\n        const lower = k.toLowerCase();\n        if (lower === 'subscription-userinfo') {\n            sub = headers[k];\n        } else if (lower === 'profile-web-page-url') {\n            webPage = headers[k];\n        } else if (lower === 'plan-name') {\n            planName = headers[k];\n        }\n    }\n\n    return `${sub || ''}${\n        webPage ? `; app_url=${encodeURIComponent(webPage)}` : ''\n    }${planName ? `; plan_name=${encodeURIComponent(planName)}` : ''}`;\n}\nexport async function getFlowHeaders(\n    rawUrl,\n    ua,\n    timeout,\n    customProxy,\n    flowUrl,\n) {\n    let url = flowUrl || rawUrl || '';\n    let $arguments = {};\n    const rawArgs = url.split('#');\n    url = url.split('#')[0];\n    if (rawArgs.length > 1) {\n        try {\n            // 支持 `#${encodeURIComponent(JSON.stringify({arg1: \"1\"}))}`\n            $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));\n        } catch (e) {\n            for (const pair of rawArgs[1].split('&')) {\n                const key = pair.split('=')[0];\n                const value = pair.split('=')[1];\n                // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;\n                $arguments[key] =\n                    value == null || value === ''\n                        ? true\n                        : decodeURIComponent(value);\n            }\n        }\n    }\n    if ($arguments?.noFlow || !/^https?/.test(url)) {\n        return;\n    }\n    const { isStash, isLoon, isShadowRocket, isQX } = ENV();\n    const insecure = $arguments?.insecure\n        ? $.env.isNode\n            ? { strictSSL: false }\n            : { insecure: true }\n        : undefined;\n    const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =\n        $.read(SETTINGS_KEY);\n    let proxy = customProxy || defaultProxy;\n    if ($.env.isNode) {\n        proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');\n    }\n    const userAgent = ua || defaultFlowUserAgent || 'clash.meta/v1.19.16';\n    const requestTimeout = timeout || defaultTimeout || 8000;\n    const id = hex_md5(userAgent + url);\n    const cached = headersResourceCache.get(id);\n    let flowInfo;\n    if (!$arguments?.noCache && cached) {\n        $.info(`使用缓存的流量信息: ${url}, ${userAgent}`);\n        flowInfo = cached;\n    } else {\n        const http = HTTP();\n        if (flowUrl) {\n            let flowUrlHeaders;\n            try {\n                $.info(\n                    `使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${\n                        userAgent || ''\n                    }, Insecure: ${!!insecure}, Proxy: ${proxy}`,\n                );\n                const { headers, body, statusCode } = await http.get({\n                    url: flowUrl,\n                    headers: {\n                        'User-Agent': userAgent,\n                    },\n                    timeout: requestTimeout,\n                    ...(proxy ? { proxy } : {}),\n                    ...(isLoon && proxy ? { node: proxy } : {}),\n                    ...(isQX && proxy ? { opts: { policy: proxy } } : {}),\n                    ...(proxy ? getPolicyDescriptor(proxy) : {}),\n                    ...(insecure ? insecure : {}),\n                });\n                if (statusCode < 200 || statusCode >= 400) {\n                    throw new Error(`statusCode: ${statusCode}`);\n                }\n                flowUrlHeaders = headers;\n                const parsed = parseFlowHeaders(body);\n                if (\n                    Number.isFinite(parsed?.total) &&\n                    Number.isFinite(parsed?.usage?.download) &&\n                    Number.isFinite(parsed?.usage?.upload)\n                ) {\n                    flowInfo = body;\n                } else {\n                    throw new Error('响应体中未包含合法的流量信息');\n                }\n            } catch (e) {\n                $.error(\n                    `使用 GET 方法从响应体获取流量信息失败: ${flowUrl}, User-Agent: ${\n                        userAgent || ''\n                    }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${\n                        e.message ?? e\n                    }`,\n                );\n                if (flowUrlHeaders) {\n                    try {\n                        const flowField = getFlowField(flowUrlHeaders);\n                        const parsed = parseFlowHeaders(flowField);\n                        if (\n                            Number.isFinite(parsed?.total) &&\n                            Number.isFinite(parsed?.usage?.download) &&\n                            Number.isFinite(parsed?.usage?.upload)\n                        ) {\n                            $.info(\n                                `使用 GET 方法从响应头获取流量信息成功: ${flowUrl}, User-Agent: ${\n                                    userAgent || ''\n                                }, Insecure: ${!!insecure}, Proxy: ${proxy}`,\n                            );\n                            flowInfo = flowField;\n                        } else {\n                            throw new Error('响应体中未包含合法的流量信息');\n                        }\n                    } catch (e) {\n                        $.error(\n                            `使用 GET 方法从响应头获取流量信息失败: ${flowUrl}, User-Agent: ${\n                                userAgent || ''\n                            }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${\n                                e.message ?? e\n                            }`,\n                        );\n                    }\n                }\n            }\n        } else {\n            try {\n                $.info(\n                    `使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${\n                        userAgent || ''\n                    }, Insecure: ${!!insecure}, Proxy: ${proxy}`,\n                );\n                const { headers } = await http.head({\n                    url: url\n                        .split(/[\\r\\n]+/)\n                        .map((i) => i.trim())\n                        .filter((i) => i.length)[0],\n                    headers: {\n                        'User-Agent': userAgent,\n                        ...(isStash && proxy\n                            ? {\n                                  'X-Stash-Selected-Proxy':\n                                      encodeURIComponent(proxy),\n                              }\n                            : {}),\n                        ...(isShadowRocket && proxy\n                            ? { 'X-Surge-Policy': proxy }\n                            : {}),\n                    },\n                    timeout: requestTimeout,\n                    ...(proxy ? { proxy } : {}),\n                    ...(isLoon && proxy ? { node: proxy } : {}),\n                    ...(isQX && proxy ? { opts: { policy: proxy } } : {}),\n                    ...(proxy ? getPolicyDescriptor(proxy) : {}),\n                    ...(insecure ? insecure : {}),\n                });\n                flowInfo = getFlowField(headers);\n            } catch (e) {\n                $.error(\n                    `使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${\n                        userAgent || ''\n                    }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${\n                        e.message ?? e\n                    }`,\n                );\n            }\n            if (!flowInfo) {\n                $.info(\n                    `使用 GET 方法获取流量信息: ${url}, User-Agent: ${\n                        userAgent || ''\n                    }, Insecure: ${!!insecure}, Proxy: ${proxy}`,\n                );\n                const { headers } = await http.get({\n                    url: url\n                        .split(/[\\r\\n]+/)\n                        .map((i) => i.trim())\n                        .filter((i) => i.length)[0],\n                    headers: {\n                        'User-Agent': userAgent,\n                        ...(isStash && proxy\n                            ? {\n                                  'X-Stash-Selected-Proxy':\n                                      encodeURIComponent(proxy),\n                              }\n                            : {}),\n                        ...(isShadowRocket && proxy\n                            ? { 'X-Surge-Policy': proxy }\n                            : {}),\n                    },\n                    timeout: requestTimeout,\n                    ...(proxy ? { proxy } : {}),\n                    ...(isLoon && proxy ? { node: proxy } : {}),\n                    ...(isQX && proxy ? { opts: { policy: proxy } } : {}),\n                    ...(proxy ? getPolicyDescriptor(proxy) : {}),\n                    ...(insecure ? insecure : {}),\n                });\n                flowInfo = getFlowField(headers);\n            }\n        }\n        if (flowInfo) {\n            flowInfo = flowInfo.trim();\n        }\n        if (flowInfo) {\n            headersResourceCache.set(id, flowInfo);\n        }\n    }\n\n    return flowInfo;\n}\nexport function parseFlowHeaders(flowHeaders) {\n    if (!flowHeaders) return;\n    // unit is KB\n    const uploadMatch = flowHeaders.match(\n        /upload=([-+]?)([0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)/,\n    );\n    const upload =\n        uploadMatch == null ? 0 : Number(uploadMatch[1] + uploadMatch[2]);\n\n    const downloadMatch = flowHeaders.match(\n        /download=([-+]?)([0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)/,\n    );\n    const download = Number(downloadMatch[1] + downloadMatch[2]);\n    const totalMatch = flowHeaders.match(\n        /total=([-+]?)([0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)/,\n    );\n    const total = Number(totalMatch[1] + totalMatch[2]);\n\n    // optional expire timestamp\n    const expireMatch = flowHeaders.match(\n        /expire=([-+]?)([0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)/,\n    );\n    const expires = expireMatch\n        ? Number(expireMatch[1] + expireMatch[2])\n        : undefined;\n\n    const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);\n    const remainingDays = remainingDaysMatch\n        ? Number(remainingDaysMatch[1])\n        : undefined;\n\n    const appUrlMatch = flowHeaders.match(/app_url=(.*?)\\s*?(;|$)/);\n    const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;\n\n    const planNameMatch = flowHeaders.match(/plan_name=(.*?)\\s*?(;|$)/);\n    const planName = planNameMatch\n        ? decodeURIComponent(planNameMatch[1])\n        : undefined;\n\n    return {\n        expires,\n        total,\n        usage: { upload, download },\n        remainingDays,\n        appUrl,\n        planName,\n    };\n}\n\nexport function flowTransfer(flow, unit = 'B') {\n    const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n    let unitIndex = unitList.indexOf(unit);\n\n    return flow < 1024 || unitIndex === unitList.length - 1\n        ? { value: (Math.round(flow * 100) / 100).toString(), unit: unit }\n        : flowTransfer(flow / 1024, unitList[++unitIndex]);\n}\n\nexport function validCheck(flow) {\n    if (!flow) {\n        throw new Error('没有流量信息');\n    }\n    if (flow?.expires && flow.expires * 1000 < Date.now()) {\n        const date = new Date(flow.expires * 1000).toLocaleDateString();\n        throw new Error(`订阅已过期: ${date}`);\n    }\n    if (flow?.total) {\n        const upload = flow.usage?.upload || 0;\n        const download = flow.usage?.download || 0;\n        if (flow.total - upload - download < 0) {\n            const current = upload + download;\n            const currT = flowTransfer(Math.abs(current));\n            currT.value = current < 0 ? '-' + currT.value : currT.value;\n            const totalT = flowTransfer(flow.total);\n            throw new Error(\n                `流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,\n            );\n        }\n    }\n}\n\nexport function getRmainingDays(opt = {}) {\n    try {\n        let { resetDay, startDate, cycleDays } = opt;\n        if (['string', 'number'].includes(typeof opt)) {\n            resetDay = opt;\n        }\n\n        if (startDate && cycleDays) {\n            cycleDays = parseInt(cycleDays);\n            if (isNaN(cycleDays) || cycleDays <= 0)\n                throw new Error('重置周期应为正整数');\n            if (!startDate || !Date.parse(startDate))\n                throw new Error('开始日期不合法');\n\n            const start = new Date(startDate);\n            const today = new Date();\n            start.setHours(0, 0, 0, 0);\n            today.setHours(0, 0, 0, 0);\n            if (start.getTime() > today.getTime())\n                throw new Error('开始日期应早于现在');\n\n            let resetDate = new Date(startDate);\n            resetDate.setDate(resetDate.getDate() + cycleDays);\n\n            while (resetDate < today) {\n                resetDate.setDate(resetDate.getDate() + cycleDays);\n            }\n\n            resetDate.setHours(0, 0, 0, 0);\n            const timeDiff = resetDate.getTime() - today.getTime();\n            const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));\n\n            return daysDiff;\n        } else {\n            if (!resetDay) return;\n            resetDay = parseInt(resetDay);\n            if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)\n                throw new Error('月重置日应为 1-31 之间的整数');\n            let now = new Date();\n            let today = now.getDate();\n            let month = now.getMonth();\n            let year = now.getFullYear();\n            let daysInMonth;\n\n            if (resetDay > today) {\n                daysInMonth = 0;\n            } else {\n                daysInMonth = new Date(year, month + 1, 0).getDate();\n            }\n\n            return daysInMonth - today + resetDay;\n        }\n    } catch (e) {\n        $.error(`getRmainingDays failed: ${e.message ?? e}`);\n    }\n}\n\nexport function normalizeFlowHeader(flowHeaders, splitHeaders) {\n    try {\n        // 使用 Map 保持顺序并处理重复键\n        const kvMap = new Map();\n\n        flowHeaders\n            .split(';')\n            .map((p) => p.trim())\n            .filter(Boolean)\n            .forEach((pair) => {\n                const eqIndex = pair.indexOf('=');\n                if (eqIndex === -1) return;\n\n                const key = pair.slice(0, eqIndex).trim();\n                const encodedValue = pair.slice(eqIndex + 1).trim();\n\n                // 只保留第一个出现的 key\n                if (!kvMap.has(key)) {\n                    try {\n                        // 解码 URI 组件并保留原始值作为 fallback\n                        let decodedValue = decodeURIComponent(encodedValue);\n                        if (\n                            [\n                                'upload',\n                                'download',\n                                'total',\n                                'expire',\n                                'reset_day',\n                            ].includes(key)\n                        ) {\n                            try {\n                                decodedValue = Number(decodedValue);\n                                if (\n                                    ['expire', 'reset_day'].includes(key) &&\n                                    (decodedValue <= 0 ||\n                                        !Number.isFinite(decodedValue))\n                                ) {\n                                    decodedValue = '';\n                                } else if (\n                                    ['upload', 'download', 'total'].includes(\n                                        key,\n                                    ) &&\n                                    !Number.isFinite(decodedValue) // 有些机场后端会下发负数\n                                ) {\n                                    decodedValue = 0;\n                                } else {\n                                    decodedValue = decodedValue.toFixed(0);\n                                }\n                            } catch (e) {\n                                $.error(\n                                    `Failed to convert value for key \"${key}=${encodedValue}\": ${\n                                        e.message ?? e\n                                    }`,\n                                );\n                            }\n                        }\n                        kvMap.set(key, decodedValue);\n                    } catch (e) {\n                        kvMap.set(key, encodedValue);\n                    }\n                }\n            });\n        const subscriptionUserinfo = {};\n        const headers = {\n            'subscription-userinfo': '',\n            'profile-web-page-url': '',\n            'plan-name': '',\n        };\n        kvMap.forEach((v, k) => {\n            if (splitHeaders && k === 'app_url') {\n                headers['profile-web-page-url'] = v;\n            } else if (splitHeaders && k === 'plan_name') {\n                headers['plan-name'] = v;\n            } else {\n                subscriptionUserinfo[k] = v;\n            }\n        });\n        if (Object.keys(subscriptionUserinfo).length > 0) {\n            headers['subscription-userinfo'] = Object.entries(\n                subscriptionUserinfo,\n            )\n                .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)\n                .join('; ');\n        }\n        return splitHeaders ? headers : headers['subscription-userinfo'];\n    } catch (e) {\n        $.error(`normalizeFlowHeader failed: ${e.message ?? e}`);\n        return splitHeaders\n            ? {\n                  'subscription-userinfo': flowHeaders,\n              }\n            : flowHeaders;\n    }\n}\n"
  },
  {
    "path": "backend/src/utils/geo.js",
    "content": "import $ from '@/core/app';\n\nconst ISOFlags = {\n    '🏳️‍🌈': ['EXP', 'BAND'],\n    '🇸🇱': ['TEST', 'SOS'],\n    '🇲🇵': ['MP', 'MNP'],\n    '🇸🇴': ['SO', 'SOM'],\n    '🇦🇶': ['AQ', 'ATA'],\n    '🇦🇬': ['AG', 'ATG'],\n    '🇬🇱': ['GL', 'GRL'],\n    '🇿🇼': ['ZW', 'ZWE'],\n    '🇦🇼': ['AW', 'ABW'],\n    '🇲🇱': ['ML', 'MLI'],\n    '🇦🇩': ['AD', 'AND'],\n    '🇦🇪': ['AE', 'ARE'],\n    '🇦🇫': ['AF', 'AFG'],\n    '🇦🇱': ['AL', 'ALB'],\n    '🇦🇲': ['AM', 'ARM'],\n    '🇦🇷': ['AR', 'ARG'],\n    '🇦🇹': ['AT', 'AUT'],\n    '🇦🇺': ['AU', 'AUS'],\n    '🇦🇿': ['AZ', 'AZE'],\n    '🇧🇦': ['BA', 'BIH'],\n    '🇧🇩': ['BD', 'BGD'],\n    '🇧🇪': ['BE', 'BEL'],\n    '🇧🇬': ['BG', 'BGR'],\n    '🇧🇭': ['BH', 'BHR'],\n    '🇧🇴': ['BO', 'BOL'],\n    '🇧🇳': ['BN', 'BRN'],\n    '🇧🇷': ['BR', 'BRA'],\n    '🇧🇹': ['BT', 'BTN'],\n    '🇧🇾': ['BY', 'BLR'],\n    '🇨🇦': ['CA', 'CAN'],\n    '🇨🇭': ['CH', 'CHE'],\n    '🇨🇱': ['CL', 'CHL'],\n    '🇨🇴': ['CO', 'COL'],\n    '🇨🇷': ['CR', 'CRI'],\n    '🇨🇾': ['CY', 'CYP'],\n    '🇨🇿': ['CZ', 'CZE'],\n    '🇩🇪': ['DE', 'DEU'],\n    '🇩🇰': ['DK', 'DNK'],\n    // 新增阿尔及利亚 ISO 代码\n    '🇩🇿': ['DZ', 'DZA'],\n    '🇪🇨': ['EC', 'ECU'],\n    '🇪🇪': ['EE', 'EST'],\n    '🇪🇬': ['EG', 'EGY'],\n    '🇪🇸': ['ES', 'ESP'],\n    '🇪🇺': ['EU'],\n    '🇫🇮': ['FI', 'FIN'],\n    '🇫🇷': ['FR', 'FRA'],\n    '🇬🇧': ['GB', 'GBR', 'UK'],\n    '🇬🇪': ['GE', 'GEO'],\n    '🇬🇷': ['GR', 'GRC'],\n    '🇬🇹': ['GT', 'GTM'],\n    '🇬🇺': ['GU', 'GUM'],\n    '🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],\n    '🇭🇷': ['HR', 'HRV'],\n    '🇭🇺': ['HU', 'HUN'],\n    '🇮🇶': ['IQ', 'IRQ'], // 伊拉克\n    '🇯🇴': ['JO', 'JOR'],\n    '🇯🇵': ['JP', 'JPN', 'TYO'],\n    '🇰🇪': ['KE', 'KEN'],\n    '🇰🇬': ['KG', 'KGZ'],\n    '🇰🇭': ['KH', 'KGZ'],\n    '🇰🇵': ['KP', 'PRK'],\n    '🇰🇷': ['KR', 'KOR', 'SEL'],\n    '🇰🇿': ['KZ', 'KAZ'],\n    '🇮🇩': ['ID', 'IDN'],\n    '🇮🇪': ['IE', 'IRL'],\n    '🇮🇱': ['IL', 'ISR'],\n    '🇮🇲': ['IM', 'IMN'],\n    '🇮🇳': ['IN', 'IND'],\n    '🇮🇷': ['IR', 'IRN'],\n    '🇮🇸': ['IS', 'ISL'],\n    '🇮🇹': ['IT', 'ITA'],\n    '🇱🇦': ['LA', 'LAO'],\n    '🇱🇰': ['LK', 'LKA'],\n    '🇱🇹': ['LT', 'LTU'],\n    '🇱🇺': ['LU', 'LUX'],\n    '🇱🇻': ['LV', 'LVA'],\n    '🇲🇦': ['MA', 'MAR'],\n    '🇲🇩': ['MD', 'MDA'],\n    '🇳🇬': ['NG', 'NGA'],\n    '🇲🇲': ['MM', 'MMR'],\n    '🇲🇰': ['MK', 'MKD'],\n    '🇲🇳': ['MN', 'MNG'],\n    '🇲🇴': ['MO', 'MAC', 'CTM'],\n    '🇲🇹': ['MT', 'MLT'],\n    '🇲🇽': ['MX', 'MEX'],\n    '🇲🇾': ['MY', 'MYS'],\n    '🇳🇱': ['NL', 'NLD', 'AMS'],\n    '🇳🇴': ['NO', 'NOR'],\n    '🇳🇵': ['NP', 'NPL'],\n    '🇳🇿': ['NZ', 'NZL'],\n    '🇴🇲': ['OM', 'OMN'], // 阿曼\n    '🇵🇦': ['PA', 'PAN'],\n    '🇵🇪': ['PE', 'PER'],\n    '🇵🇭': ['PH', 'PHL'],\n    '🇵🇰': ['PK', 'PAK'],\n    '🇵🇱': ['PL', 'POL'],\n    '🇵🇷': ['PR', 'PRI'],\n    '🇵🇹': ['PT', 'PRT'],\n    '🇵🇾': ['PY', 'PRY'],\n    '🇵🇬': ['PG', 'PNG'],\n    '🇶🇦': ['QA', 'QAT'],\n    '🇷🇴': ['RO', 'ROU'],\n    '🇷🇸': ['RS', 'SRB'],\n    '🇷🇪': ['RE', 'REU'],\n    '🇷🇺': ['RU', 'RUS'],\n    '🇸🇦': ['SA', 'SAU'],\n    '🇼🇸': ['WS', 'WSM'],\n    '🇸🇪': ['SE', 'SWE'],\n    '🇸🇬': ['SG', 'SGP'],\n    '🇸🇮': ['SI', 'SVN'],\n    '🇸🇰': ['SK', 'SVK'],\n    '🇹🇬': ['TG', 'TGO'], // 多哥\n    '🇹🇭': ['TH', 'THA'],\n    '🇹🇳': ['TN', 'TUN'],\n    '🇹🇷': ['TR', 'TUR'],\n    '🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],\n    '🇺🇦': ['UA', 'UKR'],\n    '🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'],\n    '🇺🇾': ['UY', 'URY'],\n    // 新增 梵蒂冈 ISO 代码\n    '🇻🇦': ['VA', 'VAT'],\n    '🇻🇪': ['VE', 'VEN'],\n    '🇻🇳': ['VN', 'VNM'],\n    '🇿🇦': ['ZA', 'ZAF', 'JNB'],\n    '🇨🇳': ['CN', 'CHN', 'BACK'],\n};\n// get proxy flag according to its name\nexport function getFlag(name) {\n    // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js\n    // flags from @surgioproject: https://github.com/surgioproject/surgio/blob/master/lib/misc/flag_cn.ts\n\n    // refer: https://zh.wikipedia.org/wiki/ISO_3166-1二位字母代码\n    // refer: https://zh.wikipedia.org/wiki/ISO_3166-1三位字母代码\n    const Flags = {\n        '🏳️‍🌈': ['流量', '时间', '过期', 'Bandwidth', 'Expire'],\n        '🇸🇱': ['应急', '测试节点'],\n        '🇲🇵': ['北马里亚纳', 'Northern Mariana Islands', 'Saipan', '塞班'],\n        '🇸🇴': ['Somalia', '索马里', '摩加迪沙', 'Mogadishu'],\n        '🇦🇶': ['Antarctica', '南极洲', '南极'],\n        '🇦🇬': ['Antigua and Barbuda', '安提瓜和巴布达'],\n        '🇬🇱': ['Greenland', '格陵兰岛', '格陵兰'],\n        '🇿🇼': ['Zimbabwe', '津巴布韦'],\n        '🇦🇼': ['Aruba', '阿鲁巴'],\n        '🇲🇱': ['Mali', '马里'],\n        '🇦🇩': ['Andorra', '安道尔'],\n        '🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜', 'Dubai'],\n        '🇦🇫': ['Afghanistan', '阿富汗'],\n        '🇦🇱': ['Albania', '阿尔巴尼亚', '阿爾巴尼亞'],\n        '🇦🇲': ['Armenia', '亚美尼亚'],\n        '🇦🇷': ['Argentina', '阿根廷'],\n        '🇦🇹': ['Austria', '奥地利', '奧地利', '维也纳'],\n        '🇼🇸': ['Samoa', '萨摩亚', '薩摩亞'],\n        '🇦🇺': [\n            'Australia',\n            '澳大利亚',\n            '澳洲',\n            '墨尔本',\n            '悉尼',\n            '土澳',\n            '京澳',\n            '廣澳',\n            '滬澳',\n            '沪澳',\n            '广澳',\n            'Sydney',\n        ],\n        '🇦🇿': ['Azerbaijan', '阿塞拜疆'],\n        '🇧🇦': ['Bosnia and Herzegovina', '波黑共和国', '波黑'],\n        '🇧🇩': ['Bangladesh', '孟加拉国', '孟加拉'],\n        '🇧🇪': ['Belgium', '比利时', '比利時'],\n        '🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],\n        '🇧🇭': ['Bahrain', '巴林'],\n        '🇧🇷': ['Brazil', '巴西', '圣保罗'],\n        '🇧🇳': ['Brunei', '文莱', '汶萊'],\n        '🇧🇾': ['Belarus', '白俄罗斯', '白俄'],\n        '🇧🇴': ['Bolivia', '玻利维亚'],\n        '🇧🇹': ['Bhutan', '不丹', '不丹王国'],\n        '🇨🇦': [\n            'Canada',\n            '加拿大',\n            '蒙特利尔',\n            '温哥华',\n            '楓葉',\n            '枫叶',\n            '滑铁卢',\n            '多伦多',\n            'Waterloo',\n            'Toronto',\n        ],\n        '🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],\n        '🇨🇱': ['Chile', '智利'],\n        '🇨🇴': ['Colombia', '哥伦比亚'],\n        '🇨🇷': ['Costa Rica', '哥斯达黎加'],\n        '🇨🇾': ['Cyprus', '塞浦路斯'],\n        // 补充 Czech / Czech Republic 匹配\n        '🇨🇿': ['Czechia', '捷克', 'Czech', 'Czech Republic'],\n        '🇩🇪': [\n            'German',\n            '德国',\n            '德國',\n            '京德',\n            '滬德',\n            '廣德',\n            '沪德',\n            '广德',\n            '法兰克福',\n            'Frankfurt',\n            '德意志',\n        ],\n        '🇩🇰': ['Denmark', '丹麦', '丹麥'],\n        // 新增 阿尔及利亚\n        '🇩🇿': ['Algeria', '阿尔及利亚', '阿爾及利亞'],\n        '🇪🇨': ['Ecuador', '厄瓜多尔'],\n        '🇪🇪': ['Estonia', '爱沙尼亚'],\n        '🇪🇬': ['Egypt', '埃及'],\n        '🇪🇸': ['Spain', '西班牙'],\n        '🇪🇺': ['European Union', '欧盟', '欧罗巴'],\n        '🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],\n        '🇫🇷': ['France', '法国', '法國', '巴黎'],\n        '🇬🇧': [\n            'Great Britain',\n            '英国',\n            'England',\n            'United Kingdom',\n            '伦敦',\n            '英',\n            'London',\n        ],\n        '🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],\n        '🇬🇷': ['Greece', '希腊', '希臘'],\n        '🇬🇺': ['Guam', '关岛', '關島'],\n        '🇬🇹': ['Guatemala', '危地马拉'],\n        '🇭🇰': [\n            'Hongkong',\n            '香港',\n            'Hong Kong',\n            'HongKong',\n            'HONG KONG',\n            '深港',\n            '沪港',\n            '呼港',\n            '穗港',\n            '京港',\n            '港',\n        ],\n        '🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'],\n        '🇭🇺': ['Hungary', '匈牙利'],\n        '🇮🇶': ['Iraq', '伊拉克', '巴格达', 'Baghdad'], // 伊拉克\n        '🇯🇴': ['Jordan', '约旦'],\n        '🇯🇵': [\n            'Japan',\n            '日本',\n            '东京',\n            '大阪',\n            '埼玉',\n            '沪日',\n            '穗日',\n            '川日',\n            '中日',\n            '泉日',\n            '杭日',\n            '深日',\n            '辽日',\n            '广日',\n            '大坂',\n            'Osaka',\n            'Tokyo',\n        ],\n        '🇰🇪': ['Kenya', '肯尼亚'],\n        '🇰🇬': ['Kyrgyzstan', '吉尔吉斯斯坦'],\n        '🇰🇭': ['Cambodia', '柬埔寨'],\n        '🇰🇵': ['North Korea', '朝鲜'],\n        '🇰🇷': [\n            'Korea',\n            '韩国',\n            '韓國',\n            '韩',\n            '韓',\n            '首尔',\n            '春川',\n            'Chuncheon',\n            'Seoul',\n        ],\n        '🇰🇿': ['Kazakhstan', '哈萨克斯坦', '哈萨克'],\n        '🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],\n        '🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],\n        '🇮🇱': ['Israel', '以色列'],\n        '🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],\n        '🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],\n        '🇮🇷': ['Iran', '伊朗'],\n        '🇮🇸': ['Iceland', '冰岛', '冰島'],\n        '🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],\n        '🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],\n        '🇱🇦': ['Laos', '老挝', '老撾'],\n        '🇱🇹': ['Lithuania', '立陶宛'],\n        '🇱🇺': ['Luxembourg', '卢森堡'],\n        '🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],\n        '🇲🇦': ['Morocco', '摩洛哥'],\n        '🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],\n        '🇲🇲': ['Myanmar', '缅甸', '緬甸'],\n        '🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],\n        '🇲🇰': ['Macedonia', '马其顿', '馬其頓'],\n        '🇲🇳': ['Mongolia', '蒙古'],\n        '🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],\n        '🇲🇹': ['Malta', '马耳他'],\n        '🇲🇽': ['Mexico', '墨西哥'],\n        '🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],\n        '🇳🇱': [\n            'Netherlands',\n            '荷兰',\n            '荷蘭',\n            '尼德蘭',\n            '阿姆斯特丹',\n            'Amsterdam',\n        ],\n        '🇳🇴': ['Norway', '挪威'],\n        '🇳🇵': ['Nepal', '尼泊尔'],\n        '🇳🇿': ['New Zealand', '新西兰', '新西蘭'],\n        '🇴🇲': ['Oman', '阿曼', '马斯喀特'],\n        '🇵🇦': ['Panama', '巴拿马'],\n        '🇵🇪': ['Peru', '秘鲁', '祕魯'],\n        '🇵🇭': ['Philippines', '菲律宾', '菲律賓'],\n        '🇵🇰': ['Pakistan', '巴基斯坦'],\n        '🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],\n        '🇵🇷': ['Puerto Rico', '波多黎各'],\n        '🇵🇹': ['Portugal', '葡萄牙'],\n        '🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],\n        '🇵🇾': ['Paraguay', '巴拉圭'],\n        '🇶🇦': ['Qatar', '卡塔尔', '卡塔爾'],\n        '🇷🇴': ['Romania', '罗马尼亚'],\n        '🇷🇸': ['Serbia', '塞尔维亚'],\n        '🇷🇪': ['Réunion', '留尼汪', '法属留尼汪'],\n        '🇷🇺': [\n            'Russia',\n            '俄罗斯',\n            '俄国',\n            '俄羅斯',\n            '伯力',\n            '莫斯科',\n            '圣彼得堡',\n            '西伯利亚',\n            '京俄',\n            '杭俄',\n            '廣俄',\n            '滬俄',\n            '广俄',\n            '沪俄',\n            'Moscow',\n        ],\n        '🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],\n        '🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],\n        '🇸🇬': [\n            'Singapore',\n            '新加坡',\n            '狮城',\n            '沪新',\n            '京新',\n            '中新',\n            '泉新',\n            '穗新',\n            '深新',\n            '杭新',\n            '广新',\n            '廣新',\n            '滬新',\n        ],\n        '🇸🇮': ['Slovenia', '斯洛文尼亚'],\n        '🇸🇰': ['Slovakia', '斯洛伐克'],\n        '🇹🇬': ['Togo', '多哥', '洛美', 'Lomé', 'Lome'], // 多哥\n        '🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],\n        '🇹🇳': ['Tunisia', '突尼斯'],\n        '🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],\n        '🇹🇼': [\n            'Taiwan',\n            '台湾',\n            '臺灣',\n            '台灣',\n            '中華民國',\n            '中华民国',\n            '台北',\n            '台中',\n            '新北',\n            '彰化',\n            '台',\n            '臺',\n            'Taipei',\n            'Tai Wan',\n        ],\n        '🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],\n        '🇺🇸': [\n            'United States',\n            '美国',\n            'America',\n            '美',\n            '京美',\n            '波特兰',\n            '达拉斯',\n            '俄勒冈',\n            'Oregon',\n            '凤凰城',\n            '费利蒙',\n            '硅谷',\n            '矽谷',\n            '拉斯维加斯',\n            '洛杉矶',\n            '圣何塞',\n            '圣克拉拉',\n            '西雅图',\n            '芝加哥',\n            '沪美',\n            '哥伦布',\n            '纽约',\n            'New York',\n            'Los Angeles',\n            'San Jose',\n            'Sillicon Valley',\n            'Michigan',\n            '俄亥俄',\n            'Ohio',\n            '马纳萨斯',\n            'Manassas',\n            '弗吉尼亚',\n            'Virginia',\n        ],\n        '🇺🇾': ['Uruguay', '乌拉圭'],\n        // 新增 梵蒂冈 及别名\n        '🇻🇦': ['Vatican', 'Vatican City', 'Holy See', '梵蒂冈', '梵蒂岡'],\n        '🇻🇪': ['Venezuela', '委内瑞拉'],\n        '🇻🇳': ['Vietnam', '越南', '胡志明'],\n        '🇿🇦': ['South Africa', '南非'],\n        '🇨🇳': [\n            'China',\n            '中国',\n            '中國',\n            '回国',\n            '回國',\n            '国内',\n            '國內',\n            '华东',\n            '华西',\n            '华南',\n            '华北',\n            '华中',\n            '江苏',\n            '北京',\n            '上海',\n            '广州',\n            '深圳',\n            '杭州',\n            '徐州',\n            '青岛',\n            '宁波',\n            '镇江',\n        ],\n    };\n\n    // 原旗帜或空\n    let Flag =\n        name.match(/[\\uD83C][\\uDDE6-\\uDDFF][\\uD83C][\\uDDE6-\\uDDFF]/)?.[0] ||\n        '🏴‍☠️';\n    //console.log(`oldFlag = ${Flag}`)\n    // 旗帜匹配\n    for (let flag of Object.keys(Flags)) {\n        const keywords = Flags[flag];\n        //console.log(`keywords = ${keywords}`)\n        if (\n            // 不精确匹配（只要包含就算,忽略大小写)\n            keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))\n        ) {\n            if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {\n                return (Flag = '🇨🇳');\n            }\n            return (Flag = flag);\n        }\n    }\n    // ISO旗帜匹配\n    for (let flag of Object.keys(ISOFlags)) {\n        const keywords = ISOFlags[flag];\n        //console.log(`keywords = ${keywords}`)\n        if (\n            // 精确匹配(两侧均有分割)\n            keywords.some((keyword) =>\n                RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),\n            )\n        ) {\n            const isCN2 =\n                flag == '🇨🇳' &&\n                RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);\n            if (!isCN2) {\n                return (Flag = flag);\n            }\n        }\n    }\n\n    //console.log(`Final Flag = ${Flag}`)\n    return Flag;\n}\n\nexport function getISO(name) {\n    return ISOFlags[getFlag(name)]?.[0];\n}\n\n// remove flag\nexport function removeFlag(str) {\n    return str\n        .replace(/[\\uD83C][\\uDDE6-\\uDDFF][\\uD83C][\\uDDE6-\\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')\n        .trim();\n}\n\nexport class MMDB {\n    constructor({ country, asn } = {}) {\n        if ($.env.isNode) {\n            const Reader = eval(`require(\"@maxmind/geoip2-node\")`).Reader;\n            const fs = eval(\"require('fs')\");\n            const countryFile =\n                country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');\n            const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH');\n            // $.info(\n            //     `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync(\n            //         countryFile,\n            //     )}`,\n            // );\n            if (countryFile) {\n                this.countryReader = Reader.openBuffer(\n                    fs.readFileSync(countryFile),\n                );\n            }\n            // $.info(\n            //     `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync(\n            //         asnFile,\n            //     )}`,\n            // );\n            if (asnFile) {\n                if (!fs.existsSync(asnFile))\n                    throw new Error('GeoLite2 ASN MMDB does not exist');\n                this.asnReader = Reader.openBuffer(fs.readFileSync(asnFile));\n            }\n        }\n    }\n    geoip(ip) {\n        return this.countryReader?.country(ip)?.country?.isoCode;\n    }\n    ipaso(ip) {\n        return this.asnReader?.asn(ip)?.autonomousSystemOrganization;\n    }\n    ipasn(ip) {\n        return this.asnReader?.asn(ip)?.autonomousSystemNumber;\n    }\n}\n"
  },
  {
    "path": "backend/src/utils/gist.js",
    "content": "import { HTTP, ENV } from '@/vendor/open-api';\nimport { getPolicyDescriptor } from '@/utils';\nimport $ from '@/core/app';\nimport { SETTINGS_KEY } from '@/constants';\n\n/**\n * Gist backup\n */\nexport default class Gist {\n    constructor({ token, key, syncPlatform }) {\n        const { isStash, isLoon, isShadowRocket, isQX } = ENV();\n        const {\n            defaultProxy,\n            defaultTimeout: timeout,\n            githubProxy,\n        } = $.read(SETTINGS_KEY);\n        let proxy = defaultProxy;\n        if ($.env.isNode) {\n            proxy =\n                proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');\n        }\n\n        if (syncPlatform === 'gitlab') {\n            this.headers = {\n                'PRIVATE-TOKEN': `${token}`,\n                'User-Agent':\n                    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',\n            };\n            this.http = HTTP({\n                baseURL: 'https://gitlab.com/api/v4',\n                headers: {\n                    ...this.headers,\n                    ...(isStash && proxy\n                        ? {\n                              'X-Stash-Selected-Proxy':\n                                  encodeURIComponent(proxy),\n                          }\n                        : {}),\n                    ...(isShadowRocket && proxy\n                        ? { 'X-Surge-Policy': proxy }\n                        : {}),\n                },\n\n                ...(proxy ? { proxy } : {}),\n                ...(isLoon && proxy ? { node: proxy } : {}),\n                ...(isQX && proxy ? { opts: { policy: proxy } } : {}),\n                ...(proxy ? getPolicyDescriptor(proxy) : {}),\n                timeout: timeout || 8000,\n\n                events: {\n                    onResponse: (resp) => {\n                        if (/^[45]/.test(String(resp.statusCode))) {\n                            const body = JSON.parse(resp.body);\n                            return Promise.reject(\n                                `ERROR: ${body.message?.error ?? body.message}`,\n                            );\n                        } else {\n                            return resp;\n                        }\n                    },\n                },\n            });\n        } else {\n            this.headers = {\n                Authorization: `token ${token}`,\n                'User-Agent':\n                    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',\n            };\n            this.http = HTTP({\n                baseURL: `${\n                    githubProxy ? `${githubProxy}/` : ''\n                }https://api.github.com`,\n                headers: {\n                    ...this.headers,\n                    ...(isStash && proxy\n                        ? {\n                              'X-Stash-Selected-Proxy':\n                                  encodeURIComponent(proxy),\n                          }\n                        : {}),\n                    ...(isShadowRocket && proxy\n                        ? { 'X-Surge-Policy': proxy }\n                        : {}),\n                },\n\n                ...(proxy ? { proxy } : {}),\n                ...(isLoon && proxy ? { node: proxy } : {}),\n                ...(isQX && proxy ? { opts: { policy: proxy } } : {}),\n                ...(proxy ? getPolicyDescriptor(proxy) : {}),\n                timeout: timeout || 8000,\n\n                events: {\n                    onResponse: (resp) => {\n                        if (/^[45]/.test(String(resp.statusCode))) {\n                            return Promise.reject(\n                                `ERROR: ${JSON.parse(resp.body).message}`,\n                            );\n                        } else {\n                            return resp;\n                        }\n                    },\n                },\n            });\n        }\n\n        this.key = key;\n        this.syncPlatform = syncPlatform;\n    }\n\n    async locate() {\n        if (this.syncPlatform === 'gitlab') {\n            return this.http.get('/snippets').then((response) => {\n                const gists = JSON.parse(response.body);\n\n                for (let g of gists) {\n                    if (g.title === this.key) {\n                        return g;\n                    }\n                }\n                return;\n            });\n        } else {\n            return this.http\n                .get('/gists?per_page=100&page=1')\n                .then((response) => {\n                    const gists = JSON.parse(response.body);\n                    $.info(`获取到当前 GitHub 用户的 gist: ${gists.length} 个`);\n                    for (let g of gists) {\n                        if (g.description === this.key) {\n                            return g;\n                        }\n                    }\n                    return;\n                });\n        }\n    }\n\n    async upload(input) {\n        if (Object.keys(input).length === 0) {\n            return Promise.reject('未提供需上传的文件');\n        }\n\n        const gist = await this.locate();\n\n        let files = input;\n\n        if (gist?.id) {\n            if (this.syncPlatform === 'gitlab') {\n                gist.files = gist.files.reduce((acc, item) => {\n                    acc[item.path] = item;\n                    return acc;\n                }, {});\n            }\n            // console.log(`files`, files);\n            // console.log(`gist`, gist.files);\n            let actions = [];\n            const result = { ...gist.files };\n            Object.keys(files).map((key) => {\n                if (result[key]) {\n                    if (\n                        files[key].content == null ||\n                        files[key].content === ''\n                    ) {\n                        delete result[key];\n                        actions.push({\n                            action: 'delete',\n                            file_path: key,\n                        });\n                    } else {\n                        result[key] = files[key];\n                        actions.push({\n                            action: 'update',\n                            file_path: key,\n                            content: files[key].content,\n                        });\n                    }\n                } else {\n                    if (\n                        files[key].content == null ||\n                        files[key].content === ''\n                    ) {\n                        delete result[key];\n                        delete files[key];\n                    } else {\n                        result[key] = files[key];\n                        actions.push({\n                            action: 'create',\n                            file_path: key,\n                            content: files[key].content,\n                        });\n                    }\n                }\n            });\n            // console.log(`result`, result);\n            // console.log(`files`, files);\n            // console.log(`actions`, actions);\n\n            if (this.syncPlatform === 'gitlab') {\n                if (Object.keys(result).length === 0) {\n                    return Promise.reject(\n                        '本次操作将导致所有文件的内容都为空, 无法更新 snippet',\n                    );\n                }\n                if (Object.keys(result).length > 10) {\n                    return Promise.reject(\n                        '本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',\n                    );\n                }\n                files = actions;\n                return this.http.put({\n                    headers: {\n                        ...this.headers,\n                        'Content-Type': 'application/json',\n                    },\n                    url: `/snippets/${gist.id}`,\n                    body: JSON.stringify({ files }),\n                });\n            } else {\n                if (Object.keys(result).length === 0) {\n                    return Promise.reject(\n                        '本次操作将导致所有文件的内容都为空, 无法更新 gist',\n                    );\n                }\n                return this.http.patch({\n                    url: `/gists/${gist.id}`,\n                    body: JSON.stringify({ files }),\n                });\n            }\n        } else {\n            files = Object.entries(files).reduce((acc, [key, file]) => {\n                if (file.content !== null && file.content !== '') {\n                    acc[key] = file;\n                }\n                return acc;\n            }, {});\n            if (this.syncPlatform === 'gitlab') {\n                if (Object.keys(files).length === 0) {\n                    return Promise.reject(\n                        '所有文件的内容都为空, 无法创建 snippet',\n                    );\n                }\n                files = Object.keys(files).map((key) => ({\n                    file_path: key,\n                    content: files[key].content,\n                }));\n                return this.http.post({\n                    headers: {\n                        ...this.headers,\n                        'Content-Type': 'application/json',\n                    },\n                    url: '/snippets',\n                    body: JSON.stringify({\n                        title: this.key,\n                        visibility: 'private',\n                        files,\n                    }),\n                });\n            } else {\n                if (Object.keys(files).length === 0) {\n                    return Promise.reject(\n                        '所有文件的内容都为空, 无法创建 gist',\n                    );\n                }\n                return this.http.post({\n                    url: '/gists',\n                    body: JSON.stringify({\n                        description: this.key,\n                        public: false,\n                        files,\n                    }),\n                });\n            }\n        }\n    }\n\n    async download(filename) {\n        const gist = await this.locate();\n        if (gist?.id) {\n            try {\n                const { files } = await this.http\n                    .get(`/gists/${gist.id}`)\n                    .then((resp) => JSON.parse(resp.body));\n                const url = files[filename].raw_url;\n                return await this.http.get(url).then((resp) => resp.body);\n            } catch (err) {\n                return Promise.reject(err);\n            }\n        } else {\n            return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);\n        }\n    }\n}\n"
  },
  {
    "path": "backend/src/utils/headers-resource-cache.js",
    "content": "import $ from '@/core/app';\nimport {\n    HEADERS_RESOURCE_CACHE_KEY,\n    DEFAULT_HEADERS_CACHE_TTL,\n    SETTINGS_KEY,\n} from '@/constants';\n\nclass ResourceCache {\n    constructor() {\n        if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {\n            $.write('{}', HEADERS_RESOURCE_CACHE_KEY);\n        }\n        try {\n            this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));\n        } catch (e) {\n            $.error(\n                `解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${\n                    e?.message ?? e\n                }`,\n            );\n            this.resourceCache = {};\n            $.write('{}', HEADERS_RESOURCE_CACHE_KEY);\n        }\n        this._cleanup();\n    }\n\n    _cleanup(prefix, ttl) {\n        const resolvedTTL = normalizeTTL(ttl) ?? 0;\n        let clear = false;\n        const now = Date.now();\n        Object.entries(this.resourceCache).forEach((entry) => {\n            const [id, cached] = entry;\n            const shouldDelete =\n                !cached.time || cached.time < now + resolvedTTL;\n            if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) {\n                delete this.resourceCache[id];\n                clear = true;\n            }\n        });\n        if (clear) this._persist();\n    }\n\n    revokeAll() {\n        this.resourceCache = {};\n        this._persist();\n    }\n\n    _persist() {\n        $.write(JSON.stringify(this.resourceCache), HEADERS_RESOURCE_CACHE_KEY);\n    }\n\n    gettime(id) {\n        const time = this.resourceCache[id] && this.resourceCache[id].time;\n        if (time && new Date().getTime() <= time) {\n            return this.resourceCache[id].time;\n        }\n        return null;\n    }\n\n    get(id, ttl, remove) {\n        const resolvedTTL = normalizeTTL(ttl) ?? 0;\n        const cached = this.resourceCache[id];\n        const time = cached && cached.time;\n        if (time) {\n            if (Date.now() + resolvedTTL <= time) return cached.data;\n            if (remove) {\n                delete this.resourceCache[id];\n                this._persist();\n            }\n        }\n        return null;\n    }\n\n    set(id, value, ttl) {\n        const resolvedTTL = normalizeTTL(ttl) ?? getTTL();\n        this.resourceCache[id] = {\n            time: Date.now() + resolvedTTL,\n            data: value,\n        };\n        this._persist();\n    }\n}\n\nfunction normalizeTTL(ttl) {\n    const value = Number(ttl);\n    if (!isFinite(value)) return null;\n    if (value > 0) return value;\n    return null;\n}\n\nfunction getTTL() {\n    const settings = $.read(SETTINGS_KEY);\n    let ttl = settings?.headersCacheTtl;\n    if (ttl) {\n        ttl = Number(ttl);\n        if (isFinite(ttl) && ttl > 0) {\n            return ttl * 1000;\n        }\n    }\n    return DEFAULT_HEADERS_CACHE_TTL;\n}\n\nexport default new ResourceCache();\n"
  },
  {
    "path": "backend/src/utils/index.js",
    "content": "import * as ipAddress from 'ip-address';\n// source: https://stackoverflow.com/a/36760050\nconst IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$/;\n\n// source: https://ihateregex.io/expr/ipv6/\nconst IPV6_REGEX =\n    /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;\n\nfunction isIPv4(ip) {\n    return IPV4_REGEX.test(ip);\n}\n\nfunction isIPv6(ip) {\n    return IPV6_REGEX.test(ip);\n}\n\nfunction isValidPortNumber(port) {\n    return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(\n        port,\n    );\n}\n\nfunction isNotBlank(str) {\n    return typeof str === 'string' && str.trim().length > 0;\n}\n\nfunction getIfNotBlank(str, defaultValue) {\n    return isNotBlank(str) ? str : defaultValue;\n}\n\nfunction isPresent(obj) {\n    return typeof obj !== 'undefined' && obj !== null;\n}\n\nfunction getIfPresent(obj, defaultValue) {\n    return isPresent(obj) ? obj : defaultValue;\n}\n\nfunction getPolicyDescriptor(str) {\n    if (!str) return {};\n    return /^.+?\\s*?=\\s*?.+?\\s*?,.+?/.test(str)\n        ? {\n              'policy-descriptor': str,\n          }\n        : {\n              policy: str,\n          };\n}\n\n// const utf8ArrayToStr =\n//     typeof TextDecoder !== 'undefined'\n//         ? (v) => new TextDecoder().decode(new Uint8Array(v))\n//         : (function () {\n//               var charCache = new Array(128); // Preallocate the cache for the common single byte chars\n//               var charFromCodePt = String.fromCodePoint || String.fromCharCode;\n//               var result = [];\n\n//               return function (array) {\n//                   var codePt, byte1;\n//                   var buffLen = array.length;\n\n//                   result.length = 0;\n\n//                   for (var i = 0; i < buffLen; ) {\n//                       byte1 = array[i++];\n\n//                       if (byte1 <= 0x7f) {\n//                           codePt = byte1;\n//                       } else if (byte1 <= 0xdf) {\n//                           codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);\n//                       } else if (byte1 <= 0xef) {\n//                           codePt =\n//                               ((byte1 & 0x0f) << 12) |\n//                               ((array[i++] & 0x3f) << 6) |\n//                               (array[i++] & 0x3f);\n//                       } else if (String.fromCodePoint) {\n//                           codePt =\n//                               ((byte1 & 0x07) << 18) |\n//                               ((array[i++] & 0x3f) << 12) |\n//                               ((array[i++] & 0x3f) << 6) |\n//                               (array[i++] & 0x3f);\n//                       } else {\n//                           codePt = 63; // Cannot convert four byte code points, so use \"?\" instead\n//                           i += 3;\n//                       }\n\n//                       result.push(\n//                           charCache[codePt] ||\n//                               (charCache[codePt] = charFromCodePt(codePt)),\n//                       );\n//                   }\n\n//                   return result.join('');\n//               };\n//           })();\n\nfunction getRandomInt(min, max) {\n    min = Math.ceil(min);\n    max = Math.floor(max);\n    return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nfunction getRandomPort(portString) {\n    let portParts = portString.split(/,|\\//);\n    let randomPart = portParts[Math.floor(Math.random() * portParts.length)];\n    if (randomPart.includes('-')) {\n        let [min, max] = randomPart.split('-').map(Number);\n        return getRandomInt(min, max);\n    } else {\n        return Number(randomPart);\n    }\n}\n\nfunction numberToString(value) {\n    return Number.isSafeInteger(value)\n        ? String(value)\n        : BigInt(value).toString();\n}\n\nfunction isValidUUID(uuid) {\n    return (\n        typeof uuid === 'string' &&\n        /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(\n            uuid,\n        )\n    );\n}\n\nfunction formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {\n    const d = date instanceof Date ? date : new Date(date);\n\n    if (isNaN(d.getTime())) {\n        return '';\n    }\n\n    const pad = (num) => String(num).padStart(2, '0');\n\n    const replacements = {\n        YYYY: d.getFullYear(),\n        MM: pad(d.getMonth() + 1),\n        DD: pad(d.getDate()),\n        HH: pad(d.getHours()),\n        mm: pad(d.getMinutes()),\n        ss: pad(d.getSeconds()),\n    };\n\n    return format.replace(\n        /YYYY|MM|DD|HH|mm|ss/g,\n        (match) => replacements[match],\n    );\n}\n\nfunction isPlainObject(obj) {\n    return (\n        obj !== null &&\n        typeof obj === 'object' &&\n        [null, Object.prototype].includes(Object.getPrototypeOf(obj))\n    );\n}\nexport {\n    isPlainObject,\n    formatDateTime,\n    isValidUUID,\n    ipAddress,\n    isIPv4,\n    isIPv6,\n    isValidPortNumber,\n    isNotBlank,\n    getIfNotBlank,\n    isPresent,\n    getIfPresent,\n    // utf8ArrayToStr,\n    getPolicyDescriptor,\n    getRandomPort,\n    numberToString,\n};\n"
  },
  {
    "path": "backend/src/utils/logical.js",
    "content": "function AND(...args) {\n    return args.reduce((a, b) => a.map((c, i) => b[i] && c));\n}\n\nfunction OR(...args) {\n    return args.reduce((a, b) => a.map((c, i) => b[i] || c));\n}\n\nfunction NOT(array) {\n    return array.map((c) => !c);\n}\n\nfunction FULL(length, bool) {\n    return [...Array(length).keys()].map(() => bool);\n}\n\nexport { AND, OR, NOT, FULL };\n"
  },
  {
    "path": "backend/src/utils/migration.js",
    "content": "import {\n    SUBS_KEY,\n    COLLECTIONS_KEY,\n    SCHEMA_VERSION_KEY,\n    ARTIFACTS_KEY,\n    RULES_KEY,\n    FILES_KEY,\n    TOKENS_KEY,\n} from '@/constants';\nimport $ from '@/core/app';\n\nexport default function migrate() {\n    migrateV2();\n}\n\nfunction migrateV2() {\n    const version = $.read(SCHEMA_VERSION_KEY);\n    if (!version) doMigrationV2();\n\n    // write the current version\n    if (version !== '2.0') {\n        $.write('2.0', SCHEMA_VERSION_KEY);\n    }\n}\n\nfunction doMigrationV2() {\n    $.info('Start migrating...');\n    // 1. migrate subscriptions\n    const subs = $.read(SUBS_KEY) || {};\n    const newSubs = Object.values(subs).map((sub) => {\n        // set default source to remote\n        sub.source = sub.source || 'remote';\n\n        migrateDisplayName(sub);\n        migrateProcesses(sub);\n        return sub;\n    });\n    $.write(newSubs, SUBS_KEY);\n\n    // 2. migrate collections\n    const collections = $.read(COLLECTIONS_KEY) || {};\n    const newCollections = Object.values(collections).map((collection) => {\n        delete collection.ua;\n        migrateDisplayName(collection);\n        migrateProcesses(collection);\n        return collection;\n    });\n    $.write(newCollections, COLLECTIONS_KEY);\n\n    // 3. migrate artifacts\n    const artifacts = $.read(ARTIFACTS_KEY) || {};\n    const newArtifacts = Object.values(artifacts);\n    $.write(newArtifacts, ARTIFACTS_KEY);\n\n    // 4. migrate rules\n    const rules = $.read(RULES_KEY) || {};\n    const newRules = Object.values(rules);\n    $.write(newRules, RULES_KEY);\n\n    // 5. migrate files\n    const files = $.read(FILES_KEY) || {};\n    const newFiles = Object.values(files);\n    $.write(newFiles, FILES_KEY);\n\n    // 6. migrate tokens\n    const tokens = $.read(TOKENS_KEY) || {};\n    const newTokens = Object.values(tokens);\n    $.write(newTokens, TOKENS_KEY);\n\n    // 7. delete builtin rules\n    delete $.cache.builtin;\n    $.info('Migration complete!');\n\n    function migrateDisplayName(item) {\n        const displayName = item['display-name'];\n        if (displayName) {\n            item.displayName = displayName;\n            delete item['display-name'];\n        }\n    }\n\n    function migrateProcesses(item) {\n        const processes = item.process;\n        if (!processes || processes.length === 0) return;\n        const newProcesses = [];\n        const quickSettingOperator = {\n            type: 'Quick Setting Operator',\n            args: {\n                udp: 'DEFAULT',\n                tfo: 'DEFAULT',\n                scert: 'DEFAULT',\n                'vmess aead': 'DEFAULT',\n                useless: 'DEFAULT',\n            },\n        };\n        for (const p of processes) {\n            if (!p.type) continue;\n            if (p.type === 'Useless Filter') {\n                quickSettingOperator.args.useless = 'ENABLED';\n            } else if (p.type === 'Set Property Operator') {\n                const { key, value } = p.args;\n                switch (key) {\n                    case 'udp':\n                        quickSettingOperator.args.udp = value\n                            ? 'ENABLED'\n                            : 'DISABLED';\n                        break;\n                    case 'tfo':\n                        quickSettingOperator.args.tfo = value\n                            ? 'ENABLED'\n                            : 'DISABLED';\n                        break;\n                    case 'skip-cert-verify':\n                        quickSettingOperator.args.scert = value\n                            ? 'ENABLED'\n                            : 'DISABLED';\n                        break;\n                    case 'aead':\n                        quickSettingOperator.args['vmess aead'] = value\n                            ? 'ENABLED'\n                            : 'DISABLED';\n                        break;\n                }\n            } else if (p.type.indexOf('Keyword') !== -1) {\n                // drop keyword operators and keyword filters\n            } else if (p.type === 'Flag Operator') {\n                // set default args\n                const add = typeof p.args === 'undefined' ? true : p.args;\n                p.args = {\n                    mode: add ? 'add' : 'remove',\n                };\n                newProcesses.push(p);\n            } else {\n                newProcesses.push(p);\n            }\n        }\n        newProcesses.unshift(quickSettingOperator);\n        item.process = newProcesses;\n    }\n}\n"
  },
  {
    "path": "backend/src/utils/resource-cache.js",
    "content": "import $ from '@/core/app';\nimport {\n    RESOURCE_CACHE_KEY,\n    DEFAULT_CACHE_TTL,\n    SETTINGS_KEY,\n} from '@/constants';\n\nclass ResourceCache {\n    constructor() {\n        if (!$.read(RESOURCE_CACHE_KEY)) {\n            $.write('{}', RESOURCE_CACHE_KEY);\n        }\n        try {\n            this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));\n        } catch (e) {\n            $.error(\n                `解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${\n                    e?.message ?? e\n                }`,\n            );\n            this.resourceCache = {};\n            $.write('{}', RESOURCE_CACHE_KEY);\n        }\n        this._cleanup();\n    }\n\n    _cleanup(prefix, ttl) {\n        const resolvedTTL = normalizeTTL(ttl) ?? 0;\n        let clear = false;\n        const now = Date.now();\n        Object.entries(this.resourceCache).forEach((entry) => {\n            const [id, cached] = entry;\n            const shouldDelete =\n                !cached.time || cached.time < now + resolvedTTL;\n            if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) {\n                delete this.resourceCache[id];\n                clear = true;\n            }\n        });\n        if (clear) this._persist();\n    }\n\n    revokeAll() {\n        this.resourceCache = {};\n        this._persist();\n    }\n\n    _persist() {\n        $.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);\n    }\n\n    gettime(id) {\n        const time = this.resourceCache[id] && this.resourceCache[id].time;\n        if (time && new Date().getTime() <= time) {\n            return this.resourceCache[id].time;\n        }\n        return null;\n    }\n\n    get(id, ttl, remove) {\n        const resolvedTTL = normalizeTTL(ttl) ?? 0;\n        const cached = this.resourceCache[id];\n        const time = cached && cached.time;\n        if (time) {\n            if (Date.now() + resolvedTTL <= time) return cached.data;\n            if (remove) {\n                delete this.resourceCache[id];\n                this._persist();\n            }\n        }\n        return null;\n    }\n\n    set(id, value, ttl) {\n        const resolvedTTL = normalizeTTL(ttl) ?? getTTL();\n        this.resourceCache[id] = {\n            time: Date.now() + resolvedTTL,\n            data: value,\n        };\n        this._persist();\n    }\n}\n\nfunction normalizeTTL(ttl) {\n    const value = Number(ttl);\n    if (!isFinite(value)) return null;\n    if (value > 0) return value;\n    return null;\n}\n\nfunction getTTL() {\n    const settings = $.read(SETTINGS_KEY);\n    let ttl = settings?.resourceCacheTtl;\n    if (ttl) {\n        ttl = Number(ttl);\n        if (isFinite(ttl) && ttl > 0) {\n            return ttl * 1000;\n        }\n    }\n    return DEFAULT_CACHE_TTL;\n}\n\nexport default new ResourceCache();\n"
  },
  {
    "path": "backend/src/utils/rs.js",
    "content": "import rs from 'jsrsasign';\n\nexport function generateFingerprint(caStr) {\n    const hex = rs.pemtohex(caStr);\n    const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256');\n    return fingerPrint.match(/.{2}/g).join(':').toUpperCase();\n}\n\nexport default {\n    generateFingerprint,\n};\n"
  },
  {
    "path": "backend/src/utils/script-resource-cache.js",
    "content": "import $ from '@/core/app';\nimport {\n    SCRIPT_RESOURCE_CACHE_KEY,\n    DEFAULT_SCRIPT_CACHE_TTL,\n    SETTINGS_KEY,\n} from '@/constants';\n\nclass ResourceCache {\n    constructor() {\n        if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {\n            $.write('{}', SCRIPT_RESOURCE_CACHE_KEY);\n        }\n        try {\n            this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));\n        } catch (e) {\n            $.error(\n                `解析持久化缓存中的 ${SCRIPT_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${\n                    e?.message ?? e\n                }`,\n            );\n            this.resourceCache = {};\n            $.write('{}', SCRIPT_RESOURCE_CACHE_KEY);\n        }\n        this._cleanup();\n    }\n\n    _cleanup(prefix, ttl) {\n        const resolvedTTL = normalizeTTL(ttl) ?? 0;\n        let clear = false;\n        const now = Date.now();\n        Object.entries(this.resourceCache).forEach((entry) => {\n            const [id, cached] = entry;\n            const shouldDelete =\n                !cached.time || cached.time < now + resolvedTTL;\n            if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) {\n                delete this.resourceCache[id];\n                clear = true;\n            }\n        });\n        if (clear) this._persist();\n    }\n\n    revokeAll() {\n        this.resourceCache = {};\n        this._persist();\n    }\n\n    _persist() {\n        $.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);\n    }\n\n    gettime(id) {\n        const time = this.resourceCache[id] && this.resourceCache[id].time;\n        if (time && new Date().getTime() <= time) {\n            return this.resourceCache[id].time;\n        }\n        return null;\n    }\n\n    get(id, ttl, remove) {\n        const resolvedTTL = normalizeTTL(ttl) ?? 0;\n        const cached = this.resourceCache[id];\n        const time = cached && cached.time;\n        if (time) {\n            if (Date.now() + resolvedTTL <= time) return cached.data;\n            if (remove) {\n                delete this.resourceCache[id];\n                this._persist();\n            }\n        }\n        return null;\n    }\n\n    set(id, value, ttl) {\n        const resolvedTTL = normalizeTTL(ttl) ?? getTTL();\n        this.resourceCache[id] = {\n            time: Date.now() + resolvedTTL,\n            data: value,\n        };\n        this._persist();\n    }\n}\n\nfunction normalizeTTL(ttl) {\n    const value = Number(ttl);\n    if (!isFinite(value)) return null;\n    if (value > 0) return value;\n    return null;\n}\n\nfunction getTTL() {\n    const settings = $.read(SETTINGS_KEY);\n    let ttl = settings?.scriptCacheTtl;\n    if (ttl) {\n        ttl = Number(ttl);\n        if (isFinite(ttl) && ttl > 0) {\n            return ttl * 1000;\n        }\n    }\n    return DEFAULT_SCRIPT_CACHE_TTL;\n}\n\nexport default new ResourceCache();\n"
  },
  {
    "path": "backend/src/utils/user-agent.js",
    "content": "import gte from 'semver/functions/gte';\nimport coerce from 'semver/functions/coerce';\nimport $ from '@/core/app';\n\nexport function getUserAgentFromHeaders(headers) {\n    const keys = Object.keys(headers);\n    let UA = '';\n    let ua = '';\n    let accept = '';\n    for (let k of keys) {\n        const lower = k.toLowerCase();\n        if (lower === 'user-agent') {\n            UA = headers[k];\n            ua = UA.toLowerCase();\n        } else if (lower === 'accept') {\n            accept = headers[k];\n        }\n    }\n    return { UA, ua, accept };\n}\n\nexport function getPlatformFromUserAgent({ ua, UA, accept }) {\n    if (UA.indexOf('Quantumult%20X') !== -1) {\n        return 'QX';\n    } else if (ua.indexOf('egern') !== -1) {\n        return 'Egern';\n    } else if (UA.indexOf('Surfboard') !== -1) {\n        return 'Surfboard';\n    } else if (UA.indexOf('Surge Mac') !== -1) {\n        return 'SurgeMac';\n    } else if (UA.indexOf('Surge') !== -1) {\n        return 'Surge';\n    } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {\n        return 'Loon';\n    } else if (UA.indexOf('Shadowrocket') !== -1) {\n        return 'Shadowrocket';\n    } else if (UA.indexOf('Stash') !== -1) {\n        return 'Stash';\n    } else if (\n        ua === 'meta' ||\n        (ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) ||\n        ua.indexOf('clash-verge') !== -1 ||\n        ua.indexOf('flclash') !== -1\n    ) {\n        return 'ClashMeta';\n    } else if (ua.indexOf('clash') !== -1) {\n        return 'Clash';\n    } else if (ua.indexOf('v2ray') !== -1) {\n        return 'V2Ray';\n    } else if (ua.indexOf('sing-box') !== -1 || ua.indexOf('singbox') !== -1) {\n        return 'sing-box';\n    } else if (accept.indexOf('application/json') === 0) {\n        return 'JSON';\n    } else {\n        return 'V2Ray';\n    }\n}\n\nexport function getPlatformFromHeaders(headers) {\n    const { UA, ua, accept } = getUserAgentFromHeaders(headers);\n    return getPlatformFromUserAgent({ ua, UA, accept });\n}\n\nexport function shouldIncludeUnsupportedProxy(platform, headers) {\n    try {\n        const { UA, ua, accept } = getUserAgentFromHeaders(headers);\n        const target = getPlatformFromUserAgent({ UA, ua, accept });\n        const coerceVersion = coerce(ua);\n        const { major } = coerceVersion;\n        if (\n            (['SurgeMac', 'Surge'].includes(platform) &&\n                target === 'SurgeMac' &&\n                major >= 9860) ||\n            (platform === 'Surge' && target === 'Surge' && major >= 3613)\n        ) {\n            return true;\n        }\n        // if (\n        //     platform === 'Egern' &&\n        //     target === 'Egern' &&\n        //     ua.match(/build\\/(\\d+)/i)?.[1] >= 718\n        // ) {\n        //     return true;\n        // }\n        // // if (\n        // //     platform === 'Stash' &&\n        // //     target === 'Stash' &&\n        // //     gte(version, '3.1.0')\n        // // ) {\n        // //     return true;\n        // // }\n\n        // // if (\n        // //     platform === 'Loon' &&\n        // //     target === 'Loon' &&\n        // //     gte(version, '842.0.0')\n        // // ) {\n        // //     return true;\n        // // }\n    } catch (e) {\n        // $.error(`获取版本号失败: ${e}`);\n    }\n    return false;\n}\n"
  },
  {
    "path": "backend/src/utils/yaml.js",
    "content": "import YAML from 'static-js-yaml';\n\nfunction retry(fn, content, ...args) {\n    try {\n        return fn(content, ...args);\n    } catch (e) {\n        return fn(\n            dump(\n                fn(\n                    content.replace(/!<str>\\s*/g, '__SubStoreJSYAMLString__'),\n                    ...args,\n                ),\n            ).replace(/__SubStoreJSYAMLString__/g, ''),\n            ...args,\n        );\n    }\n}\n\nexport function safeLoad(content, ...args) {\n    return retry(YAML.safeLoad, JSON.parse(JSON.stringify(content)), ...args);\n}\nexport function load(content, ...args) {\n    return retry(YAML.load, JSON.parse(JSON.stringify(content)), ...args);\n}\nexport function safeDump(content, ...args) {\n    return YAML.safeDump(JSON.parse(JSON.stringify(content)), ...args);\n}\nexport function dump(content, ...args) {\n    return YAML.dump(JSON.parse(JSON.stringify(content)), ...args);\n}\n\nexport default {\n    safeLoad,\n    load,\n    safeDump,\n    dump,\n    parse: safeLoad,\n    stringify: safeDump,\n};\n"
  },
  {
    "path": "backend/src/vendor/express.js",
    "content": "/* eslint-disable no-undef */\nimport { ENV } from './open-api';\n\nexport default function express({ substore: $, port, host }) {\n    const { isNode } = ENV();\n    const DEFAULT_HEADERS = {\n        'Content-Type': 'text/plain;charset=UTF-8',\n        'Access-Control-Allow-Origin': '*',\n        'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',\n        'Access-Control-Allow-Headers':\n            'Origin, X-Requested-With, Content-Type, Accept',\n        'X-Powered-By': isNode\n            ? eval('process.env.SUB_STORE_X_POWERED_BY') || 'Sub-Store'\n            : 'Sub-Store',\n    };\n\n    // node support\n    if (isNode) {\n        const express_ = eval(`require(\"express\")`);\n        const bodyParser = eval(`require(\"body-parser\")`);\n        const app = express_();\n        const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb';\n        $.info(`[BACKEND] body JSON limit: ${limit}`);\n        app.use(\n            bodyParser.json({\n                verify: rawBodySaver,\n                limit,\n            }),\n        );\n        app.use(\n            bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),\n        );\n        app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));\n        app.use((req, res, next) => {\n            const originalSetHeader = res.setHeader.bind(res);\n\n            res.setHeader = function (name, value) {\n                function normalize(v) {\n                    if (typeof v !== 'string') return v;\n\n                    if (['profile-web-page-url'].includes(name.toLowerCase())) {\n                        try {\n                            const url = new URL(v);\n\n                            return url.href; // 自动 punycode + 标准化\n                        } catch {\n                            return v;\n                        }\n                    }\n\n                    return v;\n                }\n\n                try {\n                    if (Array.isArray(value)) {\n                        value = value.map(normalize);\n                    } else {\n                        value = normalize(value);\n                    }\n\n                    return originalSetHeader(name, value);\n                } catch (err) {\n                    console.log(`Invalid header ignored\\n${name}: ${value}`);\n                    return this;\n                }\n            };\n\n            next();\n        });\n        app.use((_, res, next) => {\n            res.set(DEFAULT_HEADERS);\n            next();\n        });\n\n        // adapter\n        app.start = () => {\n            app.get('*', function (req, res) {\n                res.status(404).end();\n            });\n            const listener = app.listen(port, host, () => {\n                const { address, port } = listener.address();\n                $.info(`[BACKEND] listening on ${address}:${port}`);\n            });\n        };\n        return app;\n    }\n\n    // route handlers\n    const handlers = [];\n\n    // http methods\n    const METHODS_NAMES = [\n        'GET',\n        'POST',\n        'PUT',\n        'DELETE',\n        'PATCH',\n        'OPTIONS',\n        \"HEAD'\",\n        'ALL',\n    ];\n\n    // dispatch url to route\n    const dispatch = (request, start = 0) => {\n        let { method, url, headers, body } = request;\n        headers = formatHeaders(headers);\n        if (/json/i.test(headers['content-type'])) {\n            body = JSON.parse(body);\n        }\n\n        method = method.toUpperCase();\n        const { path, query } = extractURL(url);\n\n        // pattern match\n        let handler = null;\n        let i;\n        let longestMatchedPattern = 0;\n        for (i = start; i < handlers.length; i++) {\n            if (handlers[i].method === 'ALL' || method === handlers[i].method) {\n                const { pattern } = handlers[i];\n                if (patternMatched(pattern, path)) {\n                    if (pattern.split('/').length > longestMatchedPattern) {\n                        handler = handlers[i];\n                        longestMatchedPattern = pattern.split('/').length;\n                    }\n                }\n            }\n        }\n        if (handler) {\n            // dispatch to next handler\n            const next = () => {\n                dispatch(method, url, i);\n            };\n            const req = {\n                method,\n                url,\n                path,\n                query,\n                params: extractPathParams(handler.pattern, path),\n                headers,\n                body,\n            };\n            const res = Response();\n            const cb = handler.callback;\n\n            const errFunc = (err) => {\n                res.status(500).json({\n                    status: 'failed',\n                    message: `Internal Server Error: ${err}`,\n                });\n            };\n\n            if (cb.constructor.name === 'AsyncFunction') {\n                cb(req, res, next).catch(errFunc);\n            } else {\n                try {\n                    cb(req, res, next);\n                } catch (err) {\n                    errFunc(err);\n                }\n            }\n        } else {\n            // no route, return 404\n            const res = Response();\n            res.status(404).json({\n                status: 'failed',\n                message: 'ERROR: 404 not found',\n            });\n        }\n    };\n\n    const app = {};\n\n    // attach http methods\n    METHODS_NAMES.forEach((method) => {\n        app[method.toLowerCase()] = (pattern, callback) => {\n            // add handler\n            handlers.push({ method, pattern, callback });\n        };\n    });\n\n    // chainable route\n    app.route = (pattern) => {\n        const chainApp = {};\n        METHODS_NAMES.forEach((method) => {\n            chainApp[method.toLowerCase()] = (callback) => {\n                // add handler\n                handlers.push({ method, pattern, callback });\n                return chainApp;\n            };\n        });\n        return chainApp;\n    };\n\n    // start service\n    app.start = () => {\n        dispatch($request);\n    };\n\n    return app;\n\n    /************************************************\n     Utility Functions\n     *************************************************/\n    function rawBodySaver(req, res, buf, encoding) {\n        if (buf && buf.length) {\n            req.rawBody = buf.toString(encoding || 'utf8');\n        }\n    }\n\n    function Response() {\n        let statusCode = 200;\n        const { isQX, isLoon, isSurge, isGUIforCores } = ENV();\n        const headers = DEFAULT_HEADERS;\n        const STATUS_CODE_MAP = {\n            200: 'HTTP/1.1 200 OK',\n            201: 'HTTP/1.1 201 Created',\n            302: 'HTTP/1.1 302 Found',\n            307: 'HTTP/1.1 307 Temporary Redirect',\n            308: 'HTTP/1.1 308 Permanent Redirect',\n            404: 'HTTP/1.1 404 Not Found',\n            500: 'HTTP/1.1 500 Internal Server Error',\n        };\n        return new (class {\n            status(code) {\n                statusCode = code;\n                return this;\n            }\n\n            send(body = '') {\n                const response = {\n                    status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode,\n                    body,\n                    headers,\n                };\n                if (isQX || isGUIforCores) {\n                    $done(response);\n                } else if (isLoon || isSurge) {\n                    $done({\n                        response,\n                    });\n                }\n            }\n\n            end() {\n                this.send();\n            }\n\n            html(data) {\n                this.set('Content-Type', 'text/html;charset=UTF-8');\n                this.send(data);\n            }\n\n            json(data) {\n                this.set('Content-Type', 'application/json;charset=UTF-8');\n                this.send(JSON.stringify(data));\n            }\n\n            set(key, val) {\n                headers[key] = val;\n                return this;\n            }\n\n            removeHeader(key) {\n                delete headers[key];\n                return this;\n            }\n        })();\n    }\n}\n\nfunction formatHeaders(headers) {\n    const result = {};\n    for (const k of Object.keys(headers)) {\n        result[k.toLowerCase()] = headers[k];\n    }\n    return result;\n}\n\nfunction patternMatched(pattern, path) {\n    if (pattern instanceof RegExp && pattern.test(path)) {\n        return true;\n    } else {\n        // root pattern, match all\n        if (pattern === '/') return true;\n        // normal string pattern\n        if (pattern.indexOf(':') === -1) {\n            const spath = path.split('/');\n            const spattern = pattern.split('/');\n            for (let i = 0; i < spattern.length; i++) {\n                if (spath[i] !== spattern[i]) {\n                    return false;\n                }\n            }\n            return true;\n        } else if (extractPathParams(pattern, path)) {\n            // string pattern with path parameters\n            return true;\n        }\n    }\n    return false;\n}\n\nfunction extractURL(url) {\n    // extract path\n    const match = url.match(/https?:\\/\\/[^/]+(\\/[^?]*)/) || [];\n    const path = match[1] || '/';\n\n    // extract query string\n    const split = url.indexOf('?');\n    const query = {};\n    if (split !== -1) {\n        let hashes = url.slice(url.indexOf('?') + 1).split('&');\n        for (let i = 0; i < hashes.length; i++) {\n            const hash = hashes[i].split('=');\n            query[hash[0]] = decodeURIComponent(hash[1]);\n        }\n    }\n    return {\n        path,\n        query,\n    };\n}\n\nfunction extractPathParams(pattern, path) {\n    if (pattern.indexOf(':') === -1) {\n        return null;\n    } else {\n        const params = {};\n        for (let i = 0, j = 0; i < pattern.length; i++, j++) {\n            if (pattern[i] === ':') {\n                let key = [];\n                let val = [];\n                while (pattern[++i] !== '/' && i < pattern.length) {\n                    key.push(pattern[i]);\n                }\n                while (path[j] !== '/' && j < path.length) {\n                    val.push(path[j++]);\n                }\n                params[key.join('')] = decodeURIComponent(val.join(''));\n            } else {\n                if (pattern[i] !== path[j]) {\n                    return null;\n                }\n            }\n        }\n        return params;\n    }\n}\n"
  },
  {
    "path": "backend/src/vendor/md5.js",
    "content": "/*\n * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message\n * Digest Algorithm, as defined in RFC 1321.\n * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009\n * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet\n * Distributed under the BSD License\n * See http://pajhome.org.uk/crypt/md5 for more info.\n */\n\n/*\n * Configurable variables. You may need to tweak these to be compatible with\n * the server-side, but the defaults work in most cases.\n */\nvar hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase        */\nvar b64pad = ''; /* base-64 pad character. \"=\" for strict RFC compliance   */\n\n/*\n * These are the functions you'll usually want to call\n * They take string arguments and return either hex or base-64 encoded strings\n */\nexport function hex_md5(s) {\n    return rstr2hex(rstr_md5(str2rstr_utf8(s)));\n}\n\nexport function b64_md5(s) {\n    return rstr2b64(rstr_md5(str2rstr_utf8(s)));\n}\n\nexport function any_md5(s, e) {\n    return rstr2any(rstr_md5(str2rstr_utf8(s)), e);\n}\n\nexport function hex_hmac_md5(k, d) {\n    return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));\n}\n\nexport function b64_hmac_md5(k, d) {\n    return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));\n}\n\nexport function any_hmac_md5(k, d, e) {\n    return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e);\n}\n\n/*\n * Perform a simple self-test to see if the VM is working\n */\nfunction md5_vm_test() {\n    return hex_md5('abc').toLowerCase() == '900150983cd24fb0d6963f7d28e17f72';\n}\n\n/*\n * Calculate the MD5 of a raw string\n */\nfunction rstr_md5(s) {\n    return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));\n}\n\n/*\n * Calculate the HMAC-MD5, of a key and some data (raw strings)\n */\nfunction rstr_hmac_md5(key, data) {\n    var bkey = rstr2binl(key);\n    if (bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);\n\n    var ipad = Array(16),\n        opad = Array(16);\n    for (var i = 0; i < 16; i++) {\n        ipad[i] = bkey[i] ^ 0x36363636;\n        opad[i] = bkey[i] ^ 0x5c5c5c5c;\n    }\n\n    var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);\n    return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));\n}\n\n/*\n * Convert a raw string to a hex string\n */\nfunction rstr2hex(input) {\n    try {\n        hexcase;\n    } catch (e) {\n        hexcase = 0;\n    }\n    var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef';\n    var output = '';\n    var x;\n    for (var i = 0; i < input.length; i++) {\n        x = input.charCodeAt(i);\n        output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f);\n    }\n    return output;\n}\n\n/*\n * Convert a raw string to a base-64 string\n */\nfunction rstr2b64(input) {\n    try {\n        b64pad;\n    } catch (e) {\n        b64pad = '';\n    }\n    var tab =\n        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n    var output = '';\n    var len = input.length;\n    for (var i = 0; i < len; i += 3) {\n        var triplet =\n            (input.charCodeAt(i) << 16) |\n            (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) |\n            (i + 2 < len ? input.charCodeAt(i + 2) : 0);\n        for (var j = 0; j < 4; j++) {\n            if (i * 8 + j * 6 > input.length * 8) output += b64pad;\n            else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f);\n        }\n    }\n    return output;\n}\n\n/*\n * Convert a raw string to an arbitrary string encoding\n */\nfunction rstr2any(input, encoding) {\n    var divisor = encoding.length;\n    var i, j, q, x, quotient;\n\n    /* Convert to an array of 16-bit big-endian values, forming the dividend */\n    var dividend = Array(Math.ceil(input.length / 2));\n    for (i = 0; i < dividend.length; i++) {\n        dividend[i] =\n            (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);\n    }\n\n    /*\n     * Repeatedly perform a long division. The binary array forms the dividend,\n     * the length of the encoding is the divisor. Once computed, the quotient\n     * forms the dividend for the next step. All remainders are stored for later\n     * use.\n     */\n    var full_length = Math.ceil(\n        (input.length * 8) / (Math.log(encoding.length) / Math.log(2)),\n    );\n    var remainders = Array(full_length);\n    for (j = 0; j < full_length; j++) {\n        quotient = Array();\n        x = 0;\n        for (i = 0; i < dividend.length; i++) {\n            x = (x << 16) + dividend[i];\n            q = Math.floor(x / divisor);\n            x -= q * divisor;\n            if (quotient.length > 0 || q > 0) quotient[quotient.length] = q;\n        }\n        remainders[j] = x;\n        dividend = quotient;\n    }\n\n    /* Convert the remainders to the output string */\n    var output = '';\n    for (i = remainders.length - 1; i >= 0; i--)\n        output += encoding.charAt(remainders[i]);\n\n    return output;\n}\n\n/*\n * Encode a string as utf-8.\n * For efficiency, this assumes the input is valid utf-16.\n */\nfunction str2rstr_utf8(input) {\n    var output = '';\n    var i = -1;\n    var x, y;\n\n    while (++i < input.length) {\n        /* Decode utf-16 surrogate pairs */\n        x = input.charCodeAt(i);\n        y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;\n        if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) {\n            x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff);\n            i++;\n        }\n\n        /* Encode output as utf-8 */\n        if (x <= 0x7f) output += String.fromCharCode(x);\n        else if (x <= 0x7ff)\n            output += String.fromCharCode(\n                0xc0 | ((x >>> 6) & 0x1f),\n                0x80 | (x & 0x3f),\n            );\n        else if (x <= 0xffff)\n            output += String.fromCharCode(\n                0xe0 | ((x >>> 12) & 0x0f),\n                0x80 | ((x >>> 6) & 0x3f),\n                0x80 | (x & 0x3f),\n            );\n        else if (x <= 0x1fffff)\n            output += String.fromCharCode(\n                0xf0 | ((x >>> 18) & 0x07),\n                0x80 | ((x >>> 12) & 0x3f),\n                0x80 | ((x >>> 6) & 0x3f),\n                0x80 | (x & 0x3f),\n            );\n    }\n    return output;\n}\n\n/*\n * Encode a string as utf-16\n */\nfunction str2rstr_utf16le(input) {\n    var output = '';\n    for (var i = 0; i < input.length; i++)\n        output += String.fromCharCode(\n            input.charCodeAt(i) & 0xff,\n            (input.charCodeAt(i) >>> 8) & 0xff,\n        );\n    return output;\n}\n\nfunction str2rstr_utf16be(input) {\n    var output = '';\n    for (var i = 0; i < input.length; i++)\n        output += String.fromCharCode(\n            (input.charCodeAt(i) >>> 8) & 0xff,\n            input.charCodeAt(i) & 0xff,\n        );\n    return output;\n}\n\n/*\n * Convert a raw string to an array of little-endian words\n * Characters >255 have their high-byte silently ignored.\n */\nfunction rstr2binl(input) {\n    var output = Array(input.length >> 2);\n    for (var i = 0; i < output.length; i++) output[i] = 0;\n    for (var i = 0; i < input.length * 8; i += 8)\n        output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;\n    return output;\n}\n\n/*\n * Convert an array of little-endian words to a string\n */\nfunction binl2rstr(input) {\n    var output = '';\n    for (var i = 0; i < input.length * 32; i += 8)\n        output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);\n    return output;\n}\n\n/*\n * Calculate the MD5 of an array of little-endian words, and a bit length.\n */\nfunction binl_md5(x, len) {\n    /* append padding */\n    x[len >> 5] |= 0x80 << len % 32;\n    x[(((len + 64) >>> 9) << 4) + 14] = len;\n\n    var a = 1732584193;\n    var b = -271733879;\n    var c = -1732584194;\n    var d = 271733878;\n\n    for (var i = 0; i < x.length; i += 16) {\n        var olda = a;\n        var oldb = b;\n        var oldc = c;\n        var oldd = d;\n\n        a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);\n        d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);\n        c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);\n        b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);\n        a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);\n        d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);\n        c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);\n        b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);\n        a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);\n        d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);\n        c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);\n        b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);\n        a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);\n        d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);\n        c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);\n        b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);\n\n        a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);\n        d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);\n        c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);\n        b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);\n        a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);\n        d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);\n        c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);\n        b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);\n        a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);\n        d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);\n        c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);\n        b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);\n        a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);\n        d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);\n        c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);\n        b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);\n\n        a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);\n        d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);\n        c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);\n        b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);\n        a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);\n        d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);\n        c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);\n        b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);\n        a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);\n        d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);\n        c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);\n        b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);\n        a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);\n        d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);\n        c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);\n        b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);\n\n        a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);\n        d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);\n        c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);\n        b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);\n        a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);\n        d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);\n        c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);\n        b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);\n        a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);\n        d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);\n        c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);\n        b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);\n        a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);\n        d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);\n        c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);\n        b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);\n\n        a = safe_add(a, olda);\n        b = safe_add(b, oldb);\n        c = safe_add(c, oldc);\n        d = safe_add(d, oldd);\n    }\n    return Array(a, b, c, d);\n}\n\n/*\n * These functions implement the four basic operations the algorithm uses.\n */\nfunction md5_cmn(q, a, b, x, s, t) {\n    return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);\n}\n\nfunction md5_ff(a, b, c, d, x, s, t) {\n    return md5_cmn((b & c) | (~b & d), a, b, x, s, t);\n}\n\nfunction md5_gg(a, b, c, d, x, s, t) {\n    return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);\n}\n\nfunction md5_hh(a, b, c, d, x, s, t) {\n    return md5_cmn(b ^ c ^ d, a, b, x, s, t);\n}\n\nfunction md5_ii(a, b, c, d, x, s, t) {\n    return md5_cmn(c ^ (b | ~d), a, b, x, s, t);\n}\n\n/*\n * Add integers, wrapping at 2^32. This uses 16-bit operations internally\n * to work around bugs in some JS interpreters.\n */\nfunction safe_add(x, y) {\n    var lsw = (x & 0xffff) + (y & 0xffff);\n    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);\n    return (msw << 16) | (lsw & 0xffff);\n}\n\n/*\n * Bitwise rotate a 32-bit number to the left.\n */\nfunction bit_rol(num, cnt) {\n    return (num << cnt) | (num >>> (32 - cnt));\n}\n"
  },
  {
    "path": "backend/src/vendor/open-api.js",
    "content": "/* eslint-disable no-undef */\nconst isQX = typeof $task !== 'undefined';\nconst isLoon = typeof $loon !== 'undefined';\n// 可能有一些兼容环境依赖于这个, 先不改成 $environment.surge-version\nconst isSurge = typeof $httpClient !== 'undefined' && !isLoon;\nconst isNode = eval(`typeof process !== \"undefined\"`); // eval is needed in order to avoid browserify processing\nconst isStash =\n    'undefined' !== typeof $environment && $environment['stash-version'];\nconst isShadowRocket = 'undefined' !== typeof $rocket;\nconst isEgern = 'undefined' !== typeof Egern && Egern.version;\nconst isLanceX = 'undefined' != typeof $native;\nconst isGUIforCores = typeof $Plugins !== 'undefined';\nimport { Base64 } from 'js-base64';\n\nfunction isPlainObject(obj) {\n    return (\n        obj !== null &&\n        typeof obj === 'object' &&\n        [null, Object.prototype].includes(Object.getPrototypeOf(obj))\n    );\n}\n\nfunction parseSocks5Uri(uri) {\n    // eslint-disable-next-line no-unused-vars\n    let [__, username, password, server, port, query, name] = uri.match(\n        /^socks5:\\/\\/(?:(.*?):(.*?)@)?(.*?)(?::(\\d+?))?(\\?.*?)?(?:#(.*?))?$/,\n    );\n    if (port) {\n        port = parseInt(port, 10);\n    } else {\n        $.error(`port is not present in line: ${uri}`);\n        throw new Error(`port is not present in line: ${uri}`);\n    }\n    return {\n        type: 5,\n        host: server,\n        port,\n\n        userId: username != null ? decodeURIComponent(username) : undefined,\n        password: password != null ? decodeURIComponent(password) : undefined,\n    };\n}\nexport class OpenAPI {\n    constructor(name = 'untitled', debug = false) {\n        this.name = name;\n        this.debug = debug;\n\n        this.http = HTTP();\n        this.env = ENV();\n\n        if (isNode) {\n            const dotenv = eval(`require(\"dotenv\")`);\n            dotenv.config();\n        }\n        this.node = (() => {\n            if (isNode) {\n                const fs = eval(\"require('fs')\");\n\n                return {\n                    fs,\n                };\n            } else {\n                return null;\n            }\n        })();\n        this.initCache();\n\n        const delay = (t, v) =>\n            new Promise(function (resolve) {\n                setTimeout(resolve.bind(null, v), t);\n            });\n\n        Promise.prototype.delay = async function (t) {\n            const v = await this;\n            return await delay(t, v);\n        };\n    }\n\n    // persistence\n    // initialize cache\n    initCache() {\n        if (isQX)\n            this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');\n        if (isLoon || isSurge)\n            this.cache = JSON.parse($persistentStore.read(this.name) || '{}');\n        if (isGUIforCores)\n            this.cache = JSON.parse(\n                $Plugins.SubStoreCache.get(this.name) || '{}',\n            );\n        if (isNode) {\n            // create a json for root cache\n            const basePath =\n                eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';\n            let rootPath = `${basePath}/root.json`;\n            const backupRootPath = `${basePath}/root_${Date.now()}.json`;\n\n            this.log(`Root path: ${rootPath}`);\n            if (this.node.fs.existsSync(rootPath)) {\n                try {\n                    this.root = JSON.parse(\n                        this.node.fs.readFileSync(`${rootPath}`),\n                    );\n                } catch (e) {\n                    this.node.fs.copyFileSync(rootPath, backupRootPath);\n                    this.error(\n                        `Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`,\n                    );\n                }\n            }\n            if (!isPlainObject(this.root)) {\n                this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {\n                    flag: 'w',\n                });\n                this.root = {};\n            }\n\n            // create a json file with the given name if not exists\n            let fpath = `${basePath}/${this.name}.json`;\n            const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;\n\n            this.log(`Data path: ${fpath}`);\n            if (this.node.fs.existsSync(fpath)) {\n                try {\n                    this.cache = JSON.parse(\n                        this.node.fs.readFileSync(`${fpath}`, 'utf-8'),\n                    );\n                    if (!isPlainObject(this.cache))\n                        throw new Error('Invalid Data');\n                } catch (e) {\n                    try {\n                        const str = Base64.decode(\n                            this.node.fs.readFileSync(`${fpath}`, 'utf-8'),\n                        );\n                        this.cache = JSON.parse(str);\n                        this.node.fs.writeFileSync(fpath, str, {\n                            flag: 'w',\n                        });\n                        if (!isPlainObject(this.cache))\n                            throw new Error('Invalid Data');\n                    } catch (e) {\n                        this.node.fs.copyFileSync(fpath, backupPath);\n                        this.error(\n                            `Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`,\n                        );\n                    }\n                }\n            }\n            if (!isPlainObject(this.cache)) {\n                this.node.fs.writeFileSync(fpath, JSON.stringify({}), {\n                    flag: 'w',\n                });\n                this.cache = {};\n            }\n        }\n    }\n\n    // store cache\n    persistCache() {\n        const data = JSON.stringify(this.cache, null, 2);\n        if (isQX) $prefs.setValueForKey(data, this.name);\n        if (isLoon || isSurge) $persistentStore.write(data, this.name);\n        if (isGUIforCores) $Plugins.SubStoreCache.set(this.name, data);\n        if (isNode) {\n            const basePath =\n                eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';\n\n            this.node.fs.writeFileSync(\n                `${basePath}/${this.name}.json`,\n                data,\n                { flag: 'w' },\n                (err) => console.log(err),\n            );\n            this.node.fs.writeFileSync(\n                `${basePath}/root.json`,\n                JSON.stringify(this.root, null, 2),\n                { flag: 'w' },\n                (err) => console.log(err),\n            );\n        }\n    }\n\n    write(data, key) {\n        this.log(`SET ${key}`);\n        if (key.indexOf('#') !== -1) {\n            key = key.substr(1);\n            if (isSurge || isLoon) {\n                return $persistentStore.write(data, key);\n            }\n            if (isQX) {\n                return $prefs.setValueForKey(data, key);\n            }\n            if (isNode) {\n                this.root[key] = data;\n            }\n            if (isGUIforCores) {\n                return $Plugins.SubStoreCache.set(key, data);\n            }\n        } else {\n            this.cache[key] = data;\n        }\n        this.persistCache();\n    }\n\n    read(key) {\n        this.log(`READ ${key}`);\n        if (key.indexOf('#') !== -1) {\n            key = key.substr(1);\n            if (isSurge || isLoon) {\n                return $persistentStore.read(key);\n            }\n            if (isQX) {\n                return $prefs.valueForKey(key);\n            }\n            if (isNode) {\n                return this.root[key];\n            }\n            if (isGUIforCores) {\n                return $Plugins.SubStoreCache.get(key);\n            }\n        } else {\n            return this.cache[key];\n        }\n    }\n\n    delete(key) {\n        this.log(`DELETE ${key}`);\n        if (key.indexOf('#') !== -1) {\n            key = key.substr(1);\n            if (isSurge || isLoon) {\n                return $persistentStore.write(null, key);\n            }\n            if (isQX) {\n                return $prefs.removeValueForKey(key);\n            }\n            if (isNode) {\n                delete this.root[key];\n            }\n            if (isGUIforCores) {\n                return $Plugins.SubStoreCache.remove(key);\n            }\n        } else {\n            delete this.cache[key];\n        }\n        this.persistCache();\n    }\n\n    // notification\n    notify(title, subtitle = '', content = '', options = {}) {\n        const openURL = options['open-url'];\n        const mediaURL = options['media-url'];\n\n        if (isQX) $notify(title, subtitle, content, options);\n        if (isSurge) {\n            $notification.post(\n                title,\n                subtitle,\n                content + `${mediaURL ? '\\n多媒体:' + mediaURL : ''}`,\n                {\n                    url: openURL,\n                },\n            );\n        }\n        if (isLoon) {\n            let opts = {};\n            if (openURL) opts['openUrl'] = openURL;\n            if (mediaURL) opts['mediaUrl'] = mediaURL;\n            if (JSON.stringify(opts) === '{}') {\n                $notification.post(title, subtitle, content);\n            } else {\n                $notification.post(title, subtitle, content, opts);\n            }\n        }\n        if (isNode) {\n            const content_ =\n                content +\n                (openURL ? `\\n点击跳转: ${openURL}` : '') +\n                (mediaURL ? `\\n多媒体: ${mediaURL}` : '');\n            console.log(`${title}\\n${subtitle}\\n${content_}\\n\\n`);\n\n            let push = eval('process.env.SUB_STORE_PUSH_SERVICE');\n            if (push) {\n                if (/^https?:\\/\\//.test(push)) {\n                    // 处理 HTTP/HTTPS URL\n                    const url = push\n                        .replace(\n                            '[推送标题]',\n                            encodeURIComponent(title || 'Sub-Store'),\n                        )\n                        .replace(\n                            '[推送内容]',\n                            encodeURIComponent(\n                                [subtitle, content_].map((i) => i).join('\\n'),\n                            ),\n                        );\n                    const $http = HTTP();\n                    $http\n                        .get({ url })\n                        .then((resp) => {\n                            console.log(\n                                `[Push Service] URL: ${url}\\nRES: ${resp.statusCode} ${resp.body}`,\n                            );\n                        })\n                        .catch((e) => {\n                            console.log(\n                                `[Push Service] URL: ${url}\\nERROR: ${e}`,\n                            );\n                        });\n                } else {\n                    const { execFile } = eval(`require(\"child_process\")`);\n                    execFile(\n                        'shoutrrr',\n                        [\n                            'send',\n                            '--url',\n                            push,\n                            '--message',\n                            `${title}\\n${subtitle}\\n${content_}`,\n                        ],\n                        (error, stdout, stderr) => {\n                            if (error) {\n                                console.log(\n                                    `[Push Service] URL: ${push}\\nERROR: ${error}`,\n                                );\n                                return;\n                            }\n                            if (stderr) {\n                                console.log(\n                                    `[Push Service] URL: ${push}\\nstderr: ${stderr}`,\n                                );\n                            }\n                            console.log(\n                                `[Push Service] URL: ${push}\\nstdout: ${stdout}`,\n                            );\n                        },\n                    );\n                }\n            }\n        }\n        if (isGUIforCores) {\n            $Plugins.Notify(title, subtitle + '\\n' + content);\n        }\n    }\n\n    // other helper functions\n    log(msg) {\n        if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);\n    }\n\n    info(msg) {\n        console.log(`[${this.name}] INFO: ${msg}`);\n    }\n\n    error(msg) {\n        console.log(`[${this.name}] ERROR: ${msg}`);\n    }\n\n    wait(millisec) {\n        return new Promise((resolve) => setTimeout(resolve, millisec));\n    }\n\n    done(value = {}) {\n        if (isQX || isLoon || isSurge || isGUIforCores) {\n            $done(value);\n        } else if (isNode) {\n            if (typeof $context !== 'undefined') {\n                $context.headers = value.headers;\n                $context.statusCode = value.statusCode;\n                $context.body = value.body;\n            }\n        }\n    }\n}\n\nexport function ENV() {\n    return {\n        isQX,\n        isLoon,\n        isSurge,\n        isNode,\n        isStash,\n        isShadowRocket,\n        isEgern,\n        isLanceX,\n        isGUIforCores,\n    };\n}\n\nexport function HTTP(defaultOptions = { baseURL: '' }) {\n    const { isQX, isLoon, isSurge, isNode, isGUIforCores } = ENV();\n    const methods = [\n        'GET',\n        'POST',\n        'PUT',\n        'DELETE',\n        'HEAD',\n        'OPTIONS',\n        'PATCH',\n    ];\n    const URL_REGEX =\n        /https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;\n\n    function send(method, options) {\n        options = typeof options === 'string' ? { url: options } : options;\n        const baseURL = defaultOptions.baseURL;\n        if (baseURL && !URL_REGEX.test(options.url || '')) {\n            options.url = baseURL ? baseURL + options.url : options.url;\n        }\n        options = { ...defaultOptions, ...options };\n        const timeout = options.timeout;\n        const events = {\n            ...{\n                onRequest: () => {},\n                onResponse: (resp) => resp,\n                onTimeout: () => {},\n            },\n            ...options.events,\n        };\n\n        events.onRequest(method, options);\n\n        if (options.node) {\n            // Surge & Loon allow connecting to a server using a specified proxy node\n            if (isSurge) {\n                const build = $environment['surge-build'];\n                if (build && parseInt(build) >= 2407) {\n                    options['policy-descriptor'] = options.node;\n                    delete options.node;\n                }\n            }\n        }\n\n        let worker;\n        if (isQX) {\n            worker = $task.fetch({\n                method,\n                url: options.url,\n                headers: options.headers,\n                body: options.body,\n                opts: options.opts,\n            });\n        } else if (isLoon || isSurge || isNode) {\n            worker = new Promise(async (resolve, reject) => {\n                const body = options.body;\n                const opts = JSON.parse(JSON.stringify(options));\n                opts.body = body;\n                opts.timeout = opts.timeout || 8000;\n                if (opts.timeout) {\n                    opts.timeout++;\n                    if (isNaN(opts.timeout)) {\n                        opts.timeout = 8000;\n                    }\n                    if (!isNode) {\n                        let unit = 'ms';\n                        // 这些客户端单位为 s\n                        if (isSurge || isStash || isShadowRocket) {\n                            opts.timeout = Math.ceil(opts.timeout / 1000);\n                            unit = 's';\n                        }\n                        // Loon 为 ms\n                        // console.log(`[httpClient timeout] ${opts.timeout}${unit}`);\n                    }\n                }\n                if (isNode) {\n                    const undici = eval(\"require('undici')\");\n                    const { socksDispatcher } = eval(\"require('fetch-socks')\");\n                    const {\n                        ProxyAgent,\n                        EnvHttpProxyAgent,\n                        request,\n                        interceptors,\n                    } = undici;\n                    const agentOpts = {\n                        connect: {\n                            rejectUnauthorized:\n                                opts.strictSSL === false ||\n                                opts.insecure === true ||\n                                opts.rejectUnauthorized === false\n                                    ? false\n                                    : true,\n                        },\n                        bodyTimeout: opts.timeout,\n                        headersTimeout: opts.timeout,\n                        maxHeaderSize:\n                            eval('process.env.SUB_STORE_MAX_HEADER_SIZE') ||\n                            32 * 1024,\n                    };\n                    const tlsOptions = {\n                        rejectUnauthorized:\n                            agentOpts.connect.rejectUnauthorized,\n                    };\n                    opts.tls = {\n                        ...(opts.tls || {}),\n                        ...tlsOptions,\n                    };\n                    try {\n                        const url = new URL(opts.url);\n                        if (url.username || url.password) {\n                            opts.headers = {\n                                ...(opts.headers || {}),\n                                Authorization: `Basic ${Buffer.from(\n                                    `${url.username || ''}:${\n                                        url.password || ''\n                                    }`,\n                                ).toString('base64')}`,\n                            };\n                        }\n                        let dispatcher;\n                        if (!opts.proxy) {\n                            const allProxy =\n                                eval('process.env.all_proxy') ||\n                                eval('process.env.ALL_PROXY');\n                            if (allProxy && /^socks5:\\/\\//.test(allProxy)) {\n                                opts.proxy = allProxy;\n                            }\n                        }\n                        if (opts.proxy) {\n                            if (/^socks5:\\/\\//.test(opts.proxy)) {\n                                dispatcher = socksDispatcher(\n                                    parseSocks5Uri(opts.proxy),\n                                    {\n                                        ...agentOpts,\n                                        requestTls: tlsOptions,\n                                    },\n                                );\n                            } else {\n                                dispatcher = new ProxyAgent({\n                                    ...agentOpts,\n                                    uri: opts.proxy,\n                                    requestTls: tlsOptions,\n                                });\n                            }\n                        } else {\n                            dispatcher = new EnvHttpProxyAgent({\n                                ...agentOpts,\n                                requestTls: tlsOptions,\n                            });\n                        }\n                        const response = await request(opts.url, {\n                            ...opts,\n                            method: method.toUpperCase(),\n                            dispatcher: dispatcher.compose(\n                                interceptors.redirect({\n                                    maxRedirections: 3,\n                                    throwOnMaxRedirects: true,\n                                }),\n                            ),\n                        });\n                        resolve({\n                            statusCode: response.statusCode,\n                            headers: response.headers,\n                            body:\n                                opts.encoding === null\n                                    ? await response.body.arrayBuffer()\n                                    : await response.body.text(),\n                        });\n                    } catch (e) {\n                        reject(e);\n                    }\n                } else {\n                    $httpClient[method.toLowerCase()](\n                        opts,\n                        (err, response, body) => {\n                            // if (err) {\n                            //     console.log(err);\n                            // } else {\n                            //     console.log({\n                            //         statusCode:\n                            //             response.status || response.statusCode,\n                            //         headers: response.headers,\n                            //         body,\n                            //     });\n                            // }\n\n                            if (err) reject(err);\n                            else\n                                resolve({\n                                    statusCode:\n                                        response.status || response.statusCode,\n                                    headers: response.headers,\n                                    body,\n                                });\n                        },\n                    );\n                }\n            });\n        } else if (isGUIforCores) {\n            worker = new Promise(async (resolve, reject) => {\n                try {\n                    const response = await $Plugins.Requests({\n                        method,\n                        url: options.url,\n                        headers: options.headers,\n                        body: options.body,\n                        autoTransformBody: false,\n                        options: {\n                            Proxy: options.proxy,\n                            Timeout: options.timeout\n                                ? options.timeout / 1000\n                                : 15,\n                        },\n                    });\n                    resolve({\n                        statusCode: response.status,\n                        headers: response.headers,\n                        body: response.body,\n                    });\n                } catch (error) {\n                    reject(error);\n                }\n            });\n        }\n\n        let timeoutid;\n\n        const timer = timeout\n            ? new Promise((_, reject) => {\n                  //   console.log(`[request timeout] ${timeout}ms`);\n                  timeoutid = setTimeout(() => {\n                      events.onTimeout();\n                      return reject(\n                          `${method} URL: ${options.url} exceeds the timeout ${timeout} ms`,\n                      );\n                  }, timeout);\n              })\n            : null;\n\n        return (\n            timer\n                ? Promise.race([timer, worker]).then((res) => {\n                      if (typeof clearTimeout !== 'undefined') {\n                          clearTimeout(timeoutid);\n                      }\n                      return res;\n                  })\n                : worker\n        ).then((resp) => events.onResponse(resp));\n    }\n\n    const http = {};\n    methods.forEach(\n        (method) =>\n            (http[method.toLowerCase()] = (options) => send(method, options)),\n    );\n    return http;\n}\n"
  },
  {
    "path": "config/Egern.yaml",
    "content": "name: Sub-Store\ndescription: \"支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *\"\ncompat_arguments:\n  ability: http-client-policy\n  cronexp: 55 23 * * *\n  sync: \"Sub-Store Sync\"\n  timeout: 120\n  engine: auto\n  produce: \"# Sub-Store Produce\"\n  produce_cronexp: 50 */6 * * *\n  produce_sub: \"sub1,sub2\"\n  produce_col: \"col1,col2\"\ncompat_arguments_desc: '\\n1️⃣ ability\\n\\n默认已开启测落地能力\\n需要配合脚本操作\\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\\n填写任意其他值关闭\\n\\n2️⃣ cronexp\\n\\n同步配置定时任务\\n默认为每天 23 点 55 分\\n\\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\\n\\n3️⃣ sync\\n\\n自定义定时任务名\\n便于在脚本编辑器中选择\\n若设为 # 可取消定时任务\\n\\n4️⃣ timeout\\n\\n脚本超时, 单位为秒\\n\\n5️⃣ engine\\n\\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\\n\\n6️⃣ produce\\n\\n自定义处理订阅的定时任务名\\n一般用于定时处理耗时较长的订阅, 以更新缓存\\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\\n若设为 # 可取消此定时任务\\n默认不开启\\n\\n7️⃣ produce_cronexp\\n\\n配置处理订阅的定时任务\\n\\n默认为每 6 小时\\n\\n9️⃣ produce_sub\\n\\n自定义需定时处理的单条订阅名\\n多个用 , 连接\\n\\n🔟 produce_col\\n\\n自定义需定时处理的组合订阅名\\n多个用 , 连接\\n\\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\\n如果名称需要编码, 请编码后再用 , 连接\\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'\nscriptings:\n  - http_request:\n      name: Sub-Store Core\n      match: ^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info)))\n      script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js\n      body_required: true\n  - http_request:\n      name: Sub-Store Simple\n      match: ^https?:\\/\\/sub\\.store\n      script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js\n      body_required: true\n  - schedule:\n      name: \"{{{sync}}}\"\n      cron: \"{{{cronexp}}}\"\n      script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js\n  - schedule:\n      name: \"{{{produce}}}\"\n      cron: \"{{{produce_cronexp}}}\"\n      script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js\n      arguments:\n        _compat.$argument: \"sub={{{produce_sub}}}&col={{{produce_col}}}\"\nmitm:\n  hostnames:\n    includes:\n      - sub.store\n"
  },
  {
    "path": "config/Loon.plugin",
    "content": "#!name=Sub-Store\n#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分, 可在插件设置中自定义. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n#!openUrl=https://sub.store\n#!author=Peng-YM\n#!homepage=https://github.com/sub-store-org/Sub-Store\n#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png\n\n[Argument]\ncron=input, \"55 23 * * *\", tag=定时参数, desc=这里需要输入符合CRON表达式的参数\n\n[Rule]\nDOMAIN,sub-store.vercel.app,PROXY\n\n[MITM]\nhostname=sub.store\n\n[Script]\nhttp-request ^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core\nhttp-request ^https?:\\/\\/sub\\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple\n\ncron {cron} script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync"
  },
  {
    "path": "config/QX-Task.json",
    "content": "{\n  \"name\": \"Sub-Store\",\n  \"description\": \"定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\",\n  \"task\": [\n    \"55 23 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png\"\n  ]\n}"
  },
  {
    "path": "config/QX.snippet",
    "content": "hostname=sub.store\n\n^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js\n^https?:\\/\\/sub\\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js"
  },
  {
    "path": "config/README.md",
    "content": "# Sub-Store 配置指南\n\n## 查看更新说明:\n\nSub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)\n\nTelegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)\n\n## 服务器/云平台/Docker/Android 版\n\nhttps://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87\n\n## App 版\n\n### 1. Loon\n\n安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。\n\n资源解析器中使用 [https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-parser.loon.min.js](https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-parser.loon.min.js)\n\n### 2. Surge\n\n#### 关于 Surge 的格外说明\n\nSurge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E)\n\n定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)\n\n0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)\n\n1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)\n\n> 最新版 Surge 已删除 `ability: http-client-policy` 参数, 模块暂不做修改, 对测落地功能无影响\n\n2. 经典版, 不支持编辑参数, 固定带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者 cname 脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)\n\n3. 经典版, 不支持编辑参数, 固定不带 ability 参数版本： [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)\n\n### 3. QX\n\n订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。\n\n定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)\n\n### 4. Stash\n\n安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。\n\n### 5. Shadowrocket\n\n安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。\n\n### 6. Egern\n\n安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。\n\n## 使用 Sub-Store\n\n1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示，说明 Sub-Store 已经配置成功。\n2. 可以把 Sub-Store 添加到主屏幕，即可获得类似于 APP 的使用体验。\n3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。\n\n## 链接参数说明\n\nhttps://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E\n\n## 脚本使用说明\n\nhttps://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E\n"
  },
  {
    "path": "config/Stash.stoverride",
    "content": "name: Sub-Store\ndesc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天  23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\nicon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png\n\nhttp:\n  mitm:\n    - sub.store\n  script:\n    - match: ^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info)))\n      name: sub-store-1\n      type: request\n      require-body: true\n      timeout: 120\n    - match: ^https?:\\/\\/sub\\.store\n      name: sub-store-0\n      type: request\n      require-body: true\n      timeout: 120\n\ncron:\n  script:\n    - name: cron-sync-artifacts\n      cron: \"55 23 * * *\"\n      timeout: 120\n\nscript-providers:\n  sub-store-0:\n    url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js\n    interval: 86400\n\n  sub-store-1:\n    url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js\n    interval: 86400\n\n  cron-sync-artifacts:\n    url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js\n    interval: 86400\n"
  },
  {
    "path": "config/Surge-Beta.sgmodule",
    "content": "#!name=Sub-Store(β)\n#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *\n#!category=订阅管理\n#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:\"Sub-Store Sync\",timeout:120,engine:auto,produce:\"# Sub-Store Produce\",produce_cronexp:50 */6 * * *,produce_sub:\"sub1,sub2\",produce_col:\"col1,col2\",max_size:-1\n#!arguments-desc=\\n1️⃣ ability\\n\\n默认已开启测落地能力\\n需要配合脚本操作\\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\\n填写任意其他值关闭\\n\\n2️⃣ cronexp\\n\\n同步配置定时任务\\n默认为每天 23 点 55 分\\n\\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\\n\\n3️⃣ sync\\n\\n自定义定时任务名\\n便于在脚本编辑器中选择\\n若设为 # 可取消定时任务\\n\\n4️⃣ timeout\\n\\n脚本超时, 单位为秒\\n\\n5️⃣ engine\\n\\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\\n\\n6️⃣ produce\\n\\n自定义处理订阅的定时任务名\\n一般用于定时处理耗时较长的订阅, 以更新缓存\\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\\n若设为 # 可取消此定时任务\\n默认不开启\\n\\n7️⃣ produce_cronexp\\n\\n配置处理订阅的定时任务\\n\\n默认为每 6 小时\\n\\n9️⃣ produce_sub\\n\\n自定义需定时处理的单条订阅名\\n多个用 , 连接\\n\\n🔟 produce_col\\n\\n自定义需定时处理的组合订阅名\\n多个用 , 连接\\n\\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\\n如果名称需要编码, 请编码后再用 , 连接\\n顺序: 并发执行单条订阅, 然后并发执行组合订阅\n\n[MITM]\nhostname = %APPEND% sub.store\n\n[Script]\nSub-Store Core=type=http-request,pattern=^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability=\"{{{ability}}}\",engine={{{engine}}},max-size={{{max_size}}}\n\nSub-Store Simple=type=http-request,pattern=^https?:\\/\\/sub\\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}},max-size={{{max_size}}}\n\n{{{sync}}}=type=cron,cronexp=\"{{{cronexp}}}\",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}\n\n{{{produce}}}=type=cron,cronexp=\"{{{produce_cronexp}}}\",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument=\"sub={{{produce_sub}}}&col={{{produce_col}}}\""
  },
  {
    "path": "config/Surge-Noability.sgmodule",
    "content": "#!name=Sub-Store\n#!desc=高级订阅管理工具 @Peng-YM  无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天  23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n#!category=订阅管理\n\n[MITM]\nhostname = %APPEND% sub.store\n\n[Script]\n# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存，这个参数在 Surge 非常占用内存； 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本\nSub-Store Core=type=http-request,pattern=^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120\nSub-Store Simple=type=http-request,pattern=^https?:\\/\\/sub\\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120\n\nSub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js\n"
  },
  {
    "path": "config/Surge-ability.sgmodule",
    "content": "#!name=Sub-Store\n#!desc=高级订阅管理工具 @Peng-YM  带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n#!category=订阅管理\n\n[MITM]\nhostname = %APPEND% sub.store\n\n[Script]\nSub-Store Core=type=http-request,pattern=^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy\nSub-Store Simple=type=http-request,pattern=^https?:\\/\\/sub\\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120\n\nSub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js\n"
  },
  {
    "path": "config/Surge.sgmodule",
    "content": "#!name=Sub-Store\n#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *\n#!category=订阅管理\n#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:\"Sub-Store Sync\",timeout:120,engine:auto,produce:\"# Sub-Store Produce\",produce_cronexp:50 */6 * * *,produce_sub:\"sub1,sub2\",produce_col:\"col1,col2\",max_size:-1\n#!arguments-desc=\\n1️⃣ ability\\n\\n默认已开启测落地能力\\n需要配合脚本操作\\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\\n填写任意其他值关闭\\n\\n2️⃣ cronexp\\n\\n同步配置定时任务\\n默认为每天 23 点 55 分\\n\\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\\n\\n3️⃣ sync\\n\\n自定义定时任务名\\n便于在脚本编辑器中选择\\n若设为 # 可取消定时任务\\n\\n4️⃣ timeout\\n\\n脚本超时, 单位为秒\\n\\n5️⃣ engine\\n\\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\\n\\n6️⃣ produce\\n\\n自定义处理订阅的定时任务名\\n一般用于定时处理耗时较长的订阅, 以更新缓存\\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\\n若设为 # 可取消此定时任务\\n默认不开启\\n\\n7️⃣ produce_cronexp\\n\\n配置处理订阅的定时任务\\n\\n默认为每 6 小时\\n\\n9️⃣ produce_sub\\n\\n自定义需定时处理的单条订阅名\\n多个用 , 连接\\n\\n🔟 produce_col\\n\\n自定义需定时处理的组合订阅名\\n多个用 , 连接\\n\\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\\n如果名称需要编码, 请编码后再用 , 连接\\n顺序: 并发执行单条订阅, 然后并发执行组合订阅\n\n[MITM]\nhostname = %APPEND% sub.store\n\n[Script]\nSub-Store Core=type=http-request,pattern=^https?:\\/\\/sub\\.store\\/((download)|api\\/(preview|sync|(utils\\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability=\"{{{ability}}}\",engine={{{engine}}},max-size={{{max_size}}}\n\nSub-Store Simple=type=http-request,pattern=^https?:\\/\\/sub\\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}},max-size={{{max_size}}}\n\n{{{sync}}}=type=cron,cronexp=\"{{{cronexp}}}\",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}\n\n{{{produce}}}=type=cron,cronexp=\"{{{produce_cronexp}}}\",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument=\"sub={{{produce_sub}}}&col={{{produce_col}}}\""
  },
  {
    "path": "scripts/demo.js",
    "content": "function operator(proxies = [], targetPlatform, context) {\n  // 支持快捷操作 不一定要写一个 function\n  // 可参考 https://t.me/zhetengsha/970\n  // https://t.me/zhetengsha/1009\n\n  // proxies 为传入的内部节点数组\n  // 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅\n  // 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货. 顺便说一下, 关于 mihomo 不支持的协议, 其实也可以用 JSON/JSON5/YAML 格式来输入, 写法可参考使用 includeUnsupportedProxy 参数或开启 包含官方/商店版不支持的协议 开关时的 mihomo 输出内容, 例如 NaiveProxy 输入写法 (https://t.me/zhetengsha/4308)\n  // 1. `_no-resolve` 为不解析域名\n  // 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功\n  // 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP\n  // 4. 节点字段 _exec 为 mihomo 路径, 默认 /usr/local/bin/mihomo; 节点字段 _localPort 端口为初始端口号, 逐个递减, 默认为 65535. _defaultNameserver(默认为 [ '180.76.76.76', '52.80.52.52', '119.28.28.28', '223.6.6.6' ]) 和 _nameserver (默认为 [ 'https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query', 'https://doh-pure.onedns.net/dns-query' ]) 为数组 用于自定义mihomo 的 default-nameserver 和 nameserver, 这个是配置 Surge for macOS 必须手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议. 详见 https://t.me/zhetengsha/1735\n  // 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名\n  // 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名\n  // 7. `tls-fingerprint` 为 tls 指纹\n  // 8. `underlying-proxy` 为前置代理, 不同平台会自动转换\n  //    例如 $server['underlying-proxy'] = '名称'\n  //    只给 mihomo 输出的话, `dialer-proxy` 也行\n  //    只给 sing-box 输出的话, `detour` 也行\n  //    只给 Egern 输出的话, `prev_hop` 也行\n  //    只给 Shadowrocket 输出的话, `chain` 也行\n  //    输出到 Clash/Stash 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.\n  // 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑),  输出时删除\n  // 10. `sni` 在某些协议里会自动与 `servername` 转换\n  // 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)\n  // 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`\n  // 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时\n  // 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔\n  // 15. `ip-version` 设置节点使用 IP 版本，兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server`\n  // 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`\n  // 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换\n  // 18. `sing-box` 支持 `_fragment`, `_fragment_fallback_delay`, `_record_fragment` 设置 `tls` 的 `fragment`, `fragment_fallback_delay`, `record_fragment`\n  // 19. `sing-box` 支持 `_certificate`, `_certificate_path`, `_certificate_public_key_sha256`, `_client_certificate`, `_client_certificate_path`, `_client_key`, `_client_key_path` 设置 `tls` 的 `certificate`, `certificate_path`, `certificate_public_key_sha256`, `client_certificate`, `client_certificate_path`, `client_key`, `client_key_path`\n  // 20. `sing-box` 支持使用完整的 `_ech` 结构设置 `tls` 的 `ech`. 避免冲突, URI 里的改为 _echConfigList\n  // 21. `sing-box` 支持使用完整的 `_curve_preferences` 结构设置 `tls` 的 `curve_preferences`\n  // 22. `interface-name` 指定流量出站接口 只给 Surge 用的话, `interface` 也可以\n\n  // require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块\n  // 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件\n  // const fs = eval(`require(\"fs\")`)\n  // // const path = eval(`require(\"path\")`)\n  // fs.writeFileSync('/tmp/1.txt', $content, \"utf8\");\n\n  // $arguments 为传入的脚本参数\n\n  // $options 为通过链接传入的参数\n  // 例如: { arg1: 'a', arg2: 'b' }\n  // 可这样传:\n  // 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))\n  // /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D\n  // 或这样传:\n  // 先这样处理 encodeURIComponent('arg1=a&arg2=b')\n  // /api/file/foo?$options=arg1%3Da%26arg2%3Db\n\n  // 注意, 编辑页面左下角那个即可预览只是获取数据 并不是一个真实的请求, 故此时无法使用 $options\n  // 默认会带上 _req 字段, 结构为\n  // {\n  //     method,\n  //     url,\n  //     path,\n  //     query,\n  //     params,\n  //     headers,\n  //     body,\n  // }\n  // console.log($options)\n\n  // 若设置 $options._res.headers\n  // 则会在输出时设置响应头, 例如:\n  // if ($options) {\n  //   $options._res = {\n  //     headers: {\n  //       'X-Custom': '1'\n  //     }\n  //   }\n  // }\n\n  // 若设置 $options._res.status\n  // 则会在输出时设置响应状态码, 例如:\n  // if ($options) {\n  //   $options._res = {\n  //     status: 404\n  //   }\n  // }\n\n  // 一个示例: 请求来自分享且 ua 不符合时, 返回自定义状态码和响应内容\n\n  // const { headers, url, path } = $options?._req || {}\n  // const ua = headers?.['user-agent'] || headers?.['User-Agent']\n\n  // if ($options && /^\\/share\\//.test(url) && !/surge/i.test(ua)) {\n  //   $options._res = {\n  //     status: 418\n  //   }\n  //   $content = `I'm a teapot`\n  // }\n\n  // targetPlatform 为输出的目标平台\n\n  // lodash\n\n  // $substore 为 OpenAPI\n  // 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md\n\n  // scriptResourceCache 缓存\n  // 可参考 https://t.me/zhetengsha/1003\n  // const cache = scriptResourceCache\n  // 写入\n  // 第三个参数为自定义过期时间(单位: 毫秒)\n  // cache.set('a:1', 1, 1000)\n  // cache.set('a:2', 2)\n  // 获取\n  // cache.get('a:1')\n  // 获取到期时间\n  // cache.gettime('a:1')\n  // 支持第二个参数: 自定义过期时间(单位: 毫秒)\n  // 支持第三个参数: 是否删除过期项\n  // 下面的例子意思是原来是看 a:2 现在有没有到期的, 加了自定义过期时间后是看 +1000ms 会不会过期, 如果过期就删除\n  // cache.get('a:2', 1000, true)\n\n  // 清理\n  // 本来是内部的 反正也能用...先这么用吧...\n  // 清理所有过期的\n  // cache._cleanup()\n  // 支持第一个参数: 匹配前缀的项\n  // 支持第二个参数: 自定义过期时间(单位: 毫秒)\n  // 只清理 a: 开头的过期项\n  // cache._cleanup('a:')\n  // 如果想删除所有的 a: 开头的过期项, 目前先传一个大的过期时间吧...\n  // cache._cleanup(undefined, 48 * 3600 * 1000)\n  // 下面的例子意思是原来是看现在有没有到期的, 加了自定义过期时间后是看 +1000ms 会不会过期, 如果过期就删除\n  // cache._cleanup(undefined, 1000)\n\n  // 关于缓存时长\n\n  // 拉取 Sub-Store 订阅时, 会自动拉取远程订阅\n\n  // 通过链接下载资源时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true\n\n  // 前端(>= 2.16.0) 后端(>= 2.21.0) 支持自定义各种缓存的 TTL 配置\n\n  // 持久化缓存数据在 JSON 里\n\n  // 当配合脚本使用时, 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活\n\n  // async function operator() {\n  //     scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);\n  // }\n\n  // ProxyUtils 为节点处理工具\n  // 可参考 https://t.me/zhetengsha/1066\n  // const ProxyUtils = {\n  //     parse, // 订阅解析\n  //     process, // 节点操作/文件操作\n  //     produce, // 输出订阅\n  //     getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000)\n  //     ipAddress, // https://github.com/beaugunderson/ip-address\n  //     isIPv4,\n  //     isIPv6,\n  //     isIP,\n  //     yaml, // yaml 解析和生成\n  //     getFlag, // 获取 emoji 旗帜\n  //     removeFlag, // 移除 emoji 旗帜\n  //     getISO, // 获取 ISO 3166-1 alpha-2 代码\n  //     Gist, // Gist 类\n  //     download, // 内部的下载方法, 见 backend/src/utils/download.js\n  //     downloadFile, // 下载二进制文件, 见 backend/src/utils/download.js\n  //     MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269\n  //     isValidUUID, // 辅助判断是否为有效的 UUID\n  //     Buffer, // https://github.com/feross/buffer\n  //     Base64, // https://github.com/dankogai/js-base64\n  //     JSON5, // https://github.com/json5/json5\n  // }\n  //  为兼容 https://github.com/xishang0128/sparkle 的 JavaScript 覆写, 也可以直接使用 `b64d`(Base64 解码), `b64e`(Base64 编码), `Buffer`, `yaml`(简单兼容了下 `yaml.parse` 和 `yaml.stringify`)\n\n  // 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009\n  // ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一\n  // 示例: 给节点名添加前缀\n  // $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`\n  // 示例: 给节点名添加旗帜\n  // $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`\n\n  // 示例: 从 sni 文件中读取内容并进行节点操作\n  // const sni = await produceArtifact({\n  //     type: 'file',\n  //     name: 'sni' // 文件名\n  // });\n  // $server.sni = sni\n\n  // 示例: 从 config 文件中读取配置项并进行节点操作\n  // config 的本地内容为\n  // {\n  //   \"reuse\": false\n  // }\n  // 脚本操作为\n  // const config = (ProxyUtils.JSON5 || JSON).parse(await produceArtifact({\n  //     type: 'file',\n  //     name: 'config' // 文件名\n  // }))\n  // $server.reuse = config.reuse\n\n  // 1. Surge 输出 WireGuard 完整配置\n\n  // let proxies = await produceArtifact({\n  //   type: 'subscription',\n  //   name: 'sub',\n  //   platform: 'Surge',\n  //   produceOpts: {\n  //     'include-unsupported-proxy': true,\n  //   }\n  // })\n  // $content = proxies\n\n  // 2. sing-box\n\n  // 但是一般不需要这样用, 可参考\n  // 1. https://t.me/zhetengsha/1111\n  // 2. https://t.me/zhetengsha/1070\n  // 3. https://t.me/zhetengsha/1241\n\n  // let singboxProxies = await produceArtifact({\n  //     type: 'subscription', // type: 'subscription' 或 'collection'\n  //     name: 'sub', // subscription name\n  //     platform: 'sing-box', // target platform\n  //     produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') )\n  // })\n\n  // // JSON\n  // $content = JSON.stringify({}, null, 2)\n\n  // 3. clash.meta\n\n  // 但是一般不需要这样用, 可参考\n  // 1. https://t.me/zhetengsha/1111\n  // 2. https://t.me/zhetengsha/1070\n  // 3. https://t.me/zhetengsha/1234\n\n  // let clashMetaProxies = await produceArtifact({\n  //     type: 'subscription',\n  //     name: 'sub',\n  //     platform: 'ClashMeta',\n  //     produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )\n  // })\n\n  // 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist\n  // 见 https://t.me/zhetengsha/1428\n  //\n  // const content = ProxyUtils.produce([...proxies], platform)\n\n  // // YAML\n  // ProxyUtils.yaml.load('YAML String')\n  // ProxyUtils.yaml.safeLoad('YAML String')\n  // $content = ProxyUtils.yaml.safeDump({})\n  // $content = ProxyUtils.yaml.dump({})\n\n  // 一个往文件里插入本地节点的例子:\n  // const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0])\n  // let clashMetaProxies = await produceArtifact({\n  //     type: 'collection',\n  //     name: '机场',\n  //     platform: 'ClashMeta',\n  //     produceType: 'internal'\n  // })\n  // yaml.proxies.unshift(...clashMetaProxies)\n  // $content = ProxyUtils.yaml.dump(yaml)\n\n  // { $content, $files, $options } will be passed to the next operator\n  // $content is the final content of the file\n\n  // flowUtils 为机场订阅流量信息处理工具\n  // 可参考:\n  // 1. https://t.me/zhetengsha/948\n\n  // context 为传入的上下文, 可在多个脚本中共享使用\n  // 其中 env 为 环境信息, 包含运行版本和其他后端信息\n\n  // 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可)\n\n  // 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上\n\n  // 若存在 `source._collection` 但 `source._collection.subscriptions` 中的 key 在 `source` 上不存在, 说明输出结果为组合订阅, 脚本设置在组合订阅上\n\n  // 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上\n\n  // 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称\n\n  // let name = ''\n  // for (const [key, value] of Object.entries(context.source)) {\n  //   if (!key.startsWith('_')) {\n  //     name = value.displayName || value.name\n  //     break\n  //   }\n  // }\n  // if (!name) {\n  //   const collection = context.source._collection\n  //   name = collection.displayName || collection.name\n  // }\n\n  // 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:\n  // {\n  //   \"source\": {\n  //     \"sub-1\": {\n  //       \"name\": \"sub-1\",\n  //       \"displayName\": \"\",\n  //       \"mergeSources\": \"\",\n  //       \"ignoreFailedRemoteSub\": true,\n  //       \"process\": [],\n  //       \"icon\": \"\",\n  //       \"source\": \"local\",\n  //       \"url\": \"\",\n  //       \"content\": \"\",\n  //       \"ua\": \"\",\n  //       \"display-name\": \"\",\n  //       \"useCacheForFailedRemoteSub\": false\n  //     }\n  //   },\n  //   \"backend\": \"Node\",\n  //   \"version\": \"2.14.198\"\n  // }\n  // 2. 输出组合订阅 collection-1 时, 该组合订阅中的脚本上下文为:\n  // {\n  //   \"source\": {\n  //     \"_collection\": {\n  //       \"name\": \"collection-1\",\n  //       \"displayName\": \"\",\n  //       \"mergeSources\": \"\",\n  //       \"ignoreFailedRemoteSub\": false,\n  //       \"icon\": \"\",\n  //       \"process\": [],\n  //       \"subscriptions\": [\n  //         \"sub-1\"\n  //       ],\n  //       \"display-name\": \"\"\n  //     }\n  //   },\n  //   \"backend\": \"Node\",\n  //   \"version\": \"2.14.198\"\n  // }\n  // 3. 输出组合订阅 collection-1 时, 该组合订阅中的单条订阅 sub-1 中的某个脚本上下文为:\n  // {\n  //   \"source\": {\n  //     \"sub-1\": {\n  //       \"name\": \"sub-1\",\n  //       \"displayName\": \"\",\n  //       \"mergeSources\": \"\",\n  //       \"ignoreFailedRemoteSub\": true,\n  //       \"icon\": \"\",\n  //       \"process\": [],\n  //       \"source\": \"local\",\n  //       \"url\": \"\",\n  //       \"content\": \"\",\n  //       \"ua\": \"\",\n  //       \"display-name\": \"\",\n  //       \"useCacheForFailedRemoteSub\": false\n  //     },\n  //     \"_collection\": {\n  //       \"name\": \"collection-1\",\n  //       \"displayName\": \"\",\n  //       \"mergeSources\": \"\",\n  //       \"ignoreFailedRemoteSub\": false,\n  //       \"icon\": \"\",\n  //       \"process\": [],\n  //       \"subscriptions\": [\n  //         \"sub-1\"\n  //       ],\n  //       \"display-name\": \"\"\n  //     }\n  //   },\n  //   \"backend\": \"Node\",\n  //   \"version\": \"2.14.198\"\n  // }\n\n  // 参数说明\n  // 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E\n\n  console.log(JSON.stringify(context, null, 2));\n\n  return proxies;\n}\n"
  },
  {
    "path": "scripts/fancy-characters.js",
    "content": "/**\n * 节点名改为花里胡哨字体，仅支持英文字符和数字\n *\n * 【字体】\n * 可参考：https://www.dute.org/weird-fonts\n * serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)\n *\n * 【示例】\n * 1️⃣ 设置所有格式为 \"serif-bold\"\n * #type=serif-bold\n *\n * 2️⃣ 设置字母格式为 \"serif-bold\"，数字格式为 \"circle-regular\"\n * #type=serif-bold&num=circle-regular\n */\n\nfunction operator(proxies) {\n    const { type, num } = $arguments;\n    const TABLE = {\n        \"serif-bold\": [\"𝟎\",\"𝟏\",\"𝟐\",\"𝟑\",\"𝟒\",\"𝟓\",\"𝟔\",\"𝟕\",\"𝟖\",\"𝟗\",\"𝐚\",\"𝐛\",\"𝐜\",\"𝐝\",\"𝐞\",\"𝐟\",\"𝐠\",\"𝐡\",\"𝐢\",\"𝐣\",\"𝐤\",\"𝐥\",\"𝐦\",\"𝐧\",\"𝐨\",\"𝐩\",\"𝐪\",\"𝐫\",\"𝐬\",\"𝐭\",\"𝐮\",\"𝐯\",\"𝐰\",\"𝐱\",\"𝐲\",\"𝐳\",\"𝐀\",\"𝐁\",\"𝐂\",\"𝐃\",\"𝐄\",\"𝐅\",\"𝐆\",\"𝐇\",\"𝐈\",\"𝐉\",\"𝐊\",\"𝐋\",\"𝐌\",\"𝐍\",\"𝐎\",\"𝐏\",\"𝐐\",\"𝐑\",\"𝐒\",\"𝐓\",\"𝐔\",\"𝐕\",\"𝐖\",\"𝐗\",\"𝐘\",\"𝐙\"] ,\n        \"serif-italic\": [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"𝑎\", \"𝑏\", \"𝑐\", \"𝑑\", \"𝑒\", \"𝑓\", \"𝑔\", \"ℎ\", \"𝑖\", \"𝑗\", \"𝑘\", \"𝑙\", \"𝑚\", \"𝑛\", \"𝑜\", \"𝑝\", \"𝑞\", \"𝑟\", \"𝑠\", \"𝑡\", \"𝑢\", \"𝑣\", \"𝑤\", \"𝑥\", \"𝑦\", \"𝑧\", \"𝐴\", \"𝐵\", \"𝐶\", \"𝐷\", \"𝐸\", \"𝐹\", \"𝐺\", \"𝐻\", \"𝐼\", \"𝐽\", \"𝐾\", \"𝐿\", \"𝑀\", \"𝑁\", \"𝑂\", \"𝑃\", \"𝑄\", \"𝑅\", \"𝑆\", \"𝑇\", \"𝑈\", \"𝑉\", \"𝑊\", \"𝑋\", \"𝑌\", \"𝑍\"],\n        \"serif-bold-italic\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝒂\",\"𝒃\",\"𝒄\",\"𝒅\",\"𝒆\",\"𝒇\",\"𝒈\",\"𝒉\",\"𝒊\",\"𝒋\",\"𝒌\",\"𝒍\",\"𝒎\",\"𝒏\",\"𝒐\",\"𝒑\",\"𝒒\",\"𝒓\",\"𝒔\",\"𝒕\",\"𝒖\",\"𝒗\",\"𝒘\",\"𝒙\",\"𝒚\",\"𝒛\",\"𝑨\",\"𝑩\",\"𝑪\",\"𝑫\",\"𝑬\",\"𝑭\",\"𝑮\",\"𝑯\",\"𝑰\",\"𝑱\",\"𝑲\",\"𝑳\",\"𝑴\",\"𝑵\",\"𝑶\",\"𝑷\",\"𝑸\",\"𝑹\",\"𝑺\",\"𝑻\",\"𝑼\",\"𝑽\",\"𝑾\",\"𝑿\",\"𝒀\",\"𝒁\"],\n        \"sans-serif-regular\": [\"𝟢\", \"𝟣\", \"𝟤\", \"𝟥\", \"𝟦\", \"𝟧\", \"𝟨\", \"𝟩\", \"𝟪\", \"𝟫\", \"𝖺\", \"𝖻\", \"𝖼\", \"𝖽\", \"𝖾\", \"𝖿\", \"𝗀\", \"𝗁\", \"𝗂\", \"𝗃\", \"𝗄\", \"𝗅\", \"𝗆\", \"𝗇\", \"𝗈\", \"𝗉\", \"𝗊\", \"𝗋\", \"𝗌\", \"𝗍\", \"𝗎\", \"𝗏\", \"𝗐\", \"𝗑\", \"𝗒\", \"𝗓\", \"𝖠\", \"𝖡\", \"𝖢\", \"𝖣\", \"𝖤\", \"𝖥\", \"𝖦\", \"𝖧\", \"𝖨\", \"𝖩\", \"𝖪\", \"𝖫\", \"𝖬\", \"𝖭\", \"𝖮\", \"𝖯\", \"𝖰\", \"𝖱\", \"𝖲\", \"𝖳\", \"𝖴\", \"𝖵\", \"𝖶\", \"𝖷\", \"𝖸\", \"𝖹\"],\n        \"sans-serif-bold\": [\"𝟬\",\"𝟭\",\"𝟮\",\"𝟯\",\"𝟰\",\"𝟱\",\"𝟲\",\"𝟳\",\"𝟴\",\"𝟵\",\"𝗮\",\"𝗯\",\"𝗰\",\"𝗱\",\"𝗲\",\"𝗳\",\"𝗴\",\"𝗵\",\"𝗶\",\"𝗷\",\"𝗸\",\"𝗹\",\"𝗺\",\"𝗻\",\"𝗼\",\"𝗽\",\"𝗾\",\"𝗿\",\"𝘀\",\"𝘁\",\"𝘂\",\"𝘃\",\"𝘄\",\"𝘅\",\"𝘆\",\"𝘇\",\"𝗔\",\"𝗕\",\"𝗖\",\"𝗗\",\"𝗘\",\"𝗙\",\"𝗚\",\"𝗛\",\"𝗜\",\"𝗝\",\"𝗞\",\"𝗟\",\"𝗠\",\"𝗡\",\"𝗢\",\"𝗣\",\"𝗤\",\"𝗥\",\"𝗦\",\"𝗧\",\"𝗨\",\"𝗩\",\"𝗪\",\"𝗫\",\"𝗬\",\"𝗭\"],\n        \"sans-serif-italic\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝘢\",\"𝘣\",\"𝘤\",\"𝘥\",\"𝘦\",\"𝘧\",\"𝘨\",\"𝘩\",\"𝘪\",\"𝘫\",\"𝘬\",\"𝘭\",\"𝘮\",\"𝘯\",\"𝘰\",\"𝘱\",\"𝘲\",\"𝘳\",\"𝘴\",\"𝘵\",\"𝘶\",\"𝘷\",\"𝘸\",\"𝘹\",\"𝘺\",\"𝘻\",\"𝘈\",\"𝘉\",\"𝘊\",\"𝘋\",\"𝘌\",\"𝘍\",\"𝘎\",\"𝘏\",\"𝘐\",\"𝘑\",\"𝘒\",\"𝘓\",\"𝘔\",\"𝘕\",\"𝘖\",\"𝘗\",\"𝘘\",\"𝘙\",\"𝘚\",\"𝘛\",\"𝘜\",\"𝘝\",\"𝘞\",\"𝘟\",\"𝘠\",\"𝘡\"],\n        \"sans-serif-bold-italic\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝙖\",\"𝙗\",\"𝙘\",\"𝙙\",\"𝙚\",\"𝙛\",\"𝙜\",\"𝙝\",\"𝙞\",\"𝙟\",\"𝙠\",\"𝙡\",\"𝙢\",\"𝙣\",\"𝙤\",\"𝙥\",\"𝙦\",\"𝙧\",\"𝙨\",\"𝙩\",\"𝙪\",\"𝙫\",\"𝙬\",\"𝙭\",\"𝙮\",\"𝙯\",\"𝘼\",\"𝘽\",\"𝘾\",\"𝘿\",\"𝙀\",\"𝙁\",\"𝙂\",\"𝙃\",\"𝙄\",\"𝙅\",\"𝙆\",\"𝙇\",\"𝙈\",\"𝙉\",\"𝙊\",\"𝙋\",\"𝙌\",\"𝙍\",\"𝙎\",\"𝙏\",\"𝙐\",\"𝙑\",\"𝙒\",\"𝙓\",\"𝙔\",\"𝙕\"],\n        \"script-regular\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝒶\",\"𝒷\",\"𝒸\",\"𝒹\",\"ℯ\",\"𝒻\",\"ℊ\",\"𝒽\",\"𝒾\",\"𝒿\",\"𝓀\",\"𝓁\",\"𝓂\",\"𝓃\",\"ℴ\",\"𝓅\",\"𝓆\",\"𝓇\",\"𝓈\",\"𝓉\",\"𝓊\",\"𝓋\",\"𝓌\",\"𝓍\",\"𝓎\",\"𝓏\",\"𝒜\",\"ℬ\",\"𝒞\",\"𝒟\",\"ℰ\",\"ℱ\",\"𝒢\",\"ℋ\",\"ℐ\",\"𝒥\",\"𝒦\",\"ℒ\",\"ℳ\",\"𝒩\",\"𝒪\",\"𝒫\",\"𝒬\",\"ℛ\",\"𝒮\",\"𝒯\",\"𝒰\",\"𝒱\",\"𝒲\",\"𝒳\",\"𝒴\",\"𝒵\"],\n        \"script-bold\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝓪\",\"𝓫\",\"𝓬\",\"𝓭\",\"𝓮\",\"𝓯\",\"𝓰\",\"𝓱\",\"𝓲\",\"𝓳\",\"𝓴\",\"𝓵\",\"𝓶\",\"𝓷\",\"𝓸\",\"𝓹\",\"𝓺\",\"𝓻\",\"𝓼\",\"𝓽\",\"𝓾\",\"𝓿\",\"𝔀\",\"𝔁\",\"𝔂\",\"𝔃\",\"𝓐\",\"𝓑\",\"𝓒\",\"𝓓\",\"𝓔\",\"𝓕\",\"𝓖\",\"𝓗\",\"𝓘\",\"𝓙\",\"𝓚\",\"𝓛\",\"𝓜\",\"𝓝\",\"𝓞\",\"𝓟\",\"𝓠\",\"𝓡\",\"𝓢\",\"𝓣\",\"𝓤\",\"𝓥\",\"𝓦\",\"𝓧\",\"𝓨\",\"𝓩\"],\n        \"fraktur-regular\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝔞\",\"𝔟\",\"𝔠\",\"𝔡\",\"𝔢\",\"𝔣\",\"𝔤\",\"𝔥\",\"𝔦\",\"𝔧\",\"𝔨\",\"𝔩\",\"𝔪\",\"𝔫\",\"𝔬\",\"𝔭\",\"𝔮\",\"𝔯\",\"𝔰\",\"𝔱\",\"𝔲\",\"𝔳\",\"𝔴\",\"𝔵\",\"𝔶\",\"𝔷\",\"𝔄\",\"𝔅\",\"ℭ\",\"𝔇\",\"𝔈\",\"𝔉\",\"𝔊\",\"ℌ\",\"ℑ\",\"𝔍\",\"𝔎\",\"𝔏\",\"𝔐\",\"𝔑\",\"𝔒\",\"𝔓\",\"𝔔\",\"ℜ\",\"𝔖\",\"𝔗\",\"𝔘\",\"𝔙\",\"𝔚\",\"𝔛\",\"𝔜\",\"ℨ\"],\n        \"fraktur-bold\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"𝖆\",\"𝖇\",\"𝖈\",\"𝖉\",\"𝖊\",\"𝖋\",\"𝖌\",\"𝖍\",\"𝖎\",\"𝖏\",\"𝖐\",\"𝖑\",\"𝖒\",\"𝖓\",\"𝖔\",\"𝖕\",\"𝖖\",\"𝖗\",\"𝖘\",\"𝖙\",\"𝖚\",\"𝖛\",\"𝖜\",\"𝖝\",\"𝖞\",\"𝖟\",\"𝕬\",\"𝕭\",\"𝕮\",\"𝕯\",\"𝕰\",\"𝕱\",\"𝕲\",\"𝕳\",\"𝕴\",\"𝕵\",\"𝕶\",\"𝕷\",\"𝕸\",\"𝕹\",\"𝕺\",\"𝕻\",\"𝕼\",\"𝕽\",\"𝕾\",\"𝕿\",\"𝖀\",\"𝖁\",\"𝖂\",\"𝖃\",\"𝖄\",\"𝖅\"],\n        \"monospace-regular\": [\"𝟶\",\"𝟷\",\"𝟸\",\"𝟹\",\"𝟺\",\"𝟻\",\"𝟼\",\"𝟽\",\"𝟾\",\"𝟿\",\"𝚊\",\"𝚋\",\"𝚌\",\"𝚍\",\"𝚎\",\"𝚏\",\"𝚐\",\"𝚑\",\"𝚒\",\"𝚓\",\"𝚔\",\"𝚕\",\"𝚖\",\"𝚗\",\"𝚘\",\"𝚙\",\"𝚚\",\"𝚛\",\"𝚜\",\"𝚝\",\"𝚞\",\"𝚟\",\"𝚠\",\"𝚡\",\"𝚢\",\"𝚣\",\"𝙰\",\"𝙱\",\"𝙲\",\"𝙳\",\"𝙴\",\"𝙵\",\"𝙶\",\"𝙷\",\"𝙸\",\"𝙹\",\"𝙺\",\"𝙻\",\"𝙼\",\"𝙽\",\"𝙾\",\"𝙿\",\"𝚀\",\"𝚁\",\"𝚂\",\"𝚃\",\"𝚄\",\"𝚅\",\"𝚆\",\"𝚇\",\"𝚈\",\"𝚉\"],\n        \"double-struck-bold\": [\"𝟘\",\"𝟙\",\"𝟚\",\"𝟛\",\"𝟜\",\"𝟝\",\"𝟞\",\"𝟟\",\"𝟠\",\"𝟡\",\"𝕒\",\"𝕓\",\"𝕔\",\"𝕕\",\"𝕖\",\"𝕗\",\"𝕘\",\"𝕙\",\"𝕚\",\"𝕛\",\"𝕜\",\"𝕝\",\"𝕞\",\"𝕟\",\"𝕠\",\"𝕡\",\"𝕢\",\"𝕣\",\"𝕤\",\"𝕥\",\"𝕦\",\"𝕧\",\"𝕨\",\"𝕩\",\"𝕪\",\"𝕫\",\"𝔸\",\"𝔹\",\"ℂ\",\"𝔻\",\"𝔼\",\"𝔽\",\"𝔾\",\"ℍ\",\"𝕀\",\"𝕁\",\"𝕂\",\"𝕃\",\"𝕄\",\"ℕ\",\"𝕆\",\"ℙ\",\"ℚ\",\"ℝ\",\"𝕊\",\"𝕋\",\"𝕌\",\"𝕍\",\"𝕎\",\"𝕏\",\"𝕐\",\"ℤ\"],\n        \"circle-regular\": [\"⓪\",\"①\",\"②\",\"③\",\"④\",\"⑤\",\"⑥\",\"⑦\",\"⑧\",\"⑨\",\"ⓐ\",\"ⓑ\",\"ⓒ\",\"ⓓ\",\"ⓔ\",\"ⓕ\",\"ⓖ\",\"ⓗ\",\"ⓘ\",\"ⓙ\",\"ⓚ\",\"ⓛ\",\"ⓜ\",\"ⓝ\",\"ⓞ\",\"ⓟ\",\"ⓠ\",\"ⓡ\",\"ⓢ\",\"ⓣ\",\"ⓤ\",\"ⓥ\",\"ⓦ\",\"ⓧ\",\"ⓨ\",\"ⓩ\",\"Ⓐ\",\"Ⓑ\",\"Ⓒ\",\"Ⓓ\",\"Ⓔ\",\"Ⓕ\",\"Ⓖ\",\"Ⓗ\",\"Ⓘ\",\"Ⓙ\",\"Ⓚ\",\"Ⓛ\",\"Ⓜ\",\"Ⓝ\",\"Ⓞ\",\"Ⓟ\",\"Ⓠ\",\"Ⓡ\",\"Ⓢ\",\"Ⓣ\",\"Ⓤ\",\"Ⓥ\",\"Ⓦ\",\"Ⓧ\",\"Ⓨ\",\"Ⓩ\"],\n        \"square-regular\": [\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"🄰\",\"🄱\",\"🄲\",\"🄳\",\"🄴\",\"🄵\",\"🄶\",\"🄷\",\"🄸\",\"🄹\",\"🄺\",\"🄻\",\"🄼\",\"🄽\",\"🄾\",\"🄿\",\"🅀\",\"🅁\",\"🅂\",\"🅃\",\"🅄\",\"🅅\",\"🅆\",\"🅇\",\"🅈\",\"🅉\",\"🄰\",\"🄱\",\"🄲\",\"🄳\",\"🄴\",\"🄵\",\"🄶\",\"🄷\",\"🄸\",\"🄹\",\"🄺\",\"🄻\",\"🄼\",\"🄽\",\"🄾\",\"🄿\",\"🅀\",\"🅁\",\"🅂\",\"🅃\",\"🅄\",\"🅅\",\"🅆\",\"🅇\",\"🅈\",\"🅉\"],\n        \"modifier-letter\": [\"⁰\", \"¹\", \"²\", \"³\", \"⁴\", \"⁵\", \"⁶\", \"⁷\", \"⁸\", \"⁹\", \"ᵃ\", \"ᵇ\", \"ᶜ\", \"ᵈ\", \"ᵉ\", \"ᶠ\", \"ᵍ\", \"ʰ\", \"ⁱ\", \"ʲ\", \"ᵏ\", \"ˡ\", \"ᵐ\", \"ⁿ\", \"ᵒ\", \"ᵖ\", \"ᵠ\", \"ʳ\", \"ˢ\", \"ᵗ\", \"ᵘ\", \"ᵛ\", \"ʷ\", \"ˣ\", \"ʸ\", \"ᶻ\", \"ᴬ\", \"ᴮ\", \"ᶜ\", \"ᴰ\", \"ᴱ\", \"ᶠ\", \"ᴳ\", \"ʰ\", \"ᴵ\", \"ᴶ\", \"ᴷ\", \"ᴸ\", \"ᴹ\", \"ᴺ\", \"ᴼ\", \"ᴾ\", \"ᵠ\", \"ᴿ\", \"ˢ\", \"ᵀ\", \"ᵁ\", \"ᵛ\", \"ᵂ\", \"ˣ\", \"ʸ\", \"ᶻ\"],\n    };\n\n    // charCode => index in `TABLE`\n    const INDEX = { \"48\": 0, \"49\": 1, \"50\": 2, \"51\": 3, \"52\": 4, \"53\": 5, \"54\": 6, \"55\": 7, \"56\": 8, \"57\": 9, \"65\": 36, \"66\": 37, \"67\": 38, \"68\": 39, \"69\": 40, \"70\": 41, \"71\": 42, \"72\": 43, \"73\": 44, \"74\": 45, \"75\": 46, \"76\": 47, \"77\": 48, \"78\": 49, \"79\": 50, \"80\": 51, \"81\": 52, \"82\": 53, \"83\": 54, \"84\": 55, \"85\": 56, \"86\": 57, \"87\": 58, \"88\": 59, \"89\": 60, \"90\": 61, \"97\": 10, \"98\": 11, \"99\": 12, \"100\": 13, \"101\": 14, \"102\": 15, \"103\": 16, \"104\": 17, \"105\": 18, \"106\": 19, \"107\": 20, \"108\": 21, \"109\": 22, \"110\": 23, \"111\": 24, \"112\": 25, \"113\": 26, \"114\": 27, \"115\": 28, \"116\": 29, \"117\": 30, \"118\": 31, \"119\": 32, \"120\": 33, \"121\": 34, \"122\": 35 };\n\n    return proxies.map(p => {\n        p.name = [...p.name].map(c => {\n            if (/[a-zA-Z0-9]/.test(c)) {\n                const code = c.charCodeAt(0);\n                const index = INDEX[code];\n                if (isNumber(code) && num) {\n                    return TABLE[num][index];\n                } else {\n                    return TABLE[type][index];\n                }\n            }\n            return c;\n        }).join(\"\");\n        return p;\n    })\n}\n\nfunction isNumber(code) { return code >= 48 && code <= 57; }"
  },
  {
    "path": "scripts/ip-flag-node.js",
    "content": "const $ = $substore;\n\nconst {onlyFlagIP = true} = $arguments\n\nasync function operator(proxies) {\n    const BATCH_SIZE = 10;\n\n    let i = 0;\n    while (i < proxies.length) {\n        const batch = proxies.slice(i, i + BATCH_SIZE);\n        await Promise.all(batch.map(async proxy => {\n            if (onlyFlagIP && !ProxyUtils.isIP(proxy.server)) return;\n            try {\n                // remove the original flag\n                let proxyName = removeFlag(proxy.name);\n\n                // query ip-api\n                const countryCode = await queryIpApi(proxy);\n\n                proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;\n                proxy.name = proxyName;\n            } catch (err) {\n                // TODO:\n            }\n        }));\n\n        await sleep(1000);\n        i += BATCH_SIZE;\n    }\n    return proxies;\n}\n\n\nasync function queryIpApi(proxy) {\n    const ua = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0\";\n    const headers = {\n        \"User-Agent\": ua\n    };\n    const result = new Promise((resolve, reject) => {\n        const url =\n            `http://ip-api.com/json/${encodeURIComponent(proxy.server)}?lang=zh-CN`;\n        $.http.get({\n            url,\n            headers,\n        }).then(resp => {\n            const data = JSON.parse(resp.body);\n            if (data.status === \"success\") {\n                resolve(data.countryCode);\n            } else {\n                reject(new Error(data.message));\n            }\n        }).catch(err => {\n            console.log(err);\n            reject(err);\n        });\n    });\n    return result;\n}\n\nfunction getFlagEmoji(countryCode) {\n    const codePoints = countryCode\n        .toUpperCase()\n        .split('')\n        .map(char => 127397 + char.charCodeAt());\n    return String\n        .fromCodePoint(...codePoints)\n        .replace(/🇹🇼/g, '🇨🇳');\n}\n\nfunction removeFlag(str) {\n    return str\n        .replace(/[\\uD83C][\\uDDE6-\\uDDFF][\\uD83C][\\uDDE6-\\uDDFF]/g, '')\n        .trim();\n}\n\nfunction sleep(ms) {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n"
  },
  {
    "path": "scripts/ip-flag.js",
    "content": "const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';\nconst CACHE_EXPIRATION_TIME_MS = 10 * 60 * 1000;\nconst $ = $substore;\n\nclass ResourceCache {\n    constructor(expires) {\n        this.expires = expires;\n        if (!$.read(RESOURCE_CACHE_KEY)) {\n            $.write('{}', RESOURCE_CACHE_KEY);\n        }\n        this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));\n        this._cleanup();\n    }\n\n    _cleanup() {\n        // clear obsolete cached resource\n        let clear = false;\n        Object.entries(this.resourceCache).forEach((entry) => {\n            const [id, updated] = entry;\n            if (!updated.time) {\n                // clear old version cache\n                delete this.resourceCache[id];\n                $.delete(`#${id}`);\n                clear = true;\n            }\n            if (new Date().getTime() - updated.time > this.expires) {\n                delete this.resourceCache[id];\n                clear = true;\n            }\n        });\n        if (clear) this._persist();\n    }\n\n    revokeAll() {\n        this.resourceCache = {};\n        this._persist();\n    }\n\n    _persist() {\n        $.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);\n    }\n\n    get(id) {\n        const updated = this.resourceCache[id] && this.resourceCache[id].time;\n        if (updated && new Date().getTime() - updated <= this.expires) {\n            return this.resourceCache[id].data;\n        }\n        return null;\n    }\n\n    set(id, value) {\n        this.resourceCache[id] = { time: new Date().getTime(), data: value }\n        this._persist();\n    }\n}\n\nconst resourceCache = new ResourceCache(CACHE_EXPIRATION_TIME_MS);\n\nasync function operator(proxies) {\n    const { isLoon, isSurge } = $substore.env;\n    let support = false;\n    if (isLoon) {\n        support = true;\n    } else if (isSurge) {\n        const build = $environment['surge-build'];\n        if (build && parseInt(build) >= 2407) {\n            support = true;\n        }\n    }\n\n    if (support) {\n        const batches = [];\n        const BATCH_SIZE = 10;\n\n        let i = 0;\n        while (i < proxies.length) {\n            const batch = proxies.slice(i, i + BATCH_SIZE);\n            await Promise.all(batch.map(async proxy => {\n                try {\n                    // remove the original flag\n                    let proxyName = removeFlag(proxy.name);\n\n                    // query ip-api\n                    const countryCode = await queryIpApi(proxy);\n\n                    proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;\n                    proxy.name = proxyName;\n                } catch (err) {\n                    // TODO: \n                }\n            }));\n\n            await sleep(1000);\n            i += BATCH_SIZE;\n        }\n    } else {\n        $.error(`IP Flag only supports Loon and Surge!`);\n    }\n    return proxies;\n}\n\nconst tasks = new Map();\nasync function queryIpApi(proxy) {\n    const id = getId(proxy);\n    if (tasks.has(id)) {\n        return tasks.get(id);\n    }\n\n    const ua = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0\";\n    const headers = {\n        \"User-Agent\": ua\n    };\n    const { isLoon } = $substore.env;\n    const target = isLoon ? \"Loon\" : \"Surge\";\n    const result = new Promise((resolve, reject) => {\n        const cached = resourceCache.get(id);\n        if (cached) {\n            resolve(cached);\n        }\n        const url = `http://ip-api.com/json`;\n        let node = ProxyUtils.produce([proxy], target);\n\n        // Loon 需要去掉节点名字\n        if (isLoon) {\n            const s = node.indexOf(\"=\");\n            node = node.substring(s + 1);\n        }\n\n        $.http.get({\n            url,\n            headers,\n            node\n        }).then(resp => {\n            const body = resp.body;\n            const data = JSON.parse(body);\n            if (data.status === \"success\") {\n                resourceCache.set(id, data.countryCode);\n                resolve(data.countryCode);\n            } else {\n                reject(new Error(data.message));\n            }\n        }).catch(err => {\n            console.log(err);\n            reject(err);\n        });\n    });\n    tasks.set(id, result);\n    return result;\n}\n\nfunction getId(proxy) {\n    return MD5(`IP-FLAG-${proxy.server}-${proxy.port}`);\n}\n\nfunction getFlagEmoji(countryCode) {\n    const codePoints = countryCode\n        .toUpperCase()\n        .split('')\n        .map(char => 127397 + char.charCodeAt());\n    return String\n        .fromCodePoint(...codePoints)\n        .replace(/🇹🇼/g, '🇨🇳');\n}\n\nfunction removeFlag(str) {\n    return str\n        .replace(/[\\uD83C][\\uDDE6-\\uDDFF][\\uD83C][\\uDDE6-\\uDDFF]/g, '')\n        .trim();\n}\n\nfunction sleep(ms) {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nvar MD5 = function (d) { var r = M(V(Y(X(d), 8 * d.length))); return r.toLowerCase() }; function M(d) { for (var _, m = \"0123456789ABCDEF\", f = \"\", r = 0; r < d.length; r++)_ = d.charCodeAt(r), f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _); return f } function X(d) { for (var _ = Array(d.length >> 2), m = 0; m < _.length; m++)_[m] = 0; for (m = 0; m < 8 * d.length; m += 8)_[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; return _ } function V(d) { for (var _ = \"\", m = 0; m < 32 * d.length; m += 8)_ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); return _ } function Y(d, _) { d[_ >> 5] |= 128 << _ % 32, d[14 + (_ + 64 >>> 9 << 4)] = _; for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) { var h = m, t = f, g = r, e = i; f = md5_ii(f = md5_ii(f = md5_ii(f = md5_ii(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_ff(f = md5_ff(f = md5_ff(f = md5_ff(f, r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = safe_add(m, h), f = safe_add(f, t), r = safe_add(r, g), i = safe_add(i, e) } return Array(m, f, r, i) } function md5_cmn(d, _, m, f, r, i) { return safe_add(bit_rol(safe_add(safe_add(_, d), safe_add(f, i)), r), m) } function md5_ff(d, _, m, f, r, i, n) { return md5_cmn(_ & m | ~_ & f, d, _, r, i, n) } function md5_gg(d, _, m, f, r, i, n) { return md5_cmn(_ & f | m & ~f, d, _, r, i, n) } function md5_hh(d, _, m, f, r, i, n) { return md5_cmn(_ ^ m ^ f, d, _, r, i, n) } function md5_ii(d, _, m, f, r, i, n) { return md5_cmn(m ^ (_ | ~f), d, _, r, i, n) } function safe_add(d, _) { var m = (65535 & d) + (65535 & _); return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m } function bit_rol(d, _) { return d << _ | d >>> 32 - _ }"
  },
  {
    "path": "scripts/media-filter.js",
    "content": ""
  },
  {
    "path": "scripts/revert.js",
    "content": "const $ = API()\n$.write(\"{}\", \"#sub-store\")\n$.done()\n\nfunction ENV(){const e=\"function\"==typeof require&&\"undefined\"!=typeof $jsbox;return{isQX:\"undefined\"!=typeof $task,isLoon:\"undefined\"!=typeof $loon,isSurge:\"undefined\"!=typeof $httpClient&&\"undefined\"!=typeof $utils,isBrowser:\"undefined\"!=typeof document,isNode:\"function\"==typeof require&&!e,isJSBox:e,isRequest:\"undefined\"!=typeof $request,isScriptable:\"undefined\"!=typeof importModule}}function HTTP(e={baseURL:\"\"}){const{isQX:t,isLoon:s,isSurge:o,isScriptable:n,isNode:i,isBrowser:r}=ENV(),u=/https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)/;const a={};return[\"GET\",\"POST\",\"PUT\",\"DELETE\",\"HEAD\",\"OPTIONS\",\"PATCH\"].forEach(h=>a[h.toLowerCase()]=(a=>(function(a,h){h=\"string\"==typeof h?{url:h}:h;const d=e.baseURL;d&&!u.test(h.url||\"\")&&(h.url=d?d+h.url:h.url),h.body&&h.headers&&!h.headers[\"Content-Type\"]&&(h.headers[\"Content-Type\"]=\"application/x-www-form-urlencoded\");const l=(h={...e,...h}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...h.events};let f,p;if(c.onRequest(a,h),t)f=$task.fetch({method:a,...h});else if(s||o||i)f=new Promise((e,t)=>{(i?require(\"request\"):$httpClient)[a.toLowerCase()](h,(s,o,n)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:n})})});else if(n){const e=new Request(h.url);e.method=a,e.headers=h.headers,e.body=h.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}else r&&(f=new Promise((e,t)=>{fetch(h.url,{method:a,headers:h.headers,body:h.body}).then(e=>e.json()).then(t=>e({statusCode:t.status,headers:t.headers,body:t.data})).catch(t)}));const y=l?new Promise((e,t)=>{p=setTimeout(()=>(c.onTimeout(),t(`${a} URL: ${h.url} exceeds the timeout ${l} ms`)),l)}):null;return(y?Promise.race([y,f]).then(e=>(clearTimeout(p),e)):f).then(e=>c.onResponse(e))})(h,a))),a}function API(e=\"untitled\",t=!1){const{isQX:s,isLoon:o,isSurge:n,isNode:i,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(i){return{fs:require(\"fs\")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||\"{}\")),(o||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||\"{}\")),i){let e=\"root.json\";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:\"wx\"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:\"wx\"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(o||n)&&$persistentStore.write(e,this.name),i&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:\"w\"},e=>console.log(e)),this.node.fs.writeFileSync(\"root.json\",JSON.stringify(this.root,null,2),{flag:\"w\"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf(\"#\")){if(t=t.substr(1),n||o)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);i&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf(\"#\")?this.cache[e]:(e=e.substr(1),n||o?$persistentStore.read(e):s?$prefs.valueForKey(e):i?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf(\"#\")){if(e=e.substr(1),n||o)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);i&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t=\"\",a=\"\",h={}){const d=h[\"open-url\"],l=h[\"media-url\"];if(s&&$notify(e,t,a,h),n&&$notification.post(e,t,a+`${l?\"\\n多媒体:\"+l:\"\"}`,{url:d}),o){let s={};d&&(s.openUrl=d),l&&(s.mediaUrl=l),\"{}\"===JSON.stringify(s)?$notification.post(e,t,a):$notification.post(e,t,a,s)}if(i||u){const s=a+(d?`\\n点击跳转: ${d}`:\"\")+(l?`\\n多媒体: ${l}`:\"\");if(r){require(\"push\").schedule({title:e,body:(t?t+\"\\n\":\"\")+s})}else console.log(`${e}\\n${t}\\n${s}\\n\\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||n?$done(e):i&&!r&&\"undefined\"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if(\"string\"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return\"[object Object]\"}}}(e,t)}\n"
  },
  {
    "path": "scripts/tls-fingerprint.js",
    "content": "/**\n * 为节点添加 tls 证书指纹\n * 示例\n * #fingerprint=...\n */\nfunction operator(proxies) {\n    const { fingerprint } = $arguments;\n    proxies.forEach(proxy => {\n        proxy['tls-fingerprint'] = fingerprint;\n    });\n    return proxies;\n}"
  },
  {
    "path": "scripts/udp-filter.js",
    "content": "/**\n * 过滤 UDP 节点\n */\nfunction filter(proxies) {\n  return proxies.map(p => p.udp);\n}\n"
  },
  {
    "path": "scripts/vmess-ws-obfs-host.js",
    "content": "/**\n * 为 VMess WebSocket 节点修改混淆 host\n * 示例\n * #host=google.com\n */\nfunction operator(proxies) {\n    const { host } = $arguments;\n    proxies.forEach(p => {\n        if (p.type === 'vmess' && p.network === 'ws') {\n            p[\"ws-opts\"] = p[\"ws-opts\"] || {};\n            p[\"ws-opts\"][\"headers\"] = p[\"ws-opts\"][\"headers\"] || {};\n            p[\"ws-opts\"][\"headers\"][\"Host\"] = host;\n        }\n    });\n    return proxies;\n}"
  },
  {
    "path": "vs.code-workspace",
    "content": "{\n\t\"folders\": [\n\t\t{\n\t\t\t\"path\": \".\"\n\t\t}\n\t],\n\t\"settings\": {}\n}"
  }
]