commit e98e7803605fff142de40ef7e548a2339c1ceb89 Author: xmg0828888 Date: Sun May 3 11:34:48 2026 +0800 Import x-panel source diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9f3004d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: xeefei +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: xeefeiz +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..9d3c960 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,77 @@ +name: Bug report +description: Create a report to help us improve +title: "Bug report" +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Thank you for reporting a bug! Please fill out the following information. + + - type: textarea + id: what-happened + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: My problem is... + validations: + required: true + + - type: textarea + id: how-repeat-problem + attributes: + label: How to repeat the problem? + description: Sequence of actions that allow you to reproduce the bug + placeholder: | + 1. Open `Inbounds` page + 2. ... + validations: + required: true + + - type: textarea + id: expected-action + attributes: + label: Expected action + description: What's going to happen + placeholder: Must be... + validations: + required: false + + - type: textarea + id: received-action + attributes: + label: Received action + description: What's really happening + placeholder: It's actually happening... + validations: + required: false + + - type: input + id: xui-version + attributes: + label: x-panel Version + description: Which version of x-panel are you using? + placeholder: 2.X.X + validations: + required: true + + - type: input + id: xray-version + attributes: + label: Xray-core Version + description: Which version of Xray-core are you using? + placeholder: 2.X.X + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Please check all the checkboxes + options: + - label: This bug report is written entirely in English. + required: true + - label: This bug report is new and no one has reported it before me. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..7ffbad7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,39 @@ +name: Feature request +description: Suggest an idea for this project +title: "Feature request" +labels: ["enhancement"] + +body: + - type: textarea + id: is-related-problem + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of what the problem is. + placeholder: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Please check all the checkboxes + options: + - label: This feature report is written entirely in English. + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml new file mode 100644 index 0000000..0021f6b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -0,0 +1,22 @@ +name: Question +description: Describe this issue template's purpose here. +title: "Question" +labels: ["question"] + +body: + - type: textarea + id: question + attributes: + label: Question + placeholder: I have a question, ..., how can I solve it? + validations: + required: true + + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Please check all the checkboxes + options: + - label: This question is written entirely in English. + required: true diff --git a/.github/pull_request_template.yml b/.github/pull_request_template.yml new file mode 100644 index 0000000..c7eb8e7 --- /dev/null +++ b/.github/pull_request_template.yml @@ -0,0 +1,20 @@ +## What is the pull request? + + + +## Which part of the application is affected by the change? + +- [ ] Frontend +- [ ] Backend + +## Type of Changes + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactoring +- [ ] Other + +## Screenshots + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fa4eeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Ignore editor and IDE settings +.idea/ +.vscode/ +.cache/ +.sync* + +# Ignore log files +*.log + +# Ignore temporary files +tmp/ +*.tar.gz + +# Ignore build and distribution directories +backup/ +bin/ +dist/ +release/ +node_modules/ + +# Ignore compiled binaries +main + +# Ignore script and executable files +/release.sh +/x-ui + +# Ignore OS specific files +.DS_Store +Thumbs.db + +# Ignore Go build files +*.exe +x-ui.db + +# Ignore Docker specific files +docker-compose.override.yml + +# Ignore .env (Environment Variables) file +.env \ No newline at end of file diff --git a/DockerEntrypoint.sh b/DockerEntrypoint.sh new file mode 100644 index 0000000..7511d2e --- /dev/null +++ b/DockerEntrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Start fail2ban +[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start + +# Run x-ui +exec /app/x-ui diff --git a/DockerInit.sh b/DockerInit.sh new file mode 100755 index 0000000..100cea0 --- /dev/null +++ b/DockerInit.sh @@ -0,0 +1,40 @@ +#!/bin/sh +case $1 in + amd64) + ARCH="64" + FNAME="amd64" + ;; + i386) + ARCH="32" + FNAME="i386" + ;; + armv8 | arm64 | aarch64) + ARCH="arm64-v8a" + FNAME="arm64" + ;; + armv7 | arm | arm32) + ARCH="arm32-v7a" + FNAME="arm32" + ;; + armv6) + ARCH="arm32-v6" + FNAME="armv6" + ;; + *) + ARCH="64" + FNAME="amd64" + ;; +esac +mkdir -p build/bin +cd build/bin +curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.4.17/Xray-linux-${ARCH}.zip" +unzip "Xray-linux-${ARCH}.zip" +rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat +mv xray "xray-linux-${FNAME}" +wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat +wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat +wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat +wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat +wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat +wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat +cd ../../ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3e71f4b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# ======================================================== +# ----->> Stage: Builder +# ======================================================== +FROM golang:1.26-alpine AS builder +WORKDIR /app +ARG TARGETARCH + +RUN apk --no-cache --update add \ + build-base \ + gcc \ + wget \ + unzip + +COPY . . + +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" +RUN go build -ldflags "-w -s" -o build/x-ui main.go +RUN ./DockerInit.sh "$TARGETARCH" + +# ======================================================== +# ----->> Stage: Final Image of X-Panel +# ======================================================== +FROM alpine +ENV TZ=Asia/Tehran +WORKDIR /app + +RUN apk add --no-cache --update \ + ca-certificates \ + tzdata \ + fail2ban \ + bash + +COPY --from=builder /app/build/ /app/ +COPY --from=builder /app/DockerEntrypoint.sh /app/ +COPY --from=builder /app/x-ui.sh /usr/bin/x-ui + + +# Configure fail2ban +RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ + && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \ + && sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local \ + && sed -i "s/^\[sshd\]$/&\nenabled = false/" /etc/fail2ban/jail.local \ + && sed -i "s/#allowipv6 = auto/allowipv6 = auto/g" /etc/fail2ban/fail2ban.conf + +RUN chmod +x \ + /app/DockerEntrypoint.sh \ + /app/x-ui \ + /usr/bin/x-ui + +ENV XUI_ENABLE_FAIL2BAN="true" +VOLUME [ "/etc/x-ui" ] +CMD [ "./x-ui" ] +ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34fe9f0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed... + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.ar_EG.md b/README.ar_EG.md new file mode 100644 index 0000000..fd4e038 --- /dev/null +++ b/README.ar_EG.md @@ -0,0 +1,56 @@ +[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) + +

+ + + 3x-ui + +

+ +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/actions) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg?style=for-the-badge)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases/latest) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة. + +> [!IMPORTANT] +> هذا المشروع مخصص للاستخدام الشخصي والاتصال فقط، يرجى عدم استخدامه لأغراض غير قانونية، يرجى عدم استخدامه في بيئة الإنتاج. + +كمشروع محسن من مشروع X-UI الأصلي، يوفر 3X-UI استقرارًا محسنًا ودعمًا أوسع للبروتوكولات وميزات إضافية. + +## البدء السريع + +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` + +للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/xeefei/x-panel/wiki). + +## شكر خاص إلى + +- [alireza0](https://github.com/alireza0/) + +## الاعتراف + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (الترخيص: **GPL-3.0**): _قواعد توجيه v2ray/xray و v2ray/xray-clients المحسنة مع النطاقات الإيرانية المدمجة وتركيز على الأمان وحظر الإعلانات._ +- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (الترخيص: **GPL-3.0**): _يحتوي هذا المستودع على قواعد توجيه V2Ray محدثة تلقائيًا بناءً على بيانات النطاقات والعناوين المحظورة في روسيا._ + +## دعم المشروع + +**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2: + +

+ + Image + +

+ +- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` +- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` +- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` + +## النجوم عبر الزمن + +[![Stargazers over time](https://starchart.cc/xeefei/x-panel.svg?variant=adaptive)](https://starchart.cc/xeefei/x-panel) diff --git a/README.es_ES.md b/README.es_ES.md new file mode 100644 index 0000000..20a58b8 --- /dev/null +++ b/README.es_ES.md @@ -0,0 +1,502 @@ +[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md) + +

Image

+ +**Un Panel Web Avanzado • Construido sobre Xray Core** + +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg)](#) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg)](#) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +> **Descargo de responsabilidad:** Este proyecto es solo para aprendizaje personal y comunicación, por favor no lo uses con fines ilegales, por favor no lo uses en un entorno de producción + +**Si este proyecto te es útil, podrías considerar darle una**:star2: + +

+ + Image + +

+ +- Sponsorship Address (USDT): +- TRC20 ---->>> `TYQEmQp1P65u9bG7KPehgJdvuokfb72YkZ` +- Polygon ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` +- Solana ---->>> `7qVEZuV98QTDN5qUmsFwvqTSvkYpmLtNf8o1sh1mppwR` +- BSC/BEP20 ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` + +## Instalar y Actualizar + +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` + +## Instalar una Versión Personalizada + +Para instalar la versión deseada, agrega la versión al final del comando de instalación. Por ejemplo, ver `v2.4.1`: + +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) v2.4.1 +``` + +## Certificado SSL + +
+ Haz clic para el Certificado SSL + +### Cloudflare + +El script de gestión tiene una aplicación de certificado SSL incorporada para Cloudflare. Para usar este script para colocar un certificado, necesitas lo siguiente: + +- Correo electrónico registrado en Cloudflare +- Clave Global de API de Cloudflare +- El nombre de dominio se ha resuelto en el servidor actual a través de Cloudflare + +**1:** Ejecuta el comando`x-ui`en la terminal, luego elige `Certificado SSL de Cloudflare`. + + +### Certbot +``` +apt-get install certbot -y +certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com +certbot renew --dry-run +``` + +***Consejo:*** *Certbot también está integrado en el script de gestión. Puedes ejecutar el comando `x-ui` , luego elegir `Gestión de Certificados SSL`.* + +
+ +## Instalación y Actualización Manual + +
+ Haz clic para más detalles de la instalación manual + +#### Uso + +1. Para descargar la última versión del paquete comprimido directamente en tu servidor, ejecuta el siguiente comando: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + *) XUI_ARCH="amd64" ;; +esac + + +wget https://github.com/xeefei/x-panel/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz +``` + +2. Una vez que se haya descargado el paquete comprimido, ejecuta los siguientes comandos para instalar o actualizar x-ui: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + *) XUI_ARCH="amd64" ;; +esac + +cd /root/ +rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui +tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz +chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh +cp x-ui/x-ui.sh /usr/bin/x-ui +cp -f x-ui/x-ui.service /etc/systemd/system/ +mv x-ui/ /usr/local/ +systemctl daemon-reload +systemctl enable x-ui +systemctl restart x-ui +``` + +
+ +## Instalar con Docker + +
+ Haz clic para más detalles del Docker + +#### Uso + +1. Instala Docker: + + ```sh + bash <(curl -sSL https://get.docker.com) + ``` + +2. Clona el Repositorio del Proyecto: + + ```sh + git clone https://github.com/xeefei/x-panel.git + cd 3x-ui + ``` + +3. Inicia el Servicio + + ```sh + docker compose up -d + ``` + + O tambien + + ```sh + docker run -itd \ + -e XRAY_VMESS_AEAD_FORCED=false \ + -v $PWD/db/:/etc/x-ui/ \ + -v $PWD/cert/:/root/cert/ \ + --network=host \ + --restart=unless-stopped \ + --name 3x-ui \ + ghcr.io/xeefei/x-panel:latest + ``` + +actualizar a la última versión + + ```sh + cd 3x-ui + docker compose down + docker compose pull 3x-ui + docker compose up -d + ``` + +eliminar 3x-ui de docker + + ```sh + docker stop 3x-ui + docker rm 3x-ui + cd -- + rm -r 3x-ui + ``` + +
+ + +## SO Recomendados + +- Ubuntu 20.04+ +- Debian 11+ +- CentOS 8+ +- Fedora 36+ +- Arch Linux +- Manjaro +- Armbian +- AlmaLinux 9+ +- Rockylinux 9+ +- OpenSUSE Tubleweed + +## Arquitecturas y Dispositivos Compatibles + +
+ Haz clic para detalles de arquitecturas y dispositivos compatibles + +Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y dispositivos, garantizando flexibilidad en diversos entornos informáticos. A continuación se presentan las principales arquitecturas que admitimos: + +- **amd64**: Esta arquitectura predominante es la estándar para computadoras personales y servidores, y admite la mayoría de los sistemas operativos modernos sin problemas. + +- **x86 / i386**: Ampliamente adoptada en computadoras de escritorio y portátiles, esta arquitectura cuenta con un amplio soporte de numerosos sistemas operativos y aplicaciones, incluidos, entre otros, Windows, macOS y sistemas Linux. + +- **armv8 / arm64 / aarch64**: Diseñada para dispositivos móviles y embebidos contemporáneos, como teléfonos inteligentes y tabletas, esta arquitectura está ejemplificada por dispositivos como Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, entre otros. + +- **armv7 / arm / arm32**: Sirve como arquitectura para dispositivos móviles y embebidos más antiguos, y sigue siendo ampliamente utilizada en dispositivos como Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, entre otros. + +- **armv6 / arm / arm32**: Orientada a dispositivos embebidos muy antiguos, esta arquitectura, aunque menos común, todavía se utiliza. Dispositivos como Raspberry Pi 1, Raspberry Pi Zero/Zero W, dependen de esta arquitectura. + +- **armv5 / arm / arm32**: Una arquitectura más antigua asociada principalmente con sistemas embebidos tempranos, es menos común hoy en día pero aún puede encontrarse en dispositivos heredados como versiones antiguas de Raspberry Pi y algunos teléfonos inteligentes más antiguos. +
+ +## Idiomas + +- Inglés +- Farsi +- Chino +- Ruso +- Vietnamita +- Español +- Indonesio +- Ucraniano + + +## Características + +- Monitoreo del Estado del Sistema +- Búsqueda dentro de todas las reglas de entrada y clientes +- Tema Oscuro/Claro +- Soporta multiusuario y multiprotocolo +- Soporta protocolos, incluyendo VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard +- Soporta Protocolos nativos XTLS, incluyendo RPRX-Direct, Visión, REALITY +- Estadísticas de tráfico, límite de tráfico, límite de tiempo de vencimiento +- Plantillas de configuración de Xray personalizables +- Soporta acceso HTTPS al panel (dominio proporcionado por uno mismo + certificado SSL) +- Soporta la solicitud y renovación automática de certificados SSL con un clic +- Para elementos de configuración más avanzados, consulta el panel +- Corrige rutas de API (la configuración del usuario se creará con la API) +- Soporta cambiar las configuraciones por diferentes elementos proporcionados en el panel. +- Soporta exportar/importar base de datos desde el panel + + +## Configuración Predeterminada del Panel + +
+ Haz clic para ver los detalles de la configuración predeterminada + +### Nombre de Usuario & Contraseña & Ruta Base Web: + + Estos se generarán aleatoriamente si no los modificas. + + - **Puerto:** el puerto predeterminado para el panel es `2053` + +### Gestión de la Base de Datos: + + Puedes realizar copias de seguridad y restauraciones de la base de datos directamente desde el panel. + +### Ruta Base Web + - /etc/x-ui/x-ui.db +- **Ruta de Configuración de Xray:** + - /usr/local/x-ui/bin/config.json +- **Ruta del Panel Web sin Implementar SSL:** + - http://ip:2053/panel + - http://domain:2053/panel +- **Ruta del Panel Web con Implementación de SSL:** + - https://domain:2053/panel + +
+ +1. **Restablecer la Ruta Base Web:** + - Abre tu terminal. + - Ejecuta el comando `x-ui`. + - Selecciona la opción `Restablecer la Ruta Base Web`. + +2. **Generar o Personalizar la Ruta:** + - La ruta se generará aleatoriamente, o puedes ingresar una ruta personalizada. + +3. **Ver Configuración Actual:** + - Para ver tu configuración actual, utiliza el comando `x-ui settings` en el terminal o selecciona `Ver Configuración Actual` en `x-ui`. + +### Recomendación de Seguridad: +- Para mayor seguridad, utiliza una palabra larga y aleatoria en la estructura de tu URL. + +**Ejemplos:** +- `http://ip:port/*webbasepath*/panel` +- `http://domain:port/*webbasepath*/panel` + + + +## Configuración de WARP + +
+ Haz clic para ver los detalles de la configuración de WARP + +#### Uso + +**Para versiones `v2.1.0` y posteriores:** + +WARP está integrado, no se requiere instalación adicional. Simplemente habilita la configuración necesaria en el panel. + +
+ +## Límite de IP + +
+ Haz clic para ver los detalles del límite de IP + +#### Uso + +**Nota:** El Límite de IP no funcionará correctamente cuando uses Túnel IP. + +- **Para versiones hasta `v1.6.1`:** + - El límite de IP está integrado en el panel. + +**Para versiones `v1.7.0` y posteriores:** + +Para habilitar la funcionalidad de límite de IP, necesitas instalar `fail2ban` y los archivos requeridos siguiendo estos pasos: + +1. Ejecuta el comando `x-ui` en el terminal, luego elige `Gestión de Límite de IP`. +2. Verás las siguientes opciones: + + - **Cambiar la Duración del Bloqueo:** Ajustar la duración de los bloqueos. + - **Desbloquear a Todos:** Levantar todos los bloqueos actuales. + - **Revisar los Registros:** Revisar los registros. + - **Estado de Fail2ban:** Verificar el estado de `fail2ban`. + - **Reiniciar Fail2ban:** Reiniciar el servicio `fail2ban`. + - **Desinstalar Fail2ban:** Desinstalar Fail2ban con la configuración. + +3. Agrega una ruta para el registro de acceso en el panel configurando `Xray Configs/log/Access log` a `./access.log`, luego guarda y reinicia Xray. + +- **Para versiones anteriores a `v2.1.3`:** + - Necesitas configurar manualmente la ruta del registro de acceso en tu configuración de Xray: + + ```sh + "log": { + "access": "./access.log", + "dnsLog": false, + "loglevel": "warning" + }, + ``` + +- **Para versiones `v2.1.3` y posteriores:** + - Hay una opción para configurar `access.log` directamente desde el panel. + +
+ +## Bot de Telegram + +
+ Haz clic para más detalles del bot de Telegram + +#### Uso + +El panel web admite tráfico diario, inicio de sesión en el panel, copia de seguridad de la base de datos, estado del sistema, información del cliente y otras notificaciones y funciones a través del Bot de Telegram. Para usar el bot, debes establecer los parámetros relacionados con el bot en el panel, que incluyen: + +- Token de Telegram +- ID de chat de administrador(es) +- Hora de Notificación (en sintaxis cron) +- Notificación de Fecha de Caducidad +- Notificación de Capacidad de Tráfico +- Copia de seguridad de la base de datos +- Notificación de Carga de CPU + + +**Sintaxis de referencia:** + +- `30 \* \* \* \* \*` - Notifica a los 30s de cada punto +- `0 \*/10 \* \* \* \*` - Notifica en el primer segundo de cada 10 minutos +- `@hourly` - Notificación por hora +- `@daily` - Notificación diaria (00:00 de la mañana) +- `@weekly` - Notificación semanal +- `@every 8h` - Notifica cada 8 horas + +### Funcionalidades del Bot de Telegram + +- Reporte periódico +- Notificación de inicio de sesión +- Notificación de umbral de CPU +- Umbral de Notificación para Fecha de Caducidad y Tráfico para informar con anticipación +- Soporte para menú de reporte de cliente si el nombre de usuario de Telegram del cliente se agrega a las configuraciones de usuario +- Soporte para reporte de tráfico de Telegram buscado con UUID (VMESS/VLESS) o Contraseña (TROJAN) - anónimamente +- Bot basado en menú +- Buscar cliente por correo electrónico (solo administrador) +- Ver todas las Entradas +- Ver estado del servidor +- Ver clientes agotados +- Recibir copia de seguridad bajo demanda y en informes periódicos +- Bot multilingüe + +### Configuración del Bot de Telegram + +- Inicia [Botfather](https://t.me/BotFather) en tu cuenta de Telegram: + ![Botfather](./media/botfather.png) + +- Crea un nuevo bot usando el comando /newbot: Te hará 2 preguntas, Un nombre y un nombre de usuario para tu bot. Ten en cuenta que el nombre de usuario debe terminar con la palabra "bot". + ![Create new bot](./media/newbot.png) + +- Inicia el bot que acabas de crear. Puedes encontrar el enlace a tu bot aquí. + ![token](./media/token.png) + +- Ingresa a tu panel y configura los ajustes del bot de Telegram como se muestra a continuación: +![Panel Config](./media/panel-bot-config.png) + +Ingresa el token de tu bot en el campo de entrada número 3. +Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas de Telegram con esta ID serán los administradores del bot. (Puedes ingresar más de uno, solo sepáralos con ,) + +- ¿Cómo obtener el ID de chat de Telegram? Usa este [bot](https://t.me/useridinfobot), Inicia el bot y te dará el ID de chat del usuario de Telegram. +![User ID](./media/user-id.png) + +
+ +## Rutas de API + +
+ Haz clic para más detalles de las rutas de API + +#### Uso + +- `/login` con `POST` datos de usuario: `{username: '', password: ''}` para iniciar sesión +- `/panel/api/inbounds` base para las siguientes acciones: + +| Método | Ruta | Acción | +| :----: | ---------------------------------- | --------------------------------------------------------- | +| `GET` | `"/list"` | Obtener todas los Entradas | +| `GET` | `"/get/:id"` | Obtener Entrada con inbound.id | +| `GET` | `"/getClientTraffics/:email"` | Obtener Tráficos del Cliente con email | +| `GET` | `"/createbackup"` | El bot de Telegram envía copia de seguridad a los admins | +| `POST` | `"/add"` | Agregar Entrada | +| `POST` | `"/del/:id"` | Eliminar Entrada | +| `POST` | `"/update/:id"` | Actualizar Entrada | +| `POST` | `"/clientIps/:email"` | Dirección IP del Cliente | +| `POST` | `"/clearClientIps/:email"` | Borrar Dirección IP del Cliente | +| `POST` | `"/addClient"` | Agregar Cliente a la Entrada | +| `POST` | `"/:id/delClient/:clientId"` | Eliminar Cliente por clientId\* | +| `POST` | `"/updateClient/:clientId"` | Actualizar Cliente por clientId\* | +| `POST` | `"/:id/resetClientTraffic/:email"` | Restablecer Tráfico del Cliente | +| `POST` | `"/resetAllTraffics"` | Restablecer tráfico de todos las Entradas | +| `POST` | `"/resetAllClientTraffics/:id"` | Restablecer tráfico de todos los clientes en una Entrada | +| `POST` | `"/delDepletedClients/:id"` | Eliminar clientes agotados de la entrada (-1: todos) | +| `POST` | `"/onlines"` | Obtener usuarios en línea (lista de correos electrónicos) | + +\*- El campo `clientId` debe llenarse por: + +- `client.id` para VMESS y VLESS +- `client.password` para TROJAN +- `client.email` para Shadowsocks + + +- [Documentación de API](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm) +- [Run In Postman](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9) +
+ +## Variables de Entorno + +
+ Haz clic para más detalles de las variables de entorno + +#### Uso + +| Variable | Tipo | Predeterminado | +| -------------- | :--------------------------------------------: | :------------- | +| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` | +| XUI_DEBUG | `boolean` | `false` | +| XUI_BIN_FOLDER | `string` | `"bin"` | +| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` | +| XUI_LOG_FOLDER | `string` | `"/var/log"` | + +Ejemplo: + +```sh +XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go +``` + +
+ +## Vista previa + +![1](./media/1.png) +![2](./media/2.png) +![3](./media/3.png) +![4](./media/4.png) +![5](./media/5.png) +![6](./media/6.png) +![7](./media/7.png) + +## Un agradecimiento especial a + +- [alireza0](https://github.com/alireza0/) + +## Reconocimientos + +赞助地址(USDT/TRC20):TYQEmQp1P65u9bG7KPehgJdvuokfb72YkZ + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas de v2ray/xray y v2ray/xray-clients con dominios iraníes integrados y un enfoque en seguridad y bloqueo de anuncios._ +- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _Un dominio alojado en Vietnam y una lista de bloqueo con la máxima eficiencia para vietnamitas._ + +## Estrellas a lo largo del tiempo + +[![Stargazers over time](https://starchart.cc/xeefei/x-panel.svg)](https://starchart.cc/xeefei/x-panel) diff --git a/README.fa_IR.md b/README.fa_IR.md new file mode 100644 index 0000000..a3150d7 --- /dev/null +++ b/README.fa_IR.md @@ -0,0 +1,52 @@ +

Image

+ +**---------------------------------------一个更好的面板 • 基于Xray Core构建------------------------------** + + +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/actions) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg?style=for-the-badge)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases/latest) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکل‌های مختلف VPN و پراکسی ارائه می‌دهد. + +> [!IMPORTANT] +> این پروژه فقط برای استفاده شخصی و ارتباطات است، لطفاً از آن برای اهداف غیرقانونی استفاده نکنید، لطفاً از آن در محیط تولید استفاده نکنید. + +به عنوان یک نسخه بهبود یافته از پروژه اصلی X-UI، 3X-UI پایداری بهتر، پشتیبانی گسترده‌تر از پروتکل‌ها و ویژگی‌های اضافی را ارائه می‌دهد. + +## شروع سریع + +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` + +برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/xeefei/x-panel/wiki) مراجعه کنید. + +## تشکر ویژه از + +- [alireza0](https://github.com/alireza0/) + +## قدردانی + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**): _قوانین مسیریابی بهبود یافته v2ray/xray و v2ray/xray-clients با دامنه‌های ایرانی داخلی و تمرکز بر امنیت و مسدود کردن تبلیغات._ +- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**): _این مخزن شامل قوانین مسیریابی V2Ray به‌روزرسانی شده خودکار بر اساس داده‌های دامنه‌ها و آدرس‌های مسدود شده در روسیه است._ + +## پشتیبانی از پروژه + +**اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید + +

+ + Image + +

+ +- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` +- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` +- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` + +## ستاره‌ها در طول زمان + +[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) diff --git a/README.md b/README.md new file mode 100644 index 0000000..54b4a4c --- /dev/null +++ b/README.md @@ -0,0 +1,938 @@ +

Image

+ +**---------------------------------------一个更好的面板 • 基于Xray Core构建------------------------------** + + +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/actions) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg?style=for-the-badge)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases/latest) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +> **声明:** 此项目仅供个人学习、交流使用,请遵守当地法律法规,勿用于非法用途;请勿用于生产环境。 + +> **注意:** 在使用此项目和〔教程〕过程中,若因违反以上声明使用规则而产生的一切后果由使用者自负。 + +**如果此项目对你有用,请给一个**:star2: + +

+ + Image + +

+ +- “支持项目”赞助地址(USDT): +- TRC20 ---->>> `TYQEmQp1P65u9bG7KPehgJdvuokfb72YkZ` +- Polygon ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` +- Solana ---->>> `7qVEZuV98QTDN5qUmsFwvqTSvkYpmLtNf8o1sh1mppwR` +- BSC/BEP20 ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` + +## [【X-Panel面板】交流群:https://t.me/XUI_CN](https://t.me/XUI_CN) +## [【X-Panel面板】详细安装流程步骤:https://xeefei.blogspot.com/2025/09/x-panel.html](https://xeefei.blogspot.com/2025/09/x-panel.html) + + +------------ +## ✰怎么购买【授权码】使用〔X-Panel-Pro版 面板〕?✰ +#### 1、【授权码】100/15U一个,一机一码,一直有效,永久使用,包括:重装,后期的升级/更新,都能使用,但是不能用于不同的机器,所以推荐稳定使用的机器用【授权码】,注:“授权码”属虚拟商品,购买之后,一经激活生效,概不退款, +#### 2、对于一年期限(年付/年抛)的机器,后期可以【换绑】,为什么要有时间限制?就是为了防止,有些人拿【授权码】滥用, +#### 3、购买方式/渠道联系机器人:[https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) + +#### 4、经常换的机器,去使用“免费基础版”就行,目前【安装界面】是有“两种方式”可选择的, + +#### 5、对于有业务需求,比如做 TK 的,做〔跨境电商〕的,如果很多机器,比如:20台,50台,或者 100台 以上机器的宝子,若都要用【收费Pro版】的话,可以用【批量授权】:5 ——> 20台,8折,尾号:5555 ,20 ——> 50台,7折,尾号:66666 ,50 ——> 100台,6折,尾号:777777 ,100 ——> 200台,5折,尾号:8888888,200台 ——> 以上,4折,尾号:99999999,以上列举出来的,就是【批量折扣】的统一“优惠”。注:〔批量授权码〕要求最低5台以上,是“一码通用”,一个“授权码”,可以绑定验证多台 VPS 机器,并且有专属的“豹子号”授权码。 +![63](./media/63.png) +———————————————————————————————— + +#### 6、若您需要购买【授权码】,请跟下面这个“机器人”去对话, + +#### ———————————————- + +#### [https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) +#### (授权码购买机器人) + +#### ———————————————- + +#### 输入:/start 或 购买,即可“在线下单”, + +#### 7、弹出【购买页面】,选择或输入“数量”,付款了支付系统会收到回调,金额到账之后,就会通过那个机器人【发放授权码】给您,整个流程,是【全自助】的“自动处理”方式,若您是〔增加配额〕,基本也是一样的流程, + +#### 8、请注意:机器人发给您的所有信息,尽量都去自己【耐心阅读】一遍,“使用说明”,以及〔VIP 群〕的信息,也全部都包含在里面的, + +#### 9、按照之前您安装更新〔X-Panel 面板〕的方式,直接重新输入【安装命令】,选择【2】,就能去,把之前的【免费版】,“无缝升级”到最新的【Pro版】。 + +———————————————————————————————— +#### 10、不能重复用于不同的机器 VPS,后期视情况不定时会上涨价格;对于〔X-Panel 面板〕后期的“新功能”,都将在【付费Pro版】中进行更新, + +#### 11、目前的【安装界面】,是有:两种可选,“免费基础版”一样可用,只是后期不再提供技术支持和重大更新,另外,在【免费基础版】中,【一键配置】功能将不再可用,全部放到了【付费Pro版】中, + +#### 12、后期的开发精力,全部会放到【付费Pro版】中,免费基础版不删库,持续保留,会大幅降低更新频率,后期只会同步更新 Xray 那边的【内核版本】等基础,想继续用的不影响,只是没有【新功能】可用,翻墙也足够。 + +![62](./media/62.png) + +------------ +## ✰〔X-Panel-Pro版 面板〕已实现的功能✰ +#### 1、新增 -【付费Pro版】的面板后台UI,添加醒目的“X-Panel-Pro”标识, +#### 2、优化 -【付费Pro版】TG端 【版本更新】提示功能,增加详细的“更新说明”, +#### 3、增加〔Pro版面板后台〕,使用 Reality 协议时,可点击随机更换所偷的域名, +#### 4、新增 -【付费Pro版】TG端 的【发送授权报告】,增加“唯一授权防伪码”, +#### 5、优化 -【付费Pro版】安装脚本界面,增加【Pro版】该有的“明确标识”, +#### 6、优化 -【付费Pro版】TG端的显示方式,增加该有的“会员标识”, +#### 7、新增 -【付费Pro版】安装脚本,有“网页版SSH工具”可选部署,脚本中〔第26选项〕, +#### 8、新增 - 【付费Pro版】安装脚本,有“线路和IP质量检测”可去使用,脚本中〔第27选项〕, +#### 9、新增 - 【付费Pro版】安装脚本,有“地区服务器DNS检测”可去使用,脚本中〔第28选项〕, +#### 10、新增 -【付费Pro版】---->>>TG端,同步有“网页版SSH工具”可选安装, +#### 11、优化 - 【付费Pro版】---->>>TG端,点击“服务器状态”时的“版本号显示”, +#### 12、说明 - 【付费Pro版】TG端中,使用命令:/webssh,安装“网页版SSH”, +#### 13、优化 -〔Pro版〕中的〔一键配置〕功能,有更友好的提示方式, +#### 14、新增 -【付费Pro版】---->>>面板后台的【首页 UI】,目前是有“5种”可选:标准布局 (默认),炫彩动画,深海科技,暮光薰衣,和幽林秘境;你喜欢什么类型的主题,就去点击“选定”之后,就不会自动变了,若后期需要更换,就重选, +#### 15、新增 -【付费Pro版】---->>>在“创建入站”时,可以在页面上更加方便地选择【重置流量】的方式:有每日重置,每周重置,按月重置,或从不重置, +#### 16、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“每日报告”,可定制【发送内容】,自己可点击“打开或关闭”,并且可以选择【发送时间】,可按天,或者每周,每月发, +#### 17、优化 -【付费Pro版】的“授权码验证机制”,增加【后台联网验证】,以及“机器指纹”等属性, +#### 18、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“多面板管理”,一个机器人可同时管理其他面板,可以很丝滑地远程操作【被控端 VPS】, +#### 19、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“获取节点链接”功能,支持【本机】和【远程被控端 VPS】都能获取,开发此功能的目的在于:不用进〔面板后台〕,就能在 TG端 获取到之前已经创建过的“链接”, +#### 20、新增 -〔X-Panel 面板〕----->>【付费Pro版】“一键部署中转节点”,解决了不懂配置的麻烦,已实现:远程Socks5创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”,“小手一点”,直接可用, +#### 21、新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”,有“备用方式申请证书”,当用常规方式【1】申请不下来时,可以试试“备用方式”, +#### 22、新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”,有“可自定义证书路径”,自己进入 VPS 中“手动上传证书”,复制路径,在脚本中填入即可, +#### 23、新增 -〔X-Panel 面板〕----->>【付费Pro版】“深度调优脚本”,包含 BBR+FQ, TCP Fast Open, 内存缓冲区及队列优化,在〔Pro版〕脚本中“第29选项”可直接用, +#### 24、新增 -〔X-Panel 面板〕----->>【付费Pro版】“首页会员等级”显示,能够明确展示:自己的会员等级,授权码信息,以及“版本更新”提示, +#### 25、新增 -〔X-Panel 面板〕----->>【付费Pro版】“节点上/下线TG通知”功能,对于【拼车】的宝子,能明确知道:哪个节点,什么时候上线?或者下线时间,做到“心中有数”, +#### 26、新增 -〔X-Panel 面板〕----->>【付费Pro版】“签到得积分”功能,后期针对有【积分】的宝子,会不断推出:相应的【特权】和【福利待遇】, +#### 27、新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出:积分查询,积分换购,授权码查询,修改用户名,积分转移/打赏,以及“积分排行榜”, +#### 28、新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出【“积分换购”的可用功能】:A、消耗1000积分“自助重置换绑时间”,B、消耗5000积分“自助换购一个普通授权码”, +#### 29、新增 -〔X-Panel 面板〕----->>【付费Pro版】“创建数据快照 + 远程急救还原”的提示功能: 可直观通过「终端UI界面」看到具体「备份 + 还原」步骤;用法:面板报错“崩了”,不用像之前那样:卸载面板 -->> 重装面板,更不用很麻烦去“重装系统”解决,直接:远程急救还原,前提就是:你自己要知道,在面板“正常运行”的时候,去「备份数据快照」, +#### 30、新增 -〔X-Panel 面板〕----->>【付费Pro版】“每月重置流量”功能:可输入1—31之间的任意数字,比如:输入12,即代表“每月12号”「重置入站流量」,以便提供更友好的“重置流量方式”, +#### 31、新增 -〔X-Panel 面板〕----->>【付费Pro版】“批量部署节点”功能:可直接在面板后台的“一键配置”中去使用,点击一次可批量部署生成10条「VLESS + TCP + Reality + Vision」协议组合的入站, +#### 32、新增 -〔X-Panel 面板〕----->>【付费Pro版】“购买机器人”功能,可自助全自动在“机器人”中:购买授权码,增加配额,充值积分,自助重置换绑等,联系:[https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) 。 +![76](./media/76.png) + +------------ +## ✰你必须要看的【重要安全提示/警告】✰ +#### 1、请勿使用【http明文模式】登录管理面板,因为明文会造成信息泄露;这个安全问题社区讨论过, +#### 2、可使用设置【SSH端口转发功能】去登录面板或安装证书之后用https加密方式登录;两种方式选择其一, +![30](./media/30.png) +#### 3、若无域名那就按照脚本提示去做【ssh转发】;有域名则可选择更加安全的【申请安装证书】方式, +#### 4、Windows电脑首先通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务, +![36](./media/36.png) +![31](./media/31.png) +![32](./media/32.png) +![33](./media/33.png) +#### 5、若在搭建之前没有翻墙加密,则【http明文模式】登录面板有很大的信息泄露安全风险,那建议你第一次搭建成功之后,去修改用户/密码,和访问路径,后期则通过搭建好的代理加密访问, +![34](./media/34.png) +#### 6、在做【ssh转发】过程中,本地电脑的终端不能关闭,保持打开不能断开;且每一次要登录〔X-Panel面板〕管理后台都要做【ssh转发】,因为关闭之后就失效了。 +![35](./media/35.png) +#### PS:上述两种方法:【ssh端口转发】或申请安装证书的目的都是为了更安全地登录面板,而至于搭建的其他流程和步骤,都是一样的;如果你已经【申请安装证书】了,并不会受到其他什么额外影响,就不用去折腾【ssh转发】了,直接用 【https://你的域名:端口/路径】 去登录你的面板管理后台就行了。 + +------------ +## 如何在〔X-Panel-Pro 面板〕中去使用【一个机器人管理多面板】功能? +#### 1、先购买【授权码】安装好〔X-Panel-Pro 面板〕,进入后台,在【主从管理】界面, +![64](./media/64.png) +#### 2、点击【添加被控端 VPS】,按照要求填入面板登录地址,用户名和密码,备注等信息, +![65](./media/65.png) +#### 3、然后点击【配置本机“主控机器人”】,跳转到【机器人设置页面】,输入token令牌配置好, +![66](./media/66.png) +![73](./media/73.png) +#### 4、现在重启本机 VPS,打开TG端机器人,即可看到【切换控制 VPS】等菜单,切换操作即可使用。 +![67](./media/67.png) +![68](./media/68.png) + +------------ +## 如何在〔X-Panel-Pro 面板〕中去使用【一键部署中转节点】的功能? +#### 1、先购买【授权码】安装好〔X-Panel-Pro 面板〕,进入后台,在【主从管理】界面, +#### 2、点击【添加中转机 VPS】,按照要求填入面板登录地址,用户名和密码,备注等信息, +![69](./media/69.png) +#### 3、然后点击列表页中的【一键部署中转】,即可在后续的流程中,自动进入配置流程, +![70](./media/70.png) +#### 4、稍等片刻,本机配置好的话就几秒,全自动给您生成了【二维码和中转链接】,复制可用, +![71](./media/71.png) +#### 5、记得要去放行【本机】和【远程中转机】对应的端口,也可点击【检测中转节点】功能去看是否通? +![72](./media/72.png) + +------------ +## ✰如何从其他x-ui版本迁移到〔X-Panel面板〕?✰ +#### 1、若你用的是伊朗老哥的3X-UI,是可以直接〔覆盖安装〕的,因为数据库文件等位置是没有改变的,所以直接覆盖安装,并不会影响你〔原有节点及配置〕等数据;安装命令如下: +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` +#### 2、若你之前用的是Docker方式安装,那先进入容器里面/命令:docker exec -it 容器id /bin/sh,再执行以上脚本命令直接【覆盖安装】即可, +#### 3、若你用的是之前F佬的x-ui或者其他分支版本,那直接覆盖安装的话,并不能确保一定就能够兼容?建议你先去备份〔数据库〕配置文件,再进行安装〔X-Panel面板〕。 + + +------------ +## 安装之前的准备 +- 购买一台性能还不错的VPS,可通过本页底部链接购买, +- PS:若你不想升级系统,则可以跳过此步骤。 +- 若你需要更新/升级系统,Debian系统可用如下命令: + ``` + apt update + apt upgrade -y + apt dist-upgrade -y + apt autoclean + apt autoremove -y + ``` +- 查看系统当前版本: + ``` + cat /etc/debian_version + ``` +- 查看内核版本: + ``` + uname -r + ``` +- 列出所有内核: + ``` + dpkg --list | grep linux-image + ``` +- 更新完成后执行重新引导: + ``` + update-grub + ``` +- 完成以上步骤之后输入reboot重启系统 + +------------ +## 【搬瓦工】重装/升级系统之后SSH连不上如何解决? +- 【搬瓦工】重装/升级系统会恢复默认22端口,如果需要修改SSH的端口号,您需要进行以下步骤: +- 以管理员身份使用默认22端口登录到SSH服务器 +- 打开SSH服务器的配置文件进行编辑,SSH配置文件通常位于/etc/ssh/sshd_config +- 找到"Port"选项,并将其更改为您想要的端口号 +- Port <新端口号>,请将<新端口号>替换为您想要使用的端口号 +- 保存文件并退出编辑器 +- 重启服务器以使更改生效 + +------------ +## 安装 & 升级 +- 使用〔X-Panel面板〕脚本一般情况下,安装完成创建入站之后,端口是默认关闭的,所以必须进入脚本选择【22】去放行端口 +- 要使用【自动续签】证书功能,也必须放行【80】端口,保持80端口是打开的,才会每3个月自动续签一次 + +- 【全新安装】请执行以下脚本: +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` +#### 如果执行了上面的代码但是报错,证明你的系统里面没有curl这个软件,请执行以下命令先安装curl软件,安装curl之后再去执行上面代码, +``` +apt update -y&&apt install -y curl&&apt install -y socat +``` + +- 若要对版本进行升级,可直接通过脚本选择【2】,如下图: +![8](./media/8.png) +![10](./media/10.png) +- 在到这一步必须要注意:要保留旧设置的话,需要输入【n】 +![11](./media/11.png) + + +## 安装指定版本 + +若要安装指定的版本,请使用以下安装命令。 e.g., ver `v26.2.15`: + +``` +VERSION=v26.2.15 && bash <(curl -Ls "https://raw.githubusercontent.com/xeefei/x-panel/$VERSION/install.sh") $VERSION +``` +------------ +## 若你的VPS默认有防火墙,请在安装完成之后放行指定端口 +- 放行【面板登录端口】 +- 放行出入站管理协议端口 +- 如果要申请安装证书并每3个月【自动续签】证书,请确保80和443端口是放行打开的 +- 可通过此脚本的第【21】选项去安装防火墙进行管理,如下图: +![9](./media/9.png) +- 若要一次性放行多个端口或一整个段的端口,用英文逗号隔开。 +#### PS:若你的VPS没有防火墙,则所有端口都是能够ping通的,可自行选择是否进入脚本安装防火墙保证安全,但安装了防火墙必须放行相应端口。 + +------------ +## 如何在〔X-Panel面板〕中使用简单快捷的【一键配置】生成功能? +#### 1、进入后台,并且你已经【安装了证书】,可在【添加入站】处看到, +![53](./media/53.png) +#### 2、点击【一键配置】,在弹出的页面中【按需选择】去生成协议配置即可, +![54](./media/54.png) +#### 3、直接复制【链接】,导入软件;若后台出现【醒目提示】,那就手动放行【相应端口】, +![55](./media/55.png) +#### 4、若你是用【TG端电报机器人】的【一键配置】生成功能,那直接点击就用, +![56](./media/56.png) +#### 5、选择好自己想要【一键创建】的协议组合类型,点击之后稍作等待, +![57](./media/57.png) +#### 6、TG端【一键配置】创建成功之后,二维码和链接地址机器人会发送给你,如下: +![58](./media/58.png) + +------------ +## 如何在〔X-Panel 项目〕中进行【抽奖游戏】赢奖品? +#### 1、必须绑定好【TG端机器人】,怎么绑定?去看下面“绑定机器人”那部分教程, +#### 2、在【TG端】直接点击【娱乐抽奖】菜单,就会弹出【每日幸运】抽奖游戏, +![59](./media/59.png) +#### 3、点击【进行抽奖】,就可“全凭手气”得到随机的抽奖结果,如下图所示, +![60](./media/60.png) +#### 4、在你【中奖】之后,截完整的“中奖页面”图片给交流群内管理员,即可兑奖。 +![61](./media/61.png) + +------------ +## 安装证书开启https方式实现域名登录访问管理面板/----->>偷自己 +#### PS:如果不需要以上功能或无域名,可以跳过这步;建议申请证书, +##### 1、把自己的域名托管到CF,并解析到自己VPS的IP,不要开启【小云朵】, +##### 2、如果要申请安装证书并每3个月【自动续签】证书,请确保80和443端口是放行打开的, +##### 3、输入x-ui命令进入面板管理脚本,通过选择第【18】选项去进行安装,若使用 18 ---> 1方式不能申请下来,那就用 18--->5 备用方式去申请,或者也可用【19选项】去申请, +![74](./media/74.png) +##### 4、首先输入解析好的【域名】进行验证,然后可以默认用【80端口】,直接回车申请, +![27](./media/27.png) +##### 5、进入后台【面板设置】—–>【常规】中,会看到脚本已经自动填好了证书公钥、私钥路径, +##### 6、点击左上角的【保存】和【重启面板】,即可用自己域名进行登录管理;也可按照后续方法实现【自己偷自己】。 + +------------ +## 登录面板进行【常规】设置 +### 特别是如果在安装过程中,全部都是默认【回车键】安装的话,用户名/密码/访问路径是随机的,而面板监听端口默认是:13688,可自行进入面板更改, +##### 1、填写自己想要设置的【面板监听端口】,并去登录SSH放行, +##### 2、更改自己想要设置的【面板登录访问路径】,后续加上路径登录访问, +![25](./media/25.png) +##### 3、其他:安全设定和电报机器人等配置,可自行根据需求去进行设置, +##### 4、强烈建议配置电报机器人,使用【TG端】可方便远程接管 VPS 服务器, +![26](./media/26.png) +##### 5、面板设置【改动保存】之后,都需要点击左上角【重启面板】,才能生效。 +#### PS:若你在正确完成了上述步骤之后,你没有安装证书的情况下,去用【ssh转发】的方式却不能访问面板,那请检查一下是不是你的浏览器自动默认开启了https模式,需要手动调整一下改成http方式,把“s”去掉,即可访问成功;或查看一下是不是对应的端口被占用? + +------------ +## 创建【入站协议】和添加【客户端】,并测试上网 +##### 1、点击左边【入站列表】,然后【添加入站】,传输方式保持【TCP】不变,尽量选择主流的vless+reality+vision协议组合, +![23](./media/23.png) +##### 2、在选择reality安全选项时,偷的域名可以使用默认的,要使用其他的,请替换尽量保持一致就行,比如Apple、Yahoo,VPS所在地区的旅游、学校网站等;如果要实现【偷自己】,请参看后续【如何偷自己】的说明部分;而私钥/公钥部分,可以直接点击下方的【Get New Cert】获取一个随机的, +##### 3、在创建reality安全选项过程中,至于其他诸如:PROXY Protocol,HTTP 伪装,TPROXY,External Proxy等等选项,若无特殊要求,保持默认设置即可,不用去动它们, +![24](./media/24.png) +##### 4、创建好入站协议之后,默认只有一个客户端,可根据自己需求继续添加;重点:并编辑客户端,选择【Flow流控】为xtls-rprx-vision, +![19](./media/19.png) +##### 5、其他:流量限制,到期时间,客户TG的ID等选项根据自己需求填写, +![4](./media/4.png) +##### 6、一定要放行端口之后,确保端口能够ping通,再导入软件, +##### 7、点击二维码或者复制链接导入到v2rayN等软件中进行测试。 + +------------ +## 备份与恢复/迁移数据库(以Debian系统为例) +#### 一、备份:通过配置好电报管理机器人,可点击管理机器人的“相应菜单按钮”获取【备份配置】文件,有x-ui.db和config.json两个文件,可自行下载保存到自己电脑里面, +![14](./media/14.png) +#### 二、搭建:在新的VPS中全新安装好〔X-Panel面板〕,通过脚本放行之前配置的所有端口,一次性放行多个端口请用【英文逗号】分隔, +#### 三、若需要安装证书,则提前把域名解析到新的VPS对应的IP,并且去输入x-ui选择第【18】选项去安装,并记录公钥/私钥的路径,无域名则跳过这一步, +#### 四、恢复:SSH登录服务器找到/etc/x-ui/x-ui.db和/usr/local/x-ui/bin/config.json文件位置,上传之前的两个备份文件,进行覆盖, +![12](./media/12.png) +##### PS:把之前通过自动备份下载得到的两个文件上传覆盖掉旧文件,重启〔X-Panel面板〕即可【迁移成功】;即使迁移过程中出现问题,你是有备份文件的,不用担心,多试几次。 +![13](./media/13.png) +#### 五、若安装了证书,去核对/更改一下证书的路径,一般是同一个域名的话,位置在:/root/cert/域名/fullchain.pem,路径是相同的就不用更改, +#### 六、重启面板/重启服务器,让上述步骤生效即可,这时可以看到所有配置都是之前自己常用的,包括面板用户名、密码,入站、客户端,电报机器人配置等。 +#### PS:若您使用的是【Pro版】,则可直接使用:“创建数据快照 + 远程急救还原”功能,对面板数据库和配置文件进行操作。 + +------------ +## 安装完成后如何设置调整成【中文界面】? +- 方法一:通过管理后台【登录页面】调整,登录时可以选择,如下图: +![15](./media/15.png) +- 方法二:通过在管理后台-->【面板设置】中去选择设置,如下图: +![16](./media/16.png) +- 【TG机器人】设置中文:通过在管理后台-->【面板设置】-->【机器人配置】中去选择设置,并建议打开数据库备份和登录通知,如下图: +![17](./media/17.png) + +------------ +## 用〔X-Panel面板〕如何实现【自己偷自己】? +- 其实很简单,只要你为面板设置了证书, +- 开启了HTTPS登录,就可以将〔X-Panel面板〕自身作为Web Server, +- 无需Nginx等,这里给一个示例: +- 其中目标网站(Dest)请填写面板监听端口, +- 可选域名(SNI)填写面板登录域名, +- 如果您使用其他web server(如nginx)等, +- 将目标网站改为对应监听端口也可。 +- 需要说明的是,如果您处于白名单地区,自己“偷”自己并不适合你; +- 其次,可选域名一项实际上可以填写任意SNI,只要客户端保持一致即可,不过并不推荐这样做。 +- 配置方法如下图所示: +![18](./media/18.png) + +------------ +## 〔子域名〕被墙针对特征 +#### 网络表现: +##### 1、可以Ping通域名和IP地址, +##### 2、子域名无法打开〔X-Panel面板〕管理界面, +##### 3、什么都正常就是不能上网; + +#### 问题: +##### 你的子域名被墙针对了:无法上网! + +#### 解决方案: +##### 1、更换为新的子域名, +##### 2、解析新的子域名到VPS的IP, +##### 3、重新去安装新证书, +##### 4、重启〔X-Panel面板〕和服务器, +##### 5、重新去获取链接并测试上网。 +#### PS:若通过以上步骤还是不能正常上网,则重装VPS服务器OS系统,以及〔X-Panel面板〕全部重新安装,之后就正常了! + +------------ +## 用〔X-Panel面板〕如何开启【设备限制】功能? +##### 1、进入后台在【添加入站】的时候,弹出来的页面就能有【设备数量】输入框, +![37](./media/37.png) +##### 2、通过步骤1设置完成后,在后台的【入站列表】页面也有对应的同步数据显示。 +![38](./media/38.png) +##### 3、具体要查看【设备限制】功能的封禁情况,就进入〔X-Panel面板〕后台用日志查看。 +![39](./media/39.png) +##### 4、以下图片里面,详细阐述了我们的〔设备限制〕功能,跟3X-UI原本就有的〔IP Limit〕之间的区别对比。 +![40](./media/40.png) + +------------ +## 用〔X-Panel面板〕如何开启【独立限速】功能? +##### 1、进入后台在【添加入站】的时候,弹出来的页面在【客户/用户】那里就能输入具体数字, +![47](./media/47.png) +##### 2、也可以在添加好一个【入站】之后,点击【添加客户端】去找到【独立限速】输入框, +![48](./media/48.png) +##### 3、当你想批量一次性创建多个【客户/用户】时,同样可以使用【独立限速】功能, +![49](./media/49.png) +![50](./media/50.png) +##### 4、若后期你想更改某一个【客户端/用户】的限制速率,那就自己先找到,然后点击【编辑】即可, +![51](./media/51.png) +##### 5、具体要查看【独立限速】功能的启用情况,就进入〔X-Panel面板〕后台用日志查看。 +![52](./media/52.png) + +------------ +## 用〔X-Panel面板〕如何开启【每月流量自动重置】? +##### 1、进入后台的【入站列表】,选择需要设置的【客户端】, +![29](./media/29.png) +##### 2、要注意是编辑【入站】下面的【客户端】,才会有效果, +##### 2、并不是编辑【入站】,所以不要弄错对象,如下图所示: +![28](./media/28.png) + + +------------ +## 在自己的VPS服务器部署【订阅转换】功能 +### 如何把vless/vmess等协议转换成Clash/Surge等软件支持的格式? +##### 1、进入脚本输入x-ui命令调取面板,选择第【25】选项安装订阅转换模块, +##### 2、等待安装【订阅转换】成功之后,访问地址:https://你的域名:15268 , +![41](./media/41.png) +##### 3、因为在转换过程中需要调取后端API,所以请确保端口 8000 和 15268 是打开放行的, +##### 4、直接复制脚本中提供的【登录地址】,进入后台,点击【节点列表】,如下图: +![42](./media/42.png) +##### 5、接下来点击左边侧边栏的【订阅列表】去【添加订阅】,按照下图中去操作, +![43](./media/43.png) +##### 6、最后一步,点击【客户端】,即可导入Clash等软件中使用。 +![44](./media/44.png) +![45](./media/45.png) + +------------ +## 常见的翻墙软件/工具: +- [1、Windows系统v2rayN:https://github.com/2dust/v2rayN](https://github.com/2dust/v2rayN) +- [2、安卓手机版【v2rayNG】:https://github.com/2dust/v2rayNG](https://github.com/2dust/v2rayNG) +- [3、苹果手机IOS【小火箭】:https://apple02.com/(自己购买)](https://apple02.com/) +- [4、苹果MacOS电脑【Clash Verge】:https://github.com/clash-verge-rev/clash-verge-rev/releases](https://github.com/clash-verge-rev/clash-verge-rev/releases) + [或v2rayN:https://github.com/2dust/v2rayN](https://github.com/2dust/v2rayN) + +------------ +## 如何保护自己的IP不被墙被封? +##### 1、使用的代理协议要安全,加密是必备,推荐使用vless+reality+vision协议组合, +##### 2、因为有时节点会共享,在不同的地区,多个省份之间不要共同连接同一个IP, +##### 3、连接同一个IP就算了,不要同一个端口,不要同IP+同端口到处漫游,要分开, +##### 4、同一台VPS,不要在一天内一直大流量去下载东西使用,不要流量过高要切换, +##### 5、创建【入站协议】的时候,尽量用【高位端口】,比如40000--65000之间的端口号。 +#### 提醒:为什么在特殊时期,比如:两会,春节等被封得最严重最惨? +##### 尼玛同一个IP+同一个端口号,多个省份去漫游,跟开飞机场一样!不封你,封谁的IP和端口? +#### 总结:不要多终端/多省份/多个朋友/共同使用同一个IP和端口号!使用〔X-Panel面板〕多创建几个【入站】, +#### 多做几条备用,各用各的!各行其道才比较安全!GFW的思维模式是干掉机场,机场的特征个人用户不要去沾染,自然IP就保护好了。 + +------------ +## SSL 认证 + +
+ 点击查看 SSL 认证 + +### ACME + +要使用 ACME 管理 SSL 证书: + +1. 确保您的域名已正确解析到服务器, +2. 输入“x-ui”命令并选择“SSL 证书管理”, +3. 您将看到以下选项: + + - **获取证书** ----获取SSL证书 + - **吊销证书** ----吊销现有的SSL证书 + - **续签证书** ----强制续签SSL证书 + - **显示所有证书** ----显示服务器中所有能用的证书 + - **设置面板证书路径** ----指定面板要使用的证书 + + +### Certbot + +安装和使用 Certbot: + +```sh +apt-get install certbot -y +certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com +certbot renew --dry-run +``` + +### Cloudflare + +管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件: + +- Cloudflare 邮箱地址 +- Cloudflare Global API Key +- 域名已通过 cloudflare 解析到当前服务器 + +**如何获取 Cloudflare全局API密钥:** + +1. 在终端中输入“x-ui”命令,然后选择“CF SSL 证书”。 +2. 访问链接: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens). +3. 点击“查看全局 API 密钥”(如下图所示): + ![](media/APIKey1.PNG) +4. 您可能需要重新验证您的帐户。之后,将显示 API 密钥(请参见下面的屏幕截图): + ![](media/APIKey2.png) + +使用时,只需输入您的“域名”、“电子邮件”和“API KEY”即可。示意图如下: + ![](media/DetailEnter.png) + + +
+ +------------ +## 手动安装 & 升级 + +
+ 点击查看 手动安装 & 升级 + +#### 使用 + +1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + s390x) echo 's390x' ;; + *) XUI_ARCH="amd64" ;; +esac + + +wget https://github.com/xeefei/x-panel/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz +``` + +2. 下载压缩包后,执行以下命令安装或升级 x-ui: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + s390x) echo 's390x' ;; + *) XUI_ARCH="amd64" ;; +esac + +cd /root/ +rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui +tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz +chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh +cp x-ui/x-ui.sh /usr/bin/x-ui +cp -f x-ui/x-ui.service /etc/systemd/system/ +mv x-ui/ /usr/local/ +systemctl daemon-reload +systemctl enable x-ui +systemctl restart x-ui +``` + +
+ +------------ +## 通过Docker安装 + +
+ 点击查看 通过Docker安装 + +#### 使用 + + +1. **安装Docker** + + ```sh + bash <(curl -sSL https://get.docker.com) + ``` + + +2. **克隆项目仓库** + + ```sh + git clone https://github.com/xeefei/x-panel.git + cd x-panel + ``` + +3. **启动服务**: + + ```sh + docker compose up -d + ``` + + 添加 ```--pull always``` 标志使 docker 在拉取更新的镜像时自动重新创建容器。有关更多信息,请参阅:https://docs.docker.com/reference/cli/docker/container/run/#pull + + **或** + + ```sh + docker run -itd \ + -e XRAY_VMESS_AEAD_FORCED=false \ + -v $PWD/db/:/etc/x-ui/ \ + -v $PWD/cert/:/root/cert/ \ + --network=host \ + --restart=unless-stopped \ + --name x-panel \ + ghcr.io/xeefei/x-panel:latest + ``` + +4. **更新至最新版本** + + ```sh + cd x-panel + docker compose down + docker compose pull x-panel + docker compose up -d + ``` + +5. **从Docker中删除x-panel ** + + ```sh + docker stop x-panel + docker rm x-panel + cd -- + rm -r x-panel + ``` + +
+ +------------ +## 建议使用的操作系统 + +- Ubuntu 20.04+ +- Debian 11+ +- CentOS 8+ +- OpenEuler 22.03+ +- Fedora 36+ +- Arch Linux +- Manjaro +- Armbian +- AlmaLinux 8.0+ +- Rocky Linux 8+ +- Oracle Linux 8+ +- OpenSUSE Tubleweed +- Amazon Linux 2023 + +------------ +## 支持的架构和设备 +
+ 点击查看 支持的架构和设备 + +我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构: + +- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。 + +- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。 + +- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。 + +- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构,它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。 + +- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备,虽然不太普遍,但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。 + +- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。 +
+ +------------ +## Languages + +- English(英语) +- Farsi(伊朗语) +- Simplified Chinese(简体中文) +- Traditional Chinese(繁体中文) +- Russian(俄语) +- Vietnamese(越南语) +- Spanish(西班牙语) +- Indonesian (印度尼西亚语) +- Ukrainian(乌克兰语) +- Turkish(土耳其语) +- Português (葡萄牙语) + +------------ +## 项目特点 + +- 系统状态查看与监控 +- 可搜索所有入站和客户端信息 +- 深色/浅色主题随意切换 +- 支持多用户和多协议 +- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard +- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY +- 流量统计、流量限制、过期时间限制 +- 可自定义的 Xray配置模板 +- 支持HTTPS访问面板(自备域名+SSL证书) +- 支持一键式SSL证书申请和自动续签证书 +- 更多高级配置项目请参考面板去进行设定 +- 修复了 API 路由(用户设置将使用 API 创建) +- 支持通过面板中提供的不同项目更改配置。 +- 支持从面板导出/导入数据库 + +## 默认面板设置 + +
+ + 点击查看 默认设置 + + ### 默认信息 + +- **端口** + - 13688 +- **用户名 & 密码 & 访问路径** + - 当您跳过设置时,这些信息会随机生成, + - 您也可以在安装的时候自定义访问路径。 +- **数据库文件路径:** + - /etc/x-ui/x-ui.db +- **Xray 配置文件路径:** + - /usr/local/x-ui/bin/config.json +- **面板链接(有SSL):** + - https://你的域名:13688/访问路径/panel + +
+ +------------ +## [WARP 配置](https://gitlab.com/fscarmen/warp) + +
+ 点击查看 WARP 配置 + +#### 使用 + +**对于版本 `v2.1.0` 及更高版本:** + +WARP 是内置的,无需额外安装;只需在面板中打开必要的配置即可。 + +**如果要在 v2.1.0 之前使用 WARP 路由**,请按照以下步骤操作: + +**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap + + - **Account Type (free, plus, team):** Choose the appropriate account type. + - **Enable/Disable WireProxy:** Toggle WireProxy on or off. + - **Uninstall WARP:** Remove the WARP application. + +**2.** 如果您已经安装了 warp,您可以使用以下命令卸载: + + ```sh + warp u + ``` + +**3.** 在面板中打开您需要的配置 + + 配置: + + - Block Ads + - Route Google, Netflix, Spotify, and OpenAI (ChatGPT) traffic to WARP + - Fix Google 403 error + + +
+ +------------ +## IP 限制 + +
+ 点击查看 IP 限制 + +#### 使用 + +**注意:** 使用 IP 隧道时,IP 限制无法正常工作。 + +- 对于 `v1.6.1`之前的版本 : + + - IP 限制 已被集成在面板中。 + +- 对于 `v1.7.0` 以及更新的版本: + + - 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件: + + 1. 使用面板内置的 `x-ui` 指令 + 2. 选择 `IP Limit Management`. + 3. 根据您的需要选择合适的选项。 + + - 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。 + + ```sh + "log": { + "access": "./access.log", + "dnsLog": false, + "loglevel": "warning" + }, + ``` + - 您需要在Xray配置中手动设置〔访问日志〕的路径。 + +
+ +------------ +## Telegram 机器人 + +
+ 点击查看 Telegram 机器人 + +#### 使用 + +Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括: + +- 电报令牌 +- 管理员聊天 ID +- 通知时间(cron 语法) +- 到期日期通知 +- 流量上限通知 +- 数据库备份 +- CPU 负载通知 + + +**参考:** + +- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知 +- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知 +- `@hourly` - 每小时通知 +- `@daily` - 每天通知 (00:00) +- `@weekly` - 每周通知 +- `@every 8h` - 每8小时通知 + +### Telegram Bot 功能 + +- 定期报告 +- 登录通知 +- CPU 阈值通知 +- 提前报告的过期时间和流量阈值 +- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单 +- 支持使用UUID(VMESS/VLESS)或密码(TROJAN)搜索报文流量报告 - 匿名 +- 基于菜单的机器人 +- 通过电子邮件搜索客户端(仅限管理员) +- 检查所有入库 +- 检查服务器状态 +- 检查耗尽的用户 +- 根据请求和定期报告接收备份 +- 多语言机器人 + +### 注册 Telegram bot + +- 与 [Botfather](https://t.me/BotFather) 对话: + ![Botfather](./media/botfather.png) + +- 使用 /newbot 创建新机器人:你需要提供机器人名称以及用户名,注意名称中末尾要包含“bot” + ![创建机器人](./media/newbot.png) + +- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。 + ![令牌](./media/token.png) + +- 输入您的面板并配置 Telegram 机器人设置,如下所示: + ![面板设置](./media/panel-bot-config.png) + +在输入字段编号 3 中输入机器人令牌。 +在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可) + +- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot), 启动机器人,它会给你 Telegram 用户 ID。 +![用户 ID](./media/user-id.png) + +
+ +------------ +## API 路由 + +
+ 点击查看 API 路由 + +#### 使用 + +- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录 +- `/panel/api/inbounds` 以下操作的基础: + +| 方法 | 路径 | 操作 | +| :----: | ---------------------------------- | ------------------------------------------- | +| `GET` | `"/list"` | 获取所有入站 | +| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id | +| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 | +| `GET` | `"/getClientTrafficsById/:id"` | 通过用户ID获取客户端流量 | +| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 | +| `POST` | `"/add"` | 添加入站 | +| `POST` | `"/del/:id"` | 删除入站 | +| `POST` | `"/update/:id"` | 更新入站 | +| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 | +| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 | +| `POST` | `"/addClient"` | 将客户端添加到入站 | +| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 | +| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 | +| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 | +| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 | +| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 | +| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) | +| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) | + +- 使用`clientId` 项应该填写下列数据: + +- `client.id` for VMESS and VLESS +- `client.password` for TROJAN +- `client.email` for Shadowsocks + + + +- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm) + +- [Run In Postman](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9) +
+ +------------ +## 环境变量 + +
+ 点击查看 环境变量 + +#### Usage + +| 变量 | Type | 默认 | +| -------------- | :--------------------------------------------: | :------------ | +| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` | +| XUI_DEBUG | `boolean` | `false` | +| XUI_BIN_FOLDER | `string` | `"bin"` | +| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` | +| XUI_LOG_FOLDER | `string` | `"/var/log"` | + +例子: + +```sh +XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go +``` + +
+ +------------ +## 预览 + +![1](./media/1.png) +![2](./media/2.png) +![3](./media/3.png) +![5](./media/5.png) +![6](./media/6.png) +![7](./media/7.png) + +------------ +## 广告赞助 +- 如果你觉得本项目对你有用,而且你也恰巧有这方面的需求,你也可以选择通过我的购买链接赞助我。 +- [1、搬瓦工GIA高端线路,仅推荐购买GIA套餐:https://bandwagonhost.com/aff.php?aff=75015](https://bandwagonhost.com/aff.php?aff=75015) +- [2、Dmit高端GIA线路:https://www.dmit.io/aff.php?aff=9326](https://www.dmit.io/aff.php?aff=9326) +- [3、Gomami亚太顶尖优化线路:https://gomami.io/aff.php?aff=174](https://gomami.io/aff.php?aff=174) +- [4、ISIF优质亚太优化线路:https://cloud.isif.net/login?affiliation_code=333](https://cloud.isif.net/login?affiliation_code=333) +- [5、ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选:https://my.zorocloud.com/aff.php?aff=1072](https://my.zorocloud.com/aff.php?aff=1072) +- [6、三网直连 IEPL / IPLC 直播流量转发:https://idc333.top/#register/BCUZXNELNO](https://idc333.top/#register/BCUZXNELNO) +- [7、Bagevm优质落地鸡(原生IP全解锁):https://www.bagevm.com/aff.php?aff=754](https://www.bagevm.com/aff.php?aff=754) +- [8、白丝云【4837线路】实惠量大管饱:https://cloudsilk.io/aff.php?aff=706](https://cloudsilk.io/aff.php?aff=706) +- [9、RackNerd极致性价比机器:https://my.racknerd.com/aff.php?aff=15268&pid=912](https://my.racknerd.com/aff.php?aff=15268&pid=912) + +------------ +## 特别感谢 + +- [MHSanaei](https://github.com/MHSanaei/) +- [alireza0](https://github.com/alireza0/) +- [FranzKafkaYu](https://github.com/FranzKafkaYu/) +- [vaxilu](https://github.com/vaxilu/) + +------------ +## 致谢 + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._ +- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ + +------------ +## Star 趋势 + +[![Stargazers over time](https://starchart.cc/xeefei/x-panel.svg)](https://starchart.cc/xeefei/x-panel) diff --git a/README.ru_RU.md b/README.ru_RU.md new file mode 100644 index 0000000..bad79c1 --- /dev/null +++ b/README.ru_RU.md @@ -0,0 +1,56 @@ +[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) + +

+ + + 3x-ui + +

+ +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/actions) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg?style=for-the-badge)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases/latest) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов. + +> [!IMPORTANT] +> Этот проект предназначен только для личного использования, пожалуйста, не используйте его в незаконных целях и в производственной среде. + +Как улучшенная версия оригинального проекта X-UI, 3X-UI обеспечивает повышенную стабильность, более широкую поддержку протоколов и дополнительные функции. + +## Быстрый старт + +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` + +Полную документацию смотрите в [вики проекта](https://github.com/xeefei/x-panel/wiki). + +## Особая благодарность + +- [alireza0](https://github.com/alireza0/) + +## Благодарности + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Лицензия: **GPL-3.0**): _Улучшенные правила маршрутизации для v2ray/xray и v2ray/xray-clients со встроенными иранскими доменами и фокусом на безопасность и блокировку рекламы._ +- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Лицензия: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._ + +## Поддержка проекта + +**Если этот проект полезен для вас, вы можете поставить ему**:star2: + +

+ + Image + +

+ +- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` +- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` +- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` + +## Звезды с течением времени + +[![Stargazers over time](https://starchart.cc/xeefei/x-panel.svg?variant=adaptive)](https://starchart.cc/xeefei/x-panel) diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..54b4a4c --- /dev/null +++ b/README.zh.md @@ -0,0 +1,938 @@ +

Image

+ +**---------------------------------------一个更好的面板 • 基于Xray Core构建------------------------------** + + +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/actions) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg?style=for-the-badge)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases/latest) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +> **声明:** 此项目仅供个人学习、交流使用,请遵守当地法律法规,勿用于非法用途;请勿用于生产环境。 + +> **注意:** 在使用此项目和〔教程〕过程中,若因违反以上声明使用规则而产生的一切后果由使用者自负。 + +**如果此项目对你有用,请给一个**:star2: + +

+ + Image + +

+ +- “支持项目”赞助地址(USDT): +- TRC20 ---->>> `TYQEmQp1P65u9bG7KPehgJdvuokfb72YkZ` +- Polygon ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` +- Solana ---->>> `7qVEZuV98QTDN5qUmsFwvqTSvkYpmLtNf8o1sh1mppwR` +- BSC/BEP20 ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` + +## [【X-Panel面板】交流群:https://t.me/XUI_CN](https://t.me/XUI_CN) +## [【X-Panel面板】详细安装流程步骤:https://xeefei.blogspot.com/2025/09/x-panel.html](https://xeefei.blogspot.com/2025/09/x-panel.html) + + +------------ +## ✰怎么购买【授权码】使用〔X-Panel-Pro版 面板〕?✰ +#### 1、【授权码】100/15U一个,一机一码,一直有效,永久使用,包括:重装,后期的升级/更新,都能使用,但是不能用于不同的机器,所以推荐稳定使用的机器用【授权码】,注:“授权码”属虚拟商品,购买之后,一经激活生效,概不退款, +#### 2、对于一年期限(年付/年抛)的机器,后期可以【换绑】,为什么要有时间限制?就是为了防止,有些人拿【授权码】滥用, +#### 3、购买方式/渠道联系机器人:[https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) + +#### 4、经常换的机器,去使用“免费基础版”就行,目前【安装界面】是有“两种方式”可选择的, + +#### 5、对于有业务需求,比如做 TK 的,做〔跨境电商〕的,如果很多机器,比如:20台,50台,或者 100台 以上机器的宝子,若都要用【收费Pro版】的话,可以用【批量授权】:5 ——> 20台,8折,尾号:5555 ,20 ——> 50台,7折,尾号:66666 ,50 ——> 100台,6折,尾号:777777 ,100 ——> 200台,5折,尾号:8888888,200台 ——> 以上,4折,尾号:99999999,以上列举出来的,就是【批量折扣】的统一“优惠”。注:〔批量授权码〕要求最低5台以上,是“一码通用”,一个“授权码”,可以绑定验证多台 VPS 机器,并且有专属的“豹子号”授权码。 +![63](./media/63.png) +———————————————————————————————— + +#### 6、若您需要购买【授权码】,请跟下面这个“机器人”去对话, + +#### ———————————————- + +#### [https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) +#### (授权码购买机器人) + +#### ———————————————- + +#### 输入:/start 或 购买,即可“在线下单”, + +#### 7、弹出【购买页面】,选择或输入“数量”,付款了支付系统会收到回调,金额到账之后,就会通过那个机器人【发放授权码】给您,整个流程,是【全自助】的“自动处理”方式,若您是〔增加配额〕,基本也是一样的流程, + +#### 8、请注意:机器人发给您的所有信息,尽量都去自己【耐心阅读】一遍,“使用说明”,以及〔VIP 群〕的信息,也全部都包含在里面的, + +#### 9、按照之前您安装更新〔X-Panel 面板〕的方式,直接重新输入【安装命令】,选择【2】,就能去,把之前的【免费版】,“无缝升级”到最新的【Pro版】。 + +———————————————————————————————— +#### 10、不能重复用于不同的机器 VPS,后期视情况不定时会上涨价格;对于〔X-Panel 面板〕后期的“新功能”,都将在【付费Pro版】中进行更新, + +#### 11、目前的【安装界面】,是有:两种可选,“免费基础版”一样可用,只是后期不再提供技术支持和重大更新,另外,在【免费基础版】中,【一键配置】功能将不再可用,全部放到了【付费Pro版】中, + +#### 12、后期的开发精力,全部会放到【付费Pro版】中,免费基础版不删库,持续保留,会大幅降低更新频率,后期只会同步更新 Xray 那边的【内核版本】等基础,想继续用的不影响,只是没有【新功能】可用,翻墙也足够。 + +![62](./media/62.png) + +------------ +## ✰〔X-Panel-Pro版 面板〕已实现的功能✰ +#### 1、新增 -【付费Pro版】的面板后台UI,添加醒目的“X-Panel-Pro”标识, +#### 2、优化 -【付费Pro版】TG端 【版本更新】提示功能,增加详细的“更新说明”, +#### 3、增加〔Pro版面板后台〕,使用 Reality 协议时,可点击随机更换所偷的域名, +#### 4、新增 -【付费Pro版】TG端 的【发送授权报告】,增加“唯一授权防伪码”, +#### 5、优化 -【付费Pro版】安装脚本界面,增加【Pro版】该有的“明确标识”, +#### 6、优化 -【付费Pro版】TG端的显示方式,增加该有的“会员标识”, +#### 7、新增 -【付费Pro版】安装脚本,有“网页版SSH工具”可选部署,脚本中〔第26选项〕, +#### 8、新增 - 【付费Pro版】安装脚本,有“线路和IP质量检测”可去使用,脚本中〔第27选项〕, +#### 9、新增 - 【付费Pro版】安装脚本,有“地区服务器DNS检测”可去使用,脚本中〔第28选项〕, +#### 10、新增 -【付费Pro版】---->>>TG端,同步有“网页版SSH工具”可选安装, +#### 11、优化 - 【付费Pro版】---->>>TG端,点击“服务器状态”时的“版本号显示”, +#### 12、说明 - 【付费Pro版】TG端中,使用命令:/webssh,安装“网页版SSH”, +#### 13、优化 -〔Pro版〕中的〔一键配置〕功能,有更友好的提示方式, +#### 14、新增 -【付费Pro版】---->>>面板后台的【首页 UI】,目前是有“5种”可选:标准布局 (默认),炫彩动画,深海科技,暮光薰衣,和幽林秘境;你喜欢什么类型的主题,就去点击“选定”之后,就不会自动变了,若后期需要更换,就重选, +#### 15、新增 -【付费Pro版】---->>>在“创建入站”时,可以在页面上更加方便地选择【重置流量】的方式:有每日重置,每周重置,按月重置,或从不重置, +#### 16、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“每日报告”,可定制【发送内容】,自己可点击“打开或关闭”,并且可以选择【发送时间】,可按天,或者每周,每月发, +#### 17、优化 -【付费Pro版】的“授权码验证机制”,增加【后台联网验证】,以及“机器指纹”等属性, +#### 18、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“多面板管理”,一个机器人可同时管理其他面板,可以很丝滑地远程操作【被控端 VPS】, +#### 19、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“获取节点链接”功能,支持【本机】和【远程被控端 VPS】都能获取,开发此功能的目的在于:不用进〔面板后台〕,就能在 TG端 获取到之前已经创建过的“链接”, +#### 20、新增 -〔X-Panel 面板〕----->>【付费Pro版】“一键部署中转节点”,解决了不懂配置的麻烦,已实现:远程Socks5创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”,“小手一点”,直接可用, +#### 21、新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”,有“备用方式申请证书”,当用常规方式【1】申请不下来时,可以试试“备用方式”, +#### 22、新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”,有“可自定义证书路径”,自己进入 VPS 中“手动上传证书”,复制路径,在脚本中填入即可, +#### 23、新增 -〔X-Panel 面板〕----->>【付费Pro版】“深度调优脚本”,包含 BBR+FQ, TCP Fast Open, 内存缓冲区及队列优化,在〔Pro版〕脚本中“第29选项”可直接用, +#### 24、新增 -〔X-Panel 面板〕----->>【付费Pro版】“首页会员等级”显示,能够明确展示:自己的会员等级,授权码信息,以及“版本更新”提示, +#### 25、新增 -〔X-Panel 面板〕----->>【付费Pro版】“节点上/下线TG通知”功能,对于【拼车】的宝子,能明确知道:哪个节点,什么时候上线?或者下线时间,做到“心中有数”, +#### 26、新增 -〔X-Panel 面板〕----->>【付费Pro版】“签到得积分”功能,后期针对有【积分】的宝子,会不断推出:相应的【特权】和【福利待遇】, +#### 27、新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出:积分查询,积分换购,授权码查询,修改用户名,积分转移/打赏,以及“积分排行榜”, +#### 28、新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出【“积分换购”的可用功能】:A、消耗1000积分“自助重置换绑时间”,B、消耗5000积分“自助换购一个普通授权码”, +#### 29、新增 -〔X-Panel 面板〕----->>【付费Pro版】“创建数据快照 + 远程急救还原”的提示功能: 可直观通过「终端UI界面」看到具体「备份 + 还原」步骤;用法:面板报错“崩了”,不用像之前那样:卸载面板 -->> 重装面板,更不用很麻烦去“重装系统”解决,直接:远程急救还原,前提就是:你自己要知道,在面板“正常运行”的时候,去「备份数据快照」, +#### 30、新增 -〔X-Panel 面板〕----->>【付费Pro版】“每月重置流量”功能:可输入1—31之间的任意数字,比如:输入12,即代表“每月12号”「重置入站流量」,以便提供更友好的“重置流量方式”, +#### 31、新增 -〔X-Panel 面板〕----->>【付费Pro版】“批量部署节点”功能:可直接在面板后台的“一键配置”中去使用,点击一次可批量部署生成10条「VLESS + TCP + Reality + Vision」协议组合的入站, +#### 32、新增 -〔X-Panel 面板〕----->>【付费Pro版】“购买机器人”功能,可自助全自动在“机器人”中:购买授权码,增加配额,充值积分,自助重置换绑等,联系:[https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) 。 +![76](./media/76.png) + +------------ +## ✰你必须要看的【重要安全提示/警告】✰ +#### 1、请勿使用【http明文模式】登录管理面板,因为明文会造成信息泄露;这个安全问题社区讨论过, +#### 2、可使用设置【SSH端口转发功能】去登录面板或安装证书之后用https加密方式登录;两种方式选择其一, +![30](./media/30.png) +#### 3、若无域名那就按照脚本提示去做【ssh转发】;有域名则可选择更加安全的【申请安装证书】方式, +#### 4、Windows电脑首先通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务, +![36](./media/36.png) +![31](./media/31.png) +![32](./media/32.png) +![33](./media/33.png) +#### 5、若在搭建之前没有翻墙加密,则【http明文模式】登录面板有很大的信息泄露安全风险,那建议你第一次搭建成功之后,去修改用户/密码,和访问路径,后期则通过搭建好的代理加密访问, +![34](./media/34.png) +#### 6、在做【ssh转发】过程中,本地电脑的终端不能关闭,保持打开不能断开;且每一次要登录〔X-Panel面板〕管理后台都要做【ssh转发】,因为关闭之后就失效了。 +![35](./media/35.png) +#### PS:上述两种方法:【ssh端口转发】或申请安装证书的目的都是为了更安全地登录面板,而至于搭建的其他流程和步骤,都是一样的;如果你已经【申请安装证书】了,并不会受到其他什么额外影响,就不用去折腾【ssh转发】了,直接用 【https://你的域名:端口/路径】 去登录你的面板管理后台就行了。 + +------------ +## 如何在〔X-Panel-Pro 面板〕中去使用【一个机器人管理多面板】功能? +#### 1、先购买【授权码】安装好〔X-Panel-Pro 面板〕,进入后台,在【主从管理】界面, +![64](./media/64.png) +#### 2、点击【添加被控端 VPS】,按照要求填入面板登录地址,用户名和密码,备注等信息, +![65](./media/65.png) +#### 3、然后点击【配置本机“主控机器人”】,跳转到【机器人设置页面】,输入token令牌配置好, +![66](./media/66.png) +![73](./media/73.png) +#### 4、现在重启本机 VPS,打开TG端机器人,即可看到【切换控制 VPS】等菜单,切换操作即可使用。 +![67](./media/67.png) +![68](./media/68.png) + +------------ +## 如何在〔X-Panel-Pro 面板〕中去使用【一键部署中转节点】的功能? +#### 1、先购买【授权码】安装好〔X-Panel-Pro 面板〕,进入后台,在【主从管理】界面, +#### 2、点击【添加中转机 VPS】,按照要求填入面板登录地址,用户名和密码,备注等信息, +![69](./media/69.png) +#### 3、然后点击列表页中的【一键部署中转】,即可在后续的流程中,自动进入配置流程, +![70](./media/70.png) +#### 4、稍等片刻,本机配置好的话就几秒,全自动给您生成了【二维码和中转链接】,复制可用, +![71](./media/71.png) +#### 5、记得要去放行【本机】和【远程中转机】对应的端口,也可点击【检测中转节点】功能去看是否通? +![72](./media/72.png) + +------------ +## ✰如何从其他x-ui版本迁移到〔X-Panel面板〕?✰ +#### 1、若你用的是伊朗老哥的3X-UI,是可以直接〔覆盖安装〕的,因为数据库文件等位置是没有改变的,所以直接覆盖安装,并不会影响你〔原有节点及配置〕等数据;安装命令如下: +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` +#### 2、若你之前用的是Docker方式安装,那先进入容器里面/命令:docker exec -it 容器id /bin/sh,再执行以上脚本命令直接【覆盖安装】即可, +#### 3、若你用的是之前F佬的x-ui或者其他分支版本,那直接覆盖安装的话,并不能确保一定就能够兼容?建议你先去备份〔数据库〕配置文件,再进行安装〔X-Panel面板〕。 + + +------------ +## 安装之前的准备 +- 购买一台性能还不错的VPS,可通过本页底部链接购买, +- PS:若你不想升级系统,则可以跳过此步骤。 +- 若你需要更新/升级系统,Debian系统可用如下命令: + ``` + apt update + apt upgrade -y + apt dist-upgrade -y + apt autoclean + apt autoremove -y + ``` +- 查看系统当前版本: + ``` + cat /etc/debian_version + ``` +- 查看内核版本: + ``` + uname -r + ``` +- 列出所有内核: + ``` + dpkg --list | grep linux-image + ``` +- 更新完成后执行重新引导: + ``` + update-grub + ``` +- 完成以上步骤之后输入reboot重启系统 + +------------ +## 【搬瓦工】重装/升级系统之后SSH连不上如何解决? +- 【搬瓦工】重装/升级系统会恢复默认22端口,如果需要修改SSH的端口号,您需要进行以下步骤: +- 以管理员身份使用默认22端口登录到SSH服务器 +- 打开SSH服务器的配置文件进行编辑,SSH配置文件通常位于/etc/ssh/sshd_config +- 找到"Port"选项,并将其更改为您想要的端口号 +- Port <新端口号>,请将<新端口号>替换为您想要使用的端口号 +- 保存文件并退出编辑器 +- 重启服务器以使更改生效 + +------------ +## 安装 & 升级 +- 使用〔X-Panel面板〕脚本一般情况下,安装完成创建入站之后,端口是默认关闭的,所以必须进入脚本选择【22】去放行端口 +- 要使用【自动续签】证书功能,也必须放行【80】端口,保持80端口是打开的,才会每3个月自动续签一次 + +- 【全新安装】请执行以下脚本: +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` +#### 如果执行了上面的代码但是报错,证明你的系统里面没有curl这个软件,请执行以下命令先安装curl软件,安装curl之后再去执行上面代码, +``` +apt update -y&&apt install -y curl&&apt install -y socat +``` + +- 若要对版本进行升级,可直接通过脚本选择【2】,如下图: +![8](./media/8.png) +![10](./media/10.png) +- 在到这一步必须要注意:要保留旧设置的话,需要输入【n】 +![11](./media/11.png) + + +## 安装指定版本 + +若要安装指定的版本,请使用以下安装命令。 e.g., ver `v26.2.15`: + +``` +VERSION=v26.2.15 && bash <(curl -Ls "https://raw.githubusercontent.com/xeefei/x-panel/$VERSION/install.sh") $VERSION +``` +------------ +## 若你的VPS默认有防火墙,请在安装完成之后放行指定端口 +- 放行【面板登录端口】 +- 放行出入站管理协议端口 +- 如果要申请安装证书并每3个月【自动续签】证书,请确保80和443端口是放行打开的 +- 可通过此脚本的第【21】选项去安装防火墙进行管理,如下图: +![9](./media/9.png) +- 若要一次性放行多个端口或一整个段的端口,用英文逗号隔开。 +#### PS:若你的VPS没有防火墙,则所有端口都是能够ping通的,可自行选择是否进入脚本安装防火墙保证安全,但安装了防火墙必须放行相应端口。 + +------------ +## 如何在〔X-Panel面板〕中使用简单快捷的【一键配置】生成功能? +#### 1、进入后台,并且你已经【安装了证书】,可在【添加入站】处看到, +![53](./media/53.png) +#### 2、点击【一键配置】,在弹出的页面中【按需选择】去生成协议配置即可, +![54](./media/54.png) +#### 3、直接复制【链接】,导入软件;若后台出现【醒目提示】,那就手动放行【相应端口】, +![55](./media/55.png) +#### 4、若你是用【TG端电报机器人】的【一键配置】生成功能,那直接点击就用, +![56](./media/56.png) +#### 5、选择好自己想要【一键创建】的协议组合类型,点击之后稍作等待, +![57](./media/57.png) +#### 6、TG端【一键配置】创建成功之后,二维码和链接地址机器人会发送给你,如下: +![58](./media/58.png) + +------------ +## 如何在〔X-Panel 项目〕中进行【抽奖游戏】赢奖品? +#### 1、必须绑定好【TG端机器人】,怎么绑定?去看下面“绑定机器人”那部分教程, +#### 2、在【TG端】直接点击【娱乐抽奖】菜单,就会弹出【每日幸运】抽奖游戏, +![59](./media/59.png) +#### 3、点击【进行抽奖】,就可“全凭手气”得到随机的抽奖结果,如下图所示, +![60](./media/60.png) +#### 4、在你【中奖】之后,截完整的“中奖页面”图片给交流群内管理员,即可兑奖。 +![61](./media/61.png) + +------------ +## 安装证书开启https方式实现域名登录访问管理面板/----->>偷自己 +#### PS:如果不需要以上功能或无域名,可以跳过这步;建议申请证书, +##### 1、把自己的域名托管到CF,并解析到自己VPS的IP,不要开启【小云朵】, +##### 2、如果要申请安装证书并每3个月【自动续签】证书,请确保80和443端口是放行打开的, +##### 3、输入x-ui命令进入面板管理脚本,通过选择第【18】选项去进行安装,若使用 18 ---> 1方式不能申请下来,那就用 18--->5 备用方式去申请,或者也可用【19选项】去申请, +![74](./media/74.png) +##### 4、首先输入解析好的【域名】进行验证,然后可以默认用【80端口】,直接回车申请, +![27](./media/27.png) +##### 5、进入后台【面板设置】—–>【常规】中,会看到脚本已经自动填好了证书公钥、私钥路径, +##### 6、点击左上角的【保存】和【重启面板】,即可用自己域名进行登录管理;也可按照后续方法实现【自己偷自己】。 + +------------ +## 登录面板进行【常规】设置 +### 特别是如果在安装过程中,全部都是默认【回车键】安装的话,用户名/密码/访问路径是随机的,而面板监听端口默认是:13688,可自行进入面板更改, +##### 1、填写自己想要设置的【面板监听端口】,并去登录SSH放行, +##### 2、更改自己想要设置的【面板登录访问路径】,后续加上路径登录访问, +![25](./media/25.png) +##### 3、其他:安全设定和电报机器人等配置,可自行根据需求去进行设置, +##### 4、强烈建议配置电报机器人,使用【TG端】可方便远程接管 VPS 服务器, +![26](./media/26.png) +##### 5、面板设置【改动保存】之后,都需要点击左上角【重启面板】,才能生效。 +#### PS:若你在正确完成了上述步骤之后,你没有安装证书的情况下,去用【ssh转发】的方式却不能访问面板,那请检查一下是不是你的浏览器自动默认开启了https模式,需要手动调整一下改成http方式,把“s”去掉,即可访问成功;或查看一下是不是对应的端口被占用? + +------------ +## 创建【入站协议】和添加【客户端】,并测试上网 +##### 1、点击左边【入站列表】,然后【添加入站】,传输方式保持【TCP】不变,尽量选择主流的vless+reality+vision协议组合, +![23](./media/23.png) +##### 2、在选择reality安全选项时,偷的域名可以使用默认的,要使用其他的,请替换尽量保持一致就行,比如Apple、Yahoo,VPS所在地区的旅游、学校网站等;如果要实现【偷自己】,请参看后续【如何偷自己】的说明部分;而私钥/公钥部分,可以直接点击下方的【Get New Cert】获取一个随机的, +##### 3、在创建reality安全选项过程中,至于其他诸如:PROXY Protocol,HTTP 伪装,TPROXY,External Proxy等等选项,若无特殊要求,保持默认设置即可,不用去动它们, +![24](./media/24.png) +##### 4、创建好入站协议之后,默认只有一个客户端,可根据自己需求继续添加;重点:并编辑客户端,选择【Flow流控】为xtls-rprx-vision, +![19](./media/19.png) +##### 5、其他:流量限制,到期时间,客户TG的ID等选项根据自己需求填写, +![4](./media/4.png) +##### 6、一定要放行端口之后,确保端口能够ping通,再导入软件, +##### 7、点击二维码或者复制链接导入到v2rayN等软件中进行测试。 + +------------ +## 备份与恢复/迁移数据库(以Debian系统为例) +#### 一、备份:通过配置好电报管理机器人,可点击管理机器人的“相应菜单按钮”获取【备份配置】文件,有x-ui.db和config.json两个文件,可自行下载保存到自己电脑里面, +![14](./media/14.png) +#### 二、搭建:在新的VPS中全新安装好〔X-Panel面板〕,通过脚本放行之前配置的所有端口,一次性放行多个端口请用【英文逗号】分隔, +#### 三、若需要安装证书,则提前把域名解析到新的VPS对应的IP,并且去输入x-ui选择第【18】选项去安装,并记录公钥/私钥的路径,无域名则跳过这一步, +#### 四、恢复:SSH登录服务器找到/etc/x-ui/x-ui.db和/usr/local/x-ui/bin/config.json文件位置,上传之前的两个备份文件,进行覆盖, +![12](./media/12.png) +##### PS:把之前通过自动备份下载得到的两个文件上传覆盖掉旧文件,重启〔X-Panel面板〕即可【迁移成功】;即使迁移过程中出现问题,你是有备份文件的,不用担心,多试几次。 +![13](./media/13.png) +#### 五、若安装了证书,去核对/更改一下证书的路径,一般是同一个域名的话,位置在:/root/cert/域名/fullchain.pem,路径是相同的就不用更改, +#### 六、重启面板/重启服务器,让上述步骤生效即可,这时可以看到所有配置都是之前自己常用的,包括面板用户名、密码,入站、客户端,电报机器人配置等。 +#### PS:若您使用的是【Pro版】,则可直接使用:“创建数据快照 + 远程急救还原”功能,对面板数据库和配置文件进行操作。 + +------------ +## 安装完成后如何设置调整成【中文界面】? +- 方法一:通过管理后台【登录页面】调整,登录时可以选择,如下图: +![15](./media/15.png) +- 方法二:通过在管理后台-->【面板设置】中去选择设置,如下图: +![16](./media/16.png) +- 【TG机器人】设置中文:通过在管理后台-->【面板设置】-->【机器人配置】中去选择设置,并建议打开数据库备份和登录通知,如下图: +![17](./media/17.png) + +------------ +## 用〔X-Panel面板〕如何实现【自己偷自己】? +- 其实很简单,只要你为面板设置了证书, +- 开启了HTTPS登录,就可以将〔X-Panel面板〕自身作为Web Server, +- 无需Nginx等,这里给一个示例: +- 其中目标网站(Dest)请填写面板监听端口, +- 可选域名(SNI)填写面板登录域名, +- 如果您使用其他web server(如nginx)等, +- 将目标网站改为对应监听端口也可。 +- 需要说明的是,如果您处于白名单地区,自己“偷”自己并不适合你; +- 其次,可选域名一项实际上可以填写任意SNI,只要客户端保持一致即可,不过并不推荐这样做。 +- 配置方法如下图所示: +![18](./media/18.png) + +------------ +## 〔子域名〕被墙针对特征 +#### 网络表现: +##### 1、可以Ping通域名和IP地址, +##### 2、子域名无法打开〔X-Panel面板〕管理界面, +##### 3、什么都正常就是不能上网; + +#### 问题: +##### 你的子域名被墙针对了:无法上网! + +#### 解决方案: +##### 1、更换为新的子域名, +##### 2、解析新的子域名到VPS的IP, +##### 3、重新去安装新证书, +##### 4、重启〔X-Panel面板〕和服务器, +##### 5、重新去获取链接并测试上网。 +#### PS:若通过以上步骤还是不能正常上网,则重装VPS服务器OS系统,以及〔X-Panel面板〕全部重新安装,之后就正常了! + +------------ +## 用〔X-Panel面板〕如何开启【设备限制】功能? +##### 1、进入后台在【添加入站】的时候,弹出来的页面就能有【设备数量】输入框, +![37](./media/37.png) +##### 2、通过步骤1设置完成后,在后台的【入站列表】页面也有对应的同步数据显示。 +![38](./media/38.png) +##### 3、具体要查看【设备限制】功能的封禁情况,就进入〔X-Panel面板〕后台用日志查看。 +![39](./media/39.png) +##### 4、以下图片里面,详细阐述了我们的〔设备限制〕功能,跟3X-UI原本就有的〔IP Limit〕之间的区别对比。 +![40](./media/40.png) + +------------ +## 用〔X-Panel面板〕如何开启【独立限速】功能? +##### 1、进入后台在【添加入站】的时候,弹出来的页面在【客户/用户】那里就能输入具体数字, +![47](./media/47.png) +##### 2、也可以在添加好一个【入站】之后,点击【添加客户端】去找到【独立限速】输入框, +![48](./media/48.png) +##### 3、当你想批量一次性创建多个【客户/用户】时,同样可以使用【独立限速】功能, +![49](./media/49.png) +![50](./media/50.png) +##### 4、若后期你想更改某一个【客户端/用户】的限制速率,那就自己先找到,然后点击【编辑】即可, +![51](./media/51.png) +##### 5、具体要查看【独立限速】功能的启用情况,就进入〔X-Panel面板〕后台用日志查看。 +![52](./media/52.png) + +------------ +## 用〔X-Panel面板〕如何开启【每月流量自动重置】? +##### 1、进入后台的【入站列表】,选择需要设置的【客户端】, +![29](./media/29.png) +##### 2、要注意是编辑【入站】下面的【客户端】,才会有效果, +##### 2、并不是编辑【入站】,所以不要弄错对象,如下图所示: +![28](./media/28.png) + + +------------ +## 在自己的VPS服务器部署【订阅转换】功能 +### 如何把vless/vmess等协议转换成Clash/Surge等软件支持的格式? +##### 1、进入脚本输入x-ui命令调取面板,选择第【25】选项安装订阅转换模块, +##### 2、等待安装【订阅转换】成功之后,访问地址:https://你的域名:15268 , +![41](./media/41.png) +##### 3、因为在转换过程中需要调取后端API,所以请确保端口 8000 和 15268 是打开放行的, +##### 4、直接复制脚本中提供的【登录地址】,进入后台,点击【节点列表】,如下图: +![42](./media/42.png) +##### 5、接下来点击左边侧边栏的【订阅列表】去【添加订阅】,按照下图中去操作, +![43](./media/43.png) +##### 6、最后一步,点击【客户端】,即可导入Clash等软件中使用。 +![44](./media/44.png) +![45](./media/45.png) + +------------ +## 常见的翻墙软件/工具: +- [1、Windows系统v2rayN:https://github.com/2dust/v2rayN](https://github.com/2dust/v2rayN) +- [2、安卓手机版【v2rayNG】:https://github.com/2dust/v2rayNG](https://github.com/2dust/v2rayNG) +- [3、苹果手机IOS【小火箭】:https://apple02.com/(自己购买)](https://apple02.com/) +- [4、苹果MacOS电脑【Clash Verge】:https://github.com/clash-verge-rev/clash-verge-rev/releases](https://github.com/clash-verge-rev/clash-verge-rev/releases) + [或v2rayN:https://github.com/2dust/v2rayN](https://github.com/2dust/v2rayN) + +------------ +## 如何保护自己的IP不被墙被封? +##### 1、使用的代理协议要安全,加密是必备,推荐使用vless+reality+vision协议组合, +##### 2、因为有时节点会共享,在不同的地区,多个省份之间不要共同连接同一个IP, +##### 3、连接同一个IP就算了,不要同一个端口,不要同IP+同端口到处漫游,要分开, +##### 4、同一台VPS,不要在一天内一直大流量去下载东西使用,不要流量过高要切换, +##### 5、创建【入站协议】的时候,尽量用【高位端口】,比如40000--65000之间的端口号。 +#### 提醒:为什么在特殊时期,比如:两会,春节等被封得最严重最惨? +##### 尼玛同一个IP+同一个端口号,多个省份去漫游,跟开飞机场一样!不封你,封谁的IP和端口? +#### 总结:不要多终端/多省份/多个朋友/共同使用同一个IP和端口号!使用〔X-Panel面板〕多创建几个【入站】, +#### 多做几条备用,各用各的!各行其道才比较安全!GFW的思维模式是干掉机场,机场的特征个人用户不要去沾染,自然IP就保护好了。 + +------------ +## SSL 认证 + +
+ 点击查看 SSL 认证 + +### ACME + +要使用 ACME 管理 SSL 证书: + +1. 确保您的域名已正确解析到服务器, +2. 输入“x-ui”命令并选择“SSL 证书管理”, +3. 您将看到以下选项: + + - **获取证书** ----获取SSL证书 + - **吊销证书** ----吊销现有的SSL证书 + - **续签证书** ----强制续签SSL证书 + - **显示所有证书** ----显示服务器中所有能用的证书 + - **设置面板证书路径** ----指定面板要使用的证书 + + +### Certbot + +安装和使用 Certbot: + +```sh +apt-get install certbot -y +certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com +certbot renew --dry-run +``` + +### Cloudflare + +管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件: + +- Cloudflare 邮箱地址 +- Cloudflare Global API Key +- 域名已通过 cloudflare 解析到当前服务器 + +**如何获取 Cloudflare全局API密钥:** + +1. 在终端中输入“x-ui”命令,然后选择“CF SSL 证书”。 +2. 访问链接: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens). +3. 点击“查看全局 API 密钥”(如下图所示): + ![](media/APIKey1.PNG) +4. 您可能需要重新验证您的帐户。之后,将显示 API 密钥(请参见下面的屏幕截图): + ![](media/APIKey2.png) + +使用时,只需输入您的“域名”、“电子邮件”和“API KEY”即可。示意图如下: + ![](media/DetailEnter.png) + + +
+ +------------ +## 手动安装 & 升级 + +
+ 点击查看 手动安装 & 升级 + +#### 使用 + +1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + s390x) echo 's390x' ;; + *) XUI_ARCH="amd64" ;; +esac + + +wget https://github.com/xeefei/x-panel/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz +``` + +2. 下载压缩包后,执行以下命令安装或升级 x-ui: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + s390x) echo 's390x' ;; + *) XUI_ARCH="amd64" ;; +esac + +cd /root/ +rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui +tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz +chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh +cp x-ui/x-ui.sh /usr/bin/x-ui +cp -f x-ui/x-ui.service /etc/systemd/system/ +mv x-ui/ /usr/local/ +systemctl daemon-reload +systemctl enable x-ui +systemctl restart x-ui +``` + +
+ +------------ +## 通过Docker安装 + +
+ 点击查看 通过Docker安装 + +#### 使用 + + +1. **安装Docker** + + ```sh + bash <(curl -sSL https://get.docker.com) + ``` + + +2. **克隆项目仓库** + + ```sh + git clone https://github.com/xeefei/x-panel.git + cd x-panel + ``` + +3. **启动服务**: + + ```sh + docker compose up -d + ``` + + 添加 ```--pull always``` 标志使 docker 在拉取更新的镜像时自动重新创建容器。有关更多信息,请参阅:https://docs.docker.com/reference/cli/docker/container/run/#pull + + **或** + + ```sh + docker run -itd \ + -e XRAY_VMESS_AEAD_FORCED=false \ + -v $PWD/db/:/etc/x-ui/ \ + -v $PWD/cert/:/root/cert/ \ + --network=host \ + --restart=unless-stopped \ + --name x-panel \ + ghcr.io/xeefei/x-panel:latest + ``` + +4. **更新至最新版本** + + ```sh + cd x-panel + docker compose down + docker compose pull x-panel + docker compose up -d + ``` + +5. **从Docker中删除x-panel ** + + ```sh + docker stop x-panel + docker rm x-panel + cd -- + rm -r x-panel + ``` + +
+ +------------ +## 建议使用的操作系统 + +- Ubuntu 20.04+ +- Debian 11+ +- CentOS 8+ +- OpenEuler 22.03+ +- Fedora 36+ +- Arch Linux +- Manjaro +- Armbian +- AlmaLinux 8.0+ +- Rocky Linux 8+ +- Oracle Linux 8+ +- OpenSUSE Tubleweed +- Amazon Linux 2023 + +------------ +## 支持的架构和设备 +
+ 点击查看 支持的架构和设备 + +我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构: + +- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。 + +- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。 + +- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。 + +- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构,它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。 + +- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备,虽然不太普遍,但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。 + +- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。 +
+ +------------ +## Languages + +- English(英语) +- Farsi(伊朗语) +- Simplified Chinese(简体中文) +- Traditional Chinese(繁体中文) +- Russian(俄语) +- Vietnamese(越南语) +- Spanish(西班牙语) +- Indonesian (印度尼西亚语) +- Ukrainian(乌克兰语) +- Turkish(土耳其语) +- Português (葡萄牙语) + +------------ +## 项目特点 + +- 系统状态查看与监控 +- 可搜索所有入站和客户端信息 +- 深色/浅色主题随意切换 +- 支持多用户和多协议 +- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard +- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY +- 流量统计、流量限制、过期时间限制 +- 可自定义的 Xray配置模板 +- 支持HTTPS访问面板(自备域名+SSL证书) +- 支持一键式SSL证书申请和自动续签证书 +- 更多高级配置项目请参考面板去进行设定 +- 修复了 API 路由(用户设置将使用 API 创建) +- 支持通过面板中提供的不同项目更改配置。 +- 支持从面板导出/导入数据库 + +## 默认面板设置 + +
+ + 点击查看 默认设置 + + ### 默认信息 + +- **端口** + - 13688 +- **用户名 & 密码 & 访问路径** + - 当您跳过设置时,这些信息会随机生成, + - 您也可以在安装的时候自定义访问路径。 +- **数据库文件路径:** + - /etc/x-ui/x-ui.db +- **Xray 配置文件路径:** + - /usr/local/x-ui/bin/config.json +- **面板链接(有SSL):** + - https://你的域名:13688/访问路径/panel + +
+ +------------ +## [WARP 配置](https://gitlab.com/fscarmen/warp) + +
+ 点击查看 WARP 配置 + +#### 使用 + +**对于版本 `v2.1.0` 及更高版本:** + +WARP 是内置的,无需额外安装;只需在面板中打开必要的配置即可。 + +**如果要在 v2.1.0 之前使用 WARP 路由**,请按照以下步骤操作: + +**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap + + - **Account Type (free, plus, team):** Choose the appropriate account type. + - **Enable/Disable WireProxy:** Toggle WireProxy on or off. + - **Uninstall WARP:** Remove the WARP application. + +**2.** 如果您已经安装了 warp,您可以使用以下命令卸载: + + ```sh + warp u + ``` + +**3.** 在面板中打开您需要的配置 + + 配置: + + - Block Ads + - Route Google, Netflix, Spotify, and OpenAI (ChatGPT) traffic to WARP + - Fix Google 403 error + + +
+ +------------ +## IP 限制 + +
+ 点击查看 IP 限制 + +#### 使用 + +**注意:** 使用 IP 隧道时,IP 限制无法正常工作。 + +- 对于 `v1.6.1`之前的版本 : + + - IP 限制 已被集成在面板中。 + +- 对于 `v1.7.0` 以及更新的版本: + + - 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件: + + 1. 使用面板内置的 `x-ui` 指令 + 2. 选择 `IP Limit Management`. + 3. 根据您的需要选择合适的选项。 + + - 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。 + + ```sh + "log": { + "access": "./access.log", + "dnsLog": false, + "loglevel": "warning" + }, + ``` + - 您需要在Xray配置中手动设置〔访问日志〕的路径。 + +
+ +------------ +## Telegram 机器人 + +
+ 点击查看 Telegram 机器人 + +#### 使用 + +Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括: + +- 电报令牌 +- 管理员聊天 ID +- 通知时间(cron 语法) +- 到期日期通知 +- 流量上限通知 +- 数据库备份 +- CPU 负载通知 + + +**参考:** + +- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知 +- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知 +- `@hourly` - 每小时通知 +- `@daily` - 每天通知 (00:00) +- `@weekly` - 每周通知 +- `@every 8h` - 每8小时通知 + +### Telegram Bot 功能 + +- 定期报告 +- 登录通知 +- CPU 阈值通知 +- 提前报告的过期时间和流量阈值 +- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单 +- 支持使用UUID(VMESS/VLESS)或密码(TROJAN)搜索报文流量报告 - 匿名 +- 基于菜单的机器人 +- 通过电子邮件搜索客户端(仅限管理员) +- 检查所有入库 +- 检查服务器状态 +- 检查耗尽的用户 +- 根据请求和定期报告接收备份 +- 多语言机器人 + +### 注册 Telegram bot + +- 与 [Botfather](https://t.me/BotFather) 对话: + ![Botfather](./media/botfather.png) + +- 使用 /newbot 创建新机器人:你需要提供机器人名称以及用户名,注意名称中末尾要包含“bot” + ![创建机器人](./media/newbot.png) + +- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。 + ![令牌](./media/token.png) + +- 输入您的面板并配置 Telegram 机器人设置,如下所示: + ![面板设置](./media/panel-bot-config.png) + +在输入字段编号 3 中输入机器人令牌。 +在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可) + +- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot), 启动机器人,它会给你 Telegram 用户 ID。 +![用户 ID](./media/user-id.png) + +
+ +------------ +## API 路由 + +
+ 点击查看 API 路由 + +#### 使用 + +- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录 +- `/panel/api/inbounds` 以下操作的基础: + +| 方法 | 路径 | 操作 | +| :----: | ---------------------------------- | ------------------------------------------- | +| `GET` | `"/list"` | 获取所有入站 | +| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id | +| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 | +| `GET` | `"/getClientTrafficsById/:id"` | 通过用户ID获取客户端流量 | +| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 | +| `POST` | `"/add"` | 添加入站 | +| `POST` | `"/del/:id"` | 删除入站 | +| `POST` | `"/update/:id"` | 更新入站 | +| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 | +| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 | +| `POST` | `"/addClient"` | 将客户端添加到入站 | +| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 | +| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 | +| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 | +| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 | +| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 | +| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) | +| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) | + +- 使用`clientId` 项应该填写下列数据: + +- `client.id` for VMESS and VLESS +- `client.password` for TROJAN +- `client.email` for Shadowsocks + + + +- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm) + +- [Run In Postman](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9) +
+ +------------ +## 环境变量 + +
+ 点击查看 环境变量 + +#### Usage + +| 变量 | Type | 默认 | +| -------------- | :--------------------------------------------: | :------------ | +| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` | +| XUI_DEBUG | `boolean` | `false` | +| XUI_BIN_FOLDER | `string` | `"bin"` | +| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` | +| XUI_LOG_FOLDER | `string` | `"/var/log"` | + +例子: + +```sh +XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go +``` + +
+ +------------ +## 预览 + +![1](./media/1.png) +![2](./media/2.png) +![3](./media/3.png) +![5](./media/5.png) +![6](./media/6.png) +![7](./media/7.png) + +------------ +## 广告赞助 +- 如果你觉得本项目对你有用,而且你也恰巧有这方面的需求,你也可以选择通过我的购买链接赞助我。 +- [1、搬瓦工GIA高端线路,仅推荐购买GIA套餐:https://bandwagonhost.com/aff.php?aff=75015](https://bandwagonhost.com/aff.php?aff=75015) +- [2、Dmit高端GIA线路:https://www.dmit.io/aff.php?aff=9326](https://www.dmit.io/aff.php?aff=9326) +- [3、Gomami亚太顶尖优化线路:https://gomami.io/aff.php?aff=174](https://gomami.io/aff.php?aff=174) +- [4、ISIF优质亚太优化线路:https://cloud.isif.net/login?affiliation_code=333](https://cloud.isif.net/login?affiliation_code=333) +- [5、ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选:https://my.zorocloud.com/aff.php?aff=1072](https://my.zorocloud.com/aff.php?aff=1072) +- [6、三网直连 IEPL / IPLC 直播流量转发:https://idc333.top/#register/BCUZXNELNO](https://idc333.top/#register/BCUZXNELNO) +- [7、Bagevm优质落地鸡(原生IP全解锁):https://www.bagevm.com/aff.php?aff=754](https://www.bagevm.com/aff.php?aff=754) +- [8、白丝云【4837线路】实惠量大管饱:https://cloudsilk.io/aff.php?aff=706](https://cloudsilk.io/aff.php?aff=706) +- [9、RackNerd极致性价比机器:https://my.racknerd.com/aff.php?aff=15268&pid=912](https://my.racknerd.com/aff.php?aff=15268&pid=912) + +------------ +## 特别感谢 + +- [MHSanaei](https://github.com/MHSanaei/) +- [alireza0](https://github.com/alireza0/) +- [FranzKafkaYu](https://github.com/FranzKafkaYu/) +- [vaxilu](https://github.com/vaxilu/) + +------------ +## 致谢 + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._ +- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ + +------------ +## Star 趋势 + +[![Stargazers over time](https://starchart.cc/xeefei/x-panel.svg)](https://starchart.cc/xeefei/x-panel) diff --git a/README.zh_CN.md b/README.zh_CN.md new file mode 100644 index 0000000..54b4a4c --- /dev/null +++ b/README.zh_CN.md @@ -0,0 +1,938 @@ +

Image

+ +**---------------------------------------一个更好的面板 • 基于Xray Core构建------------------------------** + + +[![](https://img.shields.io/github/v/release/xeefei/x-panel.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases) +[![](https://img.shields.io/github/actions/workflow/status/xeefei/x-panel/release.yml.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/actions) +[![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/x-panel.svg?style=for-the-badge)](#) +[![Downloads](https://img.shields.io/github/downloads/xeefei/x-panel/total.svg?style=for-the-badge)](https://github.com/xeefei/x-panel/releases/latest) +[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +> **声明:** 此项目仅供个人学习、交流使用,请遵守当地法律法规,勿用于非法用途;请勿用于生产环境。 + +> **注意:** 在使用此项目和〔教程〕过程中,若因违反以上声明使用规则而产生的一切后果由使用者自负。 + +**如果此项目对你有用,请给一个**:star2: + +

+ + Image + +

+ +- “支持项目”赞助地址(USDT): +- TRC20 ---->>> `TYQEmQp1P65u9bG7KPehgJdvuokfb72YkZ` +- Polygon ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` +- Solana ---->>> `7qVEZuV98QTDN5qUmsFwvqTSvkYpmLtNf8o1sh1mppwR` +- BSC/BEP20 ---->>> `0xd20eBE429c2398793178e015B2ca1Dc42601f3Eb` + +## [【X-Panel面板】交流群:https://t.me/XUI_CN](https://t.me/XUI_CN) +## [【X-Panel面板】详细安装流程步骤:https://xeefei.blogspot.com/2025/09/x-panel.html](https://xeefei.blogspot.com/2025/09/x-panel.html) + + +------------ +## ✰怎么购买【授权码】使用〔X-Panel-Pro版 面板〕?✰ +#### 1、【授权码】100/15U一个,一机一码,一直有效,永久使用,包括:重装,后期的升级/更新,都能使用,但是不能用于不同的机器,所以推荐稳定使用的机器用【授权码】,注:“授权码”属虚拟商品,购买之后,一经激活生效,概不退款, +#### 2、对于一年期限(年付/年抛)的机器,后期可以【换绑】,为什么要有时间限制?就是为了防止,有些人拿【授权码】滥用, +#### 3、购买方式/渠道联系机器人:[https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) + +#### 4、经常换的机器,去使用“免费基础版”就行,目前【安装界面】是有“两种方式”可选择的, + +#### 5、对于有业务需求,比如做 TK 的,做〔跨境电商〕的,如果很多机器,比如:20台,50台,或者 100台 以上机器的宝子,若都要用【收费Pro版】的话,可以用【批量授权】:5 ——> 20台,8折,尾号:5555 ,20 ——> 50台,7折,尾号:66666 ,50 ——> 100台,6折,尾号:777777 ,100 ——> 200台,5折,尾号:8888888,200台 ——> 以上,4折,尾号:99999999,以上列举出来的,就是【批量折扣】的统一“优惠”。注:〔批量授权码〕要求最低5台以上,是“一码通用”,一个“授权码”,可以绑定验证多台 VPS 机器,并且有专属的“豹子号”授权码。 +![63](./media/63.png) +———————————————————————————————— + +#### 6、若您需要购买【授权码】,请跟下面这个“机器人”去对话, + +#### ———————————————- + +#### [https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) +#### (授权码购买机器人) + +#### ———————————————- + +#### 输入:/start 或 购买,即可“在线下单”, + +#### 7、弹出【购买页面】,选择或输入“数量”,付款了支付系统会收到回调,金额到账之后,就会通过那个机器人【发放授权码】给您,整个流程,是【全自助】的“自动处理”方式,若您是〔增加配额〕,基本也是一样的流程, + +#### 8、请注意:机器人发给您的所有信息,尽量都去自己【耐心阅读】一遍,“使用说明”,以及〔VIP 群〕的信息,也全部都包含在里面的, + +#### 9、按照之前您安装更新〔X-Panel 面板〕的方式,直接重新输入【安装命令】,选择【2】,就能去,把之前的【免费版】,“无缝升级”到最新的【Pro版】。 + +———————————————————————————————— +#### 10、不能重复用于不同的机器 VPS,后期视情况不定时会上涨价格;对于〔X-Panel 面板〕后期的“新功能”,都将在【付费Pro版】中进行更新, + +#### 11、目前的【安装界面】,是有:两种可选,“免费基础版”一样可用,只是后期不再提供技术支持和重大更新,另外,在【免费基础版】中,【一键配置】功能将不再可用,全部放到了【付费Pro版】中, + +#### 12、后期的开发精力,全部会放到【付费Pro版】中,免费基础版不删库,持续保留,会大幅降低更新频率,后期只会同步更新 Xray 那边的【内核版本】等基础,想继续用的不影响,只是没有【新功能】可用,翻墙也足够。 + +![62](./media/62.png) + +------------ +## ✰〔X-Panel-Pro版 面板〕已实现的功能✰ +#### 1、新增 -【付费Pro版】的面板后台UI,添加醒目的“X-Panel-Pro”标识, +#### 2、优化 -【付费Pro版】TG端 【版本更新】提示功能,增加详细的“更新说明”, +#### 3、增加〔Pro版面板后台〕,使用 Reality 协议时,可点击随机更换所偷的域名, +#### 4、新增 -【付费Pro版】TG端 的【发送授权报告】,增加“唯一授权防伪码”, +#### 5、优化 -【付费Pro版】安装脚本界面,增加【Pro版】该有的“明确标识”, +#### 6、优化 -【付费Pro版】TG端的显示方式,增加该有的“会员标识”, +#### 7、新增 -【付费Pro版】安装脚本,有“网页版SSH工具”可选部署,脚本中〔第26选项〕, +#### 8、新增 - 【付费Pro版】安装脚本,有“线路和IP质量检测”可去使用,脚本中〔第27选项〕, +#### 9、新增 - 【付费Pro版】安装脚本,有“地区服务器DNS检测”可去使用,脚本中〔第28选项〕, +#### 10、新增 -【付费Pro版】---->>>TG端,同步有“网页版SSH工具”可选安装, +#### 11、优化 - 【付费Pro版】---->>>TG端,点击“服务器状态”时的“版本号显示”, +#### 12、说明 - 【付费Pro版】TG端中,使用命令:/webssh,安装“网页版SSH”, +#### 13、优化 -〔Pro版〕中的〔一键配置〕功能,有更友好的提示方式, +#### 14、新增 -【付费Pro版】---->>>面板后台的【首页 UI】,目前是有“5种”可选:标准布局 (默认),炫彩动画,深海科技,暮光薰衣,和幽林秘境;你喜欢什么类型的主题,就去点击“选定”之后,就不会自动变了,若后期需要更换,就重选, +#### 15、新增 -【付费Pro版】---->>>在“创建入站”时,可以在页面上更加方便地选择【重置流量】的方式:有每日重置,每周重置,按月重置,或从不重置, +#### 16、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“每日报告”,可定制【发送内容】,自己可点击“打开或关闭”,并且可以选择【发送时间】,可按天,或者每周,每月发, +#### 17、优化 -【付费Pro版】的“授权码验证机制”,增加【后台联网验证】,以及“机器指纹”等属性, +#### 18、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“多面板管理”,一个机器人可同时管理其他面板,可以很丝滑地远程操作【被控端 VPS】, +#### 19、新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“获取节点链接”功能,支持【本机】和【远程被控端 VPS】都能获取,开发此功能的目的在于:不用进〔面板后台〕,就能在 TG端 获取到之前已经创建过的“链接”, +#### 20、新增 -〔X-Panel 面板〕----->>【付费Pro版】“一键部署中转节点”,解决了不懂配置的麻烦,已实现:远程Socks5创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”,“小手一点”,直接可用, +#### 21、新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”,有“备用方式申请证书”,当用常规方式【1】申请不下来时,可以试试“备用方式”, +#### 22、新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”,有“可自定义证书路径”,自己进入 VPS 中“手动上传证书”,复制路径,在脚本中填入即可, +#### 23、新增 -〔X-Panel 面板〕----->>【付费Pro版】“深度调优脚本”,包含 BBR+FQ, TCP Fast Open, 内存缓冲区及队列优化,在〔Pro版〕脚本中“第29选项”可直接用, +#### 24、新增 -〔X-Panel 面板〕----->>【付费Pro版】“首页会员等级”显示,能够明确展示:自己的会员等级,授权码信息,以及“版本更新”提示, +#### 25、新增 -〔X-Panel 面板〕----->>【付费Pro版】“节点上/下线TG通知”功能,对于【拼车】的宝子,能明确知道:哪个节点,什么时候上线?或者下线时间,做到“心中有数”, +#### 26、新增 -〔X-Panel 面板〕----->>【付费Pro版】“签到得积分”功能,后期针对有【积分】的宝子,会不断推出:相应的【特权】和【福利待遇】, +#### 27、新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出:积分查询,积分换购,授权码查询,修改用户名,积分转移/打赏,以及“积分排行榜”, +#### 28、新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出【“积分换购”的可用功能】:A、消耗1000积分“自助重置换绑时间”,B、消耗5000积分“自助换购一个普通授权码”, +#### 29、新增 -〔X-Panel 面板〕----->>【付费Pro版】“创建数据快照 + 远程急救还原”的提示功能: 可直观通过「终端UI界面」看到具体「备份 + 还原」步骤;用法:面板报错“崩了”,不用像之前那样:卸载面板 -->> 重装面板,更不用很麻烦去“重装系统”解决,直接:远程急救还原,前提就是:你自己要知道,在面板“正常运行”的时候,去「备份数据快照」, +#### 30、新增 -〔X-Panel 面板〕----->>【付费Pro版】“每月重置流量”功能:可输入1—31之间的任意数字,比如:输入12,即代表“每月12号”「重置入站流量」,以便提供更友好的“重置流量方式”, +#### 31、新增 -〔X-Panel 面板〕----->>【付费Pro版】“批量部署节点”功能:可直接在面板后台的“一键配置”中去使用,点击一次可批量部署生成10条「VLESS + TCP + Reality + Vision」协议组合的入站, +#### 32、新增 -〔X-Panel 面板〕----->>【付费Pro版】“购买机器人”功能,可自助全自动在“机器人”中:购买授权码,增加配额,充值积分,自助重置换绑等,联系:[https://t.me/Buy_ShouQuan_Bot](https://t.me/Buy_ShouQuan_Bot) 。 +![76](./media/76.png) + +------------ +## ✰你必须要看的【重要安全提示/警告】✰ +#### 1、请勿使用【http明文模式】登录管理面板,因为明文会造成信息泄露;这个安全问题社区讨论过, +#### 2、可使用设置【SSH端口转发功能】去登录面板或安装证书之后用https加密方式登录;两种方式选择其一, +![30](./media/30.png) +#### 3、若无域名那就按照脚本提示去做【ssh转发】;有域名则可选择更加安全的【申请安装证书】方式, +#### 4、Windows电脑首先通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务, +![36](./media/36.png) +![31](./media/31.png) +![32](./media/32.png) +![33](./media/33.png) +#### 5、若在搭建之前没有翻墙加密,则【http明文模式】登录面板有很大的信息泄露安全风险,那建议你第一次搭建成功之后,去修改用户/密码,和访问路径,后期则通过搭建好的代理加密访问, +![34](./media/34.png) +#### 6、在做【ssh转发】过程中,本地电脑的终端不能关闭,保持打开不能断开;且每一次要登录〔X-Panel面板〕管理后台都要做【ssh转发】,因为关闭之后就失效了。 +![35](./media/35.png) +#### PS:上述两种方法:【ssh端口转发】或申请安装证书的目的都是为了更安全地登录面板,而至于搭建的其他流程和步骤,都是一样的;如果你已经【申请安装证书】了,并不会受到其他什么额外影响,就不用去折腾【ssh转发】了,直接用 【https://你的域名:端口/路径】 去登录你的面板管理后台就行了。 + +------------ +## 如何在〔X-Panel-Pro 面板〕中去使用【一个机器人管理多面板】功能? +#### 1、先购买【授权码】安装好〔X-Panel-Pro 面板〕,进入后台,在【主从管理】界面, +![64](./media/64.png) +#### 2、点击【添加被控端 VPS】,按照要求填入面板登录地址,用户名和密码,备注等信息, +![65](./media/65.png) +#### 3、然后点击【配置本机“主控机器人”】,跳转到【机器人设置页面】,输入token令牌配置好, +![66](./media/66.png) +![73](./media/73.png) +#### 4、现在重启本机 VPS,打开TG端机器人,即可看到【切换控制 VPS】等菜单,切换操作即可使用。 +![67](./media/67.png) +![68](./media/68.png) + +------------ +## 如何在〔X-Panel-Pro 面板〕中去使用【一键部署中转节点】的功能? +#### 1、先购买【授权码】安装好〔X-Panel-Pro 面板〕,进入后台,在【主从管理】界面, +#### 2、点击【添加中转机 VPS】,按照要求填入面板登录地址,用户名和密码,备注等信息, +![69](./media/69.png) +#### 3、然后点击列表页中的【一键部署中转】,即可在后续的流程中,自动进入配置流程, +![70](./media/70.png) +#### 4、稍等片刻,本机配置好的话就几秒,全自动给您生成了【二维码和中转链接】,复制可用, +![71](./media/71.png) +#### 5、记得要去放行【本机】和【远程中转机】对应的端口,也可点击【检测中转节点】功能去看是否通? +![72](./media/72.png) + +------------ +## ✰如何从其他x-ui版本迁移到〔X-Panel面板〕?✰ +#### 1、若你用的是伊朗老哥的3X-UI,是可以直接〔覆盖安装〕的,因为数据库文件等位置是没有改变的,所以直接覆盖安装,并不会影响你〔原有节点及配置〕等数据;安装命令如下: +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` +#### 2、若你之前用的是Docker方式安装,那先进入容器里面/命令:docker exec -it 容器id /bin/sh,再执行以上脚本命令直接【覆盖安装】即可, +#### 3、若你用的是之前F佬的x-ui或者其他分支版本,那直接覆盖安装的话,并不能确保一定就能够兼容?建议你先去备份〔数据库〕配置文件,再进行安装〔X-Panel面板〕。 + + +------------ +## 安装之前的准备 +- 购买一台性能还不错的VPS,可通过本页底部链接购买, +- PS:若你不想升级系统,则可以跳过此步骤。 +- 若你需要更新/升级系统,Debian系统可用如下命令: + ``` + apt update + apt upgrade -y + apt dist-upgrade -y + apt autoclean + apt autoremove -y + ``` +- 查看系统当前版本: + ``` + cat /etc/debian_version + ``` +- 查看内核版本: + ``` + uname -r + ``` +- 列出所有内核: + ``` + dpkg --list | grep linux-image + ``` +- 更新完成后执行重新引导: + ``` + update-grub + ``` +- 完成以上步骤之后输入reboot重启系统 + +------------ +## 【搬瓦工】重装/升级系统之后SSH连不上如何解决? +- 【搬瓦工】重装/升级系统会恢复默认22端口,如果需要修改SSH的端口号,您需要进行以下步骤: +- 以管理员身份使用默认22端口登录到SSH服务器 +- 打开SSH服务器的配置文件进行编辑,SSH配置文件通常位于/etc/ssh/sshd_config +- 找到"Port"选项,并将其更改为您想要的端口号 +- Port <新端口号>,请将<新端口号>替换为您想要使用的端口号 +- 保存文件并退出编辑器 +- 重启服务器以使更改生效 + +------------ +## 安装 & 升级 +- 使用〔X-Panel面板〕脚本一般情况下,安装完成创建入站之后,端口是默认关闭的,所以必须进入脚本选择【22】去放行端口 +- 要使用【自动续签】证书功能,也必须放行【80】端口,保持80端口是打开的,才会每3个月自动续签一次 + +- 【全新安装】请执行以下脚本: +``` +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh) +``` +#### 如果执行了上面的代码但是报错,证明你的系统里面没有curl这个软件,请执行以下命令先安装curl软件,安装curl之后再去执行上面代码, +``` +apt update -y&&apt install -y curl&&apt install -y socat +``` + +- 若要对版本进行升级,可直接通过脚本选择【2】,如下图: +![8](./media/8.png) +![10](./media/10.png) +- 在到这一步必须要注意:要保留旧设置的话,需要输入【n】 +![11](./media/11.png) + + +## 安装指定版本 + +若要安装指定的版本,请使用以下安装命令。 e.g., ver `v26.2.15`: + +``` +VERSION=v26.2.15 && bash <(curl -Ls "https://raw.githubusercontent.com/xeefei/x-panel/$VERSION/install.sh") $VERSION +``` +------------ +## 若你的VPS默认有防火墙,请在安装完成之后放行指定端口 +- 放行【面板登录端口】 +- 放行出入站管理协议端口 +- 如果要申请安装证书并每3个月【自动续签】证书,请确保80和443端口是放行打开的 +- 可通过此脚本的第【21】选项去安装防火墙进行管理,如下图: +![9](./media/9.png) +- 若要一次性放行多个端口或一整个段的端口,用英文逗号隔开。 +#### PS:若你的VPS没有防火墙,则所有端口都是能够ping通的,可自行选择是否进入脚本安装防火墙保证安全,但安装了防火墙必须放行相应端口。 + +------------ +## 如何在〔X-Panel面板〕中使用简单快捷的【一键配置】生成功能? +#### 1、进入后台,并且你已经【安装了证书】,可在【添加入站】处看到, +![53](./media/53.png) +#### 2、点击【一键配置】,在弹出的页面中【按需选择】去生成协议配置即可, +![54](./media/54.png) +#### 3、直接复制【链接】,导入软件;若后台出现【醒目提示】,那就手动放行【相应端口】, +![55](./media/55.png) +#### 4、若你是用【TG端电报机器人】的【一键配置】生成功能,那直接点击就用, +![56](./media/56.png) +#### 5、选择好自己想要【一键创建】的协议组合类型,点击之后稍作等待, +![57](./media/57.png) +#### 6、TG端【一键配置】创建成功之后,二维码和链接地址机器人会发送给你,如下: +![58](./media/58.png) + +------------ +## 如何在〔X-Panel 项目〕中进行【抽奖游戏】赢奖品? +#### 1、必须绑定好【TG端机器人】,怎么绑定?去看下面“绑定机器人”那部分教程, +#### 2、在【TG端】直接点击【娱乐抽奖】菜单,就会弹出【每日幸运】抽奖游戏, +![59](./media/59.png) +#### 3、点击【进行抽奖】,就可“全凭手气”得到随机的抽奖结果,如下图所示, +![60](./media/60.png) +#### 4、在你【中奖】之后,截完整的“中奖页面”图片给交流群内管理员,即可兑奖。 +![61](./media/61.png) + +------------ +## 安装证书开启https方式实现域名登录访问管理面板/----->>偷自己 +#### PS:如果不需要以上功能或无域名,可以跳过这步;建议申请证书, +##### 1、把自己的域名托管到CF,并解析到自己VPS的IP,不要开启【小云朵】, +##### 2、如果要申请安装证书并每3个月【自动续签】证书,请确保80和443端口是放行打开的, +##### 3、输入x-ui命令进入面板管理脚本,通过选择第【18】选项去进行安装,若使用 18 ---> 1方式不能申请下来,那就用 18--->5 备用方式去申请,或者也可用【19选项】去申请, +![74](./media/74.png) +##### 4、首先输入解析好的【域名】进行验证,然后可以默认用【80端口】,直接回车申请, +![27](./media/27.png) +##### 5、进入后台【面板设置】—–>【常规】中,会看到脚本已经自动填好了证书公钥、私钥路径, +##### 6、点击左上角的【保存】和【重启面板】,即可用自己域名进行登录管理;也可按照后续方法实现【自己偷自己】。 + +------------ +## 登录面板进行【常规】设置 +### 特别是如果在安装过程中,全部都是默认【回车键】安装的话,用户名/密码/访问路径是随机的,而面板监听端口默认是:13688,可自行进入面板更改, +##### 1、填写自己想要设置的【面板监听端口】,并去登录SSH放行, +##### 2、更改自己想要设置的【面板登录访问路径】,后续加上路径登录访问, +![25](./media/25.png) +##### 3、其他:安全设定和电报机器人等配置,可自行根据需求去进行设置, +##### 4、强烈建议配置电报机器人,使用【TG端】可方便远程接管 VPS 服务器, +![26](./media/26.png) +##### 5、面板设置【改动保存】之后,都需要点击左上角【重启面板】,才能生效。 +#### PS:若你在正确完成了上述步骤之后,你没有安装证书的情况下,去用【ssh转发】的方式却不能访问面板,那请检查一下是不是你的浏览器自动默认开启了https模式,需要手动调整一下改成http方式,把“s”去掉,即可访问成功;或查看一下是不是对应的端口被占用? + +------------ +## 创建【入站协议】和添加【客户端】,并测试上网 +##### 1、点击左边【入站列表】,然后【添加入站】,传输方式保持【TCP】不变,尽量选择主流的vless+reality+vision协议组合, +![23](./media/23.png) +##### 2、在选择reality安全选项时,偷的域名可以使用默认的,要使用其他的,请替换尽量保持一致就行,比如Apple、Yahoo,VPS所在地区的旅游、学校网站等;如果要实现【偷自己】,请参看后续【如何偷自己】的说明部分;而私钥/公钥部分,可以直接点击下方的【Get New Cert】获取一个随机的, +##### 3、在创建reality安全选项过程中,至于其他诸如:PROXY Protocol,HTTP 伪装,TPROXY,External Proxy等等选项,若无特殊要求,保持默认设置即可,不用去动它们, +![24](./media/24.png) +##### 4、创建好入站协议之后,默认只有一个客户端,可根据自己需求继续添加;重点:并编辑客户端,选择【Flow流控】为xtls-rprx-vision, +![19](./media/19.png) +##### 5、其他:流量限制,到期时间,客户TG的ID等选项根据自己需求填写, +![4](./media/4.png) +##### 6、一定要放行端口之后,确保端口能够ping通,再导入软件, +##### 7、点击二维码或者复制链接导入到v2rayN等软件中进行测试。 + +------------ +## 备份与恢复/迁移数据库(以Debian系统为例) +#### 一、备份:通过配置好电报管理机器人,可点击管理机器人的“相应菜单按钮”获取【备份配置】文件,有x-ui.db和config.json两个文件,可自行下载保存到自己电脑里面, +![14](./media/14.png) +#### 二、搭建:在新的VPS中全新安装好〔X-Panel面板〕,通过脚本放行之前配置的所有端口,一次性放行多个端口请用【英文逗号】分隔, +#### 三、若需要安装证书,则提前把域名解析到新的VPS对应的IP,并且去输入x-ui选择第【18】选项去安装,并记录公钥/私钥的路径,无域名则跳过这一步, +#### 四、恢复:SSH登录服务器找到/etc/x-ui/x-ui.db和/usr/local/x-ui/bin/config.json文件位置,上传之前的两个备份文件,进行覆盖, +![12](./media/12.png) +##### PS:把之前通过自动备份下载得到的两个文件上传覆盖掉旧文件,重启〔X-Panel面板〕即可【迁移成功】;即使迁移过程中出现问题,你是有备份文件的,不用担心,多试几次。 +![13](./media/13.png) +#### 五、若安装了证书,去核对/更改一下证书的路径,一般是同一个域名的话,位置在:/root/cert/域名/fullchain.pem,路径是相同的就不用更改, +#### 六、重启面板/重启服务器,让上述步骤生效即可,这时可以看到所有配置都是之前自己常用的,包括面板用户名、密码,入站、客户端,电报机器人配置等。 +#### PS:若您使用的是【Pro版】,则可直接使用:“创建数据快照 + 远程急救还原”功能,对面板数据库和配置文件进行操作。 + +------------ +## 安装完成后如何设置调整成【中文界面】? +- 方法一:通过管理后台【登录页面】调整,登录时可以选择,如下图: +![15](./media/15.png) +- 方法二:通过在管理后台-->【面板设置】中去选择设置,如下图: +![16](./media/16.png) +- 【TG机器人】设置中文:通过在管理后台-->【面板设置】-->【机器人配置】中去选择设置,并建议打开数据库备份和登录通知,如下图: +![17](./media/17.png) + +------------ +## 用〔X-Panel面板〕如何实现【自己偷自己】? +- 其实很简单,只要你为面板设置了证书, +- 开启了HTTPS登录,就可以将〔X-Panel面板〕自身作为Web Server, +- 无需Nginx等,这里给一个示例: +- 其中目标网站(Dest)请填写面板监听端口, +- 可选域名(SNI)填写面板登录域名, +- 如果您使用其他web server(如nginx)等, +- 将目标网站改为对应监听端口也可。 +- 需要说明的是,如果您处于白名单地区,自己“偷”自己并不适合你; +- 其次,可选域名一项实际上可以填写任意SNI,只要客户端保持一致即可,不过并不推荐这样做。 +- 配置方法如下图所示: +![18](./media/18.png) + +------------ +## 〔子域名〕被墙针对特征 +#### 网络表现: +##### 1、可以Ping通域名和IP地址, +##### 2、子域名无法打开〔X-Panel面板〕管理界面, +##### 3、什么都正常就是不能上网; + +#### 问题: +##### 你的子域名被墙针对了:无法上网! + +#### 解决方案: +##### 1、更换为新的子域名, +##### 2、解析新的子域名到VPS的IP, +##### 3、重新去安装新证书, +##### 4、重启〔X-Panel面板〕和服务器, +##### 5、重新去获取链接并测试上网。 +#### PS:若通过以上步骤还是不能正常上网,则重装VPS服务器OS系统,以及〔X-Panel面板〕全部重新安装,之后就正常了! + +------------ +## 用〔X-Panel面板〕如何开启【设备限制】功能? +##### 1、进入后台在【添加入站】的时候,弹出来的页面就能有【设备数量】输入框, +![37](./media/37.png) +##### 2、通过步骤1设置完成后,在后台的【入站列表】页面也有对应的同步数据显示。 +![38](./media/38.png) +##### 3、具体要查看【设备限制】功能的封禁情况,就进入〔X-Panel面板〕后台用日志查看。 +![39](./media/39.png) +##### 4、以下图片里面,详细阐述了我们的〔设备限制〕功能,跟3X-UI原本就有的〔IP Limit〕之间的区别对比。 +![40](./media/40.png) + +------------ +## 用〔X-Panel面板〕如何开启【独立限速】功能? +##### 1、进入后台在【添加入站】的时候,弹出来的页面在【客户/用户】那里就能输入具体数字, +![47](./media/47.png) +##### 2、也可以在添加好一个【入站】之后,点击【添加客户端】去找到【独立限速】输入框, +![48](./media/48.png) +##### 3、当你想批量一次性创建多个【客户/用户】时,同样可以使用【独立限速】功能, +![49](./media/49.png) +![50](./media/50.png) +##### 4、若后期你想更改某一个【客户端/用户】的限制速率,那就自己先找到,然后点击【编辑】即可, +![51](./media/51.png) +##### 5、具体要查看【独立限速】功能的启用情况,就进入〔X-Panel面板〕后台用日志查看。 +![52](./media/52.png) + +------------ +## 用〔X-Panel面板〕如何开启【每月流量自动重置】? +##### 1、进入后台的【入站列表】,选择需要设置的【客户端】, +![29](./media/29.png) +##### 2、要注意是编辑【入站】下面的【客户端】,才会有效果, +##### 2、并不是编辑【入站】,所以不要弄错对象,如下图所示: +![28](./media/28.png) + + +------------ +## 在自己的VPS服务器部署【订阅转换】功能 +### 如何把vless/vmess等协议转换成Clash/Surge等软件支持的格式? +##### 1、进入脚本输入x-ui命令调取面板,选择第【25】选项安装订阅转换模块, +##### 2、等待安装【订阅转换】成功之后,访问地址:https://你的域名:15268 , +![41](./media/41.png) +##### 3、因为在转换过程中需要调取后端API,所以请确保端口 8000 和 15268 是打开放行的, +##### 4、直接复制脚本中提供的【登录地址】,进入后台,点击【节点列表】,如下图: +![42](./media/42.png) +##### 5、接下来点击左边侧边栏的【订阅列表】去【添加订阅】,按照下图中去操作, +![43](./media/43.png) +##### 6、最后一步,点击【客户端】,即可导入Clash等软件中使用。 +![44](./media/44.png) +![45](./media/45.png) + +------------ +## 常见的翻墙软件/工具: +- [1、Windows系统v2rayN:https://github.com/2dust/v2rayN](https://github.com/2dust/v2rayN) +- [2、安卓手机版【v2rayNG】:https://github.com/2dust/v2rayNG](https://github.com/2dust/v2rayNG) +- [3、苹果手机IOS【小火箭】:https://apple02.com/(自己购买)](https://apple02.com/) +- [4、苹果MacOS电脑【Clash Verge】:https://github.com/clash-verge-rev/clash-verge-rev/releases](https://github.com/clash-verge-rev/clash-verge-rev/releases) + [或v2rayN:https://github.com/2dust/v2rayN](https://github.com/2dust/v2rayN) + +------------ +## 如何保护自己的IP不被墙被封? +##### 1、使用的代理协议要安全,加密是必备,推荐使用vless+reality+vision协议组合, +##### 2、因为有时节点会共享,在不同的地区,多个省份之间不要共同连接同一个IP, +##### 3、连接同一个IP就算了,不要同一个端口,不要同IP+同端口到处漫游,要分开, +##### 4、同一台VPS,不要在一天内一直大流量去下载东西使用,不要流量过高要切换, +##### 5、创建【入站协议】的时候,尽量用【高位端口】,比如40000--65000之间的端口号。 +#### 提醒:为什么在特殊时期,比如:两会,春节等被封得最严重最惨? +##### 尼玛同一个IP+同一个端口号,多个省份去漫游,跟开飞机场一样!不封你,封谁的IP和端口? +#### 总结:不要多终端/多省份/多个朋友/共同使用同一个IP和端口号!使用〔X-Panel面板〕多创建几个【入站】, +#### 多做几条备用,各用各的!各行其道才比较安全!GFW的思维模式是干掉机场,机场的特征个人用户不要去沾染,自然IP就保护好了。 + +------------ +## SSL 认证 + +
+ 点击查看 SSL 认证 + +### ACME + +要使用 ACME 管理 SSL 证书: + +1. 确保您的域名已正确解析到服务器, +2. 输入“x-ui”命令并选择“SSL 证书管理”, +3. 您将看到以下选项: + + - **获取证书** ----获取SSL证书 + - **吊销证书** ----吊销现有的SSL证书 + - **续签证书** ----强制续签SSL证书 + - **显示所有证书** ----显示服务器中所有能用的证书 + - **设置面板证书路径** ----指定面板要使用的证书 + + +### Certbot + +安装和使用 Certbot: + +```sh +apt-get install certbot -y +certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com +certbot renew --dry-run +``` + +### Cloudflare + +管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件: + +- Cloudflare 邮箱地址 +- Cloudflare Global API Key +- 域名已通过 cloudflare 解析到当前服务器 + +**如何获取 Cloudflare全局API密钥:** + +1. 在终端中输入“x-ui”命令,然后选择“CF SSL 证书”。 +2. 访问链接: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens). +3. 点击“查看全局 API 密钥”(如下图所示): + ![](media/APIKey1.PNG) +4. 您可能需要重新验证您的帐户。之后,将显示 API 密钥(请参见下面的屏幕截图): + ![](media/APIKey2.png) + +使用时,只需输入您的“域名”、“电子邮件”和“API KEY”即可。示意图如下: + ![](media/DetailEnter.png) + + +
+ +------------ +## 手动安装 & 升级 + +
+ 点击查看 手动安装 & 升级 + +#### 使用 + +1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + s390x) echo 's390x' ;; + *) XUI_ARCH="amd64" ;; +esac + + +wget https://github.com/xeefei/x-panel/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz +``` + +2. 下载压缩包后,执行以下命令安装或升级 x-ui: + +```sh +ARCH=$(uname -m) +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + s390x) echo 's390x' ;; + *) XUI_ARCH="amd64" ;; +esac + +cd /root/ +rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui +tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz +chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh +cp x-ui/x-ui.sh /usr/bin/x-ui +cp -f x-ui/x-ui.service /etc/systemd/system/ +mv x-ui/ /usr/local/ +systemctl daemon-reload +systemctl enable x-ui +systemctl restart x-ui +``` + +
+ +------------ +## 通过Docker安装 + +
+ 点击查看 通过Docker安装 + +#### 使用 + + +1. **安装Docker** + + ```sh + bash <(curl -sSL https://get.docker.com) + ``` + + +2. **克隆项目仓库** + + ```sh + git clone https://github.com/xeefei/x-panel.git + cd x-panel + ``` + +3. **启动服务**: + + ```sh + docker compose up -d + ``` + + 添加 ```--pull always``` 标志使 docker 在拉取更新的镜像时自动重新创建容器。有关更多信息,请参阅:https://docs.docker.com/reference/cli/docker/container/run/#pull + + **或** + + ```sh + docker run -itd \ + -e XRAY_VMESS_AEAD_FORCED=false \ + -v $PWD/db/:/etc/x-ui/ \ + -v $PWD/cert/:/root/cert/ \ + --network=host \ + --restart=unless-stopped \ + --name x-panel \ + ghcr.io/xeefei/x-panel:latest + ``` + +4. **更新至最新版本** + + ```sh + cd x-panel + docker compose down + docker compose pull x-panel + docker compose up -d + ``` + +5. **从Docker中删除x-panel ** + + ```sh + docker stop x-panel + docker rm x-panel + cd -- + rm -r x-panel + ``` + +
+ +------------ +## 建议使用的操作系统 + +- Ubuntu 20.04+ +- Debian 11+ +- CentOS 8+ +- OpenEuler 22.03+ +- Fedora 36+ +- Arch Linux +- Manjaro +- Armbian +- AlmaLinux 8.0+ +- Rocky Linux 8+ +- Oracle Linux 8+ +- OpenSUSE Tubleweed +- Amazon Linux 2023 + +------------ +## 支持的架构和设备 +
+ 点击查看 支持的架构和设备 + +我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构: + +- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。 + +- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。 + +- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。 + +- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构,它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。 + +- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备,虽然不太普遍,但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。 + +- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。 +
+ +------------ +## Languages + +- English(英语) +- Farsi(伊朗语) +- Simplified Chinese(简体中文) +- Traditional Chinese(繁体中文) +- Russian(俄语) +- Vietnamese(越南语) +- Spanish(西班牙语) +- Indonesian (印度尼西亚语) +- Ukrainian(乌克兰语) +- Turkish(土耳其语) +- Português (葡萄牙语) + +------------ +## 项目特点 + +- 系统状态查看与监控 +- 可搜索所有入站和客户端信息 +- 深色/浅色主题随意切换 +- 支持多用户和多协议 +- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard +- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY +- 流量统计、流量限制、过期时间限制 +- 可自定义的 Xray配置模板 +- 支持HTTPS访问面板(自备域名+SSL证书) +- 支持一键式SSL证书申请和自动续签证书 +- 更多高级配置项目请参考面板去进行设定 +- 修复了 API 路由(用户设置将使用 API 创建) +- 支持通过面板中提供的不同项目更改配置。 +- 支持从面板导出/导入数据库 + +## 默认面板设置 + +
+ + 点击查看 默认设置 + + ### 默认信息 + +- **端口** + - 13688 +- **用户名 & 密码 & 访问路径** + - 当您跳过设置时,这些信息会随机生成, + - 您也可以在安装的时候自定义访问路径。 +- **数据库文件路径:** + - /etc/x-ui/x-ui.db +- **Xray 配置文件路径:** + - /usr/local/x-ui/bin/config.json +- **面板链接(有SSL):** + - https://你的域名:13688/访问路径/panel + +
+ +------------ +## [WARP 配置](https://gitlab.com/fscarmen/warp) + +
+ 点击查看 WARP 配置 + +#### 使用 + +**对于版本 `v2.1.0` 及更高版本:** + +WARP 是内置的,无需额外安装;只需在面板中打开必要的配置即可。 + +**如果要在 v2.1.0 之前使用 WARP 路由**,请按照以下步骤操作: + +**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap + + - **Account Type (free, plus, team):** Choose the appropriate account type. + - **Enable/Disable WireProxy:** Toggle WireProxy on or off. + - **Uninstall WARP:** Remove the WARP application. + +**2.** 如果您已经安装了 warp,您可以使用以下命令卸载: + + ```sh + warp u + ``` + +**3.** 在面板中打开您需要的配置 + + 配置: + + - Block Ads + - Route Google, Netflix, Spotify, and OpenAI (ChatGPT) traffic to WARP + - Fix Google 403 error + + +
+ +------------ +## IP 限制 + +
+ 点击查看 IP 限制 + +#### 使用 + +**注意:** 使用 IP 隧道时,IP 限制无法正常工作。 + +- 对于 `v1.6.1`之前的版本 : + + - IP 限制 已被集成在面板中。 + +- 对于 `v1.7.0` 以及更新的版本: + + - 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件: + + 1. 使用面板内置的 `x-ui` 指令 + 2. 选择 `IP Limit Management`. + 3. 根据您的需要选择合适的选项。 + + - 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。 + + ```sh + "log": { + "access": "./access.log", + "dnsLog": false, + "loglevel": "warning" + }, + ``` + - 您需要在Xray配置中手动设置〔访问日志〕的路径。 + +
+ +------------ +## Telegram 机器人 + +
+ 点击查看 Telegram 机器人 + +#### 使用 + +Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括: + +- 电报令牌 +- 管理员聊天 ID +- 通知时间(cron 语法) +- 到期日期通知 +- 流量上限通知 +- 数据库备份 +- CPU 负载通知 + + +**参考:** + +- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知 +- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知 +- `@hourly` - 每小时通知 +- `@daily` - 每天通知 (00:00) +- `@weekly` - 每周通知 +- `@every 8h` - 每8小时通知 + +### Telegram Bot 功能 + +- 定期报告 +- 登录通知 +- CPU 阈值通知 +- 提前报告的过期时间和流量阈值 +- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单 +- 支持使用UUID(VMESS/VLESS)或密码(TROJAN)搜索报文流量报告 - 匿名 +- 基于菜单的机器人 +- 通过电子邮件搜索客户端(仅限管理员) +- 检查所有入库 +- 检查服务器状态 +- 检查耗尽的用户 +- 根据请求和定期报告接收备份 +- 多语言机器人 + +### 注册 Telegram bot + +- 与 [Botfather](https://t.me/BotFather) 对话: + ![Botfather](./media/botfather.png) + +- 使用 /newbot 创建新机器人:你需要提供机器人名称以及用户名,注意名称中末尾要包含“bot” + ![创建机器人](./media/newbot.png) + +- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。 + ![令牌](./media/token.png) + +- 输入您的面板并配置 Telegram 机器人设置,如下所示: + ![面板设置](./media/panel-bot-config.png) + +在输入字段编号 3 中输入机器人令牌。 +在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可) + +- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot), 启动机器人,它会给你 Telegram 用户 ID。 +![用户 ID](./media/user-id.png) + +
+ +------------ +## API 路由 + +
+ 点击查看 API 路由 + +#### 使用 + +- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录 +- `/panel/api/inbounds` 以下操作的基础: + +| 方法 | 路径 | 操作 | +| :----: | ---------------------------------- | ------------------------------------------- | +| `GET` | `"/list"` | 获取所有入站 | +| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id | +| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 | +| `GET` | `"/getClientTrafficsById/:id"` | 通过用户ID获取客户端流量 | +| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 | +| `POST` | `"/add"` | 添加入站 | +| `POST` | `"/del/:id"` | 删除入站 | +| `POST` | `"/update/:id"` | 更新入站 | +| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 | +| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 | +| `POST` | `"/addClient"` | 将客户端添加到入站 | +| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 | +| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 | +| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 | +| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 | +| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 | +| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) | +| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) | + +- 使用`clientId` 项应该填写下列数据: + +- `client.id` for VMESS and VLESS +- `client.password` for TROJAN +- `client.email` for Shadowsocks + + + +- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm) + +- [Run In Postman](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9) +
+ +------------ +## 环境变量 + +
+ 点击查看 环境变量 + +#### Usage + +| 变量 | Type | 默认 | +| -------------- | :--------------------------------------------: | :------------ | +| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` | +| XUI_DEBUG | `boolean` | `false` | +| XUI_BIN_FOLDER | `string` | `"bin"` | +| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` | +| XUI_LOG_FOLDER | `string` | `"/var/log"` | + +例子: + +```sh +XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go +``` + +
+ +------------ +## 预览 + +![1](./media/1.png) +![2](./media/2.png) +![3](./media/3.png) +![5](./media/5.png) +![6](./media/6.png) +![7](./media/7.png) + +------------ +## 广告赞助 +- 如果你觉得本项目对你有用,而且你也恰巧有这方面的需求,你也可以选择通过我的购买链接赞助我。 +- [1、搬瓦工GIA高端线路,仅推荐购买GIA套餐:https://bandwagonhost.com/aff.php?aff=75015](https://bandwagonhost.com/aff.php?aff=75015) +- [2、Dmit高端GIA线路:https://www.dmit.io/aff.php?aff=9326](https://www.dmit.io/aff.php?aff=9326) +- [3、Gomami亚太顶尖优化线路:https://gomami.io/aff.php?aff=174](https://gomami.io/aff.php?aff=174) +- [4、ISIF优质亚太优化线路:https://cloud.isif.net/login?affiliation_code=333](https://cloud.isif.net/login?affiliation_code=333) +- [5、ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选:https://my.zorocloud.com/aff.php?aff=1072](https://my.zorocloud.com/aff.php?aff=1072) +- [6、三网直连 IEPL / IPLC 直播流量转发:https://idc333.top/#register/BCUZXNELNO](https://idc333.top/#register/BCUZXNELNO) +- [7、Bagevm优质落地鸡(原生IP全解锁):https://www.bagevm.com/aff.php?aff=754](https://www.bagevm.com/aff.php?aff=754) +- [8、白丝云【4837线路】实惠量大管饱:https://cloudsilk.io/aff.php?aff=706](https://cloudsilk.io/aff.php?aff=706) +- [9、RackNerd极致性价比机器:https://my.racknerd.com/aff.php?aff=15268&pid=912](https://my.racknerd.com/aff.php?aff=15268&pid=912) + +------------ +## 特别感谢 + +- [MHSanaei](https://github.com/MHSanaei/) +- [alireza0](https://github.com/alireza0/) +- [FranzKafkaYu](https://github.com/FranzKafkaYu/) +- [vaxilu](https://github.com/vaxilu/) + +------------ +## 致谢 + +- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._ +- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ + +------------ +## Star 趋势 + +[![Stargazers over time](https://starchart.cc/xeefei/x-panel.svg)](https://starchart.cc/xeefei/x-panel) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..d5fe65f --- /dev/null +++ b/config/config.go @@ -0,0 +1,144 @@ +package config + +import ( + _ "embed" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" +) + +//go:embed version +var version string + +//go:embed name +var name string + +type LogLevel string + +const ( + Debug LogLevel = "debug" + Info LogLevel = "info" + Notice LogLevel = "notice" + Warn LogLevel = "warn" + Error LogLevel = "error" +) + +func GetVersion() string { + return strings.TrimSpace(version) +} + +func GetName() string { + return strings.TrimSpace(name) +} + +func GetLogLevel() LogLevel { + if IsDebug() { + return Debug + } + logLevel := os.Getenv("XUI_LOG_LEVEL") + if logLevel == "" { + return Info + } + return LogLevel(logLevel) +} + +func IsDebug() bool { + return os.Getenv("XUI_DEBUG") == "true" +} + +func GetBinFolderPath() string { + binFolderPath := os.Getenv("XUI_BIN_FOLDER") + if binFolderPath == "" { + binFolderPath = "bin" + } + return binFolderPath +} + +func getBaseDir() string { + exePath, err := os.Executable() + if err != nil { + return "." + } + exeDir := filepath.Dir(exePath) + exeDirLower := strings.ToLower(filepath.ToSlash(exeDir)) + if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") { + wd, err := os.Getwd() + if err != nil { + return "." + } + return wd + } + return exeDir +} + +func GetDBFolderPath() string { + dbFolderPath := os.Getenv("XUI_DB_FOLDER") + if dbFolderPath != "" { + return dbFolderPath + } + if runtime.GOOS == "windows" { + return getBaseDir() + } + return "/etc/x-ui" +} + +func GetDBPath() string { + return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) +} + +func GetLogFolder() string { + logFolderPath := os.Getenv("XUI_LOG_FOLDER") + if logFolderPath != "" { + return logFolderPath + } + if runtime.GOOS == "windows" { + return filepath.Join(".", "log") + } + return "/var/log" +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + return out.Sync() +} + +func init() { + if runtime.GOOS != "windows" { + return + } + if os.Getenv("XUI_DB_FOLDER") != "" { + return + } + oldDBFolder := "/etc/x-ui" + oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName()) + newDBFolder := GetDBFolderPath() + newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName()) + _, err := os.Stat(newDBPath) + if err == nil { + return // new exists + } + _, err = os.Stat(oldDBPath) + if os.IsNotExist(err) { + return // old does not exist + } + _ = copyFile(oldDBPath, newDBPath) // ignore error +} diff --git a/config/name b/config/name new file mode 100644 index 0000000..2f44469 --- /dev/null +++ b/config/name @@ -0,0 +1 @@ +x-ui diff --git a/config/version b/config/version new file mode 100644 index 0000000..e2a0a34 --- /dev/null +++ b/config/version @@ -0,0 +1 @@ +26.4.25 diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..e91d541 --- /dev/null +++ b/database/db.go @@ -0,0 +1,218 @@ +package database + +import ( + "bytes" + "io" + "io/fs" + "log" + "os" + "path" + "slices" + "time" + + "x-ui/config" + "x-ui/database/model" + "x-ui/util/crypto" + "x-ui/xray" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +const ( + defaultUsername = "admin" + defaultPassword = "admin" +) + +func initModels() error { + models := []any{ + &model.User{}, + &model.Inbound{}, + &model.OutboundTraffics{}, + &model.Setting{}, + &model.InboundClientIps{}, + &xray.ClientTraffic{}, + &model.HistoryOfSeeders{}, + &LinkHistory{}, // 把 LinkHistory 表也迁移 + &model.LotteryWin{}, // 新增 抽奖游戏LotteryWin 数据模型 + } + for _, model := range models { + if err := db.AutoMigrate(model); err != nil { + log.Printf("Error auto migrating model: %v", err) + return err + } + } + return nil +} + +func initUser() error { + empty, err := isTableEmpty("users") + if err != nil { + log.Printf("Error checking if users table is empty: %v", err) + return err + } + if empty { + hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword) + + if err != nil { + log.Printf("Error hashing default password: %v", err) + return err + } + + user := &model.User{ + Username: defaultUsername, + Password: hashedPassword, + } + return db.Create(user).Error + } + return nil +} + +func runSeeders(isUsersEmpty bool) error { + empty, err := isTableEmpty("history_of_seeders") + if err != nil { + log.Printf("Error checking if users table is empty: %v", err) + return err + } + + if empty && isUsersEmpty { + hashSeeder := &model.HistoryOfSeeders{ + SeederName: "UserPasswordHash", + } + return db.Create(hashSeeder).Error + } else { + var seedersHistory []string + db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory) + + if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty { + var users []model.User + db.Find(&users) + + for _, user := range users { + hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password) + if err != nil { + log.Printf("Error hashing password for user '%s': %v", user.Username, err) + return err + } + db.Model(&user).Update("password", hashedPassword) + } + + hashSeeder := &model.HistoryOfSeeders{ + SeederName: "UserPasswordHash", + } + return db.Create(hashSeeder).Error + } + } + + return nil +} + +func isTableEmpty(tableName string) (bool, error) { + var count int64 + err := db.Table(tableName).Count(&count).Error + return count == 0, err +} + +func InitDB(dbPath string) error { + dir := path.Dir(dbPath) + err := os.MkdirAll(dir, fs.ModePerm) + if err != nil { + return err + } + + var gormLogger logger.Interface + + if config.IsDebug() { + gormLogger = logger.Default + } else { + gormLogger = logger.Discard + } + + c := &gorm.Config{ + Logger: gormLogger, + } + db, err = gorm.Open(sqlite.Open(dbPath), c) + if err != nil { + return err + } + + if err := initModels(); err != nil { + return err + } + + isUsersEmpty, err := isTableEmpty("users") + + if err := initUser(); err != nil { + return err + } + return runSeeders(isUsersEmpty) +} + +func CloseDB() error { + if db != nil { + sqlDB, err := db.DB() + if err != nil { + return err + } + return sqlDB.Close() + } + return nil +} + +func GetDB() *gorm.DB { + return db +} + +func IsNotFound(err error) bool { + return err == gorm.ErrRecordNotFound +} + +func IsSQLiteDB(file io.ReaderAt) (bool, error) { + signature := []byte("SQLite format 3\x00") + buf := make([]byte, len(signature)) + _, err := file.ReadAt(buf, 0) + if err != nil { + return false, err + } + return bytes.Equal(buf, signature), nil +} + +func Checkpoint() error { + // Update WAL + err := db.Exec("PRAGMA wal_checkpoint;").Error + if err != nil { + return err + } + return nil +} + +// HasUserWonToday 检查指定用户今天是否已经中过奖 +// 〔中文注释〕:【修正】将 gorm.DB() 替换为全局变量 db +func HasUserWonToday(userID int64) (bool, error) { + now := time.Now() + startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + endOfDay := startOfDay.Add(24 * time.Hour) + + var count int64 + // 在 lottery_wins 表中查找符合条件(用户ID匹配且中奖日期在今天之内)的记录数量 + err := db.Model(&model.LotteryWin{}).Where("user_id = ? AND win_date >= ? AND win_date < ?", userID, startOfDay, endOfDay).Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// RecordUserWin 记录用户的中奖信息 +// 〔中文注释〕:【修正】将 gorm.DB() 替换为全局变量 db +func RecordUserWin(userID int64, prize string) error { + winRecord := &model.LotteryWin{ + UserID: userID, + Prize: prize, + WinDate: time.Now(), + } + // 在 lottery_wins 表中创建一条新的记录 + return db.Create(winRecord).Error +} diff --git a/database/history.go b/database/history.go new file mode 100644 index 0000000..f86a977 --- /dev/null +++ b/database/history.go @@ -0,0 +1,65 @@ +package database + +import ( + "time" + "gorm.io/gorm" // 【中文注释】: 确保 gorm 被导入,以便在函数签名中使用 +) + +// LinkHistory GORM aodel for link_history table +type LinkHistory struct { + Id int `gorm:"primaryKey"` + Type string `gorm:"type:varchar(255);not null"` + Link string `gorm:"type:text;not null"` + CreatedAt time.Time `gorm:"not null"` +} + +// AddLinkHistory 在一个事务中添加新链接记录并修剪旧记录。 +// 它确保了操作的原子性:所有更改要么全部应用,要么全部回滚。 +func AddLinkHistory(record *LinkHistory) error { + // 【核心修正】: 使用 GORM 的事务功能来包装所有的数据库写入和删除操作。 + // 这样可以确保数据的一致性。 + return db.Transaction(func(tx *gorm.DB) error { + // 1. 添加新记录 + // 【重要】: 在事务内部,必须使用 tx 变量,而不是全局的 db 变量。 + if err := tx.Create(record).Error; err != nil { + return err // 如果这里出错,事务将自动回滚 + } + + // 2. 修剪旧记录,仅保留最近的 10 条 + var count int64 + // 【重要】: 使用 tx 进行计数 + if err := tx.Model(&LinkHistory{}).Count(&count).Error; err != nil { + return err + } + + if count > 10 { + limit := int(count) - 10 + var recordsToDelete []LinkHistory + // 【重要】: 使用 tx 查找要删除的记录 + if err := tx.Order("created_at asc").Limit(limit).Find(&recordsToDelete).Error; err != nil { + return err + } + if len(recordsToDelete) > 0 { + // 【重要】: 使用 tx 删除记录 + if err := tx.Delete(&recordsToDelete).Error; err != nil { + return err + } + } + } + + // 【核心修正】: 从此函数中移除了 Checkpoint() 调用。 + // 事务成功后返回 nil,GORM 会自动提交事务。 + return nil + }) +} + + +// GetLinkHistory retrieves the 10 most recent link records +func GetLinkHistory() ([]*LinkHistory, error) { + var histories []*LinkHistory + err := db.Order("created_at desc").Limit(10).Find(&histories).Error + if err != nil { + return nil, err + } + return histories, nil +} diff --git a/database/model/lottery_win.go b/database/model/lottery_win.go new file mode 100644 index 0000000..6e6edc6 --- /dev/null +++ b/database/model/lottery_win.go @@ -0,0 +1,11 @@ +package model + +import "time" + +// LotteryWin 用于记录用户的中奖历史 +type LotteryWin struct { + ID int64 `gorm:"primaryKey"` + UserID int64 `gorm:"index"` // Telegram 用户 ID + Prize string // 奖品等级,如 "一等奖" + WinDate time.Time // 中奖日期 +} \ No newline at end of file diff --git a/database/model/model.go b/database/model/model.go new file mode 100644 index 0000000..f54db7b --- /dev/null +++ b/database/model/model.go @@ -0,0 +1,124 @@ +package model + +import ( + "fmt" + + "x-ui/util/json_util" + "x-ui/xray" +) + +type Protocol string + +const ( + VMESS Protocol = "vmess" + VLESS Protocol = "vless" + Tunnel Protocol = "tunnel" + HTTP Protocol = "http" + Trojan Protocol = "trojan" + Shadowsocks Protocol = "shadowsocks" + Socks Protocol = "socks" + WireGuard Protocol = "wireguard" +) + +type User struct { + Id int `json:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username"` + Password string `json:"password"` +} + +type Inbound struct { + Id int `json:"id" form:"id" gorm:"primaryKey"` + UserId int `json:"-"` + Up int64 `json:"up" form:"up"` + Down int64 `json:"down" form:"down"` + Total int64 `json:"total" form:"total"` + AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` + Remark string `json:"remark" form:"remark"` + Enable bool `json:"enable" form:"enable"` + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` + + // 中文注释: 新增设备限制字段,用于存储每个入站的设备数限制。 + // gorm:"column:device_limit;default:0" 定义了数据库中的字段名和默认值。 + DeviceLimit int `json:"deviceLimit" form:"deviceLimit" gorm:"column:device_limit;default:0"` + + ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` + + // config part + Listen string `json:"listen" form:"listen"` + Port int `json:"port" form:"port"` + Protocol Protocol `json:"protocol" form:"protocol"` + Settings string `json:"settings" form:"settings"` + StreamSettings string `json:"streamSettings" form:"streamSettings"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + Sniffing string `json:"sniffing" form:"sniffing"` +} + +type OutboundTraffics struct { + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + Up int64 `json:"up" form:"up" gorm:"default:0"` + Down int64 `json:"down" form:"down" gorm:"default:0"` + Total int64 `json:"total" form:"total" gorm:"default:0"` +} + +type InboundClientIps struct { + Id int `json:"id" gorm:"primaryKey;autoIncrement"` + ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` + Ips string `json:"ips" form:"ips"` +} + +type HistoryOfSeeders struct { + Id int `json:"id" gorm:"primaryKey;autoIncrement"` + SeederName string `json:"seederName"` +} + +func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { + listen := i.Listen + if listen != "" { + listen = fmt.Sprintf("\"%v\"", listen) + } + return &xray.InboundConfig{ + Listen: json_util.RawMessage(listen), + Port: i.Port, + Protocol: string(i.Protocol), + Settings: json_util.RawMessage(i.Settings), + StreamSettings: json_util.RawMessage(i.StreamSettings), + Tag: i.Tag, + Sniffing: json_util.RawMessage(i.Sniffing), + } +} + +type Setting struct { + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Key string `json:"key" form:"key"` + Value string `json:"value" form:"value"` +} + +type Client struct { + ID string `json:"id"` + Security string `json:"security"` + Password string `json:"password"` + + // 中文注释: 新增“限速”字段,单位 KB/s,0 表示不限速。 + SpeedLimit int `json:"speedLimit" form:"speedLimit"` + + Flow string `json:"flow"` + Email string `json:"email"` + LimitIP int `json:"limitIp"` + TotalGB int64 `json:"totalGB" form:"totalGB"` + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` + Enable bool `json:"enable" form:"enable"` + TgID int64 `json:"tgId" form:"tgId"` + SubID string `json:"subId" form:"subId"` + Comment string `json:"comment" form:"comment"` + Reset int `json:"reset" form:"reset"` + CreatedAt int64 `json:"created_at,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` +} + +type VLESSSettings struct { + Clients []Client `json:"clients"` + Decryption string `json:"decryption"` + Encryption string `json:"encryption"` + Fallbacks []any `json:"fallbacks"` +} diff --git a/dnsjc.sh b/dnsjc.sh new file mode 100644 index 0000000..78dc39e --- /dev/null +++ b/dnsjc.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash + +set -u +set -o pipefail + +COUNT=${COUNT:-10} +TOPN=${TOPN:-30} +SLEEP_META=${SLEEP_META:-0.15} +META_CACHE_DIR="${META_CACHE_DIR:-/tmp/dns_meta_cache}" +mkdir -p "$META_CACHE_DIR" >/dev/null 2>&1 || true + +# deps +require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "missing dependency: $1"; exit 1; }; } +require_cmd curl +require_cmd awk +require_cmd sed +require_cmd tr +require_cmd grep +require_cmd ping + +PING_BIN="ping" +PING6_BIN="ping -6" +if command -v ping6 >/dev/null 2>&1; then + PING6_BIN="ping6" +fi + +# color banner (red) +if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then + RED=$'\033[1;31m' + RESET=$'\033[0m' +else + RED="" + RESET="" +fi +echo "" +printf "%b\n\n" "${RED}〔X-Panel-Pro 面板〕专属 “服务器 DNS 检测”${RESET}" + +# extract valid IPs (IPv4 strict / IPv6 loose) +extract_ips() { + sed -E 's/<[^>]+>/ /g' \ + | tr -c '0-9A-Fa-f:.' '\n' \ + | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^([0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}$)' \ + | awk '!seen[$0]++' \ + | awk -F. ' + $0 ~ /:/ { print; next } + NF==4 { + ok=1 + for(i=1;i<=4;i++){ + if($i !~ /^[0-9]+$/ || $i<0 || $i>255){ ok=0; break } + } + if(ok) print $0 + } + ' +} + +# filter private/reserved IPv4 +is_public_ipv4() { + local ip="$1"; IFS=. read -r a b c d <<<"$ip" || return 1 + if ((a==10)) || ((a==127)) || ((a==192 && b==168)) || ((a==169 && b==254)) \ + || ((a==172 && b>=16 && b<=31)) || ((a==100 && b>=64 && b<=127)) \ + || ((a==0)) || ((a>=224)); then + return 1 + fi + return 0 +} + +# normalize country to slug +normalize_country_to_slug() { + local raw="$1" + local x compact + x=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]') + x=$(printf '%s' "$x" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g') + compact=$(printf '%s' "$x" | sed -E 's/[^a-z0-9]+//g') + case "$compact" in + jp) echo "japan"; return;; + kr|rok) echo "southkorea"; return;; + us|usa) echo "unitedstates"; return;; + uk|gb) echo "unitedkingdom"; return;; + ae|uae) echo "unitedarabemirates"; return;; + hk) echo "hongkong"; return;; + tw) echo "taiwan"; return;; + cn) echo "china"; return;; + de) echo "germany"; return;; + fr) echo "france"; return;; + it) echo "italy"; return;; + es) echo "spain"; return;; + ru) echo "russia"; return;; + sg) echo "singapore"; return;; + th) echo "thailand"; return;; + vn) echo "vietnam"; return;; + ph) echo "philippines"; return;; + id) echo "indonesia"; return;; + my) echo "malaysia"; return;; + au) echo "australia"; return;; + nz) echo "newzealand"; return;; + nl) echo "netherlands"; return;; + be) echo "belgium"; return;; + pl) echo "poland"; return;; + cz) echo "czechia"; return;; + ch) echo "switzerland"; return;; + at) echo "austria"; return;; + se) echo "sweden"; return;; + no) echo "norway"; return;; + fi) echo "finland"; return;; + pt) echo "portugal"; return;; + ro) echo "romania"; return;; + hu) echo "hungary"; return;; + sk) echo "slovakia"; return;; + si) echo "slovenia"; return;; + gr) echo "greece"; return;; + ie) echo "ireland"; return;; + mx) echo "mexico"; return;; + ca) echo "canada"; return;; + br) echo "brazil"; return;; + ar) echo "argentina"; return;; + cl) echo "chile"; return;; + za) echo "southafrica"; return;; + esac + x=$(printf '%s' "$x" | sed -E 's/[[:space:]]+//g') + echo "$x" +} + +# fetch IPs from publicdnsserver page +fetch_country_ips() { + local slug="$1" + local url="https://publicdnsserver.com/${slug}/" + local html + html=$(curl -sL --max-time 15 "$url" || true) + [ -z "$html" ] && { echo ""; return 0; } + printf '%s' "$html" | extract_ips +} + +# metadata (ip-api) with 1h cache +get_meta_json() { + local ip="$1" + local cache="$META_CACHE_DIR/$ip.json" + local epoch mtime age + if [ -s "$cache" ]; then + epoch=$(date +%s) + if mtime=$(stat -c %Y "$cache" 2>/dev/null); then + age=$((epoch - mtime)) + if [ "$age" -lt 3600 ]; then + cat "$cache"; return 0 + fi + fi + fi + local resp + resp=$(curl -s --max-time 5 "http://ip-api.com/json/$ip?fields=status,message,country,city,as,asname,org,isp,query") + if [ -n "$resp" ]; then + printf '%s' "$resp" >"$cache" 2>/dev/null || true + printf '%s' "$resp" + else + printf '{"status":"fail","country":"","city":"","as":"","asname":"","org":"","isp":"","query":"%s"}' "$ip" + fi + sleep "$SLEEP_META" +} + +# poor-man's json getter (no jq) +json_get() { + echo "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" +} + +# parse ping output (returns: loss,min,avg,max,mdev) +parse_ping() { + local out; out=$(cat) + local loss min avg max mdev rtt + loss=$(printf '%s\n' "$out" | LC_ALL=C grep -Eo '[0-9]+(\.[0-9]+)?% packet loss' | sed -E 's/%.*//') + [ -z "$loss" ] && loss="100.0" + rtt=$(printf '%s\n' "$out" | LC_ALL=C grep -E 'min/avg/max' | tail -n1 | awk -F'=' '{print $2}' | awk '{print $1}') + if [ -n "$rtt" ]; then + IFS=/ read -r min avg max mdev <<<"$rtt" + else + min="N/A"; avg="N/A"; max="N/A"; mdev="N/A" + fi + printf '%s,%s,%s,%s,%s\n' "$loss" "$min" "$avg" "$max" "$mdev" +} + +# main +read -rp "请输入英文国家名(如 Japan / United States / South Korea;或 ISO 两字母,如 JP): " USER_REGION || USER_REGION="" +SLUG=$(normalize_country_to_slug "${USER_REGION:-}") +[ -z "$SLUG" ] && SLUG="japan" + +MAP_IPS_RAW=$(fetch_country_ips "$SLUG") + +declare -a TARGETS TMP_LIST RESULTS +declare -A seen + +while IFS= read -r ip; do + [ -z "$ip" ] && continue + if [[ "$ip" == *:* ]]; then + TARGETS+=("$ip") + else + if is_public_ipv4 "$ip"; then + TARGETS+=("$ip") + fi + fi +done < <(printf '%s\n' "$MAP_IPS_RAW" | awk 'NF{if(!seen[$0]++){print}}') + +for ip in "${TARGETS[@]}"; do + if [ -z "${seen[$ip]+x}" ]; then + TMP_LIST+=("$ip"); seen["$ip"]=1 + fi + [ "${#TMP_LIST[@]}" -ge "$TOPN" ] && break +done +TARGETS=("${TMP_LIST[@]}") + +for must in 1.1.1.1 8.8.8.8; do + if [ -z "${seen[$must]+x}" ]; then + TARGETS+=("$must"); seen["$must"]=1 + fi +done + +N=${#TARGETS[@]} +printf "地区: %s(slug: %s)| 目标数: %d | 每个目标 ping 次数: %d\n" "${USER_REGION:-N/A}" "$SLUG" "$N" "$COUNT" +printf "将测试的目标:%s\n" "$(printf '%s ' "${TARGETS[@]}")" +echo "开始测试 ......" + +if [ "$N" -eq 0 ]; then + echo "未找到可用目标。" + exit 0 +fi + +idx=0 +for ip in "${TARGETS[@]}"; do + idx=$((idx+1)) + pct=$(( idx * 100 / (N==0 ? 1 : N) )) + + printf "进度: [%d/%d | %3d%%] #%d 正在测试: %s\r" "$idx" "$N" "$pct" "$idx" "$ip" + + if [[ "$ip" == *:* ]]; then + out=$(LC_ALL=C $PING6_BIN -n -c "$COUNT" -i 0.2 -w $((COUNT+4)) "$ip" 2>&1 || true) + else + out=$(LC_ALL=C $PING_BIN -n -c "$COUNT" -i 0.2 -w $((COUNT+4)) "$ip" 2>&1 || true) + fi + + stats=$(printf '%s' "$out" | parse_ping) + IFS=, read -r loss min avg max mdev <<<"$stats" + + meta=$(get_meta_json "$ip") + country=$(json_get "$meta" country); [ -z "$country" ] && country="N/A" + city=$(json_get "$meta" city); [ -z "$city" ] && city="N/A" + asfull=$(json_get "$meta" as); [ -z "$asfull" ] && asfull="N/A" + asname=$(json_get "$meta" asname); [ -z "$asname" ] && asname="N/A" + org=$(json_get "$meta" org) + isp=$(json_get "$meta" isp) + + asn=$(printf '%s' "$asfull" | sed -n 's/.*\(AS[0-9][0-9]*\).*/\1/p') + [ -z "$asn" ] && asn="N/A" + + company="$asname" + [ -z "$company" ] || [ "$company" = "N/A" ] && company="$org" + [ -z "$company" ] || [ "$company" = "N/A" ] && company="$isp" + [ -z "$company" ] && company="N/A" + + printf "进度: [%d/%d | %3d%%] #%d 正在测试: %-39s | 丢包 %s%% | 最小 %sms | 平均 %sms | 最大 %sms | 抖动 %sms\n" \ + "$idx" "$N" "$pct" "$idx" "$ip" "$loss" "$min" "$avg" "$max" "$mdev" + + sortkey="$avg"; [[ "$sortkey" == "N/A" || -z "$sortkey" ]] && sortkey=999999999 + RESULTS+=("$sortkey\t$idx\t$ip\t$country/$city\t$asn\t$company\t$loss\t$min\t$avg\t$max\t$mdev") +done +echo + +HEADER=$'编号\t目标\t地区\tASN\t公司\t丢包\t最小(ms)\t平均(ms)\t最大(ms)\t抖动' +BODY=$( + printf '%b\n' "${RESULTS[@]}" \ + | LC_ALL=C sort -t$'\t' -k1,1n \ + | cut -f2- \ + | while IFS=$'\t' read -r idx0 ip region asn company loss min avg max mdev; do + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$idx0" "$ip" "$region" "$asn" "$company" \ + "$(printf '%.1f%%' "${loss:-0}")" \ + "${min:-N/A}" "${avg:-N/A}" "${max:-N/A}" "${mdev:-N/A}" + done +) + +if command -v column >/dev/null 2>&1; then + { printf '%s\n' "$HEADER"; printf '%s\n' "$BODY"; } | column -t -s $'\t' +else + printf "%-4s %-39s %-18s %-8s %-28s %-6s %-9s %-9s %-9s %-7s\n" \ + "编号" "目标" "地区" "ASN" "公司" "丢包" "最小(ms)" "平均(ms)" "最大(ms)" "抖动" + printf -- "-----------------------------------------------------------------------------------------------\n" + printf '%s\n' "$BODY" | while IFS=$'\t' read -r idx0 ip region asn company loss min avg max mdev; do + printf "%-4s %-39s %-18s %-8s %-28s %-6s %-9s %-9s %-9s %-7s\n" \ + "$idx0" "$ip" "$region" "$asn" "$company" \ + "$loss" "$min" "$avg" "$max" "$mdev" + done + +fi + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2672040 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + x-panel: + build: + context: . + dockerfile: ./Dockerfile + container_name: x-panel_app + # hostname: yourhostname <<--- optional + volumes: + - $PWD/db/:/etc/x-ui/ + - $PWD/cert/:/root/cert/ + environment: + XRAY_VMESS_AEAD_FORCED: "false" + XUI_ENABLE_FAIL2BAN: "true" + tty: true + network_mode: host + restart: unless-stopped diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bf4ca73 --- /dev/null +++ b/go.mod @@ -0,0 +1,100 @@ +module x-ui + +go 1.26.2 + +replace gvisor.dev/gvisor => gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 + +require ( + github.com/gin-contrib/gzip v1.2.6 + github.com/gin-contrib/sessions v1.1.0 + github.com/gin-gonic/gin v1.12.0 + github.com/goccy/go-json v0.10.6 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/mymmrac/telego v1.8.0 + github.com/nicksnyder/go-i18n/v2 v2.6.1 + github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 + github.com/pelletier/go-toml/v2 v2.3.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v4 v4.26.3 + github.com/valyala/fasthttp v1.70.0 + github.com/xlzd/gotp v0.1.0 + github.com/xtls/xray-core v1.260327.0 + go.uber.org/atomic v1.11.0 + golang.org/x/crypto v0.50.0 + golang.org/x/text v0.36.0 + google.golang.org/grpc v1.80.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/andybalholm/brotli v1.2.1 // indirect + github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grbit/go-json v0.11.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/juju/ratelimit v1.0.2 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mattn/go-sqlite3 v1.14.42 // indirect + github.com/miekg/dns v1.1.72 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pires/go-proxyproto v0.12.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagernet/sing v0.8.9 // indirect + github.com/sagernet/sing-shadowsocks v0.2.9 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fastjson v1.6.10 // indirect + github.com/vishvananda/netlink v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/arch v0.26.0 // indirect + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.44.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gvisor.dev/gvisor v0.0.0-20260424012932-95eb8fa6e7a6 // indirect + lukechampine.com/blake3 v1.4.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..85253c1 --- /dev/null +++ b/go.sum @@ -0,0 +1,256 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU= +github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= +github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg= +github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU= +github.com/gin-contrib/sessions v1.1.0 h1:00mhHfNEGF5sP2fwxa98aRqj1FOJdL6IkR86n2hOiBo= +github.com/gin-contrib/sessions v1.1.0/go.mod h1:TyYZDIs6qCQg2SOoYPgMT9pAkmZceVNEJMcv5qbIy60= +github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= +github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= +github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo= +github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow= +github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= +github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= +github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= +github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= +github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sagernet/sing v0.8.9 h1:iX8FyMrWNl/divVgTe7cLT9n36v6bfzfnCYlcM1cLaU= +github.com/sagernet/sing v0.8.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= +github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= +github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= +github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= +github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= +github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= +github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE= +github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro= +go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4= +golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= +gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e0da67c --- /dev/null +++ b/install.sh @@ -0,0 +1,622 @@ +#!/bin/bash + +# ========================================================== +# X-Panel 统一安装脚本 (付费/免费二合一) +# 作者: X-Panel +# ========================================================== + +red='\033[0;31m' +green='\033[0;32m' +blue='\033[0;34m' +yellow='\033[0;33m' +plain='\033[0m' + +# check root +[[ $EUID -ne 0 ]] && echo -e "${red}致命错误: ${plain} 请使用 root 权限运行此脚本\n" && exit 1 + +# ---------------------------------------------------------- +# 获取机器唯一硬件标识 (HWID) +# ---------------------------------------------------------- +get_hwid() { + local machine_id="" + + # 1. 优先尝试获取 DMI Product UUID (VPS 硬件 ID,重装系统通常不变) + if [[ -r /sys/class/dmi/id/product_uuid ]]; then + machine_id=$(cat /sys/class/dmi/id/product_uuid) + + # 2. 其次尝试获取 eth0 网卡 MAC 地址 (大部分 VPS 重装后 MAC 不变) + elif [[ -r /sys/class/net/eth0/address ]]; then + machine_id=$(cat /sys/class/net/eth0/address) + + # 3. 如果都失败,才使用 machine-id (重装会变,作为最后兜底) + elif [[ -f /etc/machine-id ]]; then + machine_id=$(cat /etc/machine-id) + else + machine_id=$(hostname) + fi + + # 取 MD5 作为唯一指纹,确保格式统一 + echo -n "$machine_id" | md5sum | awk '{print $1}' +} + +# ---------------------------------------------------------- +# 函数:付费Pro版安装逻辑 (install_paid_version) +# ---------------------------------------------------------- +# 此函数负责获取授权码和IP + 机器指纹,并从远程授权服务器获取并执行付费脚本 +# +install_paid_version() { + echo "" + echo -e "${green}您正在安装/升级/更新 【X-Panel 付费Pro版】${plain}" + echo "" + echo -e "${yellow}------------------------------------------------------${plain}" + echo "" + + # 1. 提示用户输入授权码 + read -p "$(echo -e "${yellow}请输入您的授权码 (License Key): ${plain}")" auth_key + echo "" + + if [ -z "$auth_key" ]; then + echo -e "${red}错误: 您没有输入授权码。${plain}" + exit 1 + fi + + # 2. 获取本机的公共 IPv4 地址 + echo -e "${green}正在获取本机 IP 地址......${plain}" + vps_ip=$(curl -s4m8 ip.sb -k | head -n 1) + + if [ -z "$vps_ip" ]; then + echo -e "${red}致命错误: 未能获取服务器的公共 IP 地址。${plain}" + echo -e "${red}请检查您的网络连接或 curl 是否正常工作。${plain}" + exit 1 + fi + + # 3. [新增] 获取本机硬件指纹 + vps_hwid=$(get_hwid) + + echo -e "${green}本机 IP: ${vps_ip}${plain}" + echo -e "${green}机器指纹: ${vps_hwid}${plain}" # 调试用 + echo "" + + # 4. 设置您的授权服务器地址 + AUTH_SERVER_URL="https://auth.x-panel.vip/install_pro.php" + + echo -e "${green}正在连接〔远程授权服务器〕进行验证......${plain}" + echo "" + echo -e "${yellow}请稍候.........${plain}" + + # 5. 将服务器响应保存到变量 + response=$(curl -sL --connect-timeout 20 -X POST -d "key=${auth_key}&ip=${vps_ip}&hwid=${vps_hwid}" "${AUTH_SERVER_URL}") + + # 6. 简单判断响应是否为空 + if [ -z "$response" ]; then + echo -e "${red}错误: 无法连接到授权服务器或服务器无响应。${plain}" + echo -e "${yellow}请检查网络连接或联系管理员。${plain}" + exit 1 + fi + + # 7. 判断是否包含 PHP 错误 (如 Syntax error 或 Fatal error) + # 如果 PHP 报错,通常会包含 "Fatal error" 或 "Parse error" 字样 + if echo "$response" | grep -qE "Fatal error|Parse error"; then + echo -e "${red}错误: 授权服务器发生内部错误。${plain}" + echo -e "详细信息: $response" + exit 1 + fi + + # 8. 执行脚本 + bash <(echo "$response") + + exit 0 +} + + +# ---------------------------------------------------------- +# 函数:免费基础版安装逻辑 (install_free_version) +# ---------------------------------------------------------- +install_free_version() { + echo "" + echo -e "${green}您选择了安装 【X-Panel 免费基础版】${plain}" + echo "" + echo -e "${green}即将开始执行标准安装流程...${plain}" + sleep 2 + + cur_dir=$(pwd) + + # Check OS and set release variable + if [[ -f /etc/os-release ]]; then + source /etc/os-release + release=$ID + elif [[ -f /usr/lib/os-release ]]; then + source /usr/lib/os-release + release=$ID + else + echo "" + echo -e "${red}检查服务器操作系统失败,请联系作者!${plain}" >&2 + exit 1 + fi + echo "" + echo -e "${green}---------->>>>>目前服务器的操作系统为: $release${plain}" + + arch() { + case "$(uname -m)" in + x86_64 | x64 | amd64 ) echo 'amd64' ;; + i*86 | x86 ) echo '386' ;; + armv8* | armv8 | arm64 | aarch64 ) echo 'arm64' ;; + armv7* | armv7 | arm ) echo 'armv7' ;; + armv6* | armv6 ) echo 'armv6' ;; + armv5* | armv5 ) echo 'armv5' ;; + s390x) echo 's390x' ;; + *) echo -e "${green}不支持的CPU架构! ${plain}" && rm -f install.sh && exit 1 ;; + esac + } + + echo "" + # check_glibc_version() { + # glibc_version=$(ldd --version | head -n1 | awk '{print $NF}') + + # required_version="2.32" + # if [[ "$(printf '%s\n' "$required_version" "$glibc_version" | sort -V | head -n1)" != "$required_version" ]]; then + # echo -e "${red}------>>>GLIBC版本 $glibc_version 太旧了! 要求2.32或以上版本${plain}" + # echo -e "${green}-------->>>>请升级到较新版本的操作系统以便获取更高版本的GLIBC${plain}" + # exit 1 + # fi + # echo -e "${green}-------->>>>GLIBC版本: $glibc_version(符合高于2.32的要求)${plain}" + # } + # check_glibc_version + + # echo "" + echo -e "${yellow}---------->>>>>当前系统的架构为: $(arch)${plain}" + echo "" + last_version=$(curl -Ls "https://api.github.com/repos/xeefei/x-panel/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + # 获取 x-ui 版本 + xui_version=$(/usr/local/x-ui/x-ui -v) + + # 检查 xui_version 是否为空 + if [[ -z "$xui_version" ]]; then + echo "" + echo -e "${red}------>>>当前服务器没有安装任何 x-ui 系列代理面板${plain}" + echo "" + echo -e "${green}-------->>>>片刻之后脚本将会自动引导安装〔X-Panel面板〕${plain}" + else + # 检查版本号中是否包含冒号 + if [[ "$xui_version" == *:* ]]; then + echo -e "${green}---------->>>>>当前代理面板的版本为: ${red}其他 x-ui 分支版本${plain}" + echo "" + echo -e "${green}-------->>>>片刻之后脚本将会自动引导安装〔X-Panel面板〕${plain}" + else + echo -e "${green}---------->>>>>当前代理面板的版本为: ${red}〔X-Panel面板〕v${xui_version}${plain}" + fi + fi + echo "" + echo -e "${yellow}---------------------->>>>>〔X-Panel面板〕最新版为:${last_version}${plain}" + sleep 4 + + os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1) + + if [[ "${release}" == "arch" ]]; then + echo "您的操作系统是 ArchLinux" + elif [[ "${release}" == "manjaro" ]]; then + echo "您的操作系统是 Manjaro" + elif [[ "${release}" == "armbian" ]]; then + echo "您的操作系统是 Armbian" + elif [[ "${release}" == "alpine" ]]; then + echo "您的操作系统是 Alpine Linux" + elif [[ "${release}" == "opensuse-tumbleweed" ]]; then + echo "您的操作系统是 OpenSUSE Tumbleweed" + elif [[ "${release}" == "centos" ]]; then + if [[ ${os_version} -lt 8 ]]; then + echo -e "${red} 请使用 CentOS 8 或更高版本 ${plain}\n" && exit 1 + fi + elif [[ "${release}" == "ubuntu" ]]; then + if [[ ${os_version} -lt 20 ]]; then + echo -e "${red} 请使用 Ubuntu 20 或更高版本!${plain}\n" && exit 1 + fi + elif [[ "${release}" == "fedora" ]]; then + if [[ ${os_version} -lt 36 ]]; then + echo -e "${red} 请使用 Fedora 36 或更高版本!${plain}\n" && exit 1 + fi + elif [[ "${release}" == "debian" ]]; then + if [[ ${os_version} -lt 11 ]]; then + echo -e "${red} 请使用 Debian 11 或更高版本 ${plain}\n" && exit 1 + fi + elif [[ "${release}" == "almalinux" ]]; then + if [[ ${os_version} -lt 9 ]]; then + echo -e "${red} 请使用 AlmaLinux 9 或更高版本 ${plain}\n" && exit 1 + fi + elif [[ "${release}" == "rocky" ]]; then + if [[ ${os_version} -lt 9 ]]; then + echo -e "${red} 请使用 RockyLinux 9 或更高版本 ${plain}\n" && exit 1 + fi + elif [[ "${release}" == "oracle" ]]; then + if [[ ${os_version} -lt 8 ]]; then + echo -e "${red} 请使用 Oracle Linux 8 或更高版本 ${plain}\n" && exit 1 + fi + else + echo -e "${red}此脚本不支持您的操作系统。${plain}\n" + echo "请确保您使用的是以下受支持的操作系统之一:" + echo "- Ubuntu 20.04+" + echo "- Debian 11+" + echo "- CentOS 8+" + echo "- Fedora 36+" + echo "- Arch Linux" + echo "- Manjaro" + echo "- Armbian" + echo "- Alpine Linux" + echo "- AlmaLinux 9+" + echo "- Rocky Linux 9+" + echo "- Oracle Linux 8+" + echo "- OpenSUSE Tumbleweed" + exit 1 + + fi + + install_base() { + case "${release}" in + ubuntu | debian | armbian) + apt-get update && apt-get install -y -q wget curl sudo tar tzdata + ;; + centos | rhel | almalinux | rocky | ol) + yum -y --exclude=kernel* update && yum install -y -q wget curl sudo tar tzdata + ;; + fedora | amzn | virtuozzo) + dnf -y --exclude=kernel* update && dnf install -y -q wget curl sudo tar tzdata + ;; + arch | manjaro | parch) + pacman -Sy && pacman -S --noconfirm wget curl sudo tar tzdata + ;; + alpine) + apk update && apk add --no-cache wget curl sudo tar tzdata + ;; + opensuse-tumbleweed) + zypper refresh && zypper -q install -y wget curl sudo tar timezone + ;; + *) + apt-get update && apt-get install -y -q wget curl sudo tar tzdata + ;; + esac + } + + gen_random_string() { + local length="$1" + local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' >请输入:")" config_confirm + if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then + read -p "请设置您的用户名: " config_account + echo -e "${yellow}您的用户名将是: ${config_account}${plain}" + read -p "请设置您的密码: " config_password + echo -e "${yellow}您的密码将是: ${config_password}${plain}" + read -p "请设置面板端口: " config_port + echo -e "${yellow}您的面板端口号为: ${config_port}${plain}" + read -p "请设置面板登录访问路径: " config_webBasePath + echo -e "${yellow}您的面板访问路径为: ${config_webBasePath}${plain}" + echo -e "${yellow}正在初始化,请稍候...${plain}" + /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} + echo -e "${yellow}用户名和密码设置成功!${plain}" + /usr/local/x-ui/x-ui setting -port ${config_port} + echo -e "${yellow}面板端口号设置成功!${plain}" + /usr/local/x-ui/x-ui setting -webBasePath ${config_webBasePath} + echo -e "${yellow}面板登录访问路径设置成功!${plain}" + echo "" + else + echo "" + sleep 1 + echo -e "${red}--------------->>>>Cancel...--------------->>>>>>>取消修改...${plain}" + echo "" + if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then + local usernameTemp=$(head -c 10 /dev/urandom | base64) + local passwordTemp=$(head -c 10 /dev/urandom | base64) + local webBasePathTemp=$(gen_random_string 15) + /usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp} -webBasePath ${webBasePathTemp} + echo "" + echo -e "${yellow}检测到为全新安装,出于安全考虑将生成随机登录信息:${plain}" + echo -e "###############################################" + echo -e "${green}用户名: ${usernameTemp}${plain}" + echo -e "${green}密 码: ${passwordTemp}${plain}" + echo -e "${green}访问路径: ${webBasePathTemp}${plain}" + echo -e "###############################################" + echo -e "${green}如果您忘记了登录信息,可以在安装后通过 x-ui 命令然后输入${red}数字 10 选项${green}进行查看${plain}" + else + echo -e "${green}此次操作属于版本升级,保留之前旧设置项,登录方式保持不变${plain}" + echo "" + echo -e "${green}如果您忘记了登录信息,您可以通过 x-ui 命令然后输入${red}数字 10 选项${green}进行查看${plain}" + echo "" + echo "" + fi + fi + sleep 1 + echo -e ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + echo "" + /usr/local/x-ui/x-ui migrate + } + + echo "" + install_x-ui() { + cd /usr/local/ + + # Download resources + if [ $# == 0 ]; then + last_version=$(curl -Ls "https://api.github.com/repos/xeefei/x-panel/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + if [[ ! -n "$last_version" ]]; then + echo -e "${red}获取 X-Panel 版本失败,可能是 Github API 限制,请稍后再试${plain}" + exit 1 + fi + echo "" + echo -e "-----------------------------------------------------" + echo -e "${green}--------->>获取 X-Panel 最新版本:${yellow}${last_version}${plain}${green},开始安装...${plain}" + echo -e "-----------------------------------------------------" + echo "" + sleep 2 + echo -e "${green}---------------->>>>>>>>>安装进度50%${plain}" + sleep 3 + echo "" + echo -e "${green}---------------->>>>>>>>>>>>>>>>>>>>>安装进度100%${plain}" + echo "" + sleep 2 + wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/xeefei/x-panel/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz + if [[ $? -ne 0 ]]; then + echo -e "${red}下载 X-Panel 失败, 请检查服务器是否可以连接至 GitHub? ${plain}" + exit 1 + fi + else + last_version=$1 + url="https://github.com/xeefei/x-panel/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz" + echo "" + echo -e "--------------------------------------------" + echo -e "${green}---------------->>>>开始安装 X-Panel 免费基础版$1${plain}" + echo -e "--------------------------------------------" + echo "" + sleep 2 + echo -e "${green}---------------->>>>>>>>>安装进度50%${plain}" + sleep 3 + echo "" + echo -e "${green}---------------->>>>>>>>>>>>>>>>>>>>>安装进度100%${plain}" + echo "" + sleep 2 + wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url} + if [[ $? -ne 0 ]]; then + echo -e "${red}下载 X-Panel $1 失败, 请检查此版本是否存在 ${plain}" + exit 1 + fi + fi + wget -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/xeefei/x-panel/main/x-ui.sh + + # Stop x-ui service and remove old resources + if [[ -e /usr/local/x-ui/ ]]; then + systemctl stop x-ui + rm /usr/local/x-ui/ -rf + fi + + sleep 3 + echo -e "${green}------->>>>>>>>>>>检查并保存安装目录${plain}" + echo "" + tar zxvf x-ui-linux-$(arch).tar.gz + rm x-ui-linux-$(arch).tar.gz -f + + cd x-ui + chmod +x x-ui + chmod +x x-ui.sh + + # Check the system's architecture and rename the file accordingly + if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then + mv bin/xray-linux-$(arch) bin/xray-linux-arm + chmod +x bin/xray-linux-arm + fi + chmod +x x-ui bin/xray-linux-$(arch) + + # Update x-ui cli and se set permission + mv -f /usr/bin/x-ui-temp /usr/bin/x-ui + chmod +x /usr/bin/x-ui + sleep 2 + echo -e "${green}------->>>>>>>>>>>保存成功${plain}" + sleep 2 + echo "" + config_after_install + + ssh_forwarding() { + # 获取 IPv4 和 IPv6 地址 + v4=$(curl -s4m8 http://ip.sb -k) + v6=$(curl -s6m8 http://ip.sb -k) + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath(访问路径): .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port(端口号): .+' | awk '{print $2}') + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + local existing_key=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') + + if [[ -n "$existing_cert" && -n "$existing_key" ]]; then + echo -e "${green}面板已安装证书采用SSL保护${plain}" + echo "" + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + domain=$(basename "$(dirname "$existing_cert")") + echo -e "${green}登录访问面板URL: https://${domain}:${existing_port}${green}${existing_webBasePath}${plain}" + fi + echo "" + if [[ -z "$existing_cert" && -z "$existing_key" ]]; then + echo -e "${red}警告:未找到证书和密钥,面板不安全!${plain}" + echo "" + echo -e "${green}------->>>>请按照下述方法设置〔ssh转发〕<<<<-------${plain}" + echo "" + + # 检查 IP 并输出相应的 SSH 和浏览器访问信息 + if [[ -z $v4 ]]; then + echo -e "${green}1、本地电脑客户端转发命令:${plain} ${blue}ssh -L [::]:15208:127.0.0.1:${existing_port}${blue} root@[$v6]${plain}" + echo "" + echo -e "${green}2、请通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务${plain}" + echo "" + echo -e "${green}3、请在终端中成功输入服务器的〔root密码〕,注意区分大小写,用以上命令进行转发${plain}" + echo "" + echo -e "${green}4、请在浏览器地址栏复制${plain} ${blue}[::1]:15208${existing_webBasePath}${plain} ${green}进入〔X-Panel面板〕登录界面" + echo "" + echo -e "${red}注意:若不使用〔ssh转发〕请为X-Panel面板配置安装证书再行登录管理后台${plain}" + elif [[ -n $v4 && -n $v6 ]]; then + echo -e "${green}1、本地电脑客户端转发命令:${plain} ${blue}ssh -L 15208:127.0.0.1:${existing_port}${blue} root@$v4${plain} ${yellow}或者 ${blue}ssh -L [::]:15208:127.0.0.1:${existing_port}${blue} root@[$v6]${plain}" + echo "" + echo -e "${green}2、请通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务${plain}" + echo "" + echo -e "${green}3、请在终端中成功输入服务器的〔root密码〕,注意区分大小写,用以上命令进行转发${plain}" + echo "" + echo -e "${green}4、请在浏览器地址栏复制${plain} ${blue}127.0.0.1:15208${existing_webBasePath}${plain} ${yellow}或者${plain} ${blue}[::1]:15208${existing_webBasePath}${plain} ${green}进入〔X-Panel面板〕登录界面" + echo "" + echo -e "${red}注意:若不使用〔ssh转发〕请为X-Panel面板配置安装证书再行登录管理后台${plain}" + else + echo -e "${green}1、本地电脑客户端转发命令:${plain} ${blue}ssh -L 15208:127.0.0.1:${existing_port}${blue} root@$v4${plain}" + echo "" + echo -e "${green}2、请通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务${plain}" + echo "" + echo -e "${green}3、请在终端中成功输入服务器的〔root密码〕,注意区分大小写,用以上命令进行转发${plain}" + echo "" + echo -e "${green}4、请在浏览器地址栏复制${plain} ${blue}127.0.0.1:15208${existing_webBasePath}${plain} ${green}进入〔X-Panel面板〕登录界面" + echo "" + echo -e "${red}注意:若不使用〔ssh转发〕请为X-Panel面板配置安装证书再行登录管理后台${plain}" + echo "" + fi + fi + } + # 执行ssh端口转发 + ssh_forwarding + + cp -f x-ui.service /etc/systemd/system/ + systemctl daemon-reload + systemctl enable x-ui + systemctl start x-ui + systemctl stop warp-go >/dev/null 2>&1 + wg-quick down wgcf >/dev/null 2>&1 + ipv4=$(curl -s4m8 ip.p3terx.com -k | sed -n 1p) + ipv6=$(curl -s6m8 ip.p3terx.com -k | sed -n 1p) + systemctl start warp-go >/dev/null 2>&1 + wg-quick up wgcf >/dev/null 2A>&1 + + echo "" + echo -e "------->>>>${green}X-Panel 免费基础版 ${last_version}${plain}<<<<安装成功,正在启动..." + sleep 1 + echo "" + echo -e " ---------------------" + echo -e " |${green}X-Panel 控制菜单用法 ${plain}|${plain}" + echo -e " | ${yellow}一个更好的面板 ${plain}|${plain}" + echo -e " | ${yellow}基于Xray Core构建 ${plain}|${plain}" + echo -e "--------------------------------------------" + echo -e "x-ui - 进入管理脚本" + echo -e "x-ui start - 启动 X-Panel 面板" + echo -e "x-ui stop - 关闭 X-Panel 面板" + echo -e "x-ui restart - 重启 X-Panel 面板" + echo -e "x-ui status - 查看 X-Panel 状态" + echo -e "x-ui settings - 查看当前设置信息" + echo -e "x-ui enable - 启用 X-Panel 开机启动" + echo -e "x-ui disable - 禁用 X-Panel 开机启动" + echo -e "x-ui log - 查看 X-Panel 运行日志" + echo -e "x-ui banlog - 检查 Fail2ban 禁止日志" + echo -e "x-ui update - 更新 X-Panel 面板" + echo -e "x-ui custom - 自定义 X-Panel 版本" + echo -e "x-ui install - 安装 X-Panel 面板" + echo -e "x-ui uninstall - 卸载 X-Panel 面板" + echo -e "--------------------------------------------" + echo "" + # if [[ -n $ipv4 ]]; then + # echo -e "${yellow}面板 IPv4 访问地址为:${green}http://$ipv4:${config_port}/${config_webBasePath}${plain}" + # fi + # if [[ -n $ipv6 ]]; then + # echo -e "${yellow}面板 IPv6 访问地址为:${green}http://[$ipv6]:${config_port}/${config_webBasePath}${plain}" + # fi + # echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保${red} ${config_port} ${yellow}端口已放行${plain}" + sleep 3 + echo -e ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + echo "" + echo -e "${yellow}----->>>X-Panel面板和Xray启动成功<<<-----${plain}" + } + + # 设置VPS中的时区/时间为【上海时间】 + sudo timedatectl set-timezone Asia/Shanghai + + install_base + install_x-ui $1 + echo "" + echo -e "----------------------------------------------" + sleep 4 + info=$(/usr/local/x-ui/x-ui setting -show true) + echo -e "${info}${plain}" + echo "" + echo -e "若您忘记了上述面板信息,后期可通过x-ui命令进入脚本${red}输入数字〔10〕选项获取${plain}" + echo "" + echo -e "----------------------------------------------" + echo "" + sleep 2 + echo -e "${green}安装/更新完成,若在使用过程中有任何问题${plain}" + echo -e "${yellow}请先描述清楚所遇问题加〔X-Panel面板〕交流群${plain}" + echo -e "${yellow}在TG群中${red} https://t.me/XUI_CN ${yellow}截图进行反馈${plain}" + echo "" + echo -e "----------------------------------------------" + echo "" + echo -e "${green}〔X-Panel面板〕项目地址:${yellow}https://github.com/xeefei/x-panel${plain}" + echo "" + echo -e "${green} 详细安装教程:${yellow}https://xeefei.blogspot.com/2025/09/x-panel.html${plain}" + echo "" + echo -e "----------------------------------------------" + echo "" + echo -e "-------------->>>>>>>赞 助 推 广 区<<<<<<<<-------------------" + echo "" + echo -e "${green}1、搬瓦工GIA高端线路:${yellow}https://bandwagonhost.com/aff.php?aff=75015${plain}" + echo "" + echo -e "${green}2、Dmit高端GIA线路:${yellow}https://www.dmit.io/aff.php?aff=9326${plain}" + echo "" + echo -e "${green}3、Gomami亚太顶尖优化线路:${yellow}https://gomami.io/aff.php?aff=174${plain}" + echo "" + echo -e "${green}4、ISIF优质亚太优化线路:${yellow}https://cloud.isif.net/login?affiliation_code=333${plain}" + echo "" + echo -e "${green}5、ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选:${yellow}https://my.zorocloud.com/aff.php?aff=1072${plain}" + echo "" + echo -e "${green}6、三网直连 IEPL / IPLC 直播流量转发:${yellow}https://idc333.top/#register/BCUZXNELNO${plain}" + echo "" + echo -e "${green}7、Bagevm优质落地鸡(原生IP全解锁):${yellow}https://www.bagevm.com/aff.php?aff=754${plain}" + echo "" + echo -e "${green}8、白丝云〔4837线路〕实惠量大管饱:${yellow}https://cloudsilk.io/aff.php?aff=706${plain}" + echo "" + echo -e "${green}9、RackNerd极致性价比机器:${yellow}https://my.racknerd.com/aff.php?aff=15268&pid=912${plain}" + echo "" + echo -e "----------------------------------------------" + echo "" +} + +# 免费版安装逻辑函数 (install_free_version) 结束 + +# ---------------------------------------------------------- +# 脚本主菜单 +# ---------------------------------------------------------- +main_menu() { + echo -e "${green}======================================================${plain}" + echo -e " 欢迎使用 ${yellow}〔X-Panel 面板〕${plain} 一键安装脚本" + echo -e "${green}======================================================${plain}" + echo "" + echo -e "请选择您要安装的版本:" + echo "" + echo -e " ${green}1)${plain} 安装 ${yellow}〔X-Panel 面板〕免费基础版${plain} (GitHub 开源项目)" + echo "" + echo -e " ${green}2)${plain} 安装 ${yellow}〔X-Panel 面板〕付费Pro版${plain} (需要购买授权码)" + echo "" + read -p "请输入您的选择 (1 或 2): " version_choice + echo "" + + case "$version_choice" in + 1) + # 如果选择1,调用免费版函数 + install_free_version + ;; + 2) + # 如果选择2,调用付费版函数 + install_paid_version + ;; + *) + echo -e "${red}输入无效, 退出安装。${plain}" + exit 1 + ;; + esac +} + +# ---------------------------------------------------------- +# 脚本执行入口 +# ---------------------------------------------------------- +clear +main_menu diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..3705c3d --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,128 @@ +package logger + +import ( + "fmt" + "os" + "time" + + "github.com/op/go-logging" +) + +var ( + logger *logging.Logger + logBuffer []struct { + time string + level logging.Level + log string + } +) + +func init() { + InitLogger(logging.INFO) +} + +func InitLogger(level logging.Level) { + newLogger := logging.MustGetLogger("x-ui") + var err error + var backend logging.Backend + var format logging.Formatter + ppid := os.Getppid() + + backend, err = logging.NewSyslogBackend("") + if err != nil { + println(err) + backend = logging.NewLogBackend(os.Stderr, "", 0) + } + if ppid > 0 && err != nil { + format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`) + } else { + format = logging.MustStringFormatter(`%{level} - %{message}`) + } + + backendFormatter := logging.NewBackendFormatter(backend, format) + backendLeveled := logging.AddModuleLevel(backendFormatter) + backendLeveled.SetLevel(level, "x-ui") + newLogger.SetBackend(backendLeveled) + + logger = newLogger +} + +func Debug(args ...any) { + logger.Debug(args...) + addToBuffer("DEBUG", fmt.Sprint(args...)) +} + +func Debugf(format string, args ...any) { + logger.Debugf(format, args...) + addToBuffer("DEBUG", fmt.Sprintf(format, args...)) +} + +func Info(args ...any) { + logger.Info(args...) + addToBuffer("INFO", fmt.Sprint(args...)) +} + +func Infof(format string, args ...any) { + logger.Infof(format, args...) + addToBuffer("INFO", fmt.Sprintf(format, args...)) +} + +func Notice(args ...any) { + logger.Notice(args...) + addToBuffer("NOTICE", fmt.Sprint(args...)) +} + +func Noticef(format string, args ...any) { + logger.Noticef(format, args...) + addToBuffer("NOTICE", fmt.Sprintf(format, args...)) +} + +func Warning(args ...any) { + logger.Warning(args...) + addToBuffer("WARNING", fmt.Sprint(args...)) +} + +func Warningf(format string, args ...any) { + logger.Warningf(format, args...) + addToBuffer("WARNING", fmt.Sprintf(format, args...)) +} + +func Error(args ...any) { + logger.Error(args...) + addToBuffer("ERROR", fmt.Sprint(args...)) +} + +func Errorf(format string, args ...any) { + logger.Errorf(format, args...) + addToBuffer("ERROR", fmt.Sprintf(format, args...)) +} + +func addToBuffer(level string, newLog string) { + t := time.Now() + if len(logBuffer) >= 10240 { + logBuffer = logBuffer[1:] + } + + logLevel, _ := logging.LogLevel(level) + logBuffer = append(logBuffer, struct { + time string + level logging.Level + log string + }{ + time: t.Format("2006/01/02 15:04:05"), + level: logLevel, + log: newLog, + }) +} + +func GetLogs(c int, level string) []string { + var output []string + logLevel, _ := logging.LogLevel(level) + + for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- { + if logBuffer[i].level <= logLevel { + output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log)) + } + } + return output +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e6dd19b --- /dev/null +++ b/main.go @@ -0,0 +1,649 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" +// "os/exec" +// "strings" + "syscall" + _ "unsafe" + // 中文注释: 新增了 time 和 x-ui/job 的导入,这是运行定时任务所必需的包 + "time" + + "x-ui/web/job" + "x-ui/config" + "x-ui/database" + "x-ui/logger" + "x-ui/sub" + "x-ui/util/crypto" + "x-ui/web" + "x-ui/web/global" + "x-ui/web/service" + "x-ui/xray" + + "github.com/joho/godotenv" + "github.com/op/go-logging" +) + +// runWebServer 是【设备限制】项目的主执行函数 +func runWebServer() { + log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) + + switch config.GetLogLevel() { + case config.Debug: + logger.InitLogger(logging.DEBUG) + case config.Info: + logger.InitLogger(logging.INFO) + case config.Notice: + logger.InitLogger(logging.NOTICE) + case config.Warn: + logger.InitLogger(logging.WARNING) + case config.Error: + logger.InitLogger(logging.ERROR) + default: + log.Fatalf("Unknown log level: %v", config.GetLogLevel()) + } + + godotenv.Load() + + err := database.InitDB(config.GetDBPath()) + if err != nil { + log.Fatalf("Error initializing database: %v", err) + } + + // 〔中文注释〕: 1. 初始化所有需要的服务实例 + xrayService := service.XrayService{} + settingService := service.SettingService{} + serverService := service.ServerService{} + // 还需要 InboundService 等,按需添加 + inboundService := service.InboundService{} + lastStatus := service.Status{} + + // 创建 Xray API 实例 + xrayApi := xray.XrayAPI{} + + // 注入到 XrayService 中 + xrayService.SetXrayAPI(xrayApi) + + // 注入到 InboundService 中 + inboundService.SetXrayAPI(xrayApi) + + // 〔中文注释〕: 2. 初始化 TG Bot 服务 (如果已启用) + tgEnable, err := settingService.GetTgbotEnabled() + if err != nil { + logger.Warningf("无法获取 Telegram Bot 设置: %v", err) + } + + var tgBotService service.TelegramService + if tgEnable { + // 将所有需要的服务作为参数传递进去,确保返回的 tgBotService 是一个完全初始化的、可用的实例。 + tgBot := service.NewTgBot(&inboundService, &settingService, &serverService, &xrayService, &lastStatus) + tgBotService = tgBot + } + + // 〔中文注释〕: 3. 【核心步骤】执行依赖注入 + // 将 tgBotService 实例注入到 serverService 中。 + // 这样 serverService 内部的 tgService 字段就不再是 nil 了。 + serverService.SetTelegramService(tgBotService) + // 同理,也为 InboundService 注入 + inboundService.SetTelegramService(tgBotService) + + var server *web.Server + + // 〔中文注释〕: 调用我们刚刚改造过的 web.NewServer,把功能完整的 serverService 传进去。 + server = web.NewServer(serverService) + // 将 tgBotService 注入到 web.Server 中,使其在 web.go/Server.Start() 中可用 + if tgBotService != nil { + // 〔中文注释〕: 这里的注入是为了让 Web Server 可以在启动时调用 Tgbot.Start() + // 同时,也确保了 Web 层的回调处理能使用到这个完整的 Bot 实例 + server.SetTelegramService(tgBotService) + } + + global.SetWebServer(server) + err = server.Start() + if err != nil { + log.Fatalf("Error starting web server: %v", err) + return + } + + var subServer *sub.Server + subServer = sub.NewServer() + global.SetSubServer(subServer) + err = subServer.Start() + if err != nil { + log.Fatalf("Error starting sub server: %v", err) + return + } + + // 中文注释: 在面板服务启动后,我们在这里启动设备限制的后台任务 + go func() { + // 中文注释: 等待5秒,确保面板和Xray服务已基本稳定,避免任务启动过早 + time.Sleep(10 * time.Second) + + // 中文注释: 创建一个定时器。这里的 "10 * time.Second" 就是任务执行的间隔时间。 + // 您可以修改 10 为 2 或 1,来实现更短的延迟。 + // 例如: time.NewTicker(2 * time.Second) 就是2秒执行一次。 + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + // 〔中文注释〕: 步骤一:在循环外部,只声明一次 tgBotService 变量。 + // 我们将其声明为接口类型,初始值为 nil。 + var tgBotService service.TelegramService + + // 〔中文注释〕: 步骤二:检查 Telegram Bot 是否在面板设置中启用。 + settingService := service.SettingService{} + tgEnable, err := settingService.GetTgbotEnabled() + if err != nil { + logger.Warningf("无法获取 Telegram Bot 设置: %v, 设备限制通知功能可能无法使用", err) + } + + // 〔中文注释〕: 步骤三:如果 Bot 已启用,则初始化实例并赋值给上面声明的变量。 + // 注意这里使用的是 `=` 而不是 `:=`,因为我们是给已存在的变量赋值。 + if tgEnable { + tgBotService = new(service.Tgbot) + } + + // 〔中文注释〕:步骤四:创建任务实例时,将 xrayService 和 可能为 nil 的 tgBotService 一同传入。 + // 这样做是安全的,因为 check_client_ip_job.go 内部的 SendMessage 调用前,会先判断服务实例是否可用。 + checkJob := job.NewCheckDeviceLimitJob(&xrayService, tgBotService) + + + // 中文注释: 使用一个无限循环,每次定时器触发,就执行一次任务的 Run() 函数 + for { + <-ticker.C + checkJob.Run() + } + }() + + sigCh := make(chan os.Signal, 1) + // Trap shutdown signals + signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) + for { + sig := <-sigCh + + switch sig { + case syscall.SIGHUP: + logger.Info("Received SIGHUP signal. Restarting servers...") + + err := server.Stop() + if err != nil { + logger.Debug("Error stopping web server:", err) + } + err = subServer.Stop() + if err != nil { + logger.Debug("Error stopping sub server:", err) + } + + server = web.NewServer(serverService) + // 重新注入 tgBotService + if tgBotService != nil { + server.SetTelegramService(tgBotService) + } + global.SetWebServer(server) + err = server.Start() + if err != nil { + log.Fatalf("Error restarting web server: %v", err) + return + } + log.Println("Web server restarted successfully.") + + subServer = sub.NewServer() + global.SetSubServer(subServer) + err = subServer.Start() + if err != nil { + log.Fatalf("Error restarting sub server: %v", err) + return + } + log.Println("Sub server restarted successfully.") + + default: + server.Stop() + subServer.Stop() + log.Println("Shutting down servers.") + return + } + } +} + +func resetSetting() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println("Failed to initialize database:", err) + return + } + + settingService := service.SettingService{} + err = settingService.ResetSettings() + if err != nil { + fmt.Println("Failed to reset settings(重置设置失败):", err) + } else { + fmt.Println("Settings successfully reset ---->>重置设置成功") + } +} + +func showSetting(show bool) { + // 执行 shell 命令获取 IPv4 地址 + // cmdIPv4 := exec.Command("sh", "-c", "curl -s4m8 ip.p3terx.com -k | sed -n 1p") + // outputIPv4, err := cmdIPv4.Output() + // if err != nil { + // log.Fatal(err) + // } + + // 执行 shell 命令获取 IPv6 地址 + // cmdIPv6 := exec.Command("sh", "-c", "curl -s6m8 ip.p3terx.com -k | sed -n 1p") + // outputIPv6, err := cmdIPv6.Output() + // if err != nil { + // log.Fatal(err) + // } + + // 去除命令输出中的换行符 +// ipv4 := strings.TrimSpace(string(outputIPv4)) +// ipv6 := strings.TrimSpace(string(outputIPv6)) + // 定义转义字符,定义不同颜色的转义字符 + const ( + Reset = "\033[0m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + ) + if show { + settingService := service.SettingService{} + port, err := settingService.GetPort() + if err != nil { + fmt.Println("get current port failed, error info(获取当前端口失败,错误信息):", err) + } + + webBasePath, err := settingService.GetBasePath() + if err != nil { + fmt.Println("get webBasePath failed, error info(获取访问路径失败,错误信息):", err) + } + + certFile, err := settingService.GetCertFile() + if err != nil { + fmt.Println("get cert file failed, error info:", err) + } + keyFile, err := settingService.GetKeyFile() + if err != nil { + fmt.Println("get key file failed, error info:", err) + } + + userService := service.UserService{} + userModel, err := userService.GetFirstUser() + if err != nil { + fmt.Println("get current user info failed, error info(获取当前用户信息失败,错误信息):", err) + } + + if userModel.Username == "" || userModel.Password == "" { + fmt.Println("current username or password is empty --->>当前用户名或密码为空") + } + + fmt.Println("") + fmt.Println(Yellow + "----->>>以下为面板重要信息,请自行记录保存<<<-----" + Reset) + fmt.Println(Green + "Current panel settings as follows (当前面板设置如下):" + Reset) + fmt.Println("") + if certFile == "" || keyFile == "" { + fmt.Println(Red + "------>> 警告:面板未安装证书进行SSL保护" + Reset) + } else { + fmt.Println(Green + "------>> 面板已安装证书采用SSL保护" + Reset) + } + fmt.Println("") + hasDefaultCredential := func() bool { + return userModel.Username == "admin" && crypto.CheckPasswordHash(userModel.Password, "admin") + }() + if hasDefaultCredential == true { + fmt.Println(Red + "------>> 警告:使用了默认的admin账号/密码,容易被扫描" + Reset) + } else { + fmt.Println(Green + "------>> 为非默认admin账号/密码,请牢记" + Reset) + } + fmt.Println("") + fmt.Println(Green + fmt.Sprintf("port(端口号): %d", port) + Reset) + fmt.Println(Green + fmt.Sprintf("webBasePath(访问路径): %s", webBasePath) + Reset) + fmt.Println(Green + "PS:为安全起见,不显示账号和密码" + Reset) + fmt.Println(Green + "若您已经忘记账号/密码,请用脚本选项〔6〕重新设置" + Reset) + + fmt.Println("") + fmt.Println("--------------------------------------------------") + // 根据条件打印带颜色的字符串 + // if ipv4 != "" { + // fmt.Println("") + // formattedIPv4 := fmt.Sprintf("%s %s%s:%d%s" + Reset, + // Green+"面板 IPv4 访问地址------>>", + // Yellow+"http://", + // ipv4, + // port, + // Yellow+webBasePath + Reset) + // fmt.Println(formattedIPv4) + // fmt.Println("") + // } + + // if ipv6 != "" { + // fmt.Println("") + // formattedIPv6 := fmt.Sprintf("%s %s[%s%s%s]:%d%s%s", + // Green+"面板 IPv6 访问地址------>>", // 绿色的提示信息 + // Yellow+"http://", // 黄色的 http:// 部分 + // Yellow, // 黄色的[ 左方括号 + // ipv6, // IPv6 地址 + // Yellow, // 黄色的] 右方括号 + // port, // 端口号 + // Yellow+webBasePath, // 黄色的 Web 基础路径 + // Reset) // 重置颜色 + // fmt.Println(formattedIPv6) + // fmt.Println("") + // } + fmt.Println(Green + ">>>>>>>>注:若您安装了〔证书〕,请使用您的域名用https方式登录" + Reset) + fmt.Println("") + fmt.Println("--------------------------------------------------") + fmt.Println("") +// fmt.Println("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑") + fmt.Println(fmt.Sprintf("%s请确保 %s%d%s 端口已打开放行%s",Green, Red, port, Green, Reset)) + fmt.Println(Yellow + "请自行确保此端口没有被其他程序占用" + Reset) +// fmt.Println(Green + "若要登录访问面板,请复制上面的地址到浏览器" + Reset) + fmt.Println("") + fmt.Println("--------------------------------------------------") + fmt.Println("") + } +} + +func updateTgbotEnableSts(status bool) { + settingService := service.SettingService{} + currentTgSts, err := settingService.GetTgbotEnabled() + if err != nil { + fmt.Println(err) + return + } + logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status) + if currentTgSts != status { + err := settingService.SetTgbotEnabled(status) + if err != nil { + fmt.Println(err) + return + } else { + logger.Infof("SetTgbotEnabled[%v] success", status) + } + } +} + +func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println("Error initializing database(初始化数据库出错):", err) + return + } + + settingService := service.SettingService{} + + if tgBotToken != "" { + err := settingService.SetTgBotToken(tgBotToken) + if err != nil { + fmt.Printf("Error setting Telegram bot token(设置TG电报机器人令牌出错): %v\n", err) + return + } + logger.Info("Successfully updated Telegram bot token ----->>已成功更新TG电报机器人令牌") + } + + if tgBotRuntime != "" { + err := settingService.SetTgbotRuntime(tgBotRuntime) + if err != nil { + fmt.Printf("Error setting Telegram bot runtime(设置TG电报机器人通知周期出错): %v\n", err) + return + } + logger.Infof("Successfully updated Telegram bot runtime to (已成功将TG电报机器人通知周期设置为) [%s].", tgBotRuntime) + } + + if tgBotChatid != "" { + err := settingService.SetTgBotChatId(tgBotChatid) + if err != nil { + fmt.Printf("Error setting Telegram bot chat ID(设置TG电报机器人管理者聊天ID出错): %v\n", err) + return + } + logger.Info("Successfully updated Telegram bot chat ID ----->>已成功更新TG电报机器人管理者聊天ID") + } +} + +func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println("Database initialization failed(初始化数据库失败):", err) + return + } + + settingService := service.SettingService{} + userService := service.UserService{} + + if port > 0 { + err := settingService.SetPort(port) + if err != nil { + fmt.Println("Failed to set port(设置端口失败):", err) + } else { + fmt.Printf("Port set successfully(端口设置成功): %v\n", port) + } + } + + if username != "" || password != "" { + err := userService.UpdateFirstUser(username, password) + if err != nil { + fmt.Println("Failed to update username and password(更新用户名和密码失败):", err) + } else { + fmt.Println("Username and password updated successfully ------>>用户名和密码更新成功") + } + } + + if webBasePath != "" { + err := settingService.SetBasePath(webBasePath) + if err != nil { + fmt.Println("Failed to set base URI path(设置访问路径失败):", err) + } else { + fmt.Println("Base URI path set successfully ------>>设置访问路径成功") + } + } + + if resetTwoFactor { + err := settingService.SetTwoFactorEnable(false) + + if err != nil { + fmt.Println("Failed to reset two-factor authentication(设置两步验证失败):", err) + } else { + settingService.SetTwoFactorToken("") + fmt.Println("Two-factor authentication reset successfully --------->>设置两步验证成功") + } + } + + if listenIP != "" { + err := settingService.SetListen(listenIP) + if err != nil { + fmt.Println("Failed to set listen IP(设置监听IP失败):", err) + } else { + fmt.Printf("listen %v set successfully --------->>设置监听IP成功", listenIP) + } + } +} + +func updateCert(publicKey string, privateKey string) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + if (privateKey != "" && publicKey != "") || (privateKey == "" && publicKey == "") { + settingService := service.SettingService{} + err = settingService.SetCertFile(publicKey) + if err != nil { + fmt.Println("set certificate public key failed(设置证书公钥失败):", err) + } else { + fmt.Println("set certificate public key success --------->>设置证书公钥成功") + } + + err = settingService.SetKeyFile(privateKey) + if err != nil { + fmt.Println("set certificate private key failed(设置证书私钥失败):", err) + } else { + fmt.Println("set certificate private key success --------->>设置证书私钥成功") + } + } else { + fmt.Println("both public and private key should be entered ------>>必须同时输入证书公钥和私钥") + } +} + +func GetCertificate(getCert bool) { + if getCert { + settingService := service.SettingService{} + certFile, err := settingService.GetCertFile() + if err != nil { + fmt.Println("get cert file failed, error info:", err) + } + keyFile, err := settingService.GetKeyFile() + if err != nil { + fmt.Println("get key file failed, error info:", err) + } + + fmt.Println("cert:", certFile) + fmt.Println("key:", keyFile) + } +} + +func GetListenIP(getListen bool) { + if getListen { + + settingService := service.SettingService{} + ListenIP, err := settingService.GetListen() + if err != nil { + log.Printf("Failed to retrieve listen IP: %v", err) + return + } + + fmt.Println("listenIP:", ListenIP) + } +} + +func migrateDb() { + inboundService := service.InboundService{} + + err := database.InitDB(config.GetDBPath()) + if err != nil { + log.Fatal(err) + } + fmt.Println("Start migrating database... ---->>开始迁移数据库...") + inboundService.MigrateDB() + fmt.Println("Migration done! ------------>>迁移完成!") +} + +func main() { + if len(os.Args) < 2 { + runWebServer() + return + } + + var showVersion bool + flag.BoolVar(&showVersion, "v", false, "show version") + + runCmd := flag.NewFlagSet("run", flag.ExitOnError) + + settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) + var port int + var username string + var password string + var webBasePath string + var listenIP string + var getListen bool + var webCertFile string + var webKeyFile string + var tgbottoken string + var tgbotchatid string + var enabletgbot bool + var tgbotRuntime string + var reset bool + var show bool + var getCert bool + var resetTwoFactor bool + settingCmd.BoolVar(&reset, "reset", false, "Reset all settings") + settingCmd.BoolVar(&show, "show", false, "Display current settings") + settingCmd.IntVar(&port, "port", 0, "Set panel port number") + settingCmd.StringVar(&username, "username", "", "Set login username") + settingCmd.StringVar(&password, "password", "", "Set login password") + settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel") + settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP") + settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings") + settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP") + settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings") + settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel") + settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel") + settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot") + settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications") + settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications") + settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot") + + oldUsage := flag.Usage + flag.Usage = func() { + oldUsage() + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" run run web panel") + fmt.Println(" migrate migrate form other/old x-ui") + fmt.Println(" setting set settings") + } + + flag.Parse() + if showVersion { + fmt.Println(config.GetVersion()) + return + } + + switch os.Args[1] { + case "run": + err := runCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + runWebServer() + case "migrate": + migrateDb() + case "setting": + err := settingCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + if reset { + resetSetting() + } else { + updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor) + } + if show { + showSetting(show) + } + if getListen { + GetListenIP(getListen) + } + if getCert { + GetCertificate(getCert) + } + if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") { + updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) + } + if enabletgbot { + updateTgbotEnableSts(enabletgbot) + } + case "cert": + err := settingCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + if reset { + updateCert("", "") + } else { + updateCert(webCertFile, webKeyFile) + } + default: + fmt.Println("Invalid subcommands ----->>无效命令") + fmt.Println() + runCmd.Usage() + fmt.Println() + settingCmd.Usage() + } +} diff --git a/media/01-overview-dark.png b/media/01-overview-dark.png new file mode 100644 index 0000000..879cb87 Binary files /dev/null and b/media/01-overview-dark.png differ diff --git a/media/01-overview-light.png b/media/01-overview-light.png new file mode 100644 index 0000000..26e8273 Binary files /dev/null and b/media/01-overview-light.png differ diff --git a/media/02-inbounds-dark.png b/media/02-inbounds-dark.png new file mode 100644 index 0000000..6551169 Binary files /dev/null and b/media/02-inbounds-dark.png differ diff --git a/media/02-inbounds-light.png b/media/02-inbounds-light.png new file mode 100644 index 0000000..4165043 Binary files /dev/null and b/media/02-inbounds-light.png differ diff --git a/media/03-add-inbound-dark.png b/media/03-add-inbound-dark.png new file mode 100644 index 0000000..476fa87 Binary files /dev/null and b/media/03-add-inbound-dark.png differ diff --git a/media/03-add-inbound-light.png b/media/03-add-inbound-light.png new file mode 100644 index 0000000..26e6e6f Binary files /dev/null and b/media/03-add-inbound-light.png differ diff --git a/media/04-add-client-dark.png b/media/04-add-client-dark.png new file mode 100644 index 0000000..bdcc52f Binary files /dev/null and b/media/04-add-client-dark.png differ diff --git a/media/04-add-client-light.png b/media/04-add-client-light.png new file mode 100644 index 0000000..3b5901c Binary files /dev/null and b/media/04-add-client-light.png differ diff --git a/media/05-settings-dark.png b/media/05-settings-dark.png new file mode 100644 index 0000000..73f7692 Binary files /dev/null and b/media/05-settings-dark.png differ diff --git a/media/05-settings-light.png b/media/05-settings-light.png new file mode 100644 index 0000000..2f27b17 Binary files /dev/null and b/media/05-settings-light.png differ diff --git a/media/06-configs-dark.png b/media/06-configs-dark.png new file mode 100644 index 0000000..fef433a Binary files /dev/null and b/media/06-configs-dark.png differ diff --git a/media/06-configs-light.png b/media/06-configs-light.png new file mode 100644 index 0000000..f2370d4 Binary files /dev/null and b/media/06-configs-light.png differ diff --git a/media/07-bot-dark.png b/media/07-bot-dark.png new file mode 100644 index 0000000..f291256 Binary files /dev/null and b/media/07-bot-dark.png differ diff --git a/media/07-bot-light.png b/media/07-bot-light.png new file mode 100644 index 0000000..f22a2b7 Binary files /dev/null and b/media/07-bot-light.png differ diff --git a/media/1.png b/media/1.png new file mode 100644 index 0000000..8e1ecf4 Binary files /dev/null and b/media/1.png differ diff --git a/media/10.png b/media/10.png new file mode 100644 index 0000000..12fbfd9 Binary files /dev/null and b/media/10.png differ diff --git a/media/11.png b/media/11.png new file mode 100644 index 0000000..f1e5163 Binary files /dev/null and b/media/11.png differ diff --git a/media/12.png b/media/12.png new file mode 100644 index 0000000..e0bb2d1 Binary files /dev/null and b/media/12.png differ diff --git a/media/13.png b/media/13.png new file mode 100644 index 0000000..4bb16b2 Binary files /dev/null and b/media/13.png differ diff --git a/media/14.png b/media/14.png new file mode 100644 index 0000000..9dd5b05 Binary files /dev/null and b/media/14.png differ diff --git a/media/15.png b/media/15.png new file mode 100644 index 0000000..d5165f1 Binary files /dev/null and b/media/15.png differ diff --git a/media/16.png b/media/16.png new file mode 100644 index 0000000..ccc279a Binary files /dev/null and b/media/16.png differ diff --git a/media/17.png b/media/17.png new file mode 100644 index 0000000..5fd2713 Binary files /dev/null and b/media/17.png differ diff --git a/media/18.png b/media/18.png new file mode 100644 index 0000000..3c92b90 Binary files /dev/null and b/media/18.png differ diff --git a/media/19.png b/media/19.png new file mode 100644 index 0000000..13194a6 Binary files /dev/null and b/media/19.png differ diff --git a/media/2.png b/media/2.png new file mode 100644 index 0000000..d4ec873 Binary files /dev/null and b/media/2.png differ diff --git a/media/20.png b/media/20.png new file mode 100644 index 0000000..c65047d Binary files /dev/null and b/media/20.png differ diff --git a/media/21.png b/media/21.png new file mode 100644 index 0000000..2ffae77 Binary files /dev/null and b/media/21.png differ diff --git a/media/22.png b/media/22.png new file mode 100644 index 0000000..d3e5573 Binary files /dev/null and b/media/22.png differ diff --git a/media/23.png b/media/23.png new file mode 100644 index 0000000..eaee47b Binary files /dev/null and b/media/23.png differ diff --git a/media/24.png b/media/24.png new file mode 100644 index 0000000..0bd08b6 Binary files /dev/null and b/media/24.png differ diff --git a/media/25.png b/media/25.png new file mode 100644 index 0000000..e1bdd9e Binary files /dev/null and b/media/25.png differ diff --git a/media/26.png b/media/26.png new file mode 100644 index 0000000..3e8d98d Binary files /dev/null and b/media/26.png differ diff --git a/media/27.png b/media/27.png new file mode 100644 index 0000000..cd9c94b Binary files /dev/null and b/media/27.png differ diff --git a/media/28.png b/media/28.png new file mode 100644 index 0000000..52a7d3b Binary files /dev/null and b/media/28.png differ diff --git a/media/29.png b/media/29.png new file mode 100644 index 0000000..8fe55ae Binary files /dev/null and b/media/29.png differ diff --git a/media/3.png b/media/3.png new file mode 100644 index 0000000..d97e4ff Binary files /dev/null and b/media/3.png differ diff --git a/media/30.png b/media/30.png new file mode 100644 index 0000000..b67a77f Binary files /dev/null and b/media/30.png differ diff --git a/media/31.png b/media/31.png new file mode 100644 index 0000000..75c568c Binary files /dev/null and b/media/31.png differ diff --git a/media/32.png b/media/32.png new file mode 100644 index 0000000..340b0fa Binary files /dev/null and b/media/32.png differ diff --git a/media/33.png b/media/33.png new file mode 100644 index 0000000..17ced37 Binary files /dev/null and b/media/33.png differ diff --git a/media/34.png b/media/34.png new file mode 100644 index 0000000..62eb037 Binary files /dev/null and b/media/34.png differ diff --git a/media/35.png b/media/35.png new file mode 100644 index 0000000..45a8e42 Binary files /dev/null and b/media/35.png differ diff --git a/media/36.png b/media/36.png new file mode 100644 index 0000000..c75a148 Binary files /dev/null and b/media/36.png differ diff --git a/media/37.png b/media/37.png new file mode 100644 index 0000000..4be0ce6 Binary files /dev/null and b/media/37.png differ diff --git a/media/38.png b/media/38.png new file mode 100644 index 0000000..7e49dad Binary files /dev/null and b/media/38.png differ diff --git a/media/39.png b/media/39.png new file mode 100644 index 0000000..2a4d8f9 Binary files /dev/null and b/media/39.png differ diff --git a/media/3X-UI.png b/media/3X-UI.png new file mode 100644 index 0000000..7aa7c3e Binary files /dev/null and b/media/3X-UI.png differ diff --git a/media/3x-ui-dark.png b/media/3x-ui-dark.png new file mode 100644 index 0000000..e5f76b1 Binary files /dev/null and b/media/3x-ui-dark.png differ diff --git a/media/3x-ui-light.png b/media/3x-ui-light.png new file mode 100644 index 0000000..a77c830 Binary files /dev/null and b/media/3x-ui-light.png differ diff --git a/media/4.png b/media/4.png new file mode 100644 index 0000000..3762a34 Binary files /dev/null and b/media/4.png differ diff --git a/media/40.png b/media/40.png new file mode 100644 index 0000000..939aa2b Binary files /dev/null and b/media/40.png differ diff --git a/media/41.png b/media/41.png new file mode 100644 index 0000000..d79d851 Binary files /dev/null and b/media/41.png differ diff --git a/media/42.png b/media/42.png new file mode 100644 index 0000000..8ec3041 Binary files /dev/null and b/media/42.png differ diff --git a/media/43.png b/media/43.png new file mode 100644 index 0000000..f0772b3 Binary files /dev/null and b/media/43.png differ diff --git a/media/44.png b/media/44.png new file mode 100644 index 0000000..77f31c9 Binary files /dev/null and b/media/44.png differ diff --git a/media/45.png b/media/45.png new file mode 100644 index 0000000..25399e2 Binary files /dev/null and b/media/45.png differ diff --git a/media/47.png b/media/47.png new file mode 100644 index 0000000..f33fb87 Binary files /dev/null and b/media/47.png differ diff --git a/media/48.png b/media/48.png new file mode 100644 index 0000000..6f83cfb Binary files /dev/null and b/media/48.png differ diff --git a/media/49.png b/media/49.png new file mode 100644 index 0000000..4d5be07 Binary files /dev/null and b/media/49.png differ diff --git a/media/5.png b/media/5.png new file mode 100644 index 0000000..a18cb4b Binary files /dev/null and b/media/5.png differ diff --git a/media/50.png b/media/50.png new file mode 100644 index 0000000..d9a85e5 Binary files /dev/null and b/media/50.png differ diff --git a/media/51.png b/media/51.png new file mode 100644 index 0000000..174abbe Binary files /dev/null and b/media/51.png differ diff --git a/media/52.png b/media/52.png new file mode 100644 index 0000000..ad38a17 Binary files /dev/null and b/media/52.png differ diff --git a/media/53.png b/media/53.png new file mode 100644 index 0000000..2914a87 Binary files /dev/null and b/media/53.png differ diff --git a/media/54.png b/media/54.png new file mode 100644 index 0000000..e9b7cc5 Binary files /dev/null and b/media/54.png differ diff --git a/media/55.png b/media/55.png new file mode 100644 index 0000000..7a986e4 Binary files /dev/null and b/media/55.png differ diff --git a/media/56.png b/media/56.png new file mode 100644 index 0000000..f999ff5 Binary files /dev/null and b/media/56.png differ diff --git a/media/57.png b/media/57.png new file mode 100644 index 0000000..b6e01dc Binary files /dev/null and b/media/57.png differ diff --git a/media/58.png b/media/58.png new file mode 100644 index 0000000..21dbae0 Binary files /dev/null and b/media/58.png differ diff --git a/media/59.png b/media/59.png new file mode 100644 index 0000000..e1bd2af Binary files /dev/null and b/media/59.png differ diff --git a/media/6.png b/media/6.png new file mode 100644 index 0000000..704726b Binary files /dev/null and b/media/6.png differ diff --git a/media/60.png b/media/60.png new file mode 100644 index 0000000..2e5f68d Binary files /dev/null and b/media/60.png differ diff --git a/media/61.png b/media/61.png new file mode 100644 index 0000000..8ced8ce Binary files /dev/null and b/media/61.png differ diff --git a/media/62.png b/media/62.png new file mode 100644 index 0000000..602200f Binary files /dev/null and b/media/62.png differ diff --git a/media/63.png b/media/63.png new file mode 100644 index 0000000..76935d0 Binary files /dev/null and b/media/63.png differ diff --git a/media/64.png b/media/64.png new file mode 100644 index 0000000..d730dc3 Binary files /dev/null and b/media/64.png differ diff --git a/media/65.png b/media/65.png new file mode 100644 index 0000000..3cd5542 Binary files /dev/null and b/media/65.png differ diff --git a/media/66.png b/media/66.png new file mode 100644 index 0000000..eac545d Binary files /dev/null and b/media/66.png differ diff --git a/media/67.png b/media/67.png new file mode 100644 index 0000000..c866d6f Binary files /dev/null and b/media/67.png differ diff --git a/media/68.png b/media/68.png new file mode 100644 index 0000000..31dd147 Binary files /dev/null and b/media/68.png differ diff --git a/media/69.png b/media/69.png new file mode 100644 index 0000000..be5b172 Binary files /dev/null and b/media/69.png differ diff --git a/media/7.png b/media/7.png new file mode 100644 index 0000000..b3cb8eb Binary files /dev/null and b/media/7.png differ diff --git a/media/70.png b/media/70.png new file mode 100644 index 0000000..ea3b361 Binary files /dev/null and b/media/70.png differ diff --git a/media/71.png b/media/71.png new file mode 100644 index 0000000..d7cc027 Binary files /dev/null and b/media/71.png differ diff --git a/media/72.png b/media/72.png new file mode 100644 index 0000000..300c6b3 Binary files /dev/null and b/media/72.png differ diff --git a/media/73.png b/media/73.png new file mode 100644 index 0000000..a8b3421 Binary files /dev/null and b/media/73.png differ diff --git a/media/74.png b/media/74.png new file mode 100644 index 0000000..17becd9 Binary files /dev/null and b/media/74.png differ diff --git a/media/76.png b/media/76.png new file mode 100644 index 0000000..cd4dc36 Binary files /dev/null and b/media/76.png differ diff --git a/media/8.png b/media/8.png new file mode 100644 index 0000000..59af4e1 Binary files /dev/null and b/media/8.png differ diff --git a/media/9.png b/media/9.png new file mode 100644 index 0000000..0b8f151 Binary files /dev/null and b/media/9.png differ diff --git a/media/APIKey1.PNG b/media/APIKey1.PNG new file mode 100644 index 0000000..72a6d8a Binary files /dev/null and b/media/APIKey1.PNG differ diff --git a/media/APIKey2.png b/media/APIKey2.png new file mode 100644 index 0000000..7b90389 Binary files /dev/null and b/media/APIKey2.png differ diff --git a/media/DetailEnter.png b/media/DetailEnter.png new file mode 100644 index 0000000..c07dac2 Binary files /dev/null and b/media/DetailEnter.png differ diff --git a/media/X-Panel.png b/media/X-Panel.png new file mode 100644 index 0000000..2c40607 Binary files /dev/null and b/media/X-Panel.png differ diff --git a/media/botfather.png b/media/botfather.png new file mode 100644 index 0000000..fa57eff Binary files /dev/null and b/media/botfather.png differ diff --git a/media/buymeacoffe.png b/media/buymeacoffe.png new file mode 100644 index 0000000..81571db Binary files /dev/null and b/media/buymeacoffe.png differ diff --git a/media/newbot.png b/media/newbot.png new file mode 100644 index 0000000..061924a Binary files /dev/null and b/media/newbot.png differ diff --git a/media/panel-bot-config.png b/media/panel-bot-config.png new file mode 100644 index 0000000..1238736 Binary files /dev/null and b/media/panel-bot-config.png differ diff --git a/media/token.png b/media/token.png new file mode 100644 index 0000000..66fcd55 Binary files /dev/null and b/media/token.png differ diff --git a/media/user-id.png b/media/user-id.png new file mode 100644 index 0000000..d7d618f Binary files /dev/null and b/media/user-id.png differ diff --git a/sub/default.json b/sub/default.json new file mode 100644 index 0000000..fff1b3a --- /dev/null +++ b/sub/default.json @@ -0,0 +1,90 @@ +{ + "remarks": "", + "dns": { + "tag": "dns_out", + "queryStrategy": "UseIP", + "servers": [ + { + "address": "8.8.8.8", + "skipFallback": false + } + ] + }, + "inbounds": [ + { + "port": 10808, + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "userLevel": 8 + }, + "sniffing": { + "destOverride": [ + "http", + "tls", + "quic", + "fakedns" + ], + "enabled": true + }, + "tag": "socks" + }, + { + "port": 10809, + "protocol": "http", + "settings": { + "userLevel": 8 + }, + "tag": "http" + } + ], + "log": { + "loglevel": "warning" + }, + "outbounds": [ + { + "tag": "direct", + "protocol": "freedom", + "settings": { + "domainStrategy": "AsIs", + "redirect": "", + "noises": [] + } + }, + { + "tag": "block", + "protocol": "blackhole", + "settings": { + "response": { + "type": "http" + } + } + } + ], + "policy": { + "levels": { + "8": { + "connIdle": 300, + "downlinkOnly": 1, + "handshake": 4, + "uplinkOnly": 1 + } + }, + "system": { + "statsOutboundUplink": true, + "statsOutboundDownlink": true + } + }, + "routing": { + "domainStrategy": "AsIs", + "rules": [ + { + "type": "field", + "network": "tcp,udp", + "outboundTag": "proxy" + } + ] + }, + "stats": {} +} \ No newline at end of file diff --git a/sub/sub.go b/sub/sub.go new file mode 100644 index 0000000..4f8f567 --- /dev/null +++ b/sub/sub.go @@ -0,0 +1,213 @@ +package sub + +import ( + "context" + "crypto/tls" + "io" + "net" + "net/http" + "strconv" + + "x-ui/config" + "x-ui/logger" + "x-ui/util/common" + "x-ui/web/middleware" + "x-ui/web/network" + "x-ui/web/service" + + "github.com/gin-gonic/gin" +) + +type Server struct { + httpServer *http.Server + listener net.Listener + + sub *SUBController + settingService service.SettingService + + ctx context.Context + cancel context.CancelFunc +} + +func NewServer() *Server { + ctx, cancel := context.WithCancel(context.Background()) + return &Server{ + ctx: ctx, + cancel: cancel, + } +} + +func (s *Server) initRouter() (*gin.Engine, error) { + if config.IsDebug() { + gin.SetMode(gin.DebugMode) + } else { + gin.DefaultWriter = io.Discard + gin.DefaultErrorWriter = io.Discard + gin.SetMode(gin.ReleaseMode) + } + + engine := gin.Default() + + subDomain, err := s.settingService.GetSubDomain() + if err != nil { + return nil, err + } + + if subDomain != "" { + engine.Use(middleware.DomainValidatorMiddleware(subDomain)) + } + + LinksPath, err := s.settingService.GetSubPath() + if err != nil { + return nil, err + } + + JsonPath, err := s.settingService.GetSubJsonPath() + if err != nil { + return nil, err + } + + Encrypt, err := s.settingService.GetSubEncrypt() + if err != nil { + return nil, err + } + + ShowInfo, err := s.settingService.GetSubShowInfo() + if err != nil { + return nil, err + } + + RemarkModel, err := s.settingService.GetRemarkModel() + if err != nil { + RemarkModel = "-ieo" + } + + SubUpdates, err := s.settingService.GetSubUpdates() + if err != nil { + SubUpdates = "10" + } + + SubJsonFragment, err := s.settingService.GetSubJsonFragment() + if err != nil { + SubJsonFragment = "" + } + + SubJsonNoises, err := s.settingService.GetSubJsonNoises() + if err != nil { + SubJsonNoises = "" + } + + SubJsonMux, err := s.settingService.GetSubJsonMux() + if err != nil { + SubJsonMux = "" + } + + SubJsonRules, err := s.settingService.GetSubJsonRules() + if err != nil { + SubJsonRules = "" + } + + SubTitle, err := s.settingService.GetSubTitle() + if err != nil { + SubTitle = "" + } + + g := engine.Group("/") + + s.sub = NewSUBController( + g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, + SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle) + + return engine, nil +} + +func (s *Server) Start() (err error) { + // This is an anonymous function, no function name + defer func() { + if err != nil { + s.Stop() + } + }() + + subEnable, err := s.settingService.GetSubEnable() + if err != nil { + return err + } + if !subEnable { + return nil + } + + engine, err := s.initRouter() + if err != nil { + return err + } + + certFile, err := s.settingService.GetSubCertFile() + if err != nil { + return err + } + keyFile, err := s.settingService.GetSubKeyFile() + if err != nil { + return err + } + listen, err := s.settingService.GetSubListen() + if err != nil { + return err + } + port, err := s.settingService.GetSubPort() + if err != nil { + return err + } + + listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + return err + } + + if certFile != "" || keyFile != "" { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err == nil { + c := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + listener = network.NewAutoHttpsListener(listener) + listener = tls.NewListener(listener, c) + logger.Info("Sub server running HTTPS on", listener.Addr()) + } else { + logger.Error("Error loading certificates:", err) + logger.Info("Sub server running HTTP on", listener.Addr()) + } + } else { + logger.Info("Sub server running HTTP on", listener.Addr()) + } + s.listener = listener + + s.httpServer = &http.Server{ + Handler: engine, + } + + go func() { + s.httpServer.Serve(listener) + }() + + return nil +} + +func (s *Server) Stop() error { + s.cancel() + + var err1 error + var err2 error + if s.httpServer != nil { + err1 = s.httpServer.Shutdown(s.ctx) + } + if s.listener != nil { + err2 = s.listener.Close() + } + return common.Combine(err1, err2) +} + +func (s *Server) GetCtx() context.Context { + return s.ctx +} diff --git a/sub/subController.go b/sub/subController.go new file mode 100644 index 0000000..1284b1a --- /dev/null +++ b/sub/subController.go @@ -0,0 +1,137 @@ +package sub + +import ( + "encoding/base64" + "net" + "strings" + + "github.com/gin-gonic/gin" +) + +type SUBController struct { + subTitle string + subPath string + subJsonPath string + subEncrypt bool + updateInterval string + + subService *SubService + subJsonService *SubJsonService +} + +func NewSUBController( + g *gin.RouterGroup, + subPath string, + jsonPath string, + encrypt bool, + showInfo bool, + rModel string, + update string, + jsonFragment string, + jsonNoise string, + jsonMux string, + jsonRules string, + subTitle string, +) *SUBController { + sub := NewSubService(showInfo, rModel) + a := &SUBController{ + subTitle: subTitle, + subPath: subPath, + subJsonPath: jsonPath, + subEncrypt: encrypt, + updateInterval: update, + + subService: sub, + subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), + } + a.initRouter(g) + return a +} + +func (a *SUBController) initRouter(g *gin.RouterGroup) { + gLink := g.Group(a.subPath) + gJson := g.Group(a.subJsonPath) + + gLink.GET(":subid", a.subs) + + gJson.GET(":subid", a.subJsons) +} + +func (a *SUBController) subs(c *gin.Context) { + subId := c.Param("subid") + var host string + if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil { + host = h + } + if host == "" { + host = c.GetHeader("X-Real-IP") + } + if host == "" { + var err error + host, _, err = net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + } + subs, header, err := a.subService.GetSubs(subId, host) + if err != nil || len(subs) == 0 { + c.String(400, "Error!") + } else { + result := "" + for _, sub := range subs { + result += sub + "\n" + } + + // Add headers + c.Writer.Header().Set("Subscription-Userinfo", header) + c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) + c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle))) + + if a.subEncrypt { + c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) + } else { + c.String(200, result) + } + } +} + +func (a *SUBController) subJsons(c *gin.Context) { + subId := c.Param("subid") + var host string + if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil { + host = h + } + if host == "" { + host = c.GetHeader("X-Real-IP") + } + if host == "" { + var err error + host, _, err = net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + } + jsonSub, header, err := a.subJsonService.GetJson(subId, host) + if err != nil || len(jsonSub) == 0 { + c.String(400, "Error!") + } else { + + // Add headers + c.Writer.Header().Set("Subscription-Userinfo", header) + c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) + c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle))) + + c.String(200, jsonSub) + } +} + +func getHostFromXFH(s string) (string, error) { + if strings.Contains(s, ":") { + realHost, _, err := net.SplitHostPort(s) + if err != nil { + return "", err + } + return realHost, nil + } + return s, nil +} diff --git a/sub/subJsonService.go b/sub/subJsonService.go new file mode 100644 index 0000000..2a81d76 --- /dev/null +++ b/sub/subJsonService.go @@ -0,0 +1,399 @@ +package sub + +import ( + _ "embed" + "encoding/json" + "fmt" + "strings" + + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/json_util" + "x-ui/util/random" + "x-ui/web/service" + "x-ui/xray" +) + +//go:embed default.json +var defaultJson string + +type SubJsonService struct { + configJson map[string]any + defaultOutbounds []json_util.RawMessage + fragment string + noises string + mux string + + inboundService service.InboundService + SubService *SubService +} + +func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService { + var configJson map[string]any + var defaultOutbounds []json_util.RawMessage + json.Unmarshal([]byte(defaultJson), &configJson) + if outboundSlices, ok := configJson["outbounds"].([]any); ok { + for _, defaultOutbound := range outboundSlices { + jsonBytes, _ := json.Marshal(defaultOutbound) + defaultOutbounds = append(defaultOutbounds, jsonBytes) + } + } + + if rules != "" { + var newRules []any + routing, _ := configJson["routing"].(map[string]any) + defaultRules, _ := routing["rules"].([]any) + json.Unmarshal([]byte(rules), &newRules) + defaultRules = append(newRules, defaultRules...) + routing["rules"] = defaultRules + configJson["routing"] = routing + } + + if fragment != "" { + defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment)) + } + + if noises != "" { + defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noises)) + } + + return &SubJsonService{ + configJson: configJson, + defaultOutbounds: defaultOutbounds, + fragment: fragment, + noises: noises, + mux: mux, + SubService: subService, + } +} + +func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) { + inbounds, err := s.SubService.getInboundsBySubId(subId) + if err != nil || len(inbounds) == 0 { + return "", "", err + } + + var header string + var traffic xray.ClientTraffic + var clientTraffics []xray.ClientTraffic + var configArray []json_util.RawMessage + + // Prepare Inbounds + for _, inbound := range inbounds { + clients, err := s.inboundService.GetClients(inbound) + if err != nil { + logger.Error("SubJsonService - GetClients: Unable to get clients from inbound") + } + if clients == nil { + continue + } + if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { + listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings) + if err == nil { + inbound.Listen = listen + inbound.Port = port + inbound.StreamSettings = streamSettings + } + } + + for _, client := range clients { + if client.Enable && client.SubID == subId { + clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email)) + newConfigs := s.getConfig(inbound, client, host) + configArray = append(configArray, newConfigs...) + } + } + } + + if len(configArray) == 0 { + return "", "", nil + } + + // Prepare statistics + for index, clientTraffic := range clientTraffics { + if index == 0 { + traffic.Up = clientTraffic.Up + traffic.Down = clientTraffic.Down + traffic.Total = clientTraffic.Total + if clientTraffic.ExpiryTime > 0 { + traffic.ExpiryTime = clientTraffic.ExpiryTime + } + } else { + traffic.Up += clientTraffic.Up + traffic.Down += clientTraffic.Down + if traffic.Total == 0 || clientTraffic.Total == 0 { + traffic.Total = 0 + } else { + traffic.Total += clientTraffic.Total + } + if clientTraffic.ExpiryTime != traffic.ExpiryTime { + traffic.ExpiryTime = 0 + } + } + } + + // Combile outbounds + var finalJson []byte + if len(configArray) == 1 { + finalJson, _ = json.MarshalIndent(configArray[0], "", " ") + } else { + finalJson, _ = json.MarshalIndent(configArray, "", " ") + } + + header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) + return string(finalJson), header, nil +} + +func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage { + var newJsonArray []json_util.RawMessage + stream := s.streamData(inbound.StreamSettings) + + externalProxies, ok := stream["externalProxy"].([]any) + if !ok || len(externalProxies) == 0 { + externalProxies = []any{ + map[string]any{ + "forceTls": "same", + "dest": host, + "port": float64(inbound.Port), + "remark": "", + }, + } + } + + delete(stream, "externalProxy") + + for _, ep := range externalProxies { + extPrxy := ep.(map[string]any) + inbound.Listen = extPrxy["dest"].(string) + inbound.Port = int(extPrxy["port"].(float64)) + newStream := stream + switch extPrxy["forceTls"].(string) { + case "tls": + if newStream["security"] != "tls" { + newStream["security"] = "tls" + newStream["tslSettings"] = map[string]any{} + } + case "none": + if newStream["security"] != "none" { + newStream["security"] = "none" + delete(newStream, "tslSettings") + } + } + streamSettings, _ := json.MarshalIndent(newStream, "", " ") + + var newOutbounds []json_util.RawMessage + + switch inbound.Protocol { + case "vmess": + newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, "")) + case "vless": + var vlessSettings model.VLESSSettings + _ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings) + + newOutbounds = append(newOutbounds, + s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption)) + case "trojan", "shadowsocks": + newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client)) + } + + newOutbounds = append(newOutbounds, s.defaultOutbounds...) + newConfigJson := make(map[string]any) + for key, value := range s.configJson { + newConfigJson[key] = value + } + newConfigJson["outbounds"] = newOutbounds + newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string)) + + newConfig, _ := json.MarshalIndent(newConfigJson, "", " ") + newJsonArray = append(newJsonArray, newConfig) + } + + return newJsonArray +} + +func (s *SubJsonService) streamData(stream string) map[string]any { + var streamSettings map[string]any + json.Unmarshal([]byte(stream), &streamSettings) + security, _ := streamSettings["security"].(string) + switch security { + case "tls": + streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any)) + case "reality": + streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any)) + } + delete(streamSettings, "sockopt") + + if s.fragment != "" { + streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`) + } + + // remove proxy protocol + network, _ := streamSettings["network"].(string) + switch network { + case "tcp": + streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"]) + case "ws": + streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"]) + case "httpupgrade": + streamSettings["httpupgradeSettings"] = s.removeAcceptProxy(streamSettings["httpupgradeSettings"]) + } + return streamSettings +} + +func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any { + netSettings, ok := setting.(map[string]any) + if ok { + delete(netSettings, "acceptProxyProtocol") + } + return netSettings +} + +func (s *SubJsonService) tlsData(tData map[string]any) map[string]any { + tlsData := make(map[string]any, 1) + tlsClientSettings, _ := tData["settings"].(map[string]any) + + tlsData["serverName"] = tData["serverName"] + tlsData["alpn"] = tData["alpn"] + if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok { + tlsData["fingerprint"] = fingerprint + } + return tlsData +} + +func (s *SubJsonService) realityData(rData map[string]any) map[string]any { + rltyData := make(map[string]any, 1) + rltyClientSettings, _ := rData["settings"].(map[string]any) + + rltyData["show"] = false + rltyData["publicKey"] = rltyClientSettings["publicKey"] + rltyData["fingerprint"] = rltyClientSettings["fingerprint"] + rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"] + + // Set random data + rltyData["spiderX"] = "/" + random.Seq(15) + shortIds, ok := rData["shortIds"].([]any) + if ok && len(shortIds) > 0 { + rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string) + } else { + rltyData["shortId"] = "" + } + serverNames, ok := rData["serverNames"].([]any) + if ok && len(serverNames) > 0 { + rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string) + } else { + rltyData["serverName"] = "" + } + + return rltyData +} + +func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage { + outbound := Outbound{} + usersData := make([]UserVnext, 1) + + usersData[0].ID = client.ID + usersData[0].Level = 8 + if inbound.Protocol == model.VMESS { + usersData[0].Security = client.Security + } + if inbound.Protocol == model.VLESS { + usersData[0].Flow = client.Flow + usersData[0].Encryption = encryption + } + + vnextData := make([]VnextSetting, 1) + vnextData[0] = VnextSetting{ + Address: inbound.Listen, + Port: inbound.Port, + Users: usersData, + } + + outbound.Protocol = string(inbound.Protocol) + outbound.Tag = "proxy" + if s.mux != "" { + outbound.Mux = json_util.RawMessage(s.mux) + } + outbound.StreamSettings = streamSettings + outbound.Settings = OutboundSettings{ + Vnext: vnextData, + } + + result, _ := json.MarshalIndent(outbound, "", " ") + return result +} + +func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage { + outbound := Outbound{} + + serverData := make([]ServerSetting, 1) + serverData[0] = ServerSetting{ + Address: inbound.Listen, + Port: inbound.Port, + Level: 8, + Password: client.Password, + } + + if inbound.Protocol == model.Shadowsocks { + var inboundSettings map[string]any + json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + method, _ := inboundSettings["method"].(string) + serverData[0].Method = method + + // server password in multi-user 2022 protocols + if strings.HasPrefix(method, "2022") { + if serverPassword, ok := inboundSettings["password"].(string); ok { + serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password) + } + } + } + + outbound.Protocol = string(inbound.Protocol) + outbound.Tag = "proxy" + if s.mux != "" { + outbound.Mux = json_util.RawMessage(s.mux) + } + outbound.StreamSettings = streamSettings + outbound.Settings = OutboundSettings{ + Servers: serverData, + } + + result, _ := json.MarshalIndent(outbound, "", " ") + return result +} + +type Outbound struct { + Protocol string `json:"protocol"` + Tag string `json:"tag"` + StreamSettings json_util.RawMessage `json:"streamSettings"` + Mux json_util.RawMessage `json:"mux,omitempty"` + ProxySettings map[string]any `json:"proxySettings,omitempty"` + Settings OutboundSettings `json:"settings,omitempty"` +} + +type OutboundSettings struct { + Vnext []VnextSetting `json:"vnext,omitempty"` + Servers []ServerSetting `json:"servers,omitempty"` +} + +type VnextSetting struct { + Address string `json:"address"` + Port int `json:"port"` + Users []UserVnext `json:"users"` +} + +type UserVnext struct { + Encryption string `json:"encryption,omitempty"` + Flow string `json:"flow,omitempty"` + ID string `json:"id"` + Security string `json:"security,omitempty"` + Level int `json:"level"` +} + +type ServerSetting struct { + Password string `json:"password"` + Level int `json:"level"` + Address string `json:"address"` + Port int `json:"port"` + Flow string `json:"flow,omitempty"` + Method string `json:"method,omitempty"` +} diff --git a/sub/subService.go b/sub/subService.go new file mode 100644 index 0000000..78e6a2c --- /dev/null +++ b/sub/subService.go @@ -0,0 +1,985 @@ +package sub + +import ( + "encoding/base64" + "fmt" + "net/url" + "strings" + "time" + + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/common" + "x-ui/util/random" + "x-ui/web/service" + "x-ui/xray" + + "github.com/goccy/go-json" +) + +type SubService struct { + address string + showInfo bool + remarkModel string + datepicker string + inboundService service.InboundService + settingService service.SettingService +} + +func NewSubService(showInfo bool, remarkModel string) *SubService { + return &SubService{ + showInfo: showInfo, + remarkModel: remarkModel, + } +} + +func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) { + s.address = host + var result []string + var header string + var traffic xray.ClientTraffic + var clientTraffics []xray.ClientTraffic + inbounds, err := s.getInboundsBySubId(subId) + if err != nil { + return nil, "", err + } + + if len(inbounds) == 0 { + return nil, "", common.NewError("No inbounds found with ", subId) + } + + s.datepicker, err = s.settingService.GetDatepicker() + if err != nil { + s.datepicker = "gregorian" + } + for _, inbound := range inbounds { + clients, err := s.inboundService.GetClients(inbound) + if err != nil { + logger.Error("SubService - GetClients: Unable to get clients from inbound") + } + if clients == nil { + continue + } + if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { + listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings) + if err == nil { + inbound.Listen = listen + inbound.Port = port + inbound.StreamSettings = streamSettings + } + } + for _, client := range clients { + if client.Enable && client.SubID == subId { + link := s.getLink(inbound, client.Email) + result = append(result, link) + clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) + } + } + } + + // Prepare statistics + for index, clientTraffic := range clientTraffics { + if index == 0 { + traffic.Up = clientTraffic.Up + traffic.Down = clientTraffic.Down + traffic.Total = clientTraffic.Total + if clientTraffic.ExpiryTime > 0 { + traffic.ExpiryTime = clientTraffic.ExpiryTime + } + } else { + traffic.Up += clientTraffic.Up + traffic.Down += clientTraffic.Down + if traffic.Total == 0 || clientTraffic.Total == 0 { + traffic.Total = 0 + } else { + traffic.Total += clientTraffic.Total + } + if clientTraffic.ExpiryTime != traffic.ExpiryTime { + traffic.ExpiryTime = 0 + } + } + } + header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) + return result, header, nil +} + +func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { + db := database.GetDB() + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in ( + SELECT DISTINCT inbounds.id + FROM inbounds, + JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client + WHERE + protocol in ('vmess','vless','trojan','shadowsocks') + AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ? + )`, subId, true).Find(&inbounds).Error + if err != nil { + return nil, err + } + return inbounds, nil +} + +func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic { + for _, traffic := range traffics { + if traffic.Email == email { + return traffic + } + } + return xray.ClientTraffic{} +} + +func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) { + db := database.GetDB() + var inbound *model.Inbound + err := db.Model(model.Inbound{}). + Where("JSON_TYPE(settings, '$.fallbacks') = 'array'"). + Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest). + Find(&inbound).Error + if err != nil { + return "", 0, "", err + } + + var stream map[string]any + json.Unmarshal([]byte(streamSettings), &stream) + var masterStream map[string]any + json.Unmarshal([]byte(inbound.StreamSettings), &masterStream) + stream["security"] = masterStream["security"] + stream["tlsSettings"] = masterStream["tlsSettings"] + stream["externalProxy"] = masterStream["externalProxy"] + modifiedStream, _ := json.MarshalIndent(stream, "", " ") + + return inbound.Listen, inbound.Port, string(modifiedStream), nil +} + +func (s *SubService) getLink(inbound *model.Inbound, email string) string { + switch inbound.Protocol { + case "vmess": + return s.genVmessLink(inbound, email) + case "vless": + return s.genVlessLink(inbound, email) + case "trojan": + return s.genTrojanLink(inbound, email) + case "shadowsocks": + return s.genShadowsocksLink(inbound, email) + } + return "" +} + +func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { + if inbound.Protocol != model.VMESS { + return "" + } + obj := map[string]any{ + "v": "2", + "add": s.address, + "port": inbound.Port, + "type": "none", + } + var stream map[string]any + json.Unmarshal([]byte(inbound.StreamSettings), &stream) + network, _ := stream["network"].(string) + obj["net"] = network + switch network { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + typeStr, _ := header["type"].(string) + obj["type"] = typeStr + if typeStr == "http" { + request := header["request"].(map[string]any) + requestPath, _ := request["path"].([]any) + obj["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]any) + obj["host"] = searchHost(headers) + } + case "kcp": + kcp, _ := stream["kcpSettings"].(map[string]any) + header, _ := kcp["header"].(map[string]any) + obj["type"], _ = header["type"].(string) + obj["path"], _ = kcp["seed"].(string) + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + obj["path"] = ws["path"].(string) + if host, ok := ws["host"].(string); ok && len(host) > 0 { + obj["host"] = host + } else { + headers, _ := ws["headers"].(map[string]any) + obj["host"] = searchHost(headers) + } + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + obj["path"] = grpc["serviceName"].(string) + obj["authority"] = grpc["authority"].(string) + if grpc["multiMode"].(bool) { + obj["type"] = "multi" + } + case "httpupgrade": + httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) + obj["path"] = httpupgrade["path"].(string) + if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { + obj["host"] = host + } else { + headers, _ := httpupgrade["headers"].(map[string]any) + obj["host"] = searchHost(headers) + } + case "xhttp": + xhttp, _ := stream["xhttpSettings"].(map[string]any) + obj["path"] = xhttp["path"].(string) + if host, ok := xhttp["host"].(string); ok && len(host) > 0 { + obj["host"] = host + } else { + headers, _ := xhttp["headers"].(map[string]any) + obj["host"] = searchHost(headers) + } + obj["mode"] = xhttp["mode"].(string) + } + security, _ := stream["security"].(string) + obj["tls"] = security + if security == "tls" { + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) + if len(alpns) > 0 { + var alpn []string + for _, a := range alpns { + alpn = append(alpn, a.(string)) + } + obj["alpn"] = strings.Join(alpn, ",") + } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + obj["sni"], _ = sniValue.(string) + } + + tlsSettings, _ := searchKey(tlsSetting, "settings") + if tlsSetting != nil { + if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { + obj["fp"], _ = fpValue.(string) + } + } + } + + clients, _ := s.inboundService.GetClients(inbound) + clientIndex := -1 + for i, client := range clients { + if client.Email == email { + clientIndex = i + break + } + } + obj["id"] = clients[clientIndex].ID + obj["scy"] = clients[clientIndex].Security + + externalProxies, _ := stream["externalProxy"].([]any) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]any) + newSecurity, _ := ep["forceTls"].(string) + newObj := map[string]any{} + for key, value := range obj { + if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) { + newObj[key] = value + } + } + newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string)) + newObj["add"] = ep["dest"].(string) + newObj["port"] = int(ep["port"].(float64)) + + if newSecurity != "same" { + newObj["tls"] = newSecurity + } + if index > 0 { + links += "\n" + } + jsonStr, _ := json.MarshalIndent(newObj, "", " ") + links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) + } + return links + } + + obj["ps"] = s.genRemark(inbound, email, "") + + jsonStr, _ := json.MarshalIndent(obj, "", " ") + return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) +} + +func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { + address := s.address + if inbound.Protocol != model.VLESS { + return "" + } + var vlessSettings model.VLESSSettings + _ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings) + + var stream map[string]any + json.Unmarshal([]byte(inbound.StreamSettings), &stream) + clients, _ := s.inboundService.GetClients(inbound) + clientIndex := -1 + for i, client := range clients { + if client.Email == email { + clientIndex = i + break + } + } + uuid := clients[clientIndex].ID + port := inbound.Port + streamNetwork := stream["network"].(string) + params := make(map[string]string) + if vlessSettings.Encryption != "" { + params["encryption"] = vlessSettings.Encryption + } + params["type"] = streamNetwork + + switch streamNetwork { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + typeStr, _ := header["type"].(string) + if typeStr == "http" { + request := header["request"].(map[string]any) + requestPath, _ := request["path"].([]any) + params["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]any) + params["host"] = searchHost(headers) + params["headerType"] = "http" + } + case "kcp": + kcp, _ := stream["kcpSettings"].(map[string]any) + header, _ := kcp["header"].(map[string]any) + params["headerType"] = header["type"].(string) + params["seed"] = kcp["seed"].(string) + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + params["path"] = ws["path"].(string) + if host, ok := ws["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := ws["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + params["serviceName"] = grpc["serviceName"].(string) + params["authority"], _ = grpc["authority"].(string) + if grpc["multiMode"].(bool) { + params["mode"] = "multi" + } + case "httpupgrade": + httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) + params["path"] = httpupgrade["path"].(string) + if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := httpupgrade["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + case "xhttp": + xhttp, _ := stream["xhttpSettings"].(map[string]any) + params["path"] = xhttp["path"].(string) + if host, ok := xhttp["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := xhttp["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + params["mode"] = xhttp["mode"].(string) + } + security, _ := stream["security"].(string) + if security == "tls" { + params["security"] = "tls" + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) + var alpn []string + for _, a := range alpns { + alpn = append(alpn, a.(string)) + } + if len(alpn) > 0 { + params["alpn"] = strings.Join(alpn, ",") + } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } + + tlsSettings, _ := searchKey(tlsSetting, "settings") + if tlsSetting != nil { + if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { + params["fp"], _ = fpValue.(string) + } + } + + if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { + params["flow"] = clients[clientIndex].Flow + } + } + + if security == "reality" { + params["security"] = "reality" + realitySetting, _ := stream["realitySettings"].(map[string]any) + realitySettings, _ := searchKey(realitySetting, "settings") + if realitySetting != nil { + if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { + sNames, _ := sniValue.([]any) + params["sni"] = sNames[random.Num(len(sNames))].(string) + } + if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { + params["pbk"], _ = pbkValue.(string) + } + if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { + shortIds, _ := sidValue.([]any) + params["sid"] = shortIds[random.Num(len(shortIds))].(string) + } + if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { + if fp, ok := fpValue.(string); ok && len(fp) > 0 { + params["fp"] = fp + } + } + if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { + if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { + params["pqv"] = pqv + } + } + params["spx"] = "/" + random.Seq(15) + } + + if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { + params["flow"] = clients[clientIndex].Flow + } + } + + if security != "tls" && security != "reality" { + params["security"] = "none" + } + + externalProxies, _ := stream["externalProxy"].([]any) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]any) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port) + + if newSecurity != "same" { + params["security"] = newSecurity + } else { + params["security"] = security + } + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { + q.Add(k, v) + } + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) + + if index > 0 { + links += "\n" + } + links += url.String() + } + return links + } + + link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port) + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + q.Add(k, v) + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, "") + return url.String() +} + +func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { + address := s.address + if inbound.Protocol != model.Trojan { + return "" + } + var stream map[string]any + json.Unmarshal([]byte(inbound.StreamSettings), &stream) + clients, _ := s.inboundService.GetClients(inbound) + clientIndex := -1 + for i, client := range clients { + if client.Email == email { + clientIndex = i + break + } + } + password := clients[clientIndex].Password + port := inbound.Port + streamNetwork := stream["network"].(string) + params := make(map[string]string) + params["type"] = streamNetwork + + switch streamNetwork { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + typeStr, _ := header["type"].(string) + if typeStr == "http" { + request := header["request"].(map[string]any) + requestPath, _ := request["path"].([]any) + params["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]any) + params["host"] = searchHost(headers) + params["headerType"] = "http" + } + case "kcp": + kcp, _ := stream["kcpSettings"].(map[string]any) + header, _ := kcp["header"].(map[string]any) + params["headerType"] = header["type"].(string) + params["seed"] = kcp["seed"].(string) + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + params["path"] = ws["path"].(string) + if host, ok := ws["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := ws["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + params["serviceName"] = grpc["serviceName"].(string) + params["authority"], _ = grpc["authority"].(string) + if grpc["multiMode"].(bool) { + params["mode"] = "multi" + } + case "httpupgrade": + httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) + params["path"] = httpupgrade["path"].(string) + if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := httpupgrade["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + case "xhttp": + xhttp, _ := stream["xhttpSettings"].(map[string]any) + params["path"] = xhttp["path"].(string) + if host, ok := xhttp["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := xhttp["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + params["mode"] = xhttp["mode"].(string) + } + security, _ := stream["security"].(string) + if security == "tls" { + params["security"] = "tls" + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) + var alpn []string + for _, a := range alpns { + alpn = append(alpn, a.(string)) + } + if len(alpn) > 0 { + params["alpn"] = strings.Join(alpn, ",") + } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } + + tlsSettings, _ := searchKey(tlsSetting, "settings") + if tlsSetting != nil { + if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { + params["fp"], _ = fpValue.(string) + } + } + } + + if security == "reality" { + params["security"] = "reality" + realitySetting, _ := stream["realitySettings"].(map[string]any) + realitySettings, _ := searchKey(realitySetting, "settings") + if realitySetting != nil { + if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { + sNames, _ := sniValue.([]any) + params["sni"] = sNames[random.Num(len(sNames))].(string) + } + if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { + params["pbk"], _ = pbkValue.(string) + } + if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { + shortIds, _ := sidValue.([]any) + params["sid"] = shortIds[random.Num(len(shortIds))].(string) + } + if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { + if fp, ok := fpValue.(string); ok && len(fp) > 0 { + params["fp"] = fp + } + } + if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { + if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { + params["pqv"] = pqv + } + } + params["spx"] = "/" + random.Seq(15) + } + + if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { + params["flow"] = clients[clientIndex].Flow + } + } + + if security != "tls" && security != "reality" { + params["security"] = "none" + } + + externalProxies, _ := stream["externalProxy"].([]any) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]any) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port) + + if newSecurity != "same" { + params["security"] = newSecurity + } else { + params["security"] = security + } + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { + q.Add(k, v) + } + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) + + if index > 0 { + links += "\n" + } + links += url.String() + } + return links + } + + link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port) + + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + q.Add(k, v) + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, "") + return url.String() +} + +func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { + address := s.address + if inbound.Protocol != model.Shadowsocks { + return "" + } + var stream map[string]any + json.Unmarshal([]byte(inbound.StreamSettings), &stream) + clients, _ := s.inboundService.GetClients(inbound) + + var settings map[string]any + json.Unmarshal([]byte(inbound.Settings), &settings) + inboundPassword := settings["password"].(string) + method := settings["method"].(string) + clientIndex := -1 + for i, client := range clients { + if client.Email == email { + clientIndex = i + break + } + } + streamNetwork := stream["network"].(string) + params := make(map[string]string) + params["type"] = streamNetwork + + switch streamNetwork { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + typeStr, _ := header["type"].(string) + if typeStr == "http" { + request := header["request"].(map[string]any) + requestPath, _ := request["path"].([]any) + params["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]any) + params["host"] = searchHost(headers) + params["headerType"] = "http" + } + case "kcp": + kcp, _ := stream["kcpSettings"].(map[string]any) + header, _ := kcp["header"].(map[string]any) + params["headerType"] = header["type"].(string) + params["seed"] = kcp["seed"].(string) + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + params["path"] = ws["path"].(string) + if host, ok := ws["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := ws["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + params["serviceName"] = grpc["serviceName"].(string) + params["authority"], _ = grpc["authority"].(string) + if grpc["multiMode"].(bool) { + params["mode"] = "multi" + } + case "httpupgrade": + httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) + params["path"] = httpupgrade["path"].(string) + if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := httpupgrade["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + case "xhttp": + xhttp, _ := stream["xhttpSettings"].(map[string]any) + params["path"] = xhttp["path"].(string) + if host, ok := xhttp["host"].(string); ok && len(host) > 0 { + params["host"] = host + } else { + headers, _ := xhttp["headers"].(map[string]any) + params["host"] = searchHost(headers) + } + params["mode"] = xhttp["mode"].(string) + } + + security, _ := stream["security"].(string) + if security == "tls" { + params["security"] = "tls" + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + alpns, _ := tlsSetting["alpn"].([]any) + var alpn []string + for _, a := range alpns { + alpn = append(alpn, a.(string)) + } + if len(alpn) > 0 { + params["alpn"] = strings.Join(alpn, ",") + } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } + + tlsSettings, _ := searchKey(tlsSetting, "settings") + if tlsSetting != nil { + if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { + params["fp"], _ = fpValue.(string) + } + } + } + + encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password) + if method[0] == '2' { + encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) + } + + externalProxies, _ := stream["externalProxy"].([]any) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]any) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port) + + if newSecurity != "same" { + params["security"] = newSecurity + } else { + params["security"] = security + } + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { + q.Add(k, v) + } + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) + + if index > 0 { + links += "\n" + } + links += url.String() + } + return links + } + + link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port) + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + q.Add(k, v) + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, "") + return url.String() +} + +func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string { + separationChar := string(s.remarkModel[0]) + orderChars := s.remarkModel[1:] + orders := map[byte]string{ + 'i': "", + 'e': "", + 'o': "", + } + if len(email) > 0 { + orders['e'] = email + } + if len(inbound.Remark) > 0 { + orders['i'] = inbound.Remark + } + if len(extra) > 0 { + orders['o'] = extra + } + + var remark []string + for i := 0; i < len(orderChars); i++ { + char := orderChars[i] + order, exists := orders[char] + if exists && order != "" { + remark = append(remark, order) + } + } + + if s.showInfo { + statsExist := false + var stats xray.ClientTraffic + for _, clientStat := range inbound.ClientStats { + if clientStat.Email == email { + stats = clientStat + statsExist = true + break + } + } + + // Get remained days + if statsExist { + if !stats.Enable { + return fmt.Sprintf("⛔️N/A%s%s", separationChar, strings.Join(remark, separationChar)) + } + if vol := stats.Total - (stats.Up + stats.Down); vol > 0 { + remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊")) + } + now := time.Now().Unix() + switch exp := stats.ExpiryTime / 1000; { + case exp > 0: + remainingSeconds := exp - now + days := remainingSeconds / 86400 + hours := (remainingSeconds % 86400) / 3600 + minutes := (remainingSeconds % 3600) / 60 + if days > 0 { + if hours > 0 { + remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours)) + } else { + remark = append(remark, fmt.Sprintf("%dD⏳", days)) + } + } else if hours > 0 { + remark = append(remark, fmt.Sprintf("%dH⏳", hours)) + } else { + remark = append(remark, fmt.Sprintf("%dM⏳", minutes)) + } + case exp < 0: + days := exp / -86400 + hours := (exp % -86400) / 3600 + minutes := (exp % -3600) / 60 + if days > 0 { + if hours > 0 { + remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours)) + } else { + remark = append(remark, fmt.Sprintf("%dD⏳", days)) + } + } else if hours > 0 { + remark = append(remark, fmt.Sprintf("%dH⏳", hours)) + } else { + remark = append(remark, fmt.Sprintf("%dM⏳", minutes)) + } + } + } + } + return strings.Join(remark, separationChar) +} + +func searchKey(data any, key string) (any, bool) { + switch val := data.(type) { + case map[string]any: + for k, v := range val { + if k == key { + return v, true + } + if result, ok := searchKey(v, key); ok { + return result, true + } + } + case []any: + for _, v := range val { + if result, ok := searchKey(v, key); ok { + return result, true + } + } + } + return nil, false +} + +func searchHost(headers any) string { + data, _ := headers.(map[string]any) + for k, v := range data { + if strings.EqualFold(k, "host") { + switch v.(type) { + case []any: + hosts, _ := v.([]any) + if len(hosts) > 0 { + return hosts[0].(string) + } else { + return "" + } + case any: + return v.(string) + } + } + } + + return "" +} diff --git a/util/common/err.go b/util/common/err.go new file mode 100644 index 0000000..618bf8f --- /dev/null +++ b/util/common/err.go @@ -0,0 +1,28 @@ +package common + +import ( + "errors" + "fmt" + + "x-ui/logger" +) + +func NewErrorf(format string, a ...any) error { + msg := fmt.Sprintf(format, a...) + return errors.New(msg) +} + +func NewError(a ...any) error { + msg := fmt.Sprintln(a...) + return errors.New(msg) +} + +func Recover(msg string) any { + panicErr := recover() + if panicErr != nil { + if msg != "" { + logger.Error(msg, "panic:", panicErr) + } + } + return panicErr +} diff --git a/util/common/format.go b/util/common/format.go new file mode 100644 index 0000000..c73e3a0 --- /dev/null +++ b/util/common/format.go @@ -0,0 +1,17 @@ +package common + +import ( + "fmt" +) + +func FormatTraffic(trafficBytes int64) string { + units := []string{"B", "KB", "MB", "GB", "TB", "PB"} + unitIndex := 0 + size := float64(trafficBytes) + + for size >= 1024 && unitIndex < len(units)-1 { + size /= 1024 + unitIndex++ + } + return fmt.Sprintf("%.2f%s", size, units[unitIndex]) +} diff --git a/util/common/multi_error.go b/util/common/multi_error.go new file mode 100644 index 0000000..ff9ff62 --- /dev/null +++ b/util/common/multi_error.go @@ -0,0 +1,30 @@ +package common + +import ( + "strings" +) + +type multiError []error + +func (e multiError) Error() string { + var r strings.Builder + r.WriteString("multierr: ") + for _, err := range e { + r.WriteString(err.Error()) + r.WriteString(" | ") + } + return r.String() +} + +func Combine(maybeError ...error) error { + var errs multiError + for _, err := range maybeError { + if err != nil { + errs = append(errs, err) + } + } + if len(errs) == 0 { + return nil + } + return errs +} diff --git a/util/common/random.go b/util/common/random.go new file mode 100644 index 0000000..ccd35e9 --- /dev/null +++ b/util/common/random.go @@ -0,0 +1,18 @@ +package common + +import ( + "crypto/rand" + "math/big" +) + +// RandomInt 返回一个 0 .. max-1 之间的随机整数(使用 crypto/rand) +func RandomInt(max int) int { + if max <= 0 { + return 0 + } + n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + return 0 + } + return int(n.Int64()) +} diff --git a/util/crypto/crypto.go b/util/crypto/crypto.go new file mode 100644 index 0000000..f600e7a --- /dev/null +++ b/util/crypto/crypto.go @@ -0,0 +1,15 @@ +package crypto + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashPasswordAsBcrypt(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hash), err +} + +func CheckPasswordHash(hash, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/util/json_util/json.go b/util/json_util/json.go new file mode 100644 index 0000000..54e3728 --- /dev/null +++ b/util/json_util/json.go @@ -0,0 +1,24 @@ +package json_util + +import ( + "errors" +) + +type RawMessage []byte + +// MarshalJSON: Customize json.RawMessage default behavior +func (m RawMessage) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON: sets *m to a copy of data. +func (m *RawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil +} diff --git a/util/random/random.go b/util/random/random.go new file mode 100644 index 0000000..67ee069 --- /dev/null +++ b/util/random/random.go @@ -0,0 +1,46 @@ +package random + +import ( + "math/rand" +) + +var ( + numSeq [10]rune + lowerSeq [26]rune + upperSeq [26]rune + numLowerSeq [36]rune + numUpperSeq [36]rune + allSeq [62]rune +) + +func init() { + for i := 0; i < 10; i++ { + numSeq[i] = rune('0' + i) + } + for i := 0; i < 26; i++ { + lowerSeq[i] = rune('a' + i) + upperSeq[i] = rune('A' + i) + } + + copy(numLowerSeq[:], numSeq[:]) + copy(numLowerSeq[len(numSeq):], lowerSeq[:]) + + copy(numUpperSeq[:], numSeq[:]) + copy(numUpperSeq[len(numSeq):], upperSeq[:]) + + copy(allSeq[:], numSeq[:]) + copy(allSeq[len(numSeq):], lowerSeq[:]) + copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) +} + +func Seq(n int) string { + runes := make([]rune, n) + for i := 0; i < n; i++ { + runes[i] = allSeq[rand.Intn(len(allSeq))] + } + return string(runes) +} + +func Num(n int) int { + return rand.Intn(n) +} diff --git a/util/reflect_util/reflect.go b/util/reflect_util/reflect.go new file mode 100644 index 0000000..1fdaec5 --- /dev/null +++ b/util/reflect_util/reflect.go @@ -0,0 +1,21 @@ +package reflect_util + +import "reflect" + +func GetFields(t reflect.Type) []reflect.StructField { + num := t.NumField() + fields := make([]reflect.StructField, 0, num) + for i := 0; i < num; i++ { + fields = append(fields, t.Field(i)) + } + return fields +} + +func GetFieldValues(v reflect.Value) []reflect.Value { + num := v.NumField() + fields := make([]reflect.Value, 0, num) + for i := 0; i < num; i++ { + fields = append(fields, v.Field(i)) + } + return fields +} diff --git a/util/sys/psutil.go b/util/sys/psutil.go new file mode 100644 index 0000000..3d7cac8 --- /dev/null +++ b/util/sys/psutil.go @@ -0,0 +1,8 @@ +package sys + +import ( + _ "unsafe" +) + +//go:linkname HostProc github.com/shirou/gopsutil/v4/internal/common.HostProc +func HostProc(combineWith ...string) string diff --git a/util/sys/sys_darwin.go b/util/sys/sys_darwin.go new file mode 100644 index 0000000..3f5b207 --- /dev/null +++ b/util/sys/sys_darwin.go @@ -0,0 +1,24 @@ +//go:build darwin +// +build darwin + +package sys + +import ( + "github.com/shirou/gopsutil/v4/net" +) + +func GetTCPCount() (int, error) { + stats, err := net.Connections("tcp") + if err != nil { + return 0, err + } + return len(stats), nil +} + +func GetUDPCount() (int, error) { + stats, err := net.Connections("udp") + if err != nil { + return 0, err + } + return len(stats), nil +} diff --git a/util/sys/sys_linux.go b/util/sys/sys_linux.go new file mode 100644 index 0000000..d4e6e8a --- /dev/null +++ b/util/sys/sys_linux.go @@ -0,0 +1,81 @@ +//go:build linux +// +build linux + +package sys + +import ( + "bytes" + "fmt" + "io" + "os" +) + +func getLinesNum(filename string) (int, error) { + file, err := os.Open(filename) + if err != nil { + return 0, err + } + defer file.Close() + + sum := 0 + buf := make([]byte, 8192) + for { + n, err := file.Read(buf) + + var buffPosition int + for { + i := bytes.IndexByte(buf[buffPosition:n], '\n') + if i < 0 { + break + } + buffPosition += i + 1 + sum++ + } + + if err == io.EOF { + break + } else if err != nil { + return 0, err + } + } + return sum, nil +} + +func GetTCPCount() (int, error) { + root := HostProc() + + tcp4, err := safeGetLinesNum(fmt.Sprintf("%v/net/tcp", root)) + if err != nil { + return 0, err + } + tcp6, err := safeGetLinesNum(fmt.Sprintf("%v/net/tcp6", root)) + if err != nil { + return 0, err + } + + return tcp4 + tcp6, nil +} + +func GetUDPCount() (int, error) { + root := HostProc() + + udp4, err := safeGetLinesNum(fmt.Sprintf("%v/net/udp", root)) + if err != nil { + return 0, err + } + udp6, err := safeGetLinesNum(fmt.Sprintf("%v/net/udp6", root)) + if err != nil { + return 0, err + } + + return udp4 + udp6, nil +} + +func safeGetLinesNum(path string) (int, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return 0, nil + } else if err != nil { + return 0, err + } + return getLinesNum(path) +} diff --git a/util/sys/sys_windows.go b/util/sys/sys_windows.go new file mode 100644 index 0000000..fd51d47 --- /dev/null +++ b/util/sys/sys_windows.go @@ -0,0 +1,30 @@ +//go:build windows +// +build windows + +package sys + +import ( + "errors" + + "github.com/shirou/gopsutil/v4/net" +) + +func GetConnectionCount(proto string) (int, error) { + if proto != "tcp" && proto != "udp" { + return 0, errors.New("invalid protocol") + } + + stats, err := net.Connections(proto) + if err != nil { + return 0, err + } + return len(stats), nil +} + +func GetTCPCount() (int, error) { + return GetConnectionCount("tcp") +} + +func GetUDPCount() (int, error) { + return GetConnectionCount("udp") +} diff --git a/web/assets/Vazirmatn-UI-NL-Regular.woff2 b/web/assets/Vazirmatn-UI-NL-Regular.woff2 new file mode 100644 index 0000000..3ce577a Binary files /dev/null and b/web/assets/Vazirmatn-UI-NL-Regular.woff2 differ diff --git a/web/assets/ant-design-vue/antd.min.css b/web/assets/ant-design-vue/antd.min.css new file mode 100644 index 0000000..aba0167 --- /dev/null +++ b/web/assets/ant-design-vue/antd.min.css @@ -0,0 +1,8 @@ +/*! + * + * ant-design-vue v1.7.8 + * + * Copyright 2017-present, ant-design-vue. + * All rights reserved. + * + */body,html{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;color:rgba(0,0,0,.65);font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-variant:tabular-nums;line-height:1.5;background-color:#fff;font-feature-settings:"tnum"}[tabindex="-1"]:focus{outline:none!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;color:rgba(0,0,0,.85);font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=number],input[type=password],input[type=text],textarea{-webkit-appearance:none}dl,ol,ul{margin-top:0;margin-bottom:1em}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#008771;text-decoration:none;background-color:transparent;outline:none;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects}a:hover{color:#18947b}a:active{color:#006154}a:active,a:hover{text-decoration:none;outline:0}a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none}code,kbd,pre,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;color:rgba(0,0,0,.45);text-align:left;caption-side:bottom}th{text-align:inherit}button,input,optgroup,select,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}::-moz-selection{color:#fff;background:#008771}::selection{color:#fff;background:#008771}.clearfix{zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}.anticon{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.anticon>*{line-height:1}.anticon svg{display:inline-block}.anticon:before{display:none}.anticon .anticon-icon{display:block}.anticon[tabindex]{cursor:pointer}.anticon-spin,.anticon-spin:before{display:inline-block;animation:loadingCircle 1s linear infinite}.fade-appear,.fade-enter,.fade-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.fade-appear.fade-appear-active,.fade-enter.fade-enter-active{animation-name:antFadeIn;animation-play-state:running}.fade-leave.fade-leave-active{animation-name:antFadeOut;animation-play-state:running;pointer-events:none}.fade-appear,.fade-enter{opacity:0}.fade-appear,.fade-enter,.fade-leave{animation-timing-function:linear}@keyframes antFadeIn{0%{opacity:0}to{opacity:1}}@keyframes antFadeOut{0%{opacity:1}to{opacity:0}}.move-up-appear,.move-up-enter,.move-up-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.move-up-appear.move-up-appear-active,.move-up-enter.move-up-enter-active{animation-name:antMoveUpIn;animation-play-state:running}.move-up-leave.move-up-leave-active{animation-name:antMoveUpOut;animation-play-state:running;pointer-events:none}.move-up-appear,.move-up-enter{opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-up-leave{animation-timing-function:cubic-bezier(.6,.04,.98,.34)}.move-down-appear,.move-down-enter,.move-down-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.move-down-appear.move-down-appear-active,.move-down-enter.move-down-enter-active{animation-name:antMoveDownIn;animation-play-state:running}.move-down-leave.move-down-leave-active{animation-name:antMoveDownOut;animation-play-state:running;pointer-events:none}.move-down-appear,.move-down-enter{opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-down-leave{animation-timing-function:cubic-bezier(.6,.04,.98,.34)}.move-left-appear,.move-left-enter,.move-left-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.move-left-appear.move-left-appear-active,.move-left-enter.move-left-enter-active{animation-name:antMoveLeftIn;animation-play-state:running}.move-left-leave.move-left-leave-active{animation-name:antMoveLeftOut;animation-play-state:running;pointer-events:none}.move-left-appear,.move-left-enter{opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-left-leave{animation-timing-function:cubic-bezier(.6,.04,.98,.34)}.move-right-appear,.move-right-enter,.move-right-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.move-right-appear.move-right-appear-active,.move-right-enter.move-right-enter-active{animation-name:antMoveRightIn;animation-play-state:running}.move-right-leave.move-right-leave-active{animation-name:antMoveRightOut;animation-play-state:running;pointer-events:none}.move-right-appear,.move-right-enter{opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-right-leave{animation-timing-function:cubic-bezier(.6,.04,.98,.34)}@keyframes antMoveDownIn{0%{transform:translateY(100%);transform-origin:0 0;opacity:0}to{transform:translateY(0);transform-origin:0 0;opacity:1}}@keyframes antMoveDownOut{0%{transform:translateY(0);transform-origin:0 0;opacity:1}to{transform:translateY(100%);transform-origin:0 0;opacity:0}}@keyframes antMoveLeftIn{0%{transform:translateX(-100%);transform-origin:0 0;opacity:0}to{transform:translateX(0);transform-origin:0 0;opacity:1}}@keyframes antMoveLeftOut{0%{transform:translateX(0);transform-origin:0 0;opacity:1}to{transform:translateX(-100%);transform-origin:0 0;opacity:0}}@keyframes antMoveRightIn{0%{transform:translateX(100%);transform-origin:0 0;opacity:0}to{transform:translateX(0);transform-origin:0 0;opacity:1}}@keyframes antMoveRightOut{0%{transform:translateX(0);transform-origin:0 0;opacity:1}to{transform:translateX(100%);transform-origin:0 0;opacity:0}}@keyframes antMoveUpIn{0%{transform:translateY(-100%);transform-origin:0 0;opacity:0}to{transform:translateY(0);transform-origin:0 0;opacity:1}}@keyframes antMoveUpOut{0%{transform:translateY(0);transform-origin:0 0;opacity:1}to{transform:translateY(-100%);transform-origin:0 0;opacity:0}}@keyframes loadingCircle{to{transform:rotate(1turn)}}[ant-click-animating-without-extra-node=true],[ant-click-animating=true]{position:relative}html{--antd-wave-shadow-color:#008771}.ant-click-animating-node,[ant-click-animating-without-extra-node=true]:after{position:absolute;top:0;right:0;bottom:0;left:0;display:block;border-radius:inherit;box-shadow:0 0 0 0 #008771;box-shadow:0 0 0 0 var(--antd-wave-shadow-color);opacity:.2;animation:fadeEffect 2s cubic-bezier(.08,.82,.17,1),waveEffect .4s cubic-bezier(.08,.82,.17,1);animation-fill-mode:forwards;content:"";pointer-events:none}@keyframes waveEffect{to{box-shadow:0 0 0 #008771;box-shadow:0 0 0 6px var(--antd-wave-shadow-color)}}@keyframes fadeEffect{to{opacity:0}}.slide-up-appear,.slide-up-enter,.slide-up-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.slide-up-appear.slide-up-appear-active,.slide-up-enter.slide-up-enter-active{animation-name:antSlideUpIn;animation-play-state:running}.slide-up-leave.slide-up-leave-active{animation-name:antSlideUpOut;animation-play-state:running;pointer-events:none}.slide-up-appear,.slide-up-enter{opacity:0;animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-up-leave{animation-timing-function:cubic-bezier(.755,.05,.855,.06)}.slide-down-appear,.slide-down-enter,.slide-down-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.slide-down-appear.slide-down-appear-active,.slide-down-enter.slide-down-enter-active{animation-name:antSlideDownIn;animation-play-state:running}.slide-down-leave.slide-down-leave-active{animation-name:antSlideDownOut;animation-play-state:running;pointer-events:none}.slide-down-appear,.slide-down-enter{opacity:0;animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-down-leave{animation-timing-function:cubic-bezier(.755,.05,.855,.06)}.slide-left-appear,.slide-left-enter,.slide-left-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.slide-left-appear.slide-left-appear-active,.slide-left-enter.slide-left-enter-active{animation-name:antSlideLeftIn;animation-play-state:running}.slide-left-leave.slide-left-leave-active{animation-name:antSlideLeftOut;animation-play-state:running;pointer-events:none}.slide-left-appear,.slide-left-enter{opacity:0;animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-left-leave{animation-timing-function:cubic-bezier(.755,.05,.855,.06)}.slide-right-appear,.slide-right-enter,.slide-right-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.slide-right-appear.slide-right-appear-active,.slide-right-enter.slide-right-enter-active{animation-name:antSlideRightIn;animation-play-state:running}.slide-right-leave.slide-right-leave-active{animation-name:antSlideRightOut;animation-play-state:running;pointer-events:none}.slide-right-appear,.slide-right-enter{opacity:0;animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-right-leave{animation-timing-function:cubic-bezier(.755,.05,.855,.06)}@keyframes antSlideUpIn{0%{transform:scaleY(.8);transform-origin:0 0;opacity:0}to{transform:scaleY(1);transform-origin:0 0;opacity:1}}@keyframes antSlideUpOut{0%{transform:scaleY(1);transform-origin:0 0;opacity:1}to{transform:scaleY(.8);transform-origin:0 0;opacity:0}}@keyframes antSlideDownIn{0%{transform:scaleY(.8);transform-origin:100% 100%;opacity:0}to{transform:scaleY(1);transform-origin:100% 100%;opacity:1}}@keyframes antSlideDownOut{0%{transform:scaleY(1);transform-origin:100% 100%;opacity:1}to{transform:scaleY(.8);transform-origin:100% 100%;opacity:0}}@keyframes antSlideLeftIn{0%{transform:scaleX(.8);transform-origin:0 0;opacity:0}to{transform:scaleX(1);transform-origin:0 0;opacity:1}}@keyframes antSlideLeftOut{0%{transform:scaleX(1);transform-origin:0 0;opacity:1}to{transform:scaleX(.8);transform-origin:0 0;opacity:0}}@keyframes antSlideRightIn{0%{transform:scaleX(.8);transform-origin:100% 0;opacity:0}to{transform:scaleX(1);transform-origin:100% 0;opacity:1}}@keyframes antSlideRightOut{0%{transform:scaleX(1);transform-origin:100% 0;opacity:1}to{transform:scaleX(.8);transform-origin:100% 0;opacity:0}}.swing-appear,.swing-enter{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.swing-appear.swing-appear-active,.swing-enter.swing-enter-active{animation-name:antSwingIn;animation-play-state:running}@keyframes antSwingIn{0%,to{transform:translateX(0)}20%{transform:translateX(-10px)}40%{transform:translateX(10px)}60%{transform:translateX(-5px)}80%{transform:translateX(5px)}}.zoom-appear,.zoom-enter,.zoom-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.zoom-appear.zoom-appear-active,.zoom-enter.zoom-enter-active{animation-name:antZoomIn;animation-play-state:running}.zoom-leave.zoom-leave-active{animation-name:antZoomOut;animation-play-state:running;pointer-events:none}.zoom-appear,.zoom-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-big-appear,.zoom-big-enter,.zoom-big-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.zoom-big-appear.zoom-big-appear-active,.zoom-big-enter.zoom-big-enter-active{animation-name:antZoomBigIn;animation-play-state:running}.zoom-big-leave.zoom-big-leave-active{animation-name:antZoomBigOut;animation-play-state:running;pointer-events:none}.zoom-big-appear,.zoom-big-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-big-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-big-fast-appear,.zoom-big-fast-enter,.zoom-big-fast-leave{animation-duration:.1s;animation-fill-mode:both;animation-play-state:paused}.zoom-big-fast-appear.zoom-big-fast-appear-active,.zoom-big-fast-enter.zoom-big-fast-enter-active{animation-name:antZoomBigIn;animation-play-state:running}.zoom-big-fast-leave.zoom-big-fast-leave-active{animation-name:antZoomBigOut;animation-play-state:running;pointer-events:none}.zoom-big-fast-appear,.zoom-big-fast-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-big-fast-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-up-appear,.zoom-up-enter,.zoom-up-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.zoom-up-appear.zoom-up-appear-active,.zoom-up-enter.zoom-up-enter-active{animation-name:antZoomUpIn;animation-play-state:running}.zoom-up-leave.zoom-up-leave-active{animation-name:antZoomUpOut;animation-play-state:running;pointer-events:none}.zoom-up-appear,.zoom-up-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-up-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-down-appear,.zoom-down-enter,.zoom-down-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.zoom-down-appear.zoom-down-appear-active,.zoom-down-enter.zoom-down-enter-active{animation-name:antZoomDownIn;animation-play-state:running}.zoom-down-leave.zoom-down-leave-active{animation-name:antZoomDownOut;animation-play-state:running;pointer-events:none}.zoom-down-appear,.zoom-down-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-down-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-left-appear,.zoom-left-enter,.zoom-left-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.zoom-left-appear.zoom-left-appear-active,.zoom-left-enter.zoom-left-enter-active{animation-name:antZoomLeftIn;animation-play-state:running}.zoom-left-leave.zoom-left-leave-active{animation-name:antZoomLeftOut;animation-play-state:running;pointer-events:none}.zoom-left-appear,.zoom-left-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-left-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-right-appear,.zoom-right-enter,.zoom-right-leave{animation-duration:.2s;animation-fill-mode:both;animation-play-state:paused}.zoom-right-appear.zoom-right-appear-active,.zoom-right-enter.zoom-right-enter-active{animation-name:antZoomRightIn;animation-play-state:running}.zoom-right-leave.zoom-right-leave-active{animation-name:antZoomRightOut;animation-play-state:running;pointer-events:none}.zoom-right-appear,.zoom-right-enter{transform:scale(0);opacity:0;animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-right-leave{animation-timing-function:cubic-bezier(.78,.14,.15,.86)}@keyframes antZoomIn{0%{transform:scale(.2);opacity:0}to{transform:scale(1);opacity:1}}@keyframes antZoomOut{0%{transform:scale(1)}to{transform:scale(.2);opacity:0}}@keyframes antZoomBigIn{0%{transform:scale(.8);opacity:0}to{transform:scale(1);opacity:1}}@keyframes antZoomBigOut{0%{transform:scale(1)}to{transform:scale(.8);opacity:0}}@keyframes antZoomUpIn{0%{transform:scale(.8);transform-origin:50% 0;opacity:0}to{transform:scale(1);transform-origin:50% 0}}@keyframes antZoomUpOut{0%{transform:scale(1);transform-origin:50% 0}to{transform:scale(.8);transform-origin:50% 0;opacity:0}}@keyframes antZoomLeftIn{0%{transform:scale(.8);transform-origin:0 50%;opacity:0}to{transform:scale(1);transform-origin:0 50%}}@keyframes antZoomLeftOut{0%{transform:scale(1);transform-origin:0 50%}to{transform:scale(.8);transform-origin:0 50%;opacity:0}}@keyframes antZoomRightIn{0%{transform:scale(.8);transform-origin:100% 50%;opacity:0}to{transform:scale(1);transform-origin:100% 50%}}@keyframes antZoomRightOut{0%{transform:scale(1);transform-origin:100% 50%}to{transform:scale(.8);transform-origin:100% 50%;opacity:0}}@keyframes antZoomDownIn{0%{transform:scale(.8);transform-origin:50% 100%;opacity:0}to{transform:scale(1);transform-origin:50% 100%}}@keyframes antZoomDownOut{0%{transform:scale(1);transform-origin:50% 100%}to{transform:scale(.8);transform-origin:50% 100%;opacity:0}}.ant-motion-collapse-legacy{overflow:hidden}.ant-motion-collapse,.ant-motion-collapse-legacy-active{transition:height .15s cubic-bezier(.645,.045,.355,1),opacity .15s cubic-bezier(.645,.045,.355,1)!important}.ant-motion-collapse{overflow:hidden}.ant-affix{position:fixed;z-index:10}.ant-alert{box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;padding:8px 15px 8px 37px;word-wrap:break-word;border-radius:1rem}.ant-alert.ant-alert-no-icon{padding:8px 15px}.ant-alert.ant-alert-closable{padding-right:30px}.ant-alert-icon{position:absolute;top:11.5px;left:16px}.ant-alert-description{display:none;font-size:14px;line-height:22px}.ant-alert-success{background-color:#b3c7c0;border:1px solid #53ad95}.ant-alert-success .ant-alert-icon{color:#008771}.ant-alert-info{background-color:#e6f7ff;border:1px solid #91d5ff}.ant-alert-info .ant-alert-icon{color:#1890ff}.ant-alert-warning{background-color:#fffbe6;border:1px solid #ffe58f}.ant-alert-warning .ant-alert-icon{color:#faad14}.ant-alert-error{background-color:#fff1f0;border:1px solid #ffa39e}.ant-alert-error .ant-alert-icon{color:#f5222d}.ant-alert-close-icon{position:absolute;top:8px;right:16px;padding:0;overflow:hidden;font-size:12px;line-height:22px;background-color:transparent;border:none;outline:none;cursor:pointer}.ant-alert-close-icon .anticon-close{color:rgba(0,0,0,.45);transition:color .3s}.ant-alert-close-icon .anticon-close:hover{color:rgba(0,0,0,.75)}.ant-alert-close-text{color:rgba(0,0,0,.45);transition:color .3s}.ant-alert-close-text:hover{color:rgba(0,0,0,.75)}.ant-alert-with-description{position:relative;padding:15px 15px 15px 64px;color:rgba(0,0,0,.65);line-height:1.5;border-radius:1rem}.ant-alert-with-description.ant-alert-no-icon{padding:15px}.ant-alert-with-description .ant-alert-icon{position:absolute;top:16px;left:24px;font-size:24px}.ant-alert-with-description .ant-alert-close-icon{position:absolute;top:16px;right:16px;font-size:14px;cursor:pointer}.ant-alert-with-description .ant-alert-message{display:block;margin-bottom:4px;color:rgba(0,0,0,.85);font-size:16px}.ant-alert-message{color:rgba(0,0,0,.85)}.ant-alert-with-description .ant-alert-description{display:block}.ant-alert.ant-alert-closing{height:0!important;margin:0;padding-top:0;padding-bottom:0;transform-origin:50% 0;transition:all .3s cubic-bezier(.78,.14,.15,.86)}.ant-alert-slide-up-leave{animation:antAlertSlideUpOut .3s cubic-bezier(.78,.14,.15,.86);animation-fill-mode:both}.ant-alert-banner{margin-bottom:0;border:0;border-radius:0}@keyframes antAlertSlideUpIn{0%{transform:scaleY(0);transform-origin:0 0;opacity:0}to{transform:scaleY(1);transform-origin:0 0;opacity:1}}@keyframes antAlertSlideUpOut{0%{transform:scaleY(1);transform-origin:0 0;opacity:1}to{transform:scaleY(0);transform-origin:0 0;opacity:0}}.ant-anchor{box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;padding:0 0 0 2px}.ant-anchor-wrapper{margin-left:-4px;padding-left:4px;overflow:auto;background-color:#fff}.ant-anchor-ink{position:absolute;top:0;left:0;height:100%}.ant-anchor-ink:before{position:relative;display:block;width:2px;height:100%;margin:0 auto;background-color:#e8e8e8;content:" "}.ant-anchor-ink-ball{position:absolute;left:50%;display:none;width:8px;height:8px;background-color:#fff;border:2px solid #008771;border-radius:8px;transform:translateX(-50%);transition:top .3s ease-in-out}.ant-anchor-ink-ball.visible{display:inline-block}.ant-anchor.fixed .ant-anchor-ink .ant-anchor-ink-ball{display:none}.ant-anchor-link{padding:7px 0 7px 16px;line-height:1.143}.ant-anchor-link-title{position:relative;display:block;margin-bottom:6px;overflow:hidden;color:rgba(0,0,0,.65);white-space:nowrap;text-overflow:ellipsis;transition:all .3s}.ant-anchor-link-title:only-child{margin-bottom:0}.ant-anchor-link-active>.ant-anchor-link-title{color:#008771}.ant-anchor-link .ant-anchor-link{padding-top:5px;padding-bottom:5px}.ant-select-auto-complete{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"}.ant-select-auto-complete.ant-select .ant-select-selection{border:0;box-shadow:none}.ant-select-auto-complete.ant-select .ant-select-selection__rendered{height:100%;margin-right:0;margin-left:0;line-height:32px}.ant-select-auto-complete.ant-select .ant-select-selection__placeholder{margin-right:12px;margin-left:12px}.ant-select-auto-complete.ant-select .ant-select-selection--single{height:auto}.ant-select-auto-complete.ant-select .ant-select-search--inline{position:static;float:left}.ant-select-auto-complete.ant-select-allow-clear .ant-select-selection:hover .ant-select-selection__rendered{margin-right:0!important}.ant-select-auto-complete.ant-select .ant-input{height:32px;line-height:1.5;background:transparent;border-width:1px}.ant-select-auto-complete.ant-select .ant-input:focus,.ant-select-auto-complete.ant-select .ant-input:hover{border-color:#18947b;border-right-width:1px!important}.ant-select-auto-complete.ant-select .ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-select-auto-complete.ant-select .ant-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-select-auto-complete.ant-select-lg .ant-select-selection__rendered{line-height:40px}.ant-select-auto-complete.ant-select-lg .ant-input{height:40px;padding-top:6px;padding-bottom:6px}.ant-select-auto-complete.ant-select-sm .ant-select-selection__rendered{line-height:24px}.ant-select-auto-complete.ant-select-sm .ant-input{height:24px;padding-top:1px;padding-bottom:1px}.ant-input-group>.ant-select-auto-complete .ant-select-search__field.ant-input-affix-wrapper{display:inline;float:none}.ant-select{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;font-feature-settings:"tnum";position:relative;display:inline-block;outline:0}.ant-select,.ant-select ol,.ant-select ul{margin:0;padding:0;list-style:none}.ant-select>ul>li>a{padding:0;background-color:#fff}.ant-select-arrow{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;right:11px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;line-height:1;transform-origin:50% 50%}.ant-select-arrow>*{line-height:1}.ant-select-arrow svg{display:inline-block}.ant-select-arrow:before{display:none}.ant-select-arrow .ant-select-arrow-icon{display:block}.ant-select-arrow .ant-select-arrow-icon svg{transition:transform .3s}.ant-select-selection{display:block;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:1rem;outline:none;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-select-selection:hover{border-color:#18947b;border-right-width:1px!important}.ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease;text-rendering:auto}.ant-select-selection__clear:before{display:block}.ant-select-selection__clear:hover{color:rgba(0,0,0,.45)}.ant-select-selection:hover .ant-select-selection__clear{opacity:1}.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}.ant-select-disabled{color:rgba(0,0,0,.25)}.ant-select-disabled .ant-select-selection{background:#f5f5f5;cursor:not-allowed}.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none}.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice__remove{display:none}.ant-select-selection--single{position:relative;height:32px;cursor:pointer}.ant-select-selection--single .ant-select-selection__rendered{margin-right:24px}.ant-select-no-arrow .ant-select-selection__rendered{margin-right:11px}.ant-select-selection__rendered{position:relative;display:block;margin-right:11px;margin-left:11px;line-height:30px}.ant-select-selection__rendered:after{display:inline-block;width:0;visibility:hidden;content:".";pointer-events:none}.ant-select-lg{font-size:16px}.ant-select-lg .ant-select-selection--single{height:40px}.ant-select-lg .ant-select-selection__rendered{line-height:38px}.ant-select-lg .ant-select-selection--multiple{min-height:40px}.ant-select-lg .ant-select-selection--multiple .ant-select-selection__rendered li{height:32px;line-height:32px}.ant-select-lg .ant-select-selection--multiple .ant-select-arrow,.ant-select-lg .ant-select-selection--multiple .ant-select-selection__clear{top:20px}.ant-select-sm .ant-select-selection--single{height:24px}.ant-select-sm .ant-select-selection__rendered{margin-left:7px;line-height:22px}.ant-select-sm .ant-select-selection--multiple{min-height:24px}.ant-select-sm .ant-select-selection--multiple .ant-select-selection__rendered li{height:16px;line-height:14px}.ant-select-sm .ant-select-selection--multiple .ant-select-arrow,.ant-select-sm .ant-select-selection--multiple .ant-select-selection__clear{top:12px}.ant-select-sm .ant-select-arrow,.ant-select-sm .ant-select-selection__clear{right:8px}.ant-select-disabled .ant-select-selection__choice__remove{color:rgba(0,0,0,.25);cursor:default}.ant-select-disabled .ant-select-selection__choice__remove:hover{color:rgba(0,0,0,.25)}.ant-select-search__field__wrap{position:relative;display:inline-block}.ant-select-search__field__placeholder,.ant-select-selection__placeholder{position:absolute;top:50%;right:9px;left:0;max-width:100%;height:20px;margin-top:-10px;overflow:hidden;color:#bfbfbf;line-height:20px;white-space:nowrap;text-align:left;text-overflow:ellipsis}.ant-select-search__field__placeholder{left:12px}.ant-select-search__field__mirror{position:absolute;top:0;left:0;white-space:pre;opacity:0;pointer-events:none}.ant-select-search--inline{position:absolute;width:100%;height:100%}.ant-select-search--inline .ant-select-search__field__wrap{width:100%;height:100%}.ant-select-search--inline .ant-select-search__field{width:100%;height:100%;font-size:100%;line-height:1;background:transparent;border-width:0;border-radius:1rem;outline:0}.ant-select-search--inline>i{float:right}.ant-select-selection--multiple{min-height:32px;padding-bottom:3px;cursor:text;zoom:1}.ant-select-selection--multiple:after,.ant-select-selection--multiple:before{display:table;content:""}.ant-select-selection--multiple:after{clear:both}.ant-select-selection--multiple .ant-select-search--inline{position:static;float:left;width:auto;max-width:100%;padding:0}.ant-select-selection--multiple .ant-select-search--inline .ant-select-search__field{width:.75em;max-width:100%;padding:1px}.ant-select-selection--multiple .ant-select-selection__rendered{height:auto;margin-bottom:-3px;margin-left:5px}.ant-select-selection--multiple .ant-select-selection__placeholder{margin-left:6px}.ant-select-selection--multiple .ant-select-selection__rendered>ul>li,.ant-select-selection--multiple>ul>li{height:24px;margin-top:3px;line-height:22px}.ant-select-selection--multiple .ant-select-selection__choice{position:relative;float:left;max-width:99%;margin-right:4px;padding:0 20px 0 10px;overflow:hidden;color:rgba(0,0,0,.65);background-color:#fafafa;border:1px solid #e8e8e8;border-radius:2px;cursor:default;transition:padding .3s cubic-bezier(.645,.045,.355,1)}.ant-select-selection--multiple .ant-select-selection__choice__disabled{padding:0 10px}.ant-select-selection--multiple .ant-select-selection__choice__content{display:inline-block;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;transition:margin .3s cubic-bezier(.645,.045,.355,1)}.ant-select-selection--multiple .ant-select-selection__choice__remove{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;right:4px;color:rgba(0,0,0,.45);font-weight:700;line-height:inherit;cursor:pointer;transition:all .3s;display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg)}.ant-select-selection--multiple .ant-select-selection__choice__remove>*{line-height:1}.ant-select-selection--multiple .ant-select-selection__choice__remove svg{display:inline-block}.ant-select-selection--multiple .ant-select-selection__choice__remove:before{display:none}.ant-select-selection--multiple .ant-select-selection__choice__remove .ant-select-selection--multiple .ant-select-selection__choice__remove-icon{display:block}:root .ant-select-selection--multiple .ant-select-selection__choice__remove{font-size:12px}.ant-select-selection--multiple .ant-select-selection__choice__remove:hover{color:rgba(0,0,0,.75)}.ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px}.ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px}.ant-select-open .ant-select-arrow-icon svg{transform:rotate(180deg)}.ant-select-open .ant-select-selection{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-select-combobox .ant-select-arrow{display:none}.ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%}.ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%}.ant-select-combobox .ant-select-search__field{position:relative;z-index:1;width:100%;height:100%;box-shadow:none;transition:all .3s cubic-bezier(.645,.045,.355,1),height 0s}.ant-select-combobox.ant-select-allow-clear .ant-select-selection:hover .ant-select-selection__rendered,.ant-select-combobox.ant-select-show-arrow .ant-select-selection:hover .ant-select-selection__rendered{margin-right:20px}.ant-select-dropdown{margin:0;padding:0;color:rgba(0,0,0,.65);font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;top:-9999px;left:-9999px;z-index:1050;box-sizing:border-box;font-size:14px;font-variant:normal;background-color:#fff;border-radius:1rem;outline:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-select-dropdown.slide-up-appear.slide-up-appear-active.ant-select-dropdown-placement-bottomLeft,.ant-select-dropdown.slide-up-enter.slide-up-enter-active.ant-select-dropdown-placement-bottomLeft{animation-name:antSlideUpIn}.ant-select-dropdown.slide-up-appear.slide-up-appear-active.ant-select-dropdown-placement-topLeft,.ant-select-dropdown.slide-up-enter.slide-up-enter-active.ant-select-dropdown-placement-topLeft{animation-name:antSlideDownIn}.ant-select-dropdown.slide-up-leave.slide-up-leave-active.ant-select-dropdown-placement-bottomLeft{animation-name:antSlideUpOut}.ant-select-dropdown.slide-up-leave.slide-up-leave-active.ant-select-dropdown-placement-topLeft{animation-name:antSlideDownOut}.ant-select-dropdown-hidden{display:none}.ant-select-dropdown-menu{max-height:250px;margin-bottom:0;padding:4px 0;overflow:auto;list-style:none;outline:none}.ant-select-dropdown-menu-item-group-list{margin:0;padding:0}.ant-select-dropdown-menu-item-group-list>.ant-select-dropdown-menu-item{padding-left:20px}.ant-select-dropdown-menu-item-group-title{height:32px;padding:0 12px;color:rgba(0,0,0,.45);font-size:12px;line-height:32px}.ant-select-dropdown-menu-item-group-list .ant-select-dropdown-menu-item:first-child:not(:last-child),.ant-select-dropdown-menu-item-group:not(:last-child) .ant-select-dropdown-menu-item-group-list .ant-select-dropdown-menu-item:last-child{border-radius:0}.ant-select-dropdown-menu-item{position:relative;display:block;padding:5px 12px;overflow:hidden;color:rgba(0,0,0,.65);font-weight:400;font-size:14px;line-height:22px;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;transition:background .3s ease}.ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled){background-color:#b3c7c0}.ant-select-dropdown-menu-item-selected{color:rgba(0,0,0,.65);font-weight:600;background-color:#fafafa}.ant-select-dropdown-menu-item-disabled,.ant-select-dropdown-menu-item-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled){background-color:#b3c7c0}.ant-select-dropdown-menu-item-divider{height:1px;margin:1px 0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item{padding-right:32px}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item .ant-select-selected-icon{position:absolute;top:50%;right:12px;color:transparent;font-weight:700;font-size:12px;text-shadow:0 .1px 0,.1px 0 0,0 -.1px 0,-.1px 0;transform:translateY(-50%);transition:all .2s}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item:hover .ant-select-selected-icon{color:rgba(0,0,0,.87)}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-disabled .ant-select-selected-icon{display:none}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected .ant-select-selected-icon,.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected:hover .ant-select-selected-icon{display:inline-block;color:#008771}.ant-select-dropdown--empty.ant-select-dropdown--multiple .ant-select-dropdown-menu-item{padding-right:12px}.ant-select-dropdown-container-open .ant-select-dropdown,.ant-select-dropdown-open .ant-select-dropdown{display:block}.ant-empty{margin:0 8px;font-size:14px;line-height:22px;text-align:center}.ant-empty-image{height:100px;margin-bottom:8px}.ant-empty-image img{height:100%}.ant-empty-image svg{height:100%;margin:auto}.ant-empty-description{margin:0}.ant-empty-footer{margin-top:16px}.ant-empty-normal{margin:32px 0;color:rgba(0,0,0,.25)}.ant-empty-normal .ant-empty-image{height:40px}.ant-empty-small{margin:8px 0;color:rgba(0,0,0,.25)}.ant-empty-small .ant-empty-image{height:35px}.ant-input{box-sizing:border-box;margin:0;font-variant:tabular-nums;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;width:100%;height:32px;padding:4px 11px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:1rem;transition:all .3s}.ant-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-input:-ms-input-placeholder{color:#bfbfbf}.ant-input::-webkit-input-placeholder{color:#bfbfbf}.ant-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-input:placeholder-shown{text-overflow:ellipsis}.ant-input:focus,.ant-input:hover{border-color:#18947b;border-right-width:1px!important}.ant-input:focus{outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}.ant-input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-input-sm{height:24px;padding:1px 7px}.ant-input-group{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:table;width:100%;border-collapse:separate;border-spacing:0}.ant-input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.ant-input-group>[class*=col-]{padding-right:8px}.ant-input-group>[class*=col-]:last-child{padding-right:0}.ant-input-group-addon,.ant-input-group-wrap,.ant-input-group>.ant-input{display:table-cell}.ant-input-group-addon:not(:first-child):not(:last-child),.ant-input-group-wrap:not(:first-child):not(:last-child),.ant-input-group>.ant-input:not(:first-child):not(:last-child){border-radius:0}.ant-input-group-addon,.ant-input-group-wrap{width:1px;white-space:nowrap;vertical-align:middle}.ant-input-group-wrap>*{display:block!important}.ant-input-group .ant-input{float:left;width:100%;margin-bottom:0;text-align:inherit}.ant-input-group .ant-input:focus,.ant-input-group .ant-input:hover{z-index:1;border-right-width:1px}.ant-input-group-addon{position:relative;padding:0 11px;color:rgba(0,0,0,.65);font-weight:400;font-size:14px;text-align:center;background-color:#fafafa;border:1px solid #d9d9d9;border-radius:1rem;transition:all .3s}.ant-input-group-addon .ant-select{margin:-5px -11px}.ant-input-group-addon .ant-select .ant-select-selection{margin:-1px;background-color:inherit;border:1px solid transparent;box-shadow:none}.ant-input-group-addon .ant-select-focused .ant-select-selection,.ant-input-group-addon .ant-select-open .ant-select-selection{color:#008771}.ant-input-group-addon>i:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;content:""}.ant-input-group-addon:first-child,.ant-input-group-addon:first-child .ant-select .ant-select-selection,.ant-input-group>.ant-input:first-child,.ant-input-group>.ant-input:first-child .ant-select .ant-select-selection{border-top-right-radius:0;border-bottom-right-radius:0}.ant-input-group>.ant-input-affix-wrapper:not(:first-child) .ant-input{border-top-left-radius:0;border-bottom-left-radius:0}.ant-input-group>.ant-input-affix-wrapper:not(:last-child) .ant-input{border-top-right-radius:0;border-bottom-right-radius:0}.ant-input-group-addon:first-child{border-right:0}.ant-input-group-addon:last-child{border-left:0}.ant-input-group-addon:last-child,.ant-input-group-addon:last-child .ant-select .ant-select-selection,.ant-input-group>.ant-input:last-child,.ant-input-group>.ant-input:last-child .ant-select .ant-select-selection{border-top-left-radius:0;border-bottom-left-radius:0}.ant-input-group-lg .ant-input,.ant-input-group-lg>.ant-input-group-addon{height:40px;padding:6px 11px;font-size:16px}.ant-input-group-sm .ant-input,.ant-input-group-sm>.ant-input-group-addon{height:24px;padding:1px 7px}.ant-input-group-lg .ant-select-selection--single{height:40px}.ant-input-group-sm .ant-select-selection--single{height:24px}.ant-input-group .ant-input-affix-wrapper{display:table-cell;float:left;width:100%}.ant-input-group.ant-input-group-compact{display:block;zoom:1}.ant-input-group.ant-input-group-compact:after,.ant-input-group.ant-input-group-compact:before{display:table;content:""}.ant-input-group.ant-input-group-compact:after{clear:both}.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child){border-right-width:1px}.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child):focus,.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child):hover,.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child):focus,.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child):hover,.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child):focus,.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child):hover{z-index:1}.ant-input-group.ant-input-group-compact>*{display:inline-block;float:none;vertical-align:top;border-radius:0}.ant-input-group.ant-input-group-compact>:not(:last-child){margin-right:-1px;border-right-width:1px}.ant-input-group.ant-input-group-compact .ant-input{float:none}.ant-input-group.ant-input-group-compact>.ant-calendar-picker .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker .ant-input,.ant-input-group.ant-input-group-compact>.ant-input-group-wrapper .ant-input,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper .ant-mention-editor,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input,.ant-input-group.ant-input-group-compact>.ant-select>.ant-select-selection,.ant-input-group.ant-input-group-compact>.ant-time-picker .ant-time-picker-input{border-right-width:1px;border-radius:0}.ant-input-group.ant-input-group-compact>.ant-calendar-picker .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-calendar-picker .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-cascader-picker .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-cascader-picker .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-input-group-wrapper .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-input-group-wrapper .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper .ant-mention-editor:focus,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper .ant-mention-editor:hover,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-select-focused,.ant-input-group.ant-input-group-compact>.ant-select>.ant-select-selection:focus,.ant-input-group.ant-input-group-compact>.ant-select>.ant-select-selection:hover,.ant-input-group.ant-input-group-compact>.ant-time-picker .ant-time-picker-input:focus,.ant-input-group.ant-input-group-compact>.ant-time-picker .ant-time-picker-input:hover{z-index:1}.ant-input-group.ant-input-group-compact>.ant-calendar-picker:first-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker:first-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper:first-child .ant-mention-editor,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete:first-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-select:first-child>.ant-select-selection,.ant-input-group.ant-input-group-compact>.ant-time-picker:first-child .ant-time-picker-input,.ant-input-group.ant-input-group-compact>:first-child{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.ant-input-group.ant-input-group-compact>.ant-calendar-picker:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker-focused:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper:last-child .ant-mention-editor,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-select:last-child>.ant-select-selection,.ant-input-group.ant-input-group-compact>.ant-time-picker:last-child .ant-time-picker-input,.ant-input-group.ant-input-group-compact>:last-child{border-right-width:1px;border-top-right-radius:1rem;border-bottom-right-radius:1rem}.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input{vertical-align:top}.ant-input-group-wrapper{display:inline-block;width:100%;text-align:start;vertical-align:top}.ant-input-affix-wrapper{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;width:100%;text-align:start}.ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:#18947b;border-right-width:1px!important}.ant-input-affix-wrapper .ant-input{position:relative;text-align:inherit}.ant-input-affix-wrapper .ant-input-prefix,.ant-input-affix-wrapper .ant-input-suffix{position:absolute;top:50%;z-index:2;display:flex;align-items:center;color:rgba(0,0,0,.65);line-height:0;transform:translateY(-50%)}.ant-input-affix-wrapper .ant-input-prefix :not(.anticon),.ant-input-affix-wrapper .ant-input-suffix :not(.anticon){line-height:1.5}.ant-input-affix-wrapper .ant-input-disabled~.ant-input-suffix .anticon{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-input-affix-wrapper .ant-input-prefix{left:12px}.ant-input-affix-wrapper .ant-input-suffix{right:12px}.ant-input-affix-wrapper .ant-input:not(:first-child){padding-left:30px}.ant-input-affix-wrapper .ant-input:not(:last-child){padding-right:30px}.ant-input-affix-wrapper.ant-input-affix-wrapper-input-with-clear-btn .ant-input:not(:last-child){padding-right:49px}.ant-input-affix-wrapper.ant-input-affix-wrapper-textarea-with-clear-btn .ant-input{padding-right:22px}.ant-input-password-icon{color:rgba(0,0,0,.45);cursor:pointer;transition:all .3s}.ant-input-password-icon:hover{color:#333}.ant-input-clear-icon{color:rgba(0,0,0,.25);font-size:12px;cursor:pointer;transition:color .3s;vertical-align:0}.ant-input-clear-icon:hover{color:rgba(0,0,0,.45)}.ant-input-clear-icon:active{color:rgba(0,0,0,.65)}.ant-input-clear-icon+i{margin-left:6px}.ant-input-textarea-clear-icon{color:rgba(0,0,0,.25);font-size:12px;cursor:pointer;transition:color .3s;position:absolute;top:0;right:0;margin:8px 8px 0 0}.ant-input-textarea-clear-icon:hover{color:rgba(0,0,0,.45)}.ant-input-textarea-clear-icon:active{color:rgba(0,0,0,.65)}.ant-input-textarea-clear-icon+i{margin-left:6px}.ant-input-search-icon{color:rgba(0,0,0,.45);cursor:pointer;transition:all .3s}.ant-input-search-icon:hover{color:rgba(0,0,0,.8)}.ant-input-search-enter-button input{border-right:0}.ant-input-search-enter-button+.ant-input-group-addon,.ant-input-search-enter-button input+.ant-input-group-addon{padding:0;border:0}.ant-input-search-enter-button+.ant-input-group-addon .ant-input-search-button,.ant-input-search-enter-button input+.ant-input-group-addon .ant-input-search-button{border-top-left-radius:0;border-bottom-left-radius:0}.ant-btn{line-height:1.499;position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;touch-action:manipulation;height:32px;padding:0 15px;font-size:14px;border-radius:1rem;color:rgba(0,0,0,.65);background-color:#fff;border:1px solid #d9d9d9}.ant-btn>.anticon{line-height:1}.ant-btn,.ant-btn:active,.ant-btn:focus{outline:0}.ant-btn:not([disabled]):hover{text-decoration:none}.ant-btn:not([disabled]):active{outline:0;box-shadow:none}.ant-btn.disabled,.ant-btn[disabled]{cursor:not-allowed}.ant-btn.disabled>*,.ant-btn[disabled]>*{pointer-events:none}.ant-btn-lg{height:40px;padding:0 15px;font-size:16px;border-radius:1rem}.ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:1rem}.ant-btn>a:only-child{color:currentColor}.ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn:focus,.ant-btn:hover{color:#18947b;background-color:#fff;border-color:#18947b}.ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor}.ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn.active,.ant-btn:active{color:#006154;background-color:#fff;border-color:#006154}.ant-btn.active>a:only-child,.ant-btn:active>a:only-child{color:currentColor}.ant-btn.active>a:only-child:after,.ant-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-disabled,.ant-btn-disabled.active,.ant-btn-disabled:active,.ant-btn-disabled:focus,.ant-btn-disabled:hover,.ant-btn.disabled,.ant-btn.disabled.active,.ant-btn.disabled:active,.ant-btn.disabled:focus,.ant-btn.disabled:hover,.ant-btn[disabled],.ant-btn[disabled].active,.ant-btn[disabled]:active,.ant-btn[disabled]:focus,.ant-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-disabled.active>a:only-child,.ant-btn-disabled:active>a:only-child,.ant-btn-disabled:focus>a:only-child,.ant-btn-disabled:hover>a:only-child,.ant-btn-disabled>a:only-child,.ant-btn.disabled.active>a:only-child,.ant-btn.disabled:active>a:only-child,.ant-btn.disabled:focus>a:only-child,.ant-btn.disabled:hover>a:only-child,.ant-btn.disabled>a:only-child,.ant-btn[disabled].active>a:only-child,.ant-btn[disabled]:active>a:only-child,.ant-btn[disabled]:focus>a:only-child,.ant-btn[disabled]:hover>a:only-child,.ant-btn[disabled]>a:only-child{color:currentColor}.ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff}.ant-btn>i,.ant-btn>span{display:inline-block;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none}.ant-btn-primary{color:#fff;background-color:#008771;border-color:#008771;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}.ant-btn-primary>a:only-child{color:currentColor}.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:#18947b;border-color:#18947b}.ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor}.ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:#006154;border-color:#006154}.ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor}.ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor}.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#18947b;border-left-color:#18947b}.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child):disabled{border-color:#d9d9d9}.ant-btn-group .ant-btn-primary:first-child:not(:last-child){border-right-color:#18947b}.ant-btn-group .ant-btn-primary:first-child:not(:last-child)[disabled]{border-right-color:#d9d9d9}.ant-btn-group .ant-btn-primary+.ant-btn-primary,.ant-btn-group .ant-btn-primary:last-child:not(:first-child){border-left-color:#18947b}.ant-btn-group .ant-btn-primary+.ant-btn-primary[disabled],.ant-btn-group .ant-btn-primary:last-child:not(:first-child)[disabled]{border-left-color:#d9d9d9}.ant-btn-ghost{color:rgba(0,0,0,.65);background-color:transparent;border-color:#d9d9d9}.ant-btn-ghost>a:only-child{color:currentColor}.ant-btn-ghost>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-ghost:focus,.ant-btn-ghost:hover{color:#18947b;background-color:transparent;border-color:#18947b}.ant-btn-ghost:focus>a:only-child,.ant-btn-ghost:hover>a:only-child{color:currentColor}.ant-btn-ghost:focus>a:only-child:after,.ant-btn-ghost:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-ghost.active,.ant-btn-ghost:active{color:#006154;background-color:transparent;border-color:#006154}.ant-btn-ghost.active>a:only-child,.ant-btn-ghost:active>a:only-child{color:currentColor}.ant-btn-ghost.active>a:only-child:after,.ant-btn-ghost:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-ghost-disabled,.ant-btn-ghost-disabled.active,.ant-btn-ghost-disabled:active,.ant-btn-ghost-disabled:focus,.ant-btn-ghost-disabled:hover,.ant-btn-ghost.disabled,.ant-btn-ghost.disabled.active,.ant-btn-ghost.disabled:active,.ant-btn-ghost.disabled:focus,.ant-btn-ghost.disabled:hover,.ant-btn-ghost[disabled],.ant-btn-ghost[disabled].active,.ant-btn-ghost[disabled]:active,.ant-btn-ghost[disabled]:focus,.ant-btn-ghost[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-ghost-disabled.active>a:only-child,.ant-btn-ghost-disabled:active>a:only-child,.ant-btn-ghost-disabled:focus>a:only-child,.ant-btn-ghost-disabled:hover>a:only-child,.ant-btn-ghost-disabled>a:only-child,.ant-btn-ghost.disabled.active>a:only-child,.ant-btn-ghost.disabled:active>a:only-child,.ant-btn-ghost.disabled:focus>a:only-child,.ant-btn-ghost.disabled:hover>a:only-child,.ant-btn-ghost.disabled>a:only-child,.ant-btn-ghost[disabled].active>a:only-child,.ant-btn-ghost[disabled]:active>a:only-child,.ant-btn-ghost[disabled]:focus>a:only-child,.ant-btn-ghost[disabled]:hover>a:only-child,.ant-btn-ghost[disabled]>a:only-child{color:currentColor}.ant-btn-ghost-disabled.active>a:only-child:after,.ant-btn-ghost-disabled:active>a:only-child:after,.ant-btn-ghost-disabled:focus>a:only-child:after,.ant-btn-ghost-disabled:hover>a:only-child:after,.ant-btn-ghost-disabled>a:only-child:after,.ant-btn-ghost.disabled.active>a:only-child:after,.ant-btn-ghost.disabled:active>a:only-child:after,.ant-btn-ghost.disabled:focus>a:only-child:after,.ant-btn-ghost.disabled:hover>a:only-child:after,.ant-btn-ghost.disabled>a:only-child:after,.ant-btn-ghost[disabled].active>a:only-child:after,.ant-btn-ghost[disabled]:active>a:only-child:after,.ant-btn-ghost[disabled]:focus>a:only-child:after,.ant-btn-ghost[disabled]:hover>a:only-child:after,.ant-btn-ghost[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed{color:rgba(0,0,0,.65);background-color:#fff;border-color:#d9d9d9;border-style:dashed}.ant-btn-dashed>a:only-child{color:currentColor}.ant-btn-dashed>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed:focus,.ant-btn-dashed:hover{color:#18947b;background-color:#fff;border-color:#18947b}.ant-btn-dashed:focus>a:only-child,.ant-btn-dashed:hover>a:only-child{color:currentColor}.ant-btn-dashed:focus>a:only-child:after,.ant-btn-dashed:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed.active,.ant-btn-dashed:active{color:#006154;background-color:#fff;border-color:#006154}.ant-btn-dashed.active>a:only-child,.ant-btn-dashed:active>a:only-child{color:currentColor}.ant-btn-dashed.active>a:only-child:after,.ant-btn-dashed:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor}.ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}.ant-btn-danger>a:only-child{color:currentColor}.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff7875;border-color:#ff7875}.ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor}.ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e}.ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor}.ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor}.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link{color:#008771;background-color:transparent;border-color:transparent;box-shadow:none}.ant-btn-link>a:only-child{color:currentColor}.ant-btn-link>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link:focus,.ant-btn-link:hover{color:#18947b;background-color:transparent;border-color:#18947b}.ant-btn-link:focus>a:only-child,.ant-btn-link:hover>a:only-child{color:currentColor}.ant-btn-link:focus>a:only-child:after,.ant-btn-link:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link.active,.ant-btn-link:active{color:#006154;background-color:transparent;border-color:#006154}.ant-btn-link.active>a:only-child,.ant-btn-link:active>a:only-child{color:currentColor}.ant-btn-link.active>a:only-child:after,.ant-btn-link:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link-disabled,.ant-btn-link-disabled.active,.ant-btn-link-disabled:active,.ant-btn-link-disabled:focus,.ant-btn-link-disabled:hover,.ant-btn-link.disabled,.ant-btn-link.disabled.active,.ant-btn-link.disabled:active,.ant-btn-link.disabled:focus,.ant-btn-link.disabled:hover,.ant-btn-link[disabled],.ant-btn-link[disabled].active,.ant-btn-link[disabled]:active,.ant-btn-link[disabled]:focus,.ant-btn-link[disabled]:hover{background-color:#f5f5f5;border-color:#d9d9d9}.ant-btn-link:active,.ant-btn-link:focus,.ant-btn-link:hover{border-color:transparent}.ant-btn-link-disabled,.ant-btn-link-disabled.active,.ant-btn-link-disabled:active,.ant-btn-link-disabled:focus,.ant-btn-link-disabled:hover,.ant-btn-link.disabled,.ant-btn-link.disabled.active,.ant-btn-link.disabled:active,.ant-btn-link.disabled:focus,.ant-btn-link.disabled:hover,.ant-btn-link[disabled],.ant-btn-link[disabled].active,.ant-btn-link[disabled]:active,.ant-btn-link[disabled]:focus,.ant-btn-link[disabled]:hover{color:rgba(0,0,0,.25);background-color:transparent;border-color:transparent;text-shadow:none;box-shadow:none}.ant-btn-link-disabled.active>a:only-child,.ant-btn-link-disabled:active>a:only-child,.ant-btn-link-disabled:focus>a:only-child,.ant-btn-link-disabled:hover>a:only-child,.ant-btn-link-disabled>a:only-child,.ant-btn-link.disabled.active>a:only-child,.ant-btn-link.disabled:active>a:only-child,.ant-btn-link.disabled:focus>a:only-child,.ant-btn-link.disabled:hover>a:only-child,.ant-btn-link.disabled>a:only-child,.ant-btn-link[disabled].active>a:only-child,.ant-btn-link[disabled]:active>a:only-child,.ant-btn-link[disabled]:focus>a:only-child,.ant-btn-link[disabled]:hover>a:only-child,.ant-btn-link[disabled]>a:only-child{color:currentColor}.ant-btn-link-disabled.active>a:only-child:after,.ant-btn-link-disabled:active>a:only-child:after,.ant-btn-link-disabled:focus>a:only-child:after,.ant-btn-link-disabled:hover>a:only-child:after,.ant-btn-link-disabled>a:only-child:after,.ant-btn-link.disabled.active>a:only-child:after,.ant-btn-link.disabled:active>a:only-child:after,.ant-btn-link.disabled:focus>a:only-child:after,.ant-btn-link.disabled:hover>a:only-child:after,.ant-btn-link.disabled>a:only-child:after,.ant-btn-link[disabled].active>a:only-child:after,.ant-btn-link[disabled]:active>a:only-child:after,.ant-btn-link[disabled]:focus>a:only-child:after,.ant-btn-link[disabled]:hover>a:only-child:after,.ant-btn-link[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-icon-only{width:32px;height:32px;padding:0;font-size:16px;border-radius:1rem}.ant-btn-icon-only.ant-btn-lg{width:40px;height:40px;padding:0;font-size:18px;border-radius:1rem}.ant-btn-icon-only.ant-btn-sm{width:24px;height:24px;padding:0;font-size:14px;border-radius:1rem}.ant-btn-icon-only>i{vertical-align:middle}.ant-btn-round{height:32px;padding:0 16px;font-size:14px;border-radius:32px}.ant-btn-round.ant-btn-lg{height:40px;padding:0 20px;font-size:16px;border-radius:40px}.ant-btn-round.ant-btn-sm{height:24px;padding:0 12px;font-size:14px;border-radius:24px}.ant-btn-round.ant-btn-icon-only{width:auto}.ant-btn-circle,.ant-btn-circle-outline{min-width:32px;padding-right:0;padding-left:0;text-align:center;border-radius:50%}.ant-btn-circle-outline.ant-btn-lg,.ant-btn-circle.ant-btn-lg{min-width:40px;border-radius:50%}.ant-btn-circle-outline.ant-btn-sm,.ant-btn-circle.ant-btn-sm{min-width:24px;border-radius:50%}.ant-btn:before{position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;z-index:1;display:none;background:#fff;border-radius:inherit;opacity:.35;transition:opacity .2s;content:"";pointer-events:none}.ant-btn .anticon{transition:margin-left .3s cubic-bezier(.645,.045,.355,1)}.ant-btn .anticon.anticon-minus>svg,.ant-btn .anticon.anticon-plus>svg{shape-rendering:optimizeSpeed}.ant-btn.ant-btn-loading{position:relative}.ant-btn.ant-btn-loading:not([disabled]){pointer-events:none}.ant-btn.ant-btn-loading:before{display:block}.ant-btn.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only){padding-left:29px}.ant-btn.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only) .anticon:not(:last-child){margin-left:-14px}.ant-btn-sm.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only){padding-left:24px}.ant-btn-sm.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only) .anticon{margin-left:-17px}.ant-btn-group{display:inline-flex}.ant-btn-group,.ant-btn-group>.ant-btn,.ant-btn-group>span>.ant-btn{position:relative}.ant-btn-group>.ant-btn.active,.ant-btn-group>.ant-btn:active,.ant-btn-group>.ant-btn:focus,.ant-btn-group>.ant-btn:hover,.ant-btn-group>span>.ant-btn.active,.ant-btn-group>span>.ant-btn:active,.ant-btn-group>span>.ant-btn:focus,.ant-btn-group>span>.ant-btn:hover{z-index:2}.ant-btn-group>.ant-btn:disabled,.ant-btn-group>span>.ant-btn:disabled{z-index:0}.ant-btn-group>.ant-btn-icon-only{font-size:14px}.ant-btn-group-lg>.ant-btn,.ant-btn-group-lg>span>.ant-btn{height:40px;padding:0 15px;font-size:16px;border-radius:0;line-height:38px}.ant-btn-group-lg>.ant-btn.ant-btn-icon-only{width:40px;height:40px;padding-right:0;padding-left:0}.ant-btn-group-sm>.ant-btn,.ant-btn-group-sm>span>.ant-btn{height:24px;padding:0 7px;font-size:14px;border-radius:0;line-height:22px}.ant-btn-group-sm>.ant-btn>.anticon,.ant-btn-group-sm>span>.ant-btn>.anticon{font-size:14px}.ant-btn-group-sm>.ant-btn.ant-btn-icon-only{width:24px;height:24px;padding-right:0;padding-left:0}.ant-btn+.ant-btn-group,.ant-btn-group+.ant-btn,.ant-btn-group+.ant-btn-group,.ant-btn-group .ant-btn+.ant-btn,.ant-btn-group .ant-btn+span,.ant-btn-group>span+span,.ant-btn-group span+.ant-btn{margin-left:-1px}.ant-btn-group .ant-btn-primary+.ant-btn:not(.ant-btn-primary):not([disabled]){border-left-color:transparent}.ant-btn-group .ant-btn{border-radius:0}.ant-btn-group>.ant-btn:first-child,.ant-btn-group>span:first-child>.ant-btn{margin-left:0}.ant-btn-group>.ant-btn:only-child,.ant-btn-group>span:only-child>.ant-btn{border-radius:1rem}.ant-btn-group>.ant-btn:first-child:not(:last-child),.ant-btn-group>span:first-child:not(:last-child)>.ant-btn{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.ant-btn-group>.ant-btn:last-child:not(:first-child),.ant-btn-group>span:last-child:not(:first-child)>.ant-btn{border-top-right-radius:1rem;border-bottom-right-radius:1rem}.ant-btn-group-sm>.ant-btn:only-child,.ant-btn-group-sm>span:only-child>.ant-btn{border-radius:1rem}.ant-btn-group-sm>.ant-btn:first-child:not(:last-child),.ant-btn-group-sm>span:first-child:not(:last-child)>.ant-btn{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.ant-btn-group-sm>.ant-btn:last-child:not(:first-child),.ant-btn-group-sm>span:last-child:not(:first-child)>.ant-btn{border-top-right-radius:1rem;border-bottom-right-radius:1rem}.ant-btn-group>.ant-btn-group{float:left}.ant-btn-group>.ant-btn-group:not(:first-child):not(:last-child)>.ant-btn{border-radius:0}.ant-btn-group>.ant-btn-group:first-child:not(:last-child)>.ant-btn:last-child{padding-right:8px;border-top-right-radius:0;border-bottom-right-radius:0}.ant-btn-group>.ant-btn-group:last-child:not(:first-child)>.ant-btn:first-child{padding-left:8px;border-top-left-radius:0;border-bottom-left-radius:0}.ant-btn:active>span,.ant-btn:focus>span{position:relative}.ant-btn>.anticon+span,.ant-btn>span+.anticon{margin-left:8px}.ant-btn-background-ghost{color:#fff;background:transparent!important;border-color:#fff}.ant-btn-background-ghost.ant-btn-primary{color:#008771;background-color:transparent;border-color:#008771;text-shadow:none}.ant-btn-background-ghost.ant-btn-primary>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-primary:focus,.ant-btn-background-ghost.ant-btn-primary:hover{color:#18947b;background-color:transparent;border-color:#18947b}.ant-btn-background-ghost.ant-btn-primary:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary:hover>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-primary.active,.ant-btn-background-ghost.ant-btn-primary:active{color:#006154;background-color:transparent;border-color:#006154}.ant-btn-background-ghost.ant-btn-primary.active>a:only-child,.ant-btn-background-ghost.ant-btn-primary:active>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-primary-disabled,.ant-btn-background-ghost.ant-btn-primary-disabled.active,.ant-btn-background-ghost.ant-btn-primary-disabled:active,.ant-btn-background-ghost.ant-btn-primary-disabled:focus,.ant-btn-background-ghost.ant-btn-primary-disabled:hover,.ant-btn-background-ghost.ant-btn-primary.disabled,.ant-btn-background-ghost.ant-btn-primary.disabled.active,.ant-btn-background-ghost.ant-btn-primary.disabled:active,.ant-btn-background-ghost.ant-btn-primary.disabled:focus,.ant-btn-background-ghost.ant-btn-primary.disabled:hover,.ant-btn-background-ghost.ant-btn-primary[disabled],.ant-btn-background-ghost.ant-btn-primary[disabled].active,.ant-btn-background-ghost.ant-btn-primary[disabled]:active,.ant-btn-background-ghost.ant-btn-primary[disabled]:focus,.ant-btn-background-ghost.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-background-ghost.ant-btn-primary-disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled].active>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger{color:#ff4d4f;background-color:transparent;border-color:#ff4d4f;text-shadow:none}.ant-btn-background-ghost.ant-btn-danger>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger:focus,.ant-btn-background-ghost.ant-btn-danger:hover{color:#ff7875;background-color:transparent;border-color:#ff7875}.ant-btn-background-ghost.ant-btn-danger:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger:hover>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger.active,.ant-btn-background-ghost.ant-btn-danger:active{color:#d9363e;background-color:transparent;border-color:#d9363e}.ant-btn-background-ghost.ant-btn-danger.active>a:only-child,.ant-btn-background-ghost.ant-btn-danger:active>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger-disabled,.ant-btn-background-ghost.ant-btn-danger-disabled.active,.ant-btn-background-ghost.ant-btn-danger-disabled:active,.ant-btn-background-ghost.ant-btn-danger-disabled:focus,.ant-btn-background-ghost.ant-btn-danger-disabled:hover,.ant-btn-background-ghost.ant-btn-danger.disabled,.ant-btn-background-ghost.ant-btn-danger.disabled.active,.ant-btn-background-ghost.ant-btn-danger.disabled:active,.ant-btn-background-ghost.ant-btn-danger.disabled:focus,.ant-btn-background-ghost.ant-btn-danger.disabled:hover,.ant-btn-background-ghost.ant-btn-danger[disabled],.ant-btn-background-ghost.ant-btn-danger[disabled].active,.ant-btn-background-ghost.ant-btn-danger[disabled]:active,.ant-btn-background-ghost.ant-btn-danger[disabled]:focus,.ant-btn-background-ghost.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-background-ghost.ant-btn-danger-disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled].active>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link{color:#008771;background-color:transparent;border-color:transparent;text-shadow:none;color:#fff}.ant-btn-background-ghost.ant-btn-link>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link:focus,.ant-btn-background-ghost.ant-btn-link:hover{color:#18947b;background-color:transparent;border-color:transparent}.ant-btn-background-ghost.ant-btn-link:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link:hover>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link.active,.ant-btn-background-ghost.ant-btn-link:active{color:#006154;background-color:transparent;border-color:transparent}.ant-btn-background-ghost.ant-btn-link.active>a:only-child,.ant-btn-background-ghost.ant-btn-link:active>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link-disabled,.ant-btn-background-ghost.ant-btn-link-disabled.active,.ant-btn-background-ghost.ant-btn-link-disabled:active,.ant-btn-background-ghost.ant-btn-link-disabled:focus,.ant-btn-background-ghost.ant-btn-link-disabled:hover,.ant-btn-background-ghost.ant-btn-link.disabled,.ant-btn-background-ghost.ant-btn-link.disabled.active,.ant-btn-background-ghost.ant-btn-link.disabled:active,.ant-btn-background-ghost.ant-btn-link.disabled:focus,.ant-btn-background-ghost.ant-btn-link.disabled:hover,.ant-btn-background-ghost.ant-btn-link[disabled],.ant-btn-background-ghost.ant-btn-link[disabled].active,.ant-btn-background-ghost.ant-btn-link[disabled]:active,.ant-btn-background-ghost.ant-btn-link[disabled]:focus,.ant-btn-background-ghost.ant-btn-link[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-btn-background-ghost.ant-btn-link-disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled].active>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]:active>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]:hover>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link-disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled].active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-two-chinese-chars:first-letter{letter-spacing:.34em}.ant-btn-two-chinese-chars>:not(.anticon){margin-right:-.34em;letter-spacing:.34em}.ant-btn-block{width:100%}.ant-btn:empty{vertical-align:top}a.ant-btn{padding-top:.1px;line-height:30px}a.ant-btn-lg{line-height:38px}a.ant-btn-sm{line-height:22px}.ant-avatar{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;overflow:hidden;color:#fff;white-space:nowrap;text-align:center;vertical-align:middle;background:#ccc;width:32px;height:32px;line-height:32px;border-radius:50%}.ant-avatar-image{background:transparent}.ant-avatar-string{position:absolute;left:50%;transform-origin:0 center}.ant-avatar.ant-avatar-icon{font-size:18px}.ant-avatar-lg{width:40px;height:40px;line-height:40px;border-radius:50%}.ant-avatar-lg-string{position:absolute;left:50%;transform-origin:0 center}.ant-avatar-lg.ant-avatar-icon{font-size:24px}.ant-avatar-sm{width:24px;height:24px;line-height:24px;border-radius:50%}.ant-avatar-sm-string{position:absolute;left:50%;transform-origin:0 center}.ant-avatar-sm.ant-avatar-icon{font-size:14px}.ant-avatar-square{border-radius:1rem}.ant-avatar>img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.ant-back-top{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:fixed;right:100px;bottom:50px;z-index:10;width:40px;height:40px;cursor:pointer}.ant-back-top-content{width:40px;height:40px;overflow:hidden;color:#fff;text-align:center;background-color:rgba(0,0,0,.45);border-radius:20px;transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-back-top-content:hover{background-color:rgba(0,0,0,.65);transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-back-top-icon{width:14px;height:16px;margin:12px auto;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAABGdBTUEAALGPC/xhBQAAAbtJREFUWAntmMtKw0AUhhMvS5cuxILgQlRUpIggIoKIIoigG1eC+AA+jo+i6FIXBfeuXIgoeKVeitVWJX5HWhhDksnUpp3FDPyZk3Nm5nycmZKkXhAEOXSA3lG7muTeRzmfy6HneUvIhnYkQK+Q9NhAA0Opg0vBEhjBKHiyb8iGMyQMOYuK41BcBSypAL+MYXSKjtFAW7EAGEO3qN4uMQbbAkXiSfRQJ1H6a+yhlkKRcAoVFYiweYNjtCVQJJpBz2GCiPt7fBOZQpFgDpUikse5HgnkM4Fi4QX0Fpc5wf9EbLqpUCy4jMoJSXWhFwbMNgWKhVbRhy5jirhs9fy/oFhgHVVTJEs7RLZ8sSEoJm6iz7SZDMbJ+/OKERQTttCXQRLToRUmrKWCYuA2+jbN0MB4OQobYShfdTCgn/sL1K36M7TLrN3n+758aPy2rrpR6+/od5E8tf/A1uLS9aId5T7J3CNYihkQ4D9PiMdMC7mp4rjB9kjFjZp8BlnVHJBuO1yFXIV0FdDF3RlyFdJVQBdv5AxVdIsq8apiZ2PyYO1EVykesGfZEESsCkweyR8MUW+V8uJ1gkYipmpdP1pm2aJVPEGzAAAAAElFTkSuQmCC) 100%/100% no-repeat}@media screen and (max-width:768px){.ant-back-top{right:60px}}@media screen and (max-width:480px){.ant-back-top{right:20px}}.ant-badge{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;color:unset;line-height:1}.ant-badge-count{min-width:20px;height:20px;padding:0 6px;color:#fff;font-weight:400;font-size:12px;line-height:20px;white-space:nowrap;text-align:center;background:#f5222d;border-radius:10px;box-shadow:0 0 0 1px #fff}.ant-badge-count a,.ant-badge-count a:hover{color:#fff}.ant-badge-multiple-words{padding:0 8px}.ant-badge-dot{width:6px;height:6px;background:#f5222d;border-radius:100%;box-shadow:0 0 0 1px #fff}.ant-badge-count,.ant-badge-dot,.ant-badge .ant-scroll-number-custom-component{position:absolute;top:0;right:0;z-index:1;transform:translate(50%,-50%);transform-origin:100% 0}.ant-badge-status{line-height:inherit;vertical-align:baseline}.ant-badge-status-dot{position:relative;top:-1px;display:inline-block;width:6px;height:6px;vertical-align:middle;border-radius:50%}.ant-badge-status-success{background-color:#008771}.ant-badge-status-processing{position:relative;background-color:#1890ff}.ant-badge-status-processing:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:50%;animation:antStatusProcessing 1.2s ease-in-out infinite;content:""}.ant-badge-status-default{background-color:#d9d9d9}.ant-badge-status-error{background-color:#f5222d}.ant-badge-status-warning{background-color:#faad14}.ant-badge-status-magenta,.ant-badge-status-pink{background:#eb2f96}.ant-badge-status-red{background:#f5222d}.ant-badge-status-volcano{background:#fa541c}.ant-badge-status-orange{background:#fa8c16}.ant-badge-status-yellow{background:#fadb14}.ant-badge-status-gold{background:#faad14}.ant-badge-status-cyan{background:#13c2c2}.ant-badge-status-lime{background:#a0d911}.ant-badge-status-green{background:#008771}.ant-badge-status-blue{background:#1890ff}.ant-badge-status-geekblue{background:#2f54eb}.ant-badge-status-purple{background:#722ed1}.ant-badge-status-text{margin-left:8px;color:rgba(0,0,0,.65);font-size:14px}.ant-badge-zoom-appear,.ant-badge-zoom-enter{animation:antZoomBadgeIn .3s cubic-bezier(.12,.4,.29,1.46);animation-fill-mode:both}.ant-badge-zoom-leave{animation:antZoomBadgeOut .3s cubic-bezier(.71,-.46,.88,.6);animation-fill-mode:both}.ant-badge-not-a-wrapper:not(.ant-badge-status){vertical-align:middle}.ant-badge-not-a-wrapper .ant-scroll-number{position:relative;top:auto;display:block}.ant-badge-not-a-wrapper .ant-badge-count{transform:none}@keyframes antStatusProcessing{0%{transform:scale(.8);opacity:.5}to{transform:scale(2.4);opacity:0}}.ant-scroll-number{overflow:hidden}.ant-scroll-number-only{display:inline-block;height:20px;transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-scroll-number-only>p.ant-scroll-number-only-unit{height:20px;margin:0}.ant-scroll-number-symbol{vertical-align:top}@keyframes antZoomBadgeIn{0%{transform:scale(0) translate(50%,-50%);opacity:0}to{transform:scale(1) translate(50%,-50%)}}@keyframes antZoomBadgeOut{0%{transform:scale(1) translate(50%,-50%)}to{transform:scale(0) translate(50%,-50%);opacity:0}}.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";color:rgba(0,0,0,.45);font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:rgba(0,0,0,.45);transition:color .3s}.ant-breadcrumb a:hover{color:#18947b}.ant-breadcrumb>span:last-child,.ant-breadcrumb>span:last-child a{color:rgba(0,0,0,.65)}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:rgba(0,0,0,.45)}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-menu{box-sizing:border-box;font-size:14px;font-variant:tabular-nums;line-height:1.5;font-feature-settings:"tnum";margin:0;padding:0;color:rgba(0,0,0,.65);line-height:0;list-style:none;background:#fff;outline:none;box-shadow:0 2px 8px rgba(0,0,0,.15);transition:background .3s,width .3s cubic-bezier(.2,0,0,1) 0s;zoom:1}.ant-menu:after,.ant-menu:before{display:table;content:""}.ant-menu:after{clear:both}.ant-menu ol,.ant-menu ul{margin:0;padding:0;list-style:none}.ant-menu-hidden{display:none}.ant-menu-item-group-title{padding:8px 16px;color:rgba(0,0,0,.45);font-size:14px;line-height:1.5;transition:all .3s}.ant-menu-submenu,.ant-menu-submenu-inline{transition:border-color .3s cubic-bezier(.645,.045,.355,1),background .3s cubic-bezier(.645,.045,.355,1),padding .15s cubic-bezier(.645,.045,.355,1)}.ant-menu-submenu-selected{color:#008771}.ant-menu-item:active,.ant-menu-submenu-title:active{background:#b3c7c0}.ant-menu-submenu .ant-menu-sub{cursor:auto;transition:background .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-item>a{display:block;color:rgba(0,0,0,.65)}.ant-menu-item>a:hover{color:#008771}.ant-menu-item>a:before{position:absolute;top:0;right:0;bottom:0;left:0;background-color:transparent;content:""}.ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)}.ant-menu-item>.ant-badge>a:hover{color:#008771}.ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#008771}.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#008771}.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color:#b3c7c0}.ant-menu-inline,.ant-menu-vertical,.ant-menu-vertical-left{border-right:1px solid #e8e8e8}.ant-menu-vertical-right{border-left:1px solid #e8e8e8}.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}.ant-menu-vertical-left.ant-menu-sub .ant-menu-item:after,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item:after,.ant-menu-vertical.ant-menu-sub .ant-menu-item:after{border-right:0}.ant-menu-vertical-left.ant-menu-sub>.ant-menu-item,.ant-menu-vertical-left.ant-menu-sub>.ant-menu-submenu,.ant-menu-vertical-right.ant-menu-sub>.ant-menu-item,.ant-menu-vertical-right.ant-menu-sub>.ant-menu-submenu,.ant-menu-vertical.ant-menu-sub>.ant-menu-item,.ant-menu-vertical.ant-menu-sub>.ant-menu-submenu{transform-origin:0 0}.ant-menu-horizontal.ant-menu-sub{min-width:114px}.ant-menu-item,.ant-menu-submenu-title{position:relative;display:block;margin:0;padding:0 20px;white-space:nowrap;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1),border-color .3s cubic-bezier(.645,.045,.355,1),background .3s cubic-bezier(.645,.045,.355,1),padding .15s cubic-bezier(.645,.045,.355,1)}.ant-menu-item .anticon,.ant-menu-submenu-title .anticon{min-width:14px;margin-right:10px;font-size:14px;transition:font-size .15s cubic-bezier(.215,.61,.355,1),margin .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-item .anticon+span,.ant-menu-submenu-title .anticon+span{opacity:1;transition:opacity .3s cubic-bezier(.645,.045,.355,1),width .3s cubic-bezier(.645,.045,.355,1)}.ant-menu>.ant-menu-item-divider{height:1px;margin:1px 0;padding:0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-menu-submenu-popup{position:absolute;z-index:1050;border-radius:1rem}.ant-menu-submenu-popup .submenu-title-wrapper{padding-right:20px}.ant-menu-submenu-popup:before{position:absolute;top:-7px;right:0;bottom:0;left:0;opacity:.0001;content:" "}.ant-menu-submenu>.ant-menu{background-color:#fff;border-radius:1rem}.ant-menu-submenu>.ant-menu-submenu-title:after{transition:transform .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow{position:absolute;top:50%;right:16px;width:10px;transition:transform .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{position:absolute;width:6px;height:1.5px;background:#fff;background:rgba(0,0,0,.65)\9;background-image:linear-gradient(90deg,rgba(0,0,0,.65),rgba(0,0,0,.65));background-image:none\9;border-radius:2px;transition:background .3s cubic-bezier(.645,.045,.355,1),transform .3s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1);content:""}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{transform:rotate(45deg) translateY(-2px)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:after{transform:rotate(-45deg) translateY(2px)}.ant-menu-submenu-inline>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-inline>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before{background:linear-gradient(90deg,#008771,#008771)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{transform:rotate(-45deg) translateX(2px)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after{transform:rotate(45deg) translateX(-2px)}.ant-menu-submenu-open.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow{transform:translateY(-2px)}.ant-menu-submenu-open.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after{transform:rotate(-45deg) translateX(-2px)}.ant-menu-submenu-open.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{transform:rotate(45deg) translateX(2px)}.ant-menu-vertical-left .ant-menu-submenu-selected,.ant-menu-vertical-left .ant-menu-submenu-selected>a,.ant-menu-vertical-right .ant-menu-submenu-selected,.ant-menu-vertical-right .ant-menu-submenu-selected>a,.ant-menu-vertical .ant-menu-submenu-selected,.ant-menu-vertical .ant-menu-submenu-selected>a{color:#008771}.ant-menu-horizontal{line-height:46px;white-space:nowrap;border:0;border-bottom:1px solid #e8e8e8;box-shadow:none}.ant-menu-horizontal>.ant-menu-item,.ant-menu-horizontal>.ant-menu-submenu{position:relative;top:1px;display:inline-block;vertical-align:bottom;border-bottom:2px solid transparent}.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover{color:#008771;border-bottom:2px solid #008771}.ant-menu-horizontal>.ant-menu-item>a{display:block;color:rgba(0,0,0,.65)}.ant-menu-horizontal>.ant-menu-item>a:hover{color:#008771}.ant-menu-horizontal>.ant-menu-item>a:before{bottom:-2px}.ant-menu-horizontal>.ant-menu-item-selected>a{color:#008771}.ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"}.ant-menu-inline .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical .ant-menu-item{position:relative}.ant-menu-inline .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0;border-right:3px solid #008771;transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""}.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis}.ant-menu-inline .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu{padding-bottom:.02px}.ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child){margin-bottom:8px}.ant-menu-inline>.ant-menu-item,.ant-menu-inline>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-vertical-left>.ant-menu-item,.ant-menu-vertical-left>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-vertical-right>.ant-menu-item,.ant-menu-vertical-right>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-vertical>.ant-menu-item,.ant-menu-vertical>.ant-menu-submenu>.ant-menu-submenu-title{height:40px;line-height:40px}.ant-menu-inline{width:100%}.ant-menu-inline .ant-menu-item-selected:after,.ant-menu-inline .ant-menu-selected:after{transform:scaleY(1);opacity:1;transition:transform .15s cubic-bezier(.645,.045,.355,1),opacity .15s cubic-bezier(.645,.045,.355,1)}.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title{width:calc(100% + 1px)}.ant-menu-inline .ant-menu-submenu-title{padding-right:34px}.ant-menu-inline-collapsed{width:80px}.ant-menu-inline-collapsed>.ant-menu-item,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title{left:0;padding:0 32px!important;text-overflow:clip}.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item .ant-menu-submenu-arrow,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-inline-collapsed>.ant-menu-item .ant-menu-submenu-arrow,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title .ant-menu-submenu-arrow{display:none}.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item .anticon,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title .anticon,.ant-menu-inline-collapsed>.ant-menu-item .anticon,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title .anticon{margin:0;font-size:16px;line-height:40px}.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item .anticon+span,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title .anticon+span,.ant-menu-inline-collapsed>.ant-menu-item .anticon+span,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title .anticon+span{display:inline-block;max-width:0;opacity:0}.ant-menu-inline-collapsed-tooltip{pointer-events:none}.ant-menu-inline-collapsed-tooltip .anticon{display:none}.ant-menu-inline-collapsed-tooltip a{color:hsla(0,0%,100%,.85)}.ant-menu-inline-collapsed .ant-menu-item-group-title{padding-right:4px;padding-left:4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-menu-item-group-list{margin:0;padding:0}.ant-menu-item-group-list .ant-menu-item,.ant-menu-item-group-list .ant-menu-submenu-title{padding:0 16px 0 28px}.ant-menu-root.ant-menu-inline,.ant-menu-root.ant-menu-vertical,.ant-menu-root.ant-menu-vertical-left,.ant-menu-root.ant-menu-vertical-right{box-shadow:none}.ant-menu-sub.ant-menu-inline{padding:0;border:0;border-radius:0;box-shadow:none}.ant-menu-sub.ant-menu-inline>.ant-menu-item,.ant-menu-sub.ant-menu-inline>.ant-menu-submenu>.ant-menu-submenu-title{height:40px;line-height:40px;list-style-position:inside;list-style-type:disc}.ant-menu-sub.ant-menu-inline .ant-menu-item-group-title{padding-left:32px}.ant-menu-item-disabled,.ant-menu-submenu-disabled{color:rgba(0,0,0,.25)!important;background:none;border-color:transparent!important;cursor:not-allowed}.ant-menu-item-disabled>a,.ant-menu-submenu-disabled>a{color:rgba(0,0,0,.25)!important;pointer-events:none}.ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-submenu-disabled>.ant-menu-submenu-title{color:rgba(0,0,0,.25)!important;cursor:not-allowed}.ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:rgba(0,0,0,.25)!important}.ant-menu-dark,.ant-menu-dark .ant-menu-sub{color:hsla(0,0%,100%,.65);background:#001529}.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow{opacity:.45;transition:all .3s}.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow:before{background:#fff}.ant-menu-dark.ant-menu-submenu-popup{background:transparent}.ant-menu-dark .ant-menu-inline.ant-menu-sub{background:#000c17;box-shadow:inset 0 2px 8px rgba(0,0,0,.45)}.ant-menu-dark.ant-menu-horizontal{border-bottom:0}.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu{top:0;margin-top:0;border-color:#001529;border-bottom:0}.ant-menu-dark.ant-menu-horizontal>.ant-menu-item>a:before{bottom:0}.ant-menu-dark .ant-menu-item,.ant-menu-dark .ant-menu-item-group-title,.ant-menu-dark .ant-menu-item>a{color:hsla(0,0%,100%,.65)}.ant-menu-dark.ant-menu-inline,.ant-menu-dark.ant-menu-vertical,.ant-menu-dark.ant-menu-vertical-left,.ant-menu-dark.ant-menu-vertical-right{border-right:0}.ant-menu-dark.ant-menu-inline .ant-menu-item,.ant-menu-dark.ant-menu-vertical-left .ant-menu-item,.ant-menu-dark.ant-menu-vertical-right .ant-menu-item,.ant-menu-dark.ant-menu-vertical .ant-menu-item{left:0;margin-left:0;border-right:0}.ant-menu-dark.ant-menu-inline .ant-menu-item:after,.ant-menu-dark.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-dark.ant-menu-vertical-right .ant-menu-item:after,.ant-menu-dark.ant-menu-vertical .ant-menu-item:after{border-right:0}.ant-menu-dark.ant-menu-inline .ant-menu-item,.ant-menu-dark.ant-menu-inline .ant-menu-submenu-title{width:100%}.ant-menu-dark .ant-menu-item-active,.ant-menu-dark .ant-menu-item:hover,.ant-menu-dark .ant-menu-submenu-active,.ant-menu-dark .ant-menu-submenu-open,.ant-menu-dark .ant-menu-submenu-selected,.ant-menu-dark .ant-menu-submenu-title:hover{color:#fff;background-color:transparent}.ant-menu-dark .ant-menu-item-active>a,.ant-menu-dark .ant-menu-item:hover>a,.ant-menu-dark .ant-menu-submenu-active>a,.ant-menu-dark .ant-menu-submenu-open>a,.ant-menu-dark .ant-menu-submenu-selected>a,.ant-menu-dark .ant-menu-submenu-title:hover>a{color:#fff}.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow{opacity:1}.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:#fff}.ant-menu-dark .ant-menu-item:hover{background-color:transparent}.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}.ant-menu-dark .ant-menu-item-selected:after{border-right:0}.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#fff}.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#008771}.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}.ant-tooltip{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;z-index:1060;display:block;max-width:250px;visibility:visible}.ant-tooltip-hidden{display:none}.ant-tooltip-placement-top,.ant-tooltip-placement-topLeft,.ant-tooltip-placement-topRight{padding-bottom:8px}.ant-tooltip-placement-right,.ant-tooltip-placement-rightBottom,.ant-tooltip-placement-rightTop{padding-left:8px}.ant-tooltip-placement-bottom,.ant-tooltip-placement-bottomLeft,.ant-tooltip-placement-bottomRight{padding-top:8px}.ant-tooltip-placement-left,.ant-tooltip-placement-leftBottom,.ant-tooltip-placement-leftTop{padding-right:8px}.ant-tooltip-inner{min-width:30px;min-height:32px;padding:6px 8px;color:#fff;text-align:left;text-decoration:none;word-wrap:break-word;background-color:rgba(0,0,0,.75);border-radius:1rem;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-tooltip-arrow{position:absolute;display:block;width:13.07106781px;height:13.07106781px;overflow:hidden;background:transparent;pointer-events:none}.ant-tooltip-arrow:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;width:5px;height:5px;margin:auto;background-color:rgba(0,0,0,.75);content:"";pointer-events:auto}.ant-tooltip-placement-top .ant-tooltip-arrow,.ant-tooltip-placement-topLeft .ant-tooltip-arrow,.ant-tooltip-placement-topRight .ant-tooltip-arrow{bottom:-5.07106781px}.ant-tooltip-placement-top .ant-tooltip-arrow:before,.ant-tooltip-placement-topLeft .ant-tooltip-arrow:before,.ant-tooltip-placement-topRight .ant-tooltip-arrow:before{box-shadow:3px 3px 7px rgba(0,0,0,.07);transform:translateY(-6.53553391px) rotate(45deg)}.ant-tooltip-placement-top .ant-tooltip-arrow{left:50%;transform:translateX(-50%)}.ant-tooltip-placement-topLeft .ant-tooltip-arrow{left:13px}.ant-tooltip-placement-topRight .ant-tooltip-arrow{right:13px}.ant-tooltip-placement-right .ant-tooltip-arrow,.ant-tooltip-placement-rightBottom .ant-tooltip-arrow,.ant-tooltip-placement-rightTop .ant-tooltip-arrow{left:-5.07106781px}.ant-tooltip-placement-right .ant-tooltip-arrow:before,.ant-tooltip-placement-rightBottom .ant-tooltip-arrow:before,.ant-tooltip-placement-rightTop .ant-tooltip-arrow:before{box-shadow:-3px 3px 7px rgba(0,0,0,.07);transform:translateX(6.53553391px) rotate(45deg)}.ant-tooltip-placement-right .ant-tooltip-arrow{top:50%;transform:translateY(-50%)}.ant-tooltip-placement-rightTop .ant-tooltip-arrow{top:5px}.ant-tooltip-placement-rightBottom .ant-tooltip-arrow{bottom:5px}.ant-tooltip-placement-left .ant-tooltip-arrow,.ant-tooltip-placement-leftBottom .ant-tooltip-arrow,.ant-tooltip-placement-leftTop .ant-tooltip-arrow{right:-5.07106781px}.ant-tooltip-placement-left .ant-tooltip-arrow:before,.ant-tooltip-placement-leftBottom .ant-tooltip-arrow:before,.ant-tooltip-placement-leftTop .ant-tooltip-arrow:before{box-shadow:3px -3px 7px rgba(0,0,0,.07);transform:translateX(-6.53553391px) rotate(45deg)}.ant-tooltip-placement-left .ant-tooltip-arrow{top:50%;transform:translateY(-50%)}.ant-tooltip-placement-leftTop .ant-tooltip-arrow{top:5px}.ant-tooltip-placement-leftBottom .ant-tooltip-arrow{bottom:5px}.ant-tooltip-placement-bottom .ant-tooltip-arrow,.ant-tooltip-placement-bottomLeft .ant-tooltip-arrow,.ant-tooltip-placement-bottomRight .ant-tooltip-arrow{top:-5.07106781px}.ant-tooltip-placement-bottom .ant-tooltip-arrow:before,.ant-tooltip-placement-bottomLeft .ant-tooltip-arrow:before,.ant-tooltip-placement-bottomRight .ant-tooltip-arrow:before{box-shadow:-3px -3px 7px rgba(0,0,0,.07);transform:translateY(6.53553391px) rotate(45deg)}.ant-tooltip-placement-bottom .ant-tooltip-arrow{left:50%;transform:translateX(-50%)}.ant-tooltip-placement-bottomLeft .ant-tooltip-arrow{left:13px}.ant-tooltip-placement-bottomRight .ant-tooltip-arrow{right:13px}.ant-dropdown{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;top:-9999px;left:-9999px;z-index:1050;display:block}.ant-dropdown:before{position:absolute;top:-7px;right:0;bottom:-7px;left:-7px;z-index:-9999;opacity:.0001;content:" "}.ant-dropdown-wrap{position:relative}.ant-dropdown-wrap .ant-btn>.anticon-down{display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-wrap .ant-btn>.anticon-down{font-size:12px}.ant-dropdown-wrap .anticon-down:before{transition:transform .2s}.ant-dropdown-wrap-open .anticon-down:before{transform:rotate(180deg)}.ant-dropdown-hidden,.ant-dropdown-menu-hidden{display:none}.ant-dropdown-menu{position:relative;margin:0;padding:4px 0;text-align:left;list-style-type:none;background-color:#fff;background-clip:padding-box;border-radius:1rem;outline:none;box-shadow:0 2px 8px rgba(0,0,0,.15);-webkit-transform:translateZ(0)}.ant-dropdown-menu-item-group-title{padding:5px 12px;color:rgba(0,0,0,.45);transition:all .3s}.ant-dropdown-menu-submenu-popup{position:absolute;z-index:1050}.ant-dropdown-menu-submenu-popup>.ant-dropdown-menu{transform-origin:0 0}.ant-dropdown-menu-submenu-popup li,.ant-dropdown-menu-submenu-popup ul{list-style:none}.ant-dropdown-menu-submenu-popup ul{margin-right:.3em;margin-left:.3em;padding:0}.ant-dropdown-menu-item,.ant-dropdown-menu-submenu-title{clear:both;margin:0;padding:5px 12px;color:rgba(0,0,0,.65);font-weight:400;font-size:14px;line-height:22px;white-space:nowrap;cursor:pointer;transition:all .3s}.ant-dropdown-menu-item>.anticon:first-child,.ant-dropdown-menu-item>span>.anticon:first-child,.ant-dropdown-menu-submenu-title>.anticon:first-child,.ant-dropdown-menu-submenu-title>span>.anticon:first-child{min-width:12px;margin-right:8px;font-size:12px}.ant-dropdown-menu-item>a,.ant-dropdown-menu-submenu-title>a{display:block;margin:-5px -12px;padding:5px 12px;color:rgba(0,0,0,.65);transition:all .3s}.ant-dropdown-menu-item-selected,.ant-dropdown-menu-item-selected>a,.ant-dropdown-menu-submenu-title-selected,.ant-dropdown-menu-submenu-title-selected>a{color:#008771;background-color:#b3c7c0}.ant-dropdown-menu-item:hover,.ant-dropdown-menu-submenu-title:hover{background-color:#b3c7c0}.ant-dropdown-menu-item-disabled,.ant-dropdown-menu-submenu-title-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-dropdown-menu-item-disabled:hover,.ant-dropdown-menu-submenu-title-disabled:hover{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-dropdown-menu-item-divider,.ant-dropdown-menu-submenu-title-divider{height:1px;margin:4px 0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow,.ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow{position:absolute;right:8px}.ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow-icon,.ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow-icon{color:rgba(0,0,0,.45);font-style:normal;display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow-icon,:root .ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow-icon{font-size:12px}.ant-dropdown-menu-item-group-list{margin:0 8px;padding:0;list-style:none}.ant-dropdown-menu-submenu-title{padding-right:26px}.ant-dropdown-menu-submenu-vertical{position:relative}.ant-dropdown-menu-submenu-vertical>.ant-dropdown-menu{position:absolute;top:0;left:100%;min-width:100%;margin-left:4px;transform-origin:0 0}.ant-dropdown-menu-submenu.ant-dropdown-menu-submenu-disabled .ant-dropdown-menu-submenu-title,.ant-dropdown-menu-submenu.ant-dropdown-menu-submenu-disabled .ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow-icon{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-dropdown-menu-submenu-selected .ant-dropdown-menu-submenu-title{color:#008771}.ant-dropdown.slide-down-appear.slide-down-appear-active.ant-dropdown-placement-bottomCenter,.ant-dropdown.slide-down-appear.slide-down-appear-active.ant-dropdown-placement-bottomLeft,.ant-dropdown.slide-down-appear.slide-down-appear-active.ant-dropdown-placement-bottomRight,.ant-dropdown.slide-down-enter.slide-down-enter-active.ant-dropdown-placement-bottomCenter,.ant-dropdown.slide-down-enter.slide-down-enter-active.ant-dropdown-placement-bottomLeft,.ant-dropdown.slide-down-enter.slide-down-enter-active.ant-dropdown-placement-bottomRight{animation-name:antSlideUpIn}.ant-dropdown.slide-up-appear.slide-up-appear-active.ant-dropdown-placement-topCenter,.ant-dropdown.slide-up-appear.slide-up-appear-active.ant-dropdown-placement-topLeft,.ant-dropdown.slide-up-appear.slide-up-appear-active.ant-dropdown-placement-topRight,.ant-dropdown.slide-up-enter.slide-up-enter-active.ant-dropdown-placement-topCenter,.ant-dropdown.slide-up-enter.slide-up-enter-active.ant-dropdown-placement-topLeft,.ant-dropdown.slide-up-enter.slide-up-enter-active.ant-dropdown-placement-topRight{animation-name:antSlideDownIn}.ant-dropdown.slide-down-leave.slide-down-leave-active.ant-dropdown-placement-bottomCenter,.ant-dropdown.slide-down-leave.slide-down-leave-active.ant-dropdown-placement-bottomLeft,.ant-dropdown.slide-down-leave.slide-down-leave-active.ant-dropdown-placement-bottomRight{animation-name:antSlideUpOut}.ant-dropdown.slide-up-leave.slide-up-leave-active.ant-dropdown-placement-topCenter,.ant-dropdown.slide-up-leave.slide-up-leave-active.ant-dropdown-placement-topLeft,.ant-dropdown.slide-up-leave.slide-up-leave-active.ant-dropdown-placement-topRight{animation-name:antSlideDownOut}.ant-dropdown-link>.anticon.anticon-down,.ant-dropdown-trigger>.anticon.anticon-down{display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-link>.anticon.anticon-down,:root .ant-dropdown-trigger>.anticon.anticon-down{font-size:12px}.ant-dropdown-button{white-space:nowrap}.ant-dropdown-button.ant-btn-group>.ant-btn:last-child:not(:first-child){padding-right:8px;padding-left:8px}.ant-dropdown-button .anticon.anticon-down{display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-button .anticon.anticon-down{font-size:12px}.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu{background:#001529}.ant-dropdown-menu-dark .ant-dropdown-menu-item,.ant-dropdown-menu-dark .ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow:after,.ant-dropdown-menu-dark .ant-dropdown-menu-item>a,.ant-dropdown-menu-dark .ant-dropdown-menu-item>a .ant-dropdown-menu-submenu-arrow:after,.ant-dropdown-menu-dark .ant-dropdown-menu-submenu-title,.ant-dropdown-menu-dark .ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow:after{color:hsla(0,0%,100%,.65)}.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,.ant-dropdown-menu-dark .ant-dropdown-menu-item>a:hover,.ant-dropdown-menu-dark .ant-dropdown-menu-submenu-title:hover{color:#fff;background:transparent}.ant-dropdown-menu-dark .ant-dropdown-menu-item-selected,.ant-dropdown-menu-dark .ant-dropdown-menu-item-selected:hover,.ant-dropdown-menu-dark .ant-dropdown-menu-item-selected>a{color:#fff;background:#008771}.ant-fullcalendar{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";border-top:1px solid #d9d9d9;outline:none}.ant-select.ant-fullcalendar-year-select{min-width:90px}.ant-select.ant-fullcalendar-year-select.ant-select-sm{min-width:70px}.ant-select.ant-fullcalendar-month-select{min-width:80px;margin-left:8px}.ant-select.ant-fullcalendar-month-select.ant-select-sm{min-width:70px}.ant-fullcalendar-header{padding:11px 16px 11px 0;text-align:right}.ant-fullcalendar-header .ant-select-dropdown{text-align:left}.ant-fullcalendar-header .ant-radio-group{margin-left:8px;text-align:left}.ant-fullcalendar-header label.ant-radio-button{height:22px;padding:0 10px;line-height:20px}.ant-fullcalendar-date-panel{position:relative;outline:none}.ant-fullcalendar-calendar-body{padding:8px 12px}.ant-fullcalendar table{width:100%;max-width:100%;height:256px;background-color:transparent;border-collapse:collapse}.ant-fullcalendar table,.ant-fullcalendar td,.ant-fullcalendar th{border:0}.ant-fullcalendar td{position:relative}.ant-fullcalendar-calendar-table{margin-bottom:0;border-spacing:0}.ant-fullcalendar-column-header{width:33px;padding:0;line-height:18px;text-align:center}.ant-fullcalendar-column-header .ant-fullcalendar-column-header-inner{display:block;font-weight:400}.ant-fullcalendar-week-number-header .ant-fullcalendar-column-header-inner{display:none}.ant-fullcalendar-date,.ant-fullcalendar-month{text-align:center;transition:all .3s}.ant-fullcalendar-value{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:24px;background:transparent;border-radius:2px;transition:all .3s}.ant-fullcalendar-value:hover{background:#b3c7c0;cursor:pointer}.ant-fullcalendar-value:active{color:#fff;background:#008771}.ant-fullcalendar-month-panel-cell .ant-fullcalendar-value{width:48px}.ant-fullcalendar-month-panel-current-cell .ant-fullcalendar-value,.ant-fullcalendar-today .ant-fullcalendar-value{box-shadow:inset 0 0 0 1px #008771}.ant-fullcalendar-month-panel-selected-cell .ant-fullcalendar-value,.ant-fullcalendar-selected-day .ant-fullcalendar-value{color:#fff;background:#008771}.ant-fullcalendar-disabled-cell-first-of-row .ant-fullcalendar-value{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.ant-fullcalendar-disabled-cell-last-of-row .ant-fullcalendar-value{border-top-right-radius:1rem;border-bottom-right-radius:1rem}.ant-fullcalendar-last-month-cell .ant-fullcalendar-value,.ant-fullcalendar-next-month-btn-day .ant-fullcalendar-value{color:rgba(0,0,0,.25)}.ant-fullcalendar-month-panel-table{width:100%;table-layout:fixed;border-collapse:separate}.ant-fullcalendar-content{position:absolute;bottom:-9px;left:0;width:100%}.ant-fullcalendar-fullscreen{border-top:0}.ant-fullcalendar-fullscreen .ant-fullcalendar-table{table-layout:fixed}.ant-fullcalendar-fullscreen .ant-fullcalendar-header .ant-radio-group{margin-left:16px}.ant-fullcalendar-fullscreen .ant-fullcalendar-header label.ant-radio-button{height:32px;line-height:30px}.ant-fullcalendar-fullscreen .ant-fullcalendar-date,.ant-fullcalendar-fullscreen .ant-fullcalendar-month{display:block;height:116px;margin:0 4px;padding:4px 8px;color:rgba(0,0,0,.65);text-align:left;border-top:2px solid #e8e8e8;transition:background .3s}.ant-fullcalendar-fullscreen .ant-fullcalendar-date:hover,.ant-fullcalendar-fullscreen .ant-fullcalendar-month:hover{background:#b3c7c0;cursor:pointer}.ant-fullcalendar-fullscreen .ant-fullcalendar-date:active,.ant-fullcalendar-fullscreen .ant-fullcalendar-month:active{background:#77baa6}.ant-fullcalendar-fullscreen .ant-fullcalendar-column-header{padding-right:12px;padding-bottom:5px;text-align:right}.ant-fullcalendar-fullscreen .ant-fullcalendar-value{width:auto;text-align:right;background:transparent}.ant-fullcalendar-fullscreen .ant-fullcalendar-today .ant-fullcalendar-value{color:rgba(0,0,0,.65)}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-current-cell .ant-fullcalendar-month,.ant-fullcalendar-fullscreen .ant-fullcalendar-today .ant-fullcalendar-date{background:transparent;border-top-color:#008771}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-current-cell .ant-fullcalendar-value,.ant-fullcalendar-fullscreen .ant-fullcalendar-today .ant-fullcalendar-value{box-shadow:none}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-selected-cell .ant-fullcalendar-month,.ant-fullcalendar-fullscreen .ant-fullcalendar-selected-day .ant-fullcalendar-date{background:#b3c7c0}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-selected-cell .ant-fullcalendar-value,.ant-fullcalendar-fullscreen .ant-fullcalendar-selected-day .ant-fullcalendar-value{color:#008771}.ant-fullcalendar-fullscreen .ant-fullcalendar-last-month-cell .ant-fullcalendar-date,.ant-fullcalendar-fullscreen .ant-fullcalendar-next-month-btn-day .ant-fullcalendar-date{color:rgba(0,0,0,.25)}.ant-fullcalendar-fullscreen .ant-fullcalendar-content{position:static;width:auto;height:88px;overflow-y:auto}.ant-fullcalendar-disabled-cell .ant-fullcalendar-date,.ant-fullcalendar-disabled-cell .ant-fullcalendar-date:hover{cursor:not-allowed}.ant-fullcalendar-disabled-cell:not(.ant-fullcalendar-today) .ant-fullcalendar-date,.ant-fullcalendar-disabled-cell:not(.ant-fullcalendar-today) .ant-fullcalendar-date:hover{background:transparent}.ant-fullcalendar-disabled-cell .ant-fullcalendar-value{width:auto;color:rgba(0,0,0,.25);border-radius:0;cursor:not-allowed}.ant-radio-group{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}.ant-radio-wrapper{margin:0 8px 0 0}.ant-radio,.ant-radio-wrapper{box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;white-space:nowrap;cursor:pointer}.ant-radio{margin:0;line-height:1;vertical-align:sub;outline:none}.ant-radio-input:focus+.ant-radio-inner,.ant-radio-wrapper:hover .ant-radio,.ant-radio:hover .ant-radio-inner{border-color:#008771}.ant-radio-input:focus+.ant-radio-inner{box-shadow:0 0 0 3px rgba(0,135,113,.08)}.ant-radio-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #008771;border-radius:50%;visibility:hidden;animation:antRadioEffect .36s ease-in-out;animation-fill-mode:both;content:""}.ant-radio-wrapper:hover .ant-radio:after,.ant-radio:hover:after{visibility:visible}.ant-radio-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:100px;transition:all .3s}.ant-radio-inner:after{position:absolute;top:3px;left:3px;display:table;width:8px;height:8px;background-color:#008771;border-top:0;border-left:0;border-radius:8px;transform:scale(0);opacity:0;transition:all .3s cubic-bezier(.78,.14,.15,.86);content:" "}.ant-radio-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;cursor:pointer;opacity:0}.ant-radio-checked .ant-radio-inner{border-color:#008771}.ant-radio-checked .ant-radio-inner:after{transform:scale(1);opacity:1;transition:all .3s cubic-bezier(.78,.14,.15,.86)}.ant-radio-disabled .ant-radio-inner{background-color:#f5f5f5;border-color:#d9d9d9!important;cursor:not-allowed}.ant-radio-disabled .ant-radio-inner:after{background-color:rgba(0,0,0,.2)}.ant-radio-disabled .ant-radio-input{cursor:not-allowed}.ant-radio-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}span.ant-radio+*{padding-right:8px;padding-left:8px}.ant-radio-button-wrapper{position:relative;display:inline-block;height:32px;margin:0;padding:0 15px;color:rgba(0,0,0,.65);line-height:30px;background:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-left:0;cursor:pointer;transition:color .3s,background .3s,border-color .3s,box-shadow .3s}.ant-radio-button-wrapper a{color:rgba(0,0,0,.65)}.ant-radio-button-wrapper>.ant-radio-button{display:block;width:0;height:0;margin-left:0}.ant-radio-group-large .ant-radio-button-wrapper{height:40px;font-size:16px;line-height:38px}.ant-radio-group-small .ant-radio-button-wrapper{height:24px;padding:0 7px;line-height:22px}.ant-radio-button-wrapper:not(:first-child):before{position:absolute;top:-1px;left:-1px;display:block;box-sizing:content-box;width:1px;height:100%;padding:1px 0;background-color:#d9d9d9;transition:background-color .3s;content:""}.ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:1rem 0 0 1rem}.ant-radio-button-wrapper:last-child{border-radius:0 1rem 1rem 0}.ant-radio-button-wrapper:first-child:last-child{border-radius:1rem}.ant-radio-button-wrapper:hover{position:relative;color:#008771}.ant-radio-button-wrapper:focus-within{box-shadow:0 0 0 3px rgba(0,135,113,.08)}.ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#008771;background:#fff;border-color:#008771}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#008771}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#008771}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#18947b;border-color:#18947b}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover:before{background-color:#18947b}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#006154;border-color:#006154}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active:before{background-color:#006154}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{box-shadow:0 0 0 3px rgba(0,135,113,.08)}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#008771;border-color:#008771}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#18947b;border-color:#18947b}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#006154;border-color:#006154}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{box-shadow:0 0 0 3px rgba(0,135,113,.08)}.ant-radio-button-wrapper-disabled{cursor:not-allowed}.ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9}.ant-radio-button-wrapper-disabled:first-child{border-left-color:#d9d9d9}.ant-radio-button-wrapper-disabled.ant-radio-button-wrapper-checked{color:#fff;background-color:#e6e6e6;border-color:#d9d9d9;box-shadow:none}@keyframes antRadioEffect{0%{transform:scale(1);opacity:.5}to{transform:scale(1.6);opacity:0}}@supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal){.ant-radio{vertical-align:text-bottom}}.ant-card{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;background:#fff;border-radius:2px;transition:all .3s}.ant-card-hoverable{cursor:pointer}.ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)}.ant-card-bordered{border:1px solid #e8e8e8}.ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:transparent;border-bottom:1px solid #e8e8e8;border-radius:2px 2px 0 0;zoom:1}.ant-card-head:after,.ant-card-head:before{display:table;content:""}.ant-card-head:after{clear:both}.ant-card-head-wrapper{display:flex;align-items:center}.ant-card-head-title{display:inline-block;flex:1;padding:16px 0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-card-head .ant-tabs{clear:both;margin-bottom:-17px;color:rgba(0,0,0,.65);font-weight:400;font-size:14px}.ant-card-head .ant-tabs-bar{border-bottom:1px solid #e8e8e8}.ant-card-extra{float:right;margin-left:auto;padding:16px 0;color:rgba(0,0,0,.65);font-weight:400;font-size:14px}.ant-card-body{padding:24px;zoom:1}.ant-card-body:after,.ant-card-body:before{display:table;content:""}.ant-card-body:after{clear:both}.ant-card-contain-grid:not(.ant-card-loading) .ant-card-body{margin:-1px 0 0 -1px;padding:0}.ant-card-grid{float:left;width:33.33%;padding:24px;border:0;border-radius:0;box-shadow:1px 0 0 0 #e8e8e8,0 1px 0 0 #e8e8e8,1px 1px 0 0 #e8e8e8,inset 1px 0 0 0 #e8e8e8,inset 0 1px 0 0 #e8e8e8;transition:all .3s}.ant-card-grid-hoverable:hover{position:relative;z-index:1;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-card-contain-tabs>.ant-card-head .ant-card-head-title{min-height:32px;padding-bottom:0}.ant-card-contain-tabs>.ant-card-head .ant-card-extra{padding-bottom:0}.ant-card-cover>*{display:block;width:100%}.ant-card-cover img{border-radius:2px 2px 0 0}.ant-card-actions{margin:0;padding:0;list-style:none;background:#fafafa;border-top:1px solid #e8e8e8;zoom:1}.ant-card-actions:after,.ant-card-actions:before{display:table;content:""}.ant-card-actions:after{clear:both}.ant-card-actions>li{float:left;margin:12px 0;color:rgba(0,0,0,.45);text-align:center}.ant-card-actions>li>span{position:relative;display:block;min-width:32px;font-size:14px;line-height:22px;cursor:pointer}.ant-card-actions>li>span:hover{color:#008771;transition:color .3s}.ant-card-actions>li>span>.anticon,.ant-card-actions>li>span a:not(.ant-btn){display:inline-block;width:100%;color:rgba(0,0,0,.45);line-height:22px;transition:color .3s}.ant-card-actions>li>span>.anticon:hover,.ant-card-actions>li>span a:not(.ant-btn):hover{color:#008771}.ant-card-actions>li>span>.anticon{font-size:16px;line-height:22px}.ant-card-actions>li:not(:last-child){border-right:1px solid #e8e8e8}.ant-card-type-inner .ant-card-head{padding:0 24px;background:#fafafa}.ant-card-type-inner .ant-card-head-title{padding:12px 0;font-size:14px}.ant-card-type-inner .ant-card-body{padding:16px 24px}.ant-card-type-inner .ant-card-extra{padding:13.5px 0}.ant-card-meta{margin:-4px 0;zoom:1}.ant-card-meta:after,.ant-card-meta:before{display:table;content:""}.ant-card-meta:after{clear:both}.ant-card-meta-avatar{float:left;padding-right:16px}.ant-card-meta-detail{overflow:hidden}.ant-card-meta-detail>div:not(:last-child){margin-bottom:8px}.ant-card-meta-title{overflow:hidden;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-overflow:ellipsis}.ant-card-meta-description{color:rgba(0,0,0,.45)}.ant-card-loading{overflow:hidden}.ant-card-loading .ant-card-body{-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-card-loading-content p{margin:0}.ant-card-loading-block{height:14px;margin:4px 0;background:linear-gradient(90deg,rgba(207,216,220,.2),rgba(207,216,220,.4),rgba(207,216,220,.2));background-size:600% 600%;border-radius:2px;animation:card-loading 1.4s ease infinite}@keyframes card-loading{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.ant-card-small>.ant-card-head{min-height:36px;padding:0 12px;font-size:14px}.ant-card-small>.ant-card-head>.ant-card-head-wrapper>.ant-card-head-title{padding:8px 0}.ant-card-small>.ant-card-head>.ant-card-head-wrapper>.ant-card-extra{padding:8px 0;font-size:14px}.ant-card-small>.ant-card-body{padding:12px}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-container{height:40px}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-ink-bar{visibility:hidden}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab{height:40px;margin:0 2px 0 0;padding:0 16px;line-height:38px;background:#fafafa;border:1px solid #e8e8e8;border-radius:1rem 1rem 0 0;transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active{height:40px;color:#008771;background:#fff;border-color:#e8e8e8;border-bottom:1px solid #fff}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active:before{border-top:2px solid transparent}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-disabled{color:#008771;color:rgba(0,0,0,.25)}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-inactive{padding:0}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-wrap{margin-bottom:0}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x{width:16px;height:16px;height:14px;margin-right:-5px;margin-left:3px;overflow:hidden;color:rgba(0,0,0,.45);font-size:12px;vertical-align:middle;transition:all .3s}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x:hover{color:rgba(0,0,0,.85)}.ant-tabs.ant-tabs-card .ant-tabs-card-content>.ant-tabs-tabpane,.ant-tabs.ant-tabs-editable-card .ant-tabs-card-content>.ant-tabs-tabpane{transition:none!important}.ant-tabs.ant-tabs-card .ant-tabs-card-content>.ant-tabs-tabpane-inactive,.ant-tabs.ant-tabs-editable-card .ant-tabs-card-content>.ant-tabs-tabpane-inactive{overflow:hidden}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab:hover .anticon-close{opacity:1}.ant-tabs-extra-content{line-height:45px}.ant-tabs-extra-content .ant-tabs-new-tab{position:relative;width:20px;height:20px;color:rgba(0,0,0,.65);font-size:12px;line-height:20px;text-align:center;border:1px solid #e8e8e8;border-radius:2px;cursor:pointer;transition:all .3s}.ant-tabs-extra-content .ant-tabs-new-tab:hover{color:#008771;border-color:#008771}.ant-tabs-extra-content .ant-tabs-new-tab svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-tabs.ant-tabs-large .ant-tabs-extra-content{line-height:56px}.ant-tabs.ant-tabs-small .ant-tabs-extra-content{line-height:37px}.ant-tabs.ant-tabs-card .ant-tabs-extra-content{line-height:40px}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-nav-container{height:100%}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab{margin-bottom:8px;border-bottom:1px solid #e8e8e8}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab-active,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{padding-bottom:4px}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab:last-child,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab:last-child{margin-bottom:8px}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-new-tab,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-new-tab{width:90%}.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-nav-wrap{margin-right:0}.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab{margin-right:1px;border-right:0;border-radius:1rem 0 0 1rem}.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab-active{margin-right:-1px;padding-right:18px}.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-nav-wrap{margin-left:0}.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab{margin-left:1px;border-left:0;border-radius:0 1rem 1rem 0}.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px}.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 1rem 1rem}.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#008771}.ant-tabs{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;overflow:hidden;zoom:1}.ant-tabs:after,.ant-tabs:before{display:table;content:""}.ant-tabs:after{clear:both}.ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:#008771;transform-origin:0 0}.ant-tabs-bar{margin:0 0 16px;border-bottom:1px solid #e8e8e8;outline:none}.ant-tabs-bar,.ant-tabs-nav-container{transition:padding .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-nav-container{position:relative;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1}.ant-tabs-nav-container:after,.ant-tabs-nav-container:before{display:table;content:""}.ant-tabs-nav-container:after{clear:both}.ant-tabs-nav-container-scrolling{padding-right:32px;padding-left:32px}.ant-tabs-bottom .ant-tabs-bottom-bar{margin-top:16px;margin-bottom:0;border-top:1px solid #e8e8e8;border-bottom:none}.ant-tabs-bottom .ant-tabs-bottom-bar .ant-tabs-ink-bar{top:1px;bottom:auto}.ant-tabs-bottom .ant-tabs-bottom-bar .ant-tabs-nav-container{margin-top:-1px;margin-bottom:0}.ant-tabs-tab-next,.ant-tabs-tab-prev{position:absolute;z-index:2;width:0;height:100%;color:rgba(0,0,0,.45);text-align:center;background-color:transparent;border:0;cursor:pointer;opacity:0;transition:width .3s cubic-bezier(.645,.045,.355,1),opacity .3s cubic-bezier(.645,.045,.355,1),color .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none}.ant-tabs-tab-next.ant-tabs-tab-arrow-show,.ant-tabs-tab-prev.ant-tabs-tab-arrow-show{width:32px;height:100%;opacity:1;pointer-events:auto}.ant-tabs-tab-next:hover,.ant-tabs-tab-prev:hover{color:rgba(0,0,0,.65)}.ant-tabs-tab-next-icon,.ant-tabs-tab-prev-icon{position:absolute;top:50%;left:50%;font-weight:700;font-style:normal;font-variant:normal;line-height:inherit;text-align:center;text-transform:none;transform:translate(-50%,-50%)}.ant-tabs-tab-next-icon-target,.ant-tabs-tab-prev-icon-target{display:block;display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg)}:root .ant-tabs-tab-next-icon-target,:root .ant-tabs-tab-prev-icon-target{font-size:12px}.ant-tabs-tab-btn-disabled{cursor:not-allowed}.ant-tabs-tab-btn-disabled,.ant-tabs-tab-btn-disabled:hover{color:rgba(0,0,0,.25)}.ant-tabs-tab-next{right:2px}.ant-tabs-tab-prev{left:0}:root .ant-tabs-tab-prev{filter:none}.ant-tabs-nav-wrap{margin-bottom:-1px;overflow:hidden}.ant-tabs-nav-scroll{overflow:hidden;white-space:nowrap}.ant-tabs-nav{position:relative;display:inline-block;box-sizing:border-box;margin:0;padding-left:0;list-style:none;transition:transform .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-nav:after,.ant-tabs-nav:before{display:table;content:" "}.ant-tabs-nav:after{clear:both}.ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:1rem 1rem 0 0;transition:all .3s;content:"";pointer-events:none}.ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0}.ant-tabs-nav .ant-tabs-tab:hover{color:#18947b}.ant-tabs-nav .ant-tabs-tab:active{color:#006154}.ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px}.ant-tabs-nav .ant-tabs-tab-active{color:#008771;text-shadow:0 0 .25px currentColor}.ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px}.ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px}.ant-tabs .ant-tabs-small-bar .ant-tabs-nav-container{font-size:14px}.ant-tabs .ant-tabs-small-bar .ant-tabs-tab{padding:8px 16px}.ant-tabs-content:before{display:block;overflow:hidden;content:""}.ant-tabs .ant-tabs-bottom-content,.ant-tabs .ant-tabs-top-content{width:100%}.ant-tabs .ant-tabs-bottom-content>.ant-tabs-tabpane,.ant-tabs .ant-tabs-top-content>.ant-tabs-tabpane{flex-shrink:0;width:100%;-webkit-backface-visibility:hidden;opacity:1;transition:opacity .45s}.ant-tabs .ant-tabs-bottom-content>.ant-tabs-tabpane-inactive,.ant-tabs .ant-tabs-top-content>.ant-tabs-tabpane-inactive{height:0;padding:0!important;overflow:hidden;opacity:0;pointer-events:none}.ant-tabs .ant-tabs-bottom-content>.ant-tabs-tabpane-inactive input,.ant-tabs .ant-tabs-top-content>.ant-tabs-tabpane-inactive input{visibility:hidden}.ant-tabs .ant-tabs-bottom-content.ant-tabs-content-animated,.ant-tabs .ant-tabs-top-content.ant-tabs-content-animated{display:flex;flex-direction:row;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);will-change:margin-left}.ant-tabs .ant-tabs-left-bar,.ant-tabs .ant-tabs-right-bar{height:100%;border-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-arrow-show,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-arrow-show{width:100%;height:32px}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab{display:block;float:none;margin:0 0 16px;padding:8px 24px}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab:last-child,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab:last-child{margin-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-extra-content,.ant-tabs .ant-tabs-right-bar .ant-tabs-extra-content{text-align:center}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-scroll,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-scroll{width:auto}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap{height:100%}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container{margin-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container.ant-tabs-nav-container-scrolling,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container.ant-tabs-nav-container-scrolling{padding:32px 0}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap{margin-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav{width:100%}.ant-tabs .ant-tabs-left-bar .ant-tabs-ink-bar,.ant-tabs .ant-tabs-right-bar .ant-tabs-ink-bar{top:0;bottom:auto;left:auto;width:2px;height:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-next,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-next{right:0;bottom:0;width:100%;height:32px}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-prev,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-prev{top:0;width:100%;height:32px}.ant-tabs .ant-tabs-left-content,.ant-tabs .ant-tabs-right-content{width:auto;margin-top:0!important;overflow:hidden}.ant-tabs .ant-tabs-left-bar{float:left;margin-right:-1px;margin-bottom:0;border-right:1px solid #e8e8e8}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab{text-align:right}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap{margin-right:-1px}.ant-tabs .ant-tabs-left-bar .ant-tabs-ink-bar{right:1px}.ant-tabs .ant-tabs-left-content{padding-left:24px;border-left:1px solid #e8e8e8}.ant-tabs .ant-tabs-right-bar{float:right;margin-bottom:0;margin-left:-1px;border-left:1px solid #e8e8e8}.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap{margin-left:-1px}.ant-tabs .ant-tabs-right-bar .ant-tabs-ink-bar{left:1px}.ant-tabs .ant-tabs-right-content{padding-right:24px;border-right:1px solid #e8e8e8}.ant-tabs-bottom .ant-tabs-ink-bar-animated,.ant-tabs-top .ant-tabs-ink-bar-animated{transition:transform .3s cubic-bezier(.645,.045,.355,1),width .2s cubic-bezier(.645,.045,.355,1),left .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-left .ant-tabs-ink-bar-animated,.ant-tabs-right .ant-tabs-ink-bar-animated{transition:transform .3s cubic-bezier(.645,.045,.355,1),height .2s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-no-animation>.ant-tabs-content>.ant-tabs-content-animated,.no-flex>.ant-tabs-content>.ant-tabs-content-animated{margin-left:0!important;transform:none!important}.ant-tabs-no-animation>.ant-tabs-content>.ant-tabs-tabpane-inactive,.no-flex>.ant-tabs-content>.ant-tabs-tabpane-inactive{height:0;padding:0!important;overflow:hidden;opacity:0;pointer-events:none}.ant-tabs-no-animation>.ant-tabs-content>.ant-tabs-tabpane-inactive input,.no-flex>.ant-tabs-content>.ant-tabs-tabpane-inactive input{visibility:hidden}.ant-tabs-left-content>.ant-tabs-content-animated,.ant-tabs-right-content>.ant-tabs-content-animated{margin-left:0!important;transform:none!important}.ant-tabs-left-content>.ant-tabs-tabpane-inactive,.ant-tabs-right-content>.ant-tabs-tabpane-inactive{height:0;padding:0!important;overflow:hidden;opacity:0;pointer-events:none}.ant-tabs-left-content>.ant-tabs-tabpane-inactive input,.ant-tabs-right-content>.ant-tabs-tabpane-inactive input{visibility:hidden}.ant-row{position:relative;height:auto;margin-right:0;margin-left:0;zoom:1;display:block;box-sizing:border-box}.ant-row:after,.ant-row:before{display:table;content:""}.ant-row+.ant-row:before,.ant-row:after{clear:both}.ant-row-flex{display:flex;flex-flow:row wrap}.ant-row-flex:after,.ant-row-flex:before{display:flex}.ant-row-flex-start{justify-content:flex-start}.ant-row-flex-center{justify-content:center}.ant-row-flex-end{justify-content:flex-end}.ant-row-flex-space-between{justify-content:space-between}.ant-row-flex-space-around{justify-content:space-around}.ant-row-flex-top{align-items:flex-start}.ant-row-flex-middle{align-items:center}.ant-row-flex-bottom{align-items:flex-end}.ant-col{position:relative;min-height:1px}.ant-col-1,.ant-col-2,.ant-col-3,.ant-col-4,.ant-col-5,.ant-col-6,.ant-col-7,.ant-col-8,.ant-col-9,.ant-col-10,.ant-col-11,.ant-col-12,.ant-col-13,.ant-col-14,.ant-col-15,.ant-col-16,.ant-col-17,.ant-col-18,.ant-col-19,.ant-col-20,.ant-col-21,.ant-col-22,.ant-col-23,.ant-col-24,.ant-col-lg-1,.ant-col-lg-2,.ant-col-lg-3,.ant-col-lg-4,.ant-col-lg-5,.ant-col-lg-6,.ant-col-lg-7,.ant-col-lg-8,.ant-col-lg-9,.ant-col-lg-10,.ant-col-lg-11,.ant-col-lg-12,.ant-col-lg-13,.ant-col-lg-14,.ant-col-lg-15,.ant-col-lg-16,.ant-col-lg-17,.ant-col-lg-18,.ant-col-lg-19,.ant-col-lg-20,.ant-col-lg-21,.ant-col-lg-22,.ant-col-lg-23,.ant-col-lg-24,.ant-col-md-1,.ant-col-md-2,.ant-col-md-3,.ant-col-md-4,.ant-col-md-5,.ant-col-md-6,.ant-col-md-7,.ant-col-md-8,.ant-col-md-9,.ant-col-md-10,.ant-col-md-11,.ant-col-md-12,.ant-col-md-13,.ant-col-md-14,.ant-col-md-15,.ant-col-md-16,.ant-col-md-17,.ant-col-md-18,.ant-col-md-19,.ant-col-md-20,.ant-col-md-21,.ant-col-md-22,.ant-col-md-23,.ant-col-md-24,.ant-col-sm-1,.ant-col-sm-2,.ant-col-sm-3,.ant-col-sm-4,.ant-col-sm-5,.ant-col-sm-6,.ant-col-sm-7,.ant-col-sm-8,.ant-col-sm-9,.ant-col-sm-10,.ant-col-sm-11,.ant-col-sm-12,.ant-col-sm-13,.ant-col-sm-14,.ant-col-sm-15,.ant-col-sm-16,.ant-col-sm-17,.ant-col-sm-18,.ant-col-sm-19,.ant-col-sm-20,.ant-col-sm-21,.ant-col-sm-22,.ant-col-sm-23,.ant-col-sm-24,.ant-col-xs-1,.ant-col-xs-2,.ant-col-xs-3,.ant-col-xs-4,.ant-col-xs-5,.ant-col-xs-6,.ant-col-xs-7,.ant-col-xs-8,.ant-col-xs-9,.ant-col-xs-10,.ant-col-xs-11,.ant-col-xs-12,.ant-col-xs-13,.ant-col-xs-14,.ant-col-xs-15,.ant-col-xs-16,.ant-col-xs-17,.ant-col-xs-18,.ant-col-xs-19,.ant-col-xs-20,.ant-col-xs-21,.ant-col-xs-22,.ant-col-xs-23,.ant-col-xs-24{position:relative;padding-right:0;padding-left:0}.ant-col-1,.ant-col-2,.ant-col-3,.ant-col-4,.ant-col-5,.ant-col-6,.ant-col-7,.ant-col-8,.ant-col-9,.ant-col-10,.ant-col-11,.ant-col-12,.ant-col-13,.ant-col-14,.ant-col-15,.ant-col-16,.ant-col-17,.ant-col-18,.ant-col-19,.ant-col-20,.ant-col-21,.ant-col-22,.ant-col-23,.ant-col-24{flex:0 0 auto;float:left}.ant-col-24{display:block;box-sizing:border-box;width:100%}.ant-col-push-24{left:100%}.ant-col-pull-24{right:100%}.ant-col-offset-24{margin-left:100%}.ant-col-order-24{order:24}.ant-col-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-push-23{left:95.83333333%}.ant-col-pull-23{right:95.83333333%}.ant-col-offset-23{margin-left:95.83333333%}.ant-col-order-23{order:23}.ant-col-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-push-22{left:91.66666667%}.ant-col-pull-22{right:91.66666667%}.ant-col-offset-22{margin-left:91.66666667%}.ant-col-order-22{order:22}.ant-col-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-push-21{left:87.5%}.ant-col-pull-21{right:87.5%}.ant-col-offset-21{margin-left:87.5%}.ant-col-order-21{order:21}.ant-col-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-push-20{left:83.33333333%}.ant-col-pull-20{right:83.33333333%}.ant-col-offset-20{margin-left:83.33333333%}.ant-col-order-20{order:20}.ant-col-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-push-19{left:79.16666667%}.ant-col-pull-19{right:79.16666667%}.ant-col-offset-19{margin-left:79.16666667%}.ant-col-order-19{order:19}.ant-col-18{display:block;box-sizing:border-box;width:75%}.ant-col-push-18{left:75%}.ant-col-pull-18{right:75%}.ant-col-offset-18{margin-left:75%}.ant-col-order-18{order:18}.ant-col-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-push-17{left:70.83333333%}.ant-col-pull-17{right:70.83333333%}.ant-col-offset-17{margin-left:70.83333333%}.ant-col-order-17{order:17}.ant-col-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-push-16{left:66.66666667%}.ant-col-pull-16{right:66.66666667%}.ant-col-offset-16{margin-left:66.66666667%}.ant-col-order-16{order:16}.ant-col-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-push-15{left:62.5%}.ant-col-pull-15{right:62.5%}.ant-col-offset-15{margin-left:62.5%}.ant-col-order-15{order:15}.ant-col-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-push-14{left:58.33333333%}.ant-col-pull-14{right:58.33333333%}.ant-col-offset-14{margin-left:58.33333333%}.ant-col-order-14{order:14}.ant-col-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-push-13{left:54.16666667%}.ant-col-pull-13{right:54.16666667%}.ant-col-offset-13{margin-left:54.16666667%}.ant-col-order-13{order:13}.ant-col-12{display:block;box-sizing:border-box;width:50%}.ant-col-push-12{left:50%}.ant-col-pull-12{right:50%}.ant-col-offset-12{margin-left:50%}.ant-col-order-12{order:12}.ant-col-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-push-11{left:45.83333333%}.ant-col-pull-11{right:45.83333333%}.ant-col-offset-11{margin-left:45.83333333%}.ant-col-order-11{order:11}.ant-col-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-push-10{left:41.66666667%}.ant-col-pull-10{right:41.66666667%}.ant-col-offset-10{margin-left:41.66666667%}.ant-col-order-10{order:10}.ant-col-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-push-9{left:37.5%}.ant-col-pull-9{right:37.5%}.ant-col-offset-9{margin-left:37.5%}.ant-col-order-9{order:9}.ant-col-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-push-8{left:33.33333333%}.ant-col-pull-8{right:33.33333333%}.ant-col-offset-8{margin-left:33.33333333%}.ant-col-order-8{order:8}.ant-col-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-push-7{left:29.16666667%}.ant-col-pull-7{right:29.16666667%}.ant-col-offset-7{margin-left:29.16666667%}.ant-col-order-7{order:7}.ant-col-6{display:block;box-sizing:border-box;width:25%}.ant-col-push-6{left:25%}.ant-col-pull-6{right:25%}.ant-col-offset-6{margin-left:25%}.ant-col-order-6{order:6}.ant-col-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-push-5{left:20.83333333%}.ant-col-pull-5{right:20.83333333%}.ant-col-offset-5{margin-left:20.83333333%}.ant-col-order-5{order:5}.ant-col-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-push-4{left:16.66666667%}.ant-col-pull-4{right:16.66666667%}.ant-col-offset-4{margin-left:16.66666667%}.ant-col-order-4{order:4}.ant-col-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-push-3{left:12.5%}.ant-col-pull-3{right:12.5%}.ant-col-offset-3{margin-left:12.5%}.ant-col-order-3{order:3}.ant-col-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-push-2{left:8.33333333%}.ant-col-pull-2{right:8.33333333%}.ant-col-offset-2{margin-left:8.33333333%}.ant-col-order-2{order:2}.ant-col-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-push-1{left:4.16666667%}.ant-col-pull-1{right:4.16666667%}.ant-col-offset-1{margin-left:4.16666667%}.ant-col-order-1{order:1}.ant-col-0{display:none}.ant-col-offset-0{margin-left:0}.ant-col-order-0{order:0}.ant-col-xs-1,.ant-col-xs-2,.ant-col-xs-3,.ant-col-xs-4,.ant-col-xs-5,.ant-col-xs-6,.ant-col-xs-7,.ant-col-xs-8,.ant-col-xs-9,.ant-col-xs-10,.ant-col-xs-11,.ant-col-xs-12,.ant-col-xs-13,.ant-col-xs-14,.ant-col-xs-15,.ant-col-xs-16,.ant-col-xs-17,.ant-col-xs-18,.ant-col-xs-19,.ant-col-xs-20,.ant-col-xs-21,.ant-col-xs-22,.ant-col-xs-23,.ant-col-xs-24{flex:0 0 auto;float:left}.ant-col-xs-24{display:block;box-sizing:border-box;width:100%}.ant-col-xs-push-24{left:100%}.ant-col-xs-pull-24{right:100%}.ant-col-xs-offset-24{margin-left:100%}.ant-col-xs-order-24{order:24}.ant-col-xs-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-xs-push-23{left:95.83333333%}.ant-col-xs-pull-23{right:95.83333333%}.ant-col-xs-offset-23{margin-left:95.83333333%}.ant-col-xs-order-23{order:23}.ant-col-xs-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-xs-push-22{left:91.66666667%}.ant-col-xs-pull-22{right:91.66666667%}.ant-col-xs-offset-22{margin-left:91.66666667%}.ant-col-xs-order-22{order:22}.ant-col-xs-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-xs-push-21{left:87.5%}.ant-col-xs-pull-21{right:87.5%}.ant-col-xs-offset-21{margin-left:87.5%}.ant-col-xs-order-21{order:21}.ant-col-xs-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-xs-push-20{left:83.33333333%}.ant-col-xs-pull-20{right:83.33333333%}.ant-col-xs-offset-20{margin-left:83.33333333%}.ant-col-xs-order-20{order:20}.ant-col-xs-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-xs-push-19{left:79.16666667%}.ant-col-xs-pull-19{right:79.16666667%}.ant-col-xs-offset-19{margin-left:79.16666667%}.ant-col-xs-order-19{order:19}.ant-col-xs-18{display:block;box-sizing:border-box;width:75%}.ant-col-xs-push-18{left:75%}.ant-col-xs-pull-18{right:75%}.ant-col-xs-offset-18{margin-left:75%}.ant-col-xs-order-18{order:18}.ant-col-xs-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-xs-push-17{left:70.83333333%}.ant-col-xs-pull-17{right:70.83333333%}.ant-col-xs-offset-17{margin-left:70.83333333%}.ant-col-xs-order-17{order:17}.ant-col-xs-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-xs-push-16{left:66.66666667%}.ant-col-xs-pull-16{right:66.66666667%}.ant-col-xs-offset-16{margin-left:66.66666667%}.ant-col-xs-order-16{order:16}.ant-col-xs-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-xs-push-15{left:62.5%}.ant-col-xs-pull-15{right:62.5%}.ant-col-xs-offset-15{margin-left:62.5%}.ant-col-xs-order-15{order:15}.ant-col-xs-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-xs-push-14{left:58.33333333%}.ant-col-xs-pull-14{right:58.33333333%}.ant-col-xs-offset-14{margin-left:58.33333333%}.ant-col-xs-order-14{order:14}.ant-col-xs-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-xs-push-13{left:54.16666667%}.ant-col-xs-pull-13{right:54.16666667%}.ant-col-xs-offset-13{margin-left:54.16666667%}.ant-col-xs-order-13{order:13}.ant-col-xs-12{display:block;box-sizing:border-box;width:50%}.ant-col-xs-push-12{left:50%}.ant-col-xs-pull-12{right:50%}.ant-col-xs-offset-12{margin-left:50%}.ant-col-xs-order-12{order:12}.ant-col-xs-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-xs-push-11{left:45.83333333%}.ant-col-xs-pull-11{right:45.83333333%}.ant-col-xs-offset-11{margin-left:45.83333333%}.ant-col-xs-order-11{order:11}.ant-col-xs-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-xs-push-10{left:41.66666667%}.ant-col-xs-pull-10{right:41.66666667%}.ant-col-xs-offset-10{margin-left:41.66666667%}.ant-col-xs-order-10{order:10}.ant-col-xs-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-xs-push-9{left:37.5%}.ant-col-xs-pull-9{right:37.5%}.ant-col-xs-offset-9{margin-left:37.5%}.ant-col-xs-order-9{order:9}.ant-col-xs-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-xs-push-8{left:33.33333333%}.ant-col-xs-pull-8{right:33.33333333%}.ant-col-xs-offset-8{margin-left:33.33333333%}.ant-col-xs-order-8{order:8}.ant-col-xs-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-xs-push-7{left:29.16666667%}.ant-col-xs-pull-7{right:29.16666667%}.ant-col-xs-offset-7{margin-left:29.16666667%}.ant-col-xs-order-7{order:7}.ant-col-xs-6{display:block;box-sizing:border-box;width:25%}.ant-col-xs-push-6{left:25%}.ant-col-xs-pull-6{right:25%}.ant-col-xs-offset-6{margin-left:25%}.ant-col-xs-order-6{order:6}.ant-col-xs-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-xs-push-5{left:20.83333333%}.ant-col-xs-pull-5{right:20.83333333%}.ant-col-xs-offset-5{margin-left:20.83333333%}.ant-col-xs-order-5{order:5}.ant-col-xs-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-xs-push-4{left:16.66666667%}.ant-col-xs-pull-4{right:16.66666667%}.ant-col-xs-offset-4{margin-left:16.66666667%}.ant-col-xs-order-4{order:4}.ant-col-xs-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-xs-push-3{left:12.5%}.ant-col-xs-pull-3{right:12.5%}.ant-col-xs-offset-3{margin-left:12.5%}.ant-col-xs-order-3{order:3}.ant-col-xs-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-xs-push-2{left:8.33333333%}.ant-col-xs-pull-2{right:8.33333333%}.ant-col-xs-offset-2{margin-left:8.33333333%}.ant-col-xs-order-2{order:2}.ant-col-xs-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-xs-push-1{left:4.16666667%}.ant-col-xs-pull-1{right:4.16666667%}.ant-col-xs-offset-1{margin-left:4.16666667%}.ant-col-xs-order-1{order:1}.ant-col-xs-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-xs-push-0{left:auto}.ant-col-xs-pull-0{right:auto}.ant-col-xs-offset-0{margin-left:0}.ant-col-xs-order-0{order:0}@media (min-width:576px){.ant-col-sm-1,.ant-col-sm-2,.ant-col-sm-3,.ant-col-sm-4,.ant-col-sm-5,.ant-col-sm-6,.ant-col-sm-7,.ant-col-sm-8,.ant-col-sm-9,.ant-col-sm-10,.ant-col-sm-11,.ant-col-sm-12,.ant-col-sm-13,.ant-col-sm-14,.ant-col-sm-15,.ant-col-sm-16,.ant-col-sm-17,.ant-col-sm-18,.ant-col-sm-19,.ant-col-sm-20,.ant-col-sm-21,.ant-col-sm-22,.ant-col-sm-23,.ant-col-sm-24{flex:0 0 auto;float:left}.ant-col-sm-24{display:block;box-sizing:border-box;width:100%}.ant-col-sm-push-24{left:100%}.ant-col-sm-pull-24{right:100%}.ant-col-sm-offset-24{margin-left:100%}.ant-col-sm-order-24{order:24}.ant-col-sm-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-sm-push-23{left:95.83333333%}.ant-col-sm-pull-23{right:95.83333333%}.ant-col-sm-offset-23{margin-left:95.83333333%}.ant-col-sm-order-23{order:23}.ant-col-sm-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-sm-push-22{left:91.66666667%}.ant-col-sm-pull-22{right:91.66666667%}.ant-col-sm-offset-22{margin-left:91.66666667%}.ant-col-sm-order-22{order:22}.ant-col-sm-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-sm-push-21{left:87.5%}.ant-col-sm-pull-21{right:87.5%}.ant-col-sm-offset-21{margin-left:87.5%}.ant-col-sm-order-21{order:21}.ant-col-sm-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-sm-push-20{left:83.33333333%}.ant-col-sm-pull-20{right:83.33333333%}.ant-col-sm-offset-20{margin-left:83.33333333%}.ant-col-sm-order-20{order:20}.ant-col-sm-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-sm-push-19{left:79.16666667%}.ant-col-sm-pull-19{right:79.16666667%}.ant-col-sm-offset-19{margin-left:79.16666667%}.ant-col-sm-order-19{order:19}.ant-col-sm-18{display:block;box-sizing:border-box;width:75%}.ant-col-sm-push-18{left:75%}.ant-col-sm-pull-18{right:75%}.ant-col-sm-offset-18{margin-left:75%}.ant-col-sm-order-18{order:18}.ant-col-sm-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-sm-push-17{left:70.83333333%}.ant-col-sm-pull-17{right:70.83333333%}.ant-col-sm-offset-17{margin-left:70.83333333%}.ant-col-sm-order-17{order:17}.ant-col-sm-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-sm-push-16{left:66.66666667%}.ant-col-sm-pull-16{right:66.66666667%}.ant-col-sm-offset-16{margin-left:66.66666667%}.ant-col-sm-order-16{order:16}.ant-col-sm-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-sm-push-15{left:62.5%}.ant-col-sm-pull-15{right:62.5%}.ant-col-sm-offset-15{margin-left:62.5%}.ant-col-sm-order-15{order:15}.ant-col-sm-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-sm-push-14{left:58.33333333%}.ant-col-sm-pull-14{right:58.33333333%}.ant-col-sm-offset-14{margin-left:58.33333333%}.ant-col-sm-order-14{order:14}.ant-col-sm-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-sm-push-13{left:54.16666667%}.ant-col-sm-pull-13{right:54.16666667%}.ant-col-sm-offset-13{margin-left:54.16666667%}.ant-col-sm-order-13{order:13}.ant-col-sm-12{display:block;box-sizing:border-box;width:50%}.ant-col-sm-push-12{left:50%}.ant-col-sm-pull-12{right:50%}.ant-col-sm-offset-12{margin-left:50%}.ant-col-sm-order-12{order:12}.ant-col-sm-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-sm-push-11{left:45.83333333%}.ant-col-sm-pull-11{right:45.83333333%}.ant-col-sm-offset-11{margin-left:45.83333333%}.ant-col-sm-order-11{order:11}.ant-col-sm-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-sm-push-10{left:41.66666667%}.ant-col-sm-pull-10{right:41.66666667%}.ant-col-sm-offset-10{margin-left:41.66666667%}.ant-col-sm-order-10{order:10}.ant-col-sm-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-sm-push-9{left:37.5%}.ant-col-sm-pull-9{right:37.5%}.ant-col-sm-offset-9{margin-left:37.5%}.ant-col-sm-order-9{order:9}.ant-col-sm-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-sm-push-8{left:33.33333333%}.ant-col-sm-pull-8{right:33.33333333%}.ant-col-sm-offset-8{margin-left:33.33333333%}.ant-col-sm-order-8{order:8}.ant-col-sm-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-sm-push-7{left:29.16666667%}.ant-col-sm-pull-7{right:29.16666667%}.ant-col-sm-offset-7{margin-left:29.16666667%}.ant-col-sm-order-7{order:7}.ant-col-sm-6{display:block;box-sizing:border-box;width:25%}.ant-col-sm-push-6{left:25%}.ant-col-sm-pull-6{right:25%}.ant-col-sm-offset-6{margin-left:25%}.ant-col-sm-order-6{order:6}.ant-col-sm-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-sm-push-5{left:20.83333333%}.ant-col-sm-pull-5{right:20.83333333%}.ant-col-sm-offset-5{margin-left:20.83333333%}.ant-col-sm-order-5{order:5}.ant-col-sm-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-sm-push-4{left:16.66666667%}.ant-col-sm-pull-4{right:16.66666667%}.ant-col-sm-offset-4{margin-left:16.66666667%}.ant-col-sm-order-4{order:4}.ant-col-sm-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-sm-push-3{left:12.5%}.ant-col-sm-pull-3{right:12.5%}.ant-col-sm-offset-3{margin-left:12.5%}.ant-col-sm-order-3{order:3}.ant-col-sm-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-sm-push-2{left:8.33333333%}.ant-col-sm-pull-2{right:8.33333333%}.ant-col-sm-offset-2{margin-left:8.33333333%}.ant-col-sm-order-2{order:2}.ant-col-sm-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-sm-push-1{left:4.16666667%}.ant-col-sm-pull-1{right:4.16666667%}.ant-col-sm-offset-1{margin-left:4.16666667%}.ant-col-sm-order-1{order:1}.ant-col-sm-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-sm-push-0{left:auto}.ant-col-sm-pull-0{right:auto}.ant-col-sm-offset-0{margin-left:0}.ant-col-sm-order-0{order:0}}@media (min-width:768px){.ant-col-md-1,.ant-col-md-2,.ant-col-md-3,.ant-col-md-4,.ant-col-md-5,.ant-col-md-6,.ant-col-md-7,.ant-col-md-8,.ant-col-md-9,.ant-col-md-10,.ant-col-md-11,.ant-col-md-12,.ant-col-md-13,.ant-col-md-14,.ant-col-md-15,.ant-col-md-16,.ant-col-md-17,.ant-col-md-18,.ant-col-md-19,.ant-col-md-20,.ant-col-md-21,.ant-col-md-22,.ant-col-md-23,.ant-col-md-24{flex:0 0 auto;float:left}.ant-col-md-24{display:block;box-sizing:border-box;width:100%}.ant-col-md-push-24{left:100%}.ant-col-md-pull-24{right:100%}.ant-col-md-offset-24{margin-left:100%}.ant-col-md-order-24{order:24}.ant-col-md-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-md-push-23{left:95.83333333%}.ant-col-md-pull-23{right:95.83333333%}.ant-col-md-offset-23{margin-left:95.83333333%}.ant-col-md-order-23{order:23}.ant-col-md-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-md-push-22{left:91.66666667%}.ant-col-md-pull-22{right:91.66666667%}.ant-col-md-offset-22{margin-left:91.66666667%}.ant-col-md-order-22{order:22}.ant-col-md-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-md-push-21{left:87.5%}.ant-col-md-pull-21{right:87.5%}.ant-col-md-offset-21{margin-left:87.5%}.ant-col-md-order-21{order:21}.ant-col-md-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-md-push-20{left:83.33333333%}.ant-col-md-pull-20{right:83.33333333%}.ant-col-md-offset-20{margin-left:83.33333333%}.ant-col-md-order-20{order:20}.ant-col-md-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-md-push-19{left:79.16666667%}.ant-col-md-pull-19{right:79.16666667%}.ant-col-md-offset-19{margin-left:79.16666667%}.ant-col-md-order-19{order:19}.ant-col-md-18{display:block;box-sizing:border-box;width:75%}.ant-col-md-push-18{left:75%}.ant-col-md-pull-18{right:75%}.ant-col-md-offset-18{margin-left:75%}.ant-col-md-order-18{order:18}.ant-col-md-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-md-push-17{left:70.83333333%}.ant-col-md-pull-17{right:70.83333333%}.ant-col-md-offset-17{margin-left:70.83333333%}.ant-col-md-order-17{order:17}.ant-col-md-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-md-push-16{left:66.66666667%}.ant-col-md-pull-16{right:66.66666667%}.ant-col-md-offset-16{margin-left:66.66666667%}.ant-col-md-order-16{order:16}.ant-col-md-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-md-push-15{left:62.5%}.ant-col-md-pull-15{right:62.5%}.ant-col-md-offset-15{margin-left:62.5%}.ant-col-md-order-15{order:15}.ant-col-md-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-md-push-14{left:58.33333333%}.ant-col-md-pull-14{right:58.33333333%}.ant-col-md-offset-14{margin-left:58.33333333%}.ant-col-md-order-14{order:14}.ant-col-md-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-md-push-13{left:54.16666667%}.ant-col-md-pull-13{right:54.16666667%}.ant-col-md-offset-13{margin-left:54.16666667%}.ant-col-md-order-13{order:13}.ant-col-md-12{display:block;box-sizing:border-box;width:50%}.ant-col-md-push-12{left:50%}.ant-col-md-pull-12{right:50%}.ant-col-md-offset-12{margin-left:50%}.ant-col-md-order-12{order:12}.ant-col-md-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-md-push-11{left:45.83333333%}.ant-col-md-pull-11{right:45.83333333%}.ant-col-md-offset-11{margin-left:45.83333333%}.ant-col-md-order-11{order:11}.ant-col-md-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-md-push-10{left:41.66666667%}.ant-col-md-pull-10{right:41.66666667%}.ant-col-md-offset-10{margin-left:41.66666667%}.ant-col-md-order-10{order:10}.ant-col-md-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-md-push-9{left:37.5%}.ant-col-md-pull-9{right:37.5%}.ant-col-md-offset-9{margin-left:37.5%}.ant-col-md-order-9{order:9}.ant-col-md-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-md-push-8{left:33.33333333%}.ant-col-md-pull-8{right:33.33333333%}.ant-col-md-offset-8{margin-left:33.33333333%}.ant-col-md-order-8{order:8}.ant-col-md-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-md-push-7{left:29.16666667%}.ant-col-md-pull-7{right:29.16666667%}.ant-col-md-offset-7{margin-left:29.16666667%}.ant-col-md-order-7{order:7}.ant-col-md-6{display:block;box-sizing:border-box;width:25%}.ant-col-md-push-6{left:25%}.ant-col-md-pull-6{right:25%}.ant-col-md-offset-6{margin-left:25%}.ant-col-md-order-6{order:6}.ant-col-md-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-md-push-5{left:20.83333333%}.ant-col-md-pull-5{right:20.83333333%}.ant-col-md-offset-5{margin-left:20.83333333%}.ant-col-md-order-5{order:5}.ant-col-md-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-md-push-4{left:16.66666667%}.ant-col-md-pull-4{right:16.66666667%}.ant-col-md-offset-4{margin-left:16.66666667%}.ant-col-md-order-4{order:4}.ant-col-md-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-md-push-3{left:12.5%}.ant-col-md-pull-3{right:12.5%}.ant-col-md-offset-3{margin-left:12.5%}.ant-col-md-order-3{order:3}.ant-col-md-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-md-push-2{left:8.33333333%}.ant-col-md-pull-2{right:8.33333333%}.ant-col-md-offset-2{margin-left:8.33333333%}.ant-col-md-order-2{order:2}.ant-col-md-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-md-push-1{left:4.16666667%}.ant-col-md-pull-1{right:4.16666667%}.ant-col-md-offset-1{margin-left:4.16666667%}.ant-col-md-order-1{order:1}.ant-col-md-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-md-push-0{left:auto}.ant-col-md-pull-0{right:auto}.ant-col-md-offset-0{margin-left:0}.ant-col-md-order-0{order:0}}@media (min-width:992px){.ant-col-lg-1,.ant-col-lg-2,.ant-col-lg-3,.ant-col-lg-4,.ant-col-lg-5,.ant-col-lg-6,.ant-col-lg-7,.ant-col-lg-8,.ant-col-lg-9,.ant-col-lg-10,.ant-col-lg-11,.ant-col-lg-12,.ant-col-lg-13,.ant-col-lg-14,.ant-col-lg-15,.ant-col-lg-16,.ant-col-lg-17,.ant-col-lg-18,.ant-col-lg-19,.ant-col-lg-20,.ant-col-lg-21,.ant-col-lg-22,.ant-col-lg-23,.ant-col-lg-24{flex:0 0 auto;float:left}.ant-col-lg-24{display:block;box-sizing:border-box;width:100%}.ant-col-lg-push-24{left:100%}.ant-col-lg-pull-24{right:100%}.ant-col-lg-offset-24{margin-left:100%}.ant-col-lg-order-24{order:24}.ant-col-lg-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-lg-push-23{left:95.83333333%}.ant-col-lg-pull-23{right:95.83333333%}.ant-col-lg-offset-23{margin-left:95.83333333%}.ant-col-lg-order-23{order:23}.ant-col-lg-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-lg-push-22{left:91.66666667%}.ant-col-lg-pull-22{right:91.66666667%}.ant-col-lg-offset-22{margin-left:91.66666667%}.ant-col-lg-order-22{order:22}.ant-col-lg-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-lg-push-21{left:87.5%}.ant-col-lg-pull-21{right:87.5%}.ant-col-lg-offset-21{margin-left:87.5%}.ant-col-lg-order-21{order:21}.ant-col-lg-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-lg-push-20{left:83.33333333%}.ant-col-lg-pull-20{right:83.33333333%}.ant-col-lg-offset-20{margin-left:83.33333333%}.ant-col-lg-order-20{order:20}.ant-col-lg-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-lg-push-19{left:79.16666667%}.ant-col-lg-pull-19{right:79.16666667%}.ant-col-lg-offset-19{margin-left:79.16666667%}.ant-col-lg-order-19{order:19}.ant-col-lg-18{display:block;box-sizing:border-box;width:75%}.ant-col-lg-push-18{left:75%}.ant-col-lg-pull-18{right:75%}.ant-col-lg-offset-18{margin-left:75%}.ant-col-lg-order-18{order:18}.ant-col-lg-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-lg-push-17{left:70.83333333%}.ant-col-lg-pull-17{right:70.83333333%}.ant-col-lg-offset-17{margin-left:70.83333333%}.ant-col-lg-order-17{order:17}.ant-col-lg-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-lg-push-16{left:66.66666667%}.ant-col-lg-pull-16{right:66.66666667%}.ant-col-lg-offset-16{margin-left:66.66666667%}.ant-col-lg-order-16{order:16}.ant-col-lg-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-lg-push-15{left:62.5%}.ant-col-lg-pull-15{right:62.5%}.ant-col-lg-offset-15{margin-left:62.5%}.ant-col-lg-order-15{order:15}.ant-col-lg-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-lg-push-14{left:58.33333333%}.ant-col-lg-pull-14{right:58.33333333%}.ant-col-lg-offset-14{margin-left:58.33333333%}.ant-col-lg-order-14{order:14}.ant-col-lg-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-lg-push-13{left:54.16666667%}.ant-col-lg-pull-13{right:54.16666667%}.ant-col-lg-offset-13{margin-left:54.16666667%}.ant-col-lg-order-13{order:13}.ant-col-lg-12{display:block;box-sizing:border-box;width:50%}.ant-col-lg-push-12{left:50%}.ant-col-lg-pull-12{right:50%}.ant-col-lg-offset-12{margin-left:50%}.ant-col-lg-order-12{order:12}.ant-col-lg-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-lg-push-11{left:45.83333333%}.ant-col-lg-pull-11{right:45.83333333%}.ant-col-lg-offset-11{margin-left:45.83333333%}.ant-col-lg-order-11{order:11}.ant-col-lg-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-lg-push-10{left:41.66666667%}.ant-col-lg-pull-10{right:41.66666667%}.ant-col-lg-offset-10{margin-left:41.66666667%}.ant-col-lg-order-10{order:10}.ant-col-lg-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-lg-push-9{left:37.5%}.ant-col-lg-pull-9{right:37.5%}.ant-col-lg-offset-9{margin-left:37.5%}.ant-col-lg-order-9{order:9}.ant-col-lg-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-lg-push-8{left:33.33333333%}.ant-col-lg-pull-8{right:33.33333333%}.ant-col-lg-offset-8{margin-left:33.33333333%}.ant-col-lg-order-8{order:8}.ant-col-lg-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-lg-push-7{left:29.16666667%}.ant-col-lg-pull-7{right:29.16666667%}.ant-col-lg-offset-7{margin-left:29.16666667%}.ant-col-lg-order-7{order:7}.ant-col-lg-6{display:block;box-sizing:border-box;width:25%}.ant-col-lg-push-6{left:25%}.ant-col-lg-pull-6{right:25%}.ant-col-lg-offset-6{margin-left:25%}.ant-col-lg-order-6{order:6}.ant-col-lg-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-lg-push-5{left:20.83333333%}.ant-col-lg-pull-5{right:20.83333333%}.ant-col-lg-offset-5{margin-left:20.83333333%}.ant-col-lg-order-5{order:5}.ant-col-lg-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-lg-push-4{left:16.66666667%}.ant-col-lg-pull-4{right:16.66666667%}.ant-col-lg-offset-4{margin-left:16.66666667%}.ant-col-lg-order-4{order:4}.ant-col-lg-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-lg-push-3{left:12.5%}.ant-col-lg-pull-3{right:12.5%}.ant-col-lg-offset-3{margin-left:12.5%}.ant-col-lg-order-3{order:3}.ant-col-lg-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-lg-push-2{left:8.33333333%}.ant-col-lg-pull-2{right:8.33333333%}.ant-col-lg-offset-2{margin-left:8.33333333%}.ant-col-lg-order-2{order:2}.ant-col-lg-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-lg-push-1{left:4.16666667%}.ant-col-lg-pull-1{right:4.16666667%}.ant-col-lg-offset-1{margin-left:4.16666667%}.ant-col-lg-order-1{order:1}.ant-col-lg-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-lg-push-0{left:auto}.ant-col-lg-pull-0{right:auto}.ant-col-lg-offset-0{margin-left:0}.ant-col-lg-order-0{order:0}}@media (min-width:1200px){.ant-col-xl-1,.ant-col-xl-2,.ant-col-xl-3,.ant-col-xl-4,.ant-col-xl-5,.ant-col-xl-6,.ant-col-xl-7,.ant-col-xl-8,.ant-col-xl-9,.ant-col-xl-10,.ant-col-xl-11,.ant-col-xl-12,.ant-col-xl-13,.ant-col-xl-14,.ant-col-xl-15,.ant-col-xl-16,.ant-col-xl-17,.ant-col-xl-18,.ant-col-xl-19,.ant-col-xl-20,.ant-col-xl-21,.ant-col-xl-22,.ant-col-xl-23,.ant-col-xl-24{flex:0 0 auto;float:left}.ant-col-xl-24{display:block;box-sizing:border-box;width:100%}.ant-col-xl-push-24{left:100%}.ant-col-xl-pull-24{right:100%}.ant-col-xl-offset-24{margin-left:100%}.ant-col-xl-order-24{order:24}.ant-col-xl-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-xl-push-23{left:95.83333333%}.ant-col-xl-pull-23{right:95.83333333%}.ant-col-xl-offset-23{margin-left:95.83333333%}.ant-col-xl-order-23{order:23}.ant-col-xl-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-xl-push-22{left:91.66666667%}.ant-col-xl-pull-22{right:91.66666667%}.ant-col-xl-offset-22{margin-left:91.66666667%}.ant-col-xl-order-22{order:22}.ant-col-xl-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-xl-push-21{left:87.5%}.ant-col-xl-pull-21{right:87.5%}.ant-col-xl-offset-21{margin-left:87.5%}.ant-col-xl-order-21{order:21}.ant-col-xl-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-xl-push-20{left:83.33333333%}.ant-col-xl-pull-20{right:83.33333333%}.ant-col-xl-offset-20{margin-left:83.33333333%}.ant-col-xl-order-20{order:20}.ant-col-xl-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-xl-push-19{left:79.16666667%}.ant-col-xl-pull-19{right:79.16666667%}.ant-col-xl-offset-19{margin-left:79.16666667%}.ant-col-xl-order-19{order:19}.ant-col-xl-18{display:block;box-sizing:border-box;width:75%}.ant-col-xl-push-18{left:75%}.ant-col-xl-pull-18{right:75%}.ant-col-xl-offset-18{margin-left:75%}.ant-col-xl-order-18{order:18}.ant-col-xl-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-xl-push-17{left:70.83333333%}.ant-col-xl-pull-17{right:70.83333333%}.ant-col-xl-offset-17{margin-left:70.83333333%}.ant-col-xl-order-17{order:17}.ant-col-xl-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-xl-push-16{left:66.66666667%}.ant-col-xl-pull-16{right:66.66666667%}.ant-col-xl-offset-16{margin-left:66.66666667%}.ant-col-xl-order-16{order:16}.ant-col-xl-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-xl-push-15{left:62.5%}.ant-col-xl-pull-15{right:62.5%}.ant-col-xl-offset-15{margin-left:62.5%}.ant-col-xl-order-15{order:15}.ant-col-xl-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-xl-push-14{left:58.33333333%}.ant-col-xl-pull-14{right:58.33333333%}.ant-col-xl-offset-14{margin-left:58.33333333%}.ant-col-xl-order-14{order:14}.ant-col-xl-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-xl-push-13{left:54.16666667%}.ant-col-xl-pull-13{right:54.16666667%}.ant-col-xl-offset-13{margin-left:54.16666667%}.ant-col-xl-order-13{order:13}.ant-col-xl-12{display:block;box-sizing:border-box;width:50%}.ant-col-xl-push-12{left:50%}.ant-col-xl-pull-12{right:50%}.ant-col-xl-offset-12{margin-left:50%}.ant-col-xl-order-12{order:12}.ant-col-xl-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-xl-push-11{left:45.83333333%}.ant-col-xl-pull-11{right:45.83333333%}.ant-col-xl-offset-11{margin-left:45.83333333%}.ant-col-xl-order-11{order:11}.ant-col-xl-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-xl-push-10{left:41.66666667%}.ant-col-xl-pull-10{right:41.66666667%}.ant-col-xl-offset-10{margin-left:41.66666667%}.ant-col-xl-order-10{order:10}.ant-col-xl-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-xl-push-9{left:37.5%}.ant-col-xl-pull-9{right:37.5%}.ant-col-xl-offset-9{margin-left:37.5%}.ant-col-xl-order-9{order:9}.ant-col-xl-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-xl-push-8{left:33.33333333%}.ant-col-xl-pull-8{right:33.33333333%}.ant-col-xl-offset-8{margin-left:33.33333333%}.ant-col-xl-order-8{order:8}.ant-col-xl-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-xl-push-7{left:29.16666667%}.ant-col-xl-pull-7{right:29.16666667%}.ant-col-xl-offset-7{margin-left:29.16666667%}.ant-col-xl-order-7{order:7}.ant-col-xl-6{display:block;box-sizing:border-box;width:25%}.ant-col-xl-push-6{left:25%}.ant-col-xl-pull-6{right:25%}.ant-col-xl-offset-6{margin-left:25%}.ant-col-xl-order-6{order:6}.ant-col-xl-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-xl-push-5{left:20.83333333%}.ant-col-xl-pull-5{right:20.83333333%}.ant-col-xl-offset-5{margin-left:20.83333333%}.ant-col-xl-order-5{order:5}.ant-col-xl-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-xl-push-4{left:16.66666667%}.ant-col-xl-pull-4{right:16.66666667%}.ant-col-xl-offset-4{margin-left:16.66666667%}.ant-col-xl-order-4{order:4}.ant-col-xl-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-xl-push-3{left:12.5%}.ant-col-xl-pull-3{right:12.5%}.ant-col-xl-offset-3{margin-left:12.5%}.ant-col-xl-order-3{order:3}.ant-col-xl-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-xl-push-2{left:8.33333333%}.ant-col-xl-pull-2{right:8.33333333%}.ant-col-xl-offset-2{margin-left:8.33333333%}.ant-col-xl-order-2{order:2}.ant-col-xl-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-xl-push-1{left:4.16666667%}.ant-col-xl-pull-1{right:4.16666667%}.ant-col-xl-offset-1{margin-left:4.16666667%}.ant-col-xl-order-1{order:1}.ant-col-xl-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-xl-push-0{left:auto}.ant-col-xl-pull-0{right:auto}.ant-col-xl-offset-0{margin-left:0}.ant-col-xl-order-0{order:0}}@media (min-width:1600px){.ant-col-xxl-1,.ant-col-xxl-2,.ant-col-xxl-3,.ant-col-xxl-4,.ant-col-xxl-5,.ant-col-xxl-6,.ant-col-xxl-7,.ant-col-xxl-8,.ant-col-xxl-9,.ant-col-xxl-10,.ant-col-xxl-11,.ant-col-xxl-12,.ant-col-xxl-13,.ant-col-xxl-14,.ant-col-xxl-15,.ant-col-xxl-16,.ant-col-xxl-17,.ant-col-xxl-18,.ant-col-xxl-19,.ant-col-xxl-20,.ant-col-xxl-21,.ant-col-xxl-22,.ant-col-xxl-23,.ant-col-xxl-24{flex:0 0 auto;float:left}.ant-col-xxl-24{display:block;box-sizing:border-box;width:100%}.ant-col-xxl-push-24{left:100%}.ant-col-xxl-pull-24{right:100%}.ant-col-xxl-offset-24{margin-left:100%}.ant-col-xxl-order-24{order:24}.ant-col-xxl-23{display:block;box-sizing:border-box;width:95.83333333%}.ant-col-xxl-push-23{left:95.83333333%}.ant-col-xxl-pull-23{right:95.83333333%}.ant-col-xxl-offset-23{margin-left:95.83333333%}.ant-col-xxl-order-23{order:23}.ant-col-xxl-22{display:block;box-sizing:border-box;width:91.66666667%}.ant-col-xxl-push-22{left:91.66666667%}.ant-col-xxl-pull-22{right:91.66666667%}.ant-col-xxl-offset-22{margin-left:91.66666667%}.ant-col-xxl-order-22{order:22}.ant-col-xxl-21{display:block;box-sizing:border-box;width:87.5%}.ant-col-xxl-push-21{left:87.5%}.ant-col-xxl-pull-21{right:87.5%}.ant-col-xxl-offset-21{margin-left:87.5%}.ant-col-xxl-order-21{order:21}.ant-col-xxl-20{display:block;box-sizing:border-box;width:83.33333333%}.ant-col-xxl-push-20{left:83.33333333%}.ant-col-xxl-pull-20{right:83.33333333%}.ant-col-xxl-offset-20{margin-left:83.33333333%}.ant-col-xxl-order-20{order:20}.ant-col-xxl-19{display:block;box-sizing:border-box;width:79.16666667%}.ant-col-xxl-push-19{left:79.16666667%}.ant-col-xxl-pull-19{right:79.16666667%}.ant-col-xxl-offset-19{margin-left:79.16666667%}.ant-col-xxl-order-19{order:19}.ant-col-xxl-18{display:block;box-sizing:border-box;width:75%}.ant-col-xxl-push-18{left:75%}.ant-col-xxl-pull-18{right:75%}.ant-col-xxl-offset-18{margin-left:75%}.ant-col-xxl-order-18{order:18}.ant-col-xxl-17{display:block;box-sizing:border-box;width:70.83333333%}.ant-col-xxl-push-17{left:70.83333333%}.ant-col-xxl-pull-17{right:70.83333333%}.ant-col-xxl-offset-17{margin-left:70.83333333%}.ant-col-xxl-order-17{order:17}.ant-col-xxl-16{display:block;box-sizing:border-box;width:66.66666667%}.ant-col-xxl-push-16{left:66.66666667%}.ant-col-xxl-pull-16{right:66.66666667%}.ant-col-xxl-offset-16{margin-left:66.66666667%}.ant-col-xxl-order-16{order:16}.ant-col-xxl-15{display:block;box-sizing:border-box;width:62.5%}.ant-col-xxl-push-15{left:62.5%}.ant-col-xxl-pull-15{right:62.5%}.ant-col-xxl-offset-15{margin-left:62.5%}.ant-col-xxl-order-15{order:15}.ant-col-xxl-14{display:block;box-sizing:border-box;width:58.33333333%}.ant-col-xxl-push-14{left:58.33333333%}.ant-col-xxl-pull-14{right:58.33333333%}.ant-col-xxl-offset-14{margin-left:58.33333333%}.ant-col-xxl-order-14{order:14}.ant-col-xxl-13{display:block;box-sizing:border-box;width:54.16666667%}.ant-col-xxl-push-13{left:54.16666667%}.ant-col-xxl-pull-13{right:54.16666667%}.ant-col-xxl-offset-13{margin-left:54.16666667%}.ant-col-xxl-order-13{order:13}.ant-col-xxl-12{display:block;box-sizing:border-box;width:50%}.ant-col-xxl-push-12{left:50%}.ant-col-xxl-pull-12{right:50%}.ant-col-xxl-offset-12{margin-left:50%}.ant-col-xxl-order-12{order:12}.ant-col-xxl-11{display:block;box-sizing:border-box;width:45.83333333%}.ant-col-xxl-push-11{left:45.83333333%}.ant-col-xxl-pull-11{right:45.83333333%}.ant-col-xxl-offset-11{margin-left:45.83333333%}.ant-col-xxl-order-11{order:11}.ant-col-xxl-10{display:block;box-sizing:border-box;width:41.66666667%}.ant-col-xxl-push-10{left:41.66666667%}.ant-col-xxl-pull-10{right:41.66666667%}.ant-col-xxl-offset-10{margin-left:41.66666667%}.ant-col-xxl-order-10{order:10}.ant-col-xxl-9{display:block;box-sizing:border-box;width:37.5%}.ant-col-xxl-push-9{left:37.5%}.ant-col-xxl-pull-9{right:37.5%}.ant-col-xxl-offset-9{margin-left:37.5%}.ant-col-xxl-order-9{order:9}.ant-col-xxl-8{display:block;box-sizing:border-box;width:33.33333333%}.ant-col-xxl-push-8{left:33.33333333%}.ant-col-xxl-pull-8{right:33.33333333%}.ant-col-xxl-offset-8{margin-left:33.33333333%}.ant-col-xxl-order-8{order:8}.ant-col-xxl-7{display:block;box-sizing:border-box;width:29.16666667%}.ant-col-xxl-push-7{left:29.16666667%}.ant-col-xxl-pull-7{right:29.16666667%}.ant-col-xxl-offset-7{margin-left:29.16666667%}.ant-col-xxl-order-7{order:7}.ant-col-xxl-6{display:block;box-sizing:border-box;width:25%}.ant-col-xxl-push-6{left:25%}.ant-col-xxl-pull-6{right:25%}.ant-col-xxl-offset-6{margin-left:25%}.ant-col-xxl-order-6{order:6}.ant-col-xxl-5{display:block;box-sizing:border-box;width:20.83333333%}.ant-col-xxl-push-5{left:20.83333333%}.ant-col-xxl-pull-5{right:20.83333333%}.ant-col-xxl-offset-5{margin-left:20.83333333%}.ant-col-xxl-order-5{order:5}.ant-col-xxl-4{display:block;box-sizing:border-box;width:16.66666667%}.ant-col-xxl-push-4{left:16.66666667%}.ant-col-xxl-pull-4{right:16.66666667%}.ant-col-xxl-offset-4{margin-left:16.66666667%}.ant-col-xxl-order-4{order:4}.ant-col-xxl-3{display:block;box-sizing:border-box;width:12.5%}.ant-col-xxl-push-3{left:12.5%}.ant-col-xxl-pull-3{right:12.5%}.ant-col-xxl-offset-3{margin-left:12.5%}.ant-col-xxl-order-3{order:3}.ant-col-xxl-2{display:block;box-sizing:border-box;width:8.33333333%}.ant-col-xxl-push-2{left:8.33333333%}.ant-col-xxl-pull-2{right:8.33333333%}.ant-col-xxl-offset-2{margin-left:8.33333333%}.ant-col-xxl-order-2{order:2}.ant-col-xxl-1{display:block;box-sizing:border-box;width:4.16666667%}.ant-col-xxl-push-1{left:4.16666667%}.ant-col-xxl-pull-1{right:4.16666667%}.ant-col-xxl-offset-1{margin-left:4.16666667%}.ant-col-xxl-order-1{order:1}.ant-col-xxl-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-xxl-push-0{left:auto}.ant-col-xxl-pull-0{right:auto}.ant-col-xxl-offset-0{margin-left:0}.ant-col-xxl-order-0{order:0}}.ant-carousel{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"}.ant-carousel .slick-slider{position:relative;display:block;box-sizing:border-box;-webkit-touch-callout:none;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.ant-carousel .slick-list{position:relative;display:block;margin:0;padding:0;overflow:hidden}.ant-carousel .slick-list:focus{outline:none}.ant-carousel .slick-list.dragging{cursor:pointer}.ant-carousel .slick-list .slick-slide{pointer-events:none}.ant-carousel .slick-list .slick-slide input.ant-checkbox-input,.ant-carousel .slick-list .slick-slide input.ant-radio-input{visibility:hidden}.ant-carousel .slick-list .slick-slide.slick-active{pointer-events:auto}.ant-carousel .slick-list .slick-slide.slick-active input.ant-checkbox-input,.ant-carousel .slick-list .slick-slide.slick-active input.ant-radio-input{visibility:visible}.ant-carousel .slick-slider .slick-list,.ant-carousel .slick-slider .slick-track{transform:translateZ(0)}.ant-carousel .slick-track{position:relative;top:0;left:0;display:block}.ant-carousel .slick-track:after,.ant-carousel .slick-track:before{display:table;content:""}.ant-carousel .slick-track:after{clear:both}.slick-loading .ant-carousel .slick-track{visibility:hidden}.ant-carousel .slick-slide{display:none;float:left;height:100%;min-height:1px}[dir=rtl] .ant-carousel .slick-slide{float:right}.ant-carousel .slick-slide img{display:block}.ant-carousel .slick-slide.slick-loading img{display:none}.ant-carousel .slick-slide.dragging img{pointer-events:none}.ant-carousel .slick-initialized .slick-slide{display:block}.ant-carousel .slick-loading .slick-slide{visibility:hidden}.ant-carousel .slick-vertical .slick-slide{display:block;height:auto;border:1px solid transparent}.ant-carousel .slick-arrow.slick-hidden{display:none}.ant-carousel .slick-next,.ant-carousel .slick-prev{position:absolute;top:50%;display:block;width:20px;height:20px;margin-top:-10px;padding:0;font-size:0;line-height:0;border:0;cursor:pointer}.ant-carousel .slick-next,.ant-carousel .slick-next:focus,.ant-carousel .slick-next:hover,.ant-carousel .slick-prev,.ant-carousel .slick-prev:focus,.ant-carousel .slick-prev:hover{color:transparent;background:transparent;outline:none}.ant-carousel .slick-next:focus:before,.ant-carousel .slick-next:hover:before,.ant-carousel .slick-prev:focus:before,.ant-carousel .slick-prev:hover:before{opacity:1}.ant-carousel .slick-next.slick-disabled:before,.ant-carousel .slick-prev.slick-disabled:before{opacity:.25}.ant-carousel .slick-prev{left:-25px}.ant-carousel .slick-prev:before{content:"←"}.ant-carousel .slick-next{right:-25px}.ant-carousel .slick-next:before{content:"→"}.ant-carousel .slick-dots{position:absolute;display:block;width:100%;height:3px;margin:0;padding:0;text-align:center;list-style:none}.ant-carousel .slick-dots-bottom{bottom:12px}.ant-carousel .slick-dots-top{top:12px}.ant-carousel .slick-dots li{position:relative;display:inline-block;margin:0 2px;padding:0;text-align:center;vertical-align:top}.ant-carousel .slick-dots li button{display:block;width:16px;height:3px;padding:0;color:transparent;font-size:0;background:#fff;border:0;border-radius:1px;outline:none;cursor:pointer;opacity:.3;transition:all .5s}.ant-carousel .slick-dots li button:focus,.ant-carousel .slick-dots li button:hover{opacity:.75}.ant-carousel .slick-dots li.slick-active button{width:24px;background:#fff;opacity:1}.ant-carousel .slick-dots li.slick-active button:focus,.ant-carousel .slick-dots li.slick-active button:hover{opacity:1}.ant-carousel-vertical .slick-dots{top:50%;bottom:auto;width:3px;height:auto;transform:translateY(-50%)}.ant-carousel-vertical .slick-dots-left{left:12px}.ant-carousel-vertical .slick-dots-right{right:12px}.ant-carousel-vertical .slick-dots li{margin:0 2px;vertical-align:baseline}.ant-carousel-vertical .slick-dots li button{width:3px;height:16px}.ant-carousel-vertical .slick-dots li.slick-active button{width:3px;height:24px}.ant-cascader{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"}.ant-cascader-input.ant-input{position:static;width:100%;padding-right:24px;background-color:transparent!important;cursor:pointer}.ant-cascader-picker-show-search .ant-cascader-input.ant-input{position:relative}.ant-cascader-picker{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;background-color:#fff;border-radius:1rem;outline:0;cursor:pointer;transition:color .3s}.ant-cascader-picker-with-value .ant-cascader-picker-label{color:transparent}.ant-cascader-picker-disabled{color:rgba(0,0,0,.25);background:#f5f5f5;cursor:not-allowed}.ant-cascader-picker-disabled .ant-cascader-input{cursor:not-allowed}.ant-cascader-picker:focus .ant-cascader-input{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-cascader-picker-show-search.ant-cascader-picker-focused{color:rgba(0,0,0,.25)}.ant-cascader-picker-label{position:absolute;top:50%;left:0;width:100%;height:20px;margin-top:-10px;padding:0 20px 0 12px;overflow:hidden;line-height:20px;white-space:nowrap;text-overflow:ellipsis}.ant-cascader-picker-clear{position:absolute;top:50%;right:12px;z-index:2;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;line-height:12px;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease}.ant-cascader-picker-clear:hover{color:rgba(0,0,0,.45)}.ant-cascader-picker:hover .ant-cascader-picker-clear{opacity:1}.ant-cascader-picker-arrow{position:absolute;top:50%;right:12px;z-index:1;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;line-height:12px;transition:transform .2s}.ant-cascader-picker-arrow.ant-cascader-picker-arrow-expand{transform:rotate(180deg)}.ant-cascader-picker-label:hover+.ant-cascader-input{border-color:#18947b;border-right-width:1px!important}.ant-cascader-picker-small .ant-cascader-picker-arrow,.ant-cascader-picker-small .ant-cascader-picker-clear{right:8px}.ant-cascader-menus{position:absolute;z-index:1050;font-size:14px;white-space:nowrap;background:#fff;border-radius:1rem;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-cascader-menus ol,.ant-cascader-menus ul{margin:0;list-style:none}.ant-cascader-menus-empty,.ant-cascader-menus-hidden{display:none}.ant-cascader-menus.slide-up-appear.slide-up-appear-active.ant-cascader-menus-placement-bottomLeft,.ant-cascader-menus.slide-up-enter.slide-up-enter-active.ant-cascader-menus-placement-bottomLeft{animation-name:antSlideUpIn}.ant-cascader-menus.slide-up-appear.slide-up-appear-active.ant-cascader-menus-placement-topLeft,.ant-cascader-menus.slide-up-enter.slide-up-enter-active.ant-cascader-menus-placement-topLeft{animation-name:antSlideDownIn}.ant-cascader-menus.slide-up-leave.slide-up-leave-active.ant-cascader-menus-placement-bottomLeft{animation-name:antSlideUpOut}.ant-cascader-menus.slide-up-leave.slide-up-leave-active.ant-cascader-menus-placement-topLeft{animation-name:antSlideDownOut}.ant-cascader-menu{display:inline-block;min-width:111px;height:180px;margin:0;padding:4px 0;overflow:auto;vertical-align:top;list-style:none;border-right:1px solid #e8e8e8;-ms-overflow-style:-ms-autohiding-scrollbar}.ant-cascader-menu:first-child{border-radius:1rem 0 0 1rem}.ant-cascader-menu:last-child{margin-right:-1px;border-right-color:transparent;border-radius:0 1rem 1rem 0}.ant-cascader-menu:only-child{border-radius:1rem}.ant-cascader-menu-item{padding:5px 12px;line-height:22px;white-space:nowrap;cursor:pointer;transition:all .3s}.ant-cascader-menu-item:hover{background:#b3c7c0}.ant-cascader-menu-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-cascader-menu-item-disabled:hover{background:transparent}.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled),.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled):hover{font-weight:600;background-color:#fafafa}.ant-cascader-menu-item-expand{position:relative;padding-right:24px}.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-loading-icon{display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);position:absolute;right:12px;color:rgba(0,0,0,.45)}:root .ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,:root .ant-cascader-menu-item-loading-icon{font-size:12px}.ant-cascader-menu-item-disabled.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-disabled.ant-cascader-menu-item-loading-icon{color:rgba(0,0,0,.25)}.ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d}.ant-checkbox{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:none;cursor:pointer}.ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:#008771}.ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #008771;border-radius:2px;visibility:hidden;animation:antCheckboxEffect .36s ease-in-out;animation-fill-mode:backwards;content:""}.ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible}.ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s}.ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}.ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}.ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}.ant-checkbox-checked .ant-checkbox-inner{background-color:#008771;border-color:#008771}.ant-checkbox-disabled{cursor:not-allowed}.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);animation-name:none}.ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed}.ant-checkbox-disabled .ant-checkbox-inner{background-color:#f5f5f5;border-color:#d9d9d9!important}.ant-checkbox-disabled .ant-checkbox-inner:after{border-color:#f5f5f5;border-collapse:separate;animation-name:none}.ant-checkbox-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-checkbox-disabled:hover:after,.ant-checkbox-wrapper:hover .ant-checkbox-disabled:after{visibility:hidden}.ant-checkbox-wrapper{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block;line-height:unset;cursor:pointer}.ant-checkbox-wrapper.ant-checkbox-wrapper-disabled{cursor:not-allowed}.ant-checkbox-wrapper+.ant-checkbox-wrapper{margin-left:8px}.ant-checkbox+span{padding-right:8px;padding-left:8px}.ant-checkbox-group{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}.ant-checkbox-group-item{display:inline-block;margin-right:8px}.ant-checkbox-group-item:last-child{margin-right:0}.ant-checkbox-group-item+.ant-checkbox-group-item{margin-left:0}.ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9}.ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#008771;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}.ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-collapse{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background-color:#fafafa;border:1px solid #d9d9d9;border-bottom:0;border-radius:1rem}.ant-collapse>.ant-collapse-item{border-bottom:1px solid #d9d9d9}.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0 0 1rem 1rem}.ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;transition:all .3s}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;transform:translateY(-50%)}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow svg{display:inline-block}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow:before{display:none}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow .ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow-icon{display:block}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow svg{transition:transform .24s}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-extra{float:right}.ant-collapse>.ant-collapse-item>.ant-collapse-header:focus{outline:none}.ant-collapse>.ant-collapse-item.ant-collapse-no-arrow>.ant-collapse-header{padding-left:12px}.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px}.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto}.ant-collapse-anim-active{transition:height .2s cubic-bezier(.215,.61,.355,1)}.ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid #d9d9d9}.ant-collapse-content>.ant-collapse-content-box{padding:16px}.ant-collapse-content-inactive{display:none}.ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 1rem 1rem}.ant-collapse-borderless{background-color:#fafafa;border:0}.ant-collapse-borderless>.ant-collapse-item{border-bottom:1px solid #d9d9d9}.ant-collapse-borderless>.ant-collapse-item:last-child,.ant-collapse-borderless>.ant-collapse-item:last-child .ant-collapse-header{border-radius:0}.ant-collapse-borderless>.ant-collapse-item>.ant-collapse-content{background-color:transparent;border-top:0}.ant-collapse-borderless>.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box{padding-top:4px}.ant-collapse .ant-collapse-item-disabled>.ant-collapse-header,.ant-collapse .ant-collapse-item-disabled>.ant-collapse-header>.arrow{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-comment{position:relative}.ant-comment-inner{display:flex;padding:16px 0}.ant-comment-avatar{position:relative;flex-shrink:0;margin-right:12px;cursor:pointer}.ant-comment-avatar img{width:32px;height:32px;border-radius:50%}.ant-comment-content{position:relative;flex:1 1 auto;min-width:1px;font-size:14px;word-wrap:break-word}.ant-comment-content-author{display:flex;flex-wrap:wrap;justify-content:flex-start;margin-bottom:4px;font-size:14px}.ant-comment-content-author>a,.ant-comment-content-author>span{padding-right:8px;font-size:12px;line-height:18px}.ant-comment-content-author-name{color:rgba(0,0,0,.45);font-size:14px;transition:color .3s}.ant-comment-content-author-name>*,.ant-comment-content-author-name>:hover{color:rgba(0,0,0,.45)}.ant-comment-content-author-time{color:#ccc;white-space:nowrap;cursor:auto}.ant-comment-content-detail p{white-space:pre-wrap}.ant-comment-actions{margin-top:12px;padding-left:0}.ant-comment-actions>li{display:inline-block;color:rgba(0,0,0,.45)}.ant-comment-actions>li>span{padding-right:10px;color:rgba(0,0,0,.45);font-size:12px;cursor:pointer;transition:color .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-comment-actions>li>span:hover{color:#595959}.ant-comment-nested{margin-left:44px}.ant-calendar-picker-container{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;z-index:1050;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-topLeft,.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-topRight,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-topLeft,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-topRight{animation-name:antSlideDownIn}.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-bottomLeft,.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-bottomRight,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-bottomLeft,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-bottomRight{animation-name:antSlideUpIn}.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-topLeft,.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-topRight{animation-name:antSlideDownOut}.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-bottomLeft,.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-bottomRight{animation-name:antSlideUpOut}.ant-calendar-picker{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;outline:none;cursor:text;transition:opacity .3s}.ant-calendar-picker-input{outline:none}.ant-calendar-picker-input.ant-input{line-height:1.5}.ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0}.ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#18947b}.ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none}.ant-calendar-picker-clear:hover{color:rgba(0,0,0,.45)}.ant-calendar-picker:hover .ant-calendar-picker-clear{opacity:1;pointer-events:auto}.ant-calendar-picker-icon{display:inline-block;color:rgba(0,0,0,.25);font-size:14px;line-height:1}.ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed}.ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px}.ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:1rem;outline:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid #e8e8e8}.ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto}.ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-calendar-input:-ms-input-placeholder{color:#bfbfbf}.ant-calendar-input::-webkit-input-placeholder{color:#bfbfbf}.ant-calendar-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-calendar-input:placeholder-shown{text-overflow:ellipsis}.ant-calendar-week-number{width:286px}.ant-calendar-week-number-cell{text-align:center}.ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-calendar-header a:hover{color:#18947b}.ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none}.ant-calendar-header .ant-calendar-next-century-btn,.ant-calendar-header .ant-calendar-next-decade-btn,.ant-calendar-header .ant-calendar-next-month-btn,.ant-calendar-header .ant-calendar-next-year-btn,.ant-calendar-header .ant-calendar-prev-century-btn,.ant-calendar-header .ant-calendar-prev-decade-btn,.ant-calendar-header .ant-calendar-prev-month-btn,.ant-calendar-header .ant-calendar-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,Hiragino Sans GB,Microsoft Yahei,"Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-header .ant-calendar-prev-century-btn,.ant-calendar-header .ant-calendar-prev-decade-btn,.ant-calendar-header .ant-calendar-prev-year-btn{left:7px;height:100%}.ant-calendar-header .ant-calendar-prev-century-btn:after,.ant-calendar-header .ant-calendar-prev-century-btn:before,.ant-calendar-header .ant-calendar-prev-decade-btn:after,.ant-calendar-header .ant-calendar-prev-decade-btn:before,.ant-calendar-header .ant-calendar-prev-year-btn:after,.ant-calendar-header .ant-calendar-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-header .ant-calendar-prev-century-btn:hover:after,.ant-calendar-header .ant-calendar-prev-century-btn:hover:before,.ant-calendar-header .ant-calendar-prev-decade-btn:hover:after,.ant-calendar-header .ant-calendar-prev-decade-btn:hover:before,.ant-calendar-header .ant-calendar-prev-year-btn:hover:after,.ant-calendar-header .ant-calendar-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-prev-century-btn:after,.ant-calendar-header .ant-calendar-prev-decade-btn:after,.ant-calendar-header .ant-calendar-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-header .ant-calendar-next-century-btn,.ant-calendar-header .ant-calendar-next-decade-btn,.ant-calendar-header .ant-calendar-next-year-btn{right:7px;height:100%}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-century-btn:before,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:before,.ant-calendar-header .ant-calendar-next-year-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-header .ant-calendar-next-century-btn:hover:after,.ant-calendar-header .ant-calendar-next-century-btn:hover:before,.ant-calendar-header .ant-calendar-next-decade-btn:hover:after,.ant-calendar-header .ant-calendar-next-decade-btn:hover:before,.ant-calendar-header .ant-calendar-next-year-btn:hover:after,.ant-calendar-header .ant-calendar-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:after{display:none}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-century-btn:before,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:before,.ant-calendar-header .ant-calendar-next-year-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-header .ant-calendar-next-century-btn:before,.ant-calendar-header .ant-calendar-next-decade-btn:before,.ant-calendar-header .ant-calendar-next-year-btn:before{position:relative;left:3px}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:after{display:inline-block}.ant-calendar-header .ant-calendar-prev-month-btn{left:29px;height:100%}.ant-calendar-header .ant-calendar-prev-month-btn:after,.ant-calendar-header .ant-calendar-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-header .ant-calendar-prev-month-btn:hover:after,.ant-calendar-header .ant-calendar-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-prev-month-btn:after{display:none}.ant-calendar-header .ant-calendar-next-month-btn{right:29px;height:100%}.ant-calendar-header .ant-calendar-next-month-btn:after,.ant-calendar-header .ant-calendar-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-header .ant-calendar-next-month-btn:hover:after,.ant-calendar-header .ant-calendar-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-next-month-btn:after{display:none}.ant-calendar-header .ant-calendar-next-month-btn:after,.ant-calendar-header .ant-calendar-next-month-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-body{padding:8px 12px}.ant-calendar table{width:100%;max-width:100%;background-color:transparent;border-collapse:collapse}.ant-calendar table,.ant-calendar td,.ant-calendar th{text-align:center;border:0}.ant-calendar-calendar-table{margin-bottom:0;border-spacing:0}.ant-calendar-column-header{width:33px;padding:6px 0;line-height:18px;text-align:center}.ant-calendar-column-header .ant-calendar-column-header-inner{display:block;font-weight:400}.ant-calendar-week-number-header .ant-calendar-column-header-inner{display:none}.ant-calendar-cell{height:30px;padding:3px 0}.ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:transparent;border:1px solid transparent;border-radius:2px;transition:background .3s ease}.ant-calendar-date-panel{position:relative;outline:none}.ant-calendar-date:hover{background:#b3c7c0;cursor:pointer}.ant-calendar-date:active{color:#fff;background:#18947b}.ant-calendar-today .ant-calendar-date{color:#008771;font-weight:700;border-color:#008771}.ant-calendar-selected-day .ant-calendar-date{background:#77baa6}.ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:transparent;border-color:transparent}.ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed}.ant-calendar-disabled-cell .ant-calendar-date:hover{background:#f5f5f5}.ant-calendar-disabled-cell.ant-calendar-selected-day .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;background:rgba(0,0,0,.1);border-radius:2px;content:""}.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date{position:relative;padding-right:5px;padding-left:5px}.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "}.ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px}.ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px}.ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid #e8e8e8}.ant-calendar-footer:empty{border-top:0}.ant-calendar-footer-btn{display:block;text-align:center}.ant-calendar-footer-extra{text-align:left}.ant-calendar .ant-calendar-clear-btn,.ant-calendar .ant-calendar-today-btn{display:inline-block;margin:0 0 0 8px;text-align:center}.ant-calendar .ant-calendar-clear-btn-disabled,.ant-calendar .ant-calendar-today-btn-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-calendar .ant-calendar-clear-btn:only-child,.ant-calendar .ant-calendar-today-btn:only-child{margin:0}.ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px}.ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;transition:color .3s ease}.ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)}.ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:#008771;border:1px solid #008771;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:1rem;line-height:22px}.ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1}.ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0}.ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none}.ant-calendar .ant-calendar-ok-btn:not([disabled]):active{outline:0;box-shadow:none}.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn[disabled]{cursor:not-allowed}.ant-calendar .ant-calendar-ok-btn.disabled>*,.ant-calendar .ant-calendar-ok-btn[disabled]>*{pointer-events:none}.ant-calendar .ant-calendar-ok-btn-lg{height:40px;padding:0 15px;font-size:16px;border-radius:1rem}.ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:1rem}.ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:#18947b;border-color:#18947b}.ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#006154;border-color:#006154}.ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0}.ant-calendar-range-picker-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-calendar-range-picker-input:-ms-input-placeholder{color:#bfbfbf}.ant-calendar-range-picker-input::-webkit-input-placeholder{color:#bfbfbf}.ant-calendar-range-picker-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-calendar-range-picker-input:placeholder-shown{text-overflow:ellipsis}.ant-calendar-range-picker-input[disabled]{cursor:not-allowed}.ant-calendar-range-picker-separator{display:inline-block;min-width:10px;height:100%;color:rgba(0,0,0,.45);white-space:nowrap;text-align:center;vertical-align:top;pointer-events:none}.ant-input-disabled .ant-calendar-range-picker-separator{color:rgba(0,0,0,.25)}.ant-calendar-range{width:552px;overflow:hidden}.ant-calendar-range .ant-calendar-date-panel:after{display:block;clear:both;height:0;visibility:hidden;content:"."}.ant-calendar-range-part{position:relative;width:50%}.ant-calendar-range-left{float:left}.ant-calendar-range-left .ant-calendar-time-picker-inner{border-right:1px solid #e8e8e8}.ant-calendar-range-right{float:right}.ant-calendar-range-right .ant-calendar-time-picker-inner{border-left:1px solid #e8e8e8}.ant-calendar-range-middle{position:absolute;left:50%;z-index:1;height:34px;margin:1px 0 0;padding:0 200px 0 0;color:rgba(0,0,0,.45);line-height:34px;text-align:center;transform:translateX(-50%);pointer-events:none}.ant-calendar-range-right .ant-calendar-date-input-wrap{margin-left:-90px}.ant-calendar-range.ant-calendar-time .ant-calendar-range-middle{padding:0 10px 0 0;transform:translateX(-50%)}.ant-calendar-range .ant-calendar-today :not(.ant-calendar-disabled-cell) :not(.ant-calendar-last-month-cell) :not(.ant-calendar-next-month-btn-day) .ant-calendar-date{color:#008771;background:#77baa6;border-color:#008771}.ant-calendar-range .ant-calendar-selected-end-date .ant-calendar-date,.ant-calendar-range .ant-calendar-selected-start-date .ant-calendar-date{color:#fff;background:#008771;border:1px solid transparent}.ant-calendar-range .ant-calendar-selected-end-date .ant-calendar-date:hover,.ant-calendar-range .ant-calendar-selected-start-date .ant-calendar-date:hover{background:#008771}.ant-calendar-range.ant-calendar-time .ant-calendar-range-right .ant-calendar-date-input-wrap{margin-left:0}.ant-calendar-range .ant-calendar-input-wrap{position:relative;height:34px}.ant-calendar-range .ant-calendar-input,.ant-calendar-range .ant-calendar-time-picker-input{position:relative;display:inline-block;width:100%;height:32px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;border-radius:1rem;transition:all .3s;height:24px;padding:4px 0;line-height:24px;border:0;box-shadow:none}.ant-calendar-range .ant-calendar-input::-moz-placeholder,.ant-calendar-range .ant-calendar-time-picker-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-calendar-range .ant-calendar-input:-ms-input-placeholder,.ant-calendar-range .ant-calendar-time-picker-input:-ms-input-placeholder{color:#bfbfbf}.ant-calendar-range .ant-calendar-input::-webkit-input-placeholder,.ant-calendar-range .ant-calendar-time-picker-input::-webkit-input-placeholder{color:#bfbfbf}.ant-calendar-range .ant-calendar-input:-moz-placeholder-shown,.ant-calendar-range .ant-calendar-time-picker-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-calendar-range .ant-calendar-input:placeholder-shown,.ant-calendar-range .ant-calendar-time-picker-input:placeholder-shown{text-overflow:ellipsis}.ant-calendar-range .ant-calendar-input:hover,.ant-calendar-range .ant-calendar-time-picker-input:hover{border-color:#18947b;border-right-width:1px!important}.ant-calendar-range .ant-calendar-input:focus,.ant-calendar-range .ant-calendar-time-picker-input:focus{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-calendar-range .ant-calendar-input-disabled,.ant-calendar-range .ant-calendar-time-picker-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-calendar-range .ant-calendar-input-disabled:hover,.ant-calendar-range .ant-calendar-time-picker-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-calendar-range .ant-calendar-input[disabled],.ant-calendar-range .ant-calendar-time-picker-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-calendar-range .ant-calendar-input[disabled]:hover,.ant-calendar-range .ant-calendar-time-picker-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-calendar-range .ant-calendar-input,textarea.ant-calendar-range .ant-calendar-time-picker-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}.ant-calendar-range .ant-calendar-input-lg,.ant-calendar-range .ant-calendar-time-picker-input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-calendar-range .ant-calendar-input-sm,.ant-calendar-range .ant-calendar-time-picker-input-sm{height:24px;padding:1px 7px}.ant-calendar-range .ant-calendar-input:focus,.ant-calendar-range .ant-calendar-time-picker-input:focus{box-shadow:none}.ant-calendar-range .ant-calendar-time-picker-icon{display:none}.ant-calendar-range.ant-calendar-week-number{width:574px}.ant-calendar-range.ant-calendar-week-number .ant-calendar-range-part{width:286px}.ant-calendar-range .ant-calendar-decade-panel,.ant-calendar-range .ant-calendar-month-panel,.ant-calendar-range .ant-calendar-year-panel{top:34px}.ant-calendar-range .ant-calendar-month-panel .ant-calendar-year-panel{top:0}.ant-calendar-range .ant-calendar-decade-panel-table,.ant-calendar-range .ant-calendar-month-panel-table,.ant-calendar-range .ant-calendar-year-panel-table{height:208px}.ant-calendar-range .ant-calendar-in-range-cell{position:relative;border-radius:0}.ant-calendar-range .ant-calendar-in-range-cell>div{position:relative;z-index:1}.ant-calendar-range .ant-calendar-in-range-cell:before{position:absolute;top:4px;right:0;bottom:4px;left:0;display:block;background:#b3c7c0;border:0;border-radius:0;content:""}.ant-calendar-range .ant-calendar-footer-extra{float:left}div.ant-calendar-range-quick-selector{text-align:left}div.ant-calendar-range-quick-selector>a{margin-right:8px}.ant-calendar-range .ant-calendar-decade-panel-header,.ant-calendar-range .ant-calendar-header,.ant-calendar-range .ant-calendar-month-panel-header,.ant-calendar-range .ant-calendar-year-panel-header{border-bottom:0}.ant-calendar-range .ant-calendar-body,.ant-calendar-range .ant-calendar-decade-panel-body,.ant-calendar-range .ant-calendar-month-panel-body,.ant-calendar-range .ant-calendar-year-panel-body{border-top:1px solid #e8e8e8}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker{top:68px;z-index:2;width:100%;height:207px}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-panel{height:267px;margin-top:-34px}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-inner{height:100%;padding-top:40px;background:none}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-combobox{display:inline-block;height:100%;background-color:#fff;border-top:1px solid #e8e8e8}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-select{height:100%}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-select ul{max-height:100%}.ant-calendar-range.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn{margin-right:8px}.ant-calendar-range.ant-calendar-time .ant-calendar-today-btn{height:22px;margin:8px 12px;line-height:22px}.ant-calendar-range-with-ranges.ant-calendar-time .ant-calendar-time-picker{height:233px}.ant-calendar-range.ant-calendar-show-time-picker .ant-calendar-body{border-top-color:transparent}.ant-calendar-time-picker{position:absolute;top:40px;width:100%;background-color:#fff}.ant-calendar-time-picker-panel{position:absolute;z-index:1050;width:100%}.ant-calendar-time-picker-inner{position:relative;display:inline-block;width:100%;overflow:hidden;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;outline:none}.ant-calendar-time-picker-column-1,.ant-calendar-time-picker-column-1 .ant-calendar-time-picker-select,.ant-calendar-time-picker-combobox{width:100%}.ant-calendar-time-picker-column-2 .ant-calendar-time-picker-select{width:50%}.ant-calendar-time-picker-column-3 .ant-calendar-time-picker-select{width:33.33%}.ant-calendar-time-picker-column-4 .ant-calendar-time-picker-select{width:25%}.ant-calendar-time-picker-input-wrap{display:none}.ant-calendar-time-picker-select{position:relative;float:left;height:226px;overflow:hidden;font-size:14px;border-right:1px solid #e8e8e8}.ant-calendar-time-picker-select:hover{overflow-y:auto}.ant-calendar-time-picker-select:first-child{margin-left:0;border-left:0}.ant-calendar-time-picker-select:last-child{border-right:0}.ant-calendar-time-picker-select ul{width:100%;max-height:206px;margin:0;padding:0;list-style:none}.ant-calendar-time-picker-select li{width:100%;height:24px;margin:0;line-height:24px;text-align:center;list-style:none;cursor:pointer;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-calendar-time-picker-select li:last-child:after{display:block;height:202px;content:""}.ant-calendar-time-picker-select li:hover{background:#b3c7c0}.ant-calendar-time-picker-select li:focus{color:#008771;font-weight:600;outline:none}li.ant-calendar-time-picker-select-option-selected{font-weight:600;background:#f5f5f5}li.ant-calendar-time-picker-select-option-disabled{color:rgba(0,0,0,.25)}li.ant-calendar-time-picker-select-option-disabled:hover{background:transparent;cursor:not-allowed}.ant-calendar-time .ant-calendar-day-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:34px}.ant-calendar-time .ant-calendar-footer{position:relative;height:auto}.ant-calendar-time .ant-calendar-footer-btn{text-align:right}.ant-calendar-time .ant-calendar-footer .ant-calendar-today-btn{float:left;margin:0}.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn{display:inline-block;margin-right:8px}.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn-disabled{color:rgba(0,0,0,.25)}.ant-calendar-month-panel{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#fff;border-radius:1rem;outline:none}.ant-calendar-month-panel>div{display:flex;flex-direction:column;height:100%}.ant-calendar-month-panel-hidden{display:none}.ant-calendar-month-panel-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.ant-calendar-month-panel-header a:hover{color:#18947b}.ant-calendar-month-panel-header .ant-calendar-month-panel-century-select,.ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select,.ant-calendar-month-panel-header .ant-calendar-month-panel-month-select,.ant-calendar-month-panel-header .ant-calendar-month-panel-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-month-panel-header .ant-calendar-month-panel-century-select-arrow,.ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select-arrow,.ant-calendar-month-panel-header .ant-calendar-month-panel-month-select-arrow,.ant-calendar-month-panel-header .ant-calendar-month-panel-year-select-arrow{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,Hiragino Sans GB,Microsoft Yahei,"Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn{left:7px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn{right:7px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:before{position:relative;left:3px}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after{display:inline-block}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn{left:29px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:after{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn{right:29px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:after{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-month-panel-body{flex:1}.ant-calendar-month-panel-footer{border-top:1px solid #e8e8e8}.ant-calendar-month-panel-footer .ant-calendar-footer-extra{padding:0 12px}.ant-calendar-month-panel-table{width:100%;height:100%;table-layout:fixed;border-collapse:separate}.ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month,.ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month:hover{color:#fff;background:#008771}.ant-calendar-month-panel-cell{text-align:center}.ant-calendar-month-panel-cell-disabled .ant-calendar-month-panel-month,.ant-calendar-month-panel-cell-disabled .ant-calendar-month-panel-month:hover{color:rgba(0,0,0,.25);background:#f5f5f5;cursor:not-allowed}.ant-calendar-month-panel-month{display:inline-block;height:24px;margin:0 auto;padding:0 8px;color:rgba(0,0,0,.65);line-height:24px;text-align:center;background:transparent;border-radius:2px;transition:background .3s ease}.ant-calendar-month-panel-month:hover{background:#b3c7c0;cursor:pointer}.ant-calendar-year-panel{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#fff;border-radius:1rem;outline:none}.ant-calendar-year-panel>div{display:flex;flex-direction:column;height:100%}.ant-calendar-year-panel-hidden{display:none}.ant-calendar-year-panel-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.ant-calendar-year-panel-header a:hover{color:#18947b}.ant-calendar-year-panel-header .ant-calendar-year-panel-century-select,.ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select,.ant-calendar-year-panel-header .ant-calendar-year-panel-month-select,.ant-calendar-year-panel-header .ant-calendar-year-panel-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-year-panel-header .ant-calendar-year-panel-century-select-arrow,.ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select-arrow,.ant-calendar-year-panel-header .ant-calendar-year-panel-month-select-arrow,.ant-calendar-year-panel-header .ant-calendar-year-panel-year-select-arrow{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,Hiragino Sans GB,Microsoft Yahei,"Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn{left:7px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn{right:7px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:before{position:relative;left:3px}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after{display:inline-block}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn{left:29px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:after{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn{right:29px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:after{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-year-panel-body{flex:1}.ant-calendar-year-panel-footer{border-top:1px solid #e8e8e8}.ant-calendar-year-panel-footer .ant-calendar-footer-extra{padding:0 12px}.ant-calendar-year-panel-table{width:100%;height:100%;table-layout:fixed;border-collapse:separate}.ant-calendar-year-panel-cell{text-align:center}.ant-calendar-year-panel-cell-disabled .ant-calendar-year-panel-year,.ant-calendar-year-panel-cell-disabled .ant-calendar-year-panel-year:hover{color:rgba(0,0,0,.25);background:#f5f5f5;cursor:not-allowed}.ant-calendar-year-panel-year{display:inline-block;height:24px;margin:0 auto;padding:0 8px;color:rgba(0,0,0,.65);line-height:24px;text-align:center;background:transparent;border-radius:2px;transition:background .3s ease}.ant-calendar-year-panel-year:hover{background:#b3c7c0;cursor:pointer}.ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,.ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year:hover{color:#fff;background:#008771}.ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,.ant-calendar-year-panel-next-decade-cell .ant-calendar-year-panel-year{color:rgba(0,0,0,.25);-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-calendar-decade-panel{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;display:flex;flex-direction:column;background:#fff;border-radius:1rem;outline:none}.ant-calendar-decade-panel-hidden{display:none}.ant-calendar-decade-panel-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.ant-calendar-decade-panel-header a:hover{color:#18947b}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-century-select,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-decade-select,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-month-select,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-century-select-arrow,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-decade-select-arrow,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-month-select-arrow,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-year-select-arrow{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,Hiragino Sans GB,Microsoft Yahei,"Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn{left:7px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn{right:7px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:before{position:relative;left:3px}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after{display:inline-block}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn{left:29px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:after{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn{right:29px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;transform:rotate(-45deg) scale(.8);transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:after{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:before{transform:rotate(135deg) scale(.8)}.ant-calendar-decade-panel-body{flex:1}.ant-calendar-decade-panel-footer{border-top:1px solid #e8e8e8}.ant-calendar-decade-panel-footer .ant-calendar-footer-extra{padding:0 12px}.ant-calendar-decade-panel-table{width:100%;height:100%;table-layout:fixed;border-collapse:separate}.ant-calendar-decade-panel-cell{white-space:nowrap;text-align:center}.ant-calendar-decade-panel-decade{display:inline-block;height:24px;margin:0 auto;padding:0 6px;color:rgba(0,0,0,.65);line-height:24px;text-align:center;background:transparent;border-radius:2px;transition:background .3s ease}.ant-calendar-decade-panel-decade:hover{background:#b3c7c0;cursor:pointer}.ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade,.ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade:hover{color:#fff;background:#008771}.ant-calendar-decade-panel-last-century-cell .ant-calendar-decade-panel-decade,.ant-calendar-decade-panel-next-century-cell .ant-calendar-decade-panel-decade{color:rgba(0,0,0,.25);-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-calendar-month .ant-calendar-month-header-wrap{position:relative;height:288px}.ant-calendar-month .ant-calendar-month-panel,.ant-calendar-month .ant-calendar-year-panel{top:0;height:100%}.ant-calendar-week-number-cell{opacity:.5}.ant-calendar-week-number .ant-calendar-body tr{cursor:pointer;transition:all .3s}.ant-calendar-week-number .ant-calendar-body tr:hover{background:#b3c7c0}.ant-calendar-week-number .ant-calendar-body tr.ant-calendar-active-week{font-weight:700;background:#77baa6}.ant-calendar-week-number .ant-calendar-body tr .ant-calendar-selected-day .ant-calendar-date,.ant-calendar-week-number .ant-calendar-body tr .ant-calendar-selected-day:hover .ant-calendar-date{color:rgba(0,0,0,.65);background:transparent}.ant-time-picker-panel{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;z-index:1050;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.ant-time-picker-panel-inner{position:relative;left:-2px;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border-radius:1rem;outline:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-time-picker-panel-input{width:100%;max-width:154px;margin:0;padding:0;line-height:normal;border:0;outline:0;cursor:auto}.ant-time-picker-panel-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-time-picker-panel-input:-ms-input-placeholder{color:#bfbfbf}.ant-time-picker-panel-input::-webkit-input-placeholder{color:#bfbfbf}.ant-time-picker-panel-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-time-picker-panel-input:placeholder-shown{text-overflow:ellipsis}.ant-time-picker-panel-input-wrap{position:relative;padding:7px 2px 7px 12px;border-bottom:1px solid #e8e8e8}.ant-time-picker-panel-input-invalid{border-color:#f5222d}.ant-time-picker-panel-narrow .ant-time-picker-panel-input-wrap{max-width:112px}.ant-time-picker-panel-select{position:relative;float:left;width:56px;max-height:192px;overflow:hidden;font-size:14px;border-left:1px solid #e8e8e8}.ant-time-picker-panel-select:hover{overflow-y:auto}.ant-time-picker-panel-select:first-child{margin-left:0;border-left:0}.ant-time-picker-panel-select:last-child{border-right:0}.ant-time-picker-panel-select:only-child{width:100%}.ant-time-picker-panel-select ul{width:56px;margin:0;padding:0 0 160px;list-style:none}.ant-time-picker-panel-select li{width:100%;height:32px;margin:0;padding:0 0 0 12px;line-height:32px;text-align:left;list-style:none;cursor:pointer;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-time-picker-panel-select li:focus{color:#008771;font-weight:600;outline:none}.ant-time-picker-panel-select li:hover{background:#b3c7c0}li.ant-time-picker-panel-select-option-selected{font-weight:600;background:#f5f5f5}li.ant-time-picker-panel-select-option-selected:hover{background:#f5f5f5}li.ant-time-picker-panel-select-option-disabled{color:rgba(0,0,0,.25)}li.ant-time-picker-panel-select-option-disabled:hover{background:transparent;cursor:not-allowed}li.ant-time-picker-panel-select-option-disabled:focus{color:rgba(0,0,0,.25);font-weight:inherit}.ant-time-picker-panel-combobox{zoom:1}.ant-time-picker-panel-combobox:after,.ant-time-picker-panel-combobox:before{display:table;content:""}.ant-time-picker-panel-combobox:after{clear:both}.ant-time-picker-panel-addon{padding:8px;border-top:1px solid #e8e8e8}.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-topLeft,.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-topRight,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-topLeft,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-topRight{animation-name:antSlideDownIn}.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-bottomLeft,.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-bottomRight,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-bottomLeft,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-bottomRight{animation-name:antSlideUpIn}.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-topLeft,.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-topRight{animation-name:antSlideDownOut}.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-bottomLeft,.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-bottomRight{animation-name:antSlideUpOut}.ant-time-picker{box-sizing:border-box;margin:0;padding:0;font-size:14px;font-variant:tabular-nums;list-style:none;font-feature-settings:"tnum";width:128px;outline:none;cursor:text;transition:opacity .3s}.ant-time-picker,.ant-time-picker-input{color:rgba(0,0,0,.65);line-height:1.5;position:relative;display:inline-block}.ant-time-picker-input{width:100%;height:32px;padding:4px 11px;font-size:14px;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:1rem;transition:all .3s}.ant-time-picker-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-time-picker-input:-ms-input-placeholder{color:#bfbfbf}.ant-time-picker-input::-webkit-input-placeholder{color:#bfbfbf}.ant-time-picker-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-time-picker-input:placeholder-shown{text-overflow:ellipsis}.ant-time-picker-input:focus,.ant-time-picker-input:hover{border-color:#18947b;border-right-width:1px!important}.ant-time-picker-input:focus{outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-time-picker-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-time-picker-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}.ant-time-picker-input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-time-picker-input-sm{height:24px;padding:1px 7px}.ant-time-picker-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-time-picker-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-time-picker-open{opacity:0}.ant-time-picker-clear,.ant-time-picker-icon{position:absolute;top:50%;right:11px;z-index:1;width:14px;height:14px;margin-top:-7px;color:rgba(0,0,0,.25);line-height:14px;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-time-picker-clear .ant-time-picker-clock-icon,.ant-time-picker-icon .ant-time-picker-clock-icon{display:block;color:rgba(0,0,0,.25);line-height:1}.ant-time-picker-clear{z-index:2;background:#fff;opacity:0;pointer-events:none}.ant-time-picker-clear:hover{color:rgba(0,0,0,.45)}.ant-time-picker:hover .ant-time-picker-clear{opacity:1;pointer-events:auto}.ant-time-picker-large .ant-time-picker-input{height:40px;padding:6px 11px;font-size:16px}.ant-time-picker-small .ant-time-picker-input{height:24px;padding:1px 7px}.ant-time-picker-small .ant-time-picker-clear,.ant-time-picker-small .ant-time-picker-icon{right:7px}@media not all and (min-resolution:0.001dpcm){@supports (-webkit-appearance:none) and (stroke-color:transparent){.ant-input{line-height:1.5}}}.ant-tag{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block;height:auto;margin:0 8px 0 0;padding:0 7px;font-size:12px;line-height:20px;white-space:nowrap;background:#fafafa;border:1px solid #d9d9d9;border-radius:1rem;cursor:default;opacity:1;transition:all .3s cubic-bezier(.78,.14,.15,.86)}.ant-tag:hover{opacity:.85}.ant-tag,.ant-tag a,.ant-tag a:hover{color:rgba(0,0,0,.65)}.ant-tag>a:first-child:last-child{display:inline-block;margin:0 -8px;padding:0 8px}.ant-tag .anticon-close{display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);margin-left:3px;color:rgba(0,0,0,.45);font-weight:700;cursor:pointer;transition:all .3s cubic-bezier(.78,.14,.15,.86)}:root .ant-tag .anticon-close{font-size:12px}.ant-tag .anticon-close:hover{color:rgba(0,0,0,.85)}.ant-tag-has-color{border-color:transparent}.ant-tag-has-color,.ant-tag-has-color .anticon-close,.ant-tag-has-color .anticon-close:hover,.ant-tag-has-color a,.ant-tag-has-color a:hover{color:#fff}.ant-tag-checkable{background-color:transparent;border-color:transparent}.ant-tag-checkable:not(.ant-tag-checkable-checked):hover{color:#008771}.ant-tag-checkable-checked,.ant-tag-checkable:active{color:#fff}.ant-tag-checkable-checked{background-color:#008771}.ant-tag-checkable:active{background-color:#006154}.ant-tag-hidden{display:none}.ant-tag-pink{color:#eb2f96;background:#fff0f6;border-color:#ffadd2}.ant-tag-pink-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-magenta{color:#eb2f96;background:#fff0f6;border-color:#ffadd2}.ant-tag-magenta-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-red{color:#f5222d;background:#fff1f0;border-color:#ffa39e}.ant-tag-red-inverse{color:#fff;background:#f5222d;border-color:#f5222d}.ant-tag-volcano{color:#fa541c;background:#fff2e8;border-color:#ffbb96}.ant-tag-volcano-inverse{color:#fff;background:#fa541c;border-color:#fa541c}.ant-tag-orange{color:#fa8c16;background:#fff7e6;border-color:#ffd591}.ant-tag-orange-inverse{color:#fff;background:#fa8c16;border-color:#fa8c16}.ant-tag-yellow{color:#fadb14;background:#feffe6;border-color:#fffb8f}.ant-tag-yellow-inverse{color:#fff;background:#fadb14;border-color:#fadb14}.ant-tag-gold{color:#faad14;background:#fffbe6;border-color:#ffe58f}.ant-tag-gold-inverse{color:#fff;background:#faad14;border-color:#faad14}.ant-tag-cyan{color:#13c2c2;background:#e6fffb;border-color:#87e8de}.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}.ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f}.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}.ant-tag-green{color:#008771;background:#b3c7c0;border-color:#53ad95}.ant-tag-green-inverse{color:#fff;background:#008771;border-color:#008771}.ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff}.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}.ant-tag-geekblue{color:#2f54eb;background:#f0f5ff;border-color:#adc6ff}.ant-tag-geekblue-inverse{color:#fff;background:#2f54eb;border-color:#2f54eb}.ant-tag-purple{color:#722ed1;background:#f9f0ff;border-color:#d3adf7}.ant-tag-purple-inverse{color:#fff;background:#722ed1;border-color:#722ed1}.ant-descriptions-title{margin-bottom:20px;color:rgba(0,0,0,.85);font-weight:700;font-size:16px;line-height:1.5}.ant-descriptions-view{width:100%;overflow:hidden;border-radius:1rem}.ant-descriptions-view table{width:100%;table-layout:fixed}.ant-descriptions-row>td,.ant-descriptions-row>th{padding-bottom:16px}.ant-descriptions-row:last-child{border-bottom:none}.ant-descriptions-item-label{color:rgba(0,0,0,.85);font-weight:400;font-size:14px;line-height:1.5}.ant-descriptions-item-label:after{position:relative;top:-.5px;margin:0 8px 0 2px;content:" "}.ant-descriptions-item-colon:after{content:":"}.ant-descriptions-item-no-label:after{margin:0;content:""}.ant-descriptions-item-content{display:table-cell;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5}.ant-descriptions-item{padding-bottom:0}.ant-descriptions-item>span{display:inline-block}.ant-descriptions-middle .ant-descriptions-row>td,.ant-descriptions-middle .ant-descriptions-row>th{padding-bottom:12px}.ant-descriptions-small .ant-descriptions-row>td,.ant-descriptions-small .ant-descriptions-row>th{padding-bottom:8px}.ant-descriptions-bordered .ant-descriptions-view{border:1px solid #e8e8e8}.ant-descriptions-bordered .ant-descriptions-view>table{table-layout:auto}.ant-descriptions-bordered .ant-descriptions-item-content,.ant-descriptions-bordered .ant-descriptions-item-label{padding:16px 24px;border-right:1px solid #e8e8e8}.ant-descriptions-bordered .ant-descriptions-item-content:last-child,.ant-descriptions-bordered .ant-descriptions-item-label:last-child{border-right:none}.ant-descriptions-bordered .ant-descriptions-item-label{background-color:#fafafa}.ant-descriptions-bordered .ant-descriptions-item-label:after{display:none}.ant-descriptions-bordered .ant-descriptions-row{border-bottom:1px solid #e8e8e8}.ant-descriptions-bordered .ant-descriptions-row:last-child{border-bottom:none}.ant-descriptions-bordered.ant-descriptions-middle .ant-descriptions-item-content,.ant-descriptions-bordered.ant-descriptions-middle .ant-descriptions-item-label{padding:12px 24px}.ant-descriptions-bordered.ant-descriptions-small .ant-descriptions-item-content,.ant-descriptions-bordered.ant-descriptions-small .ant-descriptions-item-label{padding:8px 16px}.ant-divider{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background:#e8e8e8}.ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle}.ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0}.ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:16px 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:transparent}.ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid #e8e8e8;transform:translateY(50%);content:""}.ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px}.ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%}.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%}.ant-divider-horizontal.ant-divider-with-text-right:after{top:50%;width:5%}.ant-divider-inner-text{display:inline-block;padding:0 24px}.ant-divider-dashed{background:none;border:dashed #e8e8e8;border-width:1px 0 0}.ant-divider-horizontal.ant-divider-with-text-center.ant-divider-dashed,.ant-divider-horizontal.ant-divider-with-text-left.ant-divider-dashed,.ant-divider-horizontal.ant-divider-with-text-right.ant-divider-dashed{border-top:0}.ant-divider-horizontal.ant-divider-with-text-center.ant-divider-dashed:after,.ant-divider-horizontal.ant-divider-with-text-center.ant-divider-dashed:before,.ant-divider-horizontal.ant-divider-with-text-left.ant-divider-dashed:after,.ant-divider-horizontal.ant-divider-with-text-left.ant-divider-dashed:before,.ant-divider-horizontal.ant-divider-with-text-right.ant-divider-dashed:after,.ant-divider-horizontal.ant-divider-with-text-right.ant-divider-dashed:before{border-style:dashed none none}.ant-divider-vertical.ant-divider-dashed{border-width:0 0 0 1px}.ant-drawer{position:fixed;z-index:1000;width:0;height:100%;transition:transform .3s cubic-bezier(.7,.3,.1,1),height 0s ease .3s,width 0s ease .3s}.ant-drawer>*{transition:transform .3s cubic-bezier(.7,.3,.1,1),box-shadow .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-content-wrapper{position:absolute}.ant-drawer .ant-drawer-content{width:100%;height:100%}.ant-drawer-left,.ant-drawer-right{top:0;width:0;height:100%}.ant-drawer-left .ant-drawer-content-wrapper,.ant-drawer-right .ant-drawer-content-wrapper{height:100%}.ant-drawer-left.ant-drawer-open,.ant-drawer-right.ant-drawer-open{width:100%;transition:transform .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-left.ant-drawer-open.no-mask,.ant-drawer-right.ant-drawer-open.no-mask{width:0}.ant-drawer-left.ant-drawer-open .ant-drawer-content-wrapper{box-shadow:2px 0 8px rgba(0,0,0,.15)}.ant-drawer-right,.ant-drawer-right .ant-drawer-content-wrapper{right:0}.ant-drawer-right.ant-drawer-open .ant-drawer-content-wrapper{box-shadow:-2px 0 8px rgba(0,0,0,.15)}.ant-drawer-right.ant-drawer-open.no-mask{right:1px;transform:translateX(1px)}.ant-drawer-bottom,.ant-drawer-top{left:0;width:100%;height:0%}.ant-drawer-bottom .ant-drawer-content-wrapper,.ant-drawer-top .ant-drawer-content-wrapper{width:100%}.ant-drawer-bottom.ant-drawer-open,.ant-drawer-top.ant-drawer-open{height:100%;transition:transform .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-bottom.ant-drawer-open.no-mask,.ant-drawer-top.ant-drawer-open.no-mask{height:0%}.ant-drawer-top{top:0}.ant-drawer-top.ant-drawer-open .ant-drawer-content-wrapper{box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-drawer-bottom,.ant-drawer-bottom .ant-drawer-content-wrapper{bottom:0}.ant-drawer-bottom.ant-drawer-open .ant-drawer-content-wrapper{box-shadow:0 -2px 8px rgba(0,0,0,.15)}.ant-drawer-bottom.ant-drawer-open.no-mask{bottom:1px;transform:translateY(1px)}.ant-drawer.ant-drawer-open .ant-drawer-mask{height:100%;opacity:1;transition:none;animation:antdDrawerFadeIn .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-title{margin:0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;line-height:22px}.ant-drawer-content{position:relative;z-index:1;overflow:auto;background-color:#fff;background-clip:padding-box;border:0}.ant-drawer-close{position:absolute;top:0;right:0;z-index:10;display:block;width:56px;height:56px;padding:0;color:rgba(0,0,0,.45);font-weight:700;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-decoration:none;background:transparent;border:0;outline:0;cursor:pointer;transition:color .3s;text-rendering:auto}.ant-drawer-close:focus,.ant-drawer-close:hover{color:rgba(0,0,0,.75);text-decoration:none}.ant-drawer-header{position:relative;padding:16px 24px;border-bottom:1px solid #e8e8e8;border-radius:1rem 1rem 0 0}.ant-drawer-header,.ant-drawer-header-no-title{color:rgba(0,0,0,.65);background:#fff}.ant-drawer-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}.ant-drawer-wrapper-body{height:100%;overflow:auto}.ant-drawer-mask{position:absolute;top:0;left:0;width:100%;height:0;background-color:rgba(0,0,0,.45);opacity:0;filter:alpha(opacity=45);transition:opacity .3s linear,height 0s ease .3s}.ant-drawer-open-content{box-shadow:0 4px 12px rgba(0,0,0,.15)}@keyframes antdDrawerFadeIn{0%{opacity:0}to{opacity:1}}.ant-form{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"}.ant-form legend{display:block;width:100%;margin-bottom:20px;padding:0;color:rgba(0,0,0,.45);font-size:16px;line-height:inherit;border:0;border-bottom:1px solid #d9d9d9}.ant-form label{font-size:14px}.ant-form input[type=search]{box-sizing:border-box}.ant-form input[type=checkbox],.ant-form input[type=radio]{line-height:normal}.ant-form input[type=file]{display:block}.ant-form input[type=range]{display:block;width:100%}.ant-form select[multiple],.ant-form select[size]{height:auto}.ant-form input[type=checkbox]:focus,.ant-form input[type=file]:focus,.ant-form input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.ant-form output{display:block;padding-top:15px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5}.ant-form-item-required:before{display:inline-block;margin-right:4px;color:#f5222d;font-size:14px;font-family:SimSun,sans-serif;line-height:1;content:"*"}.ant-form-hide-required-mark .ant-form-item-required:before{display:none}.ant-form-item-label>label{color:rgba(0,0,0,.85)}.ant-form-item-label>label:after{content:":";position:relative;top:-.5px;margin:0 8px 0 2px}.ant-form-item-label>label.ant-form-item-no-colon:after{content:" "}.ant-form-item{box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";margin:0 0 24px;vertical-align:top}.ant-form-item label{position:relative}.ant-form-item label>.anticon{font-size:14px;vertical-align:top}.ant-form-item-control{position:relative;line-height:40px;zoom:1}.ant-form-item-control:after,.ant-form-item-control:before{display:table;content:""}.ant-form-item-control:after{clear:both}.ant-form-item-children{position:relative}.ant-form-item-with-help{margin-bottom:5px}.ant-form-item-label{display:inline-block;overflow:hidden;line-height:39.9999px;white-space:nowrap;text-align:right;vertical-align:middle}.ant-form-item-label-left{text-align:left}.ant-form-item .ant-switch{margin:2px 0 4px}.ant-form-explain,.ant-form-extra{clear:both;min-height:22px;margin-top:-2px;color:rgba(0,0,0,.45);font-size:14px;line-height:1.5;transition:color .3s cubic-bezier(.215,.61,.355,1)}.ant-form-explain{margin-bottom:-1px}.ant-form-extra{padding-top:4px}.ant-form-text{display:inline-block;padding-right:8px}.ant-form-split{display:block;text-align:center}form .has-feedback .ant-input{padding-right:30px}form .has-feedback .ant-input-affix-wrapper .ant-input-suffix{padding-right:18px}form .has-feedback .ant-input-affix-wrapper .ant-input{padding-right:49px}form .has-feedback .ant-input-affix-wrapper.ant-input-affix-wrapper-input-with-clear-btn .ant-input{padding-right:68px}form .has-feedback :not(.ant-input-group-addon)>.ant-select .ant-select-arrow,form .has-feedback :not(.ant-input-group-addon)>.ant-select .ant-select-selection__clear,form .has-feedback>.ant-select .ant-select-arrow,form .has-feedback>.ant-select .ant-select-selection__clear{right:28px}form .has-feedback :not(.ant-input-group-addon)>.ant-select .ant-select-selection-selected-value,form .has-feedback>.ant-select .ant-select-selection-selected-value{padding-right:42px}form .has-feedback .ant-cascader-picker-arrow{margin-right:17px}form .has-feedback .ant-calendar-picker-clear,form .has-feedback .ant-calendar-picker-icon,form .has-feedback .ant-cascader-picker-clear,form .has-feedback .ant-input-search:not(.ant-input-search-enter-button) .ant-input-suffix,form .has-feedback .ant-time-picker-clear,form .has-feedback .ant-time-picker-icon{right:28px}form .ant-mentions,form textarea.ant-input{height:auto;margin-bottom:4px}form .ant-upload{background:transparent}form input[type=checkbox],form input[type=radio]{width:14px;height:14px}form .ant-checkbox-inline,form .ant-radio-inline{display:inline-block;margin-left:8px;font-weight:400;vertical-align:middle;cursor:pointer}form .ant-checkbox-inline:first-child,form .ant-radio-inline:first-child{margin-left:0}form .ant-checkbox-vertical,form .ant-radio-vertical{display:block}form .ant-checkbox-vertical+.ant-checkbox-vertical,form .ant-radio-vertical+.ant-radio-vertical{margin-left:0}form .ant-input-number+.ant-form-text{margin-left:8px}form .ant-input-number-handler-wrap{z-index:2}form .ant-cascader-picker,form .ant-select{width:100%}form .ant-input-group .ant-cascader-picker,form .ant-input-group .ant-select{width:auto}form .ant-input-group-wrapper,form :not(.ant-input-group-wrapper)>.ant-input-group{display:inline-block;vertical-align:middle}form:not(.ant-form-vertical) .ant-input-group-wrapper,form:not(.ant-form-vertical) :not(.ant-input-group-wrapper)>.ant-input-group{position:relative;top:-1px}.ant-col-24.ant-form-item-label,.ant-col-xl-24.ant-form-item-label,.ant-form-vertical .ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-24.ant-form-item-label label:after,.ant-col-xl-24.ant-form-item-label label:after,.ant-form-vertical .ant-form-item-label label:after{display:none}.ant-form-vertical .ant-form-item{padding-bottom:8px}.ant-form-vertical .ant-form-item-control{line-height:1.5}.ant-form-vertical .ant-form-explain{margin-top:2px;margin-bottom:-5px}.ant-form-vertical .ant-form-extra{margin-top:2px;margin-bottom:-4px}@media (max-width:575px){.ant-form-item-control-wrapper,.ant-form-item-label{display:block;width:100%}.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-form-item-label label:after{display:none}.ant-col-xs-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-xs-24.ant-form-item-label label:after{display:none}}@media (max-width:767px){.ant-col-sm-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-sm-24.ant-form-item-label label:after{display:none}}@media (max-width:991px){.ant-col-md-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-md-24.ant-form-item-label label:after{display:none}}@media (max-width:1199px){.ant-col-lg-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-lg-24.ant-form-item-label label:after{display:none}}@media (max-width:1599px){.ant-col-xl-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-xl-24.ant-form-item-label label:after{display:none}}.ant-form-inline .ant-form-item{display:inline-block;margin-right:16px;margin-bottom:0}.ant-form-inline .ant-form-item-with-help{margin-bottom:24px}.ant-form-inline .ant-form-item>.ant-form-item-control-wrapper,.ant-form-inline .ant-form-item>.ant-form-item-label{display:inline-block;vertical-align:top}.ant-form-inline .ant-form-text,.ant-form-inline .has-feedback{display:inline-block}.has-error.has-feedback .ant-form-item-children-icon,.has-success.has-feedback .ant-form-item-children-icon,.has-warning.has-feedback .ant-form-item-children-icon,.is-validating.has-feedback .ant-form-item-children-icon{position:absolute;top:50%;right:0;z-index:1;width:32px;height:20px;margin-top:-10px;font-size:14px;line-height:20px;text-align:center;visibility:visible;animation:zoomIn .3s cubic-bezier(.12,.4,.29,1.46);pointer-events:none}.has-error.has-feedback .ant-form-item-children-icon svg,.has-success.has-feedback .ant-form-item-children-icon svg,.has-warning.has-feedback .ant-form-item-children-icon svg,.is-validating.has-feedback .ant-form-item-children-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.has-success.has-feedback .ant-form-item-children-icon{color:#008771;animation-name:diffZoomIn1!important}.has-warning .ant-form-explain,.has-warning .ant-form-split{color:#faad14}.has-warning .ant-input,.has-warning .ant-input:hover{background-color:#fff;border-color:#faad14}.has-warning .ant-input:focus{border-color:#ffc53d;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input:not([disabled]):hover{border-color:#faad14}.has-warning .ant-calendar-picker-open .ant-calendar-picker-input{border-color:#ffc53d;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input-affix-wrapper .ant-input,.has-warning .ant-input-affix-wrapper .ant-input:hover{background-color:#fff;border-color:#faad14}.has-warning .ant-input-affix-wrapper .ant-input:focus{border-color:#ffc53d;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:#faad14}.has-warning .ant-input-prefix{color:#faad14}.has-warning .ant-input-group-addon{color:#faad14;background-color:#fff;border-color:#faad14}.has-warning .has-feedback{color:#faad14}.has-warning.has-feedback .ant-form-item-children-icon{color:#faad14;animation-name:diffZoomIn3!important}.has-warning .ant-select-selection,.has-warning .ant-select-selection:hover{border-color:#faad14}.has-warning .ant-select-focused .ant-select-selection,.has-warning .ant-select-open .ant-select-selection{border-color:#ffc53d;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-calendar-picker-icon:after,.has-warning .ant-cascader-picker-arrow,.has-warning .ant-picker-icon:after,.has-warning .ant-select-arrow,.has-warning .ant-time-picker-icon:after{color:#faad14}.has-warning .ant-input-number,.has-warning .ant-time-picker-input{border-color:#faad14}.has-warning .ant-input-number-focused,.has-warning .ant-input-number:focus,.has-warning .ant-time-picker-input-focused,.has-warning .ant-time-picker-input:focus{border-color:#ffc53d;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input-number:not([disabled]):hover,.has-warning .ant-time-picker-input:not([disabled]):hover{border-color:#faad14}.has-warning .ant-cascader-picker:focus .ant-cascader-input{border-color:#ffc53d;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-cascader-picker:hover .ant-cascader-input{border-color:#faad14}.has-error .ant-form-explain,.has-error .ant-form-split{color:#f5222d}.has-error .ant-input,.has-error .ant-input:hover{background-color:#fff;border-color:#f5222d}.has-error .ant-input:focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input:not([disabled]):hover{border-color:#f5222d}.has-error .ant-calendar-picker-open .ant-calendar-picker-input{border-color:#ff4d4f;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input-affix-wrapper .ant-input,.has-error .ant-input-affix-wrapper .ant-input:hover{background-color:#fff;border-color:#f5222d}.has-error .ant-input-affix-wrapper .ant-input:focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:#f5222d}.has-error .ant-input-prefix{color:#f5222d}.has-error .ant-input-group-addon{color:#f5222d;background-color:#fff;border-color:#f5222d}.has-error .has-feedback{color:#f5222d}.has-error.has-feedback .ant-form-item-children-icon{color:#f5222d;animation-name:diffZoomIn2!important}.has-error .ant-select-selection,.has-error .ant-select-selection:hover{border-color:#f5222d}.has-error .ant-select-focused .ant-select-selection,.has-error .ant-select-open .ant-select-selection{border-color:#ff4d4f;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-select.ant-select-auto-complete .ant-input:focus{border-color:#f5222d}.has-error .ant-input-group-addon .ant-select-selection{border-color:transparent;box-shadow:none}.has-error .ant-calendar-picker-icon:after,.has-error .ant-cascader-picker-arrow,.has-error .ant-picker-icon:after,.has-error .ant-select-arrow,.has-error .ant-time-picker-icon:after{color:#f5222d}.has-error .ant-input-number,.has-error .ant-time-picker-input{border-color:#f5222d}.has-error .ant-input-number-focused,.has-error .ant-input-number:focus,.has-error .ant-time-picker-input-focused,.has-error .ant-time-picker-input:focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input-number:not([disabled]):hover,.has-error .ant-mention-wrapper .ant-mention-editor,.has-error .ant-mention-wrapper .ant-mention-editor:not([disabled]):hover,.has-error .ant-time-picker-input:not([disabled]):hover{border-color:#f5222d}.has-error .ant-cascader-picker:focus .ant-cascader-input,.has-error .ant-mention-wrapper.ant-mention-active:not([disabled]) .ant-mention-editor,.has-error .ant-mention-wrapper .ant-mention-editor:not([disabled]):focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-cascader-picker:hover .ant-cascader-input,.has-error .ant-transfer-list{border-color:#f5222d}.has-error .ant-transfer-list-search:not([disabled]){border-color:#d9d9d9}.has-error .ant-transfer-list-search:not([disabled]):hover{border-color:#18947b;border-right-width:1px!important}.has-error .ant-transfer-list-search:not([disabled]):focus{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.is-validating.has-feedback .ant-form-item-children-icon{display:inline-block;color:#008771}.ant-advanced-search-form .ant-form-item{margin-bottom:24px}.ant-advanced-search-form .ant-form-item-with-help{margin-bottom:5px}.show-help-appear,.show-help-enter,.show-help-leave{animation-duration:.3s;animation-fill-mode:both;animation-play-state:paused}.show-help-appear.show-help-appear-active,.show-help-enter.show-help-enter-active{animation-name:antShowHelpIn;animation-play-state:running}.show-help-leave.show-help-leave-active{animation-name:antShowHelpOut;animation-play-state:running;pointer-events:none}.show-help-appear,.show-help-enter{opacity:0}.show-help-appear,.show-help-enter,.show-help-leave{animation-timing-function:cubic-bezier(.645,.045,.355,1)}@keyframes antShowHelpIn{0%{transform:translateY(-5px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes antShowHelpOut{to{transform:translateY(-5px);opacity:0}}@keyframes diffZoomIn1{0%{transform:scale(0)}to{transform:scale(1)}}@keyframes diffZoomIn2{0%{transform:scale(0)}to{transform:scale(1)}}@keyframes diffZoomIn3{0%{transform:scale(0)}to{transform:scale(1)}}.ant-input-number{box-sizing:border-box;font-variant:tabular-nums;list-style:none;font-feature-settings:"tnum";position:relative;width:100%;height:32px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;transition:all .3s;display:inline-block;width:90px;margin:0;padding:0;border:1px solid #d9d9d9;border-radius:1rem}.ant-input-number::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-input-number:-ms-input-placeholder{color:#bfbfbf}.ant-input-number::-webkit-input-placeholder{color:#bfbfbf}.ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis}.ant-input-number:placeholder-shown{text-overflow:ellipsis}.ant-input-number:focus{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}.ant-input-number-lg{height:40px;padding:6px 11px}.ant-input-number-sm{height:24px;padding:1px 7px}.ant-input-number-handler{position:relative;display:block;width:100%;height:50%;overflow:hidden;color:rgba(0,0,0,.45);font-weight:700;line-height:0;text-align:center;transition:all .1s linear}.ant-input-number-handler:active{background:#f4f4f4}.ant-input-number-handler:hover .ant-input-number-handler-down-inner,.ant-input-number-handler:hover .ant-input-number-handler-up-inner{color:#18947b}.ant-input-number-handler-down-inner,.ant-input-number-handler-up-inner{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;right:4px;width:12px;height:12px;color:rgba(0,0,0,.45);line-height:12px;transition:all .1s linear;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-input-number-handler-down-inner>*,.ant-input-number-handler-up-inner>*{line-height:1}.ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block}.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}.ant-input-number-focused,.ant-input-number:hover{border-color:#18947b;border-right-width:1px!important}.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}.ant-input-number-disabled .ant-input-number-handler-wrap{display:none}.ant-input-number-input{width:100%;height:30px;padding:0 11px;text-align:left;background-color:transparent;border:0;border-radius:1rem;outline:0;transition:all .3s linear;-moz-appearance:textfield!important}.ant-input-number-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-input-number-input:-ms-input-placeholder{color:#bfbfbf}.ant-input-number-input::-webkit-input-placeholder{color:#bfbfbf}.ant-input-number-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-input-number-input:placeholder-shown{text-overflow:ellipsis}.ant-input-number-input[type=number]::-webkit-inner-spin-button,.ant-input-number-input[type=number]::-webkit-outer-spin-button{margin:0;-webkit-appearance:none}.ant-input-number-lg{padding:0;font-size:16px}.ant-input-number-lg input{height:38px}.ant-input-number-sm{padding:0}.ant-input-number-sm input{height:22px;padding:0 7px}.ant-input-number-handler-wrap{position:absolute;top:0;right:0;width:22px;height:100%;background:#fff;border-left:1px solid #d9d9d9;border-radius:0 1rem 1rem 0;opacity:0;transition:opacity .24s linear .1s}.ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-down-inner,.ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-up-inner{display:inline-block;font-size:12px;font-size:7px\9;transform:scale(.58333333) rotate(0deg);min-width:auto;margin-right:0}:root .ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-down-inner,:root .ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-up-inner{font-size:12px}.ant-input-number-handler-wrap:hover .ant-input-number-handler{height:40%}.ant-input-number:hover .ant-input-number-handler-wrap{opacity:1}.ant-input-number-handler-up{border-top-right-radius:1rem;cursor:pointer}.ant-input-number-handler-up-inner{top:50%;margin-top:-5px;text-align:center}.ant-input-number-handler-up:hover{height:60%!important}.ant-input-number-handler-down{top:0;border-top:1px solid #d9d9d9;border-bottom-right-radius:1rem;cursor:pointer}.ant-input-number-handler-down-inner{top:50%;margin-top:-6px;text-align:center}.ant-input-number-handler-down:hover{height:60%!important}.ant-input-number-handler-down-disabled,.ant-input-number-handler-up-disabled{cursor:not-allowed}.ant-input-number-handler-down-disabled:hover .ant-input-number-handler-down-inner,.ant-input-number-handler-up-disabled:hover .ant-input-number-handler-up-inner{color:rgba(0,0,0,.25)}.ant-layout{display:flex;flex:auto;flex-direction:column;min-height:0;background:#f0f2f5}.ant-layout,.ant-layout *{box-sizing:border-box}.ant-layout.ant-layout-has-sider{flex-direction:row}.ant-layout.ant-layout-has-sider>.ant-layout,.ant-layout.ant-layout-has-sider>.ant-layout-content{overflow-x:hidden}.ant-layout-footer,.ant-layout-header{flex:0 0 auto}.ant-layout-header{height:64px;padding:0 50px;line-height:64px;background:#001529}.ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5}.ant-layout-content{flex:auto;min-height:0}.ant-layout-sider{position:relative;min-width:0;background:#001529;transition:all .2s}.ant-layout-sider-children{height:100%;margin-top:-.1px;padding-top:.1px}.ant-layout-sider-has-trigger{padding-bottom:48px}.ant-layout-sider-right{order:1}.ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;transition:all .2s}.ant-layout-sider-zero-width>*{overflow:hidden}.ant-layout-sider-zero-width-trigger{position:absolute;top:64px;right:-36px;z-index:1;width:36px;height:42px;color:#fff;font-size:18px;line-height:42px;text-align:center;background:#001529;border-radius:0 1rem 1rem 0;cursor:pointer;transition:background .3s ease}.ant-layout-sider-zero-width-trigger:hover{background:#192c3e}.ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:1rem 0 0 1rem}.ant-layout-sider-light{background:#fff}.ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff}.ant-list{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative}.ant-list *{outline:none}.ant-list-pagination{margin-top:24px;text-align:right}.ant-list-pagination .ant-pagination-options{text-align:left}.ant-list-more{margin-top:12px;text-align:center}.ant-list-more button{padding-right:32px;padding-left:32px}.ant-list-spin{min-height:40px;text-align:center}.ant-list-empty-text{padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center}.ant-list-items{margin:0;padding:0;list-style:none}.ant-list-item{display:flex;align-items:center;justify-content:space-between;padding:12px 0}.ant-list-item-content{color:rgba(0,0,0,.65)}.ant-list-item-meta{display:flex;flex:1;align-items:flex-start;font-size:0}.ant-list-item-meta-avatar{margin-right:16px}.ant-list-item-meta-content{flex:1 0}.ant-list-item-meta-title{margin-bottom:4px;color:rgba(0,0,0,.65);font-size:14px;line-height:22px}.ant-list-item-meta-title>a{color:rgba(0,0,0,.65);transition:all .3s}.ant-list-item-meta-title>a:hover{color:#008771}.ant-list-item-meta-description{color:rgba(0,0,0,.45);font-size:14px;line-height:22px}.ant-list-item-action{flex:0 0 auto;margin-left:48px;padding:0;font-size:0;list-style:none}.ant-list-item-action>li{position:relative;display:inline-block;padding:0 8px;color:rgba(0,0,0,.45);font-size:14px;line-height:22px;text-align:center;cursor:pointer}.ant-list-item-action>li:first-child{padding-left:0}.ant-list-item-action-split{position:absolute;top:50%;right:0;width:1px;height:14px;margin-top:-7px;background-color:#e8e8e8}.ant-list-footer,.ant-list-header{background:transparent}.ant-list-footer,.ant-list-header{padding-top:12px;padding-bottom:12px}.ant-list-empty{padding:16px 0;color:rgba(0,0,0,.45);font-size:12px;text-align:center}.ant-list-split .ant-list-item{border-bottom:1px solid #e8e8e8}.ant-list-split .ant-list-item:last-child{border-bottom:none}.ant-list-split .ant-list-header{border-bottom:1px solid #e8e8e8}.ant-list-loading .ant-list-spin-nested-loading{min-height:32px}.ant-list-something-after-last-item .ant-spin-container>.ant-list-items>.ant-list-item:last-child{border-bottom:1px solid #e8e8e8}.ant-list-lg .ant-list-item{padding-top:16px;padding-bottom:16px}.ant-list-sm .ant-list-item{padding-top:8px;padding-bottom:8px}.ant-list-vertical .ant-list-item{align-items:normal}.ant-list-vertical .ant-list-item-main{display:block;flex:1}.ant-list-vertical .ant-list-item-extra{margin-left:40px}.ant-list-vertical .ant-list-item-meta{margin-bottom:16px}.ant-list-vertical .ant-list-item-meta-title{margin-bottom:12px;color:rgba(0,0,0,.85);font-size:16px;line-height:24px}.ant-list-vertical .ant-list-item-action{margin-top:16px;margin-left:auto}.ant-list-vertical .ant-list-item-action>li{padding:0 16px}.ant-list-vertical .ant-list-item-action>li:first-child{padding-left:0}.ant-list-grid .ant-col>.ant-list-item{display:block;max-width:100%;margin-bottom:16px;padding-top:0;padding-bottom:0;border-bottom:none}.ant-list-item-no-flex{display:block}.ant-list:not(.ant-list-vertical) .ant-list-item-no-flex .ant-list-item-action{float:right}.ant-list-bordered{border:1px solid #d9d9d9;border-radius:1rem}.ant-list-bordered .ant-list-footer,.ant-list-bordered .ant-list-header,.ant-list-bordered .ant-list-item{padding-right:24px;padding-left:24px}.ant-list-bordered .ant-list-item{border-bottom:1px solid #e8e8e8}.ant-list-bordered .ant-list-pagination{margin:16px 24px}.ant-list-bordered.ant-list-sm .ant-list-item{padding-right:16px;padding-left:16px}.ant-list-bordered.ant-list-sm .ant-list-footer,.ant-list-bordered.ant-list-sm .ant-list-header{padding:8px 16px}.ant-list-bordered.ant-list-lg .ant-list-footer,.ant-list-bordered.ant-list-lg .ant-list-header{padding:16px 24px}@media screen and (max-width:768px){.ant-list-item-action,.ant-list-vertical .ant-list-item-extra{margin-left:24px}}@media screen and (max-width:576px){.ant-list-item{flex-wrap:wrap}.ant-list-item-action{margin-left:12px}.ant-list-vertical .ant-list-item{flex-wrap:wrap-reverse}.ant-list-vertical .ant-list-item-main{min-width:220px}.ant-list-vertical .ant-list-item-extra{margin:auto auto 16px}}.ant-spin{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;display:none;color:#008771;text-align:center;vertical-align:middle;opacity:0;transition:transform .3s cubic-bezier(.78,.14,.15,.86)}.ant-spin-spinning{position:static;display:inline-block;opacity:1}.ant-spin-nested-loading{position:relative}.ant-spin-nested-loading>div>.ant-spin{position:absolute;top:0;left:0;z-index:4;display:block;width:100%;height:100%;max-height:400px}.ant-spin-nested-loading>div>.ant-spin .ant-spin-dot{position:absolute;top:50%;left:50%;margin:-10px}.ant-spin-nested-loading>div>.ant-spin .ant-spin-text{position:absolute;top:50%;width:100%;padding-top:5px;text-shadow:0 1px 2px #fff}.ant-spin-nested-loading>div>.ant-spin.ant-spin-show-text .ant-spin-dot{margin-top:-20px}.ant-spin-nested-loading>div>.ant-spin-sm .ant-spin-dot{margin:-7px}.ant-spin-nested-loading>div>.ant-spin-sm .ant-spin-text{padding-top:2px}.ant-spin-nested-loading>div>.ant-spin-sm.ant-spin-show-text .ant-spin-dot{margin-top:-17px}.ant-spin-nested-loading>div>.ant-spin-lg .ant-spin-dot{margin:-16px}.ant-spin-nested-loading>div>.ant-spin-lg .ant-spin-text{padding-top:11px}.ant-spin-nested-loading>div>.ant-spin-lg.ant-spin-show-text .ant-spin-dot{margin-top:-26px}.ant-spin-container{position:relative;transition:opacity .3s}.ant-spin-container:after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;display:none\9;width:100%;height:100%;background:#fff;opacity:0;transition:all .3s;content:"";pointer-events:none}.ant-spin-blur{clear:both;overflow:hidden;opacity:.5;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none}.ant-spin-blur:after{opacity:.4;pointer-events:auto}.ant-spin-tip{color:rgba(0,0,0,.45)}.ant-spin-dot{position:relative;display:inline-block;font-size:20px;width:1em;height:1em}.ant-spin-dot-item{position:absolute;display:block;width:9px;height:9px;background-color:#008771;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s linear infinite alternate}.ant-spin-dot-item:first-child{top:0;left:0}.ant-spin-dot-item:nth-child(2){top:0;right:0;animation-delay:.4s}.ant-spin-dot-item:nth-child(3){right:0;bottom:0;animation-delay:.8s}.ant-spin-dot-item:nth-child(4){bottom:0;left:0;animation-delay:1.2s}.ant-spin-dot-spin{transform:rotate(45deg);animation:antRotate 1.2s linear infinite}.ant-spin-sm .ant-spin-dot{font-size:14px}.ant-spin-sm .ant-spin-dot i{width:6px;height:6px}.ant-spin-lg .ant-spin-dot{font-size:32px}.ant-spin-lg .ant-spin-dot i{width:14px;height:14px}.ant-spin.ant-spin-show-text .ant-spin-text{display:block}@media (-ms-high-contrast:active),(-ms-high-contrast:none){.ant-spin-blur{background:#fff;opacity:.5}}@keyframes antSpinMove{to{opacity:1}}@keyframes antRotate{to{transform:rotate(405deg)}}.ant-pagination{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;font-feature-settings:"tnum"}.ant-pagination,.ant-pagination ol,.ant-pagination ul{margin:0;padding:0;list-style:none}.ant-pagination:after{display:block;clear:both;height:0;overflow:hidden;visibility:hidden;content:" "}.ant-pagination-item,.ant-pagination-total-text{display:inline-block;height:32px;margin-right:8px;line-height:30px;vertical-align:middle}.ant-pagination-item{min-width:32px;font-family:Arial;text-align:center;list-style:none;background-color:#fff;border:1px solid #d9d9d9;border-radius:1rem;outline:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-pagination-item a{display:block;padding:0 6px;color:rgba(0,0,0,.65);transition:none}.ant-pagination-item a:hover{text-decoration:none}.ant-pagination-item:focus,.ant-pagination-item:hover{border-color:#008771;transition:all .3s}.ant-pagination-item:focus a,.ant-pagination-item:hover a{color:#008771}.ant-pagination-item-active{font-weight:500;background:#fff;border-color:#008771}.ant-pagination-item-active a{color:#008771}.ant-pagination-item-active:focus,.ant-pagination-item-active:hover{border-color:#18947b}.ant-pagination-item-active:focus a,.ant-pagination-item-active:hover a{color:#18947b}.ant-pagination-jump-next,.ant-pagination-jump-prev{outline:0}.ant-pagination-jump-next .ant-pagination-item-container,.ant-pagination-jump-prev .ant-pagination-item-container{position:relative}.ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon,.ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon{display:inline-block;font-size:12px;font-size:12px\9;transform:scale(1) rotate(0deg);color:#008771;letter-spacing:-1px;opacity:0;transition:all .2s}:root .ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon,:root .ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon{font-size:12px}.ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon-svg,.ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon-svg{top:0;right:0;bottom:0;left:0;margin:auto}.ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-ellipsis,.ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-ellipsis{position:absolute;top:0;right:0;bottom:0;left:0;display:block;margin:auto;color:rgba(0,0,0,.25);letter-spacing:2px;text-align:center;text-indent:.13em;opacity:1;transition:all .2s}.ant-pagination-jump-next:focus .ant-pagination-item-link-icon,.ant-pagination-jump-next:hover .ant-pagination-item-link-icon,.ant-pagination-jump-prev:focus .ant-pagination-item-link-icon,.ant-pagination-jump-prev:hover .ant-pagination-item-link-icon{opacity:1}.ant-pagination-jump-next:focus .ant-pagination-item-ellipsis,.ant-pagination-jump-next:hover .ant-pagination-item-ellipsis,.ant-pagination-jump-prev:focus .ant-pagination-item-ellipsis,.ant-pagination-jump-prev:hover .ant-pagination-item-ellipsis{opacity:0}.ant-pagination-jump-next,.ant-pagination-jump-prev,.ant-pagination-prev{margin-right:8px}.ant-pagination-jump-next,.ant-pagination-jump-prev,.ant-pagination-next,.ant-pagination-prev{display:inline-block;min-width:32px;height:32px;color:rgba(0,0,0,.65);font-family:Arial;line-height:32px;text-align:center;vertical-align:middle;list-style:none;border-radius:1rem;cursor:pointer;transition:all .3s}.ant-pagination-next,.ant-pagination-prev{outline:0}.ant-pagination-next a,.ant-pagination-prev a{color:rgba(0,0,0,.65);-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-pagination-next:hover a,.ant-pagination-prev:hover a{border-color:#18947b}.ant-pagination-next .ant-pagination-item-link,.ant-pagination-prev .ant-pagination-item-link{display:block;height:100%;font-size:12px;text-align:center;background-color:#fff;border:1px solid #d9d9d9;border-radius:1rem;outline:none;transition:all .3s}.ant-pagination-next:focus .ant-pagination-item-link,.ant-pagination-next:hover .ant-pagination-item-link,.ant-pagination-prev:focus .ant-pagination-item-link,.ant-pagination-prev:hover .ant-pagination-item-link{color:#008771;border-color:#008771}.ant-pagination-disabled,.ant-pagination-disabled:focus,.ant-pagination-disabled:hover{cursor:not-allowed}.ant-pagination-disabled .ant-pagination-item-link,.ant-pagination-disabled:focus .ant-pagination-item-link,.ant-pagination-disabled:focus a,.ant-pagination-disabled:hover .ant-pagination-item-link,.ant-pagination-disabled:hover a,.ant-pagination-disabled a{color:rgba(0,0,0,.25);border-color:#d9d9d9;cursor:not-allowed}.ant-pagination-slash{margin:0 10px 0 5px}.ant-pagination-options{display:inline-block;margin-left:16px;vertical-align:middle}.ant-pagination-options-size-changer.ant-select{display:inline-block;width:auto;margin-right:8px}.ant-pagination-options-quick-jumper{display:inline-block;height:32px;line-height:32px;vertical-align:top}.ant-pagination-options-quick-jumper input{position:relative;display:inline-block;width:100%;height:32px;padding:4px 11px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:1rem;transition:all .3s;width:50px;margin:0 8px}.ant-pagination-options-quick-jumper input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-pagination-options-quick-jumper input:-ms-input-placeholder{color:#bfbfbf}.ant-pagination-options-quick-jumper input::-webkit-input-placeholder{color:#bfbfbf}.ant-pagination-options-quick-jumper input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-pagination-options-quick-jumper input:placeholder-shown{text-overflow:ellipsis}.ant-pagination-options-quick-jumper input:focus,.ant-pagination-options-quick-jumper input:hover{border-color:#18947b;border-right-width:1px!important}.ant-pagination-options-quick-jumper input:focus{outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-pagination-options-quick-jumper input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-pagination-options-quick-jumper input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-pagination-options-quick-jumper input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-pagination-options-quick-jumper input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-pagination-options-quick-jumper input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}.ant-pagination-options-quick-jumper input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-pagination-options-quick-jumper input-sm{height:24px;padding:1px 7px}.ant-pagination-simple .ant-pagination-next,.ant-pagination-simple .ant-pagination-prev{height:24px;line-height:24px;vertical-align:top}.ant-pagination-simple .ant-pagination-next .ant-pagination-item-link,.ant-pagination-simple .ant-pagination-prev .ant-pagination-item-link{height:24px;border:0}.ant-pagination-simple .ant-pagination-next .ant-pagination-item-link:after,.ant-pagination-simple .ant-pagination-prev .ant-pagination-item-link:after{height:24px;line-height:24px}.ant-pagination-simple .ant-pagination-simple-pager{display:inline-block;height:24px;margin-right:8px}.ant-pagination-simple .ant-pagination-simple-pager input{box-sizing:border-box;height:100%;margin-right:8px;padding:0 6px;text-align:center;background-color:#fff;border:1px solid #d9d9d9;border-radius:1rem;outline:none;transition:border-color .3s}.ant-pagination-simple .ant-pagination-simple-pager input:hover{border-color:#008771}.ant-pagination.mini .ant-pagination-simple-pager,.ant-pagination.mini .ant-pagination-total-text{height:24px;line-height:24px}.ant-pagination.mini .ant-pagination-item{min-width:24px;height:24px;margin:0;line-height:22px}.ant-pagination.mini .ant-pagination-item:not(.ant-pagination-item-active){background:transparent;border-color:transparent}.ant-pagination.mini .ant-pagination-next,.ant-pagination.mini .ant-pagination-prev{min-width:24px;height:24px;margin:0;line-height:24px}.ant-pagination.mini .ant-pagination-next .ant-pagination-item-link,.ant-pagination.mini .ant-pagination-prev .ant-pagination-item-link{background:transparent;border-color:transparent}.ant-pagination.mini .ant-pagination-next .ant-pagination-item-link:after,.ant-pagination.mini .ant-pagination-prev .ant-pagination-item-link:after{height:24px;line-height:24px}.ant-pagination.mini .ant-pagination-jump-next,.ant-pagination.mini .ant-pagination-jump-prev{height:24px;margin-right:0;line-height:24px}.ant-pagination.mini .ant-pagination-options{margin-left:2px}.ant-pagination.mini .ant-pagination-options-quick-jumper{height:24px;line-height:24px}.ant-pagination.mini .ant-pagination-options-quick-jumper input{height:24px;padding:1px 7px;width:44px}.ant-pagination.ant-pagination-disabled{cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-item{background:#f5f5f5;border-color:#d9d9d9;cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-item a{color:rgba(0,0,0,.25);background:transparent;border:none;cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-item-active{background:#dbdbdb;border-color:transparent}.ant-pagination.ant-pagination-disabled .ant-pagination-item-active a{color:#fff}.ant-pagination.ant-pagination-disabled .ant-pagination-item-link,.ant-pagination.ant-pagination-disabled .ant-pagination-item-link:focus,.ant-pagination.ant-pagination-disabled .ant-pagination-item-link:hover{color:rgba(0,0,0,.45);background:#f5f5f5;border-color:#d9d9d9;cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:focus .ant-pagination-item-link-icon,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:hover .ant-pagination-item-link-icon,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:focus .ant-pagination-item-link-icon,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:hover .ant-pagination-item-link-icon{opacity:0}.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:focus .ant-pagination-item-ellipsis,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:hover .ant-pagination-item-ellipsis,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:focus .ant-pagination-item-ellipsis,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:hover .ant-pagination-item-ellipsis{opacity:1}@media only screen and (max-width:992px){.ant-pagination-item-after-jump-prev,.ant-pagination-item-before-jump-next{display:none}}@media only screen and (max-width:576px){.ant-pagination-options{display:none}}.ant-mentions{box-sizing:border-box;margin:0;font-variant:tabular-nums;list-style:none;font-feature-settings:"tnum";width:100%;height:32px;color:rgba(0,0,0,.65);font-size:14px;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:1rem;transition:all .3s;position:relative;display:inline-block;height:auto;padding:0;overflow:hidden;line-height:1.5;white-space:pre-wrap;vertical-align:bottom}.ant-mentions::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-mentions:-ms-input-placeholder{color:#bfbfbf}.ant-mentions::-webkit-input-placeholder{color:#bfbfbf}.ant-mentions:-moz-placeholder-shown{text-overflow:ellipsis}.ant-mentions:placeholder-shown{text-overflow:ellipsis}.ant-mentions:focus,.ant-mentions:hover{border-color:#18947b;border-right-width:1px!important}.ant-mentions:focus{outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-mentions-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mentions-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-mentions[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mentions[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-mentions{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}.ant-mentions-lg{height:40px;padding:6px 11px;font-size:16px}.ant-mentions-sm{height:24px;padding:1px 7px}.ant-mentions-disabled>textarea{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mentions-disabled>textarea:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-mentions-focused{border-color:#18947b;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-mentions-measure,.ant-mentions>textarea{min-height:30px;margin:0;padding:4px 11px;overflow:inherit;overflow-x:hidden;overflow-y:auto;font-weight:inherit;font-size:inherit;font-family:inherit;font-style:inherit;font-variant:inherit;font-size-adjust:inherit;font-stretch:inherit;line-height:inherit;direction:inherit;letter-spacing:inherit;white-space:inherit;text-align:inherit;vertical-align:top;word-wrap:break-word;word-break:inherit;-moz-tab-size:inherit;-o-tab-size:inherit;tab-size:inherit}.ant-mentions>textarea{width:100%;border:none;outline:none;resize:none}.ant-mentions>textarea::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-mentions>textarea:-ms-input-placeholder{color:#bfbfbf}.ant-mentions>textarea::-webkit-input-placeholder{color:#bfbfbf}.ant-mentions>textarea:-moz-placeholder-shown{text-overflow:ellipsis}.ant-mentions>textarea:placeholder-shown{text-overflow:ellipsis}.ant-mentions>textarea:-moz-read-only{cursor:default}.ant-mentions>textarea:read-only{cursor:default}.ant-mentions-measure{position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;color:transparent;pointer-events:none}.ant-mentions-measure>span{display:inline-block;min-height:1em}.ant-mentions-dropdown{margin:0;padding:0;color:rgba(0,0,0,.65);font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;top:-9999px;left:-9999px;z-index:1050;box-sizing:border-box;font-size:14px;font-variant:normal;background-color:#fff;border-radius:1rem;outline:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-mentions-dropdown-hidden{display:none}.ant-mentions-dropdown-menu{max-height:250px;margin-bottom:0;padding-left:0;overflow:auto;list-style:none;outline:none}.ant-mentions-dropdown-menu-item{position:relative;display:block;min-width:100px;padding:5px 12px;overflow:hidden;color:rgba(0,0,0,.65);font-weight:400;line-height:22px;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;transition:background .3s ease}.ant-mentions-dropdown-menu-item:hover{background-color:#b3c7c0}.ant-mentions-dropdown-menu-item:first-child{border-radius:1rem 1rem 0 0}.ant-mentions-dropdown-menu-item:last-child{border-radius:0 0 1rem 1rem}.ant-mentions-dropdown-menu-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-mentions-dropdown-menu-item-disabled:hover{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-mentions-dropdown-menu-item-selected{color:rgba(0,0,0,.65);font-weight:600;background-color:#fafafa}.ant-mentions-dropdown-menu-item-active{background-color:#b3c7c0}.ant-message{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:fixed;top:16px;left:0;z-index:1010;width:100%;pointer-events:none}.ant-message-notice{padding:8px;text-align:center}.ant-message-notice:first-child{margin-top:-8px}.ant-message-notice-content{display:inline-block;padding:10px 16px;background:#fff;border-radius:1rem;box-shadow:0 4px 12px rgba(0,0,0,.15);pointer-events:all}.ant-message-success .anticon{color:#008771}.ant-message-error .anticon{color:#f5222d}.ant-message-warning .anticon{color:#faad14}.ant-message-info .anticon,.ant-message-loading .anticon{color:#1890ff}.ant-message .anticon{position:relative;top:1px;margin-right:8px;font-size:16px}.ant-message-notice.move-up-leave.move-up-leave-active{overflow:hidden;animation-name:MessageMoveOut;animation-duration:.3s}@keyframes MessageMoveOut{0%{max-height:150px;padding:8px;opacity:1}to{max-height:0;padding:0;opacity:0}}.ant-modal{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:100px;width:auto;margin:0 auto;padding:0 0 24px;pointer-events:none}.ant-modal-wrap{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;overflow:auto;outline:0;-webkit-overflow-scrolling:touch}.ant-modal-title{margin:0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;line-height:22px;word-wrap:break-word}.ant-modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:0;border-radius:1rem;box-shadow:0 4px 12px rgba(0,0,0,.15);pointer-events:auto}.ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:transparent;border:0;outline:0;cursor:pointer;transition:color .3s}.ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto}.ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none}.ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:1rem 1rem 0 0}.ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}.ant-modal-footer{padding:10px 16px;text-align:right;background:transparent;border-top:1px solid #e8e8e8;border-radius:0 0 1rem 1rem}.ant-modal-footer button+button{margin-bottom:0;margin-left:8px}.ant-modal.zoom-appear,.ant-modal.zoom-enter{transform:none;opacity:0;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)}.ant-modal-mask-hidden{display:none}.ant-modal-open{overflow:hidden}.ant-modal-centered{text-align:center}.ant-modal-centered:before{display:inline-block;width:0;height:100%;vertical-align:middle;content:""}.ant-modal-centered .ant-modal{top:0;display:inline-block;text-align:left;vertical-align:middle}@media (max-width:767px){.ant-modal{max-width:calc(100vw - 16px);margin:8px auto}.ant-modal-centered .ant-modal{flex:1}}.ant-modal-confirm .ant-modal-header{display:none}.ant-modal-confirm .ant-modal-body{padding:32px 32px 24px}.ant-modal-confirm-body-wrapper{zoom:1}.ant-modal-confirm-body-wrapper:after,.ant-modal-confirm-body-wrapper:before{display:table;content:""}.ant-modal-confirm-body-wrapper:after{clear:both}.ant-modal-confirm-body .ant-modal-confirm-title{display:block;overflow:hidden;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;line-height:1.4}.ant-modal-confirm-body .ant-modal-confirm-content{margin-top:8px;color:rgba(0,0,0,.65);font-size:14px}.ant-modal-confirm-body>.anticon{float:left;margin-right:16px;font-size:22px}.ant-modal-confirm-body>.anticon+.ant-modal-confirm-title+.ant-modal-confirm-content{margin-left:38px}.ant-modal-confirm .ant-modal-confirm-btns{float:right;margin-top:24px}.ant-modal-confirm .ant-modal-confirm-btns button+button{margin-bottom:0;margin-left:8px}.ant-modal-confirm-error .ant-modal-confirm-body>.anticon{color:#f5222d}.ant-modal-confirm-confirm .ant-modal-confirm-body>.anticon,.ant-modal-confirm-warning .ant-modal-confirm-body>.anticon{color:#faad14}.ant-modal-confirm-info .ant-modal-confirm-body>.anticon{color:#1890ff}.ant-modal-confirm-success .ant-modal-confirm-body>.anticon{color:#008771}.ant-notification{box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:fixed;z-index:1010;width:384px;max-width:calc(100vw - 32px);margin:0 24px 0 0}.ant-notification-bottomLeft,.ant-notification-topLeft{margin-right:0;margin-left:24px}.ant-notification-bottomLeft .ant-notification-fade-appear.ant-notification-fade-appear-active,.ant-notification-bottomLeft .ant-notification-fade-enter.ant-notification-fade-enter-active,.ant-notification-topLeft .ant-notification-fade-appear.ant-notification-fade-appear-active,.ant-notification-topLeft .ant-notification-fade-enter.ant-notification-fade-enter-active{animation-name:NotificationLeftFadeIn}.ant-notification-close-icon{font-size:14px;cursor:pointer}.ant-notification-notice{position:relative;margin-bottom:16px;padding:16px 24px;overflow:hidden;line-height:1.5;background:#fff;border-radius:1rem;box-shadow:0 4px 12px rgba(0,0,0,.15)}.ant-notification-notice-message{display:inline-block;margin-bottom:8px;color:rgba(0,0,0,.85);font-size:16px;line-height:24px}.ant-notification-notice-message-single-line-auto-margin{display:block;width:calc(264px - 100%);max-width:4px;background-color:transparent;pointer-events:none}.ant-notification-notice-message-single-line-auto-margin:before{display:block;content:""}.ant-notification-notice-description{font-size:14px}.ant-notification-notice-closable .ant-notification-notice-message{padding-right:24px}.ant-notification-notice-with-icon .ant-notification-notice-message{margin-bottom:4px;margin-left:48px;font-size:16px}.ant-notification-notice-with-icon .ant-notification-notice-description{margin-left:48px;font-size:14px}.ant-notification-notice-icon{position:absolute;margin-left:4px;font-size:24px;line-height:24px}.anticon.ant-notification-notice-icon-success{color:#008771}.anticon.ant-notification-notice-icon-info{color:#1890ff}.anticon.ant-notification-notice-icon-warning{color:#faad14}.anticon.ant-notification-notice-icon-error{color:#f5222d}.ant-notification-notice-close{position:absolute;top:16px;right:22px;color:rgba(0,0,0,.45);outline:none}.ant-notification-notice-close:hover{color:rgba(0,0,0,.67)}.ant-notification-notice-btn{float:right;margin-top:16px}.ant-notification .notification-fade-effect{animation-duration:.24s;animation-timing-function:cubic-bezier(.645,.045,.355,1);animation-fill-mode:both}.ant-notification-fade-appear,.ant-notification-fade-enter{opacity:0;animation-play-state:paused}.ant-notification-fade-appear,.ant-notification-fade-enter,.ant-notification-fade-leave{animation-duration:.24s;animation-timing-function:cubic-bezier(.645,.045,.355,1);animation-fill-mode:both}.ant-notification-fade-leave{animation-duration:.2s;animation-play-state:paused}.ant-notification-fade-appear.ant-notification-fade-appear-active,.ant-notification-fade-enter.ant-notification-fade-enter-active{animation-name:NotificationFadeIn;animation-play-state:running}.ant-notification-fade-leave.ant-notification-fade-leave-active{animation-name:NotificationFadeOut;animation-play-state:running}@keyframes NotificationFadeIn{0%{left:384px;opacity:0}to{left:0;opacity:1}}@keyframes NotificationLeftFadeIn{0%{right:384px;opacity:0}to{right:0;opacity:1}}@keyframes NotificationFadeOut{0%{max-height:150px;margin-bottom:16px;padding-top:16px 24px;padding-bottom:16px 24px;opacity:1}to{max-height:0;margin-bottom:0;padding-top:0;padding-bottom:0;opacity:0}}.ant-page-header{box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;padding:16px 24px;background-color:#fff}.ant-page-header-ghost{background-color:inherit}.ant-page-header.has-breadcrumb{padding-top:12px}.ant-page-header.has-footer{padding-bottom:0}.ant-page-header-back{float:left;margin:8px 16px 8px 0;font-size:16px;line-height:1}.ant-page-header-back-button{color:#008771;text-decoration:none;outline:none;transition:color .3s;color:#000;cursor:pointer}.ant-page-header-back-button:focus,.ant-page-header-back-button:hover{color:#18947b}.ant-page-header-back-button:active{color:#006154}.ant-page-header .ant-divider-vertical{height:14px;margin:0 12px;vertical-align:middle}.ant-breadcrumb+.ant-page-header-heading{margin-top:8px}.ant-page-header-heading{width:100%;overflow:hidden}.ant-page-header-heading-title{display:block;float:left;margin-bottom:0;padding-right:12px;color:rgba(0,0,0,.85);font-weight:600;font-size:20px;line-height:32px}.ant-page-header-heading .ant-avatar{float:left;margin-right:12px}.ant-page-header-heading-sub-title{float:left;margin:5px 12px 5px 0;color:rgba(0,0,0,.45);font-size:14px;line-height:22px}.ant-page-header-heading-tags{float:left;margin:4px 0}.ant-page-header-heading-extra{float:right}.ant-page-header-heading-extra>*{margin-left:8px}.ant-page-header-heading-extra>:first-child{margin-left:0}.ant-page-header-content{padding-top:12px;overflow:hidden}.ant-page-header-footer{margin-top:16px}.ant-page-header-footer .ant-tabs-bar{margin-bottom:1px;border-bottom:0}.ant-page-header-footer .ant-tabs-bar .ant-tabs-nav .ant-tabs-tab{padding:8px;font-size:16px}@media (max-width:576px){.ant-page-header-heading-extra{display:block;float:unset;width:100%;padding-top:12px;overflow:hidden}}.ant-popover{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:absolute;top:0;left:0;z-index:1030;font-weight:400;white-space:normal;text-align:left;cursor:auto;-webkit-user-select:text;-moz-user-select:text;user-select:text}.ant-popover:after{position:absolute;background:hsla(0,0%,100%,.01);content:""}.ant-popover-hidden{display:none}.ant-popover-placement-top,.ant-popover-placement-topLeft,.ant-popover-placement-topRight{padding-bottom:10px}.ant-popover-placement-right,.ant-popover-placement-rightBottom,.ant-popover-placement-rightTop{padding-left:10px}.ant-popover-placement-bottom,.ant-popover-placement-bottomLeft,.ant-popover-placement-bottomRight{padding-top:10px}.ant-popover-placement-left,.ant-popover-placement-leftBottom,.ant-popover-placement-leftTop{padding-right:10px}.ant-popover-inner{background-color:#fff;background-clip:padding-box;border-radius:1rem;box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 0 8px rgba(0,0,0,.15)\9}@media (-ms-high-contrast:none),screen and (-ms-high-contrast:active){.ant-popover-inner{box-shadow:0 2px 8px rgba(0,0,0,.15)}}.ant-popover-title{min-width:177px;min-height:32px;margin:0;padding:5px 16px 4px;color:rgba(0,0,0,.85);font-weight:500;border-bottom:1px solid #e8e8e8}.ant-popover-inner-content{padding:12px 16px;color:rgba(0,0,0,.65)}.ant-popover-message{position:relative;padding:4px 0 12px;color:rgba(0,0,0,.65);font-size:14px}.ant-popover-message>.anticon{position:absolute;top:8px;color:#faad14;font-size:14px}.ant-popover-message-title{padding-left:22px}.ant-popover-buttons{margin-bottom:4px;text-align:right}.ant-popover-buttons button{margin-left:8px}.ant-popover-arrow{position:absolute;display:block;width:8.48528137px;height:8.48528137px;background:transparent;border-style:solid;border-width:4.24264069px;transform:rotate(45deg)}.ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-topLeft>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-topRight>.ant-popover-content>.ant-popover-arrow{bottom:6.2px;border-color:transparent #fff #fff transparent;box-shadow:3px 3px 7px rgba(0,0,0,.07)}.ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow{left:50%;transform:translateX(-50%) rotate(45deg)}.ant-popover-placement-topLeft>.ant-popover-content>.ant-popover-arrow{left:16px}.ant-popover-placement-topRight>.ant-popover-content>.ant-popover-arrow{right:16px}.ant-popover-placement-right>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-rightBottom>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-rightTop>.ant-popover-content>.ant-popover-arrow{left:6px;border-color:transparent transparent #fff #fff;box-shadow:-3px 3px 7px rgba(0,0,0,.07)}.ant-popover-placement-right>.ant-popover-content>.ant-popover-arrow{top:50%;transform:translateY(-50%) rotate(45deg)}.ant-popover-placement-rightTop>.ant-popover-content>.ant-popover-arrow{top:12px}.ant-popover-placement-rightBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}.ant-popover-placement-bottom>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-bottomLeft>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-bottomRight>.ant-popover-content>.ant-popover-arrow{top:6px;border-color:#fff transparent transparent #fff;box-shadow:-2px -2px 5px rgba(0,0,0,.06)}.ant-popover-placement-bottom>.ant-popover-content>.ant-popover-arrow{left:50%;transform:translateX(-50%) rotate(45deg)}.ant-popover-placement-bottomLeft>.ant-popover-content>.ant-popover-arrow{left:16px}.ant-popover-placement-bottomRight>.ant-popover-content>.ant-popover-arrow{right:16px}.ant-popover-placement-left>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{right:6px;border-color:#fff #fff transparent transparent;box-shadow:3px -3px 7px rgba(0,0,0,.07)}.ant-popover-placement-left>.ant-popover-content>.ant-popover-arrow{top:50%;transform:translateY(-50%) rotate(45deg)}.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px}.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}.ant-progress{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}.ant-progress-line{position:relative;width:100%;font-size:14px}.ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px}.ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0}.ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)}.ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#ededed;border-radius:100px}.ant-progress-circle-trail{stroke:#ededed}.ant-progress-circle-path{animation:ant-progress-appear .3s}.ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#1890ff}.ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s}.ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#008771}.ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal}.ant-progress-text .anticon{font-size:14px}.ant-progress-status-active .ant-progress-bg:before{position:absolute;top:0;right:0;bottom:0;left:0;background:#fff;border-radius:10px;opacity:0;animation:ant-progress-active 2.4s cubic-bezier(.23,1,.32,1) infinite;content:""}.ant-progress-status-exception .ant-progress-bg{background-color:#f5222d}.ant-progress-status-exception .ant-progress-text{color:#f5222d}.ant-progress-status-exception .ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#f5222d}.ant-progress-status-success .ant-progress-bg{background-color:#008771}.ant-progress-status-success .ant-progress-text{color:#008771}.ant-progress-status-success .ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#008771}.ant-progress-circle .ant-progress-inner{position:relative;line-height:1;background-color:transparent}.ant-progress-circle .ant-progress-text{position:absolute;top:50%;left:50%;width:100%;margin:0;padding:0;color:rgba(0,0,0,.65);line-height:1;white-space:normal;text-align:center;transform:translate(-50%,-50%)}.ant-progress-circle .ant-progress-text .anticon{font-size:1.16666667em}.ant-progress-circle.ant-progress-status-exception .ant-progress-text{color:#f5222d}.ant-progress-circle.ant-progress-status-success .ant-progress-text{color:#008771}@keyframes ant-progress-active{0%{width:0;opacity:.1}20%{width:0;opacity:.5}to{width:100%;opacity:0}}.ant-rate{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;font-feature-settings:"tnum";display:inline-block;margin:0;padding:0;color:#fadb14;font-size:20px;line-height:unset;list-style:none;outline:none}.ant-rate-disabled .ant-rate-star{cursor:default}.ant-rate-disabled .ant-rate-star:hover{transform:scale(1)}.ant-rate-star{position:relative;display:inline-block;margin:0;padding:0;color:inherit;cursor:pointer;transition:all .3s}.ant-rate-star:not(:last-child){margin-right:8px}.ant-rate-star>div:focus{outline:0}.ant-rate-star>div:focus,.ant-rate-star>div:hover{transform:scale(1.1)}.ant-rate-star-first,.ant-rate-star-second{color:#e8e8e8;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-rate-star-first .anticon,.ant-rate-star-second .anticon{vertical-align:middle}.ant-rate-star-first{position:absolute;top:0;left:0;width:50%;height:100%;overflow:hidden;opacity:0}.ant-rate-star-half .ant-rate-star-first,.ant-rate-star-half .ant-rate-star-second{opacity:1}.ant-rate-star-full .ant-rate-star-second,.ant-rate-star-half .ant-rate-star-first{color:inherit}.ant-rate-text{display:inline-block;margin-left:8px;font-size:14px}.ant-result{padding:48px 32px}.ant-result-success .ant-result-icon>.anticon{color:#008771}.ant-result-error .ant-result-icon>.anticon{color:#f5222d}.ant-result-info .ant-result-icon>.anticon{color:#1890ff}.ant-result-warning .ant-result-icon>.anticon{color:#faad14}.ant-result-image{width:250px;height:295px;margin:auto}.ant-result-icon{margin-bottom:24px;text-align:center}.ant-result-icon>.anticon{font-size:72px}.ant-result-title{color:rgba(0,0,0,.85);font-size:24px;line-height:1.8;text-align:center}.ant-result-subtitle{color:rgba(0,0,0,.45);font-size:14px;line-height:1.6;text-align:center}.ant-result-extra{margin-top:32px;text-align:center}.ant-result-extra>*{margin-right:8px}.ant-result-extra>:last-child{margin-right:0}.ant-result-content{margin-top:24px;padding:24px 40px;background-color:#fafafa}.ant-skeleton{display:table;width:100%}.ant-skeleton-header{display:table-cell;padding-right:16px;vertical-align:top}.ant-skeleton-header .ant-skeleton-avatar{display:inline-block;vertical-align:top;background:#f2f2f2;width:32px;height:32px;line-height:32px}.ant-skeleton-header .ant-skeleton-avatar.ant-skeleton-avatar-circle{border-radius:50%}.ant-skeleton-header .ant-skeleton-avatar-lg{width:40px;height:40px;line-height:40px}.ant-skeleton-header .ant-skeleton-avatar-lg.ant-skeleton-avatar-circle{border-radius:50%}.ant-skeleton-header .ant-skeleton-avatar-sm{width:24px;height:24px;line-height:24px}.ant-skeleton-header .ant-skeleton-avatar-sm.ant-skeleton-avatar-circle{border-radius:50%}.ant-skeleton-content{display:table-cell;width:100%;vertical-align:top}.ant-skeleton-content .ant-skeleton-title{width:100%;height:16px;margin-top:16px;background:#f2f2f2}.ant-skeleton-content .ant-skeleton-title+.ant-skeleton-paragraph{margin-top:24px}.ant-skeleton-content .ant-skeleton-paragraph{padding:0}.ant-skeleton-content .ant-skeleton-paragraph>li{width:100%;height:16px;list-style:none;background:#f2f2f2}.ant-skeleton-content .ant-skeleton-paragraph>li:last-child:not(:first-child):not(:nth-child(2)){width:61%}.ant-skeleton-content .ant-skeleton-paragraph>li+li{margin-top:16px}.ant-skeleton-with-avatar .ant-skeleton-content .ant-skeleton-title{margin-top:12px}.ant-skeleton-with-avatar .ant-skeleton-content .ant-skeleton-title+.ant-skeleton-paragraph{margin-top:28px}.ant-skeleton.ant-skeleton-active .ant-skeleton-avatar,.ant-skeleton.ant-skeleton-active .ant-skeleton-content .ant-skeleton-paragraph>li,.ant-skeleton.ant-skeleton-active .ant-skeleton-content .ant-skeleton-title{background:linear-gradient(90deg,#f2f2f2 25%,#e6e6e6 37%,#f2f2f2 63%);background-size:400% 100%;animation:ant-skeleton-loading 1.4s ease infinite}@keyframes ant-skeleton-loading{0%{background-position:100% 50%}to{background-position:0 50%}}.ant-slider{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;height:12px;margin:14px 6px 10px;padding:4px 0;cursor:pointer;touch-action:none}.ant-slider-vertical{width:12px;height:100%;margin:6px 10px;padding:0 4px}.ant-slider-vertical .ant-slider-rail{width:4px;height:100%}.ant-slider-vertical .ant-slider-track{width:4px}.ant-slider-vertical .ant-slider-handle{margin-top:-6px;margin-left:-5px}.ant-slider-vertical .ant-slider-mark{top:0;left:12px;width:18px;height:100%}.ant-slider-vertical .ant-slider-mark-text{left:4px;white-space:nowrap}.ant-slider-vertical .ant-slider-step{width:4px;height:100%}.ant-slider-vertical .ant-slider-dot{top:auto;left:2px;margin-bottom:-4px}.ant-slider-tooltip .ant-tooltip-inner{min-width:unset}.ant-slider-with-marks{margin-bottom:28px}.ant-slider-rail{width:100%;background-color:#f5f5f5;border-radius:2px}.ant-slider-rail,.ant-slider-track{position:absolute;height:4px;transition:background-color .3s}.ant-slider-track{background-color:#53ad95;border-radius:1rem}.ant-slider-handle{position:absolute;width:14px;height:14px;margin-top:-5px;background-color:#fff;border:2px solid #53ad95;border-radius:50%;box-shadow:0;cursor:pointer;transition:border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28)}.ant-slider-handle:focus{border-color:#339f8d;outline:none;box-shadow:0 0 0 5px rgba(0,135,113,.2)}.ant-slider-handle.ant-tooltip-open{border-color:#008771}.ant-slider:hover .ant-slider-rail{background-color:#e1e1e1}.ant-slider:hover .ant-slider-track{background-color:#33a187}.ant-slider:hover .ant-slider-handle:not(.ant-tooltip-open){border-color:#33a187}.ant-slider-mark{position:absolute;top:14px;left:0;width:100%;font-size:14px}.ant-slider-mark-text{position:absolute;display:inline-block;color:rgba(0,0,0,.45);text-align:center;word-break:keep-all;cursor:pointer}.ant-slider-mark-text-active{color:rgba(0,0,0,.65)}.ant-slider-step{position:absolute;width:100%;height:4px;background:transparent}.ant-slider-dot{position:absolute;top:-2px;width:8px;height:8px;background-color:#fff;border:2px solid #e8e8e8;border-radius:50%;cursor:pointer}.ant-slider-dot,.ant-slider-dot:first-child,.ant-slider-dot:last-child{margin-left:-4px}.ant-slider-dot-active{border-color:#80c3b8}.ant-slider-disabled{cursor:not-allowed}.ant-slider-disabled .ant-slider-track{background-color:rgba(0,0,0,.25)!important}.ant-slider-disabled .ant-slider-dot,.ant-slider-disabled .ant-slider-handle{background-color:#fff;border-color:rgba(0,0,0,.25)!important;box-shadow:none;cursor:not-allowed}.ant-slider-disabled .ant-slider-dot,.ant-slider-disabled .ant-slider-mark-text{cursor:not-allowed!important}.ant-space{display:inline-flex}.ant-space-vertical{flex-direction:column}.ant-space-align-center{align-items:center}.ant-space-align-start{align-items:flex-start}.ant-space-align-end{align-items:flex-end}.ant-space-align-baseline{align-items:baseline}.ant-statistic{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"}.ant-statistic-title{margin-bottom:4px;color:rgba(0,0,0,.45);font-size:14px}.ant-statistic-content{color:rgba(0,0,0,.85);font-size:24px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.ant-statistic-content-value-decimal{font-size:16px}.ant-statistic-content-prefix,.ant-statistic-content-suffix{display:inline-block}.ant-statistic-content-prefix{margin-right:4px}.ant-statistic-content-suffix{margin-left:4px;font-size:16px}.ant-steps{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:flex;width:100%;font-size:0}.ant-steps-item{position:relative;display:inline-block;flex:1;overflow:hidden;vertical-align:top}.ant-steps-item-container{outline:none}.ant-steps-item:last-child{flex:none}.ant-steps-item:last-child>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after,.ant-steps-item:last-child>.ant-steps-item-container>.ant-steps-item-tail{display:none}.ant-steps-item-content,.ant-steps-item-icon{display:inline-block;vertical-align:top}.ant-steps-item-icon{width:32px;height:32px;margin-right:8px;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;line-height:32px;text-align:center;border:1px solid rgba(0,0,0,.25);border-radius:32px;transition:background-color .3s,border-color .3s}.ant-steps-item-icon>.ant-steps-icon{position:relative;top:-1px;color:#008771;line-height:1}.ant-steps-item-tail{position:absolute;top:12px;left:0;width:100%;padding:0 10px}.ant-steps-item-tail:after{display:inline-block;width:100%;height:1px;background:#e8e8e8;border-radius:1px;transition:background .3s;content:""}.ant-steps-item-title{position:relative;display:inline-block;padding-right:16px;color:rgba(0,0,0,.65);font-size:16px;line-height:32px}.ant-steps-item-title:after{position:absolute;top:16px;left:100%;display:block;width:9999px;height:1px;background:#e8e8e8;content:""}.ant-steps-item-subtitle{display:inline;margin-left:8px;font-weight:400}.ant-steps-item-description,.ant-steps-item-subtitle{color:rgba(0,0,0,.45);font-size:14px}.ant-steps-item-wait .ant-steps-item-icon{background-color:#fff;border-color:rgba(0,0,0,.25)}.ant-steps-item-wait .ant-steps-item-icon>.ant-steps-icon{color:rgba(0,0,0,.25)}.ant-steps-item-wait .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:rgba(0,0,0,.25)}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:rgba(0,0,0,.45)}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#e8e8e8}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:rgba(0,0,0,.45)}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#e8e8e8}.ant-steps-item-process .ant-steps-item-icon{background-color:#fff;border-color:#008771}.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon{color:#008771}.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#008771}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:rgba(0,0,0,.85)}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#e8e8e8}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:rgba(0,0,0,.65)}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#e8e8e8}.ant-steps-item-process .ant-steps-item-icon{background:#008771}.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon{color:#fff}.ant-steps-item-process .ant-steps-item-title{font-weight:500}.ant-steps-item-finish .ant-steps-item-icon{background-color:#fff;border-color:#008771}.ant-steps-item-finish .ant-steps-item-icon>.ant-steps-icon{color:#008771}.ant-steps-item-finish .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#008771}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:rgba(0,0,0,.65)}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#008771}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:rgba(0,0,0,.45)}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#008771}.ant-steps-item-error .ant-steps-item-icon{background-color:#fff;border-color:#f5222d}.ant-steps-item-error .ant-steps-item-icon>.ant-steps-icon{color:#f5222d}.ant-steps-item-error .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#f5222d}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:#f5222d}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#e8e8e8}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:#f5222d}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#e8e8e8}.ant-steps-item.ant-steps-next-error .ant-steps-item-title:after{background:#f5222d}.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]{cursor:pointer}.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button] .ant-steps-item-description,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button] .ant-steps-item-icon .ant-steps-icon,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button] .ant-steps-item-title{transition:color .3s}.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]:hover .ant-steps-item-description,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]:hover .ant-steps-item-subtitle,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]:hover .ant-steps-item-title{color:#008771}.ant-steps .ant-steps-item:not(.ant-steps-item-active):not(.ant-steps-item-process)>.ant-steps-item-container[role=button]:hover .ant-steps-item-icon{border-color:#008771}.ant-steps .ant-steps-item:not(.ant-steps-item-active):not(.ant-steps-item-process)>.ant-steps-item-container[role=button]:hover .ant-steps-item-icon .ant-steps-icon{color:#008771}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item{margin-right:16px;white-space:nowrap}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item:last-child{margin-right:0}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item:last-child .ant-steps-item-title{padding-right:0}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item-tail{display:none}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item-description{max-width:140px;white-space:normal}.ant-steps-item-custom .ant-steps-item-icon{height:auto;background:none;border:0}.ant-steps-item-custom .ant-steps-item-icon>.ant-steps-icon{top:0;left:.5px;width:32px;height:32px;font-size:24px;line-height:32px}.ant-steps-item-custom.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon{color:#008771}.ant-steps:not(.ant-steps-vertical) .ant-steps-item-custom .ant-steps-item-icon{width:auto}.ant-steps-small.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item{margin-right:12px}.ant-steps-small.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item:last-child{margin-right:0}.ant-steps-small .ant-steps-item-icon{width:24px;height:24px;font-size:12px;line-height:24px;text-align:center;border-radius:24px}.ant-steps-small .ant-steps-item-title{padding-right:12px;font-size:14px;line-height:24px}.ant-steps-small .ant-steps-item-title:after{top:12px}.ant-steps-small .ant-steps-item-description{color:rgba(0,0,0,.45);font-size:14px}.ant-steps-small .ant-steps-item-tail{top:8px}.ant-steps-small .ant-steps-item-custom .ant-steps-item-icon{width:inherit;height:inherit;line-height:inherit;background:none;border:0;border-radius:0}.ant-steps-small .ant-steps-item-custom .ant-steps-item-icon>.ant-steps-icon{font-size:24px;line-height:24px;transform:none}.ant-steps-vertical{display:block}.ant-steps-vertical .ant-steps-item{display:block;overflow:visible}.ant-steps-vertical .ant-steps-item-icon{float:left;margin-right:16px}.ant-steps-vertical .ant-steps-item-content{display:block;min-height:48px;overflow:hidden}.ant-steps-vertical .ant-steps-item-title{line-height:32px}.ant-steps-vertical .ant-steps-item-description{padding-bottom:12px}.ant-steps-vertical>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{position:absolute;top:0;left:16px;width:1px;height:100%;padding:38px 0 6px}.ant-steps-vertical>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail:after{width:1px;height:100%}.ant-steps-vertical>.ant-steps-item:not(:last-child)>.ant-steps-item-container>.ant-steps-item-tail{display:block}.ant-steps-vertical>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{display:none}.ant-steps-vertical.ant-steps-small .ant-steps-item-container .ant-steps-item-tail{position:absolute;top:0;left:12px;padding:30px 0 6px}.ant-steps-vertical.ant-steps-small .ant-steps-item-container .ant-steps-item-title{line-height:24px}@media (max-width:480px){.ant-steps-horizontal.ant-steps-label-horizontal{display:block}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item{display:block;overflow:visible}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-icon{float:left;margin-right:16px}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-content{display:block;min-height:48px;overflow:hidden}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-title{line-height:32px}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-description{padding-bottom:12px}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{position:absolute;top:0;left:16px;width:1px;height:100%;padding:38px 0 6px}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail:after{width:1px;height:100%}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item:not(:last-child)>.ant-steps-item-container>.ant-steps-item-tail{display:block}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{display:none}.ant-steps-horizontal.ant-steps-label-horizontal.ant-steps-small .ant-steps-item-container .ant-steps-item-tail{position:absolute;top:0;left:12px;padding:30px 0 6px}.ant-steps-horizontal.ant-steps-label-horizontal.ant-steps-small .ant-steps-item-container .ant-steps-item-title{line-height:24px}}.ant-steps-label-vertical .ant-steps-item{overflow:visible}.ant-steps-label-vertical .ant-steps-item-tail{margin-left:58px;padding:3.5px 24px}.ant-steps-label-vertical .ant-steps-item-content{display:block;width:116px;margin-top:8px;text-align:center}.ant-steps-label-vertical .ant-steps-item-icon{display:inline-block;margin-left:42px}.ant-steps-label-vertical .ant-steps-item-title{padding-right:0}.ant-steps-label-vertical .ant-steps-item-title:after{display:none}.ant-steps-label-vertical .ant-steps-item-subtitle{display:block;margin-bottom:4px;margin-left:0;line-height:1.5}.ant-steps-label-vertical.ant-steps-small:not(.ant-steps-dot) .ant-steps-item-icon{margin-left:46px}.ant-steps-dot .ant-steps-item-title,.ant-steps-dot.ant-steps-small .ant-steps-item-title{line-height:1.5}.ant-steps-dot .ant-steps-item-tail,.ant-steps-dot.ant-steps-small .ant-steps-item-tail{top:2px;width:100%;margin:0 0 0 70px;padding:0}.ant-steps-dot .ant-steps-item-tail:after,.ant-steps-dot.ant-steps-small .ant-steps-item-tail:after{width:calc(100% - 20px);height:3px;margin-left:12px}.ant-steps-dot .ant-steps-item:first-child .ant-steps-icon-dot,.ant-steps-dot.ant-steps-small .ant-steps-item:first-child .ant-steps-icon-dot{left:2px}.ant-steps-dot .ant-steps-item-icon,.ant-steps-dot.ant-steps-small .ant-steps-item-icon{width:8px;height:8px;margin-left:67px;padding-right:0;line-height:8px;background:transparent;border:0}.ant-steps-dot .ant-steps-item-icon .ant-steps-icon-dot,.ant-steps-dot.ant-steps-small .ant-steps-item-icon .ant-steps-icon-dot{position:relative;float:left;width:100%;height:100%;border-radius:100px;transition:all .3s}.ant-steps-dot .ant-steps-item-icon .ant-steps-icon-dot:after,.ant-steps-dot.ant-steps-small .ant-steps-item-icon .ant-steps-icon-dot:after{position:absolute;top:-12px;left:-26px;width:60px;height:32px;background:rgba(0,0,0,.001);content:""}.ant-steps-dot .ant-steps-item-content,.ant-steps-dot.ant-steps-small .ant-steps-item-content{width:140px}.ant-steps-dot .ant-steps-item-process .ant-steps-item-icon,.ant-steps-dot.ant-steps-small .ant-steps-item-process .ant-steps-item-icon{width:10px;height:10px;line-height:10px}.ant-steps-dot .ant-steps-item-process .ant-steps-item-icon .ant-steps-icon-dot,.ant-steps-dot.ant-steps-small .ant-steps-item-process .ant-steps-item-icon .ant-steps-icon-dot{top:-1px}.ant-steps-vertical.ant-steps-dot .ant-steps-item-icon{margin-top:8px;margin-left:0}.ant-steps-vertical.ant-steps-dot .ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{top:2px;left:-9px;margin:0;padding:22px 0 4px}.ant-steps-vertical.ant-steps-dot .ant-steps-item:first-child .ant-steps-icon-dot{left:0}.ant-steps-vertical.ant-steps-dot .ant-steps-item-process .ant-steps-icon-dot{left:-2px}.ant-steps-navigation{padding-top:12px}.ant-steps-navigation.ant-steps-small .ant-steps-item-container{margin-left:-12px}.ant-steps-navigation .ant-steps-item{overflow:visible;text-align:center}.ant-steps-navigation .ant-steps-item-container{display:inline-block;height:100%;margin-left:-16px;padding-bottom:12px;text-align:left;transition:opacity .3s}.ant-steps-navigation .ant-steps-item-container .ant-steps-item-content{max-width:auto}.ant-steps-navigation .ant-steps-item-container .ant-steps-item-title{max-width:100%;padding-right:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-steps-navigation .ant-steps-item-container .ant-steps-item-title:after{display:none}.ant-steps-navigation .ant-steps-item:not(.ant-steps-item-active) .ant-steps-item-container[role=button]{cursor:pointer}.ant-steps-navigation .ant-steps-item:not(.ant-steps-item-active) .ant-steps-item-container[role=button]:hover{opacity:.85}.ant-steps-navigation .ant-steps-item:last-child{flex:1}.ant-steps-navigation .ant-steps-item:last-child:after{display:none}.ant-steps-navigation .ant-steps-item:after{position:absolute;top:50%;left:100%;display:inline-block;width:12px;height:12px;margin-top:-14px;margin-left:-2px;border:1px solid rgba(0,0,0,.25);border-bottom:none;border-left:none;transform:rotate(45deg);content:""}.ant-steps-navigation .ant-steps-item:before{position:absolute;bottom:0;left:50%;display:inline-block;width:0;height:3px;background-color:#008771;transition:width .3s,left .3s;transition-timing-function:ease-out;content:""}.ant-steps-navigation .ant-steps-item.ant-steps-item-active:before{left:0;width:100%}@media (max-width:480px){.ant-steps-navigation>.ant-steps-item{margin-right:0!important}.ant-steps-navigation>.ant-steps-item:before{display:none}.ant-steps-navigation>.ant-steps-item.ant-steps-item-active:before{top:0;right:0;left:unset;display:block;width:3px;height:calc(100% - 24px)}.ant-steps-navigation>.ant-steps-item:after{position:relative;top:-2px;left:50%;display:block;width:8px;height:8px;margin-bottom:8px;text-align:center;transform:rotate(135deg)}.ant-steps-navigation>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{visibility:hidden}}.ant-steps-flex-not-supported.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item{margin-left:-16px;padding-left:16px;background:#fff}.ant-steps-flex-not-supported.ant-steps-horizontal.ant-steps-label-horizontal.ant-steps-small .ant-steps-item{margin-left:-12px;padding-left:12px}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item:last-child{overflow:hidden}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item:last-child .ant-steps-icon-dot:after{right:-200px;width:200px}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item .ant-steps-icon-dot:after,.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item .ant-steps-icon-dot:before{position:absolute;top:0;left:-10px;width:10px;height:8px;background:#fff;content:""}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item .ant-steps-icon-dot:after{right:-10px;left:auto}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item-wait .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#ccc}.ant-switch{margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;display:inline-block;box-sizing:border-box;min-width:44px;height:22px;line-height:20px;vertical-align:middle;background-color:rgba(0,0,0,.25);border:1px solid transparent;border-radius:100px;cursor:pointer;transition:all .36s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-switch-inner{display:block;margin-right:6px;margin-left:24px;color:#fff;font-size:12px}.ant-switch-loading-icon,.ant-switch:after{position:absolute;top:1px;left:1px;width:18px;height:18px;background-color:#fff;border-radius:18px;cursor:pointer;transition:all .36s cubic-bezier(.78,.14,.15,.86);content:" "}.ant-switch:after{box-shadow:0 2px 4px 0 rgba(0,35,11,.2)}.ant-switch:not(.ant-switch-disabled):active:after,.ant-switch:not(.ant-switch-disabled):active:before{width:24px}.ant-switch-loading-icon{z-index:1;display:none;font-size:12px;background:transparent}.ant-switch-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-switch-loading .ant-switch-loading-icon{display:inline-block;color:rgba(0,0,0,.65)}.ant-switch-checked.ant-switch-loading .ant-switch-loading-icon{color:#008771}.ant-switch:focus{outline:0;box-shadow:0 0 0 2px rgba(0,135,113,.2)}.ant-switch:focus:hover{box-shadow:none}.ant-switch-small{min-width:28px;height:16px;line-height:14px}.ant-switch-small .ant-switch-inner{margin-right:3px;margin-left:18px;font-size:12px}.ant-switch-small:after{width:12px;height:12px}.ant-switch-small:active:after,.ant-switch-small:active:before{width:16px}.ant-switch-small .ant-switch-loading-icon{width:12px;height:12px}.ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px}.ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px}.ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;transform:scale(.66667)}.ant-switch-checked{background-color:#008771}.ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px}.ant-switch-checked:after{left:100%;margin-left:-1px;transform:translateX(-100%)}.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px}.ant-switch-disabled,.ant-switch-loading{cursor:not-allowed;opacity:.4}.ant-switch-disabled *,.ant-switch-disabled:after,.ant-switch-disabled:before,.ant-switch-loading *,.ant-switch-loading:after,.ant-switch-loading:before{cursor:not-allowed}@keyframes AntSwitchSmallLoadingCircle{0%{transform:rotate(0deg) scale(.66667);transform-origin:50% 50%}to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}}.ant-table-wrapper{zoom:1}.ant-table-wrapper:after,.ant-table-wrapper:before{display:table;content:""}.ant-table-wrapper:after{clear:both}.ant-table{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;clear:both}.ant-table-body{transition:opacity .3s}.ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important}.ant-table table{width:100%;text-align:left;border-radius:1rem 1rem 0 0;border-collapse:separate;border-spacing:0}.ant-table-layout-fixed table{table-layout:fixed}.ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:1px solid #e8e8e8;transition:background .3s ease}.ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center}.ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;transition:all .3s}.ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px}.ant-table-thead>tr>th .ant-table-filter-selected.anticon{color:#008771}.ant-table-thead>tr>th .ant-table-column-sorter{display:table-cell;vertical-align:middle}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner{height:1em;margin-top:.35em;margin-left:.57142857em;color:#bfbfbf;line-height:1em;text-align:center;transition:all .3s}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-down,.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-up{display:inline-block;font-size:12px;font-size:11px\9;transform:scale(.91666667) rotate(0deg);display:block;height:1em;line-height:1em;transition:all .3s}:root .ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-down,:root .ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-up{font-size:12px}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-down.on,.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-up.on{color:#008771}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full{margin-top:-.15em}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full .ant-table-column-sorter-down,.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full .ant-table-column-sorter-up{height:.5em;line-height:.5em}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full .ant-table-column-sorter-down{margin-top:.125em}.ant-table-thead>tr>th.ant-table-column-has-actions{position:relative;background-clip:padding-box;-webkit-background-clip:border-box}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters{padding-right:30px!important}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters .ant-table-filter-icon.ant-table-filter-open,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters .anticon-filter.ant-table-filter-open,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .ant-table-filter-icon:hover,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .anticon-filter:hover{color:rgba(0,0,0,.45);background:#e5e5e5}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .ant-table-filter-icon:active,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .anticon-filter:active{color:rgba(0,0,0,.65)}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters{cursor:pointer}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:hover,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:hover .ant-table-filter-icon,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:hover .anticon-filter{background:#f2f2f2}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:active .ant-table-column-sorter-down:not(.on),.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:active .ant-table-column-sorter-up:not(.on){color:rgba(0,0,0,.45)}.ant-table-thead>tr>th .ant-table-header-column{display:inline-block;max-width:100%;vertical-align:top}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters{display:table}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters>.ant-table-column-title{display:table-cell;vertical-align:middle}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters>:not(.ant-table-column-sorter){position:relative}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters:before{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;transition:all .3s;content:""}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters:hover:before{background:rgba(0,0,0,.04)}.ant-table-thead>tr>th.ant-table-column-has-sorters{-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-table-thead>tr:first-child>th:first-child{border-top-left-radius:1rem}.ant-table-thead>tr:first-child>th:last-child{border-top-right-radius:1rem}.ant-table-thead>tr:not(:last-child)>th[colspan]{border-bottom:0}.ant-table-tbody>tr>td{border-bottom:1px solid #e8e8e8;transition:background .3s}.ant-table-tbody>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td{background:#b3c7c0}.ant-table-tbody>tr.ant-table-row-selected>td.ant-table-column-sort,.ant-table-tbody>tr:hover.ant-table-row-selected>td,.ant-table-tbody>tr:hover.ant-table-row-selected>td.ant-table-column-sort,.ant-table-thead>tr.ant-table-row-selected>td.ant-table-column-sort,.ant-table-thead>tr:hover.ant-table-row-selected>td,.ant-table-thead>tr:hover.ant-table-row-selected>td.ant-table-column-sort{background:#fafafa}.ant-table-thead>tr:hover{background:none}.ant-table-footer{position:relative;padding:16px;color:rgba(0,0,0,.85);background:#fafafa;border-top:1px solid #e8e8e8;border-radius:0 0 1rem 1rem}.ant-table-footer:before{position:absolute;top:-1px;left:0;width:100%;height:1px;background:#fafafa;content:""}.ant-table.ant-table-bordered .ant-table-footer{border:1px solid #e8e8e8}.ant-table-title{position:relative;top:1px;padding:16px 0;border-radius:1rem 1rem 0 0}.ant-table.ant-table-bordered .ant-table-title{padding-right:16px;padding-left:16px;border:1px solid #e8e8e8}.ant-table-title+.ant-table-content{position:relative;border-radius:1rem 1rem 0 0}.ant-table-bordered .ant-table-title+.ant-table-content,.ant-table-bordered .ant-table-title+.ant-table-content .ant-table-thead>tr:first-child>th,.ant-table-bordered .ant-table-title+.ant-table-content table,.ant-table-without-column-header .ant-table-title+.ant-table-content,.ant-table-without-column-header table{border-radius:0}.ant-table-without-column-header.ant-table-bordered.ant-table-empty .ant-table-placeholder{border-top:1px solid #e8e8e8;border-radius:1rem}.ant-table-tbody>tr.ant-table-row-selected td{color:inherit;background:#fafafa}.ant-table-thead>tr>th.ant-table-column-sort{background:#f5f5f5}.ant-table-tbody>tr>td.ant-table-column-sort{background:rgba(0,0,0,.01)}.ant-table-tbody>tr>td,.ant-table-thead>tr>th{padding:16px;overflow-wrap:break-word}.ant-table-expand-icon-th,.ant-table-row-expand-icon-cell{width:50px;min-width:50px;text-align:center}.ant-table-header{overflow:hidden;background:#fafafa}.ant-table-header table{border-radius:1rem 1rem 0 0}.ant-table-loading{position:relative}.ant-table-loading .ant-table-body{background:#fff;opacity:.5}.ant-table-loading .ant-table-spin-holder{position:absolute;top:50%;left:50%;height:20px;margin-left:-30px;line-height:20px}.ant-table-loading .ant-table-with-pagination{margin-top:-20px}.ant-table-loading .ant-table-without-pagination{margin-top:10px}.ant-table-bordered .ant-table-body>table,.ant-table-bordered .ant-table-fixed-left table,.ant-table-bordered .ant-table-fixed-right table,.ant-table-bordered .ant-table-header>table{border:1px solid #e8e8e8;border-right:0;border-bottom:0}.ant-table-bordered.ant-table-empty .ant-table-placeholder{border-right:1px solid #e8e8e8;border-left:1px solid #e8e8e8}.ant-table-bordered.ant-table-fixed-header .ant-table-header>table{border-bottom:0}.ant-table-bordered.ant-table-fixed-header .ant-table-body>table{border-top-left-radius:0;border-top-right-radius:0}.ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0}.ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8}.ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8}.ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8;border-radius:0 0 1rem 1rem}.ant-table-pagination.ant-pagination{float:right;margin:16px 0}.ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:1rem;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:1rem 1rem 0 0;box-shadow:none}.ant-table-filter-dropdown .ant-dropdown-menu-item>label+span{padding-right:0}.ant-table-filter-dropdown .ant-dropdown-menu-sub{border-radius:1rem;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-table-filter-dropdown .ant-dropdown-menu .ant-dropdown-submenu-contain-selected .ant-dropdown-menu-submenu-title:after{color:#008771;font-weight:700;text-shadow:0 0 2px #77baa6}.ant-table-filter-dropdown .ant-dropdown-menu-item{overflow:hidden}.ant-table-filter-dropdown>.ant-dropdown-menu>.ant-dropdown-menu-item:last-child,.ant-table-filter-dropdown>.ant-dropdown-menu>.ant-dropdown-menu-submenu:last-child .ant-dropdown-menu-submenu-title{border-radius:0}.ant-table-filter-dropdown-btns{padding:7px 8px;overflow:hidden;border-top:1px solid #e8e8e8}.ant-table-filter-dropdown-link{color:#008771}.ant-table-filter-dropdown-link:hover{color:#18947b}.ant-table-filter-dropdown-link:active{color:#006154}.ant-table-filter-dropdown-link.confirm{float:left}.ant-table-filter-dropdown-link.clear{float:right}.ant-table-selection{white-space:nowrap}.ant-table-selection-select-all-custom{margin-right:4px!important}.ant-table-selection .anticon-down{color:#bfbfbf;transition:all .3s}.ant-table-selection-menu{min-width:96px;margin-top:5px;margin-left:-30px;background:#fff;border-radius:1rem;box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-table-selection-menu .ant-action-down{color:#bfbfbf}.ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer}.ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)}.ant-table-row-expand-icon{color:#008771;text-decoration:none;cursor:pointer;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:none;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:#18947b}.ant-table-row-expand-icon:active{color:#006154}.ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor}.ant-table-row-expanded:after{content:"-"}.ant-table-row-collapsed:after{content:"+"}.ant-table-row-spaced{visibility:hidden}.ant-table-row-spaced:after{content:"."}.ant-table-row-cell-ellipsis,.ant-table-row-cell-ellipsis .ant-table-column-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-table-row-cell-ellipsis .ant-table-column-title{display:block}.ant-table-row-cell-break-word{word-wrap:break-word;word-break:break-word}tr.ant-table-expanded-row,tr.ant-table-expanded-row:hover{background:#fbfbfb}tr.ant-table-expanded-row td>.ant-table-wrapper{margin:-16px -16px -17px}.ant-table .ant-table-row-indent+.ant-table-row-expand-icon{margin-right:8px}.ant-table-scroll{overflow:auto;overflow-x:hidden}.ant-table-scroll table{min-width:100%}.ant-table-scroll table .ant-table-fixed-columns-in-body:not([colspan]){color:transparent}.ant-table-scroll table .ant-table-fixed-columns-in-body:not([colspan])>*{visibility:hidden}.ant-table-body-inner{height:100%}.ant-table-fixed-header>.ant-table-content>.ant-table-scroll>.ant-table-body{position:relative;background:#fff}.ant-table-fixed-header .ant-table-body-inner{overflow:scroll}.ant-table-fixed-header .ant-table-scroll .ant-table-header{margin-bottom:-20px;padding-bottom:20px;overflow:scroll;opacity:.9999}.ant-table-fixed-header .ant-table-scroll .ant-table-header::-webkit-scrollbar{border:solid #e8e8e8;border-width:0 0 1px}.ant-table-hide-scrollbar{scrollbar-color:transparent transparent;min-width:unset}.ant-table-hide-scrollbar::-webkit-scrollbar{min-width:inherit;background-color:transparent}.ant-table-bordered.ant-table-fixed-header .ant-table-scroll .ant-table-header::-webkit-scrollbar{border:1px solid #e8e8e8;border-left-width:0}.ant-table-bordered.ant-table-fixed-header .ant-table-scroll .ant-table-header.ant-table-hide-scrollbar .ant-table-thead>tr:only-child>th:last-child{border-right-color:transparent}.ant-table-fixed-left,.ant-table-fixed-right{position:absolute;top:0;z-index:1;overflow:hidden;border-radius:0;transition:box-shadow .3s ease}.ant-table-fixed-left table,.ant-table-fixed-right table{width:auto;background:#fff}.ant-table-fixed-header .ant-table-fixed-left .ant-table-body-outer .ant-table-fixed,.ant-table-fixed-header .ant-table-fixed-right .ant-table-body-outer .ant-table-fixed{border-radius:0}.ant-table-fixed-left{left:0;box-shadow:6px 0 6px -4px rgba(0,0,0,.15)}.ant-table-fixed-left .ant-table-header{overflow-y:hidden}.ant-table-fixed-left .ant-table-body-inner{margin-right:-20px;padding-right:20px}.ant-table-fixed-header .ant-table-fixed-left .ant-table-body-inner{padding-right:0}.ant-table-fixed-left,.ant-table-fixed-left table{border-radius:1rem 0 0 0}.ant-table-fixed-left .ant-table-thead>tr>th:last-child{border-top-right-radius:0}.ant-table-fixed-right{right:0;box-shadow:-6px 0 6px -4px rgba(0,0,0,.15)}.ant-table-fixed-right,.ant-table-fixed-right table{border-radius:0 1rem 0 0}.ant-table-fixed-right .ant-table-expanded-row{color:transparent;pointer-events:none}.ant-table-fixed-right .ant-table-thead>tr>th:first-child{border-top-left-radius:0}.ant-table.ant-table-scroll-position-left .ant-table-fixed-left,.ant-table.ant-table-scroll-position-right .ant-table-fixed-right{box-shadow:none}.ant-table colgroup>col.ant-table-selection-col{width:60px}.ant-table-thead>tr>th.ant-table-selection-column-custom .ant-table-selection{margin-right:-15px}.ant-table-tbody>tr>td.ant-table-selection-column,.ant-table-thead>tr>th.ant-table-selection-column{text-align:center}.ant-table-tbody>tr>td.ant-table-selection-column .ant-radio-wrapper,.ant-table-thead>tr>th.ant-table-selection-column .ant-radio-wrapper{margin-right:0}.ant-table-row[class*=ant-table-row-level-0] .ant-table-selection-column>span{display:inline-block}.ant-table-filter-dropdown-submenu .ant-checkbox-wrapper+span,.ant-table-filter-dropdown .ant-checkbox-wrapper+span{padding-left:8px}@supports (-moz-appearance:meterbar){.ant-table-thead>tr>th.ant-table-column-has-actions{background-clip:padding-box}}.ant-table-middle>.ant-table-content>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-footer,.ant-table-middle>.ant-table-content>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-title{padding:12px 8px}.ant-table-middle tr.ant-table-expanded-row td>.ant-table-wrapper{margin:-12px -8px -13px}.ant-table-small{border:1px solid #e8e8e8;border-radius:1rem}.ant-table-small>.ant-table-content>.ant-table-footer,.ant-table-small>.ant-table-title{padding:8px}.ant-table-small>.ant-table-title{top:0;border-bottom:1px solid #e8e8e8}.ant-table-small>.ant-table-content>.ant-table-footer{background-color:transparent;border-top:1px solid #e8e8e8}.ant-table-small>.ant-table-content>.ant-table-footer:before{background-color:transparent}.ant-table-small>.ant-table-content>.ant-table-body{margin:0 8px}.ant-table-small>.ant-table-content>.ant-table-body>table,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table{border:0}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th{padding:8px}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th{background-color:transparent}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr{border-bottom:1px solid #e8e8e8}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort{background-color:rgba(0,0,0,.01)}.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table{padding:0}.ant-table-small>.ant-table-content .ant-table-header{background-color:transparent;border-radius:1rem 1rem 0 0}.ant-table-small>.ant-table-content .ant-table-placeholder,.ant-table-small>.ant-table-content .ant-table-row:last-child td{border-bottom:0}.ant-table-small.ant-table-bordered{border-right:0}.ant-table-small.ant-table-bordered .ant-table-title{border:0;border-right:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-content{border-right:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-footer{border:0;border-top:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-footer:before{display:none}.ant-table-small.ant-table-bordered .ant-table-placeholder{border-right:0;border-bottom:0;border-left:0}.ant-table-small.ant-table-bordered .ant-table-tbody>tr>td:last-child,.ant-table-small.ant-table-bordered .ant-table-thead>tr>th.ant-table-row-cell-last{border-right:none}.ant-table-small.ant-table-bordered .ant-table-fixed-left .ant-table-tbody>tr>td:last-child,.ant-table-small.ant-table-bordered .ant-table-fixed-left .ant-table-thead>tr>th:last-child{border-right:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-fixed-right{border-right:1px solid #e8e8e8;border-left:1px solid #e8e8e8}.ant-table-small tr.ant-table-expanded-row td>.ant-table-wrapper{margin:-8px -8px -9px}.ant-table-small.ant-table-fixed-header>.ant-table-content>.ant-table-scroll>.ant-table-body{border-radius:0 0 1rem 1rem}.ant-timeline{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;font-feature-settings:"tnum";margin:0;padding:0;list-style:none}.ant-timeline-item{position:relative;margin:0;padding:0 0 20px;font-size:14px;list-style:none}.ant-timeline-item-tail{position:absolute;top:10px;left:4px;height:calc(100% - 10px);border-left:2px solid #e8e8e8}.ant-timeline-item-pending .ant-timeline-item-head{font-size:12px;background-color:transparent}.ant-timeline-item-pending .ant-timeline-item-tail{display:none}.ant-timeline-item-head{position:absolute;width:10px;height:10px;background-color:#fff;border:2px solid transparent;border-radius:100px}.ant-timeline-item-head-blue{color:#008771;border-color:#008771}.ant-timeline-item-head-red{color:#f5222d;border-color:#f5222d}.ant-timeline-item-head-green{color:#008771;border-color:#008771}.ant-timeline-item-head-gray{color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-timeline-item-head-custom{position:absolute;top:5.5px;left:5px;width:auto;height:auto;margin-top:0;padding:3px 1px;line-height:1;text-align:center;border:0;border-radius:0;transform:translate(-50%,-50%)}.ant-timeline-item-content{position:relative;top:-6px;margin:0 0 0 18px;word-break:break-word}.ant-timeline-item-last>.ant-timeline-item-tail{display:none}.ant-timeline-item-last>.ant-timeline-item-content{min-height:48px}.ant-timeline.ant-timeline-alternate .ant-timeline-item-head,.ant-timeline.ant-timeline-alternate .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-alternate .ant-timeline-item-tail,.ant-timeline.ant-timeline-right .ant-timeline-item-head,.ant-timeline.ant-timeline-right .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-right .ant-timeline-item-tail{left:50%}.ant-timeline.ant-timeline-alternate .ant-timeline-item-head,.ant-timeline.ant-timeline-right .ant-timeline-item-head{margin-left:-4px}.ant-timeline.ant-timeline-alternate .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-right .ant-timeline-item-head-custom{margin-left:1px}.ant-timeline.ant-timeline-alternate .ant-timeline-item-left .ant-timeline-item-content,.ant-timeline.ant-timeline-right .ant-timeline-item-left .ant-timeline-item-content{left:calc(50% - 4px);width:calc(50% - 14px);text-align:left}.ant-timeline.ant-timeline-alternate .ant-timeline-item-right .ant-timeline-item-content,.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-content{width:calc(50% - 12px);margin:0;text-align:right}.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-head,.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-tail{left:calc(100% - 6px)}.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-content{width:calc(100% - 18px)}.ant-timeline.ant-timeline-pending .ant-timeline-item-last .ant-timeline-item-tail{display:block;height:calc(100% - 14px);border-left:2px dotted #e8e8e8}.ant-timeline.ant-timeline-reverse .ant-timeline-item-last .ant-timeline-item-tail{display:none}.ant-timeline.ant-timeline-reverse .ant-timeline-item-pending .ant-timeline-item-tail{top:15px;display:block;height:calc(100% - 15px);border-left:2px dotted #e8e8e8}.ant-timeline.ant-timeline-reverse .ant-timeline-item-pending .ant-timeline-item-content{min-height:48px}.ant-transfer-customize-list{display:flex}.ant-transfer-customize-list .ant-transfer-operation{flex:none;align-self:center}.ant-transfer-customize-list .ant-transfer-list{flex:auto;width:auto;height:auto;min-height:200px}.ant-transfer-customize-list .ant-transfer-list-body-with-search{padding-top:0}.ant-transfer-customize-list .ant-transfer-list-body-search-wrapper{position:relative;padding-bottom:0}.ant-transfer-customize-list .ant-transfer-list-body-customize-wrapper{padding:12px}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small{border:0;border-radius:0}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th{background:#fafafa}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small>.ant-table-content .ant-table-row:last-child td{border-bottom:1px solid #e8e8e8}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small .ant-table-body{margin:0}.ant-transfer-customize-list .ant-table-wrapper .ant-table-pagination.ant-pagination{margin:16px 0 4px}.ant-transfer{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative}.ant-transfer-disabled .ant-transfer-list{background:#f5f5f5}.ant-transfer-list{position:relative;display:inline-block;width:180px;height:200px;padding-top:40px;vertical-align:middle;border:1px solid #d9d9d9;border-radius:1rem}.ant-transfer-list-with-footer{padding-bottom:34px}.ant-transfer-list-search{padding:0 24px 0 8px}.ant-transfer-list-search-action{position:absolute;top:12px;right:12px;bottom:12px;width:28px;color:rgba(0,0,0,.25);line-height:32px;text-align:center}.ant-transfer-list-search-action .anticon{color:rgba(0,0,0,.25);transition:all .3s}.ant-transfer-list-search-action .anticon:hover{color:rgba(0,0,0,.45)}span.ant-transfer-list-search-action{pointer-events:none}.ant-transfer-list-header{position:absolute;top:0;left:0;width:100%;padding:8px 12px 9px;overflow:hidden;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:1rem 1rem 0 0}.ant-transfer-list-header-title{position:absolute;right:12px}.ant-transfer-list-header .ant-checkbox-wrapper+span{padding-left:8px}.ant-transfer-list-body{position:relative;height:100%;font-size:14px}.ant-transfer-list-body-search-wrapper{position:absolute;top:0;left:0;width:100%;padding:12px}.ant-transfer-list-body-with-search{padding-top:56px}.ant-transfer-list-content{height:100%;margin:0;padding:0;overflow:auto;list-style:none}.ant-transfer-list-content>.LazyLoad{animation:transferHighlightIn 1s}.ant-transfer-list-content-item{min-height:32px;padding:6px 12px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;transition:all .3s}.ant-transfer-list-content-item>span{padding-right:0}.ant-transfer-list-content-item-text{padding-left:8px}.ant-transfer-list-content-item:not(.ant-transfer-list-content-item-disabled):hover{background-color:#b3c7c0;cursor:pointer}.ant-transfer-list-content-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-transfer-list-body-not-found{position:absolute;top:50%;width:100%;padding-top:0;color:rgba(0,0,0,.25);text-align:center;transform:translateY(-50%)}.ant-transfer-list-body-with-search .ant-transfer-list-body-not-found{margin-top:16px}.ant-transfer-list-footer{position:absolute;bottom:0;left:0;width:100%;border-top:1px solid #e8e8e8;border-radius:0 0 1rem 1rem}.ant-transfer-operation{display:inline-block;margin:0 8px;overflow:hidden;vertical-align:middle}.ant-transfer-operation .ant-btn{display:block}.ant-transfer-operation .ant-btn:first-child{margin-bottom:4px}.ant-transfer-operation .ant-btn .anticon{font-size:12px}@keyframes transferHighlightIn{0%{background:#77baa6}to{background:transparent}}.ant-select-tree-checkbox{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:none;cursor:pointer}.ant-select-tree-checkbox-input:focus+.ant-select-tree-checkbox-inner,.ant-select-tree-checkbox-wrapper:hover .ant-select-tree-checkbox-inner,.ant-select-tree-checkbox:hover .ant-select-tree-checkbox-inner{border-color:#008771}.ant-select-tree-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #008771;border-radius:2px;visibility:hidden;animation:antCheckboxEffect .36s ease-in-out;animation-fill-mode:backwards;content:""}.ant-select-tree-checkbox-wrapper:hover .ant-select-tree-checkbox:after,.ant-select-tree-checkbox:hover:after{visibility:visible}.ant-select-tree-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s}.ant-select-tree-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}.ant-select-tree-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}.ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}.ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner{background-color:#008771;border-color:#008771}.ant-select-tree-checkbox-disabled{cursor:not-allowed}.ant-select-tree-checkbox-disabled.ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner:after{border-color:rgba(0,0,0,.25);animation-name:none}.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-input{cursor:not-allowed}.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-inner{background-color:#f5f5f5;border-color:#d9d9d9!important}.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-inner:after{border-color:#f5f5f5;border-collapse:separate;animation-name:none}.ant-select-tree-checkbox-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-select-tree-checkbox-disabled:hover:after,.ant-select-tree-checkbox-wrapper:hover .ant-select-tree-checkbox-disabled:after{visibility:hidden}.ant-select-tree-checkbox-wrapper{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block;line-height:unset;cursor:pointer}.ant-select-tree-checkbox-wrapper.ant-select-tree-checkbox-wrapper-disabled{cursor:not-allowed}.ant-select-tree-checkbox-wrapper+.ant-select-tree-checkbox-wrapper{margin-left:8px}.ant-select-tree-checkbox+span{padding-right:8px;padding-left:8px}.ant-select-tree-checkbox-group{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}.ant-select-tree-checkbox-group-item{display:inline-block;margin-right:8px}.ant-select-tree-checkbox-group-item:last-child{margin-right:0}.ant-select-tree-checkbox-group-item+.ant-select-tree-checkbox-group-item{margin-left:0}.ant-select-tree-checkbox-indeterminate .ant-select-tree-checkbox-inner{background-color:#fff;border-color:#d9d9d9}.ant-select-tree-checkbox-indeterminate .ant-select-tree-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#008771;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}.ant-select-tree-checkbox-indeterminate.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-select-tree{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";margin:-4px 0 0;padding:0 4px}.ant-select-tree li{margin:8px 0;padding:0;white-space:nowrap;list-style:none;outline:0}.ant-select-tree li.filter-node>span{font-weight:500}.ant-select-tree li ul{margin:0;padding:0 0 0 18px}.ant-select-tree li .ant-select-tree-node-content-wrapper{display:inline-block;width:calc(100% - 24px);margin:0;padding:3px 5px;color:rgba(0,0,0,.65);text-decoration:none;border-radius:2px;cursor:pointer;transition:all .3s}.ant-select-tree li .ant-select-tree-node-content-wrapper:hover{background-color:#b3c7c0}.ant-select-tree li .ant-select-tree-node-content-wrapper.ant-select-tree-node-selected{background-color:#77baa6}.ant-select-tree li span.ant-select-tree-checkbox{margin:0 4px 0 0}.ant-select-tree li span.ant-select-tree-checkbox+.ant-select-tree-node-content-wrapper{width:calc(100% - 46px)}.ant-select-tree li span.ant-select-tree-iconEle,.ant-select-tree li span.ant-select-tree-switcher{display:inline-block;width:24px;height:24px;margin:0;line-height:22px;text-align:center;vertical-align:middle;border:0;outline:none;cursor:pointer}.ant-select-tree li span.ant-select-icon_loading .ant-select-switcher-loading-icon{position:absolute;left:0;display:inline-block;color:#008771;font-size:14px;transform:none}.ant-select-tree li span.ant-select-icon_loading .ant-select-switcher-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-select-tree li span.ant-select-tree-switcher{position:relative}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher-noop{cursor:auto}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-icon,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-icon,:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-tree-switcher-icon{font-size:12px}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-icon svg,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-tree-switcher-icon svg{transition:transform .3s}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon,:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-tree-switcher-icon{font-size:12px}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon svg,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-tree-switcher-icon svg{transition:transform .3s}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon svg{transform:rotate(-90deg)}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-loading-icon,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-loading-icon{position:absolute;left:0;display:inline-block;width:24px;height:24px;color:#008771;font-size:14px;transform:none}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-loading-icon svg,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-select-tree-child-tree,.ant-select-tree .ant-select-tree-treenode-loading .ant-select-tree-iconEle{display:none}.ant-select-tree-child-tree-open{display:block}li.ant-select-tree-treenode-disabled>.ant-select-tree-node-content-wrapper,li.ant-select-tree-treenode-disabled>.ant-select-tree-node-content-wrapper span,li.ant-select-tree-treenode-disabled>span:not(.ant-select-tree-switcher){color:rgba(0,0,0,.25);cursor:not-allowed}li.ant-select-tree-treenode-disabled>.ant-select-tree-node-content-wrapper:hover{background:transparent}.ant-select-tree-icon__close,.ant-select-tree-icon__open{margin-right:2px;vertical-align:top}.ant-select-tree-dropdown{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"}.ant-select-tree-dropdown .ant-select-dropdown-search{position:sticky;top:0;z-index:1;display:block;padding:4px;background:#fff}.ant-select-tree-dropdown .ant-select-dropdown-search .ant-select-search__field__wrap{width:100%}.ant-select-tree-dropdown .ant-select-dropdown-search .ant-select-search__field{box-sizing:border-box;width:100%;padding:4px 7px;border:1px solid #d9d9d9;border-radius:4px;outline:none}.ant-select-tree-dropdown .ant-select-dropdown-search.ant-select-search--hide{display:none}.ant-select-tree-dropdown .ant-select-not-found{display:block;padding:7px 16px;color:rgba(0,0,0,.25);cursor:not-allowed}@keyframes antCheckboxEffect{0%{transform:scale(1);opacity:.5}to{transform:scale(1.6);opacity:0}}.ant-tree.ant-tree-directory{position:relative}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-switcher,.ant-tree.ant-tree-directory>li span.ant-tree-switcher{position:relative;z-index:1}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-switcher.ant-tree-switcher-noop,.ant-tree.ant-tree-directory>li span.ant-tree-switcher.ant-tree-switcher-noop{pointer-events:none}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-checkbox,.ant-tree.ant-tree-directory>li span.ant-tree-checkbox{position:relative;z-index:1}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper{border-radius:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper:hover,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper:hover{background:transparent}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper:hover:before,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper:hover:before{background:#b3c7c0}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper.ant-tree-node-selected,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper.ant-tree-node-selected{color:#fff;background:transparent}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper:before,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper:before{position:absolute;right:0;left:0;height:24px;transition:all .3s;content:""}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper>span,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper>span{position:relative;z-index:1}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-switcher,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-switcher{color:#fff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox .ant-tree-checkbox-inner,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox .ant-tree-checkbox-inner{border-color:#008771}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked:after,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked:after{border-color:#fff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner{background:#fff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after{border-color:#008771}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-node-content-wrapper:before,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-node-content-wrapper:before{background:#008771}.ant-tree-checkbox{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:none;cursor:pointer}.ant-tree-checkbox-input:focus+.ant-tree-checkbox-inner,.ant-tree-checkbox-wrapper:hover .ant-tree-checkbox-inner,.ant-tree-checkbox:hover .ant-tree-checkbox-inner{border-color:#008771}.ant-tree-checkbox-checked:after{top:0;height:100%;border:1px solid #008771;border-radius:2px;visibility:hidden;animation:antCheckboxEffect .36s ease-in-out;animation-fill-mode:backwards;content:""}.ant-tree-checkbox-wrapper:hover .ant-tree-checkbox:after,.ant-tree-checkbox:hover:after{visibility:visible}.ant-tree-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s}.ant-tree-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}.ant-tree-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}.ant-tree-checkbox-checked .ant-tree-checkbox-inner{background-color:#008771;border-color:#008771}.ant-tree-checkbox-disabled{cursor:not-allowed}.ant-tree-checkbox-disabled.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after{border-color:rgba(0,0,0,.25);animation-name:none}.ant-tree-checkbox-disabled .ant-tree-checkbox-input{cursor:not-allowed}.ant-tree-checkbox-disabled .ant-tree-checkbox-inner{background-color:#f5f5f5;border-color:#d9d9d9!important}.ant-tree-checkbox-disabled .ant-tree-checkbox-inner:after{border-color:#f5f5f5;border-collapse:separate;animation-name:none}.ant-tree-checkbox-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-tree-checkbox-disabled:hover:after,.ant-tree-checkbox-wrapper:hover .ant-tree-checkbox-disabled:after{visibility:hidden}.ant-tree-checkbox-wrapper{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block;line-height:unset;cursor:pointer}.ant-tree-checkbox-wrapper.ant-tree-checkbox-wrapper-disabled{cursor:not-allowed}.ant-tree-checkbox-wrapper+.ant-tree-checkbox-wrapper{margin-left:8px}.ant-tree-checkbox+span{padding-right:8px;padding-left:8px}.ant-tree-checkbox-group{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}.ant-tree-checkbox-group-item{display:inline-block;margin-right:8px}.ant-tree-checkbox-group-item:last-child{margin-right:0}.ant-tree-checkbox-group-item+.ant-tree-checkbox-group-item{margin-left:0}.ant-tree-checkbox-indeterminate .ant-tree-checkbox-inner{background-color:#fff;border-color:#d9d9d9}.ant-tree-checkbox-indeterminate .ant-tree-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#008771;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}.ant-tree-checkbox-indeterminate.ant-tree-checkbox-disabled .ant-tree-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-tree{box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";margin:0;padding:0}.ant-tree-checkbox-checked:after{position:absolute;top:16.67%;left:0;width:100%;height:66.67%}.ant-tree ol,.ant-tree ul{margin:0;padding:0;list-style:none}.ant-tree li{margin:0;padding:4px 0;white-space:nowrap;list-style:none;outline:0}.ant-tree li span[draggable=true],.ant-tree li span[draggable]{line-height:20px;border-top:2px solid transparent;border-bottom:2px solid transparent;-webkit-user-select:none;-moz-user-select:none;user-select:none;-khtml-user-drag:element;-webkit-user-drag:element}.ant-tree li.drag-over>span[draggable]{color:#fff;background-color:#008771;opacity:.8}.ant-tree li.drag-over-gap-top>span[draggable]{border-top-color:#008771}.ant-tree li.drag-over-gap-bottom>span[draggable]{border-bottom-color:#008771}.ant-tree li.filter-node>span{color:#f5222d!important;font-weight:500!important}.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-loading-icon,.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-loading-icon{position:absolute;left:0;display:inline-block;width:24px;height:24px;color:#008771;font-size:14px;transform:none}.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-loading-icon svg,.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}:root .ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_close:after,:root .ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_open:after{opacity:0}.ant-tree li ul{margin:0;padding:0 0 0 18px}.ant-tree li .ant-tree-node-content-wrapper{display:inline-block;height:24px;margin:0;padding:0 5px;color:rgba(0,0,0,.65);line-height:24px;text-decoration:none;vertical-align:top;border-radius:2px;cursor:pointer;transition:all .3s}.ant-tree li .ant-tree-node-content-wrapper:hover{background-color:#b3c7c0}.ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected{background-color:#77baa6}.ant-tree li span.ant-tree-checkbox{top:auto;height:24px;margin:0 4px 0 2px;padding:4px 0}.ant-tree li span.ant-tree-iconEle,.ant-tree li span.ant-tree-switcher{display:inline-block;width:24px;height:24px;margin:0;line-height:24px;text-align:center;vertical-align:top;border:0;outline:none;cursor:pointer}.ant-tree li span.ant-tree-iconEle:empty{display:none}.ant-tree li span.ant-tree-switcher{position:relative}.ant-tree li span.ant-tree-switcher.ant-tree-switcher-noop{cursor:default}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon,:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon{font-size:12px}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon svg,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon svg{transition:transform .3s}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon,:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon{font-size:12px}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon svg,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon svg{transition:transform .3s}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon svg{transform:rotate(-90deg)}.ant-tree li:last-child>span.ant-tree-iconEle:before,.ant-tree li:last-child>span.ant-tree-switcher:before{display:none}.ant-tree>li:first-child{padding-top:7px}.ant-tree>li:last-child{padding-bottom:7px}.ant-tree-child-tree>li:first-child{padding-top:8px}.ant-tree-child-tree>li:last-child{padding-bottom:0}li.ant-tree-treenode-disabled>.ant-tree-node-content-wrapper,li.ant-tree-treenode-disabled>.ant-tree-node-content-wrapper span,li.ant-tree-treenode-disabled>span:not(.ant-tree-switcher){color:rgba(0,0,0,.25);cursor:not-allowed}li.ant-tree-treenode-disabled>.ant-tree-node-content-wrapper:hover{background:transparent}.ant-tree-icon__close,.ant-tree-icon__open{margin-right:2px;vertical-align:top}.ant-tree.ant-tree-show-line li{position:relative}.ant-tree.ant-tree-show-line li span.ant-tree-switcher{color:rgba(0,0,0,.45);background:#fff}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-select-switcher-icon,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-tree-switcher-icon{display:inline-block;font-weight:400;font-size:12px}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-select-switcher-icon svg,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-tree-switcher-icon svg{transition:transform .3s}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon{display:inline-block;font-weight:400;font-size:12px}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon svg,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon svg{transition:transform .3s}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon{display:inline-block;font-weight:400;font-size:12px}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon svg,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon svg{transition:transform .3s}.ant-tree.ant-tree-show-line li:not(:last-child):before{position:absolute;left:12px;width:1px;height:100%;height:calc(100% - 22px);margin:22px 0 0;border-left:1px solid #d9d9d9;content:" "}.ant-tree.ant-tree-icon-hide .ant-tree-treenode-loading .ant-tree-iconEle{display:none}.ant-tree.ant-tree-block-node li .ant-tree-node-content-wrapper{width:calc(100% - 24px)}.ant-tree.ant-tree-block-node li span.ant-tree-checkbox+.ant-tree-node-content-wrapper{width:calc(100% - 46px)}.ant-upload{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";outline:0}.ant-upload p{margin:0}.ant-upload-btn{display:block;width:100%;outline:none}.ant-upload input[type=file]{cursor:pointer}.ant-upload.ant-upload-select{display:inline-block}.ant-upload.ant-upload-disabled{cursor:not-allowed}.ant-upload.ant-upload-select-picture-card{display:table;float:left;width:104px;height:104px;margin-right:8px;margin-bottom:8px;text-align:center;vertical-align:top;background-color:#fafafa;border:1px dashed #d9d9d9;border-radius:1rem;cursor:pointer;transition:border-color .3s ease}.ant-upload.ant-upload-select-picture-card>.ant-upload{display:table-cell;width:100%;height:100%;padding:8px;text-align:center;vertical-align:middle}.ant-upload.ant-upload-select-picture-card:hover{border-color:#008771}.ant-upload.ant-upload-drag{position:relative;width:100%;height:100%;text-align:center;background:#fafafa;border:1px dashed #d9d9d9;border-radius:1rem;cursor:pointer;transition:border-color .3s}.ant-upload.ant-upload-drag .ant-upload{padding:16px 0}.ant-upload.ant-upload-drag.ant-upload-drag-hover:not(.ant-upload-disabled){border-color:#006154}.ant-upload.ant-upload-drag.ant-upload-disabled{cursor:not-allowed}.ant-upload.ant-upload-drag .ant-upload-btn{display:table;height:100%}.ant-upload.ant-upload-drag .ant-upload-drag-container{display:table-cell;vertical-align:middle}.ant-upload.ant-upload-drag:not(.ant-upload-disabled):hover{border-color:#18947b}.ant-upload.ant-upload-drag p.ant-upload-drag-icon{margin-bottom:20px}.ant-upload.ant-upload-drag p.ant-upload-drag-icon .anticon{color:#18947b;font-size:48px}.ant-upload.ant-upload-drag p.ant-upload-text{margin:0 0 4px;color:rgba(0,0,0,.85);font-size:16px}.ant-upload.ant-upload-drag p.ant-upload-hint{color:rgba(0,0,0,.45);font-size:14px}.ant-upload.ant-upload-drag .anticon-plus{color:rgba(0,0,0,.25);font-size:30px;transition:all .3s}.ant-upload.ant-upload-drag .anticon-plus:hover,.ant-upload.ant-upload-drag:hover .anticon-plus{color:rgba(0,0,0,.45)}.ant-upload-picture-card-wrapper{zoom:1;display:inline-block;width:100%}.ant-upload-picture-card-wrapper:after,.ant-upload-picture-card-wrapper:before{display:table;content:""}.ant-upload-picture-card-wrapper:after{clear:both}.ant-upload-list{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";zoom:1}.ant-upload-list:after,.ant-upload-list:before{display:table;content:""}.ant-upload-list:after{clear:both}.ant-upload-list-item-list-type-text:hover .ant-upload-list-item-name-icon-count-1{padding-right:14px}.ant-upload-list-item-list-type-text:hover .ant-upload-list-item-name-icon-count-2{padding-right:28px}.ant-upload-list-item{position:relative;height:22px;margin-top:8px;font-size:14px}.ant-upload-list-item-name{display:inline-block;width:100%;padding-left:22px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-upload-list-item-name-icon-count-1{padding-right:14px}.ant-upload-list-item-card-actions{position:absolute;right:0;opacity:0}.ant-upload-list-item-card-actions.picture{top:25px;line-height:1;opacity:1}.ant-upload-list-item-card-actions .anticon{padding-right:6px;color:rgba(0,0,0,.45)}.ant-upload-list-item-info{height:100%;padding:0 12px 0 4px;transition:background-color .3s}.ant-upload-list-item-info>span{display:block;width:100%;height:100%}.ant-upload-list-item-info .anticon-loading,.ant-upload-list-item-info .anticon-paper-clip{position:absolute;top:5px;color:rgba(0,0,0,.45);font-size:14px}.ant-upload-list-item .anticon-close{display:inline-block;font-size:12px;font-size:10px\9;transform:scale(.83333333) rotate(0deg);position:absolute;top:6px;right:4px;color:rgba(0,0,0,.45);line-height:0;cursor:pointer;opacity:0;transition:all .3s}:root .ant-upload-list-item .anticon-close{font-size:12px}.ant-upload-list-item .anticon-close:hover{color:rgba(0,0,0,.65)}.ant-upload-list-item:hover .ant-upload-list-item-info{background-color:#b3c7c0}.ant-upload-list-item:hover .ant-upload-list-item-card-actions,.ant-upload-list-item:hover .anticon-close{opacity:1}.ant-upload-list-item-error,.ant-upload-list-item-error .ant-upload-list-item-name,.ant-upload-list-item-error .anticon-paper-clip{color:#f5222d}.ant-upload-list-item-error .ant-upload-list-item-card-actions{opacity:1}.ant-upload-list-item-error .ant-upload-list-item-card-actions .anticon{color:#f5222d}.ant-upload-list-item-progress{position:absolute;bottom:-12px;width:100%;padding-left:26px;font-size:14px;line-height:0}.ant-upload-list-picture-card .ant-upload-list-item,.ant-upload-list-picture .ant-upload-list-item{position:relative;height:66px;padding:8px;border:1px solid #d9d9d9;border-radius:1rem}.ant-upload-list-picture-card .ant-upload-list-item:hover,.ant-upload-list-picture .ant-upload-list-item:hover{background:transparent}.ant-upload-list-picture-card .ant-upload-list-item-error,.ant-upload-list-picture .ant-upload-list-item-error{border-color:#f5222d}.ant-upload-list-picture-card .ant-upload-list-item-info,.ant-upload-list-picture .ant-upload-list-item-info{padding:0}.ant-upload-list-picture-card .ant-upload-list-item:hover .ant-upload-list-item-info,.ant-upload-list-picture .ant-upload-list-item:hover .ant-upload-list-item-info{background:transparent}.ant-upload-list-picture-card .ant-upload-list-item-uploading,.ant-upload-list-picture .ant-upload-list-item-uploading{border-style:dashed}.ant-upload-list-picture-card .ant-upload-list-item-thumbnail,.ant-upload-list-picture .ant-upload-list-item-thumbnail{position:absolute;top:8px;left:8px;width:48px;height:48px;font-size:26px;line-height:54px;text-align:center;opacity:.8}.ant-upload-list-picture-card .ant-upload-list-item-icon,.ant-upload-list-picture .ant-upload-list-item-icon{position:absolute;top:50%;left:50%;font-size:26px;transform:translate(-50%,-50%)}.ant-upload-list-picture-card .ant-upload-list-item-image,.ant-upload-list-picture .ant-upload-list-item-image{max-width:100%}.ant-upload-list-picture-card .ant-upload-list-item-thumbnail img,.ant-upload-list-picture .ant-upload-list-item-thumbnail img{display:block;width:48px;height:48px;overflow:hidden}.ant-upload-list-picture-card .ant-upload-list-item-name,.ant-upload-list-picture .ant-upload-list-item-name{display:inline-block;box-sizing:border-box;max-width:100%;margin:0 0 0 8px;padding-right:8px;padding-left:48px;overflow:hidden;line-height:44px;white-space:nowrap;text-overflow:ellipsis;transition:all .3s}.ant-upload-list-picture-card .ant-upload-list-item-name-icon-count-1,.ant-upload-list-picture .ant-upload-list-item-name-icon-count-1{padding-right:18px}.ant-upload-list-picture-card .ant-upload-list-item-name-icon-count-2,.ant-upload-list-picture .ant-upload-list-item-name-icon-count-2{padding-right:36px}.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-name,.ant-upload-list-picture .ant-upload-list-item-uploading .ant-upload-list-item-name{line-height:28px}.ant-upload-list-picture-card .ant-upload-list-item-progress,.ant-upload-list-picture .ant-upload-list-item-progress{bottom:14px;width:calc(100% - 24px);margin-top:0;padding-left:56px}.ant-upload-list-picture-card .anticon-close,.ant-upload-list-picture .anticon-close{position:absolute;top:8px;right:8px;line-height:1;opacity:1}.ant-upload-list-picture-card.ant-upload-list:after{display:none}.ant-upload-list-picture-card-container,.ant-upload-list-picture-card .ant-upload-list-item{float:left;width:104px;height:104px;margin:0 8px 8px 0}.ant-upload-list-picture-card .ant-upload-list-item-info{position:relative;height:100%;overflow:hidden}.ant-upload-list-picture-card .ant-upload-list-item-info:before{position:absolute;z-index:1;width:100%;height:100%;background-color:rgba(0,0,0,.5);opacity:0;transition:all .3s;content:" "}.ant-upload-list-picture-card .ant-upload-list-item:hover .ant-upload-list-item-info:before{opacity:1}.ant-upload-list-picture-card .ant-upload-list-item-actions{position:absolute;top:50%;left:50%;z-index:10;white-space:nowrap;transform:translate(-50%,-50%);opacity:0;transition:all .3s}.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-delete,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-download,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-eye-o{z-index:10;width:16px;margin:0 4px;color:hsla(0,0%,100%,.85);font-size:16px;cursor:pointer;transition:all .3s}.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-delete:hover,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-download:hover,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-eye-o:hover{color:#fff}.ant-upload-list-picture-card .ant-upload-list-item-actions:hover,.ant-upload-list-picture-card .ant-upload-list-item-info:hover+.ant-upload-list-item-actions{opacity:1}.ant-upload-list-picture-card .ant-upload-list-item-thumbnail,.ant-upload-list-picture-card .ant-upload-list-item-thumbnail img{position:static;display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.ant-upload-list-picture-card .ant-upload-list-item-name{display:none;margin:8px 0 0;padding:0;line-height:1.5;text-align:center}.ant-upload-list-picture-card .anticon-picture+.ant-upload-list-item-name{position:absolute;bottom:10px;display:block}.ant-upload-list-picture-card .ant-upload-list-item-uploading.ant-upload-list-item{background-color:#fafafa}.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info{height:auto}.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info .anticon-delete,.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info .anticon-eye-o,.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info:before{display:none}.ant-upload-list-picture-card .ant-upload-list-item-uploading-text{margin-top:18px;color:rgba(0,0,0,.45)}.ant-upload-list-picture-card .ant-upload-list-item-progress{bottom:32px;padding-left:0}.ant-upload-list .ant-upload-success-icon{color:#008771;font-weight:700}.ant-upload-list .ant-upload-animate-enter,.ant-upload-list .ant-upload-animate-inline-enter,.ant-upload-list .ant-upload-animate-inline-leave,.ant-upload-list .ant-upload-animate-leave{animation-duration:.3s;animation-fill-mode:cubic-bezier(.78,.14,.15,.86)}.ant-upload-list .ant-upload-animate-enter{animation-name:uploadAnimateIn}.ant-upload-list .ant-upload-animate-leave{animation-name:uploadAnimateOut}.ant-upload-list .ant-upload-animate-inline-enter{animation-name:uploadAnimateInlineIn}.ant-upload-list .ant-upload-animate-inline-leave{animation-name:uploadAnimateInlineOut}@keyframes uploadAnimateIn{0%{height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateOut{to{height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateInlineIn{0%{width:0;height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateInlineOut{to{width:0;height:0;margin:0;padding:0;opacity:0}} \ No newline at end of file diff --git a/web/assets/ant-design-vue/antd.min.js b/web/assets/ant-design-vue/antd.min.js new file mode 100644 index 0000000..af5586d --- /dev/null +++ b/web/assets/ant-design-vue/antd.min.js @@ -0,0 +1,3 @@ +/*! For license information please see antd.min.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("moment"),require("vue")):"function"==typeof define&&define.amd?define(["moment","vue"],t):"object"==typeof exports?exports.antd=t(require("moment"),require("vue")):e.antd=t(e.moment,e.Vue)}(window,(function(e,t){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var r=t[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(i,r,function(t){return e[t]}.bind(null,r));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=435)}([function(e,t,n){"use strict";var i=n(14),r=n.n(i),o=n(35),a=n.n(o),s=Object.prototype,c=s.toString,l=s.hasOwnProperty,u=/^\s*function (\w+)/,h=function(e){var t=null!=e?e.type?e.type:e:null,n=t&&t.toString().match(u);return n&&n[1]},d=function(e){if(null==e)return null;var t=e.constructor.toString().match(u);return t&&t[1]},f=Number.isInteger||function(e){return"number"==typeof e&&isFinite(e)&&Math.floor(e)===e},p=Array.isArray||function(e){return"[object Array]"===c.call(e)},v=function(e){return"[object Function]"===c.call(e)},m=function(e,t){var n;return Object.defineProperty(t,"_vueTypes_name",{enumerable:!1,writable:!1,value:e}),n=t,Object.defineProperty(n,"isRequired",{get:function(){return this.required=!0,this},enumerable:!1}),function(e){Object.defineProperty(e,"def",{value:function(e){return void 0===e&&void 0===this.default?(this.default=void 0,this):v(e)||g(this,e)?(this.default=p(e)||a()(e)?function(){return e}:e,this):(b(this._vueTypes_name+' - invalid default value: "'+e+'"',e),this)},enumerable:!1,writable:!1})}(t),v(t.validator)&&(t.validator=t.validator.bind(t)),t},g=function e(t,n){var i=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=t,o=!0,s=void 0;a()(t)||(r={type:t});var c=r._vueTypes_name?r._vueTypes_name+" - ":"";return l.call(r,"type")&&null!==r.type&&(p(r.type)?(o=r.type.some((function(t){return e(t,n,!0)})),s=r.type.map((function(e){return h(e)})).join(" or ")):o="Array"===(s=h(r))?p(n):"Object"===s?a()(n):"String"===s||"Number"===s||"Boolean"===s||"Function"===s?d(n)===s:n instanceof r.type),o?l.call(r,"validator")&&v(r.validator)?((o=r.validator(n))||!1!==i||b(c+"custom validation failed"),o):o:(!1===i&&b(c+'value "'+n+'" should be of type "'+s+'"'),!1)},b=function(){},y={get any(){return m("any",{type:null})},get func(){return m("function",{type:Function}).def(C.func)},get bool(){return m("boolean",{type:Boolean}).def(C.bool)},get string(){return m("string",{type:String}).def(C.string)},get number(){return m("number",{type:Number}).def(C.number)},get array(){return m("array",{type:Array}).def(C.array)},get object(){return m("object",{type:Object}).def(C.object)},get integer(){return m("integer",{type:Number,validator:function(e){return f(e)}}).def(C.integer)},get symbol(){return m("symbol",{type:null,validator:function(e){return"symbol"===(void 0===e?"undefined":r()(e))}})},custom:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"custom validation failed";if("function"!=typeof e)throw new TypeError("[VueTypes error]: You must provide a function as argument");return m(e.name||"<>",{validator:function(){var n=e.apply(void 0,arguments);return n||b(this._vueTypes_name+" - "+t),n}})},oneOf:function(e){if(!p(e))throw new TypeError("[VueTypes error]: You must provide an array as argument");var t='oneOf - value should be one of "'+e.join('", "')+'"',n=e.reduce((function(e,t){return null!=t&&-1===e.indexOf(t.constructor)&&e.push(t.constructor),e}),[]);return m("oneOf",{type:n.length>0?n:null,validator:function(n){var i=-1!==e.indexOf(n);return i||b(t),i}})},instanceOf:function(e){return m("instanceOf",{type:e})},oneOfType:function(e){if(!p(e))throw new TypeError("[VueTypes error]: You must provide an array as argument");var t=!1,n=e.reduce((function(e,n){if(a()(n)){if("oneOf"===n._vueTypes_name)return e.concat(n.type||[]);if(n.type&&!v(n.validator)){if(p(n.type))return e.concat(n.type);e.push(n.type)}else v(n.validator)&&(t=!0);return e}return e.push(n),e}),[]);if(!t)return m("oneOfType",{type:n}).def(void 0);var i=e.map((function(e){return e&&p(e.type)?e.type.map(h):h(e)})).reduce((function(e,t){return e.concat(p(t)?t:[t])}),[]).join('", "');return this.custom((function(t){var n=e.some((function(e){return"oneOf"===e._vueTypes_name?!e.type||g(e.type,t,!0):g(e,t,!0)}));return n||b('oneOfType - value type should be one of "'+i+'"'),n})).def(void 0)},arrayOf:function(e){return m("arrayOf",{type:Array,validator:function(t){var n=t.every((function(t){return g(e,t)}));return n||b('arrayOf - value must be an array of "'+h(e)+'"'),n}})},objectOf:function(e){return m("objectOf",{type:Object,validator:function(t){var n=Object.keys(t).every((function(n){return g(e,t[n])}));return n||b('objectOf - value must be an object of "'+h(e)+'"'),n}})},shape:function(e){var t=Object.keys(e),n=t.filter((function(t){return e[t]&&!0===e[t].required})),i=m("shape",{type:Object,validator:function(i){var r=this;if(!a()(i))return!1;var o=Object.keys(i);return n.length>0&&n.some((function(e){return-1===o.indexOf(e)}))?(b('shape - at least one of required properties "'+n.join('", "')+'" is not present'),!1):o.every((function(n){if(-1===t.indexOf(n))return!0===r._vueTypes_isLoose||(b('shape - object is missing "'+n+'" property'),!1);var o=e[n];return g(o,i[n])}))}});return Object.defineProperty(i,"_vueTypes_isLoose",{enumerable:!1,writable:!0,value:!1}),Object.defineProperty(i,"loose",{get:function(){return this._vueTypes_isLoose=!0,this},enumerable:!1}),i}},C={func:void 0,bool:void 0,string:void 0,number:void 0,array:void 0,object:void 0,integer:void 0};Object.defineProperty(y,"sensibleDefaults",{enumerable:!1,set:function(e){!1===e?C={}:!0===e?C={func:void 0,bool:void 0,string:void 0,number:void 0,array:void 0,object:void 0,integer:void 0}:a()(e)&&(C=e)},get:function(){return C}});t.a=y},function(e,t,n){"use strict";n.d(t,"i",(function(){return V})),n.d(t,"h",(function(){return T})),n.d(t,"k",(function(){return H})),n.d(t,"f",(function(){return j})),n.d(t,"q",(function(){return P})),n.d(t,"u",(function(){return L})),n.d(t,"v",(function(){return _})),n.d(t,"c",(function(){return $})),n.d(t,"x",(function(){return A})),n.d(t,"s",(function(){return m})),n.d(t,"l",(function(){return z})),n.d(t,"g",(function(){return w})),n.d(t,"o",(function(){return x})),n.d(t,"m",(function(){return O})),n.d(t,"j",(function(){return k})),n.d(t,"e",(function(){return M})),n.d(t,"r",(function(){return S})),n.d(t,"y",(function(){return v})),n.d(t,"t",(function(){return E})),n.d(t,"w",(function(){return F})),n.d(t,"a",(function(){return p})),n.d(t,"p",(function(){return b})),n.d(t,"n",(function(){return y})),n.d(t,"d",(function(){return C}));var i=n(14),r=n.n(i),o=n(22),a=n.n(o),s=n(2),c=n.n(s),l=n(35),u=n.n(l),h=n(5),d=n.n(h);var f=/-(\w)/g,p=function(e){return e.replace(f,(function(e,t){return t?t.toUpperCase():""}))},v=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments[1],n={},i=/;(?![^(]*\))/g,r=/:(.+)/;return e.split(i).forEach((function(e){if(e){var i=e.split(r);if(i.length>1){var o=t?p(i[0].trim()):i[0].trim();n[o]=i[1].trim()}}})),n},m=function(e,t){return t in((e.$options||{}).propsData||{})},g=function(e){return e.data&&e.data.scopedSlots||{}},b=function(e){var t=e.componentOptions||{};e.$vnode&&(t=e.$vnode.componentOptions||{});var n=e.children||t.children||[],i={};return n.forEach((function(e){if(!L(e)){var t=e.data&&e.data.slot||"default";i[t]=i[t]||[],i[t].push(e)}})),c()({},i,g(e))},y=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"default",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e.$scopedSlots&&e.$scopedSlots[t]&&e.$scopedSlots[t](n)||e.$slots[t]||[]},C=function(e){var t=e.componentOptions||{};return e.$vnode&&(t=e.$vnode.componentOptions||{}),e.children||t.children||[]},x=function(e){if(e.fnOptions)return e.fnOptions;var t=e.componentOptions;return e.$vnode&&(t=e.$vnode.componentOptions),t&&t.Ctor.options||{}},z=function(e){if(e.componentOptions){var t=e.componentOptions,n=t.propsData,i=void 0===n?{}:n,r=t.Ctor,o=((void 0===r?{}:r).options||{}).props||{},s={},l=!0,u=!1,h=void 0;try{for(var d,f=Object.entries(o)[Symbol.iterator]();!(l=(d=f.next()).done);l=!0){var p=d.value,v=a()(p,2),m=v[0],g=v[1],b=g.default;void 0!==b&&(s[m]="function"==typeof b&&"Function"!==(y=g.type,C=void 0,(C=y&&y.toString().match(/^\s*function (\w+)/))?C[1]:"")?b.call(e):b)}}catch(e){u=!0,h=e}finally{try{!l&&f.return&&f.return()}finally{if(u)throw h}}return c()({},s,i)}var y,C,x=e.$options,z=void 0===x?{}:x,w=e.$props;return function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n={};return Object.keys(e).forEach((function(i){(i in t||void 0!==e[i])&&(n[i]=e[i])})),n}(void 0===w?{}:w,z.propsData)},w=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:e,i=!(arguments.length>3&&void 0!==arguments[3])||arguments[3];if(e.$createElement){var r=e.$createElement,o=e[t];return void 0!==o?"function"==typeof o&&i?o(r,n):o:e.$scopedSlots[t]&&i&&e.$scopedSlots[t](n)||e.$scopedSlots[t]||e.$slots[t]||void 0}var a=e.context.$createElement,s=O(e)[t];if(void 0!==s)return"function"==typeof s&&i?s(a,n):s;var c=g(e)[t];if(void 0!==c)return"function"==typeof c&&i?c(a,n):c;var l=[],u=e.componentOptions||{};return(u.children||[]).forEach((function(e){e.data&&e.data.slot===t&&(e.data.attrs&&delete e.data.attrs.slot,"template"===e.tag?l.push(e.children):l.push(e))})),l.length?l:void 0},O=function(e){var t=e.componentOptions;return e.$vnode&&(t=e.$vnode.componentOptions),t&&t.propsData||{}},S=function(e,t){return O(e)[t]},M=function(e){var t=e.data;return e.$vnode&&(t=e.$vnode.data),t&&t.attrs||{}},k=function(e){var t=e.key;return e.$vnode&&(t=e.$vnode.key),t};function V(e){var t={};return e.componentOptions&&e.componentOptions.listeners?t=e.componentOptions.listeners:e.data&&e.data.on&&(t=e.data.on),c()({},t)}function T(e){var t={};return e.data&&e.data.on&&(t=e.data.on),c()({},t)}function H(e){return(e.$vnode?e.$vnode.componentOptions.listeners:e.$listeners)||{}}function j(e){var t={};e.data?t=e.data:e.$vnode&&e.$vnode.data&&(t=e.$vnode.data);var n=t.class||{},i=t.staticClass,r={};return i&&i.split(" ").forEach((function(e){r[e.trim()]=!0})),"string"==typeof n?n.split(" ").forEach((function(e){r[e.trim()]=!0})):Array.isArray(n)?d()(n).split(" ").forEach((function(e){r[e.trim()]=!0})):r=c()({},r,n),r}function P(e,t){var n={};e.data?n=e.data:e.$vnode&&e.$vnode.data&&(n=e.$vnode.data);var i=n.style||n.staticStyle;if("string"==typeof i)i=v(i,t);else if(t&&i){var r={};return Object.keys(i).forEach((function(e){return r[p(e)]=i[e]})),r}return i}function L(e){return!(e.tag||e.text&&""!==e.text.trim())}function _(e){return!e.tag}function $(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];return e.filter((function(e){return!L(e)}))}var E=function(e,t){return Object.keys(t).forEach((function(n){if(!e[n])throw new Error("not have "+n+" prop");e[n].def&&(e[n]=e[n].def(t[n]))})),e};function A(){var e=[].slice.call(arguments,0),t={};return e.forEach((function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=!0,i=!1,r=void 0;try{for(var o,s=Object.entries(e)[Symbol.iterator]();!(n=(o=s.next()).done);n=!0){var l=o.value,h=a()(l,2),d=h[0],f=h[1];t[d]=t[d]||{},u()(f)?c()(t[d],f):t[d]=f}}catch(e){i=!0,r=e}finally{try{!n&&s.return&&s.return()}finally{if(i)throw r}}})),t}function F(e){return e&&"object"===(void 0===e?"undefined":r()(e))&&"componentOptions"in e&&"context"in e&&void 0!==e.tag}t.b=m},function(e,t,n){"use strict";t.__esModule=!0;var i,r=n(245),o=(i=r)&&i.__esModule?i:{default:i};t.default=o.default||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1],n="function"==typeof e?e(this.$data,this.$props):e;if(this.getDerivedStateFromProps){var i=this.getDerivedStateFromProps(Object(s.l)(this),a()({},this.$data,n));if(null===i)return;n=a()({},n,i||{})}a()(this.$data,n),this.$forceUpdate(),this.$nextTick((function(){t&&t()}))},__emit:function(){var e=[].slice.call(arguments,0),t=e[0],n=this.$listeners[t];if(e.length&&n)if(Array.isArray(n))for(var i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:v;if(e){var n=m.definitions.get(e);return n&&"function"==typeof n.icon&&(n=a()({},n,{icon:n.icon(t.primaryColor,t.secondaryColor)})),n}},setTwoToneColors:function(e){var t=e.primaryColor,n=e.secondaryColor;v.primaryColor=t,v.secondaryColor=n||Object(p.c)(t)},getTwoToneColors:function(){return a()({},v)},render:function(e){var t=this.$props,n=t.type,i=t.primaryColor,r=t.secondaryColor,o=void 0,s=v;if(i&&(s={primaryColor:i,secondaryColor:r||Object(p.c)(i)}),Object(p.d)(n))o=n;else if("string"==typeof n&&!(o=m.get(n,s)))return null;return o?(o&&"function"==typeof o.icon&&(o=a()({},o,{icon:o.icon(s.primaryColor,s.secondaryColor)})),Object(p.b)(e,o.icon,"svg-"+o.name,{attrs:{"data-icon":o.name,width:"1em",height:"1em",fill:"currentColor","aria-hidden":"true"},on:this.$listeners})):(Object(p.e)("type should be string or icon definiton, but got "+n),null)},install:function(e){e.component(m.name,m)}},g=m,b=n(0),y=n(12),C=n.n(y),x=n(1),z=new Set;var w=n(13),O={width:"1em",height:"1em",fill:"currentColor","aria-hidden":"true",focusable:"false"},S=/-fill$/,M=/-o$/,k=/-twotone$/;var V=n(26);function T(e){return g.setTwoToneColors({primaryColor:e})}var H=n(10);g.add.apply(g,u()(Object.keys(f).filter((function(e){return"default"!==e})).map((function(e){return f[e]})))),T("#1890ff");function j(e,t,n){var i,o=n.$props,s=n.$slots,l=Object(x.k)(n),u=o.type,h=o.component,f=o.viewBox,p=o.spin,v=o.theme,m=o.twoToneColor,b=o.rotate,y=o.tabIndex,C=Object(x.c)(s.default);C=0===C.length?void 0:C,Object(w.a)(Boolean(u||h||C),"Icon","Icon should have `type` prop or `component` prop or `children`.");var z=d()((i={},c()(i,"anticon",!0),c()(i,"anticon-"+u,!!u),i)),V=d()(c()({},"anticon-spin",!!p||"loading"===u)),T=b?{msTransform:"rotate("+b+"deg)",transform:"rotate("+b+"deg)"}:void 0,H={attrs:a()({},O,{viewBox:f}),class:V,style:T};f||delete H.attrs.viewBox;var j=y;void 0===j&&"click"in l&&(j=-1);var P={attrs:{"aria-label":u&&t.icon+": "+u,tabIndex:j},on:l,class:z,staticClass:""};return e("i",P,[function(){if(h)return e(h,H,[C]);if(C){Object(w.a)(Boolean(f)||1===C.length&&"use"===C[0].tag,"Icon","Make sure that you provide correct `viewBox` prop (default `0 0 1024 1024`) to the icon.");var t={attrs:a()({},O),class:V,style:T};return e("svg",r()([t,{attrs:{viewBox:f}}]),[C])}if("string"==typeof u){var n=u;if(v){var i=function(e){var t=null;return S.test(e)?t="filled":M.test(e)?t="outlined":k.test(e)&&(t="twoTone"),t}(u);Object(w.a)(!i||v===i,"Icon","The icon name '"+u+"' already specify a theme '"+i+"', the 'theme' prop '"+v+"' will be ignored.")}return n=function(e,t){var n=e;return"filled"===t?n+="-fill":"outlined"===t?n+="-o":"twoTone"===t?n+="-twotone":Object(w.a)(!1,"Icon","This icon '"+e+"' has unknown theme '"+t+"'"),n}(function(e){return e.replace(S,"").replace(M,"").replace(k,"")}(function(e){var t=e;switch(e){case"cross":t="close";break;case"interation":t="interaction";break;case"canlendar":t="calendar";break;case"colum-height":t="column-height"}return Object(w.a)(t===e,"Icon","Icon '"+e+"' was a typo and is now deprecated, please use '"+t+"' instead."),t}(n)),v||"outlined"),e(g,{attrs:{focusable:"false",type:n,primaryColor:m},class:V,style:T})}}()])}var P={name:"AIcon",props:{tabIndex:b.a.number,type:b.a.string,component:b.a.any,viewBox:b.a.any,spin:b.a.bool.def(!1),rotate:b.a.number,theme:b.a.oneOf(["filled","outlined","twoTone"]),twoToneColor:b.a.string,role:b.a.string},render:function(e){var t=this;return e(V.a,{attrs:{componentName:"Icon"},scopedSlots:{default:function(n){return j(e,n,t)}}})},createFromIconfontCN:function(e){var t=e.scriptUrl,n=e.extraCommonProps,i=void 0===n?{}:n;if("undefined"!=typeof document&&"undefined"!=typeof window&&"function"==typeof document.createElement&&"string"==typeof t&&t.length&&!z.has(t)){var r=document.createElement("script");r.setAttribute("src",t),r.setAttribute("data-namespace",t),z.add(t),document.body.appendChild(r)}return{functional:!0,name:"AIconfont",props:L.props,render:function(e,t){var n=t.props,r=t.slots,o=t.listeners,a=t.data,s=n.type,c=C()(n,["type"]),l=r().default,u=null;s&&(u=e("use",{attrs:{"xlink:href":"#"+s}})),l&&(u=l);var h=Object(x.x)(i,a,{props:c,on:o});return e(L,h,[u])}}},getTwoToneColor:function(){return g.getTwoToneColors().primaryColor}};P.setTwoToneColor=T,P.install=function(e){e.use(H.a),e.component(P.name,P)};var L=t.a=P},function(e,t,n){"use strict";n.d(t,"b",(function(){return h})),n.d(t,"a",(function(){return d}));var i=n(11),r=n.n(i),o=n(2),a=n.n(o),s=n(1),c=n(5),l=n.n(c);function u(e,t){var n=e.componentOptions,i=e.data,r={};n&&n.listeners&&(r=a()({},n.listeners));var o={};i&&i.on&&(o=a()({},i.on));var s=new e.constructor(e.tag,i?a()({},i,{on:o}):i,e.children,e.text,e.elm,e.context,n?a()({},n,{listeners:r}):n,e.asyncFactory);return s.ns=e.ns,s.isStatic=e.isStatic,s.key=e.key,s.isComment=e.isComment,s.fnContext=e.fnContext,s.fnOptions=e.fnOptions,s.fnScopeId=e.fnScopeId,s.isCloned=!0,t&&(e.children&&(s.children=h(e.children,!0)),n&&n.children&&(n.children=h(n.children,!0))),s}function h(e,t){for(var n=e.length,i=new Array(n),r=0;r1&&void 0!==arguments[1]?arguments[1]:{},n=arguments[2],i=e;if(Array.isArray(e)&&(i=Object(s.c)(e)[0]),!i)return null;var o=u(i,n),c=t.props,h=void 0===c?{}:c,d=t.key,f=t.on,p=void 0===f?{}:f,v=t.nativeOn,m=void 0===v?{}:v,g=t.children,b=t.directives,y=void 0===b?[]:b,C=o.data||{},x={},z={},w=t.attrs,O=void 0===w?{}:w,S=t.ref,M=t.domProps,k=void 0===M?{}:M,V=t.style,T=void 0===V?{}:V,H=t.class,j=void 0===H?{}:H,P=t.scopedSlots,L=void 0===P?{}:P;return z="string"==typeof C.style?Object(s.y)(C.style):a()({},C.style,z),z="string"==typeof T?a()({},z,Object(s.y)(z)):a()({},z,T),"string"==typeof C.class&&""!==C.class.trim()?C.class.split(" ").forEach((function(e){x[e.trim()]=!0})):Array.isArray(C.class)?l()(C.class).split(" ").forEach((function(e){x[e.trim()]=!0})):x=a()({},C.class,x),"string"==typeof j&&""!==j.trim()?j.split(" ").forEach((function(e){x[e.trim()]=!0})):x=a()({},x,j),o.data=a()({},C,{style:z,attrs:a()({},C.attrs,O),class:x,domProps:a()({},C.domProps,k),scopedSlots:a()({},C.scopedSlots,L),directives:[].concat(r()(C.directives||[]),r()(y))}),o.componentOptions?(o.componentOptions.propsData=o.componentOptions.propsData||{},o.componentOptions.listeners=o.componentOptions.listeners||{},o.componentOptions.propsData=a()({},o.componentOptions.propsData,h),o.componentOptions.listeners=a()({},o.componentOptions.listeners,p),g&&(o.componentOptions.children=g)):(g&&(o.children=g),o.data.on=a()({},o.data.on||{},p)),o.data.on=a()({},o.data.on||{},m),void 0!==d&&(o.key=d,o.data.key=d),"string"==typeof S&&(o.data.ref=S),o}},function(e,t,n){"use strict";var i=n(25),r=n.n(i),o=n(112),a=n(77);function s(e){return e.directive("ant-portal",{inserted:function(e,t){var n=t.value,i="function"==typeof n?n(e):n;i!==e.parentNode&&i.appendChild(e)},componentUpdated:function(e,t){var n=t.value,i="function"==typeof n?n(e):n;i!==e.parentNode&&i.appendChild(e)}})}var c={install:function(e){e.use(r.a,{name:"ant-ref"}),Object(o.a)(e),Object(a.a)(e),s(e)}},l={};l.install=function(e){l.Vue=e,e.use(c)};t.a=l},function(e,t,n){"use strict";t.__esModule=!0;var i,r=n(274),o=(i=r)&&i.__esModule?i:{default:i};t.default=function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t=0||Object.prototype.hasOwnProperty.call(e,i)&&(n[i]=e[i]);return n}},function(e,t,n){"use strict";var i={};function r(e,t){0}function o(e,t,n){t||i[n]||(e(!1,n),i[n]=!0)}var a=function(e,t){o(r,e,t)};t.a=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";a(e,"[antdv: "+t+"] "+n)}},function(e,t,n){"use strict";t.__esModule=!0;var i=a(n(251)),r=a(n(261)),o="function"==typeof r.default&&"symbol"==typeof i.default?function(e){return typeof e}:function(e){return e&&"function"==typeof r.default&&e.constructor===r.default&&e!==r.default.prototype?"symbol":typeof e};function a(e){return e&&e.__esModule?e:{default:e}}t.default="function"==typeof r.default&&"symbol"===o(i.default)?function(e){return void 0===e?"undefined":o(e)}:function(e){return e&&"function"==typeof r.default&&e.constructor===r.default&&e!==r.default.prototype?"symbol":void 0===e?"undefined":o(e)}},function(t,n){t.exports=e},function(e,t,n){"use strict";var i=n(2),r=n.n(i);t.a=function(e,t){for(var n=r()({},e),i=0;i=0&&n.splice(i,1),n}function y(e,t){var n=e.slice();return-1===n.indexOf(t)&&n.push(t),n}function C(e){return e.split("-")}function x(e,t){return e+"-"+t}function z(e){return Object(v.o)(e).isTreeNode}function w(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];return e.filter(z)}function O(e){var t=Object(v.l)(e)||{},n=t.disabled,i=t.disableCheckbox,r=t.checkable;return!(!n&&!i)||!1===r}function S(e,t){!function n(i,r,o){var a=i?i.componentOptions.children:e,s=i?x(o.pos,r):0,c=w(a);if(i){var l=i.key;l||null!=l||(l=s);var u={node:i,index:r,pos:s,key:l,parentPos:o.node?o.pos:null};t(u)}c.forEach((function(e,t){n(e,t,{node:i,pos:s})}))}(null)}function M(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1],n=e.map(t);return 1===n.length?n[0]:n}function k(e,t){var n=Object(v.l)(t),i=n.eventKey,r=n.pos,o=[];return S(e,(function(e){var t=e.key;o.push(t)})),o.push(i||r),o}function V(e,t){var n=e.clientY,i=t.$refs.selectHandle.getBoundingClientRect(),r=i.top,o=i.bottom,a=i.height,s=Math.max(.25*a,2);return n<=r+s?-1:n>=o-s?1:0}function T(e,t){if(e)return t.multiple?e.slice():e.length?[e[0]]:e}var H=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{props:Object(f.a)(e,["on","key","class","className","style"]),on:e.on||{},class:e.class||e.className,style:e.style,key:e.key}};function j(e,t,n){if(!t)return[];var i=(n||{}).processProps,r=void 0===i?H:i;return(Array.isArray(t)?t:[t]).map((function(t){var i=t.children,o=u()(t,["children"]),a=j(e,i,n);return e(p.a,r(o),[a])}))}function P(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.initWrapper,i=t.processEntity,r=t.onProcessFinished,o=new Map,a=new Map,s={posEntities:o,keyEntities:a};return n&&(s=n(s)||s),S(e,(function(e){var t=e.node,n=e.index,r=e.pos,c=e.key,l=e.parentPos,u={node:t,index:n,key:c,pos:r};o.set(r,u),a.set(c,u),u.parent=o.get(l),u.parent&&(u.parent.children=u.parent.children||[],u.parent.children.push(u)),i&&i(u,s)})),r&&r(s),s}function L(e){if(!e)return null;var t=void 0;if(Array.isArray(e))t={checkedKeys:e,halfCheckedKeys:void 0};else{if("object"!==(void 0===e?"undefined":c()(e)))return d()(!1,"`checkedKeys` is not an array or an object"),null;t={checkedKeys:e.checked||void 0,halfCheckedKeys:e.halfChecked||void 0}}return t}function _(e,t,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},r=new Map,o=new Map;function s(e){if(r.get(e)!==t){var i=n.get(e);if(i){var a=i.children,c=i.parent;if(!O(i.node)){var l=!0,u=!1;(a||[]).filter((function(e){return!O(e.node)})).forEach((function(e){var t=e.key,n=r.get(t),i=o.get(t);(n||i)&&(u=!0),n||(l=!1)})),t?r.set(e,l):r.set(e,!1),o.set(e,u),c&&s(c.key)}}}}function c(e){if(r.get(e)!==t){var i=n.get(e);if(i){var o=i.children;O(i.node)||(r.set(e,t),(o||[]).forEach((function(e){c(e.key)})))}}}function l(e){var i=n.get(e);if(i){var o=i.children,a=i.parent,l=i.node;r.set(e,t),O(l)||((o||[]).filter((function(e){return!O(e.node)})).forEach((function(e){c(e.key)})),a&&s(a.key))}else d()(!1,"'"+e+"' does not exist in the tree.")}(i.checkedKeys||[]).forEach((function(e){r.set(e,!0)})),(i.halfCheckedKeys||[]).forEach((function(e){o.set(e,!0)})),(e||[]).forEach((function(e){l(e)}));var u=[],h=[],f=!0,p=!1,v=void 0;try{for(var m,g=r[Symbol.iterator]();!(f=(m=g.next()).done);f=!0){var b=m.value,y=a()(b,2),C=y[0],x=y[1];x&&u.push(C)}}catch(e){p=!0,v=e}finally{try{!f&&g.return&&g.return()}finally{if(p)throw v}}var z=!0,w=!1,S=void 0;try{for(var M,k=o[Symbol.iterator]();!(z=(M=k.next()).done);z=!0){var V=M.value,T=a()(V,2),H=T[0],j=T[1];!r.get(H)&&j&&h.push(H)}}catch(e){w=!0,S=e}finally{try{!z&&k.return&&k.return()}finally{if(w)throw S}}return{checkedKeys:u,halfCheckedKeys:h}}function $(e,t){var n=new Map;return(e||[]).forEach((function(e){!function e(i){if(!n.get(i)){var r=t.get(i);if(r){n.set(i,!0);var o=r.parent,a=r.node,s=Object(v.l)(a);s&&s.disabled||o&&e(o.key)}}}(e)})),[].concat(r()(n.keys()))}},function(e,t,n){(function(t){for(var i=n(287),r="undefined"==typeof window?t:window,o=["moz","webkit"],a="AnimationFrame",s=r["request"+a],c=r["cancel"+a]||r["cancelRequest"+a],l=0;!s&&l1&&void 0!==arguments[1]?arguments[1]:{},n=t.beforeEnter,o=t.enter,a=t.afterEnter,s=t.leave,c=t.afterLeave,l=t.appear,u=void 0===l||l,h=t.tag,d=t.nativeOn,f={props:{appear:u,css:!1},on:{beforeEnter:n||r,enter:o||function(t,n){Object(i.a)(t,e+"-enter",n)},afterEnter:a||r,leave:s||function(t,n){Object(i.a)(t,e+"-leave",n)},afterLeave:c||r},nativeOn:d};return h&&(f.tag=h),f}},function(e,t,n){"use strict";var i=function(){};e.exports=i},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={install:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.name||"ref";e.directive(n,{bind:function(t,n,i){e.nextTick((function(){n.value(i.componentInstance||t,i.key)})),n.value(i.componentInstance||t,i.key)},update:function(e,t,i,r){if(r.data&&r.data.directives){var o=r.data.directives.find((function(e){return e.name===n}));if(o&&o.value!==t.value)return o&&o.value(null,r.key),void t.value(i.componentInstance||e,i.key)}i.componentInstance===r.componentInstance&&i.elm===r.elm||t.value(i.componentInstance||e,i.key)},unbind:function(e,t,n){t.value(null,n.key)}})}}},function(e,t,n){"use strict";var i=n(2),r=n.n(i),o=n(0),a=n(36);t.a={name:"LocaleReceiver",props:{componentName:o.a.string.def("global"),defaultLocale:o.a.oneOfType([o.a.object,o.a.func]),children:o.a.func},inject:{localeData:{default:function(){return{}}}},methods:{getLocale:function(){var e=this.componentName,t=this.defaultLocale||a.a[e||"global"],n=this.localeData.antLocale,i=e&&n?n[e]:{};return r()({},"function"==typeof t?t():t,i||{})},getLocaleCode:function(){var e=this.localeData.antLocale,t=e&&e.locale;return e&&e.exist&&!t?a.a.locale:t}},render:function(){var e=this.$scopedSlots,t=this.children||e.default,n=this.localeData.antLocale;return t(this.getLocale(),this.getLocaleCode(),n)}}},function(e,t){e.exports=function(e,t,n,i){var r=n?n.call(i,e,t):void 0;if(void 0!==r)return!!r;if(e===t)return!0;if("object"!=typeof e||!e||"object"!=typeof t||!t)return!1;var o=Object.keys(e),a=Object.keys(t);if(o.length!==a.length)return!1;for(var s=Object.prototype.hasOwnProperty.bind(t),c=0;c=0&&n.left>=0&&n.bottom>n.top&&n.right>n.left?n:null}function se(e){var t,n,i;if(ne.isWindow(e)||9===e.nodeType){var r=ne.getWindow(e);t={left:ne.getWindowScrollLeft(r),top:ne.getWindowScrollTop(r)},n=ne.viewportWidth(r),i=ne.viewportHeight(r)}else t=ne.offset(e),n=ne.outerWidth(e),i=ne.outerHeight(e);return t.width=n,t.height=i,t}function ce(e,t){var n=t.charAt(0),i=t.charAt(1),r=e.width,o=e.height,a=e.left,s=e.top;return"c"===n?s+=o/2:"b"===n&&(s+=o),"c"===i?a+=r/2:"r"===i&&(a+=r),{left:a,top:s}}function le(e,t,n,i,r){var o=ce(t,n[1]),a=ce(e,n[0]),s=[a.left-o.left,a.top-o.top];return{left:Math.round(e.left-s[0]+i[0]-r[0]),top:Math.round(e.top-s[1]+i[1]-r[1])}}function ue(e,t,n){return e.leftn.right}function he(e,t,n){return e.topn.bottom}function de(e,t,n){var i=[];return ne.each(e,(function(e){i.push(e.replace(t,(function(e){return n[e]})))})),i}function fe(e,t){return e[t]=-e[t],e}function pe(e,t){return(/%$/.test(e)?parseInt(e.substring(0,e.length-1),10)/100*t:parseInt(e,10))||0}function ve(e,t){e[0]=pe(e[0],t.width),e[1]=pe(e[1],t.height)}function me(e,t,n,i){var r=n.points,o=n.offset||[0,0],a=n.targetOffset||[0,0],s=n.overflow,c=n.source||e;o=[].concat(o),a=[].concat(a);var l={},u=0,h=ae(c,!(!(s=s||{})||!s.alwaysByViewport)),d=se(c);ve(o,d),ve(a,t);var f=le(d,t,r,o,a),p=ne.merge(d,f);if(h&&(s.adjustX||s.adjustY)&&i){if(s.adjustX&&ue(f,d,h)){var v=de(r,/[lr]/gi,{l:"r",r:"l"}),m=fe(o,0),g=fe(a,0);(function(e,t,n){return e.left>n.right||e.left+t.widthn.bottom||e.top+t.height=n.left&&r.left+o.width>n.right&&(o.width-=r.left+o.width-n.right),i.adjustX&&r.left+o.width>n.right&&(r.left=Math.max(n.right-o.width,n.left)),i.adjustY&&r.top=n.top&&r.top+o.height>n.bottom&&(o.height-=r.top+o.height-n.bottom),i.adjustY&&r.top+o.height>n.bottom&&(r.top=Math.max(n.bottom-o.height,n.top)),ne.mix(r,o)}(f,d,h,l))}return p.width!==d.width&&ne.css(c,"width",ne.width(c)+p.width-d.width),p.height!==d.height&&ne.css(c,"height",ne.height(c)+p.height-d.height),ne.offset(c,{left:p.left,top:p.top},{useCssRight:n.useCssRight,useCssBottom:n.useCssBottom,useCssTransform:n.useCssTransform,ignoreShake:n.ignoreShake}),{points:r,offset:o,targetOffset:a,overflow:l}}function ge(e,t,n){var i=n.target||t;return me(e,se(i),n,!function(e,t){var n=ae(e,t),i=se(e);return!n||i.left+i.width<=n.left||i.top+i.height<=n.top||i.left>=n.right||i.top>=n.bottom}(i,n.overflow&&n.overflow.alwaysByViewport))}ge.__getOffsetParent=re,ge.__getVisibleRectForElement=ae;function be(e){return e&&"object"===(void 0===e?"undefined":g()(e))&&e.window===e}function ye(e,t){var n=Math.floor(e),i=Math.floor(t);return Math.abs(n-i)<=1}var Ce=n(9),xe=n(114),ze=n.n(xe);function we(e){return"function"==typeof e&&e?e():null}function Oe(e){return"object"===(void 0===e?"undefined":g()(e))&&e?e:null}var Se={props:{childrenProps:u.a.object,align:u.a.object.isRequired,target:u.a.oneOfType([u.a.func,u.a.object]).def((function(){return window})),monitorBufferTime:u.a.number.def(50),monitorWindowResize:u.a.bool.def(!1),disabled:u.a.bool.def(!1)},data:function(){return this.aligned=!1,{}},mounted:function(){var e=this;this.$nextTick((function(){e.prevProps=o()({},e.$props);var t=e.$props;!e.aligned&&e.forceAlign(),!t.disabled&&t.monitorWindowResize&&e.startMonitorWindowResize()}))},updated:function(){var e=this;this.$nextTick((function(){var t,n,i=e.prevProps,r=e.$props,a=!1;if(!r.disabled){var s=e.$el,c=s?s.getBoundingClientRect():null;if(i.disabled)a=!0;else{var l=we(i.target),u=we(r.target),h=Oe(i.target),d=Oe(r.target);be(l)&&be(u)?a=!1:(l!==u||l&&!u&&d||h&&d&&u||d&&!((t=h)===(n=d)||t&&n&&("pageX"in n&&"pageY"in n?t.pageX===n.pageX&&t.pageY===n.pageY:"clientX"in n&&"clientY"in n&&t.clientX===n.clientX&&t.clientY===n.clientY)))&&(a=!0);var f=e.sourceRect||{};a||!s||ye(f.width,c.width)&&ye(f.height,c.height)||(a=!0)}e.sourceRect=c}a&&e.forceAlign(),r.monitorWindowResize&&!r.disabled?e.startMonitorWindowResize():e.stopMonitorWindowResize(),e.prevProps=o()({},e.$props,{align:ze()(e.$props.align)})}))},beforeDestroy:function(){this.stopMonitorWindowResize()},methods:{startMonitorWindowResize:function(){this.resizeHandler||(this.bufferMonitor=function(e,t){var n=void 0;function i(){n&&(clearTimeout(n),n=null)}function r(){i(),n=setTimeout(e,t)}return r.clear=i,r}(this.forceAlign,this.$props.monitorBufferTime),this.resizeHandler=Object(p.a)(window,"resize",this.bufferMonitor))},stopMonitorWindowResize:function(){this.resizeHandler&&(this.bufferMonitor.clear(),this.resizeHandler.remove(),this.resizeHandler=null)},forceAlign:function(){var e=this.$props,t=e.disabled,n=e.target,i=e.align;if(!t&&n){var r=this.$el,o=Object(d.k)(this),a=void 0,s=we(n),c=Oe(n),l=document.activeElement;s?a=ge(r,s,i):c&&(a=function(e,t,n){var i,r,o=ne.getDocument(e),a=o.defaultView||o.parentWindow,s=ne.getWindowScrollLeft(a),c=ne.getWindowScrollTop(a),l=ne.viewportWidth(a),u=ne.viewportHeight(a),h={left:i="pageX"in t?t.pageX:s+t.clientX,top:r="pageY"in t?t.pageY:c+t.clientY,width:0,height:0},d=i>=0&&i<=s+l&&r>=0&&r<=c+u,f=[n.points[0],"cc"];return me(e,h,y(y({},n),{},{points:f}),d)}(r,c,i)),function(e,t){e!==document.activeElement&&Object(h.a)(t,e)&&e.focus()}(l,r),this.aligned=!0,o.align&&o.align(r,a)}}},render:function(){var e=this.$props.childrenProps,t=Object(d.n)(this)[0];return t&&e?Object(Ce.a)(t,{props:e}):t}},Me=n(6),ke=n.n(Me),Ve={props:{visible:u.a.bool,hiddenClassName:u.a.string},render:function(){var e=arguments[0],t=this.$props,n=t.hiddenClassName,i=(t.visible,null);if(n||!this.$slots.default||this.$slots.default.length>1){var r="";i=e("div",{class:r},[this.$slots.default])}else i=this.$slots.default[0];return i}},Te={props:{hiddenClassName:u.a.string.def(""),prefixCls:u.a.string,visible:u.a.bool},render:function(){var e=arguments[0],t=this.$props,n=t.prefixCls,i=t.visible,r=t.hiddenClassName,o={on:Object(d.k)(this)};return e("div",ke()([o,{class:i?"":r}]),[e(Ve,{class:n+"-content",attrs:{visible:i}},[this.$slots.default])])}},He=n(46),je=n(4),Pe={name:"VCTriggerPopup",mixins:[je.a],props:{visible:u.a.bool,getClassNameFromAlign:u.a.func,getRootDomNode:u.a.func,align:u.a.any,destroyPopupOnHide:u.a.bool,prefixCls:u.a.string,getContainer:u.a.func,transitionName:u.a.string,animation:u.a.any,maskAnimation:u.a.string,maskTransitionName:u.a.string,mask:u.a.bool,zIndex:u.a.number,popupClassName:u.a.any,popupStyle:u.a.object.def((function(){return{}})),stretch:u.a.string,point:u.a.shape({pageX:u.a.number,pageY:u.a.number})},data:function(){return this.domEl=null,{stretchChecked:!1,targetWidth:void 0,targetHeight:void 0}},mounted:function(){var e=this;this.$nextTick((function(){e.rootNode=e.getPopupDomNode(),e.setStretchSize()}))},updated:function(){var e=this;this.$nextTick((function(){e.setStretchSize()}))},beforeDestroy:function(){this.$el.parentNode?this.$el.parentNode.removeChild(this.$el):this.$el.remove&&this.$el.remove()},methods:{onAlign:function(e,t){var n=this.$props.getClassNameFromAlign(t);this.currentAlignClassName!==n&&(this.currentAlignClassName=n,e.className=this.getClassName(n));var i=Object(d.k)(this);i.align&&i.align(e,t)},setStretchSize:function(){var e=this.$props,t=e.stretch,n=e.getRootDomNode,i=e.visible,r=this.$data,o=r.stretchChecked,a=r.targetHeight,s=r.targetWidth;if(t&&i){var c=n();if(c){var l=c.offsetHeight,u=c.offsetWidth;a===l&&s===u&&o||this.setState({stretchChecked:!0,targetHeight:l,targetWidth:u})}}else o&&this.setState({stretchChecked:!1})},getPopupDomNode:function(){return this.$refs.popupInstance?this.$refs.popupInstance.$el:null},getTargetElement:function(){return this.$props.getRootDomNode()},getAlignTarget:function(){var e=this.$props.point;return e||this.getTargetElement},getMaskTransitionName:function(){var e=this.$props,t=e.maskTransitionName,n=e.maskAnimation;return!t&&n&&(t=e.prefixCls+"-"+n),t},getTransitionName:function(){var e=this.$props,t=e.transitionName,n=e.animation;return t||("string"==typeof n?t=""+n:n&&n.props&&n.props.name&&(t=n.props.name)),t},getClassName:function(e){return this.$props.prefixCls+" "+this.$props.popupClassName+" "+e},getPopupElement:function(){var e=this,t=this.$createElement,n=this.$props,i=this.$slots,r=this.getTransitionName,a=this.$data,s=a.stretchChecked,c=a.targetHeight,l=a.targetWidth,u=n.align,h=n.visible,f=n.prefixCls,p=n.animation,v=n.popupStyle,m=n.getClassNameFromAlign,b=n.destroyPopupOnHide,y=n.stretch,C=this.getClassName(this.currentAlignClassName||m(u));h||(this.currentAlignClassName=null);var x={};y&&(-1!==y.indexOf("height")?x.height="number"==typeof c?c+"px":c:-1!==y.indexOf("minHeight")&&(x.minHeight="number"==typeof c?c+"px":c),-1!==y.indexOf("width")?x.width="number"==typeof l?l+"px":l:-1!==y.indexOf("minWidth")&&(x.minWidth="number"==typeof l?l+"px":l),s||setTimeout((function(){e.$refs.alignInstance&&e.$refs.alignInstance.forceAlign()}),0));var z={props:{prefixCls:f,visible:h},class:C,on:Object(d.k)(this),ref:"popupInstance",style:o()({},x,v,this.getZIndexStyle())},w={props:{appear:!0,css:!1}},O=r(),S=!!O,M={beforeEnter:function(){},enter:function(t,n){e.$nextTick((function(){e.$refs.alignInstance?e.$refs.alignInstance.$nextTick((function(){e.domEl=t,Object(He.a)(t,O+"-enter",n)})):n()}))},beforeLeave:function(){e.domEl=null},leave:function(e,t){Object(He.a)(e,O+"-leave",t)}};if("object"===(void 0===p?"undefined":g()(p))){S=!0;var k=p.on,V=void 0===k?{}:k,T=p.props,H=void 0===T?{}:T;w.props=o()({},w.props,H),w.on=o()({},M,V)}else w.on=M;return S||(w={}),t("transition",w,b?[h?t(Se,{attrs:{target:this.getAlignTarget(),monitorWindowResize:!0,align:u},key:"popup",ref:"alignInstance",on:{align:this.onAlign}},[t(Te,z,[i.default])]):null]:[t(Se,{directives:[{name:"show",value:h}],attrs:{target:this.getAlignTarget(),monitorWindowResize:!0,disabled:!h,align:u},key:"popup",ref:"alignInstance",on:{align:this.onAlign}},[t(Te,z,[i.default])])])},getZIndexStyle:function(){var e={},t=this.$props;return void 0!==t.zIndex&&(e.zIndex=t.zIndex),e},getMaskElement:function(){var e=this.$createElement,t=this.$props,n=null;if(t.mask){var i=this.getMaskTransitionName();n=e(Ve,{directives:[{name:"show",value:t.visible}],style:this.getZIndexStyle(),key:"mask",class:t.prefixCls+"-mask",attrs:{visible:t.visible}}),i&&(n=e("transition",{attrs:{appear:!0,name:i}},[n]))}return n}},render:function(){var e=arguments[0],t=this.getMaskElement,n=this.getPopupElement;return e("div",[t(),n()])}};function Le(e,t,n){return n?e[0]===t[0]:e[0]===t[0]&&e[1]===t[1]}function _e(){}var $e={props:{autoMount:u.a.bool.def(!0),autoDestroy:u.a.bool.def(!0),visible:u.a.bool,forceRender:u.a.bool.def(!1),parent:u.a.any,getComponent:u.a.func.isRequired,getContainer:u.a.func.isRequired,children:u.a.func.isRequired},mounted:function(){this.autoMount&&this.renderComponent()},updated:function(){this.autoMount&&this.renderComponent()},beforeDestroy:function(){this.autoDestroy&&this.removeContainer()},methods:{removeContainer:function(){this.container&&(this._component&&this._component.$destroy(),this.container.parentNode.removeChild(this.container),this.container=null,this._component=null)},renderComponent:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1],n=this.visible,i=this.forceRender,r=this.getContainer,o=this.parent,a=this;if(n||o._component||o.$refs._component||i){var s=this.componentEl;this.container||(this.container=r(),s=document.createElement("div"),this.componentEl=s,this.container.appendChild(s));var c={component:a.getComponent(e)};this._component?this._component.setComponent(c):this._component=new this.$root.constructor({el:s,parent:a,data:{_com:c},mounted:function(){this.$nextTick((function(){t&&t.call(a)}))},updated:function(){this.$nextTick((function(){t&&t.call(a)}))},methods:{setComponent:function(e){this.$data._com=e}},render:function(){return this.$data._com.component}})}}},render:function(){return this.children({renderComponent:this.renderComponent,removeContainer:this.removeContainer})}};s.a.use(l.a,{name:"ant-ref"});var Ee=["click","mousedown","touchstart","mouseenter","mouseleave","focus","blur","contextmenu"],Ae={name:"Trigger",mixins:[je.a],props:{action:u.a.oneOfType([u.a.string,u.a.arrayOf(u.a.string)]).def([]),showAction:u.a.any.def([]),hideAction:u.a.any.def([]),getPopupClassNameFromAlign:u.a.any.def((function(){return""})),afterPopupVisibleChange:u.a.func.def(_e),popup:u.a.any,popupStyle:u.a.object.def((function(){return{}})),prefixCls:u.a.string.def("rc-trigger-popup"),popupClassName:u.a.string.def(""),popupPlacement:u.a.string,builtinPlacements:u.a.object,popupTransitionName:u.a.oneOfType([u.a.string,u.a.object]),popupAnimation:u.a.any,mouseEnterDelay:u.a.number.def(0),mouseLeaveDelay:u.a.number.def(.1),zIndex:u.a.number,focusDelay:u.a.number.def(0),blurDelay:u.a.number.def(.15),getPopupContainer:u.a.func,getDocument:u.a.func.def((function(){return window.document})),forceRender:u.a.bool,destroyPopupOnHide:u.a.bool.def(!1),mask:u.a.bool.def(!1),maskClosable:u.a.bool.def(!0),popupAlign:u.a.object.def((function(){return{}})),popupVisible:u.a.bool,defaultPopupVisible:u.a.bool.def(!1),maskTransitionName:u.a.oneOfType([u.a.string,u.a.object]),maskAnimation:u.a.string,stretch:u.a.string,alignPoint:u.a.bool},provide:function(){return{vcTriggerContext:this}},inject:{vcTriggerContext:{default:function(){return{}}},savePopupRef:{default:function(){return _e}},dialogContext:{default:function(){return null}}},data:function(){var e=this,t=this.$props,n=void 0;return n=Object(d.s)(this,"popupVisible")?!!t.popupVisible:!!t.defaultPopupVisible,Ee.forEach((function(t){e["fire"+t]=function(n){e.fireEvents(t,n)}})),{prevPopupVisible:n,sPopupVisible:n,point:null}},watch:{popupVisible:function(e){void 0!==e&&(this.prevPopupVisible=this.sPopupVisible,this.sPopupVisible=e)}},deactivated:function(){this.setPopupVisible(!1)},mounted:function(){var e=this;this.$nextTick((function(){e.renderComponent(null),e.updatedCal()}))},updated:function(){var e=this;this.renderComponent(null,(function(){e.sPopupVisible!==e.prevPopupVisible&&e.afterPopupVisibleChange(e.sPopupVisible),e.prevPopupVisible=e.sPopupVisible})),this.$nextTick((function(){e.updatedCal()}))},beforeDestroy:function(){this.clearDelayTimer(),this.clearOutsideHandler(),clearTimeout(this.mouseDownTimeout)},methods:{updatedCal:function(){var e=this.$props;if(this.$data.sPopupVisible){var t=void 0;this.clickOutsideHandler||!this.isClickToHide()&&!this.isContextmenuToShow()||(t=e.getDocument(),this.clickOutsideHandler=Object(p.a)(t,"mousedown",this.onDocumentClick)),this.touchOutsideHandler||(t=t||e.getDocument(),this.touchOutsideHandler=Object(p.a)(t,"touchstart",this.onDocumentClick)),!this.contextmenuOutsideHandler1&&this.isContextmenuToShow()&&(t=t||e.getDocument(),this.contextmenuOutsideHandler1=Object(p.a)(t,"scroll",this.onContextmenuClose)),!this.contextmenuOutsideHandler2&&this.isContextmenuToShow()&&(this.contextmenuOutsideHandler2=Object(p.a)(window,"blur",this.onContextmenuClose))}else this.clearOutsideHandler()},onMouseenter:function(e){var t=this.$props.mouseEnterDelay;this.fireEvents("mouseenter",e),this.delaySetPopupVisible(!0,t,t?null:e)},onMouseMove:function(e){this.fireEvents("mousemove",e),this.setPoint(e)},onMouseleave:function(e){this.fireEvents("mouseleave",e),this.delaySetPopupVisible(!1,this.$props.mouseLeaveDelay)},onPopupMouseenter:function(){this.clearDelayTimer()},onPopupMouseleave:function(e){e&&e.relatedTarget&&!e.relatedTarget.setTimeout&&this._component&&this._component.getPopupDomNode&&Object(h.a)(this._component.getPopupDomNode(),e.relatedTarget)||this.delaySetPopupVisible(!1,this.$props.mouseLeaveDelay)},onFocus:function(e){this.fireEvents("focus",e),this.clearDelayTimer(),this.isFocusToShow()&&(this.focusTime=Date.now(),this.delaySetPopupVisible(!0,this.$props.focusDelay))},onMousedown:function(e){this.fireEvents("mousedown",e),this.preClickTime=Date.now()},onTouchstart:function(e){this.fireEvents("touchstart",e),this.preTouchTime=Date.now()},onBlur:function(e){Object(h.a)(e.target,e.relatedTarget||document.activeElement)||(this.fireEvents("blur",e),this.clearDelayTimer(),this.isBlurToHide()&&this.delaySetPopupVisible(!1,this.$props.blurDelay))},onContextmenu:function(e){e.preventDefault(),this.fireEvents("contextmenu",e),this.setPopupVisible(!0,e)},onContextmenuClose:function(){this.isContextmenuToShow()&&this.close()},onClick:function(e){if(this.fireEvents("click",e),this.focusTime){var t=void 0;if(this.preClickTime&&this.preTouchTime?t=Math.min(this.preClickTime,this.preTouchTime):this.preClickTime?t=this.preClickTime:this.preTouchTime&&(t=this.preTouchTime),Math.abs(t-this.focusTime)<20)return;this.focusTime=0}this.preClickTime=0,this.preTouchTime=0,this.isClickToShow()&&(this.isClickToHide()||this.isBlurToHide())&&e&&e.preventDefault&&e.preventDefault(),e&&e.domEvent&&e.domEvent.preventDefault();var n=!this.$data.sPopupVisible;(this.isClickToHide()&&!n||n&&this.isClickToShow())&&this.setPopupVisible(!this.$data.sPopupVisible,e)},onPopupMouseDown:function(){var e=this,t=this.vcTriggerContext,n=void 0===t?{}:t;this.hasPopupMouseDown=!0,clearTimeout(this.mouseDownTimeout),this.mouseDownTimeout=setTimeout((function(){e.hasPopupMouseDown=!1}),0),n.onPopupMouseDown&&n.onPopupMouseDown.apply(n,arguments)},onDocumentClick:function(e){if(!this.$props.mask||this.$props.maskClosable){var t=e.target,n=this.$el;Object(h.a)(n,t)||this.hasPopupMouseDown||this.close()}},getPopupDomNode:function(){return this._component&&this._component.getPopupDomNode?this._component.getPopupDomNode():null},getRootDomNode:function(){return this.$el},handleGetPopupClassFromAlign:function(e){var t=[],n=this.$props,i=n.popupPlacement,r=n.builtinPlacements,o=n.prefixCls,a=n.alignPoint,s=n.getPopupClassNameFromAlign;return i&&r&&t.push(function(e,t,n,i){var r=n.points;for(var o in e)if(e.hasOwnProperty(o)&&Le(e[o].points,r,i))return t+"-placement-"+o;return""}(r,o,e,a)),s&&t.push(s(e)),t.join(" ")},getPopupAlign:function(){var e=this.$props,t=e.popupPlacement,n=e.popupAlign,i=e.builtinPlacements;return t&&i?function(e,t,n){var i=e[t]||{};return o()({},i,n)}(i,t,n):n},savePopup:function(e){this._component=e,this.savePopupRef(e)},getComponent:function(){var e=this.$createElement,t={};this.isMouseEnterToShow()&&(t.mouseenter=this.onPopupMouseenter),this.isMouseLeaveToHide()&&(t.mouseleave=this.onPopupMouseleave),t.mousedown=this.onPopupMouseDown,t.touchstart=this.onPopupMouseDown;var n=this.handleGetPopupClassFromAlign,i=this.getRootDomNode,r=this.getContainer,a=this.$props,s=a.prefixCls,c=a.destroyPopupOnHide,l=a.popupClassName,u=a.action,h=a.popupAnimation,f=a.popupTransitionName,p=a.popupStyle,v=a.mask,m=a.maskAnimation,g=a.maskTransitionName,b=a.zIndex,y=a.stretch,C=a.alignPoint,x=this.$data,z=x.sPopupVisible,w=x.point,O={props:{prefixCls:s,destroyPopupOnHide:c,visible:z,point:C&&w,action:u,align:this.getPopupAlign(),animation:h,getClassNameFromAlign:n,stretch:y,getRootDomNode:i,mask:v,zIndex:b,transitionName:f,maskAnimation:m,maskTransitionName:g,getContainer:r,popupClassName:l,popupStyle:p},on:o()({align:Object(d.k)(this).popupAlign||_e},t),directives:[{name:"ant-ref",value:this.savePopup}]};return e(Pe,O,[Object(d.g)(this,"popup")])},getContainer:function(){var e=this.$props,t=this.dialogContext,n=document.createElement("div");return n.style.position="absolute",n.style.top="0",n.style.left="0",n.style.width="100%",(e.getPopupContainer?e.getPopupContainer(this.$el,t):e.getDocument().body).appendChild(n),this.popupContainer=n,n},setPopupVisible:function(e,t){var n=this.alignPoint,i=this.sPopupVisible;if(this.clearDelayTimer(),i!==e){Object(d.s)(this,"popupVisible")||this.setState({sPopupVisible:e,prevPopupVisible:i});var r=Object(d.k)(this);r.popupVisibleChange&&r.popupVisibleChange(e)}n&&t&&this.setPoint(t)},setPoint:function(e){this.$props.alignPoint&&e&&this.setState({point:{pageX:e.pageX,pageY:e.pageY}})},delaySetPopupVisible:function(e,t,n){var i=this,r=1e3*t;if(this.clearDelayTimer(),r){var o=n?{pageX:n.pageX,pageY:n.pageY}:null;this.delayTimer=Object(f.b)((function(){i.setPopupVisible(e,o),i.clearDelayTimer()}),r)}else this.setPopupVisible(e,n)},clearDelayTimer:function(){this.delayTimer&&(Object(f.a)(this.delayTimer),this.delayTimer=null)},clearOutsideHandler:function(){this.clickOutsideHandler&&(this.clickOutsideHandler.remove(),this.clickOutsideHandler=null),this.contextmenuOutsideHandler1&&(this.contextmenuOutsideHandler1.remove(),this.contextmenuOutsideHandler1=null),this.contextmenuOutsideHandler2&&(this.contextmenuOutsideHandler2.remove(),this.contextmenuOutsideHandler2=null),this.touchOutsideHandler&&(this.touchOutsideHandler.remove(),this.touchOutsideHandler=null)},createTwoChains:function(e){var t=function(){},n=Object(d.k)(this);return this.childOriginEvents[e]&&n[e]?this["fire"+e]:t=this.childOriginEvents[e]||n[e]||t},isClickToShow:function(){var e=this.$props,t=e.action,n=e.showAction;return-1!==t.indexOf("click")||-1!==n.indexOf("click")},isContextmenuToShow:function(){var e=this.$props,t=e.action,n=e.showAction;return-1!==t.indexOf("contextmenu")||-1!==n.indexOf("contextmenu")},isClickToHide:function(){var e=this.$props,t=e.action,n=e.hideAction;return-1!==t.indexOf("click")||-1!==n.indexOf("click")},isMouseEnterToShow:function(){var e=this.$props,t=e.action,n=e.showAction;return-1!==t.indexOf("hover")||-1!==n.indexOf("mouseenter")},isMouseLeaveToHide:function(){var e=this.$props,t=e.action,n=e.hideAction;return-1!==t.indexOf("hover")||-1!==n.indexOf("mouseleave")},isFocusToShow:function(){var e=this.$props,t=e.action,n=e.showAction;return-1!==t.indexOf("focus")||-1!==n.indexOf("focus")},isBlurToHide:function(){var e=this.$props,t=e.action,n=e.hideAction;return-1!==t.indexOf("focus")||-1!==n.indexOf("blur")},forcePopupAlign:function(){this.$data.sPopupVisible&&this._component&&this._component.$refs.alignInstance&&this._component.$refs.alignInstance.forceAlign()},fireEvents:function(e,t){this.childOriginEvents[e]&&this.childOriginEvents[e](t),this.__emit(e,t)},close:function(){this.setPopupVisible(!1)}},render:function(){var e=this,t=arguments[0],n=this.sPopupVisible,i=Object(d.c)(this.$slots.default),r=this.$props,o=r.forceRender,a=r.alignPoint;i.length>1&&Object(v.a)(!1,"Trigger $slots.default.length > 1, just support only one default",!0);var s=i[0];this.childOriginEvents=Object(d.h)(s);var c={props:{},nativeOn:{},key:"trigger"};return this.isContextmenuToShow()?c.nativeOn.contextmenu=this.onContextmenu:c.nativeOn.contextmenu=this.createTwoChains("contextmenu"),this.isClickToHide()||this.isClickToShow()?(c.nativeOn.click=this.onClick,c.nativeOn.mousedown=this.onMousedown,c.nativeOn.touchstart=this.onTouchstart):(c.nativeOn.click=this.createTwoChains("click"),c.nativeOn.mousedown=this.createTwoChains("mousedown"),c.nativeOn.touchstart=this.createTwoChains("onTouchstart")),this.isMouseEnterToShow()?(c.nativeOn.mouseenter=this.onMouseenter,a&&(c.nativeOn.mousemove=this.onMouseMove)):c.nativeOn.mouseenter=this.createTwoChains("mouseenter"),this.isMouseLeaveToHide()?c.nativeOn.mouseleave=this.onMouseleave:c.nativeOn.mouseleave=this.createTwoChains("mouseleave"),this.isFocusToShow()||this.isBlurToHide()?(c.nativeOn.focus=this.onFocus,c.nativeOn.blur=this.onBlur):(c.nativeOn.focus=this.createTwoChains("focus"),c.nativeOn.blur=function(t){!t||t.relatedTarget&&Object(h.a)(t.target,t.relatedTarget)||e.createTwoChains("blur")(t)}),this.trigger=Object(Ce.a)(s,c),t($e,{attrs:{parent:this,visible:n,autoMount:!1,forceRender:o,getComponent:this.getComponent,getContainer:this.getContainer,children:function(t){var n=t.renderComponent;return e.renderComponent=n,e.trigger}}})}};t.a=Ae},function(e,t,n){var i=n(33),r=n(347),o=n(196),a=Math.max,s=Math.min;e.exports=function(e,t,n){var c,l,u,h,d,f,p=0,v=!1,m=!1,g=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function b(t){var n=c,i=l;return c=l=void 0,p=t,h=e.apply(i,n)}function y(e){return p=e,d=setTimeout(x,t),v?b(e):h}function C(e){var n=e-f;return void 0===f||n>=t||n<0||m&&e-p>=u}function x(){var e=r();if(C(e))return z(e);d=setTimeout(x,function(e){var n=t-(e-f);return m?s(n,u-(e-p)):n}(e))}function z(e){return d=void 0,g&&c?b(e):(c=l=void 0,h)}function w(){var e=r(),n=C(e);if(c=arguments,l=this,f=e,n){if(void 0===d)return y(f);if(m)return clearTimeout(d),d=setTimeout(x,t),b(f)}return void 0===d&&(d=setTimeout(x,t)),h}return t=o(t)||0,i(n)&&(v=!!n.leading,u=(m="maxWait"in n)?a(o(n.maxWait)||0,t):u,g="trailing"in n?!!n.trailing:g),w.cancel=function(){void 0!==d&&clearTimeout(d),p=0,c=f=l=d=void 0},w.flush=function(){return void 0===d?h:z(r())},w}},function(e,t,n){"use strict";var i=n(3),r=n.n(i),o=n(2),a=n.n(o),s=n(9),c=n(12),l=n.n(c),u=n(0),h=n(28),d={adjustX:1,adjustY:1},f=[0,0],p={left:{points:["cr","cl"],overflow:d,offset:[-4,0],targetOffset:f},right:{points:["cl","cr"],overflow:d,offset:[4,0],targetOffset:f},top:{points:["bc","tc"],overflow:d,offset:[0,-4],targetOffset:f},bottom:{points:["tc","bc"],overflow:d,offset:[0,4],targetOffset:f},topLeft:{points:["bl","tl"],overflow:d,offset:[0,-4],targetOffset:f},leftTop:{points:["tr","tl"],overflow:d,offset:[-4,0],targetOffset:f},topRight:{points:["br","tr"],overflow:d,offset:[0,-4],targetOffset:f},rightTop:{points:["tl","tr"],overflow:d,offset:[4,0],targetOffset:f},bottomRight:{points:["tr","br"],overflow:d,offset:[0,4],targetOffset:f},rightBottom:{points:["bl","br"],overflow:d,offset:[4,0],targetOffset:f},bottomLeft:{points:["tl","bl"],overflow:d,offset:[0,4],targetOffset:f},leftBottom:{points:["br","bl"],overflow:d,offset:[-4,0],targetOffset:f}},v={props:{prefixCls:u.a.string,overlay:u.a.any,trigger:u.a.any},updated:function(){var e=this.trigger;e&&e.forcePopupAlign()},render:function(){var e=arguments[0],t=this.overlay,n=this.prefixCls;return e("div",{class:n+"-inner",attrs:{role:"tooltip"}},["function"==typeof t?t():t])}},m=n(1);function g(){}var b={props:{trigger:u.a.any.def(["hover"]),defaultVisible:u.a.bool,visible:u.a.bool,placement:u.a.string.def("right"),transitionName:u.a.oneOfType([u.a.string,u.a.object]),animation:u.a.any,afterVisibleChange:u.a.func.def((function(){})),overlay:u.a.any,overlayStyle:u.a.object,overlayClassName:u.a.string,prefixCls:u.a.string.def("rc-tooltip"),mouseEnterDelay:u.a.number.def(0),mouseLeaveDelay:u.a.number.def(.1),getTooltipContainer:u.a.func,destroyTooltipOnHide:u.a.bool.def(!1),align:u.a.object.def((function(){return{}})),arrowContent:u.a.any.def(null),tipId:u.a.string,builtinPlacements:u.a.object},methods:{getPopupElement:function(){var e=this.$createElement,t=this.$props,n=t.prefixCls,i=t.tipId;return[e("div",{class:n+"-arrow",key:"arrow"},[Object(m.g)(this,"arrowContent")]),e(v,{key:"content",attrs:{trigger:this.$refs.trigger,prefixCls:n,id:i,overlay:Object(m.g)(this,"overlay")}})]},getPopupDomNode:function(){return this.$refs.trigger.getPopupDomNode()}},render:function(e){var t=Object(m.l)(this),n=t.overlayClassName,i=t.trigger,r=t.mouseEnterDelay,o=t.mouseLeaveDelay,s=t.overlayStyle,c=t.prefixCls,u=t.afterVisibleChange,d=t.transitionName,f=t.animation,v=t.placement,b=t.align,y=t.destroyTooltipOnHide,C=t.defaultVisible,x=t.getTooltipContainer,z=l()(t,["overlayClassName","trigger","mouseEnterDelay","mouseLeaveDelay","overlayStyle","prefixCls","afterVisibleChange","transitionName","animation","placement","align","destroyTooltipOnHide","defaultVisible","getTooltipContainer"]),w=a()({},z);Object(m.s)(this,"visible")&&(w.popupVisible=this.$props.visible);var O=Object(m.k)(this),S={props:a()({popupClassName:n,prefixCls:c,action:i,builtinPlacements:p,popupPlacement:v,popupAlign:b,getPopupContainer:x,afterPopupVisibleChange:u,popupTransitionName:d,popupAnimation:f,defaultPopupVisible:C,destroyPopupOnHide:y,mouseLeaveDelay:o,popupStyle:s,mouseEnterDelay:r},w),on:a()({},O,{popupVisibleChange:O.visibleChange||g,popupAlign:O.popupAlign||g}),ref:"trigger"};return e(h.a,S,[e("template",{slot:"popup"},[this.getPopupElement(e)]),this.$slots.default])}},y={adjustX:1,adjustY:1},C={adjustX:0,adjustY:0},x=[0,0];function z(e){return"boolean"==typeof e?e?y:C:a()({},C,e)}var w=n(7),O=n(53),S=Object(O.a)(),M={name:"ATooltip",model:{prop:"visible",event:"visibleChange"},props:a()({},S,{title:u.a.any}),inject:{configProvider:{default:function(){return w.a}}},data:function(){return{sVisible:!!this.$props.visible||!!this.$props.defaultVisible}},watch:{visible:function(e){this.sVisible=e}},methods:{onVisibleChange:function(e){Object(m.s)(this,"visible")||(this.sVisible=!this.isNoTitle()&&e),this.isNoTitle()||this.$emit("visibleChange",e)},getPopupDomNode:function(){return this.$refs.tooltip.getPopupDomNode()},getPlacements:function(){var e=this.$props,t=e.builtinPlacements,n=e.arrowPointAtCenter,i=e.autoAdjustOverflow;return t||function(e){var t=e.arrowWidth,n=void 0===t?5:t,i=e.horizontalArrowShift,r=void 0===i?16:i,o=e.verticalArrowShift,s=void 0===o?12:o,c=e.autoAdjustOverflow,l=void 0===c||c,u={left:{points:["cr","cl"],offset:[-4,0]},right:{points:["cl","cr"],offset:[4,0]},top:{points:["bc","tc"],offset:[0,-4]},bottom:{points:["tc","bc"],offset:[0,4]},topLeft:{points:["bl","tc"],offset:[-(r+n),-4]},leftTop:{points:["tr","cl"],offset:[-4,-(s+n)]},topRight:{points:["br","tc"],offset:[r+n,-4]},rightTop:{points:["tl","cr"],offset:[4,-(s+n)]},bottomRight:{points:["tr","bc"],offset:[r+n,4]},rightBottom:{points:["bl","cr"],offset:[4,s+n]},bottomLeft:{points:["tl","bc"],offset:[-(r+n),4]},leftBottom:{points:["br","cl"],offset:[-4,s+n]}};return Object.keys(u).forEach((function(t){u[t]=e.arrowPointAtCenter?a()({},u[t],{overflow:z(l),targetOffset:x}):a()({},p[t],{overflow:z(l)}),u[t].ignoreShake=!0})),u}({arrowPointAtCenter:n,verticalArrowShift:8,autoAdjustOverflow:i})},getDisabledCompatibleChildren:function(e){var t=this.$createElement,n=e.componentOptions&&e.componentOptions.Ctor.options||{};if((!0===n.__ANT_BUTTON||!0===n.__ANT_SWITCH||!0===n.__ANT_CHECKBOX)&&(e.componentOptions.propsData.disabled||""===e.componentOptions.propsData.disabled)||"button"===e.tag&&e.data&&e.data.attrs&&void 0!==e.data.attrs.disabled){var i=function(e,t){var n={},i=a()({},e);return t.forEach((function(t){e&&t in e&&(n[t]=e[t],delete i[t])})),{picked:n,omitted:i}}(Object(m.q)(e),["position","left","right","top","bottom","float","display","zIndex"]),r=i.picked,o=i.omitted,c=a()({display:"inline-block"},r,{cursor:"not-allowed",width:e.componentOptions.propsData.block?"100%":null}),l=a()({},o,{pointerEvents:"none"});return t("span",{style:c,class:Object(m.f)(e)},[Object(s.a)(e,{style:l,class:null})])}return e},isNoTitle:function(){var e=Object(m.g)(this,"title");return!e&&0!==e},getOverlay:function(){var e=Object(m.g)(this,"title");return 0===e?e:e||""},onPopupAlign:function(e,t){var n=this.getPlacements(),i=Object.keys(n).filter((function(e){return n[e].points[0]===t.points[0]&&n[e].points[1]===t.points[1]}))[0];if(i){var r=e.getBoundingClientRect(),o={top:"50%",left:"50%"};i.indexOf("top")>=0||i.indexOf("Bottom")>=0?o.top=r.height-t.offset[1]+"px":(i.indexOf("Top")>=0||i.indexOf("bottom")>=0)&&(o.top=-t.offset[1]+"px"),i.indexOf("left")>=0||i.indexOf("Right")>=0?o.left=r.width-t.offset[0]+"px":(i.indexOf("right")>=0||i.indexOf("Left")>=0)&&(o.left=-t.offset[0]+"px"),e.style.transformOrigin=o.left+" "+o.top}}},render:function(){var e=arguments[0],t=this.$props,n=this.$data,i=this.$slots,o=t.prefixCls,c=t.openClassName,l=t.getPopupContainer,u=this.configProvider.getPopupContainer,h=this.configProvider.getPrefixCls,d=h("tooltip",o),f=(i.default||[]).filter((function(e){return e.tag||""!==e.text.trim()}));f=1===f.length?f[0]:f;var p=n.sVisible;if(!Object(m.s)(this,"visible")&&this.isNoTitle()&&(p=!1),!f)return null;var v=this.getDisabledCompatibleChildren(Object(m.w)(f)?f:e("span",[f])),g=r()({},c||d+"-open",!0),y={props:a()({},t,{prefixCls:d,getTooltipContainer:l||u,builtinPlacements:this.getPlacements(),overlay:this.getOverlay(),visible:p}),ref:"tooltip",on:a()({},Object(m.k)(this),{visibleChange:this.onVisibleChange,popupAlign:this.onPopupAlign})};return e(b,y,[p?Object(s.a)(v,{class:g}):v])}},k=n(10);M.install=function(e){e.use(k.a),e.component(M.name,M)};t.a=M},function(e,t,n){"use strict";n.d(t,"a",(function(){return o})),n.d(t,"b",(function(){return a}));var i=["moz","ms","webkit"];var r=function(){if("undefined"==typeof window)return function(){};if(window.requestAnimationFrame)return window.requestAnimationFrame.bind(window);var e,t=i.filter((function(e){return e+"RequestAnimationFrame"in window}))[0];return t?window[t+"RequestAnimationFrame"]:(e=0,function(t){var n=(new Date).getTime(),i=Math.max(0,16-(n-e)),r=window.setTimeout((function(){t(n+i)}),i);return e=n+i,r})}(),o=function(e){return function(e){if("undefined"==typeof window)return null;if(window.cancelAnimationFrame)return window.cancelAnimationFrame(e);var t=i.filter((function(e){return e+"CancelAnimationFrame"in window||e+"CancelRequestAnimationFrame"in window}))[0];return t?(window[t+"CancelAnimationFrame"]||window[t+"CancelRequestAnimationFrame"]).call(this,e):clearTimeout(e)}(e.id)},a=function(e,t){var n=Date.now();var i={id:r((function o(){Date.now()-n>=t?e.call():i.id=r(o)}))};return i}},function(e,t,n){var i=n(215);e.exports=function(e,t,n){return null==e?e:i(e,t,n)}},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t){var n=Array.isArray;e.exports=n},function(e,t,n){var i=n(61),r=n(135),o=n(44),a=Function.prototype,s=Object.prototype,c=a.toString,l=s.hasOwnProperty,u=c.call(Object);e.exports=function(e){if(!o(e)||"[object Object]"!=i(e))return!1;var t=r(e);if(null===t)return!0;var n=l.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&c.call(n)==u}},function(e,t,n){"use strict";var i=n(81);t.a=i.a},function(e,t,n){"use strict";n.d(t,"a",(function(){return o})),n.d(t,"b",(function(){return a})),n.d(t,"c",(function(){return s})),n.d(t,"d",(function(){return c})),n.d(t,"g",(function(){return l})),n.d(t,"e",(function(){return h})),n.d(t,"f",(function(){return d}));var i=n(2),r=n.n(i);function o(){return!0}function a(e){return r()({},e,{lastModified:e.lastModified,lastModifiedDate:e.lastModifiedDate,name:e.name,size:e.size,type:e.type,uid:e.uid,percent:0,originFileObj:e})}function s(){var e=.1;return function(t){var n=t;return n>=.98||(n+=e,(e-=.01)<.001&&(e=.001)),n}}function c(e,t){var n=void 0!==e.uid?"uid":"name";return t.filter((function(t){return t[n]===e[n]}))[0]}function l(e,t){var n=void 0!==e.uid?"uid":"name",i=t.filter((function(t){return t[n]!==e[n]}));return i.length===t.length?null:i}var u=function(e){return!!e&&0===e.indexOf("image/")},h=function(e){if(u(e.type))return!0;var t=e.thumbUrl||e.url,n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=e.split("/"),n=t[t.length-1],i=n.split(/#|\?/)[0];return(/\.[^./\\]*$/.exec(i)||[""])[0]}(t);return!(!/^data:image\//.test(t)&&!/(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico)$/i.test(n))||!/^data:/.test(t)&&!n};function d(e){return new Promise((function(t){if(u(e.type)){var n=document.createElement("canvas");n.width=200,n.height=200,n.style.cssText="position: fixed; left: 0; top: 0; width: 200px; height: 200px; z-index: 9999; display: none;",document.body.appendChild(n);var i=n.getContext("2d"),r=new Image;r.onload=function(){var e=r.width,o=r.height,a=200,s=200,c=0,l=0;e0},e.prototype.connect_=function(){i&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),s?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){i&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,n=void 0===t?"":t;a.some((function(e){return!!~n.indexOf(e)}))&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),l=function(e,t){for(var n=0,i=Object.keys(t);n0},e}(),x="undefined"!=typeof WeakMap?new WeakMap:new n,z=function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=c.getInstance(),i=new C(t,n,this);x.set(this,i)};["observe","unobserve","disconnect"].forEach((function(e){z.prototype[e]=function(){var t;return(t=x.get(this))[e].apply(t,arguments)}}));var w=void 0!==r.ResizeObserver?r.ResizeObserver:z;t.a=w}).call(this,n(134))},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t,n){"use strict";var i=n(0),r=i.a.oneOf(["hover","focus","click","contextmenu"]);t.a=function(){return{trigger:i.a.oneOfType([r,i.a.arrayOf(r)]).def("hover"),visible:i.a.bool,defaultVisible:i.a.bool,placement:i.a.oneOf(["top","left","right","bottom","topLeft","topRight","bottomLeft","bottomRight","leftTop","leftBottom","rightTop","rightBottom"]).def("top"),transitionName:i.a.string.def("zoom-big-fast"),overlayStyle:i.a.object.def((function(){return{}})),overlayClassName:i.a.string,prefixCls:i.a.string,mouseEnterDelay:i.a.number.def(.1),mouseLeaveDelay:i.a.number.def(.1),getPopupContainer:i.a.func,arrowPointAtCenter:i.a.bool.def(!1),autoAdjustOverflow:i.a.oneOfType([i.a.bool,i.a.object]).def(!0),destroyTooltipOnHide:i.a.bool.def(!1),align:i.a.object.def((function(){return{}})),builtinPlacements:i.a.object}}},function(e,t,n){try{var i=n(180)}catch(e){i=n(180)}var r=/\s+/,o=Object.prototype.toString;function a(e){if(!e||!e.nodeType)throw new Error("A DOM element reference is required");this.el=e,this.list=e.classList}e.exports=function(e){return new a(e)},a.prototype.add=function(e){if(this.list)return this.list.add(e),this;var t=this.array();return~i(t,e)||t.push(e),this.el.className=t.join(" "),this},a.prototype.remove=function(e){if("[object RegExp]"==o.call(e))return this.removeMatching(e);if(this.list)return this.list.remove(e),this;var t=this.array(),n=i(t,e);return~n&&t.splice(n,1),this.el.className=t.join(" "),this},a.prototype.removeMatching=function(e){for(var t=this.array(),n=0;n0&&void 0!==arguments[0]?arguments[0]:{};return Object.keys(e).reduce((function(t,n){var i=e[n];switch(n){case"class":t.className=i,delete t.class;break;default:t[n]=i}return t}),{})}var f=function(){function e(){a()(this,e),this.collection={}}return c()(e,[{key:"clear",value:function(){this.collection={}}},{key:"delete",value:function(e){return delete this.collection[e]}},{key:"get",value:function(e){return this.collection[e]}},{key:"has",value:function(e){return Boolean(this.collection[e])}},{key:"set",value:function(e,t){return this.collection[e]=t,this}},{key:"size",get:function(){return Object.keys(this.collection).length}}]),e}();function p(e,t,n,i){return e(t.tag,i?r()({key:n},i,{attrs:r()({},d(t.attrs),i.attrs)}):{key:n,attrs:r()({},d(t.attrs))},(t.children||[]).map((function(i,r){return p(e,i,n+"-"+t.tag+"-"+r)})))}function v(e){return Object(l.generate)(e)[0]}function m(e,t){switch(t){case"fill":return e+"-fill";case"outline":return e+"-o";case"twotone":return e+"-twotone";default:throw new TypeError("Unknown theme type: "+t+", name: "+e)}}}).call(this,n(97))},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var i=n(71),r=n(272),o=n(273),a=i?i.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":a&&a in Object(e)?r(e):o(e)}},function(e,t,n){var i=n(304),r=n(307);e.exports=function(e,t){var n=r(e,t);return i(n)?n:void 0}},function(e,t){e.exports=function(e,t){return e===t||e!=e&&t!=t}},function(e,t,n){"use strict";var i=n(2),r=n.n(i),o=n(45),a=n(65),s={lang:r()({placeholder:"Select date",rangePlaceholder:["Start date","End date"]},o.a),timePickerLocale:r()({},a.a)};t.a=s},function(e,t,n){"use strict";t.a={placeholder:"Select time"}},function(e,t,n){e.exports=function(){"use strict";return function(e,t,n){(n=n||{}).childrenKeyName=n.childrenKeyName||"children";var i=e||[],r=[],o=0;do{var a=i.filter((function(e){return t(e,o)}))[0];if(!a)break;r.push(a),i=a[n.childrenKeyName]||[],o+=1}while(i.length>0);return r}}()},function(e,t,n){var i=n(49),r=n(86);e.exports=n(50)?function(e,t,n){return i.f(e,t,r(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){var i=n(84);e.exports=function(e){if(!i(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var i=n(170),r=n(124);e.exports=function(e){return i(r(e))}},function(e,t){e.exports={}},function(e,t,n){var i=n(39).Symbol;e.exports=i},function(e,t,n){var i=n(139),r=n(140);e.exports=function(e,t,n,o){var a=!n;n||(n={});for(var s=-1,c=t.length;++s100?100:e}var b=function(e){var t=e.from,n=void 0===t?"#1890ff":t,i=e.to,r=void 0===i?"#1890ff":i,o=e.direction,a=void 0===o?"to right":o,s=p()(e,["from","to","direction"]);return 0!==Object.keys(s).length?{backgroundImage:"linear-gradient("+a+", "+function(e){var t=[],n=!0,i=!1,r=void 0;try{for(var o,a=Object.entries(e)[Symbol.iterator]();!(n=(o=a.next()).done);n=!0){var s=o.value,c=m()(s,2),l=c[0],u=c[1],h=parseFloat(l.replace(/%/g,""));if(isNaN(h))return{};t.push({key:h,value:u})}}catch(e){i=!0,r=e}finally{try{!n&&a.return&&a.return()}finally{if(i)throw r}}return(t=t.sort((function(e,t){return e.key-t.key}))).map((function(e){var t=e.key;return e.value+" "+t+"%"})).join(", ")}(s)+")"}:{backgroundImage:"linear-gradient("+a+", "+n+", "+r+")"}},y={functional:!0,render:function(e,t){var n=t.props,i=t.children,r=n.prefixCls,o=n.percent,s=n.successPercent,c=n.strokeWidth,l=n.size,u=n.strokeColor,h=n.strokeLinecap,d=void 0;d=u&&"string"!=typeof u?b(u):{background:u};var f=a()({width:g(o)+"%",height:(c||("small"===l?6:8))+"px",background:u,borderRadius:"square"===h?0:"100px"},d),p={width:g(s)+"%",height:(c||("small"===l?6:8))+"px",borderRadius:"square"===h?0:""},v=void 0!==s?e("div",{class:r+"-success-bg",style:p}):null;return e("div",[e("div",{class:r+"-outer"},[e("div",{class:r+"-inner"},[e("div",{class:r+"-bg",style:f}),v])]),i])}},C=n(6),x=n.n(C),z=n(17),w=n.n(z),O=n(25),S=n.n(O);var M=function(e){return{mixins:[e],updated:function(){var e=this,t=Date.now(),n=!1;Object.keys(this.paths).forEach((function(i){var r=e.paths[i];if(r){n=!0;var o=r.style;o.transitionDuration=".3s, .3s, .3s, .06s",e.prevTimeStamp&&t-e.prevTimeStamp<100&&(o.transitionDuration="0s, 0s")}})),n&&(this.prevTimeStamp=Date.now())}}},k=l.a.oneOfType([l.a.number,l.a.string]),V={percent:l.a.oneOfType([k,l.a.arrayOf(k)]),prefixCls:l.a.string,strokeColor:l.a.oneOfType([l.a.string,l.a.arrayOf(l.a.oneOfType([l.a.string,l.a.object])),l.a.object]),strokeLinecap:l.a.oneOf(["butt","round","square"]),strokeWidth:k,trailColor:l.a.string,trailWidth:k},T=a()({},V,{gapPosition:l.a.oneOf(["top","bottom","left","right"]),gapDegree:l.a.oneOfType([l.a.number,l.a.string,l.a.bool])}),H=a()({},{percent:0,prefixCls:"rc-progress",strokeColor:"#2db7f5",strokeLinecap:"round",strokeWidth:1,trailColor:"#D9D9D9",trailWidth:1},{gapPosition:"top"});w.a.use(S.a,{name:"ant-ref"});var j=0;function P(e){return+e.replace("%","")}function L(e){return Array.isArray(e)?e:[e]}function _(e,t,n,i){var r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:0,o=arguments[5],a=50-i/2,s=0,c=-a,l=0,u=-2*a;switch(o){case"left":s=-a,c=0,l=2*a,u=0;break;case"right":s=a,c=0,l=-2*a,u=0;break;case"bottom":c=a,u=2*a}var h="M 50,50 m "+s+","+c+"\n a "+a+","+a+" 0 1 1 "+l+","+-u+"\n a "+a+","+a+" 0 1 1 "+-l+","+u,d=2*Math.PI*a,f={stroke:n,strokeDasharray:t/100*(d-r)+"px "+d+"px",strokeDashoffset:"-"+(r/2+e/100*(d-r))+"px",transition:"stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s"};return{pathString:h,pathStyle:f}}var $=M({props:Object(u.t)(T,H),created:function(){this.paths={},this.gradientId=j,j+=1},methods:{getStokeList:function(){var e=this,t=this.$createElement,n=this.$props,i=n.prefixCls,r=n.percent,o=n.strokeColor,a=n.strokeWidth,s=n.strokeLinecap,c=n.gapDegree,l=n.gapPosition,u=L(r),h=L(o),d=0;return u.map((function(n,r){var o=h[r]||h[h.length-1],u="[object Object]"===Object.prototype.toString.call(o)?"url(#"+i+"-gradient-"+e.gradientId+")":"",f=_(d,n,o,a,c,l),p=f.pathString,v=f.pathStyle;return d+=n,t("path",{key:r,attrs:{d:p,stroke:u,"stroke-linecap":s,"stroke-width":a,opacity:0===n?0:1,"fill-opacity":"0"},class:i+"-circle-path",style:v,directives:[{name:"ant-ref",value:function(t){e.paths[r]=t}}]})}))}},render:function(){var e=arguments[0],t=this.$props,n=t.prefixCls,i=t.strokeWidth,r=t.trailWidth,o=t.gapDegree,a=t.gapPosition,s=t.trailColor,c=t.strokeLinecap,l=t.strokeColor,u=p()(t,["prefixCls","strokeWidth","trailWidth","gapDegree","gapPosition","trailColor","strokeLinecap","strokeColor"]),h=_(0,100,s,i,o,a),d=h.pathString,f=h.pathStyle;delete u.percent;var v=L(l),m=v.find((function(e){return"[object Object]"===Object.prototype.toString.call(e)})),g={attrs:{d:d,stroke:s,"stroke-linecap":c,"stroke-width":r||i,"fill-opacity":"0"},class:n+"-circle-trail",style:f};return e("svg",x()([{class:n+"-circle",attrs:{viewBox:"0 0 100 100"}},u]),[m&&e("defs",[e("linearGradient",{attrs:{id:n+"-gradient-"+this.gradientId,x1:"100%",y1:"0%",x2:"0%",y2:"0%"}},[Object.keys(m).sort((function(e,t){return P(e)-P(t)})).map((function(t,n){return e("stop",{key:n,attrs:{offset:t,"stop-color":m[t]}})}))])]),e("path",g),this.getStokeList().reverse()])}}),E={normal:"#108ee9",exception:"#ff5500",success:"#87d068"};function A(e){var t=e.percent,n=e.successPercent,i=g(t);if(!n)return i;var r=g(n);return[n,g(i-r)]}var F={functional:!0,render:function(e,t){var n,i,o,a,s,c=t.props,l=t.children,u=c.prefixCls,h=c.width,d=c.strokeWidth,f=c.trailColor,p=c.strokeLinecap,v=c.gapPosition,m=c.gapDegree,g=c.type,b=h||120,y={width:"number"==typeof b?b+"px":b,height:"number"==typeof b?b+"px":b,fontSize:.15*b+6},C=d||6,x=v||"dashboard"===g&&"bottom"||"top",z=m||"dashboard"===g&&75,w=(o=(i=c).progressStatus,a=i.successPercent,s=i.strokeColor||E[o],a?[E.success,s]:s),O="[object Object]"===Object.prototype.toString.call(w);return e("div",{class:(n={},r()(n,u+"-inner",!0),r()(n,u+"-circle-gradient",O),n),style:y},[e($,{attrs:{percent:A(c),strokeWidth:C,trailWidth:C,strokeColor:w,strokeLinecap:p,trailColor:f,prefixCls:u,gapDegree:z,gapPosition:x}}),l])}},I=["normal","exception","active","success"],R=l.a.oneOf(["line","circle","dashboard"]),D=l.a.oneOf(["default","small"]),N={prefixCls:l.a.string,type:R,percent:l.a.number,successPercent:l.a.number,format:l.a.func,status:l.a.oneOf(I),showInfo:l.a.bool,strokeWidth:l.a.number,strokeLinecap:l.a.oneOf(["butt","round","square"]),strokeColor:l.a.oneOfType([l.a.string,l.a.object]),trailColor:l.a.string,width:l.a.number,gapDegree:l.a.number,gapPosition:l.a.oneOf(["top","bottom","left","right"]),size:D},K={name:"AProgress",props:Object(u.t)(N,{type:"line",percent:0,showInfo:!0,trailColor:"#f3f3f3",size:"default",gapDegree:0,strokeLinecap:"round"}),inject:{configProvider:{default:function(){return h.a}}},methods:{getPercentNumber:function(){var e=this.$props,t=e.successPercent,n=e.percent,i=void 0===n?0:n;return parseInt(void 0!==t?t.toString():i.toString(),10)},getProgressStatus:function(){var e=this.$props.status;return I.indexOf(e)<0&&this.getPercentNumber()>=100?"success":e||"normal"},renderProcessInfo:function(e,t){var n=this.$createElement,i=this.$props,r=i.showInfo,o=i.format,a=i.type,s=i.percent,c=i.successPercent;if(!r)return null;var l=void 0,u=o||this.$scopedSlots.format||function(e){return e+"%"},h="circle"===a||"dashboard"===a?"":"-circle";return o||this.$scopedSlots.format||"exception"!==t&&"success"!==t?l=u(g(s),g(c)):"exception"===t?l=n(d.a,{attrs:{type:"close"+h,theme:"line"===a?"filled":"outlined"}}):"success"===t&&(l=n(d.a,{attrs:{type:"check"+h,theme:"line"===a?"filled":"outlined"}})),n("span",{class:e+"-text",attrs:{title:"string"==typeof l?l:void 0}},[l])}},render:function(){var e,t=arguments[0],n=Object(u.l)(this),i=n.prefixCls,o=n.size,s=n.type,l=n.showInfo,h=this.configProvider.getPrefixCls,d=h("progress",i),f=this.getProgressStatus(),p=this.renderProcessInfo(d,f),v=void 0;if("line"===s){var m={props:a()({},n,{prefixCls:d})};v=t(y,m,[p])}else if("circle"===s||"dashboard"===s){var g={props:a()({},n,{prefixCls:d,progressStatus:f})};v=t(F,g,[p])}var b=c()(d,(e={},r()(e,d+"-"+("dashboard"===s?"circle":s),!0),r()(e,d+"-status-"+f,!0),r()(e,d+"-show-info",l),r()(e,d+"-"+o,o),e)),C={on:Object(u.k)(this),class:b};return t("div",C,[v])}},B=n(10);K.install=function(e){e.use(B.a),e.component(K.name,K)};t.a=K},function(e,t,n){"use strict";t.a={items_per_page:"/ page",jump_to:"Go to",jump_to_confirm:"confirm",page:"",prev_page:"Previous Page",next_page:"Next Page",prev_5:"Previous 5 Pages",next_5:"Next 5 Pages",prev_3:"Previous 3 Pages",next_3:"Next 3 Pages"}},function(e,t,n){"use strict";var i=n(64);t.a=i.a},function(e,t,n){var i=n(169),r=n(128);e.exports=Object.keys||function(e){return i(e,r)}},function(e,t){e.exports=!0},function(e,t){var n=0,i=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+i).toString(36))}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t,n){var i=n(124);e.exports=function(e){return Object(i(e))}},function(e,t,n){"use strict";var i=n(253)(!0);n(172)(String,"String",(function(e){this._t=String(e),this._i=0}),(function(){var e,t=this._t,n=this._i;return n>=t.length?{value:void 0,done:!0}:(e=i(t,n),this._i+=e.length,{value:e,done:!1})}))},function(e,t){var n,i,r=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===o||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:o}catch(e){n=o}try{i="function"==typeof clearTimeout?clearTimeout:a}catch(e){i=a}}();var c,l=[],u=!1,h=-1;function d(){u&&c&&(u=!1,c.length?l=c.concat(l):h=-1,l.length&&f())}function f(){if(!u){var e=s(d);u=!0;for(var t=l.length;t;){for(c=l,l=[];++h1)for(var n=1;n-1&&e%1==0&&e0;var o=function(e,t){for(var n=Object.create(null),i=e.split(","),r=0;r1),t})),s(e,u(e),n),l&&(n=r(n,7,c));for(var h=t.length;h--;)o(n,t[h]);return n}));e.exports=h},function(e,t,n){var i=n(368),r=n(106),o=n(107),a=o&&o.isRegExp,s=a?r(a):i;e.exports=s},function(e,t,n){"use strict";(function(e){function n(){return(n=Object.assign||function(e){for(var t=1;t=o)return e;switch(e){case"%s":return String(t[i++]);case"%d":return Number(t[i++]);case"%j":try{return JSON.stringify(t[i++])}catch(e){return"[Circular]"}break;default:return e}}));return a}return r}function d(e,t){return null==e||(!("array"!==t||!Array.isArray(e)||e.length)||!(!function(e){return"string"===e||"url"===e||"hex"===e||"email"===e||"date"===e||"pattern"===e}(t)||"string"!=typeof e||e))}function f(e,t,n){var i=0,r=e.length;!function o(a){if(a&&a.length)n(a);else{var s=i;i+=1,s()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,url:new RegExp("^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$","i"),hex:/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i},C={integer:function(e){return C.number(e)&&parseInt(e,10)===e},float:function(e){return C.number(e)&&!C.integer(e)},array:function(e){return Array.isArray(e)},regexp:function(e){if(e instanceof RegExp)return!0;try{return!!new RegExp(e)}catch(e){return!1}},date:function(e){return"function"==typeof e.getTime&&"function"==typeof e.getMonth&&"function"==typeof e.getYear&&!isNaN(e.getTime())},number:function(e){return!isNaN(e)&&"number"==typeof e},object:function(e){return"object"==typeof e&&!C.array(e)},method:function(e){return"function"==typeof e},email:function(e){return"string"==typeof e&&!!e.match(y.email)&&e.length<255},url:function(e){return"string"==typeof e&&!!e.match(y.url)},hex:function(e){return"string"==typeof e&&!!e.match(y.hex)}};var x={required:b,whitespace:function(e,t,n,i,r){(/^\s+$/.test(t)||""===t)&&i.push(h(r.messages.whitespace,e.fullField))},type:function(e,t,n,i,r){if(e.required&&void 0===t)b(e,t,n,i,r);else{var o=e.type;["integer","float","array","regexp","object","method","email","number","date","url","hex"].indexOf(o)>-1?C[o](t)||i.push(h(r.messages.types[o],e.fullField,e.type)):o&&typeof t!==e.type&&i.push(h(r.messages.types[o],e.fullField,e.type))}},range:function(e,t,n,i,r){var o="number"==typeof e.len,a="number"==typeof e.min,s="number"==typeof e.max,c=t,l=null,u="number"==typeof t,d="string"==typeof t,f=Array.isArray(t);if(u?l="number":d?l="string":f&&(l="array"),!l)return!1;f&&(c=t.length),d&&(c=t.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,"_").length),o?c!==e.len&&i.push(h(r.messages[l].len,e.fullField,e.len)):a&&!s&&ce.max?i.push(h(r.messages[l].max,e.fullField,e.max)):a&&s&&(ce.max)&&i.push(h(r.messages[l].range,e.fullField,e.min,e.max))},enum:function(e,t,n,i,r){e.enum=Array.isArray(e.enum)?e.enum:[],-1===e.enum.indexOf(t)&&i.push(h(r.messages.enum,e.fullField,e.enum.join(", ")))},pattern:function(e,t,n,i,r){if(e.pattern)if(e.pattern instanceof RegExp)e.pattern.lastIndex=0,e.pattern.test(t)||i.push(h(r.messages.pattern.mismatch,e.fullField,t,e.pattern));else if("string"==typeof e.pattern){new RegExp(e.pattern).test(t)||i.push(h(r.messages.pattern.mismatch,e.fullField,t,e.pattern))}}};function z(e,t,n,i,r){var o=e.type,a=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t,o)&&!e.required)return n();x.required(e,t,i,a,r,o),d(t,o)||x.type(e,t,i,a,r)}n(a)}var w={string:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t,"string")&&!e.required)return n();x.required(e,t,i,o,r,"string"),d(t,"string")||(x.type(e,t,i,o,r),x.range(e,t,i,o,r),x.pattern(e,t,i,o,r),!0===e.whitespace&&x.whitespace(e,t,i,o,r))}n(o)},method:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&x.type(e,t,i,o,r)}n(o)},number:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(""===t&&(t=void 0),d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&(x.type(e,t,i,o,r),x.range(e,t,i,o,r))}n(o)},boolean:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&x.type(e,t,i,o,r)}n(o)},regexp:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),d(t)||x.type(e,t,i,o,r)}n(o)},integer:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&(x.type(e,t,i,o,r),x.range(e,t,i,o,r))}n(o)},float:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&(x.type(e,t,i,o,r),x.range(e,t,i,o,r))}n(o)},array:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(null==t&&!e.required)return n();x.required(e,t,i,o,r,"array"),null!=t&&(x.type(e,t,i,o,r),x.range(e,t,i,o,r))}n(o)},object:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&x.type(e,t,i,o,r)}n(o)},enum:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r),void 0!==t&&x.enum(e,t,i,o,r)}n(o)},pattern:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t,"string")&&!e.required)return n();x.required(e,t,i,o,r),d(t,"string")||x.pattern(e,t,i,o,r)}n(o)},date:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t,"date")&&!e.required)return n();var a;if(x.required(e,t,i,o,r),!d(t,"date"))a=t instanceof Date?t:new Date(t),x.type(e,a,i,o,r),a&&x.range(e,a.getTime(),i,o,r)}n(o)},url:z,hex:z,email:z,required:function(e,t,n,i,r){var o=[],a=Array.isArray(t)?"array":typeof t;x.required(e,t,i,o,r,a),n(o)},any:function(e,t,n,i,r){var o=[];if(e.required||!e.required&&i.hasOwnProperty(e.field)){if(d(t)&&!e.required)return n();x.required(e,t,i,o,r)}n(o)}};function O(){return{default:"Validation error on field %s",required:"%s is required",enum:"%s must be one of %s",whitespace:"%s cannot be empty",date:{format:"%s date %s is invalid for format %s",parse:"%s date could not be parsed, %s is invalid ",invalid:"%s date %s is invalid"},types:{string:"%s is not a %s",method:"%s is not a %s (function)",array:"%s is not an %s",object:"%s is not an %s",number:"%s is not a %s",date:"%s is not a %s",boolean:"%s is not a %s",integer:"%s is not an %s",float:"%s is not a %s",regexp:"%s is not a valid %s",email:"%s is not a valid %s",url:"%s is not a valid %s",hex:"%s is not a valid %s"},string:{len:"%s must be exactly %s characters",min:"%s must be at least %s characters",max:"%s cannot be longer than %s characters",range:"%s must be between %s and %s characters"},number:{len:"%s must equal %s",min:"%s cannot be less than %s",max:"%s cannot be greater than %s",range:"%s must be between %s and %s"},array:{len:"%s must be exactly %s in length",min:"%s cannot be less than %s in length",max:"%s cannot be greater than %s in length",range:"%s must be between %s and %s in length"},pattern:{mismatch:"%s value %s does not match pattern %s"},clone:function(){var e=JSON.parse(JSON.stringify(this));return e.clone=this.clone,e}}}var S=O();function M(e){this.rules=null,this._messages=S,this.define(e)}M.prototype={messages:function(e){return e&&(this._messages=g(O(),e)),this._messages},define:function(e){if(!e)throw new Error("Cannot configure a schema with no rules");if("object"!=typeof e||Array.isArray(e))throw new Error("Rules must be an object");var t,n;for(t in this.rules={},e)e.hasOwnProperty(t)&&(n=e[t],this.rules[t]=Array.isArray(n)?n:[n])},validate:function(e,t,i){var r=this;void 0===t&&(t={}),void 0===i&&(i=function(){});var o,a,s=e,c=t,l=i;if("function"==typeof c&&(l=c,c={}),!this.rules||0===Object.keys(this.rules).length)return l&&l(),Promise.resolve();if(c.messages){var d=this.messages();d===S&&(d=O()),g(d,c.messages),c.messages=d}else c.messages=this.messages();var f={};(c.keys||Object.keys(this.rules)).forEach((function(t){o=r.rules[t],a=s[t],o.forEach((function(i){var o=i;"function"==typeof o.transform&&(s===e&&(s=n({},s)),a=s[t]=o.transform(a)),(o="function"==typeof o?{validator:o}:n({},o)).validator=r.getValidationMethod(o),o.field=t,o.fullField=o.fullField||t,o.type=r.getType(o),o.validator&&(f[t]=f[t]||[],f[t].push({rule:o,value:a,source:s,field:t}))}))}));var p={};return v(f,c,(function(e,t){var i,r=e.rule,o=!("object"!==r.type&&"array"!==r.type||"object"!=typeof r.fields&&"object"!=typeof r.defaultField);function a(e,t){return n({},t,{fullField:r.fullField+"."+e})}function s(i){void 0===i&&(i=[]);var s=i;if(Array.isArray(s)||(s=[s]),!c.suppressWarning&&s.length&&M.warning("async-validator:",s),s.length&&void 0!==r.message&&(s=[].concat(r.message)),s=s.map(m(r)),c.first&&s.length)return p[r.field]=1,t(s);if(o){if(r.required&&!e.value)return void 0!==r.message?s=[].concat(r.message).map(m(r)):c.error&&(s=[c.error(r,h(c.messages.required,r.field))]),t(s);var l={};if(r.defaultField)for(var u in e.value)e.value.hasOwnProperty(u)&&(l[u]=r.defaultField);for(var d in l=n({},l,e.rule.fields))if(l.hasOwnProperty(d)){var f=Array.isArray(l[d])?l[d]:[l[d]];l[d]=f.map(a.bind(null,d))}var v=new M(l);v.messages(c.messages),e.rule.options&&(e.rule.options.messages=c.messages,e.rule.options.error=c.error),v.validate(e.value,e.rule.options||c,(function(e){var n=[];s&&s.length&&n.push.apply(n,s),e&&e.length&&n.push.apply(n,e),t(n.length?n:null)}))}else t(s)}o=o&&(r.required||!r.required&&e.value),r.field=e.field,r.asyncValidator?i=r.asyncValidator(r,e.value,s,e.source,c):r.validator&&(!0===(i=r.validator(r,e.value,s,e.source,c))?s():!1===i?s(r.message||r.field+" fails"):i instanceof Array?s(i):i instanceof Error&&s(i.message)),i&&i.then&&i.then((function(){return s()}),(function(e){return s(e)}))}),(function(e){!function(e){var t,n,i,r=[],o={};for(t=0;t0?i:n)(e)}},function(e,t,n){var i=n(127)("keys"),r=n(93);e.exports=function(e){return i[e]||(i[e]=r(e))}},function(e,t,n){var i=n(43),r=n(48),o=r["__core-js_shared__"]||(r["__core-js_shared__"]={});(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:i.version,mode:n(92)?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var i=n(49).f,r=n(60),o=n(38)("toStringTag");e.exports=function(e,t,n){e&&!r(e=n?e:e.prototype,o)&&i(e,o,{configurable:!0,value:t})}},function(e,t,n){n(258);for(var i=n(48),r=n(67),o=n(70),a=n(38)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),c=0;c-1&&e%1==0&&e<=9007199254740991}},function(e,t){var n=Object.prototype;e.exports=function(e){var t=e&&e.constructor;return e===("function"==typeof t&&t.prototype||n)}},function(e,t,n){var i=n(332),r=n(187),o=Object.prototype.propertyIsEnumerable,a=Object.getOwnPropertySymbols,s=a?function(e){return null==e?[]:(e=Object(e),i(a(e),(function(t){return o.call(e,t)})))}:r;e.exports=s},function(e,t){e.exports=function(e,t){for(var n=-1,i=t.length,r=e.length;++n0&&(t.percent=t.loaded/t.total*100),e.onProgress(t)});var n=new window.FormData;e.data&&Object.keys(e.data).forEach((function(t){var i=e.data[t];Array.isArray(i)?i.forEach((function(e){n.append(t+"[]",e)})):n.append(t,e.data[t])})),n.append(e.filename,e.file),t.onerror=function(t){e.onError(t)},t.onload=function(){if(t.status<200||t.status>=300)return e.onError(function(e,t){var n="cannot "+e.method+" "+e.action+" "+t.status+"'",i=new Error(n);return i.status=t.status,i.method=e.method,i.url=e.action,i}(e,t),p(t));e.onSuccess(p(t),t)},t.open(e.method,e.action,!0),e.withCredentials&&"withCredentials"in t&&(t.withCredentials=!0);var i=e.headers||{};for(var r in null!==i["X-Requested-With"]&&t.setRequestHeader("X-Requested-With","XMLHttpRequest"),i)i.hasOwnProperty(r)&&null!==i[r]&&t.setRequestHeader(r,i[r]);return t.send(n),{abort:function(){t.abort()}}}var m=+new Date,g=0;function b(){return"vc-upload-"+m+"-"+ ++g}var y=function(e,t){if(e&&t){var n=Array.isArray(t)?t:t.split(","),i=e.name||"",r=e.type||"",o=r.replace(/\/.*$/,"");return n.some((function(e){var t,n,a=e.trim();return"."===a.charAt(0)?(t=i.toLowerCase(),n=a.toLowerCase(),-1!==t.indexOf(n,t.length-n.length)):/\/\*$/.test(a)?o===a.replace(/\/.*$/,""):r===a}))}return!0};var C=function(e,t,n){var i=function e(i,r){r=r||"",i.isFile?i.file((function(e){n(e)&&(i.fullPath&&!e.webkitRelativePath&&(Object.defineProperties(e,{webkitRelativePath:{writable:!0}}),e.webkitRelativePath=i.fullPath.replace(/^\//,""),Object.defineProperties(e,{webkitRelativePath:{writable:!1}})),t([e]))})):i.isDirectory&&function(e,t){var n=e.createReader(),i=[];!function e(){n.readEntries((function(n){var r=Array.prototype.slice.apply(n);i=i.concat(r),!r.length?t(i):e()}))}()}(i,(function(t){t.forEach((function(t){e(t,""+r+i.name+"/")}))}))},r=!0,o=!1,a=void 0;try{for(var s,c=e[Symbol.iterator]();!(r=(s=c.next()).done);r=!0){i(s.value.webkitGetAsEntry())}}catch(e){o=!0,a=e}finally{try{!r&&c.return&&c.return()}finally{if(o)throw a}}},x={componentTag:o.a.string,prefixCls:o.a.string,name:o.a.string,multiple:o.a.bool,directory:o.a.bool,disabled:o.a.bool,accept:o.a.string,data:o.a.oneOfType([o.a.object,o.a.func]),action:o.a.oneOfType([o.a.string,o.a.func]),headers:o.a.object,beforeUpload:o.a.func,customRequest:o.a.func,withCredentials:o.a.bool,openFileDialogOnClick:o.a.bool,transformFile:o.a.func,method:o.a.string},z={inheritAttrs:!1,name:"ajaxUploader",mixins:[s.a],props:x,data:function(){return this.reqs={},{uid:b()}},mounted:function(){this._isMounted=!0},beforeDestroy:function(){this._isMounted=!1,this.abort()},methods:{onChange:function(e){var t=e.target.files;this.uploadFiles(t),this.reset()},onClick:function(){var e=this.$refs.fileInputRef;e&&e.click()},onKeyDown:function(e){"Enter"===e.key&&this.onClick()},onFileDrop:function(e){var t=this,n=this.$props.multiple;if(e.preventDefault(),"dragover"!==e.type)if(this.directory)C(e.dataTransfer.items,this.uploadFiles,(function(e){return y(e,t.accept)}));else{var i=h()(Array.prototype.slice.call(e.dataTransfer.files),(function(e){return y(e,t.accept)})),r=i[0],o=i[1];!1===n&&(r=r.slice(0,1)),this.uploadFiles(r),o.length&&this.$emit("reject",o)}},uploadFiles:function(e){var t=this,n=Array.prototype.slice.call(e);n.map((function(e){return e.uid=b(),e})).forEach((function(e){t.upload(e,n)}))},upload:function(e,t){var n=this;if(!this.beforeUpload)return setTimeout((function(){return n.post(e)}),0);var i=this.beforeUpload(e,t);i&&i.then?i.then((function(t){var i=Object.prototype.toString.call(t);return"[object File]"===i||"[object Blob]"===i?n.post(t):n.post(e)})).catch((function(e){console&&console.log(e)})):!1!==i&&setTimeout((function(){return n.post(e)}),0)},post:function(e){var t=this;if(this._isMounted){var n=this.$props,i=n.data,r=n.transformFile,o=void 0===r?function(e){return e}:r;new Promise((function(n){var i=t.action;if("function"==typeof i)return n(i(e));n(i)})).then((function(r){var a=e.uid,s=t.customRequest||v;Promise.resolve(o(e)).catch((function(e){console.error(e)})).then((function(o){"function"==typeof i&&(i=i(e));var c={action:r,filename:t.name,data:i,file:o,headers:t.headers,withCredentials:t.withCredentials,method:n.method||"post",onProgress:function(n){t.$emit("progress",n,e)},onSuccess:function(n,i){delete t.reqs[a],t.$emit("success",n,e,i)},onError:function(n,i){delete t.reqs[a],t.$emit("error",n,i,e)}};t.reqs[a]=s(c),t.$emit("start",e)}))}))}},reset:function(){this.setState({uid:b()})},abort:function(e){var t=this.reqs;if(e){var n=e;e&&e.uid&&(n=e.uid),t[n]&&t[n].abort&&t[n].abort(),delete t[n]}else Object.keys(t).forEach((function(e){t[e]&&t[e].abort&&t[e].abort(),delete t[e]}))}},render:function(){var e,t=arguments[0],n=this.$props,i=this.$attrs,o=n.componentTag,s=n.prefixCls,c=n.disabled,u=n.multiple,h=n.accept,d=n.directory,p=n.openFileDialogOnClick,v=f()((e={},l()(e,s,!0),l()(e,s+"-disabled",c),e)),m=c?{}:{click:p?this.onClick:function(){},keydown:p?this.onKeyDown:function(){},drop:this.onFileDrop,dragover:this.onFileDrop},g={on:r()({},Object(a.k)(this),m),attrs:{role:"button",tabIndex:c?null:"0"},class:v};return t(o,g,[t("input",{attrs:{id:i.id,type:"file",accept:h,directory:d?"directory":null,webkitdirectory:d?"webkitdirectory":null,multiple:u},ref:"fileInputRef",on:{click:function(e){return e.stopPropagation()},change:this.onChange},key:this.uid,style:{display:"none"}}),this.$slots.default])}},w=n(13),O={position:"absolute",top:0,opacity:0,filter:"alpha(opacity=0)",left:0,zIndex:9999},S={mixins:[s.a],props:{componentTag:o.a.string,disabled:o.a.bool,prefixCls:o.a.string,accept:o.a.string,multiple:o.a.bool,data:o.a.oneOfType([o.a.object,o.a.func]),action:o.a.oneOfType([o.a.string,o.a.func]),name:o.a.string},data:function(){return this.file={},{uploading:!1}},methods:{onLoad:function(){if(this.uploading){var e=this.file,t=void 0;try{var n=this.getIframeDocument(),i=n.getElementsByTagName("script")[0];i&&i.parentNode===n.body&&n.body.removeChild(i),t=n.body.innerHTML,this.$emit("success",t,e)}catch(n){Object(w.a)(!1,"cross domain error for Upload. Maybe server should return document.domain script. see Note from https://github.com/react-component/upload"),t="cross-domain",this.$emit("error",n,null,e)}this.endUpload()}},onChange:function(){var e=this,t=this.getFormInputNode(),n=this.file={uid:b(),name:t.value&&t.value.substring(t.value.lastIndexOf("\\")+1,t.value.length)};this.startUpload();var i=this.$props;if(!i.beforeUpload)return this.post(n);var r=i.beforeUpload(n);r&&r.then?r.then((function(){e.post(n)}),(function(){e.endUpload()})):!1!==r?this.post(n):this.endUpload()},getIframeNode:function(){return this.$refs.iframeRef},getIframeDocument:function(){return this.getIframeNode().contentDocument},getFormNode:function(){return this.getIframeDocument().getElementById("form")},getFormInputNode:function(){return this.getIframeDocument().getElementById("input")},getFormDataNode:function(){return this.getIframeDocument().getElementById("data")},getFileForMultiple:function(e){return this.multiple?[e]:e},getIframeHTML:function(e){var t="",n="";if(e){t=' + + + + + + + + +{{ end }} + +{{ define "page/body_end" }} + + +{{ end }} \ No newline at end of file diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html new file mode 100644 index 0000000..a7279e5 --- /dev/null +++ b/web/html/component/aClientTable.html @@ -0,0 +1,316 @@ +{{define "component/aClientTable"}} + + + + + + + + + + + + +{{end}} diff --git a/web/html/component/aCustomStatistic.html b/web/html/component/aCustomStatistic.html new file mode 100644 index 0000000..0bff128 --- /dev/null +++ b/web/html/component/aCustomStatistic.html @@ -0,0 +1,42 @@ +{{define "component/customStatistic"}} + +{{end}} + +{{define "component/aCustomStatistic"}} + + + +{{end}} \ No newline at end of file diff --git a/web/html/component/aPersianDatepicker.html b/web/html/component/aPersianDatepicker.html new file mode 100644 index 0000000..ebd85a0 --- /dev/null +++ b/web/html/component/aPersianDatepicker.html @@ -0,0 +1,72 @@ +{{define "component/persianDatepickerTemplate"}} + +{{end}} + +{{define "component/aPersianDatepicker"}} + + + + +{{end}} \ No newline at end of file diff --git a/web/html/component/aSettingListItem.html b/web/html/component/aSettingListItem.html new file mode 100644 index 0000000..27a7aba --- /dev/null +++ b/web/html/component/aSettingListItem.html @@ -0,0 +1,49 @@ +{{define "component/settingListItem"}} + + + + + + + + + + + + + +{{end}} + +{{define "component/aSettingListItem"}} + +{{end}} diff --git a/web/html/component/aSidebar.html b/web/html/component/aSidebar.html new file mode 100644 index 0000000..12ede9b --- /dev/null +++ b/web/html/component/aSidebar.html @@ -0,0 +1,218 @@ +{{define "component/sidebar/content"}} + +{{end}} + +{{define "component/aSidebar"}} + + + +{{end}} diff --git a/web/html/component/aTableSortable.html b/web/html/component/aTableSortable.html new file mode 100644 index 0000000..4abad8e --- /dev/null +++ b/web/html/component/aTableSortable.html @@ -0,0 +1,237 @@ +{{define "component/sortableTableTrigger"}} + +{{end}} + +{{define "component/aTableSortable"}} + + +{{end}} \ No newline at end of file diff --git a/web/html/component/aThemeSwitch.html b/web/html/component/aThemeSwitch.html new file mode 100644 index 0000000..431614d --- /dev/null +++ b/web/html/component/aThemeSwitch.html @@ -0,0 +1,119 @@ +{{define "component/themeSwitchTemplate"}} + +{{end}} + +{{define "component/themeSwitchTemplateLogin"}} + +{{end}} + +{{define "component/aThemeSwitch"}} + +{{end}} \ No newline at end of file diff --git a/web/html/form/client.html b/web/html/form/client.html new file mode 100644 index 0000000..2ee9f1b --- /dev/null +++ b/web/html/form/client.html @@ -0,0 +1,196 @@ +{{define "form/client"}} + + + + + + + + + + + + + + + + + + + [[ key ]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ i18n "none" }} + [[ key ]] + + + + + + + + + [[ SizeFormatter.sizeFormat(clientStats.up) ]] / + [[ SizeFormatter.sizeFormat(clientStats.down) ]] + ([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]]) + + + + + + + + + + + + + + + + + Expired + + + + + + +{{end}} diff --git a/web/html/form/inbound.html b/web/html/form/inbound.html new file mode 100644 index 0000000..591db9f --- /dev/null +++ b/web/html/form/inbound.html @@ -0,0 +1,166 @@ +{{define "form/inbound"}} + + + + + + + + + + + + + + + + [[ p ]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{template "form/sniffing"}} + + + +{{end}} diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html new file mode 100644 index 0000000..5229810 --- /dev/null +++ b/web/html/form/outbound.html @@ -0,0 +1,830 @@ +{{define "form/outbound"}} + + + + + + + [[ y + ]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/protocol/dokodemo.html b/web/html/form/protocol/dokodemo.html new file mode 100644 index 0000000..b73a764 --- /dev/null +++ b/web/html/form/protocol/dokodemo.html @@ -0,0 +1,37 @@ +{{define "form/tunnel"}} + + + + + + + + + + + + + + + + + + - + + + + + + TCP,UDP + TCP + UDP + + + + + + + + +{{end}} diff --git a/web/html/form/protocol/http.html b/web/html/form/protocol/http.html new file mode 100644 index 0000000..fb1d74b --- /dev/null +++ b/web/html/form/protocol/http.html @@ -0,0 +1,26 @@ +{{define "form/http"}} + + + + + + + +
{{ i18n "username" }}{{ i18n "password" }} + +
+ + + + + + + + + + + +
+{{end}} diff --git a/web/html/form/protocol/shadowsocks.html b/web/html/form/protocol/shadowsocks.html new file mode 100644 index 0000000..06e1207 --- /dev/null +++ b/web/html/form/protocol/shadowsocks.html @@ -0,0 +1,50 @@ +{{define "form/shadowsocks"}} + + + + + [[ method_name ]] + + + + + + + + + TCP,UDP + TCP + UDP + + + + + + +{{end}} diff --git a/web/html/form/protocol/socks.html b/web/html/form/protocol/socks.html new file mode 100644 index 0000000..e126c51 --- /dev/null +++ b/web/html/form/protocol/socks.html @@ -0,0 +1,34 @@ +{{define "form/socks"}} + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/protocol/trojan.html b/web/html/form/protocol/trojan.html new file mode 100644 index 0000000..fc32772 --- /dev/null +++ b/web/html/form/protocol/trojan.html @@ -0,0 +1,50 @@ +{{define "form/trojan"}} + + + {{template "form/client"}} + + + + + + + + + + + + + +
{{ i18n "pages.inbounds.email" }}Password
[[ client.email ]][[ client.password ]]
+
+
+ +{{end}} diff --git a/web/html/form/protocol/vless.html b/web/html/form/protocol/vless.html new file mode 100644 index 0000000..bb4ae14 --- /dev/null +++ b/web/html/form/protocol/vless.html @@ -0,0 +1,102 @@ +{{define "form/vless"}} + + + {{template "form/client"}} + + + + + + + + + + + + + +
{{ i18n "pages.inbounds.email" }}ID
[[ client.email ]][[ client.id ]]
+
+
+ + + +{{end}} diff --git a/web/html/form/protocol/vmess.html b/web/html/form/protocol/vmess.html new file mode 100644 index 0000000..3c5200a --- /dev/null +++ b/web/html/form/protocol/vmess.html @@ -0,0 +1,23 @@ +{{define "form/vmess"}} + + + {{template "form/client"}} + + + + + + + + + + + + + + + +
{{ i18n "pages.inbounds.email" }}ID{{ i18n "security" }}
[[ client.email ]][[ client.id ]][[ client.security ]]
+
+
+{{end}} diff --git a/web/html/form/protocol/wireguard.html b/web/html/form/protocol/wireguard.html new file mode 100644 index 0000000..51fc4f0 --- /dev/null +++ b/web/html/form/protocol/wireguard.html @@ -0,0 +1,76 @@ +{{define "form/wireguard"}} + + + + + + + + + + + + + + + + + + + Peer [[ index + 1 ]] + + + + + + + + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/form/reality_settings.html b/web/html/form/reality_settings.html new file mode 100644 index 0000000..130e7cf --- /dev/null +++ b/web/html/form/reality_settings.html @@ -0,0 +1,63 @@ +{{define "form/realitySettings"}} + +{{end}} diff --git a/web/html/form/sniffing.html b/web/html/form/sniffing.html new file mode 100644 index 0000000..d8a2e85 --- /dev/null +++ b/web/html/form/sniffing.html @@ -0,0 +1,29 @@ +{{define "form/sniffing"}} + + + + {{ i18n "enabled" }} + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/external_proxy.html b/web/html/form/stream/external_proxy.html new file mode 100644 index 0000000..187090d --- /dev/null +++ b/web/html/form/stream/external_proxy.html @@ -0,0 +1,29 @@ +{{define "form/externalProxy"}} + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_finalmask.html b/web/html/form/stream/stream_finalmask.html new file mode 100644 index 0000000..0b6418b --- /dev/null +++ b/web/html/form/stream/stream_finalmask.html @@ -0,0 +1,84 @@ +{{define "form/streamFinalMask"}} + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_grpc.html b/web/html/form/stream/stream_grpc.html new file mode 100644 index 0000000..e74a3c3 --- /dev/null +++ b/web/html/form/stream/stream_grpc.html @@ -0,0 +1,13 @@ +{{define "form/streamGRPC"}} + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_httpupgrade.html b/web/html/form/stream/stream_httpupgrade.html new file mode 100644 index 0000000..aa43338 --- /dev/null +++ b/web/html/form/stream/stream_httpupgrade.html @@ -0,0 +1,26 @@ +{{define "form/streamHTTPUpgrade"}} + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_kcp.html b/web/html/form/stream/stream_kcp.html new file mode 100644 index 0000000..11f89eb --- /dev/null +++ b/web/html/form/stream/stream_kcp.html @@ -0,0 +1,32 @@ +{{define "form/streamKCP"}} + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_settings.html b/web/html/form/stream/stream_settings.html new file mode 100644 index 0000000..5b00ef2 --- /dev/null +++ b/web/html/form/stream/stream_settings.html @@ -0,0 +1,59 @@ +{{define "form/streamSettings"}} + + + + + TCP (RAW) + mKCP + WebSocket + gRPC + HTTPUpgrade + XHTTP + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_sockopt.html b/web/html/form/stream/stream_sockopt.html new file mode 100644 index 0000000..062b83d --- /dev/null +++ b/web/html/form/stream/stream_sockopt.html @@ -0,0 +1,75 @@ +{{define "form/streamSockopt"}} + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_tcp.html b/web/html/form/stream/stream_tcp.html new file mode 100644 index 0000000..3024ef7 --- /dev/null +++ b/web/html/form/stream/stream_tcp.html @@ -0,0 +1,72 @@ +{{define "form/streamTCP"}} + + + + + + + + + + + + + {{ i18n "pages.inbounds.stream.general.request" }} + + + + + + + + + + + + + + + + + + + + + + + + + + {{ i18n "pages.inbounds.stream.general.response" }} + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_ws.html b/web/html/form/stream/stream_ws.html new file mode 100644 index 0000000..4c71a51 --- /dev/null +++ b/web/html/form/stream/stream_ws.html @@ -0,0 +1,29 @@ +{{define "form/streamWS"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/form/stream/stream_xhttp.html b/web/html/form/stream/stream_xhttp.html new file mode 100644 index 0000000..447612c --- /dev/null +++ b/web/html/form/stream/stream_xhttp.html @@ -0,0 +1,147 @@ +{{define "form/streamXHTTP"}} + + + + + + + + + + + + + + + + + + + + + + + [[ key + ]] + + + + + + + + + + + + + + + + + + + + + Default (POST) + POST + PUT + GET (packet-up only) + + + + + Default (path) + path + header + cookie + query + + + + + + + + Default (path) + path + header + cookie + query + + + + + + + + Default (body) + body + header + query + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/form/tls_settings.html b/web/html/form/tls_settings.html new file mode 100644 index 0000000..d3940ea --- /dev/null +++ b/web/html/form/tls_settings.html @@ -0,0 +1,150 @@ +{{define "form/tlsSettings"}} + + + + + + {{ i18n "none" }} + Reality + TLS + + + + + + + + + +{{end}} diff --git a/web/html/inbounds.html b/web/html/inbounds.html new file mode 100644 index 0000000..b45f3c4 --- /dev/null +++ b/web/html/inbounds.html @@ -0,0 +1,1977 @@ +{{ template "page/head_start" .}} + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + {{ i18n "none" }} + {{ i18n "disabled" }} + {{ i18n "depleted" }} + {{ i18n "depletingSoon" }} + {{ i18n "online" }} + +
+ + + + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + +
+
+
+{{template "page/body_scripts" .}} + + + + +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} +{{template "component/aCustomStatistic" .}} +{{template "component/aPersianDatepicker" .}} +{{template "modals/inboundModal"}} +{{template "modals/promptModal"}} +{{template "modals/qrcodeModal"}} +{{template "modals/textModal"}} +{{template "modals/inboundInfoModal"}} +{{template "modals/clientsModal"}} +{{template "modals/clientsBulkModal"}} + +{{ template "page/body_end" .}} diff --git a/web/html/index.html b/web/html/index.html new file mode 100644 index 0000000..0bbba1f --- /dev/null +++ b/web/html/index.html @@ -0,0 +1,1362 @@ +{{ template "page/head_start" .}} + + + + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + + + + + + + + + + + + + + + + + + + + [[ version ]] + + + + + + + + [[ file ]] + + + +
+ {{ i18n "pages.index.geofilesUpdateAll" }} +
+
+
+
+ + + + + + + 10 + 20 + 50 + 100 + 500 + + + Debug + Info + Notice + Warning + Error + + + + + SysLog + + + + + +
+
+ + + + + + + 10 + 20 + 50 + 100 + 500 + + + + + + + + Direct + Blocked + Proxy + + + + + +
+
+ + + +
+
+ + X-Panel 免费基础版 +
+ + + +

+ * 美好的一天,从〔X-Panel 面板〕开始!* +

+

+ 您当前所使用的版本为:免费基础版 +

+ +
+ +
+
若需要使用〔Pro 版〕更多“新功能”,
+
请自助联系“授权码购买”机器人:
+
+ ------->>>> + + @Buy_ShouQuan_Bot + +
+
+ + + +
+ + 〔X-Panel-Pro 面板〕已实现功能: + + 点击查看 + +
+
+ + + 进入面板 + +
+
+ + + + + + + + + + + + + + + + + + +
+ +{{template "page/body_scripts" .}} +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} +{{template "component/aCustomStatistic" .}} +{{template "modals/textModal"}} + + + + +{{ template "page/body_end" .}} diff --git a/web/html/login.html b/web/html/login.html new file mode 100644 index 0000000..1989f2d --- /dev/null +++ b/web/html/login.html @@ -0,0 +1,644 @@ +{{ template "page/head_start" .}} + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + +
+
+ + + + + + + + + + + +
+ + + + + + +
+
+
+{{template "page/body_scripts" .}} +{{template "component/aThemeSwitch" .}} + +{{ template "page/body_end" .}} diff --git a/web/html/modals/client_bulk_modal.html b/web/html/modals/client_bulk_modal.html new file mode 100644 index 0000000..4cdc97c --- /dev/null +++ b/web/html/modals/client_bulk_modal.html @@ -0,0 +1,286 @@ +{{define "modals/clientsBulkModal"}} + + + + + Random + Random+Prefix + Random+Prefix+Num + Random+Prefix+Num+Postfix + Prefix+Num+Postfix + + + + + + + + + + + + + + + + + + + + [[ key ]] + + + + + {{ i18n "none" }} + [[ key ]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/modals/client_modal.html b/web/html/modals/client_modal.html new file mode 100644 index 0000000..8b57b8b --- /dev/null +++ b/web/html/modals/client_modal.html @@ -0,0 +1,172 @@ +{{define "modals/clientsModal"}} + + + {{template "form/client"}} + + +{{end}} diff --git a/web/html/modals/dns_presets_modal.html b/web/html/modals/dns_presets_modal.html new file mode 100644 index 0000000..010b973 --- /dev/null +++ b/web/html/modals/dns_presets_modal.html @@ -0,0 +1,110 @@ +{{define "modals/dnsPresetsModal"}} + + + + + + + [[ dns.name ]] + [[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]] + + + + {{ i18n "install" }} + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/inbound_info_modal.html b/web/html/modals/inbound_info_modal.html new file mode 100644 index 0000000..e99c327 --- /dev/null +++ b/web/html/modals/inbound_info_modal.html @@ -0,0 +1,628 @@ +{{define "modals/inboundInfoModal"}} + + + + + + + + + + + + + + + + +
{{ i18n "protocol" }} + [[ dbInbound.protocol ]] +
{{ i18n "pages.inbounds.address" }} + + [[ dbInbound.address ]] + +
{{ i18n "pages.inbounds.port" }} + [[ dbInbound.port ]] +
+
+ + + + + + + + + + + + + + + + + +
{{ i18n "encryption" }} + [[ inbound.settings.method ]] +
{{ i18n "password" }} + + [[ inbound.settings.password ]] + +
{{ i18n "pages.inbounds.network" }} + [[ inbound.settings.network ]] +
+ + +
+ +{{end}} diff --git a/web/html/modals/inbound_modal.html b/web/html/modals/inbound_modal.html new file mode 100644 index 0000000..fdd39db --- /dev/null +++ b/web/html/modals/inbound_modal.html @@ -0,0 +1,314 @@ +{{define "modals/inboundModal"}} + + {{template "form/inbound"}} + + +{{end}} diff --git a/web/html/modals/prompt_modal.html b/web/html/modals/prompt_modal.html new file mode 100644 index 0000000..5073650 --- /dev/null +++ b/web/html/modals/prompt_modal.html @@ -0,0 +1,71 @@ +{{define "modals/promptModal"}} + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/qrcode_modal.html b/web/html/modals/qrcode_modal.html new file mode 100644 index 0000000..cb4e1c1 --- /dev/null +++ b/web/html/modals/qrcode_modal.html @@ -0,0 +1,304 @@ +{{define "modals/qrcodeModal"}} + + + + + + + + + + + +{{end}} diff --git a/web/html/modals/text_modal.html b/web/html/modals/text_modal.html new file mode 100644 index 0000000..38844e9 --- /dev/null +++ b/web/html/modals/text_modal.html @@ -0,0 +1,51 @@ +{{define "modals/textModal"}} + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/two_factor_modal.html b/web/html/modals/two_factor_modal.html new file mode 100644 index 0000000..1029ce2 --- /dev/null +++ b/web/html/modals/two_factor_modal.html @@ -0,0 +1,125 @@ +{{define "modals/twoFactorModal"}} + + + + + + + +{{end}} diff --git a/web/html/modals/warp_modal.html b/web/html/modals/warp_modal.html new file mode 100644 index 0000000..4bfb7ca --- /dev/null +++ b/web/html/modals/warp_modal.html @@ -0,0 +1,246 @@ +{{define "modals/warpModal"}} + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/xray_balancer_modal.html b/web/html/modals/xray_balancer_modal.html new file mode 100644 index 0000000..fea4019 --- /dev/null +++ b/web/html/modals/xray_balancer_modal.html @@ -0,0 +1,123 @@ +{{define "modals/balancerModal"}} + + + + + + + + Random + Round Robin + Least Load + Least Ping + + + + + [[ tag ]] + + + + + [[ tag ]] + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/xray_dns_modal.html b/web/html/modals/xray_dns_modal.html new file mode 100644 index 0000000..484bd2f --- /dev/null +++ b/web/html/modals/xray_dns_modal.html @@ -0,0 +1,127 @@ +{{define "modals/dnsModal"}} + + + + + + + + + + + [[ l ]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/xray_fakedns_modal.html b/web/html/modals/xray_fakedns_modal.html new file mode 100644 index 0000000..f1f4429 --- /dev/null +++ b/web/html/modals/xray_fakedns_modal.html @@ -0,0 +1,56 @@ +{{define "modals/fakednsModal"}} + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/modals/xray_outbound_modal.html b/web/html/modals/xray_outbound_modal.html new file mode 100644 index 0000000..2edb5fc --- /dev/null +++ b/web/html/modals/xray_outbound_modal.html @@ -0,0 +1,127 @@ +{{define "modals/outModal"}} + + {{template "form/outbound"}} + + +{{end}} diff --git a/web/html/modals/xray_reverse_modal.html b/web/html/modals/xray_reverse_modal.html new file mode 100644 index 0000000..22f0431 --- /dev/null +++ b/web/html/modals/xray_reverse_modal.html @@ -0,0 +1,138 @@ +{{define "modals/reverseModal"}} + + + + + [[ x ]] + + + + + + + + + + + + + +{{end}} diff --git a/web/html/modals/xray_rule_modal.html b/web/html/modals/xray_rule_modal.html new file mode 100644 index 0000000..4440b9b --- /dev/null +++ b/web/html/modals/xray_rule_modal.html @@ -0,0 +1,237 @@ +{{define "modals/ruleModal"}} + + + + + + + + + + + + + [[ x ]] + + + + + [[ x ]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [[ tag ]] + + + + + [[ tag ]] + + + + + + [[ tag ]] + + + + + +{{end}} diff --git a/web/html/navigation.html b/web/html/navigation.html new file mode 100644 index 0000000..8521d70 --- /dev/null +++ b/web/html/navigation.html @@ -0,0 +1,428 @@ +{{ template "page/head_start" .}} + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + + + + + + +
+ +

一、〔X-Panel面板〕交流群与安装教程

+
+

点击加入〔X-Panel面板〕交流群:https://t.me/XUI_CN

+

详细安装流程步骤:https://xeefei.blogspot.com/2025/09/x-panel.html

+
+ +

二、判断VPS服务器的IP是否“送中”及解决方法

+
    +
  1. 判断方法:
  2. +
  3. 点击打开 https://music.youtube.com/,能正常访问,代表没“送中”,反之就是“送中”了。
  4. +
  5. 如果送中了如何解决去【拉回来】?解决方法:
  6. +
  7. 关闭/取消登录了谷歌账户的APP定位权限/授权;
  8. +
  9. 将常用的一个谷歌账号的位置记录功能打开;
  10. +
  11. 打开谷歌浏览器,登录开了位置记录功能的谷歌账号,并安装 Location Guard 扩展插件;
  12. +
  13. 打开Location Guard插件,选择Fixed Location,并在给出的地图上单击,即可标记上你想要IP所处的国家/地区 Google IP定位错误,使用Location Guard修改;
  14. +
  15. 转换到Options选项,Default level默认设置为Use fixed location;
  16. +
  17. 访问 谷歌地图服务 获取当前GPS位置,确认是否已修改成功;
  18. +
  19. 谷歌搜索my ip,即可看到谷歌IP定位到了刚才地图上标记的位置;
  20. +
  21. 最后,通过 此网页向谷歌报告IP问题
  22. +
+
+ +

三、在VPS服务器部署“订阅转换”功能

+
    +
  1. 操作步骤::
  2. +
  3. 进入脚本输入x-ui命令调取面板,选择第【25】选项安装订阅转换模块;
  4. +
  5. 等待安装【订阅转换】成功之后,访问地址:https://你的域名:15268
  6. +
  7. 因为在转换过程中需要调取后端API,所以请确保端口 8000 和 15268 是打开放行的;
  8. +
  9. 直接复制脚本中提供的【登录地址】,进入后台,点击【节点列表】----->>>【添加节点】;
  10. +
  11. 接下来点击左边侧边栏的【订阅列表】去【添加订阅】;
  12. +
  13. 最后一步,点击【客户端】,即可导入Clash等软件中使用;
  14. +
  15. 此功能集成到〔X-Panel面板〕中,是为了保证安全,不会造成链接泄露。
  16. +
+
+ +

四、如何保护自己的IP不被封锁被墙掉?

+
    +
  1. 使用安全代理协议:: 加密是必备,推荐使用vless+reality+vision协议组合;
  2. +
  3. 避免共享节点:: 尽量不要在不同的地区,多个省份之间不要共同连接同一个IP;
  4. +
  5. 隔离IP和端口:: 避免多个用户或设备在不同地理位置漫游时使用同一个IP和端口,要分开;
  6. +
  7. 控制流量:: 不要在单台VPS上长时间进行大流量下载,在一天内不要流量过高,适时切换节点;
  8. +
  9. 使用高位端口:: 创建入站时,尽量使用 40000 至 65000 之间的高位端口号。
  10. +
  11. 核心总结:: 不要多终端、多省份、多朋友共同使用同一个IP和端口。多创建几个入站,各用各的,避免被 GFW 识别为机场特征。 + 使用〔X-Panel面板〕多创建几个【入站】, 多做几条备用,各用各的!各行其道才比较安全! + GFW的思维模式是干掉机场,机场的特征个人用户不要去沾染,自然IP就保护好了。
  12. +
+
+ +

五、检测IP纯净度

+

访问 https://scamalytics.com/,输入IP检测欺诈分数,分数越高代表IP越“脏”。

+
+ +

六、查看指定端口的网络连接数

+

Linux 命令:
+ netstat -ntu | grep :节点端口 | grep ESTABLISHED | awk '{print $5}'

+
+ +

七、如何用 X-Panel 实现“自己偷自己”?

+
    +
  1. 其实很简单,只要你为面板设置了证书, 开启了HTTPS登录,就可以将〔X-Panel面板〕自身作为web server, 无需Nginx等;
  2. +
  3. 这里给一个示例: 其中目标网站(Dest)请填写面板监听端口;
  4. +
  5. 可选域名(SNI)填写面板登录域名, 如果您使用其他web server(如nginx)等, 将目标网站改为对应监听端口也可;
  6. +
  7. 注意:需要说明的是,如果您处于白名单地区,自己“偷”自己并不适合你;
  8. +
  9. 可选域名一项实际上可以填写任意SNI,只要客户端保持一致即可,不过并不推荐这样做。
  10. +
+
+ +

八、项目声明与注意

+
+
    +
  1. 声明: 此项目仅供个人学习、交流使用,请遵守当地法律法规,勿用于非法用途;请勿用于生产环境。
  2. +
  3. 注意: 在使用此项目和〔教程〕过程中,若因违反以上声明使用规则而产生的一切后果由使用者自负。
  4. +
+
+
+
+
+ + + +
+ +

一、若此项目对你有帮助,可以考虑通过以下链接购买VPS:

+
+
    +
  1. 搬瓦工GIA线路: https://bandwagonhost.com/aff.php?aff=75015
  2. +
  3. Dmit高端GIA机: https://www.dmit.io/aff.php?aff=9326
  4. +
  5. Gomami亚太顶尖优化线路: https://gomami.io/aff.php?aff=174
  6. +
  7. ISIF优质亚太优化线路: https://cloud.isif.net/login?affiliation_code=333
  8. +
  9. ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选: https://my.zorocloud.com/aff.php?aff=1072
  10. +
  11. 三网直连 IEPL / IPLC 直播流量转发: https://idc333.top/#register/BCUZXNELNO
  12. +
  13. Bagevm优质落地鸡(原生IP全解锁): https://www.bagevm.com/aff.php?aff=754
  14. +
  15. 白丝云【4837】量大管饱: https://cloudsilk.io/aff.php?aff=706
  16. +
  17. RackNerd极致性价比机器: https://my.racknerd.com/aff.php?aff=15268&pid=912
  18. +
+
+ +

二、项目相关

+
+

--->合作咨询请联系作者<---

+

〔X-Panel面板〕项目地址

+
+
+
+
+ + + +
+ +

一、关于【Pro版】的“收费说明”

+
    +
  1. 【授权码】目前定价: 100RMB 或 15U 一个,一机一码,不能重复用于不同机器 VPS,后期视情况不定时会上涨价格;对于〔X-Panel 面板〕后期的“新功能”,都将在【付费Pro版】中进行更新。
  2. +
  3. 目前的【安装界面】: 有两种可选,“免费基础版”一样可用,只是后期不再提供技术支持和重大更新,另外,在【免费基础版】中,【一键配置】功能将不再可用,全部放到了【付费Pro版】中。
  4. +
  5. 后期的开发精力: 全部会放到【付费Pro版】中,免费基础版不删库,持续保留,会大幅降低更新频率,后期只会同步更新 Xray 那边的【内核版本】等基础,想继续用的不影响,只是没有【新功能】可用,翻墙也足够。
  6. +
+
+ +

二、〔付费Pro版〕已实现的功能

+
    +
  1. 新增 -【付费Pro版】的面板后台UI: 添加醒目的“X-Panel-Pro”标识;
  2. +
  3. 优化 -【付费Pro版】TG端 【版本更新】提示功能: 增加详细的“更新说明”;
  4. +
  5. 增加 -〔Pro版面板后台〕: 使用 Reality 协议时,可点击“随机更换”所偷的域名;
  6. +
  7. 新增 - 【付费Pro版】TG端 的【发送授权报告】: 增加“唯一授权防伪码”;
  8. +
  9. 优化 -【付费Pro版】安装脚本界面: 增加【Pro版】该有的“明确标识”;
  10. +
  11. 优化 -【付费Pro版】TG端的显示方式: 增加该有的“会员标识”;
  12. +
  13. 新增 -【付费Pro版】安装脚本: 有“网页版SSH工具”可选部署,脚本中〔第26选项〕;
  14. +
  15. 新增 - 【付费Pro版】安装脚本: 有“线路和IP质量检测”可去使用,脚本中〔第27选项〕;
  16. +
  17. 新增 - 【付费Pro版】安装脚本: 有“地区服务器DNS检测”可去使用,脚本中〔第28选项〕;
  18. +
  19. 新增 -【付费Pro版】---->>>TG端: 同步有“网页版SSH工具”可选安装;
  20. +
  21. 优化 - 【付费Pro版】---->>>TG端: 点击“服务器状态”时的“版本号显示”;
  22. +
  23. 说明 - 【付费Pro版】TG端中: 使用命令:/webssh,安装“网页版SSH”;
  24. +
  25. 优化 -〔Pro版〕中的〔一键配置〕功能: 有更友好的提示方式;
  26. +
  27. 新增 -【付费Pro版】---->>>面板后台的【首页 UI】: 目前是有“5种”可选:标准布局 (默认),炫彩动画,深海科技,暮光薰衣,和幽林秘境;你喜欢什么类型的主题,就去点击“选定”之后,就不会自动变了,若后期需要更换,就重选;
  28. +
  29. 新增 -【付费Pro版】---->>>在“创建入站”时: 可以在页面上更加方便地选择【重置流量】的方式:有每日重置,每周重置,按月重置,或从不重置;
  30. +
  31. 新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“每日报告”: 可定制【发送内容】,自己可点击“打开或关闭”,并且可以选择【发送时间】,可按天,或者每周,每月发;
  32. +
  33. 优化 -【付费Pro版】的“授权码验证机制”: 增加【后台联网验证】,以及“机器指纹”等属性;
  34. +
  35. 新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“多面板管理”: 一个机器人可同时管理其他面板,可以很丝滑地远程操作【被控端 VPS】;
  36. +
  37. 新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“获取节点链接”功能: 支持【本机】和【远程被控端 VPS】都能获取,开发此功能的目的在于:不用进〔面板后台〕,就能在 TG端 获取到之前已经创建过的“链接”;
  38. +
  39. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“一键部署中转节点”: 解决了不懂配置的麻烦,已实现:远程Socks5创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”,“小手一点”,直接可用;
  40. +
  41. 新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”: 有“备用方式申请证书”,当用常规方式【1】申请不下来时,可以试试“备用方式5”;
  42. +
  43. 新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”: 有“可自定义证书路径”,自己进入 VPS 中“手动上传证书”,复制路径,在脚本中填入即可;
  44. +
  45. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“深度调优脚本”: 包含 BBR+FQ, TCP Fast Open, 内存缓冲区及队列优化,在〔Pro版〕脚本中“第29选项”可直接用;
  46. +
  47. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“首页会员等级”显示: 能够明确展示:自己的会员等级,授权码信息,以及“版本更新”提示;
  48. +
  49. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“节点上/下线TG通知”功能: 对于【拼车】的宝子,能明确知道:哪个节点,什么时候上线?或者下线时间,做到“心中有数”;
  50. +
  51. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端 签到得积分”功能: 后期针对有【积分】的宝子,会不断推出:相应的【特权】和【福利待遇】,
  52. +
  53. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端 积分多重功能”;推出: 积分查询,积分换购,授权码查询,修改用户名,积分转移/打赏,以及“积分排行榜”,
  54. +
  55. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出【“积分换购”的可用功能】: A、消耗1000积分“自助重置换绑时间”,B、消耗5000积分“自助换购一个普通授权码”,
  56. +
  57. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“远程备份 + 急救还原”功能: 面板报错“崩了”,不用像之前那样:卸载面板 -->> 重装面板,更不用很麻烦去“重装系统”解决,直接:远程急救还原,前提就是:你自己要知道,在面板“正常运行”的时候,去「备份数据快照」,
  58. +
  59. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“每月重置流量”功能: 可输入1—31之间的任意数字,比如:输入12,即代表“每月12号”「重置入站流量」,以便提供更友好的“重置流量方式”,
  60. +
  61. 新增 -〔X-Panel 面板〕----->>【付费Pro版】“批量部署节点”功能: 可直接在面板后台的“一键配置”中去使用,点击一次可批量部署生成10条「VLESS + TCP + Reality + Vision」协议组合的入站,
  62. +
  63. 新增 -〔X-Panel 面板〕----->>【付费Pro版】推出“购买机器人”功能: 可自助全自动在“机器人”中:购买授权码,增加配额,充值积分,自助重置换绑等,联系:https://t.me/Buy_ShouQuan_Bot
  64. +
+
+
+
+
+ + +
+ +

一、“授权码说明”与批量折扣

+

【授权码】100RMB 或 15U一个,一机一码。
+ 包括:重装,后期的升级/更新,都能使用,但是不能重复用于不同的机器。
+ 所以推荐稳定使用的机器用【授权码】。
+ 注:“授权码”属虚拟商品,购买之后,一经激活生效,概不退款,
+ 对于一年期限(年付/年抛)的机器,后期可以【换绑】,
+ 为什么要有时间限制?就是为了防止,有些人拿【授权码】滥用。
+ 购买方式/渠道联系机器人:https://t.me/Buy_ShouQuan_Bot +

+

经常换的机器,去使用“免费基础版”就行,目前的【安装界面】是有“两种方式”可选择的。

+
+ +

【批量授权】折扣(适用于业务需求、Tk、跨境电商等):

+ +

若都要用【收费Pro版】的话,可以用【批量授权】,
以下列举出来的,就是【批量折扣】的统一“优惠”。
注:〔批量授权码〕要求最低5台以上,是“一码通用”,
一个“授权码”可以绑定验证多台 VPS 机器,并且有专属的“豹子号”授权码。

+
    +
  • 5 ——> 20台: 8折,尾号:5555
  • +
  • 20 ——> 50台: 7折,尾号:66666
  • +
  • 50 ——> 100台: 6折,尾号:777777
  • +
  • 100 ——> 200台: 5折,尾号:8888888
  • +
  • 200台 ——> 以上: 4折,尾号:99999999
  • +
+
+ +

二、购买赞助方式

+

1、若您需要购买【授权码】,请跟下面这个“机器人”去对话,

+

———————————————-

+ https://t.me/Buy_ShouQuan_Bot
+ (授权码购买机器人)

+ ———————————————-
+ 输入:/start 或 购买,即可“在线下单”,

+ 2、弹出【购买页面】,选择或输入“数量”,
+ 付款了支付系统会收到回调,金额到账之后,
+ 就会通过那个机器人【发放授权码】给您,
+ 整个流程,是【全自助】的“自动处理”方式,
+ 若您是〔增加配额〕,基本也是一样的流程,

+ 3、请注意:机器人发给您的所有信息,
+ 尽量都去自己【耐心阅读】一遍,“使用说明”,
+ 以及〔VIP 群〕的信息,也全部都包含在里面的,

+ 4、按照之前您安装更新〔X-Panel 面板〕的方式,
+ 直接重新输入【安装命令】,选择【2】,就能去,
+ 把之前的【免费版】,“无缝升级”到最新的【Pro版】。

+
+ +

三、关于【授权码】的一些“问题解答”

+
+

1、“授权码”的有效期是多久?一直有效吗?能一直用吗?

+

Answer:一直有效,只要不换机器,能一直用,但是,【换绑机器】有“一年时间的冷却期”,
“验证系统”会自动从你绑定这个授权码的时候开始计算,自动判断“换绑剩余时间”,
如果您等不及一年时间,需要提前【换绑机器】,那就绑定TG机器人,
每天“签到拿积分”,可以自己用“积分”去自助重置换绑,只要您有积分,想什么时候换绑随您愿意,
意思就是说:现在不限制了,用“1000积分”或者去〔购买机器人〕花20元/次可以“自由换绑”。

+
+

2、“授权码”都有什么样的?我怎么能得到一个顺一点的“授权码”?

+

Answer:目前的【授权码】,分为:普通授权码,除了开头是“XPANEL”以外,其他完全随机;
【批量豹子号授权码】,根据一次性“购买数量”的不同,有相应的“规则”去生成对应不同的“豹子号尾号”,规则是在程序中“写死了的”;
并且,不管“普通授权码”,还是“豹子号授权码”,都是“大写字母+数字”的组合,至少24位以上。

+
+

3、那我的【授权码】怎么才能升级到【豹子号授权码】?

+

Answer:两种方式:另外补足差价购买【豹子号】,或推广卖【授权码】拿“返佣”,
因为【批量授权】有折扣力度,所以这个就相当于自己可以拿【配额】出去卖“授权码”。

+
+

4、为什么有时候重装系统【授权码】会显示“换绑冻结”?

+

Answer:因为【授权码】跟 VPS 机器是绑定在一起的,而这个绑定,
识别的是由这台 VPS 机器的“硬件信息”构成的【机器指纹】,这个是唯一的,
一般来讲,重装系统,不会导致这个【机器指纹】改变,
但是,有的宝子,会存在极少数情况,重装系统之后,这台 VPS 的【机器指纹】变了,导致“授权码”冻结。
+ 所以,建议就是:尽量一次性把系统和软件什么的都弄好,既然你这台 VPS 重装系统会变,那只能稳定使用之后,一次性搞好别轻易重装,
至于:测试,练手,可以先拿【免费基础版】去搞,熟悉了之后,再从【免费版】“无缝升级”到【Pro版】,所有的“节点数据”都是在的,
PS:【授权码验证系统】识别绑定的是“机器指纹”,跟 IP 没有关系,即使 IP 被墙,
只要您没有更换机器,换 IP 都不影响;另外,重装系统尽量用 DD脚本 去搞,指纹不会变。

+
+
+
+
+ + +
+ +

一、常见的代理软件/工具

+
+
    +
  1. Windows系统v2rayN: https://github.com/2dust/v2rayN
  2. +
  3. 安卓手机版【v2rayNG】: https://github.com/2dust/v2rayNG
  4. +
  5. 苹果手机IOS【小火箭】: https://apple02.com/
  6. +
  7. 苹果MacOS电脑【Clash Verge】: https://github.com/clash-verge-rev/clash-verge-rev/releases
  8. +
+
+ +

二、“接码”网站

+

https://sms-activate.org/cn直接注册账号购买,可用于注册各种在线服务。

+
+ +

三、常用网站和群组

+
+
    +
  1. NodeSeek 论坛: https://www.nodeseek.com/
  2. +
  3. V2EX 论坛:: https://www.v2ex.com/
  4. +
  5. 搬瓦工 TG 群:: https://t.me/BWHOfficial
  6. +
  7. Xray 官方群:: https://t.me/projectXray
  8. +
  9. Dmit 交流群:: https://t.me/DmitChat
  10. +
  11. 白丝云用户群:: https://t.me/+VHZLKELTQyzPNgOV
  12. +
  13. NameSilo 域名注册:: https://www.namesilo.com/
  14. +
+
+ +
+

四、其他内容

+
+

油管4K测速:https://www.youtube.com/results?search_query=4k%E6%B5%8B%E9%90%A6

+

Project X:https://xtls.github.io/

+

我的IP查询:https://whatismyipaddress.com/

+

Google翻译:https://translate.google.com/?hl=zh-CN

+
+
+
+
+
+
+
+
+
+ +{{template "page/body_scripts" .}} +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} + +{{ template "page/body_end" .}} diff --git a/web/html/servers.html b/web/html/servers.html new file mode 100644 index 0000000..14ce012 --- /dev/null +++ b/web/html/servers.html @@ -0,0 +1,509 @@ +{{ template "page/head_start" .}} + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + + + + + + + + + + + + + + 添加〔被控端 VPS〕 + + + + + 当前已绑定:[[ normalCount ]] 台 (最多可绑:[[ maxLimit ]] 台) + + + + 刷新列表 + + + + 配置本机“主控机器人” + + + + + + + + + + + + + + +
+ + 说明:在此添加远程中转机(落地机)信息,点击“一键部署”即可自动完成: + 远程Socks创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”。 +
+ + + 添加〔中转机 VPS〕 + + + + + 当前已绑定:[[ transitCount ]] 台 (最多可绑:[[ maxLimit ]] 台) + + + + 刷新列表 + + + + 检测中转节点“连通性” + + +
+
+ + + + +
+ +
+
+
+
+
+
+ + + + + + + + + * 必须包含协议头 (https://) 和 “端口 + 路径” + + + + + + + + + +
+ 1、此功能用于集中管理多个〔X-Panel 面板〕,
+ 2、是通过在后台〔配置绑定机器人〕去管理的,
+ 3、若在输入框您已经添加了〔被控端 的 VPS 信息〕,
+ 4、则您不能在对应的那台 VPS 中去绑定任何其他机器人,
+ 5、并且,您的〔被控端 VPS〕面板不能去开启“两步验证”。 +
+ +
+ 1、此功能仅用于建立〔VLESS Reality --> Socks5〕中转链路,
+ 2、在此处添加的〔中转/落地机 VPS〕面板不能去开启“两步验证”,
+ 3、在添加输入信息后,请点击列表中的“一键部署”按钮即可去使用,
+ 4、而“中转/落地机”无需做任何设置,只需确保防火墙放行端口即可,
+ 5、请勿在连“本机节点”时去进行〔一键部署〕,因“Xray重启”会有卡顿,
+ 6、注:在已经创建好“中转节点”之后,勿去随意更改“用户Email + 端口”。 +
+ +
+ + +
+ +
[[ resultSubtitle ]]
+ +

链路:本机(Reality) --->> 中转机(Socks) --->> 互联网

+ +
+

(点击二维码或链接即可复制)

+ +

👇 VLESS Reality 中转链接:

+ + + +
+
+ + + + + + + + + + + + +
+ 关闭 +
+
+ +
+ +{{template "page/body_scripts" .}} +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} + + +{{ template "page/body_end" .}} diff --git a/web/html/settings.html b/web/html/settings.html new file mode 100644 index 0000000..d721f2b --- /dev/null +++ b/web/html/settings.html @@ -0,0 +1,612 @@ +{{ template "page/head_start" .}} + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + + + + + + + + + + + + + + + +{{template "page/body_scripts" .}} + + + +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} +{{template "component/aSettingListItem" .}} +{{template "modals/twoFactorModal"}} + +{{ template "page/body_end" .}} diff --git a/web/html/settings/panel/general.html b/web/html/settings/panel/general.html new file mode 100644 index 0000000..d12e534 --- /dev/null +++ b/web/html/settings/panel/general.html @@ -0,0 +1,155 @@ +{{define "settings/panel/general"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/settings/panel/security.html b/web/html/settings/panel/security.html new file mode 100644 index 0000000..2a570fa --- /dev/null +++ b/web/html/settings/panel/security.html @@ -0,0 +1,44 @@ +{{define "settings/panel/security"}} + + + + + + + + + + + + + + + + + + + + + {{ i18n "confirm" }} + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/panel/subscription/general.html b/web/html/settings/panel/subscription/general.html new file mode 100644 index 0000000..b6e616d --- /dev/null +++ b/web/html/settings/panel/subscription/general.html @@ -0,0 +1,98 @@ +{{define "settings/panel/subscription/general"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/panel/subscription/json.html b/web/html/settings/panel/subscription/json.html new file mode 100644 index 0000000..c8575e3 --- /dev/null +++ b/web/html/settings/panel/subscription/json.html @@ -0,0 +1,192 @@ +{{define "settings/panel/subscription/json"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Remove + + + + Add Noise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/panel/telegram.html b/web/html/settings/panel/telegram.html new file mode 100644 index 0000000..248831f --- /dev/null +++ b/web/html/settings/panel/telegram.html @@ -0,0 +1,87 @@ +{{define "settings/panel/telegram"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/xray/advanced.html b/web/html/settings/xray/advanced.html new file mode 100644 index 0000000..28e3187 --- /dev/null +++ b/web/html/settings/xray/advanced.html @@ -0,0 +1,14 @@ +{{define "settings/xray/advanced"}} + + + + {{ i18n "pages.xray.completeTemplate"}} + {{ i18n "pages.xray.Inbounds" }} + {{ i18n "pages.xray.Outbounds" }} + {{ i18n "pages.xray.Routings" }} + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/xray/balancers.html b/web/html/settings/xray/balancers.html new file mode 100644 index 0000000..af7def7 --- /dev/null +++ b/web/html/settings/xray/balancers.html @@ -0,0 +1,53 @@ +{{define "settings/xray/balancers"}} + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/xray/basics.html b/web/html/settings/xray/basics.html new file mode 100644 index 0000000..71aa0d7 --- /dev/null +++ b/web/html/settings/xray/basics.html @@ -0,0 +1,265 @@ +{{define "settings/xray/basics"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ i18n "pages.settings.resetDefaultConfig" }} + + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/xray/dns.html b/web/html/settings/xray/dns.html new file mode 100644 index 0000000..b2334d6 --- /dev/null +++ b/web/html/settings/xray/dns.html @@ -0,0 +1,169 @@ +{{define "settings/xray/dns"}} + + + + + + + + + + + +{{end}} diff --git a/web/html/settings/xray/outbounds.html b/web/html/settings/xray/outbounds.html new file mode 100644 index 0000000..a5211fc --- /dev/null +++ b/web/html/settings/xray/outbounds.html @@ -0,0 +1,76 @@ +{{define "settings/xray/outbounds"}} + + + + + + {{ i18n "pages.xray.outbound.addOutbound" }} + + WARP + + + + + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/xray/reverse.html b/web/html/settings/xray/reverse.html new file mode 100644 index 0000000..c15b4a8 --- /dev/null +++ b/web/html/settings/xray/reverse.html @@ -0,0 +1,39 @@ +{{define "settings/xray/reverse"}} + + +{{end}} \ No newline at end of file diff --git a/web/html/settings/xray/routing.html b/web/html/settings/xray/routing.html new file mode 100644 index 0000000..e5b9b6c --- /dev/null +++ b/web/html/settings/xray/routing.html @@ -0,0 +1,119 @@ +{{define "settings/xray/routing"}} + + {{ i18n "pages.xray.rules.add" }} + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/html/xray.html b/web/html/xray.html new file mode 100644 index 0000000..6071606 --- /dev/null +++ b/web/html/xray.html @@ -0,0 +1,1487 @@ +{{ template "page/head_start" .}} + + + + + +{{ template "page/head_end" .}} + +{{ template "page/body_start" .}} + + + + + + + + + + + + + + + + + + + + + + + {{ i18n "pages.xray.save" }} + + + {{ i18n "pages.xray.restart" }} + + + {{ i18n "pages.index.xrayErrorPopoverTitle" }} + + + + + + + + + + + + + + + + {{ template "settings/xray/basics" . }} + + + + {{ template "settings/xray/routing" . }} + + + + {{ template "settings/xray/outbounds" . }} + + + + {{ template "settings/xray/reverse" . }} + + + + {{ template "settings/xray/balancers" . }} + + + + {{ template "settings/xray/dns" . }} + + + + {{ template "settings/xray/advanced" . }} + + + + + + + + + +{{template "page/body_scripts" .}} + + + + + + + + + + + +{{template "component/aSidebar" .}} +{{template "component/aThemeSwitch" .}} +{{template "component/aTableSortable" .}} +{{template "component/aSettingListItem" .}} +{{template "modals/ruleModal"}} +{{template "modals/outModal"}} +{{template "modals/reverseModal"}} +{{template "modals/balancerModal"}} +{{template "modals/dnsModal"}} +{{template "modals/dnsPresetsModal"}} +{{template "modals/fakednsModal"}} +{{template "modals/warpModal"}} + +{{ template "page/body_end" .}} diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go new file mode 100644 index 0000000..3fb1d4c --- /dev/null +++ b/web/job/check_client_ip_job.go @@ -0,0 +1,714 @@ +package job + +import ( + "bufio" + "encoding/json" + "io" + "log" + "os" + "os/exec" + "regexp" + "runtime" + "sort" + "time" + "sync" + "crypto/rand" + "encoding/hex" + "fmt" // 中文注释 (新增): 导入 fmt 包用于格式化消息 + + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/xray" + "x-ui/web/service" +) + +// ================================================================= +// 中文注释: 以下是用于实现设备限制功能的核心代码 +// ================================================================= + +// ActiveClientIPs 中文注释: 用于在内存中跟踪每个用户的活跃IP (TTL机制) +// 结构: map[用户email] -> map[IP地址] -> 最后活跃时间 +var ActiveClientIPs = make(map[string]map[string]time.Time) +var activeClientsLock sync.RWMutex + +// ClientStatus 中文注释: 用于跟踪每个用户的状态(是否因为设备超限而被禁用) +// 结构: map[用户email] -> 是否被禁用(true/false) +var ClientStatus = make(map[string]bool) +var clientStatusLock sync.RWMutex + +// CheckDeviceLimitJob 中文注释: 这是我们的设备限制任务的结构体 +type CheckDeviceLimitJob struct { + inboundService service.InboundService + xrayService *service.XrayService + // 中文注释: 新增 xrayApi 字段,用于持有 Xray API 客户端实例 + xrayApi xray.XrayAPI + // lastPosition 中文注释: 用于记录上次读取 access.log 的位置,避免重复读取 + lastPosition int64 + // 〔中文注释〕: 注入 Telegram 服务用于发送通知,确保此行存在。 + telegramService service.TelegramService + + // violationStartTime: 记录用户“开始设备超限”的时间。 + // 用于实现“观察期”:刚发现超限时不封,等 3 分钟后如果还超限才封。 + violationStartTime map[string]time.Time + + // triggerLock: 保护上述 Map 的读写安全 + triggerLock sync.Mutex +} + +// RandomUUID 中文注释: 新增一个辅助函数,用于生成一个随机的 UUID +func RandomUUID() string { + uuid := make([]byte, 16) + rand.Read(uuid) + uuid[6] = (uuid[6] & 0x0f) | 0x40 + uuid[8] = (uuid[8] & 0x3f) | 0x80 + return hex.EncodeToString(uuid[0:4]) + "-" + hex.EncodeToString(uuid[4:6]) + "-" + hex.EncodeToString(uuid[6:8]) + "-" + hex.EncodeToString(uuid[8:10]) + "-" + hex.EncodeToString(uuid[10:16]) +} + +// NewCheckDeviceLimitJob 中文注释: 创建一个新的任务实例 +// 〔中文注释〕:增加一个 service.TelegramService 类型的参数。 +func NewCheckDeviceLimitJob(xrayService *service.XrayService, telegramService service.TelegramService) *CheckDeviceLimitJob { + return &CheckDeviceLimitJob{ + xrayService: xrayService, + // 中文注释: 初始化 xrayApi 字段 + xrayApi: xray.XrayAPI{}, + // 〔中文注释〕: 将传入的 telegramService 赋值给结构体实例。 + telegramService: telegramService, + + // 初始化防抖 Map + violationStartTime: make(map[string]time.Time), + } +} + +// Run 中文注释: 定时任务的主函数,每次定时器触发时执行 +func (j *CheckDeviceLimitJob) Run() { + // 中文注释: 检查 xray 是否正在运行,如果xray没运行,则无需执行此任务 + if !j.xrayService.IsXrayRunning() { + return + } + + // 1. 清理过期的IP + j.cleanupExpiredIPs() + + // 2. 解析新的日志并更新IP列表 + j.parseAccessLog() + + // 3. 检查所有用户的设备限制状态 + j.checkAllClientsLimit() +} + +// cleanupExpiredIPs 中文注释: 清理长时间不活跃的IP +func (j *CheckDeviceLimitJob) cleanupExpiredIPs() { + activeClientsLock.Lock() + defer activeClientsLock.Unlock() + + now := time.Now() + // 中文注释: 活跃判断窗口(TTL): 近3分钟内出现过就算“活跃” + const activeTTL = 3 * time.Minute + for email, ips := range ActiveClientIPs { + for ip, lastSeen := range ips { + // 中文注释: 如果一个IP超过3分钟没有新的连接日志,我们就认为它已经下线 + if now.Sub(lastSeen) > activeTTL { + delete(ActiveClientIPs[email], ip) + } + } + // 中文注释: 如果一个用户的所有IP都下线了,就从大Map中移除这个用户,节省内存 + if len(ActiveClientIPs[email]) == 0 { + delete(ActiveClientIPs, email) + } + } +} + +// parseAccessLog 中文注释: 解析 xray access log 来获取最新的用户IP信息 +func (j *CheckDeviceLimitJob) parseAccessLog() { + logPath, err := xray.GetAccessLogPath() + if err != nil || logPath == "none" || logPath == "" { + return + } + + file, err := os.Open(logPath) + if err != nil { + return + } + defer file.Close() + + // 中文注释: 移动到上次读取结束的位置,实现增量读取 + file.Seek(j.lastPosition, 0) + + scanner := bufio.NewScanner(file) + + // 中文注释: 使用正则表达式从日志行中提取 email 和 IP + emailRegex := regexp.MustCompile(`email: ([^ ]+)`) + ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`) + + activeClientsLock.Lock() + defer activeClientsLock.Unlock() + + now := time.Now() + for scanner.Scan() { + line := scanner.Text() + + emailMatch := emailRegex.FindStringSubmatch(line) + ipMatch := ipRegex.FindStringSubmatch(line) + + if len(emailMatch) > 1 && len(ipMatch) > 1 { + email := emailMatch[1] + ip := ipMatch[1] + + if ip == "127.0.0.1" || ip == "::1" { + continue + } + + if _, ok := ActiveClientIPs[email]; !ok { + ActiveClientIPs[email] = make(map[string]time.Time) + } + ActiveClientIPs[email][ip] = now + } + } + + currentPosition, err := file.Seek(0, os.SEEK_END) + if err == nil { + if currentPosition < j.lastPosition { + j.lastPosition = 0 + } else { + j.lastPosition = currentPosition + } + } +} + +// checkAllClientsLimit 中文注释: 核心功能,检查所有用户,对超限的执行封禁,对恢复的执行解封 +func (j *CheckDeviceLimitJob) checkAllClientsLimit() { + db := database.GetDB() + var inbounds []*model.Inbound + // 中文注释: 这里仅查询启用了设备限制(device_limit > 0)并且自身是开启状态的入站规则 + db.Where("device_limit > 0 AND enable = ?", true).Find(&inbounds) + + if len(inbounds) == 0 { + return + } + + // 中文注释: 获取 API 端口。如果端口为0 (说明Xray未完全启动或有问题),则直接返回 + apiPort := j.xrayService.GetApiPort() + if apiPort == 0 { + return + } + // 中文注释: 使用获取到的端口号初始化 API 客户端 + j.xrayApi.Init(apiPort) + defer j.xrayApi.Close() + + // 中文注释: 优化 - 在一次循环中同时获取 tag 和 protocol + inboundInfoMap := make(map[int]struct { + Limit int + Tag string + Protocol model.Protocol + }) + for _, inbound := range inbounds { + inboundInfoMap[inbound.Id] = struct { + Limit int + Tag string + Protocol model.Protocol + }{Limit: inbound.DeviceLimit, Tag: inbound.Tag, Protocol: inbound.Protocol} + } + + activeClientsLock.RLock() + clientStatusLock.Lock() + defer activeClientsLock.RUnlock() + defer clientStatusLock.Unlock() + + // 第一步: 处理当前在线的用户 + for email, ips := range ActiveClientIPs { + traffic, err := j.inboundService.GetClientTrafficByEmail(email) + if err != nil || traffic == nil { + continue + } + + info, ok := inboundInfoMap[traffic.InboundId] + if !ok || info.Limit <= 0 { + continue + } + + isBanned := ClientStatus[email] + activeIPCount := len(ips) + + // ===================================================================== + // 设备限制的“观察期”逻辑 (完美解决切换网络误封问题) + // ===================================================================== + + // 场景 A:用户设备数超限,且当前未被封禁 + if activeIPCount > info.Limit && !isBanned { + j.triggerLock.Lock() + startTime, exists := j.violationStartTime[email] + + if !exists { + // 如果是第一次发现超限,不要急着封!记录当前时间,开始“观察” + j.violationStartTime[email] = time.Now() + logger.Infof("〔观察期〕检测到用户 %s 设备超限 (%d > %d),进入3分钟观察期,暂不封禁...", email, activeIPCount, info.Limit) + j.triggerLock.Unlock() + continue // 跳过本次循环,给用户一点时间(例如切换网络造成的双IP) + } + + // 如果已经处于观察期,计算已持续了多久 + // 【核心设置】:这里设置为 3 分钟 (180秒)。 + if time.Since(startTime) < 3*time.Minute { + j.triggerLock.Unlock() + // 还在观察期内,暂不封禁 + continue + } + + // 观察期结束,超限状态依然存在 -> 确认封禁! + // 封禁前先清除观察记录,以便下次使用 + delete(j.violationStartTime, email) + j.triggerLock.Unlock() + + // 执行原有的封禁逻辑 + j.banUser(email, activeIPCount, &info) + } + + // 场景 B:用户恢复正常 (IP数 <= 限制),或者已被封禁但现在设备数合规 + if activeIPCount <= info.Limit { + // 如果该用户之前在“观察名单”里,现在正常了,直接移除名单,皆大欢喜 + j.triggerLock.Lock() + if _, exists := j.violationStartTime[email]; exists { + delete(j.violationStartTime, email) + logger.Infof("〔观察期〕用户 %s 设备数量已恢复正常,观察期取消。", email) + } + j.triggerLock.Unlock() + + // 如果用户处于被封禁状态,执行解封 + if isBanned { + j.unbanUser(email, activeIPCount, &info) + } + } + } + + // 第二步: 专门处理那些“已被封禁”但“已不在线”的用户,为他们解封 + for email, isBanned := range ClientStatus { + if !isBanned { + continue + } + if _, online := ActiveClientIPs[email]; !online { + traffic, err := j.inboundService.GetClientTrafficByEmail(email) + if err != nil || traffic == nil { + continue + } + info, ok := inboundInfoMap[traffic.InboundId] + if !ok { + continue + } + logger.Infof("已封禁用户 %s 已完全下线,执行解封操作。", email) + + // 调用解封函数,这种情况下:活跃IP数为0,我们直接传入0用于记录日志 + j.unbanUser(email, 0, &info) + } + } +} + +// banUser 中文注释: 封装的封禁用户函数;IP数量超限,且用户当前未被封禁 -> 执行封禁 (UUID 替换) +func (j *CheckDeviceLimitJob) banUser(email string, activeIPCount int, info *struct { + Limit int + Tag string + Protocol model.Protocol +}) { + // ================================================================= + // 这一行代码是整个解封逻辑的灵魂! + // GetClientByEmail 函数会去查询您的数据库 (x-ui.db), + // 找到 `inbounds` 表,解析其中的 `settings` 字段,并从中去, + // 读取出您最初设置的、最原始、最正确的用户信息(包括最原始的UUID), + // 然后把它赋值给 `client` 这个变量;此时,`client` 变量就持有了那个“老链接”的正确原始 UUID。 + // ================================================================= + _, client, err := j.inboundService.GetClientByEmail(email) + if err != nil || client == nil { + return + } + logger.Infof("〔设备限制〕超限:用户 %s. 限制: %d, 当前活跃: %d. 执行封禁掐网。", email, info.Limit, activeIPCount) + + // 〔中文注释〕: 以下是发送 Telegram 通知的核心代码, + // 它会调用我们注入的 telegramService 的 SendMessage 方法。 + go func() { + // 〔中文注释〕: 在调用前,先判断服务实例是否为 nil,增加代码健壮性。 + if j.telegramService == nil { + return + } + tgMessage := fmt.Sprintf( + "〔X-Panel面板〕设备超限提醒\n\n"+ + "  ------------------------------------\n"+ + "  👤 用户 Email:%s\n"+ + "  🖥️ 设备限制数量:%d\n"+ + "  🌐 当前在线IP数:%d\n"+ + "  ------------------------------------\n\n"+ + "⚠ 该用户已被自动掐网封禁!", + email, info.Limit, activeIPCount, + ) + // 〔中文注释〕: 调用接口方法发送消息。 + err := j.telegramService.SendMessage(tgMessage) + if err != nil { + logger.Warningf("发送 Telegram 封禁通知失败: %v", err) + } + }() + + + // 中文注释: 步骤一:先从 Xray-Core 中删除该用户。 + j.xrayApi.RemoveUser(info.Tag, email) + + // ================================================================= + // 中文注释: 增加 5000 毫秒延时,解决竞态条件问题 + time.Sleep(5000 * time.Millisecond) + // ================================================================= + + // 中文注释: 创建一个带有随机UUID/Password的临时客户端配置用于“封禁” + tempClient := *client + + // 适用于 VMess/VLESS + if tempClient.ID != "" { tempClient.ID = RandomUUID() } + + // 适用于 Trojan/Shadowsocks/Socks + if tempClient.Password != "" { tempClient.Password = RandomUUID() } + + var clientMap map[string]interface{} + clientJson, _ := json.Marshal(tempClient) + json.Unmarshal(clientJson, &clientMap) + + // 中文注释: 步骤二:将这个带有错误UUID/Password的临时用户添加回去。 + // 客户端持有的还是旧的UUID,自然就无法通过验证,从而达到了“封禁”的效果。 + err = j.xrayApi.AddUser(string(info.Protocol), info.Tag, clientMap) + if err != nil { + logger.Warningf("通过API封禁用户 %s 失败: %v", email, err) + } else { + // 中文注释: 封禁成功后,在内存中标记该用户为“已封禁”状态。 + ClientStatus[email] = true + } +} + +// unbanUser 中文注释: 封装的解封用户函数;IP数量已恢复正常,但用户处于封禁状态 -> 执行解封 (恢复原始 UUID) +func (j *CheckDeviceLimitJob) unbanUser(email string, activeIPCount int, info *struct { + Limit int + Tag string + Protocol model.Protocol +}) { + _, client, err := j.inboundService.GetClientByEmail(email) + if err != nil || client == nil { + return + } + logger.Infof("〔设备数量〕已恢复:用户 %s. 限制: %d, 当前活跃: %d. 执行解封/恢复用户。", email, info.Limit, activeIPCount) + + // 中文注释: 步骤一:先从 Xray-Core 中删除用于“封禁”的那个临时用户。 + j.xrayApi.RemoveUser(info.Tag, email) + + // ================================================================= + // 中文注释: 同样增加 5000 毫秒延时,确保解封操作的稳定性 + time.Sleep(5000 * time.Millisecond) + // ================================================================= + + var clientMap map[string]interface{} + clientJson, _ := json.Marshal(client) + json.Unmarshal(clientJson, &clientMap) + + // 中文注释: 步骤二:将数据库中原始的、正确的用户信息重新添加回 Xray-Core,从而实现“解封”。 + err = j.xrayApi.AddUser(string(info.Protocol), info.Tag, clientMap) + if err != nil { + logger.Warningf("通过API恢复用户 %s 失败: %v", email, err) + } else { + // 中文注释: 解封成功后,从内存中移除该用户的“已封禁”状态标记。 + delete(ClientStatus, email) + } +} + +type CheckClientIpJob struct { + lastClear int64 + disAllowedIps []string +} + +var job *CheckClientIpJob + +func NewCheckClientIpJob() *CheckClientIpJob { + job = new(CheckClientIpJob) + return job +} + +func (j *CheckClientIpJob) Run() { + if j.lastClear == 0 { + j.lastClear = time.Now().Unix() + } + + shouldClearAccessLog := false + iplimitActive := j.hasLimitIp() + f2bInstalled := j.checkFail2BanInstalled() + isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive) + + if isAccessLogAvailable { + if runtime.GOOS == "windows" { + if iplimitActive { + shouldClearAccessLog = j.processLogFile() + } + } else { + if iplimitActive { + if f2bInstalled { + shouldClearAccessLog = j.processLogFile() + } else { + if !f2bInstalled { + logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.") + } + } + } + } + } + + if shouldClearAccessLog || (isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600) { + j.clearAccessLog() + } +} + +func (j *CheckClientIpJob) clearAccessLog() { + logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + j.checkError(err) + defer logAccessP.Close() + + accessLogPath, err := xray.GetAccessLogPath() + j.checkError(err) + + file, err := os.Open(accessLogPath) + j.checkError(err) + defer file.Close() + + _, err = io.Copy(logAccessP, file) + j.checkError(err) + + err = os.Truncate(accessLogPath, 0) + j.checkError(err) + + j.lastClear = time.Now().Unix() +} + +func (j *CheckClientIpJob) hasLimitIp() bool { + db := database.GetDB() + var inbounds []*model.Inbound + + err := db.Model(model.Inbound{}).Find(&inbounds).Error + if err != nil { + return false + } + + for _, inbound := range inbounds { + if inbound.Settings == "" { + continue + } + + settings := map[string][]model.Client{} + json.Unmarshal([]byte(inbound.Settings), &settings) + clients := settings["clients"] + + for _, client := range clients { + limitIp := client.LimitIP + if limitIp > 0 { + return true + } + } + } + + return false +} + +func (j *CheckClientIpJob) processLogFile() bool { + + ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`) + emailRegex := regexp.MustCompile(`email: (.+)$`) + + accessLogPath, _ := xray.GetAccessLogPath() + file, _ := os.Open(accessLogPath) + defer file.Close() + + inboundClientIps := make(map[string]map[string]struct{}, 100) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + ipMatches := ipRegex.FindStringSubmatch(line) + if len(ipMatches) < 2 { + continue + } + + ip := ipMatches[1] + + if ip == "127.0.0.1" || ip == "::1" { + continue + } + + emailMatches := emailRegex.FindStringSubmatch(line) + if len(emailMatches) < 2 { + continue + } + email := emailMatches[1] + + if _, exists := inboundClientIps[email]; !exists { + inboundClientIps[email] = make(map[string]struct{}) + } + inboundClientIps[email][ip] = struct{}{} + } + + shouldCleanLog := false + for email, uniqueIps := range inboundClientIps { + + ips := make([]string, 0, len(uniqueIps)) + for ip := range uniqueIps { + ips = append(ips, ip) + } + sort.Strings(ips) + + clientIpsRecord, err := j.getInboundClientIps(email) + if err != nil { + j.addInboundClientIps(email, ips) + continue + } + + shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ips) || shouldCleanLog + } + + return shouldCleanLog +} + +func (j *CheckClientIpJob) checkFail2BanInstalled() bool { + cmd := "fail2ban-client" + args := []string{"-h"} + err := exec.Command(cmd, args...).Run() + return err == nil +} + +func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool { + accessLogPath, err := xray.GetAccessLogPath() + if err != nil { + return false + } + + if accessLogPath == "none" || accessLogPath == "" { + if iplimitActive { + logger.Warning("[LimitIP] Access log path is not set, Please configure the access log path in Xray configs.") + } + return false + } + + return true +} + +func (j *CheckClientIpJob) checkError(e error) { + if e != nil { + logger.Warning("client ip job err:", e) + } +} + +func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) { + db := database.GetDB() + InboundClientIps := &model.InboundClientIps{} + err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error + if err != nil { + return nil, err + } + return InboundClientIps, nil +} + +func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error { + inboundClientIps := &model.InboundClientIps{} + jsonIps, err := json.Marshal(ips) + j.checkError(err) + + inboundClientIps.ClientEmail = clientEmail + inboundClientIps.Ips = string(jsonIps) + + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err == nil { + tx.Commit() + } else { + tx.Rollback() + } + }() + + err = tx.Save(inboundClientIps).Error + if err != nil { + return err + } + return nil +} + +func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool { + jsonIps, err := json.Marshal(ips) + if err != nil { + logger.Error("failed to marshal IPs to JSON:", err) + return false + } + + inboundClientIps.ClientEmail = clientEmail + inboundClientIps.Ips = string(jsonIps) + + inbound, err := j.getInboundByEmail(clientEmail) + if err != nil { + logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err) + return false + } + + if inbound.Settings == "" { + logger.Debug("wrong data:", inbound) + return false + } + + settings := map[string][]model.Client{} + json.Unmarshal([]byte(inbound.Settings), &settings) + clients := settings["clients"] + shouldCleanLog := false + j.disAllowedIps = []string{} + + logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + logger.Errorf("failed to open IP limit log file: %s", err) + return false + } + defer logIpFile.Close() + log.SetOutput(logIpFile) + log.SetFlags(log.LstdFlags) + + for _, client := range clients { + if client.Email == clientEmail { + limitIp := client.LimitIP + + if limitIp > 0 && inbound.Enable { + shouldCleanLog = true + + if limitIp < len(ips) { + j.disAllowedIps = append(j.disAllowedIps, ips[limitIp:]...) + for i := limitIp; i < len(ips); i++ { + log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i]) + } + } + } + } + } + + sort.Strings(j.disAllowedIps) + + if len(j.disAllowedIps) > 0 { + logger.Debug("disAllowedIps:", j.disAllowedIps) + } + + db := database.GetDB() + err = db.Save(inboundClientIps).Error + if err != nil { + logger.Error("failed to save inboundClientIps:", err) + return false + } + + return shouldCleanLog +} + +func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { + db := database.GetDB() + inbound := &model.Inbound{} + + err := db.Model(&model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").First(inbound).Error + if err != nil { + return nil, err + } + + return inbound, nil +} diff --git a/web/job/check_cpu_usage.go b/web/job/check_cpu_usage.go new file mode 100644 index 0000000..4d312ff --- /dev/null +++ b/web/job/check_cpu_usage.go @@ -0,0 +1,54 @@ +package job + +import ( + "strconv" + "time" + + "x-ui/web/service" + + "github.com/shirou/gopsutil/v4/cpu" +) + +// 连续超阈值告警实现 +type CheckCpuJob struct { + tgbotService service.Tgbot + settingService service.SettingService + overThresholdCount int // 连续超阈值计数器 + lastNotifyTime time.Time // 最近一次告警时间 +} + +func NewCheckCpuJob() *CheckCpuJob { + return &CheckCpuJob{} +} + +// run 是 Job 接口方法 +func (j *CheckCpuJob) Run() { + threshold, _ := j.settingService.GetTgCpu() + notifyInterval := 10 * time.Minute // 两次告警最小间隔,可做成配置项 + + percent, err := cpu.Percent(10*time.Second, false) // 10秒采样 + if err != nil || len(percent) == 0 { + return + } + + now := time.Now() + if percent[0] > float64(threshold) { + j.overThresholdCount++ + } else { + j.overThresholdCount = 0 + } + + // 连续3次超阈值,且距离上次告警超过告警间隔 + if j.overThresholdCount >= 3 && now.Sub(j.lastNotifyTime) > notifyInterval { + msg := j.tgbotService.I18nBot( + "tgbot.messages.cpuThreshold", + "Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64), + "Threshold=="+strconv.Itoa(threshold), + "SampleInterval==10s", + "NotifyPolicy==连续3次超阈值", + ) + j.tgbotService.SendMsgToTgbotAdmins(msg) + j.lastNotifyTime = now + j.overThresholdCount = 0 + } +} diff --git a/web/job/check_hash_storage.go b/web/job/check_hash_storage.go new file mode 100644 index 0000000..468aa2e --- /dev/null +++ b/web/job/check_hash_storage.go @@ -0,0 +1,19 @@ +package job + +import ( + "x-ui/web/service" +) + +type CheckHashStorageJob struct { + tgbotService service.Tgbot +} + +func NewCheckHashStorageJob() *CheckHashStorageJob { + return new(CheckHashStorageJob) +} + +// Here Run is an interface method of the Job interface +func (j *CheckHashStorageJob) Run() { + // Remove expired hashes from storage + j.tgbotService.GetHashStorage().RemoveExpiredHashes() +} diff --git a/web/job/check_xray_running_job.go b/web/job/check_xray_running_job.go new file mode 100644 index 0000000..b28caf2 --- /dev/null +++ b/web/job/check_xray_running_job.go @@ -0,0 +1,32 @@ +package job + +import ( + "x-ui/logger" + "x-ui/web/service" +) + +type CheckXrayRunningJob struct { + xrayService service.XrayService + + checkTime int +} + +func NewCheckXrayRunningJob() *CheckXrayRunningJob { + return new(CheckXrayRunningJob) +} + +func (j *CheckXrayRunningJob) Run() { + if !j.xrayService.DidXrayCrash() { + j.checkTime = 0 + } else { + j.checkTime++ + // only restart if it's down 2 times in a row + if j.checkTime > 1 { + err := j.xrayService.RestartXray(false) + j.checkTime = 0 + if err != nil { + logger.Error("Restart xray failed:", err) + } + } + } +} diff --git a/web/job/clear_logs_job.go b/web/job/clear_logs_job.go new file mode 100644 index 0000000..c6f1d7c --- /dev/null +++ b/web/job/clear_logs_job.go @@ -0,0 +1,76 @@ +package job + +import ( + "io" + "os" + "path/filepath" + + "x-ui/logger" + "x-ui/xray" +) + +type ClearLogsJob struct{} + +func NewClearLogsJob() *ClearLogsJob { + return new(ClearLogsJob) +} + +// ensureFileExists creates the necessary directories and file if they don't exist +func ensureFileExists(path string) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return err + } + file.Close() + return nil +} + +// Here Run is an interface method of the Job interface +func (j *ClearLogsJob) Run() { + logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()} + logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()} + + // Ensure all log files and their paths exist + for _, path := range append(logFiles, logFilesPrev...) { + if err := ensureFileExists(path); err != nil { + logger.Warning("Failed to ensure log file exists:", path, "-", err) + } + } + + // Clear log files and copy to previous logs + for i := 0; i < len(logFiles); i++ { + if i > 0 { + // Copy to previous logs + logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + logger.Warning("Failed to open previous log file for writing:", logFilesPrev[i-1], "-", err) + continue + } + + logFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0644) + if err != nil { + logger.Warning("Failed to open current log file for reading:", logFiles[i], "-", err) + logFilePrev.Close() + continue + } + + _, err = io.Copy(logFilePrev, logFile) + if err != nil { + logger.Warning("Failed to copy log file:", logFiles[i], "to", logFilesPrev[i-1], "-", err) + } + + logFile.Close() + logFilePrev.Close() + } + + err := os.Truncate(logFiles[i], 0) + if err != nil { + logger.Warning("Failed to truncate log file:", logFiles[i], "-", err) + } + } +} diff --git a/web/job/stats_notify_job.go b/web/job/stats_notify_job.go new file mode 100644 index 0000000..ae5eba7 --- /dev/null +++ b/web/job/stats_notify_job.go @@ -0,0 +1,29 @@ +package job + +import ( + "x-ui/web/service" +) + +type LoginStatus byte + +const ( + LoginSuccess LoginStatus = 1 + LoginFail LoginStatus = 0 +) + +type StatsNotifyJob struct { + xrayService service.XrayService + tgbotService service.Tgbot +} + +func NewStatsNotifyJob() *StatsNotifyJob { + return new(StatsNotifyJob) +} + +// Here run is a interface method of Job interface +func (j *StatsNotifyJob) Run() { + if !j.xrayService.IsXrayRunning() { + return + } + j.tgbotService.SendReport() +} diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go new file mode 100644 index 0000000..76022bc --- /dev/null +++ b/web/job/xray_traffic_job.go @@ -0,0 +1,71 @@ +package job + +import ( + "encoding/json" + "x-ui/logger" + "x-ui/web/service" + "x-ui/xray" + + "github.com/valyala/fasthttp" +) + +type XrayTrafficJob struct { + settingService service.SettingService + xrayService service.XrayService + inboundService service.InboundService + outboundService service.OutboundService +} + +func NewXrayTrafficJob() *XrayTrafficJob { + return new(XrayTrafficJob) +} + +func (j *XrayTrafficJob) Run() { + if !j.xrayService.IsXrayRunning() { + return + } + traffics, clientTraffics, err := j.xrayService.GetXrayTraffic() + if err != nil { + return + } + err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics) + if err != nil { + logger.Warning("add inbound traffic failed:", err) + } + err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics) + if err != nil { + logger.Warning("add outbound traffic failed:", err) + } + if ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable { + j.informTrafficToExternalAPI(traffics, clientTraffics) + } else if err != nil { + logger.Warning("get ExternalTrafficInformEnable failed:", err) + } + if needRestart0 || needRestart1 { + j.xrayService.SetToNeedRestart() + } +} + +func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) { + informURL, err := j.settingService.GetExternalTrafficInformURI() + if err != nil { + logger.Warning("get ExternalTrafficInformURI failed:", err) + return + } + requestBody, err := json.Marshal(map[string]any{"clientTraffics": clientTraffics, "inboundTraffics": inboundTraffics}) + if err != nil { + logger.Warning("parse client/inbound traffic failed:", err) + return + } + request := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(request) + request.Header.SetMethod("POST") + request.Header.SetContentType("application/json; charset=UTF-8") + request.SetBody([]byte(requestBody)) + request.SetRequestURI(informURL) + response := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(response) + if err := fasthttp.Do(request, response); err != nil { + logger.Warning("POST ExternalTrafficInformURI failed:", err) + } +} diff --git a/web/locale/locale.go b/web/locale/locale.go new file mode 100644 index 0000000..cdfe539 --- /dev/null +++ b/web/locale/locale.go @@ -0,0 +1,145 @@ +package locale + +import ( + "embed" + "io/fs" + "strings" + + "x-ui/logger" + + "github.com/gin-gonic/gin" + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/pelletier/go-toml/v2" + "golang.org/x/text/language" +) + +var ( + i18nBundle *i18n.Bundle + LocalizerWeb *i18n.Localizer + LocalizerBot *i18n.Localizer +) + +type I18nType string + +const ( + Bot I18nType = "bot" + Web I18nType = "web" +) + +type SettingService interface { + GetTgLang() (string, error) +} + +func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { + // set default bundle to english + i18nBundle = i18n.NewBundle(language.MustParse("en-US")) + i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + + // parse files + if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil { + return err + } + + // setup bot locale + if err := initTGBotLocalizer(settingService); err != nil { + return err + } + + return nil +} + +func createTemplateData(params []string, seperator ...string) map[string]any { + var sep string = "==" + if len(seperator) > 0 { + sep = seperator[0] + } + + templateData := make(map[string]any) + for _, param := range params { + parts := strings.SplitN(param, sep, 2) + templateData[parts[0]] = parts[1] + } + + return templateData +} + +func I18n(i18nType I18nType, key string, params ...string) string { + var localizer *i18n.Localizer + + switch i18nType { + case "bot": + localizer = LocalizerBot + case "web": + localizer = LocalizerWeb + default: + logger.Errorf("Invalid type for I18n: %s", i18nType) + return "" + } + + templateData := createTemplateData(params) + + msg, err := localizer.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: templateData, + }) + if err != nil { + logger.Errorf("Failed to localize message: %v", err) + return "" + } + + return msg +} + +func initTGBotLocalizer(settingService SettingService) error { + botLang, err := settingService.GetTgLang() + if err != nil { + return err + } + + LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang) + return nil +} + +func LocalizerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + var lang string + + if cookie, err := c.Request.Cookie("lang"); err == nil { + lang = cookie.Value + } else { + lang = c.GetHeader("Accept-Language") + } + + LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang) + + c.Set("localizer", LocalizerWeb) + c.Set("I18n", I18n) + c.Next() + } +} + +func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error { + err := fs.WalkDir(i18nFS, "translation", + func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + data, err := i18nFS.ReadFile(path) + if err != nil { + return err + } + + _, err = i18nBundle.ParseMessageFileBytes(data, path) + return err + }) + if err != nil { + return err + } + + return nil +} diff --git a/web/middleware/domainValidator.go b/web/middleware/domainValidator.go new file mode 100644 index 0000000..c94130c --- /dev/null +++ b/web/middleware/domainValidator.go @@ -0,0 +1,25 @@ +package middleware + +import ( + "net" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func DomainValidatorMiddleware(domain string) gin.HandlerFunc { + return func(c *gin.Context) { + host := c.Request.Host + if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 { + host, _, _ = net.SplitHostPort(c.Request.Host) + } + + if host != domain { + c.AbortWithStatus(http.StatusForbidden) + return + } + + c.Next() + } +} diff --git a/web/middleware/redirect.go b/web/middleware/redirect.go new file mode 100644 index 0000000..e3dc8ad --- /dev/null +++ b/web/middleware/redirect.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func RedirectMiddleware(basePath string) gin.HandlerFunc { + return func(c *gin.Context) { + // Redirect from old '/xui' path to '/panel' + redirects := map[string]string{ + "panel/API": "panel/api", + "xui/API": "panel/api", + "xui": "panel", + } + + path := c.Request.URL.Path + for from, to := range redirects { + from, to = basePath+from, basePath+to + + if strings.HasPrefix(path, from) { + newPath := to + path[len(from):] + + c.Redirect(http.StatusMovedPermanently, newPath) + c.Abort() + return + } + } + + c.Next() + } +} diff --git a/web/network/auto_https_conn.go b/web/network/auto_https_conn.go new file mode 100644 index 0000000..d1a9d52 --- /dev/null +++ b/web/network/auto_https_conn.go @@ -0,0 +1,67 @@ +package network + +import ( + "bufio" + "bytes" + "fmt" + "net" + "net/http" + "sync" +) + +type AutoHttpsConn struct { + net.Conn + + firstBuf []byte + bufStart int + + readRequestOnce sync.Once +} + +func NewAutoHttpsConn(conn net.Conn) net.Conn { + return &AutoHttpsConn{ + Conn: conn, + } +} + +func (c *AutoHttpsConn) readRequest() bool { + c.firstBuf = make([]byte, 2048) + n, err := c.Conn.Read(c.firstBuf) + c.firstBuf = c.firstBuf[:n] + if err != nil { + return false + } + reader := bytes.NewReader(c.firstBuf) + bufReader := bufio.NewReader(reader) + request, err := http.ReadRequest(bufReader) + if err != nil { + return false + } + resp := http.Response{ + Header: http.Header{}, + } + resp.StatusCode = http.StatusTemporaryRedirect + location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) + resp.Header.Set("Location", location) + resp.Write(c.Conn) + c.Close() + c.firstBuf = nil + return true +} + +func (c *AutoHttpsConn) Read(buf []byte) (int, error) { + c.readRequestOnce.Do(func() { + c.readRequest() + }) + + if c.firstBuf != nil { + n := copy(buf, c.firstBuf[c.bufStart:]) + c.bufStart += n + if c.bufStart >= len(c.firstBuf) { + c.firstBuf = nil + } + return n, nil + } + + return c.Conn.Read(buf) +} diff --git a/web/network/auto_https_listener.go b/web/network/auto_https_listener.go new file mode 100644 index 0000000..2661469 --- /dev/null +++ b/web/network/auto_https_listener.go @@ -0,0 +1,21 @@ +package network + +import "net" + +type AutoHttpsListener struct { + net.Listener +} + +func NewAutoHttpsListener(listener net.Listener) net.Listener { + return &AutoHttpsListener{ + Listener: listener, + } +} + +func (l *AutoHttpsListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + if err != nil { + return nil, err + } + return NewAutoHttpsConn(conn), nil +} diff --git a/web/service/config.json b/web/service/config.json new file mode 100644 index 0000000..34a90dd --- /dev/null +++ b/web/service/config.json @@ -0,0 +1,98 @@ +{ + "log": { + "access": "./access.log", + "dnsLog": true, + "error": "./error.log", + "loglevel": "debug", + "maskAddress": "" + }, + "api": { + "tag": "api", + "services": [ + "HandlerService", + "LoggerService", + "StatsService" + ] + }, + "inbounds": [ + { + "tag": "api", + "listen": "127.0.0.1", + "port": 62789, + "protocol": "tunnel", + "settings": { + "address": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "tag": "direct", + "protocol": "freedom", + "settings": { + "domainStrategy": "AsIs", + "redirect": "", + "noises": [] + } + }, + { + "tag": "blocked", + "protocol": "blackhole", + "settings": {} + }, + { + "tag": "api", + "protocol": "blackhole" + } + ], + "policy": { + "levels": { + "0": { + "handshake": 4, + "connIdle": 300, + "uplinkOnly": 0, + "downlinkOnly": 0, + "statsUserUplink": true, + "statsUserDownlink": true, + "statsUserOnline": true + } + }, + "system": { + "statsInboundDownlink": true, + "statsInboundUplink": true, + "statsOutboundDownlink": true, + "statsOutboundUplink": true + } + }, + "routing": { + "domainStrategy": "AsIs", + "rules": [ + { + "type": "field", + "inboundTag": [ + "api" + ], + "outboundTag": "api" + }, + { + "type": "field", + "outboundTag": "blocked", + "ip": [ + "geoip:private" + ] + }, + { + "type": "field", + "outboundTag": "blocked", + "protocol": [ + "bittorrent" + ] + } + ] + }, + "stats": {}, + "metrics": { + "tag": "metrics_out", + "listen": "127.0.0.1:11111" + } +} diff --git a/web/service/inbound.go b/web/service/inbound.go new file mode 100644 index 0000000..d7d01cf --- /dev/null +++ b/web/service/inbound.go @@ -0,0 +1,2339 @@ +package service + +import ( + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/common" + "x-ui/xray" + + "gorm.io/gorm" +) + +type InboundService struct { + xrayApi xray.XrayAPI + tgService TelegramService +} + +// 【新增方法】: 用于从外部注入 XrayAPI 实例 +func (s *InboundService) SetXrayAPI(api xray.XrayAPI) { + s.xrayApi = api +} + +// 【新增方法】: 用于从外部注入 TelegramService 实例 +func (s *InboundService) SetTelegramService(tgService TelegramService) { + s.tgService = tgService +} + +func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { + db := database.GetDB() + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return inbounds, nil +} + +func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { + db := database.GetDB() + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return inbounds, nil +} + +func (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) { + db := database.GetDB() + if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" { + db = db.Model(model.Inbound{}).Where("port = ?", port) + } else { + db = db.Model(model.Inbound{}). + Where("port = ?", port). + Where( + db.Model(model.Inbound{}).Where( + "listen = ?", listen, + ).Or( + "listen = \"\"", + ).Or( + "listen = \"0.0.0.0\"", + ).Or( + "listen = \"::\"", + ).Or( + "listen = \"::0\"")) + } + if ignoreId > 0 { + db = db.Where("id != ?", ignoreId) + } + var count int64 + err := db.Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) { + settings := map[string][]model.Client{} + json.Unmarshal([]byte(inbound.Settings), &settings) + if settings == nil { + return nil, fmt.Errorf("setting is null") + } + + clients := settings["clients"] + if clients == nil { + return nil, nil + } + return clients, nil +} + +func (s *InboundService) getAllEmails() ([]string, error) { + db := database.GetDB() + var emails []string + err := db.Raw(` + SELECT JSON_EXTRACT(client.value, '$.email') + FROM inbounds, + JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client + `).Scan(&emails).Error + if err != nil { + return nil, err + } + return emails, nil +} + +func (s *InboundService) contains(slice []string, str string) bool { + lowerStr := strings.ToLower(str) + for _, s := range slice { + if strings.ToLower(s) == lowerStr { + return true + } + } + return false +} + +func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) { + allEmails, err := s.getAllEmails() + if err != nil { + return "", err + } + var emails []string + for _, client := range clients { + if client.Email != "" { + if s.contains(emails, client.Email) { + return client.Email, nil + } + if s.contains(allEmails, client.Email) { + return client.Email, nil + } + emails = append(emails, client.Email) + } + } + return "", nil +} + +func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) { + clients, err := s.GetClients(inbound) + if err != nil { + return "", err + } + allEmails, err := s.getAllEmails() + if err != nil { + return "", err + } + var emails []string + for _, client := range clients { + if client.Email != "" { + if s.contains(emails, client.Email) { + return client.Email, nil + } + if s.contains(allEmails, client.Email) { + return client.Email, nil + } + emails = append(emails, client.Email) + } + } + return "", nil +} + +// AddInbound adds a new inbound to db +func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { + // 中文注释:检查端口是否已存在 + exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0) + if err != nil { + return inbound, false, err + } + if exist { + return inbound, false, common.NewError("Port already exists:", inbound.Port) + } + + // 中文注释:检查邮箱是否重复 + existEmail, err := s.checkEmailExistForInbound(inbound) + if err != nil { + return inbound, false, err + } + if existEmail != "" { + return inbound, false, common.NewError("Duplicate email:", existEmail) + } + + // 中文注释:获取入站规则中的客户端信息 + clients, err := s.GetClients(inbound) + if err != nil { + return inbound, false, err + } + + // 中文注释:确保客户端设置中包含创建和更新时间戳 + if len(clients) > 0 { + var settings map[string]any + if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil { + now := time.Now().Unix() * 1000 + updatedClients := make([]model.Client, 0, len(clients)) + for _, c := range clients { + if c.CreatedAt == 0 { + c.CreatedAt = now + } + c.UpdatedAt = now + updatedClients = append(updatedClients, c) + } + settings["clients"] = updatedClients + if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil { + inbound.Settings = string(bs) + } else { + logger.Debug("Unable to marshal inbound settings with timestamps:", err3) + } + } else if err2 != nil { + logger.Debug("Unable to parse inbound settings for timestamps:", err2) + } + } + + // 中文注释:根据不同协议,验证客户端ID/密码是否为空 + for _, client := range clients { + switch inbound.Protocol { + case "trojan": + if client.Password == "" { + return inbound, false, common.NewError("empty client ID") + } + case "shadowsocks": + if client.Email == "" { + return inbound, false, common.NewError("empty client ID") + } + default: + if client.ID == "" { + return inbound, false, common.NewError("empty client ID") + } + } + } + + // ================================================================= + // 中文注释:【新增逻辑】开始:手动计算和分配 ID + // ================================================================= + // 1. 查询数据库中所有已存在的入站规则的 ID + var existingIDs []int + // 使用 Pluck 方法可以更高效地只查询出 id 这一列,而不是整个对象 + if err := database.GetDB().Model(&model.Inbound{}).Pluck("id", &existingIDs).Error; err != nil { + return inbound, false, err + } + + // 2. 对所有已存在的 ID 进行升序排序 + sort.Ints(existingIDs) + + // 3. 查找第一个可用的、空缺的 ID + // 我们从 1 开始作为期望的 ID 进行检查。 + // 例如,如果 ID 列表是 [1, 2, 4, 5],当 nextID 是 3 时,它在列表中找不到匹配的 id=3,循环就会中断,nextID 就确定为 3。 + // 如果 ID 列表是 [1, 2, 3],循环结束后 nextID 会是 4。 + nextID := 1 + for _, id := range existingIDs { + if id == nextID { + nextID++ + } else { + // 找到了第一个不连续的空缺 + break + } + } + + // 4. 将计算出的可用 ID 赋值给即将创建的入站对象 + inbound.Id = nextID + // ================================================================= + // 中文注释:【新增逻辑】结束:手动计算和分配 ID + // ================================================================= + + // 中文注释:开始数据库事务 + db := database.GetDB() + tx := db.Begin() + defer func() { + if err == nil { + // 中文注释:如果没有错误,提交事务 + tx.Commit() + } else { + // 中文注释:如果出现错误,回滚事务 + tx.Rollback() + } + }() + + // 中文注释:保存入站信息到数据库 (此时 inbound 对象已包含我们手动设置的 ID) + err = tx.Save(inbound).Error + if err == nil { + if len(inbound.ClientStats) == 0 { + for _, client := range clients { + s.AddClientStat(tx, inbound.Id, &client) + } + } + } else { + return inbound, false, err + } + + // 中文注释:如果入站规则是启用的,则尝试通过 API 热加载到 Xray-core + needRestart := false + if inbound.Enable { + s.xrayApi.Init(p.GetAPIPort()) + inboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), "", " ") + if err1 != nil { + logger.Debug("Unable to marshal inbound config:", err1) + } + + err1 = s.xrayApi.AddInbound(inboundJson) + if err1 == nil { + logger.Debug("New inbound added by api:", inbound.Tag) + } else { + // 中文注释:如果 API 调用失败,则标记需要重启面板以应用更改 + logger.Debug("Unable to add inbound by api:", err1) + needRestart = true + } + s.xrayApi.Close() + } + + // 中文注释:返回创建好的入站对象、是否需要重启以及错误信息 + return inbound, needRestart, err +} + +func (s *InboundService) DelInbound(id int) (bool, error) { + db := database.GetDB() + + var tag string + needRestart := false + result := db.Model(model.Inbound{}).Select("tag").Where("id = ? and enable = ?", id, true).First(&tag) + if result.Error == nil { + s.xrayApi.Init(p.GetAPIPort()) + err1 := s.xrayApi.DelInbound(tag) + if err1 == nil { + logger.Debug("Inbound deleted by api:", tag) + } else { + logger.Debug("Unable to delete inbound by api:", err1) + needRestart = true + } + s.xrayApi.Close() + } else { + logger.Debug("No enabled inbound founded to removing by api", tag) + } + + // Delete client traffics of inbounds + err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error + if err != nil { + return false, err + } + inbound, err := s.GetInbound(id) + if err != nil { + return false, err + } + clients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + for _, client := range clients { + err := s.DelClientIPs(db, client.Email) + if err != nil { + return false, err + } + } + + return needRestart, db.Delete(model.Inbound{}, id).Error +} + +func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { + db := database.GetDB() + inbound := &model.Inbound{} + err := db.Model(model.Inbound{}).First(inbound, id).Error + if err != nil { + return nil, err + } + return inbound, nil +} + +func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { + exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id) + if err != nil { + return inbound, false, err + } + if exist { + return inbound, false, common.NewError("Port already exists:", inbound.Port) + } + + oldInbound, err := s.GetInbound(inbound.Id) + if err != nil { + return inbound, false, err + } + + tag := oldInbound.Tag + + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err != nil { + tx.Rollback() + } else { + tx.Commit() + } + }() + + err = s.updateClientTraffics(tx, oldInbound, inbound) + if err != nil { + return inbound, false, err + } + + // Ensure created_at and updated_at exist in inbound.Settings clients + { + var oldSettings map[string]any + _ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) + emailToCreated := map[string]int64{} + emailToUpdated := map[string]int64{} + if oldSettings != nil { + if oc, ok := oldSettings["clients"].([]any); ok { + for _, it := range oc { + if m, ok2 := it.(map[string]any); ok2 { + if email, ok3 := m["email"].(string); ok3 { + switch v := m["created_at"].(type) { + case float64: + emailToCreated[email] = int64(v) + case int64: + emailToCreated[email] = v + } + switch v := m["updated_at"].(type) { + case float64: + emailToUpdated[email] = int64(v) + case int64: + emailToUpdated[email] = v + } + } + } + } + } + } + var newSettings map[string]any + if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil { + now := time.Now().Unix() * 1000 + if nSlice, ok := newSettings["clients"].([]any); ok { + for i := range nSlice { + if m, ok2 := nSlice[i].(map[string]any); ok2 { + email, _ := m["email"].(string) + if _, ok3 := m["created_at"]; !ok3 { + if v, ok4 := emailToCreated[email]; ok4 && v > 0 { + m["created_at"] = v + } else { + m["created_at"] = now + } + } + // Preserve client's updated_at if present; do not bump on parent inbound update + if _, hasUpdated := m["updated_at"]; !hasUpdated { + if v, ok4 := emailToUpdated[email]; ok4 && v > 0 { + m["updated_at"] = v + } + } + nSlice[i] = m + } + } + newSettings["clients"] = nSlice + if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil { + inbound.Settings = string(bs) + } + } + } + } + + oldInbound.Up = inbound.Up + oldInbound.Down = inbound.Down + oldInbound.Total = inbound.Total + oldInbound.Remark = inbound.Remark + oldInbound.Enable = inbound.Enable + oldInbound.ExpiryTime = inbound.ExpiryTime + // 中文注释:确保在更新数据时,将前端传来的 deviceLimit 值赋给从数据库中读出的旧对象。 + oldInbound.DeviceLimit = inbound.DeviceLimit + oldInbound.Listen = inbound.Listen + oldInbound.Port = inbound.Port + oldInbound.Protocol = inbound.Protocol + oldInbound.Settings = inbound.Settings + oldInbound.StreamSettings = inbound.StreamSettings + oldInbound.Sniffing = inbound.Sniffing + if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { + oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) + } else { + oldInbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) + } + + needRestart := false + s.xrayApi.Init(p.GetAPIPort()) + if s.xrayApi.DelInbound(tag) == nil { + logger.Debug("Old inbound deleted by api:", tag) + } + if inbound.Enable { + inboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), "", " ") + if err2 != nil { + logger.Debug("Unable to marshal updated inbound config:", err2) + needRestart = true + } else { + err2 = s.xrayApi.AddInbound(inboundJson) + if err2 == nil { + logger.Debug("Updated inbound added by api:", oldInbound.Tag) + } else { + logger.Debug("Unable to update inbound by api:", err2) + needRestart = true + } + } + } + s.xrayApi.Close() + + return inbound, needRestart, tx.Save(oldInbound).Error +} + +func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error { + oldClients, err := s.GetClients(oldInbound) + if err != nil { + return err + } + newClients, err := s.GetClients(newInbound) + if err != nil { + return err + } + + var emailExists bool + + for _, oldClient := range oldClients { + emailExists = false + for _, newClient := range newClients { + if oldClient.Email == newClient.Email { + emailExists = true + break + } + } + if !emailExists { + err = s.DelClientStat(tx, oldClient.Email) + if err != nil { + return err + } + } + } + for _, newClient := range newClients { + emailExists = false + for _, oldClient := range oldClients { + if newClient.Email == oldClient.Email { + emailExists = true + break + } + } + if !emailExists { + err = s.AddClientStat(tx, oldInbound.Id, &newClient) + if err != nil { + return err + } + } + } + return nil +} + +func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { + clients, err := s.GetClients(data) + if err != nil { + return false, err + } + + var settings map[string]any + err = json.Unmarshal([]byte(data.Settings), &settings) + if err != nil { + return false, err + } + + interfaceClients := settings["clients"].([]any) + // Add timestamps for new clients being appended + nowTs := time.Now().Unix() * 1000 + for i := range interfaceClients { + if cm, ok := interfaceClients[i].(map[string]any); ok { + if _, ok2 := cm["created_at"]; !ok2 { + cm["created_at"] = nowTs + } + cm["updated_at"] = nowTs + + // ↓↓↓↓↓↓ 【重要补充】在这里手动确保 SpeedLimit 被写入数据库 ↓↓↓↓↓↓ + // clients[i] 和 interfaceClients[i] 是一一对应的 + // 我们从强类型的 clients[i] 对象中取出 SpeedLimit,赋值给弱类型的 map + cm["speedLimit"] = clients[i].SpeedLimit // 中文注释: 确保批量添加时,speedLimit 的值也被写入数据库。 + + interfaceClients[i] = cm + } + } + existEmail, err := s.checkEmailsExistForClients(clients) + if err != nil { + return false, err + } + if existEmail != "" { + return false, common.NewError("Duplicate email:", existEmail) + } + + oldInbound, err := s.GetInbound(data.Id) + if err != nil { + return false, err + } + + // Secure client ID + for _, client := range clients { + switch oldInbound.Protocol { + case "trojan": + if client.Password == "" { + return false, common.NewError("empty client ID") + } + case "shadowsocks": + if client.Email == "" { + return false, common.NewError("empty client ID") + } + default: + if client.ID == "" { + return false, common.NewError("empty client ID") + } + } + } + + var oldSettings map[string]any + err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) + if err != nil { + return false, err + } + + oldClients := oldSettings["clients"].([]any) + oldClients = append(oldClients, interfaceClients...) + + oldSettings["clients"] = oldClients + + newSettings, err := json.MarshalIndent(oldSettings, "", " ") + if err != nil { + return false, err + } + + oldInbound.Settings = string(newSettings) + + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err != nil { + tx.Rollback() + } else { + tx.Commit() + } + }() + + needRestart := false + s.xrayApi.Init(p.GetAPIPort()) + for _, client := range clients { + if len(client.Email) > 0 { + s.AddClientStat(tx, data.Id, &client) + if client.Enable { + cipher := "" + if oldInbound.Protocol == "shadowsocks" { + cipher = oldSettings["method"].(string) + } + + // 中文注释: 在这里为 API 调用添加 speedLimit 参数。 + clientMap := map[string]any{ + "email": client.Email, + "id": client.ID, + "security": client.Security, + "flow": client.Flow, + "password": client.Password, + "cipher": cipher, + + // Xray-core 会将这个值作为 level,然后去 policy 中寻找对应的限速策略。 + "level": client.SpeedLimit, + } + err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, clientMap) + + if err1 == nil { + logger.Debug("Client added by api:", client.Email) + } else { + logger.Debug("Error in adding client by api:", err1) + needRestart = true + } + } + } else { + needRestart = true + } + } + s.xrayApi.Close() + + return needRestart, tx.Save(oldInbound).Error +} + +func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) { + oldInbound, err := s.GetInbound(inboundId) + if err != nil { + logger.Error("Load Old Data Error") + return false, err + } + var settings map[string]any + err = json.Unmarshal([]byte(oldInbound.Settings), &settings) + if err != nil { + return false, err + } + + email := "" + client_key := "id" + if oldInbound.Protocol == "trojan" { + client_key = "password" + } + if oldInbound.Protocol == "shadowsocks" { + client_key = "email" + } + + interfaceClients := settings["clients"].([]any) + var newClients []any + needApiDel := false + for _, client := range interfaceClients { + c := client.(map[string]any) + c_id := c[client_key].(string) + if c_id == clientId { + email, _ = c["email"].(string) + needApiDel, _ = c["enable"].(bool) + } else { + newClients = append(newClients, client) + } + } + + if len(newClients) == 0 { + return false, common.NewError("no client remained in Inbound") + } + + settings["clients"] = newClients + newSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, err + } + + oldInbound.Settings = string(newSettings) + + db := database.GetDB() + + err = s.DelClientIPs(db, email) + if err != nil { + logger.Error("Error in delete client IPs") + return false, err + } + needRestart := false + + if len(email) > 0 { + notDepleted := true + err = db.Model(xray.ClientTraffic{}).Select("enable").Where("email = ?", email).First(¬Depleted).Error + if err != nil { + logger.Error("Get stats error") + return false, err + } + err = s.DelClientStat(db, email) + if err != nil { + logger.Error("Delete stats Data Error") + return false, err + } + if needApiDel && notDepleted { + s.xrayApi.Init(p.GetAPIPort()) + err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email) + if err1 == nil { + logger.Debug("Client deleted by api:", email) + needRestart = false + } else { + if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) { + logger.Debug("User is already deleted. Nothing to do more...") + } else { + logger.Debug("Error in deleting client by api:", err1) + needRestart = true + } + } + s.xrayApi.Close() + } + } + return needRestart, db.Save(oldInbound).Error +} + +func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) { + clients, err := s.GetClients(data) + if err != nil { + return false, err + } + + var settings map[string]any + err = json.Unmarshal([]byte(data.Settings), &settings) + if err != nil { + return false, err + } + + interfaceClients := settings["clients"].([]any) + + oldInbound, err := s.GetInbound(data.Id) + if err != nil { + return false, err + } + + oldClients, err := s.GetClients(oldInbound) + if err != nil { + return false, err + } + + oldEmail := "" + newClientId := "" + clientIndex := -1 + for index, oldClient := range oldClients { + oldClientId := "" + switch oldInbound.Protocol { + case "trojan": + oldClientId = oldClient.Password + newClientId = clients[0].Password + case "shadowsocks": + oldClientId = oldClient.Email + newClientId = clients[0].Email + default: + oldClientId = oldClient.ID + newClientId = clients[0].ID + } + if clientId == oldClientId { + oldEmail = oldClient.Email + clientIndex = index + break + } + } + + // Validate new client ID + if newClientId == "" || clientIndex == -1 { + return false, common.NewError("empty client ID") + } + + if len(clients[0].Email) > 0 && clients[0].Email != oldEmail { + existEmail, err := s.checkEmailsExistForClients(clients) + if err != nil { + return false, err + } + if existEmail != "" { + return false, common.NewError("Duplicate email:", existEmail) + } + } + + var oldSettings map[string]any + err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) + if err != nil { + return false, err + } + settingsClients := oldSettings["clients"].([]any) + // Preserve created_at and set updated_at for the replacing client + var preservedCreated any + if clientIndex >= 0 && clientIndex < len(settingsClients) { + if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok { + if v, ok2 := oldMap["created_at"]; ok2 { + preservedCreated = v + } + } + } + if len(interfaceClients) > 0 { + if newMap, ok := interfaceClients[0].(map[string]any); ok { + if preservedCreated == nil { + preservedCreated = time.Now().Unix() * 1000 + } + newMap["created_at"] = preservedCreated + newMap["updated_at"] = time.Now().Unix() * 1000 + + // ↓↓↓↓↓↓ 【重要补充】在这里手动确保 SpeedLimit 被写入数据库 ↓↓↓↓↓↓ + // clients[0] 是从请求中解码出来的强类型对象,它的 SpeedLimit 字段是有值的。 + // 我们把它手动赋值给即将用于保存的 newMap。 + newMap["speedLimit"] = clients[0].SpeedLimit // 中文注释:确保将 speedLimit 的值写入将要保存到数据库的 map 中。 + + interfaceClients[0] = newMap + } + } + settingsClients[clientIndex] = interfaceClients[0] + oldSettings["clients"] = settingsClients + + newSettings, err := json.MarshalIndent(oldSettings, "", " ") + if err != nil { + return false, err + } + + oldInbound.Settings = string(newSettings) + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err != nil { + tx.Rollback() + } else { + tx.Commit() + } + }() + + if len(clients[0].Email) > 0 { + if len(oldEmail) > 0 { + err = s.UpdateClientStat(tx, oldEmail, &clients[0]) + if err != nil { + return false, err + } + err = s.UpdateClientIPs(tx, oldEmail, clients[0].Email) + if err != nil { + return false, err + } + } else { + s.AddClientStat(tx, data.Id, &clients[0]) + } + } else { + err = s.DelClientStat(tx, oldEmail) + if err != nil { + return false, err + } + err = s.DelClientIPs(tx, oldEmail) + if err != nil { + return false, err + } + } + needRestart := false + if len(oldEmail) > 0 { + s.xrayApi.Init(p.GetAPIPort()) + if oldClients[clientIndex].Enable { + err1 := s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail) + if err1 == nil { + logger.Debug("Old client deleted by api:", oldEmail) + } else { + if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", oldEmail)) { + logger.Debug("User is already deleted. Nothing to do more...") + } else { + logger.Debug("Error in deleting client by api:", err1) + needRestart = true + } + } + } + if clients[0].Enable { + cipher := "" + if oldInbound.Protocol == "shadowsocks" { + cipher = oldSettings["method"].(string) + } + + // 中文注释: 同样,在更新用户时,也必须把新的 speedLimit 值通过 API 传给 Xray-core。 + clientMap := map[string]any{ + "email": clients[0].Email, + "id": clients[0].ID, + "security": clients[0].Security, + "flow": clients[0].Flow, + "password": clients[0].Password, + "cipher": cipher, + + "level": clients[0].SpeedLimit, + } + err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, clientMap) + + if err1 == nil { + logger.Debug("Client edited by api:", clients[0].Email) + } else { + logger.Debug("Error in adding client by api:", err1) + needRestart = true + } + } + s.xrayApi.Close() + } else { + logger.Debug("Client old email not found") + needRestart = true + } + return needRestart, tx.Save(oldInbound).Error +} + +func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { + var err error + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err != nil { + tx.Rollback() + } else { + tx.Commit() + } + }() + err = s.addInboundTraffic(tx, inboundTraffics) + if err != nil { + return err, false + } + err = s.addClientTraffic(tx, clientTraffics) + if err != nil { + return err, false + } + + needRestart0, count, err := s.autoRenewClients(tx) + if err != nil { + logger.Warning("Error in renew clients:", err) + } else if count > 0 { + logger.Debugf("%v clients renewed", count) + } + + needRestart1, count, err := s.disableInvalidClients(tx) + if err != nil { + logger.Warning("Error in disabling invalid clients:", err) + } else if count > 0 { + logger.Debugf("%v clients disabled", count) + } + + needRestart2, count, err := s.disableInvalidInbounds(tx) + if err != nil { + logger.Warning("Error in disabling invalid inbounds:", err) + } else if count > 0 { + logger.Debugf("%v inbounds disabled", count) + } + return nil, (needRestart0 || needRestart1 || needRestart2) +} + +func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error { + if len(traffics) == 0 { + return nil + } + + var err error + + for _, traffic := range traffics { + if traffic.IsInbound { + err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag). + Updates(map[string]any{ + "up": gorm.Expr("up + ?", traffic.Up), + "down": gorm.Expr("down + ?", traffic.Down), + "all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down), + }).Error + if err != nil { + return err + } + } + } + return nil +} + +func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) { + if len(traffics) == 0 { + // Empty onlineUsers + if p != nil { + p.SetOnlineClients(nil) + } + return nil + } + + var onlineClients []string + + emails := make([]string, 0, len(traffics)) + for _, traffic := range traffics { + emails = append(emails, traffic.Email) + } + dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics)) + err = tx.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error + if err != nil { + return err + } + + // Avoid empty slice error + if len(dbClientTraffics) == 0 { + return nil + } + + dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics) + if err != nil { + return err + } + + for dbTraffic_index := range dbClientTraffics { + for traffic_index := range traffics { + if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email { + dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up + dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down + dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down) + + // Add user in onlineUsers array on traffic + if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 { + onlineClients = append(onlineClients, traffics[traffic_index].Email) + dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli() + } + break + } + } + } + + // Set onlineUsers + if p != nil { + p.SetOnlineClients(onlineClients) + } + + err = tx.Save(dbClientTraffics).Error + if err != nil { + logger.Warning("AddClientTraffic update data ", err) + } + + return nil +} + +func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) { + inboundIds := make([]int, 0, len(dbClientTraffics)) + for _, dbClientTraffic := range dbClientTraffics { + if dbClientTraffic.ExpiryTime < 0 { + inboundIds = append(inboundIds, dbClientTraffic.InboundId) + } + } + + if len(inboundIds) > 0 { + var inbounds []*model.Inbound + err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error + if err != nil { + return nil, err + } + for inbound_index := range inbounds { + settings := map[string]any{} + json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) + clients, ok := settings["clients"].([]any) + if ok { + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + for traffic_index := range dbClientTraffics { + if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email { + oldExpiryTime := c["expiryTime"].(float64) + newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime) + c["expiryTime"] = newExpiryTime + c["updated_at"] = time.Now().Unix() * 1000 + dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime + break + } + } + // Backfill created_at and updated_at + if _, ok := c["created_at"]; !ok { + c["created_at"] = time.Now().Unix() * 1000 + } + c["updated_at"] = time.Now().Unix() * 1000 + newClients = append(newClients, any(c)) + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return nil, err + } + + inbounds[inbound_index].Settings = string(modifiedSettings) + } + } + err = tx.Save(inbounds).Error + if err != nil { + logger.Warning("AddClientTraffic update inbounds ", err) + logger.Error(inbounds) + } + } + + return dbClientTraffics, nil +} + +func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) { + // check for time expired + var traffics []*xray.ClientTraffic + now := time.Now().Unix() * 1000 + var err, err1 error + + err = tx.Model(xray.ClientTraffic{}).Where("reset > 0 and expiry_time > 0 and expiry_time <= ?", now).Find(&traffics).Error + if err != nil { + return false, 0, err + } + // return if there is no client to renew + if len(traffics) == 0 { + return false, 0, nil + } + + var inbound_ids []int + var inbounds []*model.Inbound + needRestart := false + var clientsToAdd []struct { + protocol string + tag string + client map[string]any + } + + for _, traffic := range traffics { + inbound_ids = append(inbound_ids, traffic.InboundId) + } + err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error + if err != nil { + return false, 0, err + } + for inbound_index := range inbounds { + settings := map[string]any{} + json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) + clients := settings["clients"].([]any) + for client_index := range clients { + c := clients[client_index].(map[string]any) + for traffic_index, traffic := range traffics { + if traffic.Email == c["email"].(string) { + newExpiryTime := traffic.ExpiryTime + for newExpiryTime < now { + newExpiryTime += (int64(traffic.Reset) * 86400000) + } + c["expiryTime"] = newExpiryTime + traffics[traffic_index].ExpiryTime = newExpiryTime + traffics[traffic_index].Down = 0 + traffics[traffic_index].Up = 0 + if !traffic.Enable { + traffics[traffic_index].Enable = true + clientsToAdd = append(clientsToAdd, + struct { + protocol string + tag string + client map[string]any + }{ + protocol: string(inbounds[inbound_index].Protocol), + tag: inbounds[inbound_index].Tag, + client: c, + }) + } + clients[client_index] = any(c) + break + } + } + } + settings["clients"] = clients + newSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, 0, err + } + inbounds[inbound_index].Settings = string(newSettings) + } + err = tx.Save(inbounds).Error + if err != nil { + return false, 0, err + } + err = tx.Save(traffics).Error + if err != nil { + return false, 0, err + } + if p != nil { + err1 = s.xrayApi.Init(p.GetAPIPort()) + if err1 != nil { + return true, int64(len(traffics)), nil + } + for _, clientToAdd := range clientsToAdd { + err1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client) + if err1 != nil { + needRestart = true + } + } + s.xrayApi.Close() + } + return needRestart, int64(len(traffics)), nil +} + +func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) { + now := time.Now().Unix() * 1000 + needRestart := false + + if p != nil { + var tags []string + err := tx.Table("inbounds"). + Select("inbounds.tag"). + Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). + Scan(&tags).Error + if err != nil { + return false, 0, err + } + s.xrayApi.Init(p.GetAPIPort()) + for _, tag := range tags { + err1 := s.xrayApi.DelInbound(tag) + if err1 == nil { + logger.Debug("Inbound disabled by api:", tag) + } else { + logger.Debug("Error in disabling inbound by api:", err1) + needRestart = true + } + } + s.xrayApi.Close() + } + + result := tx.Model(model.Inbound{}). + Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). + Update("enable", false) + err := result.Error + count := result.RowsAffected + return needRestart, count, err +} + +func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) { + now := time.Now().Unix() * 1000 + needRestart := false + + if p != nil { + var results []struct { + Tag string + Email string + } + + err := tx.Table("inbounds"). + Select("inbounds.tag, client_traffics.email"). + Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id"). + Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true). + Scan(&results).Error + if err != nil { + return false, 0, err + } + s.xrayApi.Init(p.GetAPIPort()) + for _, result := range results { + err1 := s.xrayApi.RemoveUser(result.Tag, result.Email) + if err1 == nil { + logger.Debug("Client disabled by api:", result.Email) + } else { + if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) { + logger.Debug("User is already disabled. Nothing to do more...") + } else { + if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) { + logger.Debug("User is already disabled. Nothing to do more...") + } else { + logger.Debug("Error in disabling client by api:", err1) + needRestart = true + } + } + } + } + s.xrayApi.Close() + } + result := tx.Model(xray.ClientTraffic{}). + Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). + Update("enable", false) + err := result.Error + count := result.RowsAffected + return needRestart, count, err +} + +func (s *InboundService) GetInboundTags() (string, error) { + db := database.GetDB() + var inboundTags []string + err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error + if err != nil && err != gorm.ErrRecordNotFound { + return "", err + } + tags, _ := json.Marshal(inboundTags) + return string(tags), nil +} + +func (s *InboundService) MigrationRemoveOrphanedTraffics() { + db := database.GetDB() + db.Exec(` + DELETE FROM client_traffics + WHERE email NOT IN ( + SELECT JSON_EXTRACT(client.value, '$.email') + FROM inbounds, + JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client + ) + `) +} + +func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { + clientTraffic := xray.ClientTraffic{} + clientTraffic.InboundId = inboundId + clientTraffic.Email = client.Email + clientTraffic.Total = client.TotalGB + clientTraffic.ExpiryTime = client.ExpiryTime + clientTraffic.Enable = true + clientTraffic.Up = 0 + clientTraffic.Down = 0 + clientTraffic.Reset = client.Reset + result := tx.Create(&clientTraffic) + err := result.Error + return err +} + +func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error { + result := tx.Model(xray.ClientTraffic{}). + Where("email = ?", email). + Updates(map[string]any{ + "enable": true, + "email": client.Email, + "total": client.TotalGB, + "expiry_time": client.ExpiryTime, + "reset": client.Reset, + }) + err := result.Error + return err +} + +func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error { + return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error +} + +func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error { + return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error +} + +func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error { + return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error +} + +func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { + db := database.GetDB() + var traffics []*xray.ClientTraffic + err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error + if err != nil { + logger.Warningf("Error retrieving ClientTraffic with trafficId %d: %v", trafficId, err) + return nil, nil, err + } + if len(traffics) > 0 { + inbound, err = s.GetInbound(traffics[0].InboundId) + return traffics[0], inbound, err + } + return nil, nil, nil +} + +func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { + db := database.GetDB() + var traffics []*xray.ClientTraffic + err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error + if err != nil { + logger.Warningf("Error retrieving ClientTraffic with email %s: %v", email, err) + return nil, nil, err + } + if len(traffics) > 0 { + inbound, err = s.GetInbound(traffics[0].InboundId) + return traffics[0], inbound, err + } + return nil, nil, nil +} + +func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) { + traffic, inbound, err := s.GetClientInboundByEmail(clientEmail) + if err != nil { + return nil, nil, err + } + if inbound == nil { + return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail) + } + + clients, err := s.GetClients(inbound) + if err != nil { + return nil, nil, err + } + + for _, client := range clients { + if client.Email == clientEmail { + return traffic, &client, nil + } + } + + return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail) +} + +func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (bool, error) { + traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId) + if err != nil { + return false, err + } + if inbound == nil { + return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId) + } + + clientEmail := traffic.Email + + oldClients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + + clientId := "" + + for _, oldClient := range oldClients { + if oldClient.Email == clientEmail { + switch inbound.Protocol { + case "trojan": + clientId = oldClient.Password + case "shadowsocks": + clientId = oldClient.Email + default: + clientId = oldClient.ID + } + break + } + } + + if len(clientId) == 0 { + return false, common.NewError("Client Not Found For Email:", clientEmail) + } + + var settings map[string]any + err = json.Unmarshal([]byte(inbound.Settings), &settings) + if err != nil { + return false, err + } + clients := settings["clients"].([]any) + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + if c["email"] == clientEmail { + c["tgId"] = tgId + c["updated_at"] = time.Now().Unix() * 1000 + newClients = append(newClients, any(c)) + } + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, err + } + inbound.Settings = string(modifiedSettings) + needRestart, err := s.UpdateInboundClient(inbound, clientId) + return needRestart, err +} + +func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) { + _, inbound, err := s.GetClientInboundByEmail(clientEmail) + if err != nil { + return false, err + } + if inbound == nil { + return false, common.NewError("Inbound Not Found For Email:", clientEmail) + } + + clients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + + isEnable := false + + for _, client := range clients { + if client.Email == clientEmail { + isEnable = client.Enable + break + } + } + + return isEnable, err +} + +func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bool, error) { + _, inbound, err := s.GetClientInboundByEmail(clientEmail) + if err != nil { + return false, false, err + } + if inbound == nil { + return false, false, common.NewError("Inbound Not Found For Email:", clientEmail) + } + + oldClients, err := s.GetClients(inbound) + if err != nil { + return false, false, err + } + + clientId := "" + clientOldEnabled := false + + for _, oldClient := range oldClients { + if oldClient.Email == clientEmail { + switch inbound.Protocol { + case "trojan": + clientId = oldClient.Password + case "shadowsocks": + clientId = oldClient.Email + default: + clientId = oldClient.ID + } + clientOldEnabled = oldClient.Enable + break + } + } + + if len(clientId) == 0 { + return false, false, common.NewError("Client Not Found For Email:", clientEmail) + } + + var settings map[string]any + err = json.Unmarshal([]byte(inbound.Settings), &settings) + if err != nil { + return false, false, err + } + clients := settings["clients"].([]any) + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + if c["email"] == clientEmail { + c["enable"] = !clientOldEnabled + c["updated_at"] = time.Now().Unix() * 1000 + newClients = append(newClients, any(c)) + } + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, false, err + } + inbound.Settings = string(modifiedSettings) + + needRestart, err := s.UpdateInboundClient(inbound, clientId) + if err != nil { + return false, needRestart, err + } + + return !clientOldEnabled, needRestart, nil +} + +func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) { + _, inbound, err := s.GetClientInboundByEmail(clientEmail) + if err != nil { + return false, err + } + if inbound == nil { + return false, common.NewError("Inbound Not Found For Email:", clientEmail) + } + + oldClients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + + clientId := "" + + for _, oldClient := range oldClients { + if oldClient.Email == clientEmail { + switch inbound.Protocol { + case "trojan": + clientId = oldClient.Password + case "shadowsocks": + clientId = oldClient.Email + default: + clientId = oldClient.ID + } + break + } + } + + if len(clientId) == 0 { + return false, common.NewError("Client Not Found For Email:", clientEmail) + } + + var settings map[string]any + err = json.Unmarshal([]byte(inbound.Settings), &settings) + if err != nil { + return false, err + } + clients := settings["clients"].([]any) + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + if c["email"] == clientEmail { + c["limitIp"] = count + c["updated_at"] = time.Now().Unix() * 1000 + newClients = append(newClients, any(c)) + } + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, err + } + inbound.Settings = string(modifiedSettings) + needRestart, err := s.UpdateInboundClient(inbound, clientId) + return needRestart, err +} + +func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) (bool, error) { + _, inbound, err := s.GetClientInboundByEmail(clientEmail) + if err != nil { + return false, err + } + if inbound == nil { + return false, common.NewError("Inbound Not Found For Email:", clientEmail) + } + + oldClients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + + clientId := "" + + for _, oldClient := range oldClients { + if oldClient.Email == clientEmail { + switch inbound.Protocol { + case "trojan": + clientId = oldClient.Password + case "shadowsocks": + clientId = oldClient.Email + default: + clientId = oldClient.ID + } + break + } + } + + if len(clientId) == 0 { + return false, common.NewError("Client Not Found For Email:", clientEmail) + } + + var settings map[string]any + err = json.Unmarshal([]byte(inbound.Settings), &settings) + if err != nil { + return false, err + } + clients := settings["clients"].([]any) + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + if c["email"] == clientEmail { + c["expiryTime"] = expiry_time + c["updated_at"] = time.Now().Unix() * 1000 + newClients = append(newClients, any(c)) + } + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, err + } + inbound.Settings = string(modifiedSettings) + needRestart, err := s.UpdateInboundClient(inbound, clientId) + return needRestart, err +} + +func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, totalGB int) (bool, error) { + if totalGB < 0 { + return false, common.NewError("totalGB must be >= 0") + } + _, inbound, err := s.GetClientInboundByEmail(clientEmail) + if err != nil { + return false, err + } + if inbound == nil { + return false, common.NewError("Inbound Not Found For Email:", clientEmail) + } + + oldClients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + + clientId := "" + + for _, oldClient := range oldClients { + if oldClient.Email == clientEmail { + switch inbound.Protocol { + case "trojan": + clientId = oldClient.Password + case "shadowsocks": + clientId = oldClient.Email + default: + clientId = oldClient.ID + } + break + } + } + + if len(clientId) == 0 { + return false, common.NewError("Client Not Found For Email:", clientEmail) + } + + var settings map[string]any + err = json.Unmarshal([]byte(inbound.Settings), &settings) + if err != nil { + return false, err + } + clients := settings["clients"].([]any) + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + if c["email"] == clientEmail { + c["totalGB"] = totalGB * 1024 * 1024 * 1024 + c["updated_at"] = time.Now().Unix() * 1000 + newClients = append(newClients, any(c)) + } + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return false, err + } + inbound.Settings = string(modifiedSettings) + needRestart, err := s.UpdateInboundClient(inbound, clientId) + return needRestart, err +} + +func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error { + db := database.GetDB() + + result := db.Model(xray.ClientTraffic{}). + Where("email = ?", clientEmail). + Updates(map[string]any{"enable": true, "up": 0, "down": 0}) + + err := result.Error + if err != nil { + return err + } + return nil +} + +func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) { + needRestart := false + + traffic, err := s.GetClientTrafficByEmail(clientEmail) + if err != nil { + return false, err + } + + if !traffic.Enable { + inbound, err := s.GetInbound(id) + if err != nil { + return false, err + } + clients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + for _, client := range clients { + if client.Email == clientEmail && client.Enable { + s.xrayApi.Init(p.GetAPIPort()) + cipher := "" + if string(inbound.Protocol) == "shadowsocks" { + var oldSettings map[string]any + err = json.Unmarshal([]byte(inbound.Settings), &oldSettings) + if err != nil { + return false, err + } + cipher = oldSettings["method"].(string) + } + err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{ + "email": client.Email, + "id": client.ID, + "security": client.Security, + "flow": client.Flow, + "password": client.Password, + "cipher": cipher, + }) + if err1 == nil { + logger.Debug("Client enabled due to reset traffic:", clientEmail) + } else { + logger.Debug("Error in enabling client by api:", err1) + needRestart = true + } + s.xrayApi.Close() + break + } + } + } + + traffic.Up = 0 + traffic.Down = 0 + traffic.Enable = true + + db := database.GetDB() + err = db.Save(traffic).Error + if err != nil { + return false, err + } + + return needRestart, nil +} + +func (s *InboundService) ResetAllClientTraffics(id int) error { + db := database.GetDB() + + whereText := "inbound_id " + if id == -1 { + whereText += " > ?" + } else { + whereText += " = ?" + } + + result := db.Model(xray.ClientTraffic{}). + Where(whereText, id). + Updates(map[string]any{"enable": true, "up": 0, "down": 0}) + + err := result.Error + return err +} + +func (s *InboundService) ResetAllTraffics() error { + db := database.GetDB() + + result := db.Model(model.Inbound{}). + Where("user_id > ?", 0). + Updates(map[string]any{"up": 0, "down": 0}) + + err := result.Error + return err +} + +func (s *InboundService) DelDepletedClients(id int) (err error) { + db := database.GetDB() + tx := db.Begin() + defer func() { + if err == nil { + tx.Commit() + } else { + tx.Rollback() + } + }() + + whereText := "reset = 0 and inbound_id " + if id < 0 { + whereText += "> ?" + } else { + whereText += "= ?" + } + + depletedClients := []xray.ClientTraffic{} + err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error + if err != nil { + return err + } + + for _, depletedClient := range depletedClients { + emails := strings.Split(depletedClient.Email, ",") + oldInbound, err := s.GetInbound(depletedClient.InboundId) + if err != nil { + return err + } + var oldSettings map[string]any + err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) + if err != nil { + return err + } + + oldClients := oldSettings["clients"].([]any) + var newClients []any + for _, client := range oldClients { + deplete := false + c := client.(map[string]any) + for _, email := range emails { + if email == c["email"].(string) { + deplete = true + break + } + } + if !deplete { + newClients = append(newClients, client) + } + } + if len(newClients) > 0 { + oldSettings["clients"] = newClients + + newSettings, err := json.MarshalIndent(oldSettings, "", " ") + if err != nil { + return err + } + + oldInbound.Settings = string(newSettings) + err = tx.Save(oldInbound).Error + if err != nil { + return err + } + } else { + // Delete inbound if no client remains + s.DelInbound(depletedClient.InboundId) + } + } + + err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error + if err != nil { + return err + } + + return nil +} + +func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) { + db := database.GetDB() + var inbounds []*model.Inbound + + // Retrieve inbounds where settings contain the given tgId + err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId": %d%%`, tgId)).Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err) + return nil, err + } + + var emails []string + for _, inbound := range inbounds { + clients, err := s.GetClients(inbound) + if err != nil { + logger.Errorf("Error retrieving clients for inbound %d: %v", inbound.Id, err) + continue + } + for _, client := range clients { + if client.TgID == tgId { + emails = append(emails, client.Email) + } + } + } + + var traffics []*xray.ClientTraffic + err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + logger.Warning("No ClientTraffic records found for emails:", emails) + return nil, nil + } + logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", emails, err) + return nil, err + } + + return traffics, nil +} + +func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) { + db := database.GetDB() + var traffics []*xray.ClientTraffic + + err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error + if err != nil { + logger.Warningf("Error retrieving ClientTraffic with email %s: %v", email, err) + return nil, err + } + if len(traffics) > 0 { + return traffics[0], nil + } + + return nil, nil +} + +func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error { + db := database.GetDB() + + result := db.Model(xray.ClientTraffic{}). + Where("email = ?", email). + Updates(map[string]any{"up": upload, "down": download}) + + err := result.Error + if err != nil { + logger.Warningf("Error updating ClientTraffic with email %s: %v", email, err) + return err + } + return nil +} + +func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) { + db := database.GetDB() + var traffics []xray.ClientTraffic + + err := db.Model(xray.ClientTraffic{}).Where(`email IN( + SELECT JSON_EXTRACT(client.value, '$.email') as email + FROM inbounds, + JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client + WHERE + JSON_EXTRACT(client.value, '$.id') in (?) + )`, id).Find(&traffics).Error + + if err != nil { + logger.Debug(err) + return nil, err + } + return traffics, err +} + +func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) { + db := database.GetDB() + inbound := &model.Inbound{} + traffic = &xray.ClientTraffic{} + + // Search for inbound settings that contain the query + err = db.Model(model.Inbound{}).Where("settings LIKE ?", "%\""+query+"\"%").First(inbound).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + logger.Warningf("Inbound settings containing query %s not found: %v", query, err) + return nil, err + } + logger.Errorf("Error searching for inbound settings with query %s: %v", query, err) + return nil, err + } + + traffic.InboundId = inbound.Id + + // Unmarshal settings to get clients + settings := map[string][]model.Client{} + if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil { + logger.Errorf("Error unmarshalling inbound settings for inbound ID %d: %v", inbound.Id, err) + return nil, err + } + + clients := settings["clients"] + for _, client := range clients { + if (client.ID == query || client.Password == query) && client.Email != "" { + traffic.Email = client.Email + break + } + } + + if traffic.Email == "" { + logger.Warningf("No client found with query %s in inbound ID %d", query, inbound.Id) + return nil, gorm.ErrRecordNotFound + } + + // Retrieve ClientTraffic based on the found email + err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + logger.Warningf("ClientTraffic for email %s not found: %v", traffic.Email, err) + return nil, err + } + logger.Errorf("Error retrieving ClientTraffic for email %s: %v", traffic.Email, err) + return nil, err + } + + return traffic, nil +} + +func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { + db := database.GetDB() + InboundClientIps := &model.InboundClientIps{} + err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error + if err != nil { + return "", err + } + return InboundClientIps.Ips, nil +} + +func (s *InboundService) ClearClientIps(clientEmail string) error { + db := database.GetDB() + + result := db.Model(model.InboundClientIps{}). + Where("client_email = ?", clientEmail). + Update("ips", "") + err := result.Error + if err != nil { + return err + } + return nil +} + +func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) { + db := database.GetDB() + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return inbounds, nil +} + +func (s *InboundService) MigrationRequirements() { + db := database.GetDB() + tx := db.Begin() + var err error + defer func() { + if err == nil { + tx.Commit() + } else { + tx.Rollback() + } + }() + + // Calculate and backfill all_time from up+down for inbounds and clients + err = tx.Exec(` + UPDATE inbounds + SET all_time = IFNULL(up, 0) + IFNULL(down, 0) + WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0 + `).Error + if err != nil { + return + } + err = tx.Exec(` + UPDATE client_traffics + SET all_time = IFNULL(up, 0) + IFNULL(down, 0) + WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0 + `).Error + + if err != nil { + return + } + + // Fix inbounds based problems + var inbounds []*model.Inbound + err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + return + } + for inbound_index := range inbounds { + settings := map[string]any{} + json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) + clients, ok := settings["clients"].([]any) + if ok { + // Fix Client configuration problems + var newClients []any + for client_index := range clients { + c := clients[client_index].(map[string]any) + + // Add email='' if it is not exists + if _, ok := c["email"]; !ok { + c["email"] = "" + } + + // Convert string tgId to int64 + if _, ok := c["tgId"]; ok { + var tgId any = c["tgId"] + if tgIdStr, ok2 := tgId.(string); ok2 { + tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64) + if err == nil { + c["tgId"] = tgIdInt64 + } + } + } + + // Remove "flow": "xtls-rprx-direct" + if _, ok := c["flow"]; ok { + if c["flow"] == "xtls-rprx-direct" { + c["flow"] = "" + } + } + // Backfill created_at and updated_at + if _, ok := c["created_at"]; !ok { + c["created_at"] = time.Now().Unix() * 1000 + } + c["updated_at"] = time.Now().Unix() * 1000 + + // 中文注释: 回填 speedLimit,如果不存在设为 0,确保旧数据有字段,避免显示和配置问题 + if _, ok := c["speedLimit"]; !ok { + c["speedLimit"] = 0 + } + + newClients = append(newClients, any(c)) + } + settings["clients"] = newClients + modifiedSettings, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return + } + + inbounds[inbound_index].Settings = string(modifiedSettings) + } + + // Add client traffic row for all clients which has email + modelClients, err := s.GetClients(inbounds[inbound_index]) + if err != nil { + return + } + for _, modelClient := range modelClients { + if len(modelClient.Email) > 0 { + var count int64 + tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count) + if count == 0 { + s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient) + } + } + } + } + tx.Save(inbounds) + + // Remove orphaned traffics + tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) + + // Migrate old MultiDomain to External Proxy + var externalProxy []struct { + Id int + Port int + StreamSettings []byte + } + err = tx.Raw(`select id, port, stream_settings + from inbounds + WHERE protocol in ('vmess','vless','trojan') + AND json_extract(stream_settings, '$.security') = 'tls' + AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error + if err != nil || len(externalProxy) == 0 { + return + } + + for _, ep := range externalProxy { + var reverses any + var stream map[string]any + json.Unmarshal(ep.StreamSettings, &stream) + if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok { + if settings, ok := tlsSettings["settings"].(map[string]any); ok { + if domains, ok := settings["domains"].([]any); ok { + for _, domain := range domains { + if domainMap, ok := domain.(map[string]any); ok { + domainMap["forceTls"] = "same" + domainMap["port"] = ep.Port + domainMap["dest"] = domainMap["domain"].(string) + delete(domainMap, "domain") + } + } + } + reverses = settings["domains"] + delete(settings, "domains") + } + } + stream["externalProxy"] = reverses + newStream, _ := json.MarshalIndent(stream, " ", " ") + tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream) + } + + err = tx.Raw(`UPDATE inbounds + SET tag = REPLACE(tag, '0.0.0.0:', '') + WHERE INSTR(tag, '0.0.0.0:') > 0;`).Error + if err != nil { + return + } +} + +func (s *InboundService) MigrateDB() { + s.MigrationRequirements() + s.MigrationRemoveOrphanedTraffics() +} + +func (s *InboundService) GetOnlineClients() []string { + return p.GetOnlineClients() +} + +func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) { + db := database.GetDB() + var rows []xray.ClientTraffic + err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + result := make(map[string]int64, len(rows)) + for _, r := range rows { + result[r.Email] = r.LastOnline + } + return result, nil +} + +func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) { + db := database.GetDB() + + // Step 1: Get ClientTraffic records for emails in the input list + var clients []xray.ClientTraffic + err := db.Where("email IN ?", emails).Find(&clients).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, nil, err + } + + // Step 2: Sort clients by (Up + Down) descending + sort.Slice(clients, func(i, j int) bool { + return (clients[i].Up + clients[i].Down) > (clients[j].Up + clients[j].Down) + }) + + // Step 3: Extract sorted valid emails and track found ones + validEmails := make([]string, 0, len(clients)) + found := make(map[string]bool) + for _, client := range clients { + validEmails = append(validEmails, client.Email) + found[client.Email] = true + } + + // Step 4: Identify emails that were not found in the database + extraEmails := make([]string, 0) + for _, email := range emails { + if !found[email] { + extraEmails = append(extraEmails, email) + } + } + + return validEmails, extraEmails, nil +} diff --git a/web/service/outbound.go b/web/service/outbound.go new file mode 100644 index 0000000..506cf61 --- /dev/null +++ b/web/service/outbound.go @@ -0,0 +1,100 @@ +package service + +import ( + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/xray" + + "gorm.io/gorm" +) + +type OutboundService struct{} + +func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { + var err error + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err != nil { + tx.Rollback() + } else { + tx.Commit() + } + }() + + err = s.addOutboundTraffic(tx, traffics) + if err != nil { + return err, false + } + + return nil, false +} + +func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error { + if len(traffics) == 0 { + return nil + } + + var err error + + for _, traffic := range traffics { + if traffic.IsOutbound { + + var outbound model.OutboundTraffics + + err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag). + FirstOrCreate(&outbound).Error + if err != nil { + return err + } + + outbound.Tag = traffic.Tag + outbound.Up = outbound.Up + traffic.Up + outbound.Down = outbound.Down + traffic.Down + outbound.Total = outbound.Up + outbound.Down + + err = tx.Save(&outbound).Error + if err != nil { + return err + } + } + } + return nil +} + +func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) { + db := database.GetDB() + var traffics []*model.OutboundTraffics + + err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error + if err != nil { + logger.Warning("Error retrieving OutboundTraffics: ", err) + return nil, err + } + + return traffics, nil +} + +func (s *OutboundService) ResetOutboundTraffic(tag string) error { + db := database.GetDB() + + whereText := "tag " + if tag == "-alltags-" { + whereText += " <> ?" + } else { + whereText += " = ?" + } + + result := db.Model(model.OutboundTraffics{}). + Where(whereText, tag). + Updates(map[string]any{"up": 0, "down": 0, "total": 0}) + + err := result.Error + if err != nil { + return err + } + + return nil +} diff --git a/web/service/panel.go b/web/service/panel.go new file mode 100644 index 0000000..3b0c75a --- /dev/null +++ b/web/service/panel.go @@ -0,0 +1,26 @@ +package service + +import ( + "os" + "syscall" + "time" + + "x-ui/logger" +) + +type PanelService struct{} + +func (s *PanelService) RestartPanel(delay time.Duration) error { + p, err := os.FindProcess(syscall.Getpid()) + if err != nil { + return err + } + go func() { + time.Sleep(delay) + err := p.Signal(syscall.SIGHUP) + if err != nil { + logger.Error("failed to send SIGHUP signal:", err) + } + }() + return nil +} diff --git a/web/service/server.go b/web/service/server.go new file mode 100644 index 0000000..cd9ef22 --- /dev/null +++ b/web/service/server.go @@ -0,0 +1,1392 @@ +package service + +import ( + "archive/zip" + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "io/fs" + "mime/multipart" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + "context" + + "x-ui/config" + "x-ui/database" + "x-ui/logger" + "x-ui/util/common" + "x-ui/util/sys" + "x-ui/xray" + + "github.com/google/uuid" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" +) + +type ProcessState string + +const ( + Running ProcessState = "running" + Stop ProcessState = "stop" + Error ProcessState = "error" +) + +type Status struct { + T time.Time `json:"-"` + Cpu float64 `json:"cpu"` + CpuCores int `json:"cpuCores"` + LogicalPro int `json:"logicalPro"` + CpuSpeedMhz float64 `json:"cpuSpeedMhz"` + Mem struct { + Current uint64 `json:"current"` + Total uint64 `json:"total"` + } `json:"mem"` + Swap struct { + Current uint64 `json:"current"` + Total uint64 `json:"total"` + } `json:"swap"` + Disk struct { + Current uint64 `json:"current"` + Total uint64 `json:"total"` + } `json:"disk"` + Xray struct { + State ProcessState `json:"state"` + ErrorMsg string `json:"errorMsg"` + Version string `json:"version"` + } `json:"xray"` + Uptime uint64 `json:"uptime"` + Loads []float64 `json:"loads"` + TcpCount int `json:"tcpCount"` + UdpCount int `json:"udpCount"` + NetIO struct { + Up uint64 `json:"up"` + Down uint64 `json:"down"` + } `json:"netIO"` + NetTraffic struct { + Sent uint64 `json:"sent"` + Recv uint64 `json:"recv"` + } `json:"netTraffic"` + PublicIP struct { + IPv4 string `json:"ipv4"` + IPv6 string `json:"ipv6"` + } `json:"publicIP"` + AppStats struct { + Threads uint32 `json:"threads"` + Mem uint64 `json:"mem"` + Uptime uint64 `json:"uptime"` + } `json:"appStats"` +} + +type Release struct { + TagName string `json:"tag_name"` +} + +type ServerService struct { + xrayService XrayService + inboundService InboundService + tgService TelegramService + cachedIPv4 string + cachedIPv6 string + noIPv6 bool +} + +// 【新增方法】: 用于从外部注入 TelegramService 实例 +func (s *ServerService) SetTelegramService(tgService TelegramService) { + s.tgService = tgService +} + +func getPublicIP(url string) string { + client := &http.Client{ + Timeout: 3 * time.Second, + } + + resp, err := client.Get(url) + if err != nil { + return "N/A" + } + defer resp.Body.Close() + + // Don't retry if access is blocked or region-restricted + if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons { + return "N/A" + } + if resp.StatusCode != http.StatusOK { + return "N/A" + } + + ip, err := io.ReadAll(resp.Body) + if err != nil { + return "N/A" + } + + ipString := strings.TrimSpace(string(ip)) + if ipString == "" { + return "N/A" + } + + return ipString +} + +func (s *ServerService) GetStatus(lastStatus *Status) *Status { + now := time.Now() + status := &Status{ + T: now, + } + + // CPU stats + percents, err := cpu.Percent(0, false) + if err != nil { + logger.Warning("get cpu percent failed:", err) + } else { + status.Cpu = percents[0] + } + + status.CpuCores, err = cpu.Counts(false) + if err != nil { + logger.Warning("get cpu cores count failed:", err) + } + + status.LogicalPro = runtime.NumCPU() + + cpuInfos, err := cpu.Info() + if err != nil { + logger.Warning("get cpu info failed:", err) + } else if len(cpuInfos) > 0 { + status.CpuSpeedMhz = cpuInfos[0].Mhz + } else { + logger.Warning("could not find cpu info") + } + + // Uptime + upTime, err := host.Uptime() + if err != nil { + logger.Warning("get uptime failed:", err) + } else { + status.Uptime = upTime + } + + // Memory stats + memInfo, err := mem.VirtualMemory() + if err != nil { + logger.Warning("get virtual memory failed:", err) + } else { + status.Mem.Current = memInfo.Used + status.Mem.Total = memInfo.Total + } + + swapInfo, err := mem.SwapMemory() + if err != nil { + logger.Warning("get swap memory failed:", err) + } else { + status.Swap.Current = swapInfo.Used + status.Swap.Total = swapInfo.Total + } + + // Disk stats + diskInfo, err := disk.Usage("/") + if err != nil { + logger.Warning("get disk usage failed:", err) + } else { + status.Disk.Current = diskInfo.Used + status.Disk.Total = diskInfo.Total + } + + // Load averages + avgState, err := load.Avg() + if err != nil { + logger.Warning("get load avg failed:", err) + } else { + status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15} + } + + // Network stats + ioStats, err := net.IOCounters(false) + if err != nil { + logger.Warning("get io counters failed:", err) + } else if len(ioStats) > 0 { + ioStat := ioStats[0] + status.NetTraffic.Sent = ioStat.BytesSent + status.NetTraffic.Recv = ioStat.BytesRecv + + if lastStatus != nil { + duration := now.Sub(lastStatus.T) + seconds := float64(duration) / float64(time.Second) + up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds) + down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds) + status.NetIO.Up = up + status.NetIO.Down = down + } + } else { + logger.Warning("can not find io counters") + } + + // TCP/UDP connections + status.TcpCount, err = sys.GetTCPCount() + if err != nil { + logger.Warning("get tcp connections failed:", err) + } + + status.UdpCount, err = sys.GetUDPCount() + if err != nil { + logger.Warning("get udp connections failed:", err) + } + + // IP fetching with caching + showIp4ServiceLists := []string{ + "https://api4.ipify.org", + "https://ipv4.icanhazip.com", + "https://v4.api.ipinfo.io/ip", + "https://ipv4.myexternalip.com/raw", + "https://4.ident.me", + "https://check-host.net/ip", + } + showIp6ServiceLists := []string{ + "https://api6.ipify.org", + "https://ipv6.icanhazip.com", + "https://v6.api.ipinfo.io/ip", + "https://ipv6.myexternalip.com/raw", + "https://6.ident.me", + } + + if s.cachedIPv4 == "" { + for _, ip4Service := range showIp4ServiceLists { + s.cachedIPv4 = getPublicIP(ip4Service) + if s.cachedIPv4 != "N/A" { + break + } + } + } + + if s.cachedIPv6 == "" && !s.noIPv6 { + for _, ip6Service := range showIp6ServiceLists { + s.cachedIPv6 = getPublicIP(ip6Service) + if s.cachedIPv6 != "N/A" { + break + } + } + } + + if s.cachedIPv6 == "N/A" { + s.noIPv6 = true + } + + status.PublicIP.IPv4 = s.cachedIPv4 + status.PublicIP.IPv6 = s.cachedIPv6 + + // Xray status + if s.xrayService.IsXrayRunning() { + status.Xray.State = Running + status.Xray.ErrorMsg = "" + } else { + err := s.xrayService.GetXrayErr() + if err != nil { + status.Xray.State = Error + } else { + status.Xray.State = Stop + } + status.Xray.ErrorMsg = s.xrayService.GetXrayResult() + } + status.Xray.Version = s.xrayService.GetXrayVersion() + + // Application stats + var rtm runtime.MemStats + runtime.ReadMemStats(&rtm) + status.AppStats.Mem = rtm.Sys + status.AppStats.Threads = uint32(runtime.NumGoroutine()) + if p != nil && p.IsRunning() { + status.AppStats.Uptime = p.GetUptime() + } else { + status.AppStats.Uptime = 0 + } + + return status +} + +func (s *ServerService) GetXrayVersions() ([]string, error) { + const ( + XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases" + bufferSize = 8192 + ) + + resp, err := http.Get(XrayURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Check HTTP status code - GitHub API returns object instead of array on error + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + var errorResponse struct { + Message string `json:"message"` + } + if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" { + return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message) + } + return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status) + } + + buffer := bytes.NewBuffer(make([]byte, bufferSize)) + buffer.Reset() + if _, err := buffer.ReadFrom(resp.Body); err != nil { + return nil, err + } + + var releases []Release + if err := json.Unmarshal(buffer.Bytes(), &releases); err != nil { + return nil, err + } + + var versions []string + for _, release := range releases { + tagVersion := strings.TrimPrefix(release.TagName, "v") + tagParts := strings.Split(tagVersion, ".") + if len(tagParts) != 3 { + continue + } + + major, err1 := strconv.Atoi(tagParts[0]) + minor, err2 := strconv.Atoi(tagParts[1]) + patch, err3 := strconv.Atoi(tagParts[2]) + if err1 != nil || err2 != nil || err3 != nil { + continue + } + + if major > 26 || (major == 26 && minor > 2) || (major == 26 && minor == 2 && patch >= 6) { + versions = append(versions, release.TagName) + } + } + return versions, nil +} + +func (s *ServerService) StopXrayService() error { + err := s.xrayService.StopXray() + if err != nil { + logger.Error("stop xray failed:", err) + return err + } + return nil +} + +func (s *ServerService) RestartXrayService() error { + err := s.xrayService.RestartXray(true) + if err != nil { + logger.Error("start xray failed:", err) + return err + } + return nil +} + +func (s *ServerService) downloadXRay(version string) (string, error) { + osName := runtime.GOOS + arch := runtime.GOARCH + + switch osName { + case "darwin": + osName = "macos" + case "windows": + osName = "windows" + } + + switch arch { + case "amd64": + arch = "64" + case "arm64": + arch = "arm64-v8a" + case "armv7": + arch = "arm32-v7a" + case "armv6": + arch = "arm32-v6" + case "armv5": + arch = "arm32-v5" + case "386": + arch = "32" + case "s390x": + arch = "s390x" + } + + fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) + url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName) + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + os.Remove(fileName) + file, err := os.Create(fileName) + if err != nil { + return "", err + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return "", err + } + + return fileName, nil +} + +func (s *ServerService) UpdateXray(version string) error { + // 1. Stop xray before doing anything + if err := s.StopXrayService(); err != nil { + logger.Warning("failed to stop xray before update:", err) + } + + // 2. Download the zip + zipFileName, err := s.downloadXRay(version) + if err != nil { + return err + } + defer os.Remove(zipFileName) + + zipFile, err := os.Open(zipFileName) + if err != nil { + return err + } + defer zipFile.Close() + + stat, err := zipFile.Stat() + if err != nil { + return err + } + reader, err := zip.NewReader(zipFile, stat.Size()) + if err != nil { + return err + } + + // 3. Helper to extract files + + copyZipFile := func(zipName string, fileName string) error { + zipFile, err := reader.Open(zipName) + if err != nil { + return err + } + defer zipFile.Close() + os.MkdirAll(filepath.Dir(fileName), 0755) + os.Remove(fileName) + file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(file, zipFile) + return err + } + + // 4. Extract correct binary + if runtime.GOOS == "windows" { + targetBinary := filepath.Join("bin", "xray-windows-amd64.exe") + err = copyZipFile("xray.exe", targetBinary) + } else { + err = copyZipFile("xray", xray.GetBinaryPath()) + } + if err != nil { + return err + } + + // 5. Restart xray + if err := s.xrayService.RestartXray(true); err != nil { + logger.Error("start xray failed:", err) + return err + } + + return nil +} + +func (s *ServerService) GetLogs(count string, level string, syslog string) []string { + c, _ := strconv.Atoi(count) + var lines []string + + if syslog == "true" { + cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level} + // Run the command + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return []string{"Failed to run journalctl command!"} + } + lines = strings.Split(out.String(), "\n") + } else { + lines = logger.GetLogs(c, level) + } + + return lines +} + +func (s *ServerService) GetXrayLogs( + count string, + filter string, + showDirect string, + showBlocked string, + showProxy string, + freedoms []string, + blackholes []string) []string { + + countInt, _ := strconv.Atoi(count) + var lines []string + + pathToAccessLog, err := xray.GetAccessLogPath() + if err != nil { + return lines + } + + file, err := os.Open(pathToAccessLog) + if err != nil { + return lines + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if line == "" || strings.Contains(line, "api -> api") { + //skipping empty lines and api calls + continue + } + + if filter != "" && !strings.Contains(line, filter) { + //applying filter if it's not empty + continue + } + + //adding suffixes to further distinguish entries by outbound + if hasSuffix(line, freedoms) { + if showDirect == "false" { + continue + } + line = line + " f" + } else if hasSuffix(line, blackholes) { + if showBlocked == "false" { + continue + } + line = line + " b" + } else { + if showProxy == "false" { + continue + } + line = line + " p" + } + + lines = append(lines, line) + } + + if len(lines) > countInt { + lines = lines[len(lines)-countInt:] + } + + return lines +} + +func hasSuffix(line string, suffixes []string) bool { + for _, sfx := range suffixes { + if strings.HasSuffix(line, sfx+"]") { + return true + } + } + return false +} + +func (s *ServerService) GetConfigJson() (any, error) { + config, err := s.xrayService.GetXrayConfig() + if err != nil { + return nil, err + } + // 修复:将 U+00A0 替换为标准空格 + contents, err := json.MarshalIndent(config, "", " ") + if err != nil { + return nil, err + } + + var jsonData any + err = json.Unmarshal(contents, &jsonData) + if err != nil { + return nil, err + } + + return jsonData, nil +} + +func (s *ServerService) GetDb() ([]byte, error) { + // Update by manually trigger a checkpoint operation + err := database.Checkpoint() + if err != nil { + return nil, err + } + // Open the file for reading + file, err := os.Open(config.GetDBPath()) + if err != nil { + return nil, err + } + defer file.Close() + + // Read the file contents + fileContents, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + return fileContents, nil +} + +func (s *ServerService) ImportDB(file multipart.File) error { + // Check if the file is a SQLite database + isValidDb, err := database.IsSQLiteDB(file) + if err != nil { + return common.NewErrorf("Error checking db file format: %v", err) + } + if !isValidDb { + return common.NewError("Invalid db file format") + } + + // Reset the file reader to the beginning + _, err = file.Seek(0, 0) + if err != nil { + return common.NewErrorf("Error resetting file reader: %v", err) + } + + // Save the file as a temporary file + tempPath := fmt.Sprintf("%s.temp", config.GetDBPath()) + + // Remove the existing temporary file (if any) + if _, err := os.Stat(tempPath); err == nil { + if errRemove := os.Remove(tempPath); errRemove != nil { + return common.NewErrorf("Error removing existing temporary db file: %v", errRemove) + } + } + + // Create the temporary file + tempFile, err := os.Create(tempPath) + if err != nil { + return common.NewErrorf("Error creating temporary db file: %v", err) + } + + // Robust deferred cleanup for the temporary file + defer func() { + if tempFile != nil { + if cerr := tempFile.Close(); cerr != nil { + logger.Warningf("Warning: failed to close temp file: %v", cerr) + } + } + if _, err := os.Stat(tempPath); err == nil { + if rerr := os.Remove(tempPath); rerr != nil { + logger.Warningf("Warning: failed to remove temp file: %v", rerr) + } + } + }() + + // Save uploaded file to temporary file + if _, err = io.Copy(tempFile, file); err != nil { + return common.NewErrorf("Error saving db: %v", err) + } + + // Check if we can init the db or not + if err = database.InitDB(tempPath); err != nil { + return common.NewErrorf("Error checking db: %v", err) + } + + // Stop Xray + s.StopXrayService() + + // Backup the current database for fallback + fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath()) + + // Remove the existing fallback file (if any) + if _, err := os.Stat(fallbackPath); err == nil { + if errRemove := os.Remove(fallbackPath); errRemove != nil { + return common.NewErrorf("Error removing existing fallback db file: %v", errRemove) + } + } + + // Move the current database to the fallback location + if err = os.Rename(config.GetDBPath(), fallbackPath); err != nil { + return common.NewErrorf("Error backing up current db file: %v", err) + } + + // Defer fallback cleanup ONLY if everything goes well + defer func() { + if _, err := os.Stat(fallbackPath); err == nil { + if rerr := os.Remove(fallbackPath); rerr != nil { + logger.Warningf("Warning: failed to remove fallback file: %v", rerr) + } + } + }() + + // Move temp to DB path + if err = os.Rename(tempPath, config.GetDBPath()); err != nil { + // Restore from fallback + if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil { + return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename) + } + return common.NewErrorf("Error moving db file: %v", err) + } + + // Migrate DB + if err = database.InitDB(config.GetDBPath()); err != nil { + if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil { + return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename) + } + return common.NewErrorf("Error migrating db: %v", err) + } + + s.inboundService.MigrateDB() + + // Start Xray + if err = s.RestartXrayService(); err != nil { + return common.NewErrorf("Imported DB but failed to start Xray: %v", err) + } + + return nil +} + +// IsValidGeofileName validates that the filename is safe for geofile operations. +// It checks for path traversal attempts and ensures the filename contains only safe characters. +func (s *ServerService) IsValidGeofileName(filename string) bool { + if filename == "" { + return false + } + + // Check for path traversal attempts + if strings.Contains(filename, "..") { + return false + } + + // Check for path separators (both forward and backward slash) + if strings.ContainsAny(filename, `/\`) { + return false + } + + // Check for absolute path indicators + if filepath.IsAbs(filename) { + return false + } + + // Additional security: only allow alphanumeric, dots, underscores, and hyphens + // This is stricter than the general filename regex + validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$` + matched, _ := regexp.MatchString(validGeofilePattern, filename) + return matched +} + +func (s *ServerService) UpdateGeofile(fileName string) error { + files := []struct { + URL string + FileName string + }{ + {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"}, + {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"}, + {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"}, + {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"}, + {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"}, + {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"}, + } + + // Strict allowlist check to avoid writing uncontrolled files + if fileName != "" { + // Use the centralized validation function + if !s.IsValidGeofileName(fileName) { + return common.NewErrorf("Invalid geofile name: contains unsafe path characters: %s", fileName) + } + + // Ensure the filename matches exactly one from our allowlist + isAllowed := false + for _, file := range files { + if fileName == file.FileName { + isAllowed = true + break + } + } + if !isAllowed { + return common.NewErrorf("Invalid geofile name: %s not in allowlist", fileName) + } + } + + downloadFile := func(url, destPath string) error { + var req *http.Request + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err) + } + + var localFileModTime time.Time + if fileInfo, err := os.Stat(destPath); err == nil { + localFileModTime = fileInfo.ModTime() + if !localFileModTime.IsZero() { + req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat)) + } + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return common.NewErrorf("Failed to download Geofile from %s: %v", url, err) + } + defer resp.Body.Close() + + // Parse Last-Modified header from server + var serverModTime time.Time + serverModTimeStr := resp.Header.Get("Last-Modified") + if serverModTimeStr != "" { + parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr) + if err != nil { + logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err) + } else { + serverModTime = parsedTime + } + } + + // Function to update local file's modification time + updateFileModTime := func() { + if !serverModTime.IsZero() { + if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil { + logger.Warningf("Failed to update modification time for %s: %v", destPath, err) + } + } + } + + // Handle 304 Not Modified + if resp.StatusCode == http.StatusNotModified { + updateFileModTime() + return nil + } + + if resp.StatusCode != http.StatusOK { + return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode) + } + + file, err := os.Create(destPath) + if err != nil { + return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err) + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err) + } + + updateFileModTime() + return nil + } + + var errorMessages []string + + if fileName == "" { + for _, file := range files { + // Sanitize the filename from our allowlist as an extra precaution + destPath := filepath.Join(config.GetBinFolderPath(), filepath.Base(file.FileName)) + if err := downloadFile(file.URL, destPath); err != nil { + errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err)) + } + } + } else { + destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName) + + var fileURL string + for _, file := range files { + if file.FileName == fileName { + fileURL = file.URL + break + } + } + + if fileURL == "" { + errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName)) + } + + if err := downloadFile(fileURL, destPath); err != nil { + errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err)) + } + } + + err := s.RestartXrayService() + if err != nil { + errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err)) + } + + if len(errorMessages) > 0 { + return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n")) + } + + return nil +} + +func (s *ServerService) GetNewX25519Cert() (any, error) { + // Run the command + cmd := exec.Command(xray.GetBinaryPath(), "x25519") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + + privateKeyLine := strings.Split(lines[0], ":") + publicKeyLine := strings.Split(lines[1], ":") + + privateKey := strings.TrimSpace(privateKeyLine[1]) + publicKey := strings.TrimSpace(publicKeyLine[1]) + + keyPair := map[string]any{ + "privateKey": privateKey, + "publicKey": publicKey, // 修复:U+00A0 替换为标准空格 + } + + return keyPair, nil +} + +func (s *ServerService) GetNewmldsa65() (any, error) { + // Run the command + cmd := exec.Command(xray.GetBinaryPath(), "mldsa65") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + + SeedLine := strings.Split(lines[0], ":") + VerifyLine := strings.Split(lines[1], ":") + + seed := strings.TrimSpace(SeedLine[1]) + verify := strings.TrimSpace(VerifyLine[1]) + + keyPair := map[string]any{ + "seed": seed, + "verify": verify, + } + + return keyPair, nil +} + +func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) { + // Run the command + cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + if len(lines) < 4 { + return nil, common.NewError("invalid ech cert") + } + + configList := lines[1] + serverKeys := lines[3] + + return map[string]interface{}{ + "echServerKeys": serverKeys, + "echConfigList": configList, + }, nil +} + +func (s *ServerService) GetNewVlessEnc() (any, error) { + cmd := exec.Command(xray.GetBinaryPath(), "vlessenc") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + + var auths []map[string]string + var current map[string]string + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "Authentication:") { + if current != nil { + auths = append(auths, current) + } + current = map[string]string{ + "label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")), + } + } else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 && current != nil { + key := strings.Trim(parts[0], `" `) + val := strings.Trim(parts[1], `" `) + current[key] = val + } + } + } + + if current != nil { + auths = append(auths, current) + } + + return map[string]any{ + "auths": auths, + }, nil +} + +func (s *ServerService) GetNewUUID() (map[string]string, error) { + newUUID, err := uuid.NewRandom() + if err != nil { + return nil, fmt.Errorf("failed to generate UUID: %w", err) + } + + return map[string]string{ + "uuid": newUUID.String(), + }, nil +} + +func (s *ServerService) GetNewmlkem768() (any, error) { + // Run the command + cmd := exec.Command(xray.GetBinaryPath(), "mlkem768") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + + SeedLine := strings.Split(lines[0], ":") + ClientLine := strings.Split(lines[1], ":") + + seed := strings.TrimSpace(SeedLine[1]) + client := strings.TrimSpace(ClientLine[1]) + + keyPair := map[string]any{ + "seed": seed, + "client": client, + } + + return keyPair, nil +} + +// SaveLinkHistory 保存一个新的链接记录,并确保其被永久写入数据库文件。 +func (s *ServerService) SaveLinkHistory(historyType, link string) error { + record := &database.LinkHistory{ + Type: historyType, + Link: link, + CreatedAt: time.Now(), + } + + // 【核心修正】: 第一步,调用重构后的 AddLinkHistory 函数。 + // 这个函数现在是一个原子事务。如果它没有返回错误,就意味着数据已经成功提交到了 .wal 日志文件。 + err := database.AddLinkHistory(record) + if err != nil { + return err // 如果事务失败,直接返回错误,不执行后续操作 + } + + // 【核心修正】: 第二步,在事务成功提交后,我们在这里调用 Checkpoint。 + // 此时 .wal 文件中已经包含了我们的新数据,调用 Checkpoint 可以确保这些数据被立即写入主数据库文件。 + return database.Checkpoint() +} + +// LoadLinkHistory loads the latest 10 links from the database +func (s *ServerService) LoadLinkHistory() ([]*database.LinkHistory, error) { + return database.GetLinkHistory() +} + +// 〔新增方法〕: 安装 Subconverter (异步执行) +// 〔中文注释〕: 此方法用于接收前端或 TG 的请求,并执行 x-ui.sh 脚本中的 subconverter 函数 +func (s *ServerService) InstallSubconverter() error { + // 〔中文注释〕: 使用一个新的 goroutine 来执行耗时的安装任务,这样 API 可以立即返回 + go func() { + + // 【新增功能】:执行端口放行操作 + var ufwWarning string + if ufwErr := s.openSubconverterPorts(); ufwErr != nil { + // 不中断流程,只生成警告消息 + logger.Warningf("自动放行 Subconverter 端口失败: %v", ufwErr) + ufwWarning = fmt.Sprintf("⚠️ **警告:订阅转换端口放行失败**\n\n自动执行 UFW 命令失败,请务必**手动**在您的 VPS 上放行端口 `8000` 和 `15268`,否则服务将无法访问。失败详情:%v\n\n", ufwErr) + } + + // 〔中文注释〕: 检查全局的 TgBot 实例是否存在并且正在运行 + if s.tgService == nil || !s.tgService.IsRunning() { + logger.Warning("TgBot 未运行,无法发送【订阅转换】状态通知。") + // 即使机器人未运行,安装流程也应继续,只是不发通知 + ufwWarning = "" // 如果机器人不在线,不发送任何警告/消息 + } + + // 脚本路径为 /usr/bin/x-ui + // 〔中文注释〕: 通常,安装脚本会将主命令软链接或复制到 /usr/bin/ 目录下,使其成为一个系统命令。 + // 直接调用这个命令比调用源文件路径更规范,也能确保执行的是用户在命令行中使用的同一个脚本。 + scriptPath := "/usr/bin/x-ui" + + // 〔中文注释〕: 检查脚本文件是否存在 + if _, err := os.Stat(scriptPath); os.IsNotExist(err) { + errMsg := fmt.Sprintf("订阅转换安装失败:关键脚本文件 `%s` 未找到。", scriptPath) + logger.Error(errMsg) + if s.tgService != nil && s.tgService.IsRunning() { + // 〔中文注释〕: 使用 Markdown 格式发送错误消息 + s.tgService.SendMessage("❌ " + errMsg) + } + return + } + + // 〔中文注释〕: 正确的调用方式是:命令是 "x-ui",参数是 "subconverter"。 + cmd := exec.Command(scriptPath, "subconverter") + + // 〔中文注释〕: 执行命令并获取其合并的输出(标准输出 + 标准错误),方便排查问题。 + // 〔重要〕: 这个命令可能需要几分钟才能执行完毕,Go程序会在此等待直到脚本执行完成。 + output, err := cmd.CombinedOutput() + + if err != nil { + if s.tgService != nil && s.tgService.IsRunning() { + // 构造失败消息 + message := fmt.Sprintf("❌ **订阅转换安装失败**!\n\n**错误信息**: %v\n**输出**: %s", err, string(output)) + s.tgService.SendMessage(message) + } + logger.Errorf("订阅转换安装失败: %v\n输出: %s", err, string(output)) + return + } else { + + // 【新增逻辑】:如果之前端口放行失败,先发送警告消息 + if ufwWarning != "" { + s.tgService.SendMessage(ufwWarning) + } + + // 安装成功后,发送通知到 TG 机器人 + if s.tgService != nil && s.tgService.IsRunning() { + // 获取面板域名,注意:t.getDomain() 是 Tgbot 的方法 + domain, getDomainErr := s.tgService.GetDomain() + if getDomainErr != nil { + logger.Errorf("TG Bot: 订阅转换安装成功,但获取域名失败: %v", getDomainErr) + } else { + // 构造消息,使用用户指定的格式 + message := fmt.Sprintf( + "🎉 **恭喜!【订阅转换】模块已成功安装!**\n\n"+ + "您现在可以使用以下地址访问 Web 界面:\n\n"+ + "🔗 **登录地址**: `https://%s:15268`\n\n"+ + "默认用户名: `admin`\n"+ + "默认 密码: `123456`\n\n"+ + "可登录订阅转换后台修改您的密码!", domain) + + // 发送成功消息 + if sendErr := s.tgService.SendMessage(message); sendErr != nil { + logger.Errorf("TG Bot: 订阅转换安装成功,但发送通知失败: %v", sendErr) + } else { + logger.Info("TG Bot: 订阅转换安装成功通知已发送。") + } + } + } + + logger.Info("订阅转换安装成功。") + return + } + }() + + return nil // 立即返回,表示指令已接收 +} + +// openSubconverterPorts 检查/安装 ufw 并放行 8000 和 15268 端口 +func (s *ServerService) openSubconverterPorts() error { + // 【中文注释】: Shell 脚本更新,增加了默认端口列表和相应的放行逻辑。 + shellCommand := ` + PORTS_TO_OPEN="8000 15268" + # 【中文注释】: 定义一个包含所有必须默认放行的端口的列表。 + DEFAULT_PORTS="22 80 443 13688 8443" + + echo "脚本启动:正在为订阅转换服务配置防火墙..." + + # 1. 检查/安装 ufw + if ! command -v ufw &>/dev/null; then + echo "ufw 防火墙未安装,正在安装..." + # 静默更新和安装 + DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -qq >/dev/null + DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install -y -qq ufw >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 安装失败或权限不足。"; exit 1; fi + fi + + # 2. 【中文注释】: 新增步骤,循环检查并放行所有默认端口。 + echo "正在检查并放行基础服务端口: $DEFAULT_PORTS" + for p in $DEFAULT_PORTS; do + # 检查规则是否已存在,不存在时才添加,避免重复 + if ! ufw status | grep -qw "$p/tcp"; then + echo "端口 $p/tcp 未放行,正在添加规则..." + ufw allow $p/tcp >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 端口 $p 放行失败。"; exit 1; fi + else + echo "端口 $p/tcp 规则已存在,跳过。" + fi + done + echo "✅ 基础服务端口检查完毕。" + + + # 3. 放行 Subconverter 自身需要的端口 + echo "正在检查并放行订阅转换服务端口: $PORTS_TO_OPEN" + for port in $PORTS_TO_OPEN; do + if ! ufw status | grep -qw "$port"; then + echo "正在执行 ufw allow $port..." + ufw allow $port >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 端口 $port 放行失败。"; exit 1; fi + else + echo "端口 $port 规则已存在,跳过。" + fi + done + + # 4. 检查/激活防火墙 + if ! ufw status | grep -q "Status: active"; then + echo "ufw 状态:未激活。正在尝试激活..." + ufw --force enable + if [ $? -ne 0 ]; then echo "❌ ufw 激活失败。"; exit 1; fi + fi + + echo "✅ 所有端口 ($DEFAULT_PORTS $PORTS_TO_OPEN) 已成功放行/检查。" + exit 0 + ` + + // 使用 /bin/bash -c 执行命令,并捕获输出 + cmd := exec.CommandContext(context.Background(), "/bin/bash", "-c", shellCommand) + output, err := cmd.CombinedOutput() + logOutput := string(output) + + // 记录日志,无论成功与否 + logger.Infof("执行 Subconverter 端口放行命令结果:\n%s", logOutput) + + if err != nil { + // 如果 Shell 命令返回非零退出码,则返回错误 + return fmt.Errorf("ufw 端口放行失败: %v. 脚本输出: %s", err, logOutput) + } + + return nil +} + + +// 【新增方法实现】: 后台前端开放指定端口 +// OpenPort 供前端调用,自动检查/安装 ufw 并放行指定的端口。 +// 〔中文注释〕: 整个函数逻辑被放入一个 go func() 协程中,实现异步后台执行。 +// 〔中文注释〕: 函数签名不再返回 error,因为它会立即返回,无法得知后台任务的最终结果。 +func (s *ServerService) OpenPort(port string) { + // 〔中文注释〕: 启动一个新的协程来处理耗时任务,这样 HTTP 请求可以立刻返回。 + go func() { + // 1. 将 port string 转换为 int + portInt, err := strconv.Atoi(port) + if err != nil { + // 〔中文注释〕: 在后台任务中,如果出错,我们只能记录日志,因为无法再返回给前端。 + logger.Errorf("端口号格式错误,无法转换为数字: %s", port) + return + } + + // 2. 将 Shell 逻辑整合为一个可执行的命令,并使用 /bin/bash -c 执行 + // 【中文注释】: 此处同样增加了默认端口的定义和放行逻辑。 + shellCommand := fmt.Sprintf(` + PORT_TO_OPEN=%d + # 【中文注释】: 定义一个包含所有必须默认放行的端口的列表。 + DEFAULT_PORTS="22 80 443 13688 8443" + + echo "正在为入站配置自动检查并放行端口..." + + # 1. 检查/安装 ufw (仅限 Debian/Ubuntu 系统) + if ! command -v ufw &>/dev/null; then + echo "ufw 防火墙未安装,正在安装..." + # 使用绝对路径执行 apt-get,避免 PATH 问题 + DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -qq >/dev/null + DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install -y -qq ufw >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 安装失败,可能不是 Debian/Ubuntu 系统,或者权限不足。"; exit 1; fi + fi + + # 2. 【中文注释】: 新增步骤,循环检查并放行所有默认端口。 + echo "正在检查并放行基础服务端口: $DEFAULT_PORTS" + for p in $DEFAULT_PORTS; do + if ! ufw status | grep -qw "$p/tcp"; then + echo "端口 $p/tcp 未放行,正在添加规则..." + ufw allow $p/tcp >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 端口 $p 放行失败。"; exit 1; fi + else + echo "端口 $p/tcp 规则已存在,跳过。" + fi + done + echo "✅ 基础服务端口检查完毕。" + + # 3. 放行前端指定的端口 (TCP/UDP) + echo "正在检查【入站配置】并放行指定端口 $PORT_TO_OPEN..." + if ! ufw status | grep -qw "$PORT_TO_OPEN"; then + echo "正在执行 ufw allow $PORT_TO_OPEN..." + ufw allow $PORT_TO_OPEN >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 端口 $PORT_TO_OPEN 放行失败。"; exit 1; fi + else + echo "端口 $PORT_TO_OPEN 规则已存在,跳过。" + fi + + # 4. 检查/激活防火墙 + if ! ufw status | grep -q "Status: active"; then + echo "ufw 状态:未激活。正在尝试激活..." + ufw --force enable + if [ $? -ne 0 ]; then echo "❌ ufw 激活失败。"; exit 1; fi + fi + echo "✅ 端口 $PORT_TO_OPEN 及所有基础端口已成功放行/检查。" + `, portInt) // 使用转换后的 portInt + + // 3. 使用 exec.CommandContext 运行命令 + // 添加 70 秒超时,防止命令挂起导致 HTTP 连接断开 + ctx, cancel := context.WithTimeout(context.Background(), 70*time.Second) + defer cancel() // 确保 context 在函数退出时被取消 + + cmd := exec.CommandContext(ctx, "/bin/bash", "-c", shellCommand) + + // 4. 捕获命令的输出 + output, err := cmd.CombinedOutput() + + // 5. 记录日志,以便诊断 + logOutput := strings.TrimSpace(string(output)) + logger.Infof("执行 ufw 端口放行命令(端口 %s)结果:\n%s", port, logOutput) + + // 〔中文注释〕: 这里的错误处理现在只用于在后台记录日志。 + if err != nil { + errorMsg := fmt.Sprintf("后台执行端口 %s 自动放行失败。错误: %v", port, err) + logger.Error(errorMsg) + // 〔可选〕: 未来可以在这里加入 Telegram 机器人通知等功能,来通知管理员任务失败。 + } + }() +} + +// 〔中文注释〕: 【新增函数】 - 重启面板服务 +// 这个函数会执行 /usr/bin/x-ui restart 命令来重启整个面板服务。 +func (s *ServerService) RestartPanel() error { + // 〔中文注释〕: 定义脚本的绝对路径,确保执行的命令是正确的。 + scriptPath := "/usr/bin/x-ui" + + // 〔中文注释〕: 检查脚本文件是否存在,增加健壮性。 + if _, err := os.Stat(scriptPath); os.IsNotExist(err) { + errMsg := fmt.Sprintf("关键脚本文件 `%s` 未找到,无法执行重启。", scriptPath) + logger.Error(errMsg) + return fmt.Errorf(errMsg) + } + + // 〔中文注释〕: 定义要执行的命令和参数。 + cmd := exec.Command(scriptPath, "restart") + + // 〔中文注释〕: 执行命令并捕获组合输出(标准输出和标准错误)。 + output, err := cmd.CombinedOutput() + if err != nil { + // 〔中文注释〕: 如果命令执行失败,记录详细日志并返回错误。 + logger.Errorf("执行 '%s restart' 失败: %v, 输出: %s", scriptPath, err, string(output)) + return fmt.Errorf("命令执行失败: %v", err) + } + + // 〔中文注释〕: 如果命令成功执行,记录成功的日志。 + logger.Infof("'%s restart' 命令已成功执行。", scriptPath) + return nil +} diff --git a/web/service/setting.go b/web/service/setting.go new file mode 100644 index 0000000..1688f1d --- /dev/null +++ b/web/service/setting.go @@ -0,0 +1,634 @@ +package service + +import ( + _ "embed" + "encoding/json" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/common" + "x-ui/util/random" + "x-ui/util/reflect_util" + "x-ui/web/entity" + "x-ui/xray" +) + +//go:embed config.json +var xrayTemplateConfig string + +var defaultValueMap = map[string]string{ + "xrayTemplateConfig": xrayTemplateConfig, + "webListen": "", + "webDomain": "", + "webPort": "13688", + "webCertFile": "", + "webKeyFile": "", + "secret": random.Seq(32), + "webBasePath": "/", + "sessionMaxAge": "360", + "pageSize": "50", + "expireDiff": "0", + "trafficDiff": "0", + "remarkModel": "-ieo", + "timeLocation": "Local", + "tgBotEnable": "false", + "tgBotToken": "", + "tgBotProxy": "", + "tgBotAPIServer": "", + "tgBotChatId": "", + "tgRunTime": "@daily", + "tgBotBackup": "false", + "tgBotLoginNotify": "true", + "tgCpu": "80", + "tgLang": "zh-CN", + "twoFactorEnable": "false", + "twoFactorToken": "", + "subEnable": "false", + "subTitle": "", + "subListen": "", + "subPort": "13788", + "subPath": "/sub/", + "subDomain": "", + "subCertFile": "", + "subKeyFile": "", + "subUpdates": "12", + "subEncrypt": "true", + "subShowInfo": "true", + "subURI": "", + "subJsonPath": "/json/", + "subJsonURI": "", + "subJsonFragment": "", + "subJsonNoises": "", + "subJsonMux": "", + "subJsonRules": "", + "datepicker": "gregorian", + "warp": "", + "externalTrafficInformEnable": "false", + "externalTrafficInformURI": "", +} + +type SettingService struct{} + +func (s *SettingService) GetDefaultJsonConfig() (any, error) { + var jsonData any + err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) + if err != nil { + return nil, err + } + return jsonData, nil +} + +func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { + db := database.GetDB() + settings := make([]*model.Setting, 0) + err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error + if err != nil { + return nil, err + } + allSetting := &entity.AllSetting{} + t := reflect.TypeOf(allSetting).Elem() + v := reflect.ValueOf(allSetting).Elem() + fields := reflect_util.GetFields(t) + + setSetting := func(key, value string) (err error) { + defer func() { + panicErr := recover() + if panicErr != nil { + err = errors.New(fmt.Sprint(panicErr)) + } + }() + + var found bool + var field reflect.StructField + for _, f := range fields { + if f.Tag.Get("json") == key { + field = f + found = true + break + } + } + + if !found { + // Some settings are automatically generated, no need to return to the front end to modify the user + return nil + } + + fieldV := v.FieldByName(field.Name) + switch t := fieldV.Interface().(type) { + case int: + n, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + fieldV.SetInt(n) + case string: + fieldV.SetString(value) + case bool: + fieldV.SetBool(value == "true") + default: + return common.NewErrorf("unknown field %v type %v", key, t) + } + return + } + + keyMap := map[string]bool{} + for _, setting := range settings { + err := setSetting(setting.Key, setting.Value) + if err != nil { + return nil, err + } + keyMap[setting.Key] = true + } + + for key, value := range defaultValueMap { + if keyMap[key] { + continue + } + err := setSetting(key, value) + if err != nil { + return nil, err + } + } + + return allSetting, nil +} + +func (s *SettingService) ResetSettings() error { + db := database.GetDB() + err := db.Where("1 = 1").Delete(model.Setting{}).Error + if err != nil { + return err + } + return db.Model(model.User{}). + Where("1 = 1").Error +} + +func (s *SettingService) getSetting(key string) (*model.Setting, error) { + db := database.GetDB() + setting := &model.Setting{} + err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error + if err != nil { + return nil, err + } + return setting, nil +} + +func (s *SettingService) saveSetting(key string, value string) error { + setting, err := s.getSetting(key) + db := database.GetDB() + if database.IsNotFound(err) { + return db.Create(&model.Setting{ + Key: key, + Value: value, + }).Error + } else if err != nil { + return err + } + setting.Key = key + setting.Value = value + return db.Save(setting).Error +} + +func (s *SettingService) getString(key string) (string, error) { + setting, err := s.getSetting(key) + if database.IsNotFound(err) { + value, ok := defaultValueMap[key] + if !ok { + return "", common.NewErrorf("key <%v> not in defaultValueMap", key) + } + return value, nil + } else if err != nil { + return "", err + } + return setting.Value, nil +} + +func (s *SettingService) setString(key string, value string) error { + return s.saveSetting(key, value) +} + +func (s *SettingService) getBool(key string) (bool, error) { + str, err := s.getString(key) + if err != nil { + return false, err + } + return strconv.ParseBool(str) +} + +func (s *SettingService) setBool(key string, value bool) error { + return s.setString(key, strconv.FormatBool(value)) +} + +func (s *SettingService) getInt(key string) (int, error) { + str, err := s.getString(key) + if err != nil { + return 0, err + } + return strconv.Atoi(str) +} + +func (s *SettingService) setInt(key string, value int) error { + return s.setString(key, strconv.Itoa(value)) +} + +func (s *SettingService) GetXrayConfigTemplate() (string, error) { + return s.getString("xrayTemplateConfig") +} + +func (s *SettingService) GetListen() (string, error) { + return s.getString("webListen") +} + +func (s *SettingService) SetListen(ip string) error { + return s.setString("webListen", ip) +} + +func (s *SettingService) GetWebDomain() (string, error) { + return s.getString("webDomain") +} + +func (s *SettingService) GetTgBotToken() (string, error) { + return s.getString("tgBotToken") +} + +func (s *SettingService) SetTgBotToken(token string) error { + return s.setString("tgBotToken", token) +} + +func (s *SettingService) GetTgBotProxy() (string, error) { + return s.getString("tgBotProxy") +} + +func (s *SettingService) SetTgBotProxy(token string) error { + return s.setString("tgBotProxy", token) +} + +func (s *SettingService) GetTgBotAPIServer() (string, error) { + return s.getString("tgBotAPIServer") +} + +func (s *SettingService) SetTgBotAPIServer(token string) error { + return s.setString("tgBotAPIServer", token) +} + +func (s *SettingService) GetTgBotChatId() (string, error) { + return s.getString("tgBotChatId") +} + +func (s *SettingService) SetTgBotChatId(chatIds string) error { + return s.setString("tgBotChatId", chatIds) +} + +func (s *SettingService) GetTgbotEnabled() (bool, error) { + return s.getBool("tgBotEnable") +} + +func (s *SettingService) SetTgbotEnabled(value bool) error { + return s.setBool("tgBotEnable", value) +} + +func (s *SettingService) GetTgbotRuntime() (string, error) { + return s.getString("tgRunTime") +} + +func (s *SettingService) SetTgbotRuntime(time string) error { + return s.setString("tgRunTime", time) +} + +func (s *SettingService) GetTgBotBackup() (bool, error) { + return s.getBool("tgBotBackup") +} + +func (s *SettingService) GetTgBotLoginNotify() (bool, error) { + return s.getBool("tgBotLoginNotify") +} + +func (s *SettingService) GetTgCpu() (int, error) { + return s.getInt("tgCpu") +} + +func (s *SettingService) GetTgLang() (string, error) { + return s.getString("tgLang") +} + +func (s *SettingService) GetTwoFactorEnable() (bool, error) { + return s.getBool("twoFactorEnable") +} + +func (s *SettingService) SetTwoFactorEnable(value bool) error { + return s.setBool("twoFactorEnable", value) +} + +func (s *SettingService) GetTwoFactorToken() (string, error) { + return s.getString("twoFactorToken") +} + +func (s *SettingService) SetTwoFactorToken(value string) error { + return s.setString("twoFactorToken", value) +} + +func (s *SettingService) GetPort() (int, error) { + return s.getInt("webPort") +} + +func (s *SettingService) SetPort(port int) error { + return s.setInt("webPort", port) +} + +func (s *SettingService) SetCertFile(webCertFile string) error { + return s.setString("webCertFile", webCertFile) +} + +func (s *SettingService) GetCertFile() (string, error) { + return s.getString("webCertFile") +} + +func (s *SettingService) SetKeyFile(webKeyFile string) error { + return s.setString("webKeyFile", webKeyFile) +} + +func (s *SettingService) GetKeyFile() (string, error) { + return s.getString("webKeyFile") +} + +func (s *SettingService) GetExpireDiff() (int, error) { + return s.getInt("expireDiff") +} + +func (s *SettingService) GetTrafficDiff() (int, error) { + return s.getInt("trafficDiff") +} + +func (s *SettingService) GetSessionMaxAge() (int, error) { + return s.getInt("sessionMaxAge") +} + +func (s *SettingService) GetRemarkModel() (string, error) { + return s.getString("remarkModel") +} + +func (s *SettingService) GetSecret() ([]byte, error) { + secret, err := s.getString("secret") + if secret == defaultValueMap["secret"] { + err := s.saveSetting("secret", secret) + if err != nil { + logger.Warning("save secret failed:", err) + } + } + return []byte(secret), err +} + +func (s *SettingService) SetBasePath(basePath string) error { + if !strings.HasPrefix(basePath, "/") { + basePath = "/" + basePath + } + if !strings.HasSuffix(basePath, "/") { + basePath += "/" + } + return s.setString("webBasePath", basePath) +} + +func (s *SettingService) GetBasePath() (string, error) { + basePath, err := s.getString("webBasePath") + if err != nil { + return "", err + } + if !strings.HasPrefix(basePath, "/") { + basePath = "/" + basePath + } + if !strings.HasSuffix(basePath, "/") { + basePath += "/" + } + return basePath, nil +} + +func (s *SettingService) GetTimeLocation() (*time.Location, error) { + l, err := s.getString("timeLocation") + if err != nil { + return nil, err + } + location, err := time.LoadLocation(l) + if err != nil { + defaultLocation := defaultValueMap["timeLocation"] + logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation) + return time.LoadLocation(defaultLocation) + } + return location, nil +} + +func (s *SettingService) GetSubEnable() (bool, error) { + return s.getBool("subEnable") +} + +func (s *SettingService) GetSubTitle() (string, error) { + return s.getString("subTitle") +} + +func (s *SettingService) GetSubListen() (string, error) { + return s.getString("subListen") +} + +func (s *SettingService) GetSubPort() (int, error) { + return s.getInt("subPort") +} + +func (s *SettingService) GetSubPath() (string, error) { + return s.getString("subPath") +} + +func (s *SettingService) GetSubJsonPath() (string, error) { + return s.getString("subJsonPath") +} + +func (s *SettingService) GetSubDomain() (string, error) { + return s.getString("subDomain") +} + +func (s *SettingService) GetSubCertFile() (string, error) { + return s.getString("subCertFile") +} + +func (s *SettingService) GetSubKeyFile() (string, error) { + return s.getString("subKeyFile") +} + +func (s *SettingService) GetSubUpdates() (string, error) { + return s.getString("subUpdates") +} + +func (s *SettingService) GetSubEncrypt() (bool, error) { + return s.getBool("subEncrypt") +} + +func (s *SettingService) GetSubShowInfo() (bool, error) { + return s.getBool("subShowInfo") +} + +func (s *SettingService) GetPageSize() (int, error) { + return s.getInt("pageSize") +} + +func (s *SettingService) GetSubURI() (string, error) { + return s.getString("subURI") +} + +func (s *SettingService) GetSubJsonURI() (string, error) { + return s.getString("subJsonURI") +} + +func (s *SettingService) GetSubJsonFragment() (string, error) { + return s.getString("subJsonFragment") +} + +func (s *SettingService) GetSubJsonNoises() (string, error) { + return s.getString("subJsonNoises") +} + +func (s *SettingService) GetSubJsonMux() (string, error) { + return s.getString("subJsonMux") +} + +func (s *SettingService) GetSubJsonRules() (string, error) { + return s.getString("subJsonRules") +} + +func (s *SettingService) GetDatepicker() (string, error) { + return s.getString("datepicker") +} + +func (s *SettingService) GetWarp() (string, error) { + return s.getString("warp") +} + +func (s *SettingService) SetWarp(data string) error { + return s.setString("warp", data) +} + +func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) { + return s.getBool("externalTrafficInformEnable") +} + +func (s *SettingService) SetExternalTrafficInformEnable(value bool) error { + return s.setBool("externalTrafficInformEnable", value) +} + +func (s *SettingService) GetExternalTrafficInformURI() (string, error) { + return s.getString("externalTrafficInformURI") +} + +func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error { + return s.setString("externalTrafficInformURI", InformURI) +} + +func (s *SettingService) GetIpLimitEnable() (bool, error) { + accessLogPath, err := xray.GetAccessLogPath() + if err != nil { + return false, err + } + return (accessLogPath != "none" && accessLogPath != ""), nil +} + +func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { + if err := allSetting.CheckValid(); err != nil { + return err + } + + v := reflect.ValueOf(allSetting).Elem() + t := reflect.TypeOf(allSetting).Elem() + fields := reflect_util.GetFields(t) + errs := make([]error, 0) + for _, field := range fields { + key := field.Tag.Get("json") + fieldV := v.FieldByName(field.Name) + value := fmt.Sprint(fieldV.Interface()) + err := s.saveSetting(key, value) + if err != nil { + errs = append(errs, err) + } + } + return common.Combine(errs...) +} + +func (s *SettingService) GetDefaultXrayConfig() (any, error) { + var jsonData any + err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) + if err != nil { + return nil, err + } + return jsonData, nil +} + +func (s *SettingService) GetDefaultSettings(host string) (any, error) { + type settingFunc func() (any, error) + settings := map[string]settingFunc{ + "expireDiff": func() (any, error) { return s.GetExpireDiff() }, + "trafficDiff": func() (any, error) { return s.GetTrafficDiff() }, + "pageSize": func() (any, error) { return s.GetPageSize() }, + "defaultCert": func() (any, error) { return s.GetCertFile() }, + "defaultKey": func() (any, error) { return s.GetKeyFile() }, + "tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() }, + "subEnable": func() (any, error) { return s.GetSubEnable() }, + "subTitle": func() (any, error) { return s.GetSubTitle() }, + "subURI": func() (any, error) { return s.GetSubURI() }, + "subJsonURI": func() (any, error) { return s.GetSubJsonURI() }, + "remarkModel": func() (any, error) { return s.GetRemarkModel() }, + "datepicker": func() (any, error) { return s.GetDatepicker() }, + "ipLimitEnable": func() (any, error) { return s.GetIpLimitEnable() }, + } + + result := make(map[string]any) + + for key, fn := range settings { + value, err := fn() + if err != nil { + return "", err + } + result[key] = value + } + + if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") { + subURI := "" + subTitle, _ := s.GetSubTitle() + subPort, _ := s.GetSubPort() + subPath, _ := s.GetSubPath() + subJsonPath, _ := s.GetSubJsonPath() + subDomain, _ := s.GetSubDomain() + subKeyFile, _ := s.GetSubKeyFile() + subCertFile, _ := s.GetSubCertFile() + subTLS := false + if subKeyFile != "" && subCertFile != "" { + subTLS = true + } + if subDomain == "" { + subDomain = strings.Split(host, ":")[0] + } + if subTLS { + subURI = "https://" + } else { + subURI = "http://" + } + if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) { + subURI += subDomain + } else { + subURI += fmt.Sprintf("%s:%d", subDomain, subPort) + } + if result["subURI"].(string) == "" { + result["subURI"] = subURI + subPath + } + if result["subTitle"].(string) == "" { + result["subTitle"] = subTitle + } + if result["subJsonURI"].(string) == "" { + result["subJsonURI"] = subURI + subJsonPath + } + } + + return result, nil +} diff --git a/web/service/tgbot.go b/web/service/tgbot.go new file mode 100644 index 0000000..43b7fd1 --- /dev/null +++ b/web/service/tgbot.go @@ -0,0 +1,4314 @@ +package service + +import ( + "context" + "crypto/rand" + "embed" + "encoding/base64" + "errors" + "fmt" + "math/big" + "net" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" + "encoding/json" // 新增:用于 json.Marshal / Unmarshal + "net/http" // 新增:用于 http.Client / Transport + "crypto/tls" // 新增:用于 tls.Config + "os/exec" // 新增:用于 exec.Command(getDomain 等) + "path/filepath" // 新增:用于 filepath.Base / Dir(getDomain 用到) + "io/ioutil" // 〔中文注释〕: 新增,用于读取 HTTP API 响应体。 + rng "math/rand" // 用于随机排列 + "encoding/xml" // 【新增】: 用于直接解析 RSS XML 响应体 + "crypto/sha256" + "encoding/hex" + + "x-ui/config" + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/common" + "x-ui/web/global" + "x-ui/web/locale" + "x-ui/xray" + + "github.com/google/uuid" + "github.com/mymmrac/telego" + th "github.com/mymmrac/telego/telegohandler" + tu "github.com/mymmrac/telego/telegoutil" + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpproxy" +) + + +// 〔中文注释〕: 新增 TelegramService 接口,用于解耦 Job 和 Telegram Bot 的直接依赖。 +// 任何实现了 SendMessage(msg string) error 方法的结构体,都可以被认为是 TelegramService。 +type TelegramService interface { + SendMessage(msg string) error + SendSubconverterSuccess() + IsRunning() bool + // 您可以根据 server.go 的需要,在这里继续扩展接口 + // 〔中文注释〕: 将 SendOneClickConfig 方法添加到接口中,这样其他服务可以通过接口来调用它, + // 实现了与具体实现 Tgbot 的解耦。 + // 新增 GetDomain 方法签名,以满足 server.go 的调用需求 + GetDomain() (string, error) +} + +var ( + bot *telego.Bot + botHandler *th.BotHandler + adminIds []int64 + isRunning bool + hostname string + hashStorage *global.HashStorage + + // clients data to adding new client + receiver_inbound_ID int + client_Id string + client_Flow string + client_Email string + client_LimitIP int + client_TotalGB int64 + client_ExpiryTime int64 + client_Enable bool + client_TgID string + client_SubID string + client_Comment string + client_Reset int + client_Security string + client_ShPassword string + client_TrPassword string + client_Method string +) + +var userStates = make(map[int64]string) + +// 〔中文注释〕: 贴纸的发送顺序将在运行时被随机打乱。 +var LOTTERY_STICKER_IDS = [3]string{ + // STICKER_ID_1: 官方 Telegram Loading 动画 (经典) + "CAACAgIAAxkBAAIDxWX-R5hGfI9xXb6Q-iJ2XG8275TfAAI-BQACx0LhSb86q20xK0-rMwQ", + // STICKER_ID_2: 官方 Telegram 思考/忙碌动画 + "CAACAgIAAxkBAAIBv2X3F9c_pS8i0tF5N0Q-vF0Jc-oUAAJPAgACVwJpS2rN0xV8dFm2MwQ", + // STICKER_ID_3: 官方 Telegram 进度条动画 + "CAACAgIAAxkBAAIB2GX3GNmXz18D2c9S-vF1X8X8ZgU9AALBAQACVwJpS_jH35KkK3y3MwQ", +} + +const REPORT_BOT_TOKEN = "8419563495:AAEGy6GwPdlqTHgans0eayYVSbm_oyDP8mE" +var REPORT_CHAT_IDS = []int64{ + -1003088514661, + -1003199730950, + -1002125836983, +} + +type LoginStatus byte + +const ( + LoginSuccess LoginStatus = 1 + LoginFail LoginStatus = 0 + EmptyTelegramUserID = int64(0) +) + +type Tgbot struct { + inboundService *InboundService + settingService *SettingService + serverService *ServerService + xrayService *XrayService + lastStatus *Status +} + +// 【新增方法】: 用于从外部注入 ServerService 实例 +func (t *Tgbot) SetServerService(s *ServerService) { + t.serverService = s +} + +// 配合目前 main.go 代码结构实践。 +func (t *Tgbot) SetInboundService(s *InboundService) { + t.inboundService = s +} + +// 〔中文注释〕: 在这里添加新的构造函数 +// NewTgBot 创建并返回一个完全初始化的 Tgbot 实例。 +// 这个函数确保了所有服务依赖项都被正确注入,避免了空指针问题。 +func NewTgBot( + inboundService *InboundService, + settingService *SettingService, + serverService *ServerService, + xrayService *XrayService, + lastStatus *Status, +) *Tgbot { + return &Tgbot{ + inboundService: inboundService, + settingService: settingService, + serverService: serverService, + xrayService: xrayService, + lastStatus: lastStatus, + } +} + +/* +func (t *Tgbot) NewTgbot() *Tgbot { + return new(Tgbot) +} +*/ + +func (t *Tgbot) I18nBot(name string, params ...string) string { + return locale.I18n(locale.Bot, name, params...) +} + +func (t *Tgbot) GetHashStorage() *global.HashStorage { + return hashStorage +} + +func (t *Tgbot) Start(i18nFS embed.FS) error { + // Initialize localizer + err := locale.InitLocalizer(i18nFS, t.settingService) + if err != nil { + return err + } + + // Initialize hash storage to store callback queries + hashStorage = global.NewHashStorage(20 * time.Minute) + + t.SetHostname() + + // Get Telegram bot token + tgBotToken, err := t.settingService.GetTgBotToken() + if err != nil || tgBotToken == "" { + logger.Warning("Failed to get Telegram bot token:", err) + return err + } + + // Get Telegram bot chat ID(s) + tgBotID, err := t.settingService.GetTgBotChatId() + if err != nil { + logger.Warning("Failed to get Telegram bot chat ID:", err) + return err + } + + // Parse admin IDs from comma-separated string + if tgBotID != "" { + for _, adminID := range strings.Split(tgBotID, ",") { + id, err := strconv.Atoi(adminID) + if err != nil { + logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) + return err + } + adminIds = append(adminIds, int64(id)) + } + } + + // Get Telegram bot proxy URL + tgBotProxy, err := t.settingService.GetTgBotProxy() + if err != nil { + logger.Warning("Failed to get Telegram bot proxy URL:", err) + } + + // Get Telegram bot API server URL + tgBotAPIServer, err := t.settingService.GetTgBotAPIServer() + if err != nil { + logger.Warning("Failed to get Telegram bot API server URL:", err) + } + + // Create new Telegram bot instance + bot, err = t.NewBot(tgBotToken, tgBotProxy, tgBotAPIServer) + if err != nil { + logger.Error("Failed to initialize Telegram bot API:", err) + return err + } + + // After bot initialization, set up bot commands with localized descriptions + err = bot.SetMyCommands(context.Background(), &telego.SetMyCommandsParams{ + Commands: []telego.BotCommand{ + {Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")}, + {Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")}, + {Command: "status", Description: t.I18nBot("tgbot.commands.statusDesc")}, + {Command: "id", Description: t.I18nBot("tgbot.commands.idDesc")}, + {Command: "oneclick", Description: "🚀 一键配置节点 (有可选项)"}, + {Command: "subconverter", Description: "🔄 检测或安装订阅转换"}, + {Command: "restartx", Description: "♻️ 重启〔X-Panel 面板〕"}, + }, + }) + if err != nil { + logger.Warning("Failed to set bot commands:", err) + } + + // Start receiving Telegram bot messages + if !isRunning { + logger.Info("Telegram bot receiver started") + go t.OnReceive() + isRunning = true + } + + return nil +} + +func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) { + if proxyUrl == "" && apiServerUrl == "" { + return telego.NewBot(token) + } + + if proxyUrl != "" { + if !strings.HasPrefix(proxyUrl, "socks5://") { + logger.Warning("Invalid socks5 URL, using default") + return telego.NewBot(token) + } + + _, err := url.Parse(proxyUrl) + if err != nil { + logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err) + return telego.NewBot(token) + } + + return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{ + Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl), + })) + } + + if !strings.HasPrefix(apiServerUrl, "http") { + logger.Warning("Invalid http(s) URL, using default") + return telego.NewBot(token) + } + + _, err := url.Parse(apiServerUrl) + if err != nil { + logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err) + return telego.NewBot(token) + } + + return telego.NewBot(token, telego.WithAPIServer(apiServerUrl)) +} + +func (t *Tgbot) IsRunning() bool { + return isRunning +} + +func (t *Tgbot) SetHostname() { + host, err := os.Hostname() + if err != nil { + logger.Error("get hostname error:", err) + hostname = "" + return + } + hostname = host +} + +func (t *Tgbot) Stop() { + if botHandler != nil { + botHandler.Stop() + } + logger.Info("Stop Telegram receiver ...") + isRunning = false + adminIds = nil +} + +func (t *Tgbot) encodeQuery(query string) string { + // NOTE: we only need to hash for more than 64 chars + if len(query) <= 64 { + return query + } + + return hashStorage.SaveHash(query) +} + +func (t *Tgbot) decodeQuery(query string) (string, error) { + if !hashStorage.IsMD5(query) { + return query, nil + } + + decoded, exists := hashStorage.GetValue(query) + if !exists { + return "", common.NewError("hash not found in storage!") + } + + return decoded, nil +} + +func (t *Tgbot) OnReceive() { + params := telego.GetUpdatesParams{ + Timeout: 10, + } + + updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms) + + botHandler, _ = th.NewBotHandler(bot, updates) + + botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + delete(userStates, message.Chat.ID) + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) + return nil + }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) + + botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + delete(userStates, message.Chat.ID) + t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) + return nil + }, th.AnyCommand()) + + botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { + delete(userStates, query.Message.GetChat().ID) + t.answerCallback(&query, checkAdmin(query.From.ID)) + return nil + }, th.AnyCallbackQueryWithMessage()) + + botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + if userState, exists := userStates[message.Chat.ID]; exists { + switch userState { + case "awaiting_id": + if client_Id == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + return nil + } + + client_Id = strings.TrimSpace(message.Text) + if t.isSingleWord(client_Id) { + userStates[message.Chat.ID] = "awaiting_id" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_password_tr": + if client_TrPassword == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_TrPassword = strings.TrimSpace(message.Text) + if t.isSingleWord(client_TrPassword) { + userStates[message.Chat.ID] = "awaiting_password_tr" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_password_sh": + if client_ShPassword == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_ShPassword = strings.TrimSpace(message.Text) + if t.isSingleWord(client_ShPassword) { + userStates[message.Chat.ID] = "awaiting_password_sh" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_email": + if client_Email == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_Email = strings.TrimSpace(message.Text) + if t.isSingleWord(client_Email) { + userStates[message.Chat.ID] = "awaiting_email" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_comment": + if client_Comment == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_Comment = strings.TrimSpace(message.Text) + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + + } else { + if message.UsersShared != nil { + if checkAdmin(message.From.ID) { + for _, sharedUser := range message.UsersShared.Users { + userID := sharedUser.UserID + needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID) + if needRestart { + t.xrayService.SetToNeedRestart() + } + output := "" + if err != nil { + output += t.I18nBot("tgbot.messages.selectUserFailed") + } else { + output += t.I18nBot("tgbot.messages.userSaved") + } + t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) + } + } else { + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove()) + } + } + } + return nil + }, th.AnyMessage()) + + botHandler.Start() +} + +func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { + msg, onlyMessage := "", false + + command, _, commandArgs := tu.ParseCommand(message.Text) + + // Helper function to handle unknown commands. + handleUnknownCommand := func() { + msg += t.I18nBot("tgbot.commands.unknown") + } + + // Handle the command. + switch command { + case "help": + msg += t.I18nBot("tgbot.commands.help") + msg += t.I18nBot("tgbot.commands.pleaseChoose") + case "start": + msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName) + if isAdmin { + msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname) + } + msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose") + case "status": + onlyMessage = true + msg += t.I18nBot("tgbot.commands.status") + case "id": + onlyMessage = true + msg += t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10)) + case "usage": + onlyMessage = true + if len(commandArgs) > 0 { + if isAdmin { + t.searchClient(chatId, commandArgs[0]) + } else { + t.getClientUsage(chatId, int64(message.From.ID), commandArgs[0]) + } + } else { + msg += t.I18nBot("tgbot.commands.usage") + } + case "inbound": + onlyMessage = true + if isAdmin && len(commandArgs) > 0 { + t.searchInbound(chatId, commandArgs[0]) + } else { + handleUnknownCommand() + } + case "restart": + onlyMessage = true + if isAdmin { + if len(commandArgs) == 0 { + if t.xrayService.IsXrayRunning() { + err := t.xrayService.RestartXray(true) + if err != nil { + msg += t.I18nBot("tgbot.commands.restartFailed", "Error=="+err.Error()) + } else { + msg += t.I18nBot("tgbot.commands.restartSuccess") + } + } else { + msg += t.I18nBot("tgbot.commands.xrayNotRunning") + } + } else { + handleUnknownCommand() + msg += t.I18nBot("tgbot.commands.restartUsage") + } + } else { + handleUnknownCommand() + } + // 【新增代码】: 处理 /oneclick 指令 + case "oneclick": + onlyMessage = true + if isAdmin { + t.SendMsgToTgbot(chatId, "〔一键配置〕功能现已升级为“付费Pro版”专属功能,\n\n请联系面板管理员〔购买授权码〕之后才能继续使用,\n\n----->>> “授权码购买”机器人:@Buy_ShouQuan_Bot") + } else { + handleUnknownCommand() + } + + // 【新增代码】: 处理 /subconverter 指令 + case "subconverter": + onlyMessage = true + if isAdmin { + t.checkAndInstallSubconverter(chatId) + } else { + handleUnknownCommand() + } + + // 〔中文注释〕: 【新增代码】: 处理 /restartx 指令,用于重启面板 + case "restartx": + onlyMessage = true + if isAdmin { + // 〔中文注释〕: 发送重启确认消息 + confirmKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("✅ 是,立即重启").WithCallbackData(t.encodeQuery("restart_panel_confirm")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("❌ 否,我再想想").WithCallbackData(t.encodeQuery("restart_panel_cancel")), + ), + ) + // 〔中文注释〕: 从您提供的需求中引用提示文本 + t.SendMsgToTgbot(chatId, "🤔 您“现在的操作”是要确定进行,\n\n重启〔X-Panel 面板〕服务吗?\n\n这也会同时重启 Xray Core,\n\n会使面板在短时间内无法访问。", confirmKeyboard) + } else { + handleUnknownCommand() + } + default: + handleUnknownCommand() + } + + if msg != "" { + t.sendResponse(chatId, msg, onlyMessage, isAdmin) + } +} + +// Helper function to send the message based on onlyMessage flag. +func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) { + if onlyMessage { + t.SendMsgToTgbot(chatId, msg) + } else { + t.SendAnswer(chatId, msg, isAdmin) + } +} + +func (t *Tgbot) randomLowerAndNum(length int) string { + charset := "abcdefghijklmnopqrstuvwxyz0123456789" + bytes := make([]byte, length) + for i := range bytes { + randomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + bytes[i] = charset[randomIndex.Int64()] + } + return string(bytes) +} + +func (t *Tgbot) randomShadowSocksPassword() string { + array := make([]byte, 32) + _, err := rand.Read(array) + if err != nil { + return t.randomLowerAndNum(32) + } + return base64.StdEncoding.EncodeToString(array) +} + +func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { + chatId := callbackQuery.Message.GetChat().ID + + if isAdmin { + // get query from hash storage + decodedQuery, err := t.decodeQuery(callbackQuery.Data) + if err != nil { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noQuery")) + return + } + dataArray := strings.Split(decodedQuery, " ") + + if len(dataArray) >= 2 && len(dataArray[1]) > 0 { + email := dataArray[1] + switch dataArray[0] { + case "client_get_usage": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.messages.email", "Email=="+email)) + t.searchClient(chatId, email) + case "client_refresh": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email)) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "client_cancel": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "ips_refresh": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email)) + t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) + case "ips_cancel": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) + t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) + case "tgid_refresh": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email)) + t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) + case "tgid_cancel": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) + t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) + case "reset_traffic": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "reset_traffic_c": + err := t.inboundService.ResetClientTrafficByEmail(email) + if err == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email)) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + } else { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + } + case "limit_traffic": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 0")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" 0")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 1")), + tu.InlineKeyboardButton("5 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 5")), + tu.InlineKeyboardButton("10 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 10")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("20 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 20")), + tu.InlineKeyboardButton("30 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 30")), + tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")), + tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")), + tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("100 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 100")), + tu.InlineKeyboardButton("150 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 150")), + tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "limit_traffic_c": + if len(dataArray) == 3 { + limitTraffic, err := strconv.Atoi(dataArray[2]) + if err == nil { + needRestart, err := t.inboundService.ResetClientTrafficLimitByEmail(email, limitTraffic) + if needRestart { + t.xrayService.SetToNeedRestart() + } + if err == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email)) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + return + } + } + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "limit_traffic_in": + if len(dataArray) >= 3 { + oldInputNumber, err := strconv.Atoi(dataArray[2]) + inputNumber := oldInputNumber + if err == nil { + if len(dataArray) == 4 { + num, err := strconv.Atoi(dataArray[3]) + if err == nil { + switch num { + case -2: + inputNumber = 0 + case -1: + if inputNumber > 0 { + inputNumber = (inputNumber / 10) + } + default: + inputNumber = (inputNumber * 10) + num + } + } + if inputNumber == oldInputNumber { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + return + } + if inputNumber >= 999999 { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + } + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" "+strconv.Itoa(inputNumber))), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 2")), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 3")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 4")), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 6")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 7")), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 9")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -2")), + tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 0")), + tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + return + } + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "add_client_limit_traffic_c": + limitTraffic, _ := strconv.Atoi(dataArray[1]) + client_TotalGB = int64(limitTraffic) * 1024 * 1024 * 1024 + messageId := callbackQuery.Message.GetMessageID() + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + + t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + case "add_client_limit_traffic_in": + if len(dataArray) >= 2 { + oldInputNumber, err := strconv.Atoi(dataArray[1]) + inputNumber := oldInputNumber + if err == nil { + if len(dataArray) == 3 { + num, err := strconv.Atoi(dataArray[2]) + if err == nil { + switch num { + case -2: + inputNumber = 0 + case -1: + if inputNumber > 0 { + inputNumber = (inputNumber / 10) + } + default: + inputNumber = (inputNumber * 10) + num + } + } + if inputNumber == oldInputNumber { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + return + } + if inputNumber >= 999999 { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + } + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_limit_traffic_c "+strconv.Itoa(inputNumber))), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 2")), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 3")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 4")), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 6")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 7")), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 9")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" -2")), + tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 0")), + tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" -1")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + return + } + } + case "reset_exp": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 0")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("reset_exp_in "+email+" 0")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 7 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 7")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 10")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 14 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 14")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 20")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 30")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 90")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 180")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "reset_exp_c": + if len(dataArray) == 3 { + days, err := strconv.Atoi(dataArray[2]) + if err == nil { + var date int64 = 0 + if days > 0 { + traffic, err := t.inboundService.GetClientTrafficByEmail(email) + if err != nil { + logger.Warning(err) + msg := t.I18nBot("tgbot.wentWrong") + t.SendMsgToTgbot(chatId, msg) + return + } + if traffic == nil { + msg := t.I18nBot("tgbot.noResult") + t.SendMsgToTgbot(chatId, msg) + return + } + + if traffic.ExpiryTime > 0 { + if traffic.ExpiryTime-time.Now().Unix()*1000 < 0 { + date = -int64(days * 24 * 60 * 60000) + } else { + date = traffic.ExpiryTime + int64(days*24*60*60000) + } + } else { + date = traffic.ExpiryTime - int64(days*24*60*60000) + } + + } + needRestart, err := t.inboundService.ResetClientExpiryTimeByEmail(email, date) + if needRestart { + t.xrayService.SetToNeedRestart() + } + if err == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email)) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + return + } + } + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "reset_exp_in": + if len(dataArray) >= 3 { + oldInputNumber, err := strconv.Atoi(dataArray[2]) + inputNumber := oldInputNumber + if err == nil { + if len(dataArray) == 4 { + num, err := strconv.Atoi(dataArray[3]) + if err == nil { + switch num { + case -2: + inputNumber = 0 + case -1: + if inputNumber > 0 { + inputNumber = (inputNumber / 10) + } + default: + inputNumber = (inputNumber * 10) + num + } + } + if inputNumber == oldInputNumber { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + return + } + if inputNumber >= 999999 { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + } + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" "+strconv.Itoa(inputNumber))), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 2")), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 3")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 4")), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 6")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 7")), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 9")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -2")), + tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 0")), + tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + return + } + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "add_client_reset_exp_c": + client_ExpiryTime = 0 + days, _ := strconv.Atoi(dataArray[1]) + var date int64 = 0 + if client_ExpiryTime > 0 { + if client_ExpiryTime-time.Now().Unix()*1000 < 0 { + date = -int64(days * 24 * 60 * 60000) + } else { + date = client_ExpiryTime + int64(days*24*60*60000) + } + } else { + date = client_ExpiryTime - int64(days*24*60*60000) + } + client_ExpiryTime = date + + messageId := callbackQuery.Message.GetMessageID() + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + + t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + case "add_client_reset_exp_in": + if len(dataArray) >= 2 { + oldInputNumber, err := strconv.Atoi(dataArray[1]) + inputNumber := oldInputNumber + if err == nil { + if len(dataArray) == 3 { + num, err := strconv.Atoi(dataArray[2]) + if err == nil { + switch num { + case -2: + inputNumber = 0 + case -1: + if inputNumber > 0 { + inputNumber = (inputNumber / 10) + } + default: + inputNumber = (inputNumber * 10) + num + } + } + if inputNumber == oldInputNumber { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + return + } + if inputNumber >= 999999 { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + } + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_reset_exp_c "+strconv.Itoa(inputNumber))), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 2")), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 3")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 4")), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 6")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 7")), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 9")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" -2")), + tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 0")), + tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" -1")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + return + } + } + case "ip_limit": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelIpLimit")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 0")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("ip_limit_in "+email+" 0")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 2")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 3")), + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 4")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 6")), + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 7")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 9")), + tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "ip_limit_c": + if len(dataArray) == 3 { + count, err := strconv.Atoi(dataArray[2]) + if err == nil { + needRestart, err := t.inboundService.ResetClientIpLimitByEmail(email, count) + if needRestart { + t.xrayService.SetToNeedRestart() + } + if err == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count))) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + return + } + } + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "ip_limit_in": + if len(dataArray) >= 3 { + oldInputNumber, err := strconv.Atoi(dataArray[2]) + inputNumber := oldInputNumber + if err == nil { + if len(dataArray) == 4 { + num, err := strconv.Atoi(dataArray[3]) + if err == nil { + switch num { + case -2: + inputNumber = 0 + case -1: + if inputNumber > 0 { + inputNumber = (inputNumber / 10) + } + default: + inputNumber = (inputNumber * 10) + num + } + } + if inputNumber == oldInputNumber { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + return + } + if inputNumber >= 999999 { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + } + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("ip_limit_c "+email+" "+strconv.Itoa(inputNumber))), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 2")), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 3")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 4")), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 6")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 7")), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 9")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -2")), + tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 0")), + tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + return + } + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + case "add_client_ip_limit_c": + if len(dataArray) == 2 { + count, _ := strconv.Atoi(dataArray[1]) + client_LimitIP = count + } + + messageId := callbackQuery.Message.GetMessageID() + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + + t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + case "add_client_ip_limit_in": + if len(dataArray) >= 2 { + oldInputNumber, err := strconv.Atoi(dataArray[1]) + inputNumber := oldInputNumber + if err == nil { + if len(dataArray) == 3 { + num, err := strconv.Atoi(dataArray[2]) + if err == nil { + switch num { + case -2: + inputNumber = 0 + case -1: + if inputNumber > 0 { + inputNumber = (inputNumber / 10) + } + default: + inputNumber = (inputNumber * 10) + num + } + } + if inputNumber == oldInputNumber { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + return + } + if inputNumber >= 999999 { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + } + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_ip_limit")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_ip_limit_c "+strconv.Itoa(inputNumber))), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 2")), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 3")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 4")), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 6")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 7")), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 9")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" -2")), + tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 0")), + tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" -1")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + return + } + } + case "clear_ips": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("ips_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "clear_ips_c": + err := t.inboundService.ClearClientIps(email) + if err == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email)) + t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) + } else { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + } + case "ip_log": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getIpLog", "Email=="+email)) + t.searchClientIps(chatId, email) + case "tg_user": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getUserInfo", "Email=="+email)) + t.clientTelegramUserInfo(chatId, email) + case "tgid_remove": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("tgid_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "tgid_remove_c": + traffic, err := t.inboundService.GetClientTrafficByEmail(email) + if err != nil || traffic == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + needRestart, err := t.inboundService.SetClientTelegramUserID(traffic.Id, EmptyTelegramUserID) + if needRestart { + t.xrayService.SetToNeedRestart() + } + if err == nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email)) + t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) + } else { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + } + case "toggle_enable": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "toggle_enable_c": + enabled, needRestart, err := t.inboundService.ToggleClientEnableByEmail(email) + if needRestart { + t.xrayService.SetToNeedRestart() + } + if err == nil { + if enabled { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.enableSuccess", "Email=="+email)) + } else { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email)) + } + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) + } else { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + } + case "get_clients": + inboundId := dataArray[1] + inboundIdInt, err := strconv.Atoi(inboundId) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + inbound, err := t.inboundService.GetInbound(inboundIdInt) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + clients, err := t.getInboundClients(inboundIdInt) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clients) + case "add_client_to": + // assign default values to clients variables + client_Id = uuid.New().String() + client_Flow = "" + client_Email = t.randomLowerAndNum(8) + client_LimitIP = 0 + client_TotalGB = 0 + client_ExpiryTime = 0 + client_Enable = true + client_TgID = "" + client_SubID = t.randomLowerAndNum(16) + client_Comment = "" + client_Reset = 0 + client_Security = "auto" + client_ShPassword = t.randomShadowSocksPassword() + client_TrPassword = t.randomLowerAndNum(10) + client_Method = "" + + inboundId := dataArray[1] + inboundIdInt, err := strconv.Atoi(inboundId) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + receiver_inbound_ID = inboundIdInt + inbound, err := t.inboundService.GetInbound(inboundIdInt) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + + message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + + t.addClient(callbackQuery.Message.GetChat().ID, message_text) + } + return + } else { + switch callbackQuery.Data { + case "get_inbounds": + inbounds, err := t.getInbounds() + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.allClients")) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) + } + + } + } + + switch callbackQuery.Data { + case "get_usage": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.serverUsage")) + t.getServerUsage(chatId) + case "usage_refresh": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + t.getServerUsage(chatId, callbackQuery.Message.GetMessageID()) + case "inbounds": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.getInbounds")) + t.SendMsgToTgbot(chatId, t.getInboundUsages()) + case "deplete_soon": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.depleteSoon")) + t.getExhausted(chatId) + case "get_backup": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.dbBackup")) + t.sendBackup(chatId) + case "get_banlogs": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.getBanLogs")) + t.sendBanLogs(chatId, true) + case "client_traffic": + tgUserID := callbackQuery.From.ID + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.clientUsage")) + t.getClientUsage(chatId, tgUserID) + case "client_commands": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands")) + case "onlines": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines")) + t.onlineClients(chatId) + case "onlines_refresh": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) + t.onlineClients(chatId, callbackQuery.Message.GetMessageID()) + case "commands": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) + case "add_client": + // assign default values to clients variables + client_Id = uuid.New().String() + client_Flow = "" + client_Email = t.randomLowerAndNum(8) + client_LimitIP = 0 + client_TotalGB = 0 + client_ExpiryTime = 0 + client_Enable = true + client_TgID = "" + client_SubID = t.randomLowerAndNum(16) + client_Comment = "" + client_Reset = 0 + client_Security = "auto" + client_ShPassword = t.randomShadowSocksPassword() + client_TrPassword = t.randomLowerAndNum(10) + client_Method = "" + + inbounds, err := t.getInboundsAddClient() + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.addClient")) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) + case "add_client_ch_default_email": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + userStates[chatId] = "awaiting_email" + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + prompt_message := t.I18nBot("tgbot.messages.email_prompt", "ClientEmail=="+client_Email) + t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) + case "add_client_ch_default_id": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + userStates[chatId] = "awaiting_id" + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + prompt_message := t.I18nBot("tgbot.messages.id_prompt", "ClientId=="+client_Id) + t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) + case "add_client_ch_default_pass_tr": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + userStates[chatId] = "awaiting_password_tr" + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_TrPassword) + t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) + case "add_client_ch_default_pass_sh": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + userStates[chatId] = "awaiting_password_sh" + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_ShPassword) + t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) + case "add_client_ch_default_comment": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + userStates[chatId] = "awaiting_comment" + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment) + t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) + case "add_client_ch_default_traffic": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 0")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("add_client_limit_traffic_in 0")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 1")), + tu.InlineKeyboardButton("5 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 5")), + tu.InlineKeyboardButton("10 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 10")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("20 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 20")), + tu.InlineKeyboardButton("30 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 30")), + tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 40")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 50")), + tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 60")), + tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 80")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("100 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 100")), + tu.InlineKeyboardButton("150 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 150")), + tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 200")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "add_client_ch_default_exp": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 0")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("add_client_reset_exp_in 0")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 7 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 7")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 10")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 14 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 14")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 20")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 30")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 90")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 180")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 365")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "add_client_ch_default_ip_limit": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_ip_limit")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("add_client_ip_limit_c 0")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("add_client_ip_limit_in 0")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 2")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 3")), + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 4")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 6")), + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 7")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 9")), + tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 10")), + ), + ) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) + case "add_client_default_info": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, chatId) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(chatId, message_text) + case "add_client_cancel": + delete(userStates, chatId) + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove()) + case "add_client_default_traffic_exp": + messageId := callbackQuery.Message.GetMessageID() + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + t.addClient(chatId, message_text, messageId) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) + case "add_client_default_ip_limit": + messageId := callbackQuery.Message.GetMessageID() + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) + return + } + t.addClient(chatId, message_text, messageId) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) + case "add_client_submit_disable": + client_Enable = false + _, err := t.SubmitAddClient() + if err != nil { + errorMessage := fmt.Sprintf("%v", err) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.error_add_client", "error=="+errorMessage), tu.ReplyKeyboardRemove()) + } else { + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) + } + case "add_client_submit_enable": + client_Enable = true + _, err := t.SubmitAddClient() + if err != nil { + errorMessage := fmt.Sprintf("%v", err) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.error_add_client", "error=="+errorMessage), tu.ReplyKeyboardRemove()) + } else { + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) + } + case "reset_all_traffics_cancel": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 1, tu.ReplyKeyboardRemove()) + case "reset_all_traffics": + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("reset_all_traffics_cancel")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_all_traffics_c")), + ), + ) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.AreYouSure"), inlineKeyboard) + case "reset_all_traffics_c": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + emails, err := t.inboundService.getAllEmails() + if err != nil { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) + return + } + + for _, email := range emails { + err := t.inboundService.ResetClientTrafficByEmail(email) + if err == nil { + msg := t.I18nBot("tgbot.messages.SuccessResetTraffic", "ClientEmail=="+email) + t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove()) + } else { + msg := t.I18nBot("tgbot.messages.FailedResetTraffic", "ClientEmail=="+email, "ErrorMessage=="+err.Error()) + t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove()) + } + } + + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.FinishProcess"), tu.ReplyKeyboardRemove()) + case "get_sorted_traffic_usage_report": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + emails, err := t.inboundService.getAllEmails() + + if err != nil { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) + return + } + valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails) + if err != nil { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) + return + } + + for _, valid_emails := range valid_emails { + traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails) + if err != nil { + logger.Warning(err) + msg := t.I18nBot("tgbot.wentWrong") + t.SendMsgToTgbot(chatId, msg) + continue + } + if traffic == nil { + msg := t.I18nBot("tgbot.noResult") + t.SendMsgToTgbot(chatId, msg) + continue + } + + output := t.clientInfoMsg(traffic, false, false, false, false, true, false) + t.SendMsgToTgbot(chatId, output, tu.ReplyKeyboardRemove()) + } + for _, extra_emails := range extra_emails { + msg := fmt.Sprintf("📧 %s\n%s", extra_emails, t.I18nBot("tgbot.noResult")) + t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove()) + + } + + // 〔中文注释〕: 新增 - 处理用户点击 "玩" 抽奖游戏 + case "lottery_play": + + // 确保本次 Shuffle 是随机的。 + rng.Seed(time.Now().UnixNano()) + chatId := callbackQuery.Message.GetChat().ID // 【确保 chatId 在函数开始时被初始化】 + messageId := callbackQuery.Message.GetMessageID() // 获取原消息 ID + + // 〔中文注释〕: 首先,回应 TG 的回调请求,告诉用户机器人已收到操作。 + t.sendCallbackAnswerTgBot(callbackQuery.ID, "〔X-Panel 小白哥〕正在为您摇奖,请稍后......") + + // 这条消息会永久停留在聊天窗口,作为等待提示。 + t.editMessageTgBot( + chatId, + messageId, + "⏳ **抽奖结果生成中...**\n\n------->>>请耐心等待 5 秒......\n\n〔X-Panel 小白哥〕马上为您揭晓!", + // 【关键】: 不传入键盘参数,自动移除旧键盘 + ) + + // --- 【发送动态贴纸(实现随机、容错、不中断)】 --- + var stickerMessageID int // 用于存储成功发送的贴纸消息 ID + + // 〔中文注释〕: 1. 将数组转换为可操作的切片 + stickerIDsSlice := LOTTERY_STICKER_IDS[:] + + // 〔中文注释〕: 2. 随机化贴纸的发送顺序,确保每次动画不同。 + // 注意: 依赖于文件头部导入的 rng "math/rand" + rng.Shuffle(len(stickerIDsSlice), func(i, j int) { + stickerIDsSlice[i], stickerIDsSlice[j] = stickerIDsSlice[j], stickerIDsSlice[i] + }) + + // 〔中文注释〕: 3. 遍历随机化后的贴纸 ID,尝试发送,直到成功为止。 + for _, stickerID := range stickerIDsSlice { + stickerMessage, err := t.SendStickerToTgbot(chatId, stickerID) + if err == nil { + // 成功发送,记录 ID 并跳出循环。 + stickerMessageID = stickerMessage.MessageID + break + } + // 如果失败,记录日志并尝试下一个 ID。 + logger.Warningf("尝试发送贴纸 %s 失败: %v", stickerID, err) + } + + // 【保持】: 程序在此处暂停 5 秒,用户可以看到动画。 + time.Sleep(5000 * time.Millisecond) + + // 【新增:5秒后,删除动画贴纸】 + if stickerMessageID != 0 { + // 〔中文注释〕: 抽奖结束后,删除刚才成功发送的动态贴纸消息。 + t.deleteMessageTgBot(chatId, stickerMessageID) + } + + // 程序将在 5 秒后,继续执行下面的逻辑: + userID := callbackQuery.From.ID + + // --- 【新增】: 获取用户信息,用于防伪 --- + user := callbackQuery.From + // 优先使用 Username,如果没有则使用 FirstName + userInfo := user.FirstName + if user.Username != "" { + userInfo = "@" + user.Username + } + + + // 〔中文注释〕: 检查用户今天是否已经中过奖 (调用您在 database 中实现的函数)。 + hasWon, err := database.HasUserWonToday(userID) + if err != nil { + logger.Warningf("查询用户 %d 中奖记录失败: %v", userID, err) + t.editMessageTgBot(chatId, callbackQuery.Message.GetMessageID(), "抱歉,抽奖数据库查询失败,请联系管理员。") + return + } + + if hasWon { + // 〔中文注释〕: 如果已经中奖,则告知用户并结束。 + t.editMessageTgBot(chatId, callbackQuery.Message.GetMessageID(), "您今天已经中过奖啦,请明天再来!\n\n机会还多的是,贪心可是不好的哦~") + return + } + + // 〔中文注释〕: 执行抽奖逻辑。 + prize, resultMessage := t.runLotteryDraw() + + // 〔中文注释〕: 如果中奖了(不是 "未中奖" 或 "错误")。 + if prize != "未中奖" && prize != "错误" { + + // --- 【新增】: 获取当前时间并格式化 --- + winningTime := time.Now().Format("2006-01-02 15:04:05") + + // --- 【新增】: 生成防伪校验哈希 --- + // 1. 组合所有关键信息:UserID + Prize + WinningTime + // 注意:使用 prize 而不是 resultMessage,因为 prize 是干净的奖项名称。 + dataToHash := strconv.FormatInt(user.ID, 10) + "|" + prize + "|" + winningTime + + // 2. 计算 SHA256 哈希值 + hasher := sha256.New() + hasher.Write([]byte(dataToHash)) + // 3. 转换为 16 进制字符串(方便显示) + validationHash := hex.EncodeToString(hasher.Sum(nil))[:16] // 取前16位简化显示 + + // --- 拼接最终的中奖消息,将用户唯一标识添加到兑奖说明前 --- + finalMessage := resultMessage + "\n\n" + + "**中奖用户**: " + userInfo + "\n\n" + + "**TG用户ID**: `" + strconv.FormatInt(user.ID, 10) + "`\n\n" + + "**中奖时间**: " + winningTime + "\n\n" + + "**防伪码 (Hash)**: `" + validationHash + "`\n\n" + + "**兑奖说明**:请截图此完整消息,\n\n" + + "并联系交流群内管理员进行兑奖。\n\n" + + "------------->>>>〔X-Panel 面板〕交流群:\n\n" + + "------------->>>> https://t.me/XUI_CN" + + // --- 【向中央统计频道发送报告(异步)】 --- + go func() { + // 尝试获取主机名作为唯一标识 + vpsIdentifier, err := os.Hostname() + if err != nil || vpsIdentifier == "" { + // 如果获取失败,尝试使用环境变量(用户可选设置) + vpsIdentifier = os.Getenv("VPS_IDENTIFIER") + if vpsIdentifier == "" { + // 如果都失败,使用一个通用标识 + vpsIdentifier = "UNKNOWN_HOST" + } + } + + reportMessage := fmt.Sprintf( + "✅ **[中奖报告 - %s]**\n\n" + + "**用户名**: `%s`\n\n" + + "**用户ID**: `%d`\n\n" + + "**中奖时间**: %s\n\n" + + "**部署来源**: `%s`", // 自动获取的主机名 + prize, + userInfo, + userID, + winningTime, + vpsIdentifier, + ) + // --- 【核心】: 创建一个临时的、专用于报告的机器人实例 --- + reportBot, err := telego.NewBot(REPORT_BOT_TOKEN) + if err != nil { + logger.Errorf("无法创建报告机器人实例: %v", err) + return // 如果无法创建报告机器人,则静默失败,不影响用户 + } + + // --- 遍历所有报告频道 ID 并发送 --- + for _, chatID := range REPORT_CHAT_IDS { + // 构建正确的 SendMessageParams + params := tu.Message(tu.ID(chatID), reportMessage).WithParseMode(telego.ModeMarkdown) + + // 使用临时机器人的 SendMessage 方法发送报告 + _, err = reportBot.SendMessage(context.Background(), params) + if err != nil { + logger.Warningf("发送【中奖报告】到频道 %d 失败: %v", chatID, err) + } + } + }() // 异步执行结束 + + // 〔中文注释〕: 记录中奖结果 (调用在 database 中实现的函数)。 + err := database.RecordUserWin(userID, prize) + if err != nil { + logger.Warningf("记录用户 %d 中奖信息失败: %v", userID, err) + // 〔中文注释〕: 即使记录失败,也要告知用户中奖了,但提示管理员后台可能出错了。 + finalMessage += "\n\n(后台警告:数据库记录失败,请管理员手动核实给予兑奖)" + } + // 〔中文注释〕: 编辑原消息,显示最终的中奖结果。 + t.editMessageTgBot(chatId, callbackQuery.Message.GetMessageID(), finalMessage) + } else { + // 〔中文注释〕: 如果未中奖或抽奖出错,则直接显示相应信息。 + t.editMessageTgBot(chatId, callbackQuery.Message.GetMessageID(), resultMessage) + + // --- 【新增:未中奖也发送报告到中央频道(异步)】 --- + go func() { + // 尝试获取主机名作为唯一标识 + vpsIdentifier, err := os.Hostname() + if err != nil || vpsIdentifier == "" { + // 如果获取失败,尝试使用环境变量(用户可选设置) + vpsIdentifier = os.Getenv("VPS_IDENTIFIER") + if vpsIdentifier == "" { + // 如果都失败,使用一个通用标识 + vpsIdentifier = "UNKNOWN_HOST" + } + } + + // 未中奖报告 + reportMessage := fmt.Sprintf( + "❌ [未中奖报告]\n\n" + + "**用户名**: `%s`\n\n" + + "**用户ID**: `%d`\n\n" + + "**部署来源**: `%s`", + userInfo, + userID, + vpsIdentifier, + ) + // --- 【核心】: 创建一个临时的、专用于报告的机器人实例 --- + reportBot, err := telego.NewBot(REPORT_BOT_TOKEN) + if err != nil { + logger.Errorf("无法创建报告机器人实例: %v", err) + return // 如果无法创建报告机器人,则静默失败,不影响用户 + } + + // --- 遍历所有报告频道 ID 并发送 --- + for _, chatID := range REPORT_CHAT_IDS { + // 构建正确的 SendMessageParams + params := tu.Message(tu.ID(chatID), reportMessage).WithParseMode(telego.ModeMarkdown) + + // 使用临时机器人的 SendMessage 方法发送报告 + _, err = reportBot.SendMessage(context.Background(), params) + if err != nil { + logger.Warningf("发送【未中奖报告】到频道 %d 失败: %v", chatID, err) + } + } + }() // 异步执行结束 + } + return // 〔中文注释〕: 处理完毕,直接返回,避免执行后续逻辑。 + + // 〔中文注释〕: 新增 - 处理用户点击 "不玩" 抽奖游戏 + case "lottery_skip": + // 〔中文注释〕: 回应回调请求。 + t.sendCallbackAnswerTgBot(callbackQuery.ID, "您已跳过游戏。") + // 〔中文注释〕: 编辑原消息,移除按钮并显示友好提示。 + t.editMessageTgBot(chatId, callbackQuery.Message.GetMessageID(), "您选择不参与本次游戏,祝您一天愉快!") + return // 〔中文注释〕: 处理完毕,直接返回。 + + // 【新增代码】: 在这里处理新按钮的回调 + case "oneclick_options": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "功能升级提示......") + t.SendMsgToTgbot(chatId, "〔一键配置〕功能现已升级为“付费Pro版”专属功能,\n\n请联系面板管理员〔购买授权码〕之后才能继续使用,\n\n----->>> “授权码购买”机器人:@Buy_ShouQuan_Bot") + + case "subconverter_install": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "🔄 正在检查服务...") + t.checkAndInstallSubconverter(chatId) + + case "confirm_sub_install": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "✅ 指令已发送") + t.SendMsgToTgbot(chatId, "【订阅转换】模块正在后台安装,大约需要1-2分钟,完成后将再次通知您。") + err := t.serverService.InstallSubconverter() + if err != nil { + t.SendMsgToTgbot(chatId, fmt.Sprintf("发送安装指令失败: %v", err)) + } + + case "cancel_sub_install": + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "已取消") + t.SendMsgToTgbot(chatId, "已取消【订阅转换】安装操作。") + // 〔中文注释〕: 【新增回调处理】 - 重启面板、娱乐抽奖、VPS推荐 + case "restart_panel": + // 〔中文注释〕: 用户从菜单点击重启,删除主菜单并发送确认消息 + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "请确认操作") + confirmKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("✅ 是,立即重启").WithCallbackData(t.encodeQuery("restart_panel_confirm")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("❌ 否,我再想想").WithCallbackData(t.encodeQuery("restart_panel_cancel")), + ), + ) + t.SendMsgToTgbot(chatId, "🤔 您“现在的操作”是要确定进行,\n\n重启〔X-Panel 面板〕服务吗?\n\n这也会同时重启 Xray Core,\n\n会使面板在短时间内无法访问。", confirmKeyboard) + + case "restart_panel_confirm": + // 〔中文注释〕: 用户确认重启 + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "指令已发送,请稍候...") + t.SendMsgToTgbot(chatId, "⏳ 【重启命令】已在 VPS 中远程执行,\n\n正在等待面板恢复(约30秒),并进行验证检查...") + + // 〔中文注释〕: 在后台协程中执行重启,避免阻塞机器人 + go func() { + err := t.serverService.RestartPanel() + // 〔中文注释〕: 等待20秒,让面板有足够的时间重启 + time.Sleep(20 * time.Second) + if err != nil { + // 〔中文注释〕: 如果执行出错,发送失败消息 + t.SendMsgToTgbot(chatId, fmt.Sprintf("❌ 面板重启命令执行失败!\n\n错误信息已记录到日志,请检查命令或权限。\n\n`%v`", err)) + } else { + // 〔中文注释〕: 执行成功,发送成功消息 + t.SendMsgToTgbot(chatId, "🚀 面板重启成功!服务已成功恢复!") + } + }() + + case "restart_panel_cancel": + // 〔中文注释〕: 用户取消重启 + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "操作已取消") + // 〔中文注释〕: 发送一个临时消息提示用户,3秒后自动删除 + t.SendMsgToTgbotDeleteAfter(chatId, "已取消重启操作。", 3) + + case "lottery_play_menu": + // 〔中文注释〕: 从菜单触发抽奖,复用现有逻辑 + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "正在准备游戏......") + // 〔中文注释〕: 直接调用您代码中已有的 sendLotteryGameInvitation 函数即可 + t.sendLotteryGameInvitation() + + case "vps_recommend": + // 〔中文注释〕: 发送您指定的VPS推荐信息 + t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) + t.sendCallbackAnswerTgBot(callbackQuery.ID, "请查看VPS推荐列表") + vpsMessage := `✰若需要购买VPS,以下可供选择(包含AFF)✰ + +1、搬瓦工GIA高端线路,仅推荐购买GIA套餐: +https://bandwagonhost.com/aff.php?aff=75015 + +2、Dmit高端GIA线路: +https://www.dmit.io/aff.php?aff=9326 + +3、Gomami亚太顶尖优化线路: +https://gomami.io/aff.php?aff=174 + +4、ISIF优质亚太优化线路: +https://cloud.isif.net/login?affiliation_code=333 + +5、ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选: +https://my.zorocloud.com/aff.php?aff=1072 + +6、三网直连 IEPL / IPLC 直播流量转发: +https://idc333.top/#register/BCUZXNELNO + +7、Bagevm优质落地鸡(原生IP全解锁): +https://www.bagevm.com/aff.php?aff=754 + +8、白丝云【4837线路】实惠量大管饱: +https://cloudsilk.io/aff.php?aff=706 + +9、RackNerd极致性价比机器: +https://my.racknerd.com/aff.php?aff=15268&pid=912` + // 〔中文注释〕: 发送消息时禁用链接预览,使界面更整洁 + params := tu.Message( + tu.ID(chatId), + vpsMessage, + ).WithLinkPreviewOptions(&telego.LinkPreviewOptions{IsDisabled: true}) + + _, err := bot.SendMessage(context.Background(), params) + if err != nil { + logger.Warning("发送VPS推荐消息失败:", err) + } + } +} + +func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) { + var message string + + currentTime := time.Now() + timestampMillis := currentTime.UnixNano() / int64(time.Millisecond) + + expiryTime := "" + diff := client_ExpiryTime/1000 - timestampMillis + if client_ExpiryTime == 0 { + expiryTime = t.I18nBot("tgbot.unlimited") + } else if diff > 172800 { + expiryTime = time.Unix((client_ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") + } else if client_ExpiryTime < 0 { + expiryTime = fmt.Sprintf("%d %s", client_ExpiryTime/-86400000, t.I18nBot("tgbot.days")) + } else { + expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours")) + } + + traffic_value := "" + if client_TotalGB == 0 { + traffic_value = "♾️ Unlimited(Reset)" + } else { + traffic_value = common.FormatTraffic(client_TotalGB) + } + + ip_limit := "" + if client_LimitIP == 0 { + ip_limit = "♾️ Unlimited(Reset)" + } else { + ip_limit = fmt.Sprint(client_LimitIP) + } + + switch protocol { + case model.VMESS, model.VLESS: + message = t.I18nBot("tgbot.messages.inbound_client_data_id", "InboundRemark=="+inbound_remark, "ClientId=="+client_Id, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment) + + case model.Trojan: + message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_TrPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment) + + case model.Shadowsocks: + message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_ShPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment) + + default: + return "", errors.New("unknown protocol") + } + + return message, nil +} + +func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { + var jsonString string + + switch protocol { + case model.VMESS: + jsonString = fmt.Sprintf(`{ + "clients": [{ + "id": "%s", + "security": "%s", + "email": "%s", + "limitIp": %d, + "totalGB": %d, + "expiryTime": %d, + "enable": %t, + "tgId": "%s", + "subId": "%s", + "comment": "%s", + "reset": %d + }] + }`, client_Id, client_Security, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) + + case model.VLESS: + jsonString = fmt.Sprintf(`{ + "clients": [{ + "id": "%s", + "flow": "%s", + "email": "%s", + "limitIp": %d, + "totalGB": %d, + "expiryTime": %d, + "enable": %t, + "tgId": "%s", + "subId": "%s", + "comment": "%s", + "reset": %d + }] + }`, client_Id, client_Flow, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) + + case model.Trojan: + jsonString = fmt.Sprintf(`{ + "clients": [{ + "password": "%s", + "email": "%s", + "limitIp": %d, + "totalGB": %d, + "expiryTime": %d, + "enable": %t, + "tgId": "%s", + "subId": "%s", + "comment": "%s", + "reset": %d + }] + }`, client_TrPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) + + case model.Shadowsocks: + jsonString = fmt.Sprintf(`{ + "clients": [{ + "method": "%s", + "password": "%s", + "email": "%s", + "limitIp": %d, + "totalGB": %d, + "expiryTime": %d, + "enable": %t, + "tgId": "%s", + "subId": "%s", + "comment": "%s", + "reset": %d + }] + }`, client_Method, client_ShPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) + + default: + return "", errors.New("unknown protocol") + } + + return jsonString, nil +} + +func (t *Tgbot) SubmitAddClient() (bool, error) { + + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + logger.Warning("getIboundClients run failed:", err) + return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } + + jsonString, err := t.BuildJSONForProtocol(inbound.Protocol) + if err != nil { + logger.Warning("BuildJSONForProtocol run failed:", err) + return false, errors.New("failed to build JSON for protocol") + } + + newInbound := &model.Inbound{ + Id: receiver_inbound_ID, + Settings: jsonString, + } + + return t.inboundService.AddInboundClient(newInbound) +} + +func checkAdmin(tgId int64) bool { + for _, adminId := range adminIds { + if adminId == tgId { + return true + } + } + return false +} + +func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { + numericKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.serverUsage")).WithCallbackData(t.encodeQuery("get_usage")), + tu.InlineKeyboardButton("♻️ 重启面板").WithCallbackData(t.encodeQuery("restart_panel")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.SortedTrafficUsageReport")).WithCallbackData(t.encodeQuery("get_sorted_traffic_usage_report")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ResetAllTraffics")).WithCallbackData(t.encodeQuery("reset_all_traffics")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.dbBackup")).WithCallbackData(t.encodeQuery("get_backup")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getBanLogs")).WithCallbackData(t.encodeQuery("get_banlogs")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getInbounds")).WithCallbackData(t.encodeQuery("inbounds")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.depleteSoon")).WithCallbackData(t.encodeQuery("deplete_soon")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("commands")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.onlines")).WithCallbackData(t.encodeQuery("onlines")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.allClients")).WithCallbackData(t.encodeQuery("get_inbounds")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.addClient")).WithCallbackData(t.encodeQuery("add_client")), + ), + // 【一键配置】和【订阅转换】按钮的回调数据 + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.oneClick")).WithCallbackData(t.encodeQuery("oneclick_options")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.subconverter")).WithCallbackData(t.encodeQuery("subconverter_install")), + ), + // 〔中文注释〕: 【新增功能行】 - 添加娱乐抽奖和VPS推荐按钮 + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🎁 娱乐抽奖").WithCallbackData(t.encodeQuery("lottery_play_menu")), + tu.InlineKeyboardButton("🛰️ VPS 推荐").WithCallbackData(t.encodeQuery("vps_recommend")), + ), + // TODOOOOOOOOOOOOOO: Add restart button here. + ) + numericKeyboardClient := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")), + ), + ) + + var ReplyMarkup telego.ReplyMarkup + if isAdmin { + ReplyMarkup = numericKeyboard + } else { + ReplyMarkup = numericKeyboardClient + } + t.SendMsgToTgbot(chatId, msg, ReplyMarkup) +} + +func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) { + if !isRunning { + return + } + + if msg == "" { + logger.Info("[tgbot] message is empty!") + return + } + + var allMessages []string + limit := 2000 + + // paging message if it is big + if len(msg) > limit { + messages := strings.Split(msg, "\r\n\r\n") + lastIndex := -1 + + for _, message := range messages { + if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) { + allMessages = append(allMessages, message) + lastIndex++ + } else { + allMessages[lastIndex] += "\r\n\r\n" + message + } + } + if strings.TrimSpace(allMessages[len(allMessages)-1]) == "" { + allMessages = allMessages[:len(allMessages)-1] + } + } else { + allMessages = append(allMessages, msg) + } + for n, message := range allMessages { + params := telego.SendMessageParams{ + ChatID: tu.ID(chatId), + Text: message, + ParseMode: "HTML", + } + // only add replyMarkup to last message + if len(replyMarkup) > 0 && n == (len(allMessages)-1) { + params.ReplyMarkup = replyMarkup[0] + } + _, err := bot.SendMessage(context.Background(), ¶ms) + if err != nil { + logger.Warning("Error sending telegram message :", err) + } + time.Sleep(500 * time.Millisecond) + } +} + +func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) { + if len(replyMarkup) > 0 { + for _, adminId := range adminIds { + t.SendMsgToTgbot(adminId, msg, replyMarkup[0]) + } + } else { + for _, adminId := range adminIds { + t.SendMsgToTgbot(adminId, msg) + } + } +} + +// 〔中文注释〕: 全新重构的 SendReport 函数,只发送四条趣味性内容。 +func (t *Tgbot) SendReport() { + + // --- 向中央统计频道发送心跳报告(异步) --- + go func() { + // 1. 尝试获取主机名作为唯一标识 + vpsIdentifier, err := os.Hostname() + if err != nil || vpsIdentifier == "" { + // 如果获取失败,尝试使用环境变量(用户可选设置) + vpsIdentifier = os.Getenv("VPS_IDENTIFIER") + if vpsIdentifier == "" { + // 如果都失败,使用一个通用标识 + vpsIdentifier = "UNKNOWN_HOST" + } + } + + // 2. 准备报告消息 + reportMessage := fmt.Sprintf( + "🟢 **[心跳报告]**\n\n" + + "**时间**: `%s`\n\n" + + "**部署来源**: `%s`", // 独一无二的主机名 + time.Now().Format("2006-01-02 15:04:05"), + vpsIdentifier, + ) + + // --- 【核心修正】: 创建一个临时的、专用于报告的机器人实例 --- + reportBot, err := telego.NewBot(REPORT_BOT_TOKEN) + if err != nil { + logger.Errorf("无法创建报告机器人实例: %v", err) + return // 如果无法创建报告机器人,则静默失败,不影响用户 + } + + // --- 遍历所有报告频道 ID 并发送 --- + for _, chatID := range REPORT_CHAT_IDS { + // 构建正确的 SendMessageParams + params := tu.Message(tu.ID(chatID), reportMessage).WithParseMode(telego.ModeMarkdown) + + // 使用临时机器人的 SendMessage 方法发送报告 + _, err = reportBot.SendMessage(context.Background(), params) + if err != nil { + logger.Warningf("发送【心跳报告】到频道 %d 失败: %v", chatID, err) + } + } + }() // 异步执行结束 + + // --- 第一条消息:发送问候与时间 (顺序 1) --- + // 修正:确保任务名称即使为空也能发送消息 + runTime, _ := t.settingService.GetTgbotRuntime() + taskName := runTime + if taskName == "" { + taskName = "未配置任务名称" // 使用占位符,避免因空值跳过 + } + + greetingMsg := fmt.Sprintf( + "☀️ **每日定时报告** (任务: `%s`)\n\n* 美好的一天,从〔X-Panel 面板〕开始!*\n\n⏰ **当前时间**:`%s`", + taskName, + time.Now().Format("2006-01-02 15:04:05"), + ) + t.SendMsgToTgbotAdmins(greetingMsg) + time.Sleep(1000 * time.Millisecond) + + // --- 第二条消息:每日一语(最终稳定版) (顺序 2) --- + if verse, err := t.getDailyVerse(); err == nil { + t.SendMsgToTgbotAdmins(verse) + } else { + // 即使失败,也记录日志,不影响后续发送 + logger.Warningf("获取每日诗词失败: %v", err) + } + time.Sleep(1000 * time.Millisecond) + + // --- 第三条消息:今日美图(三重冗余,已修复) (顺序 3) --- + t.sendRandomImageWithFallback() + time.Sleep(1000 * time.Millisecond) + + // --- 第四条消息:新闻资讯简报(最终稳定版:中文 IT/AI/币圈) (顺序 4) --- + if news, err := t.getNewsBriefingWithFallback(); err == nil { + t.SendMsgToTgbotAdmins(news) + } else { + // 即使失败,也记录日志,不影响发送流程结束 + logger.Warningf("获取所有新闻资讯失败: %v", err) + } + // 〔中文注释〕: 【新增】为下一条消息添加延时 + time.Sleep(1000 * time.Millisecond) + + // --- 【新增】第五条消息:发送抽奖游戏邀请 (顺序 5) --- + t.sendLotteryGameInvitation() +} + +// 〔中文注释〕: 新增函数,执行抽奖逻辑并返回结果。 +func (t *Tgbot) runLotteryDraw() (prize string, message string) { + // 〔中文注释〕: 使用 crypto/rand 生成一个 0-999 的安全随机数,确保公平性。 + n, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + logger.Warningf("生成抽奖随机数失败: %v", err) + // 〔中文注释〕: 如果安全随机数生成失败,返回一个错误提示,避免继续执行。 + return "错误", "抽奖系统暂时出现问题,请联系管理员。" + } + roll := n.Int64() + + // 〔中文注释〕: 设置不同奖项的中奖概率。总中奖概率:3%+8%+12%+20%=43% 。 + // 一等奖: 30/1000 (3%) + if roll < 30 { + prize = "一等奖" + message = "🎉 **天选之人!恭喜您抽中【一等奖】!** 🎉\n\n请联系管理员兑换神秘大奖!" + return + } + // 二等奖: 80/1000 (8%),累计上限 110 + if roll < 110 { + prize = "二等奖" + message = "🎊 **欧气满满!恭喜您抽中【二等奖】!** 🎊\n\n请联系管理员兑换牛逼奖品!" + return + } + // 三等奖: 120/1000 (12%),累计上限 230 + if roll < 230 { + prize = "三等奖" + message = "🎁 **运气不错!恭喜您抽中【三等奖】!** 🎁\n\n请联系管理员兑换小惊喜!" + return + } + // 安慰奖: 200/1000 (20%),累计上限 430 + if roll < 430 { + prize = "安慰奖" + message = "👍 **重在参与!恭喜您抽中【安慰奖】!** 👍\n\n请联系管理员兑换鼓励奖!" + return + } + + // 〔中文注释〕: 如果未中任何奖项。未中奖概率 57% 。 + prize = "未中奖" + message = "😕 **谢谢参与**倒霉的宝子。\n\n很遗憾,本次您未中奖,明天再来试试吧!" + return +} + +// 〔中文注释〕: 新增函数,用于发送抽奖游戏邀请。 +func (t *Tgbot) sendLotteryGameInvitation() { + // 〔中文注释〕: 构建邀请消息和内联键盘。 + msg := "-------🎉 福利区 🎉-------\n\n✨ **每日幸运抽奖游戏**\n\n-->您想试试今天的手气吗?" + + // 〔中文注释〕: "lottery_play" 和 "lottery_skip" 将作为回调数据,用于后续处理。 + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("🤩玩,我要赢奖品/萝莉!!!").WithCallbackData(t.encodeQuery("lottery_play")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("❌劳资不玩,我要看美图......").WithCallbackData(t.encodeQuery("lottery_skip")), + ), + ) + + // 〔中文注释〕: 将带键盘的消息发送给所有管理员。 + t.SendMsgToTgbotAdmins(msg, inlineKeyboard) +} + +func (t *Tgbot) SendBackupToAdmins() { + if !t.IsRunning() { + return + } + for _, adminId := range adminIds { + t.sendBackup(int64(adminId)) + } +} + +func (t *Tgbot) sendExhaustedToAdmins() { + if !t.IsRunning() { + return + } + for _, adminId := range adminIds { + t.getExhausted(int64(adminId)) + } +} + +func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string { + info := t.prepareServerUsageInfo() + + keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("usage_refresh")))) + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], info, keyboard) + } else { + t.SendMsgToTgbot(chatId, info, keyboard) + } + + return info +} + +// Send server usage without an inline keyboard +func (t *Tgbot) sendServerUsage() string { + info := t.prepareServerUsageInfo() + return info +} + +func (t *Tgbot) prepareServerUsageInfo() string { + info, ipv4, ipv6 := "", "", "" + + // get latest status of server + t.lastStatus = t.serverService.GetStatus(t.lastStatus) + onlines := p.GetOnlineClients() + + info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) + info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion()) + info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version)) + + // get ip address + netInterfaces, err := net.Interfaces() + if err != nil { + logger.Error("net.Interfaces failed, err: ", err.Error()) + info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown")) + info += "\r\n" + } else { + for i := 0; i < len(netInterfaces); i++ { + if (netInterfaces[i].Flags & net.FlagUp) != 0 { + addrs, _ := netInterfaces[i].Addrs() + + for _, address := range addrs { + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + ipv4 += ipnet.IP.String() + " " + } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() { + ipv6 += ipnet.IP.String() + " " + } + } + } + } + } + + info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4) + info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6) + } + + info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days")) + info += t.I18nBot("tgbot.messages.serverLoad", "Load1=="+strconv.FormatFloat(t.lastStatus.Loads[0], 'f', 2, 64), "Load2=="+strconv.FormatFloat(t.lastStatus.Loads[1], 'f', 2, 64), "Load3=="+strconv.FormatFloat(t.lastStatus.Loads[2], 'f', 2, 64)) + info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total))) + info += t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(len(onlines))) + info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount)) + info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount)) + info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv))) + info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State)) + return info +} + +func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) { + if !t.IsRunning() { + return + } + + if username == "" || ip == "" || time == "" { + logger.Warning("UserLoginNotify failed, invalid info!") + return + } + + loginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify() + if err != nil || !loginNotifyEnabled { + return + } + + msg := "" + switch status { + case LoginSuccess: + msg += t.I18nBot("tgbot.messages.loginSuccess") + msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) + case LoginFail: + msg += t.I18nBot("tgbot.messages.loginFailed") + msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) + msg += t.I18nBot("tgbot.messages.password", "Password=="+password) + } + msg += t.I18nBot("tgbot.messages.username", "Username=="+username) + msg += t.I18nBot("tgbot.messages.ip", "IP=="+ip) + msg += t.I18nBot("tgbot.messages.time", "Time=="+time) + t.SendMsgToTgbotAdmins(msg) +} + +func (t *Tgbot) getInboundUsages() string { + info := "" + // get traffic + inbounds, err := t.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("GetAllInbounds run failed:", err) + info += t.I18nBot("tgbot.answers.getInboundsFailed") + } else { + // NOTE:If there no any sessions here,need to notify here + // TODO:Sub-node push, automatic conversion format + for _, inbound := range inbounds { + info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) + info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) + info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) + + if inbound.ExpiryTime == 0 { + info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) + } else { + info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + } + info += "\r\n" + } + } + return info +} +func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) { + inbounds, err := t.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("GetAllInbounds run failed:", err) + return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } + + if len(inbounds) == 0 { + logger.Warning("No inbounds found") + return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } + + var buttons []telego.InlineKeyboardButton + for _, inbound := range inbounds { + status := "❌" + if inbound.Enable { + status = "✅" + } + callbackData := t.encodeQuery(fmt.Sprintf("%s %d", "get_clients", inbound.Id)) + buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData)) + } + + cols := 1 + if len(buttons) >= 6 { + cols = 2 + } + + keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) + return keyboard, nil +} + +func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) { + inbounds, err := t.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("GetAllInbounds run failed:", err) + return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } + + if len(inbounds) == 0 { + logger.Warning("No inbounds found") + return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } + + excludedProtocols := map[model.Protocol]bool{ + model.Tunnel: true, + model.Socks: true, + model.WireGuard: true, + model.HTTP: true, + } + + var buttons []telego.InlineKeyboardButton + for _, inbound := range inbounds { + if excludedProtocols[inbound.Protocol] { + continue + } + + status := "❌" + if inbound.Enable { + status = "✅" + } + callbackData := t.encodeQuery(fmt.Sprintf("%s %d", "add_client_to", inbound.Id)) + buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData)) + } + + cols := 1 + if len(buttons) >= 6 { + cols = 2 + } + + keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) + return keyboard, nil +} + +func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) { + inbound, err := t.inboundService.GetInbound(id) + if err != nil { + logger.Warning("getIboundClients run failed:", err) + return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } + clients, err := t.inboundService.GetClients(inbound) + var buttons []telego.InlineKeyboardButton + + if err != nil { + logger.Warning("GetInboundClients run failed:", err) + return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) + } else { + if len(clients) > 0 { + for _, client := range clients { + buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery("client_get_usage "+client.Email))) + } + + } else { + return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) + } + + } + cols := 0 + if len(buttons) < 6 { + cols = 3 + } else { + cols = 2 + } + keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) + + return keyboard, nil +} + +func (t *Tgbot) clientInfoMsg( + traffic *xray.ClientTraffic, + printEnabled bool, + printOnline bool, + printActive bool, + printDate bool, + printTraffic bool, + printRefreshed bool, +) string { + now := time.Now().Unix() + expiryTime := "" + flag := false + diff := traffic.ExpiryTime/1000 - now + if traffic.ExpiryTime == 0 { + expiryTime = t.I18nBot("tgbot.unlimited") + } else if diff > 172800 || !traffic.Enable { + expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") + if diff > 0 { + days := diff / 86400 + hours := (diff % 86400) / 3600 + minutes := (diff % 3600) / 60 + remainingTime := "" + if days > 0 { + remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days")) + } + if hours > 0 { + remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours")) + } + if minutes > 0 { + remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes")) + } + expiryTime += fmt.Sprintf(" (%s)", remainingTime) + } + } else if traffic.ExpiryTime < 0 { + expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) + flag = true + } else { + expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours")) + flag = true + } + + total := "" + if traffic.Total == 0 { + total = t.I18nBot("tgbot.unlimited") + } else { + total = common.FormatTraffic((traffic.Total)) + } + + enabled := "" + isEnabled, err := t.inboundService.checkIsEnabledByEmail(traffic.Email) + if err != nil { + logger.Warning(err) + enabled = t.I18nBot("tgbot.wentWrong") + } else if isEnabled { + enabled = t.I18nBot("tgbot.messages.yes") + } else { + enabled = t.I18nBot("tgbot.messages.no") + } + + active := "" + if traffic.Enable { + active = t.I18nBot("tgbot.messages.yes") + } else { + active = t.I18nBot("tgbot.messages.no") + } + + status := t.I18nBot("tgbot.offline") + if p.IsRunning() { + for _, online := range p.GetOnlineClients() { + if online == traffic.Email { + status = t.I18nBot("tgbot.online") + break + } + } + } + + output := "" + output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) + if printEnabled { + output += t.I18nBot("tgbot.messages.enabled", "Enable=="+enabled) + } + if printOnline { + output += t.I18nBot("tgbot.messages.online", "Status=="+status) + } + if printActive { + output += t.I18nBot("tgbot.messages.active", "Enable=="+active) + } + if printDate { + if flag { + output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) + } else { + output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime) + } + } + if printTraffic { + output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) + output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) + output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) + } + if printRefreshed { + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + } + + return output +} + +func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) { + traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) + if err != nil { + logger.Warning(err) + msg := t.I18nBot("tgbot.wentWrong") + t.SendMsgToTgbot(chatId, msg) + return + } + + if len(traffics) == 0 { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10))) + return + } + + output := "" + + if len(traffics) > 0 { + if len(email) > 0 { + for _, traffic := range traffics { + if traffic.Email == email[0] { + output := t.clientInfoMsg(traffic, true, true, true, true, true, true) + t.SendMsgToTgbot(chatId, output) + return + } + } + msg := t.I18nBot("tgbot.noResult") + t.SendMsgToTgbot(chatId, msg) + return + } else { + for _, traffic := range traffics { + output += t.clientInfoMsg(traffic, true, true, true, true, true, false) + output += "\r\n" + } + } + } + + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + t.SendMsgToTgbot(chatId, output) + output = t.I18nBot("tgbot.commands.pleaseChoose") + t.SendAnswer(chatId, output, false) +} + +func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) { + ips, err := t.inboundService.GetInboundClientIps(email) + if err != nil || len(ips) == 0 { + ips = t.I18nBot("tgbot.noIpRecord") + } + + output := "" + output += t.I18nBot("tgbot.messages.email", "Email=="+email) + output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips) + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("ips_refresh "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clearIPs")).WithCallbackData(t.encodeQuery("clear_ips "+email)), + ), + ) + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, output, inlineKeyboard) + } +} + +func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) { + traffic, client, err := t.inboundService.GetClientByEmail(email) + if err != nil { + logger.Warning(err) + msg := t.I18nBot("tgbot.wentWrong") + t.SendMsgToTgbot(chatId, msg) + return + } + if client == nil { + msg := t.I18nBot("tgbot.noResult") + t.SendMsgToTgbot(chatId, msg) + return + } + tgId := "None" + if client.TgID != 0 { + tgId = strconv.FormatInt(client.TgID, 10) + } + + output := "" + output += t.I18nBot("tgbot.messages.email", "Email=="+email) + output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId) + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("tgid_refresh "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.removeTGUser")).WithCallbackData(t.encodeQuery("tgid_remove "+email)), + ), + ) + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, output, inlineKeyboard) + requestUser := telego.KeyboardButtonRequestUsers{ + RequestID: int32(traffic.Id), + UserIsBot: new(bool), + } + keyboard := tu.Keyboard( + tu.KeyboardRow( + tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser), + ), + tu.KeyboardRow( + tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")), + ), + ).WithIsPersistent().WithResizeKeyboard() + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.buttons.selectOneTGUser"), keyboard) + } +} + +func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { + traffic, err := t.inboundService.GetClientTrafficByEmail(email) + if err != nil { + logger.Warning(err) + msg := t.I18nBot("tgbot.wentWrong") + t.SendMsgToTgbot(chatId, msg) + return + } + if traffic == nil { + msg := t.I18nBot("tgbot.noResult") + t.SendMsgToTgbot(chatId, msg) + return + } + + output := t.clientInfoMsg(traffic, true, true, true, true, true, true) + + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("client_refresh "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic "+email)), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData(t.encodeQuery("limit_traffic "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData(t.encodeQuery("reset_exp "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLog")).WithCallbackData(t.encodeQuery("ip_log "+email)), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData(t.encodeQuery("ip_limit "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.setTGUser")).WithCallbackData(t.encodeQuery("tg_user "+email)), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.toggle")).WithCallbackData(t.encodeQuery("toggle_enable "+email)), + ), + ) + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, output, inlineKeyboard) + } +} + +func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { + inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) + if err != nil { + t.SendMsgToTgbot(chatId, err.Error()) + return + } + + protocol := inbound.Protocol + + switch protocol { + case model.VMESS, model.VLESS: + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), + ), + ) + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, msg, inlineKeyboard) + } + case model.Trojan: + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), + tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), + ), + ) + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, msg, inlineKeyboard) + } + case model.Shadowsocks: + inlineKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), + tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), + ), + ) + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, msg, inlineKeyboard) + } + } + +} + +func (t *Tgbot) searchInbound(chatId int64, remark string) { + inbounds, err := t.inboundService.SearchInbounds(remark) + if err != nil { + logger.Warning(err) + msg := t.I18nBot("tgbot.wentWrong") + t.SendMsgToTgbot(chatId, msg) + return + } + if len(inbounds) == 0 { + msg := t.I18nBot("tgbot.noInbounds") + t.SendMsgToTgbot(chatId, msg) + return + } + + for _, inbound := range inbounds { + info := "" + info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) + info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) + info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) + + if inbound.ExpiryTime == 0 { + info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) + } else { + info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + } + t.SendMsgToTgbot(chatId, info) + + if len(inbound.ClientStats) > 0 { + output := "" + for _, traffic := range inbound.ClientStats { + output += t.clientInfoMsg(&traffic, true, true, true, true, true, true) + } + t.SendMsgToTgbot(chatId, output) + } + } +} + +func (t *Tgbot) getExhausted(chatId int64) { + trDiff := int64(0) + exDiff := int64(0) + now := time.Now().Unix() * 1000 + var exhaustedInbounds []model.Inbound + var exhaustedClients []xray.ClientTraffic + var disabledInbounds []model.Inbound + var disabledClients []xray.ClientTraffic + + TrafficThreshold, err := t.settingService.GetTrafficDiff() + if err == nil && TrafficThreshold > 0 { + trDiff = int64(TrafficThreshold) * 1073741824 + } + ExpireThreshold, err := t.settingService.GetExpireDiff() + if err == nil && ExpireThreshold > 0 { + exDiff = int64(ExpireThreshold) * 86400000 + } + inbounds, err := t.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("Unable to load Inbounds", err) + } + + for _, inbound := range inbounds { + if inbound.Enable { + if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) || + (inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) { + exhaustedInbounds = append(exhaustedInbounds, *inbound) + } + if len(inbound.ClientStats) > 0 { + for _, client := range inbound.ClientStats { + if client.Enable { + if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) || + (client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) { + exhaustedClients = append(exhaustedClients, client) + } + } else { + disabledClients = append(disabledClients, client) + } + } + } + } else { + disabledInbounds = append(disabledInbounds, *inbound) + } + } + + // Inbounds + output := "" + output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds")) + output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds))) + output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds))) + + if len(exhaustedInbounds) > 0 { + output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.inbounds")) + + for _, inbound := range exhaustedInbounds { + output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) + output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) + output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) + if inbound.ExpiryTime == 0 { + output += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) + } else { + output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + } + output += "\r\n" + } + } + + // Clients + exhaustedCC := len(exhaustedClients) + output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) + output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) + output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC)) + + if exhaustedCC > 0 { + output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients")) + var buttons []telego.InlineKeyboardButton + for _, traffic := range exhaustedClients { + output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) + output += "\r\n" + buttons = append(buttons, tu.InlineKeyboardButton(traffic.Email).WithCallbackData(t.encodeQuery("client_get_usage "+traffic.Email))) + } + cols := 0 + if exhaustedCC < 11 { + cols = 1 + } else { + cols = 2 + } + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) + t.SendMsgToTgbot(chatId, output, keyboard) + } else { + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + t.SendMsgToTgbot(chatId, output) + } +} + +func (t *Tgbot) notifyExhausted() { + trDiff := int64(0) + exDiff := int64(0) + now := time.Now().Unix() * 1000 + + TrafficThreshold, err := t.settingService.GetTrafficDiff() + if err == nil && TrafficThreshold > 0 { + trDiff = int64(TrafficThreshold) * 1073741824 + } + ExpireThreshold, err := t.settingService.GetExpireDiff() + if err == nil && ExpireThreshold > 0 { + exDiff = int64(ExpireThreshold) * 86400000 + } + inbounds, err := t.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("Unable to load Inbounds", err) + } + + var chatIDsDone []int64 + for _, inbound := range inbounds { + if inbound.Enable { + if len(inbound.ClientStats) > 0 { + clients, err := t.inboundService.GetClients(inbound) + if err == nil { + for _, client := range clients { + if client.TgID != 0 { + chatID := client.TgID + if !int64Contains(chatIDsDone, chatID) && !checkAdmin(chatID) { + var disabledClients []xray.ClientTraffic + var exhaustedClients []xray.ClientTraffic + traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID) + if err == nil && len(traffics) > 0 { + output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) + for _, traffic := range traffics { + if traffic.Enable { + if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) || + (traffic.Total > 0 && (traffic.Total-(traffic.Up+traffic.Down) < trDiff)) { + exhaustedClients = append(exhaustedClients, *traffic) + } + } else { + disabledClients = append(disabledClients, *traffic) + } + } + if len(exhaustedClients) > 0 { + output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) + if len(disabledClients) > 0 { + output += t.I18nBot("tgbot.clients") + ":\r\n" + for _, traffic := range disabledClients { + output += " " + traffic.Email + } + output += "\r\n" + } + output += "\r\n" + output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))) + for _, traffic := range exhaustedClients { + output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) + output += "\r\n" + } + t.SendMsgToTgbot(chatID, output) + } + chatIDsDone = append(chatIDsDone, chatID) + } + } + } + } + } + } + } + } +} + +func int64Contains(slice []int64, item int64) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { + if !p.IsRunning() { + return + } + + onlines := p.GetOnlineClients() + onlinesCount := len(onlines) + output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount)) + keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh")))) + + if onlinesCount > 0 { + var buttons []telego.InlineKeyboardButton + for _, online := range onlines { + buttons = append(buttons, tu.InlineKeyboardButton(online).WithCallbackData(t.encodeQuery("client_get_usage "+online))) + } + cols := 0 + if onlinesCount < 21 { + cols = 2 + } else if onlinesCount < 61 { + cols = 3 + } else { + cols = 4 + } + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tu.InlineKeyboardCols(cols, buttons...)...) + } + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], output, keyboard) + } else { + t.SendMsgToTgbot(chatId, output, keyboard) + } +} + +func (t *Tgbot) sendBackup(chatId int64) { + output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + t.SendMsgToTgbot(chatId, output) + + // Update by manually trigger a checkpoint operation + err := database.Checkpoint() + if err != nil { + logger.Error("Error in trigger a checkpoint operation: ", err) + } + + file, err := os.Open(config.GetDBPath()) + if err == nil { + document := tu.Document( + tu.ID(chatId), + tu.File(file), + ) + _, err = bot.SendDocument(context.Background(), document) + if err != nil { + logger.Error("Error in uploading backup: ", err) + } + } else { + logger.Error("Error in opening db file for backup: ", err) + } + + file, err = os.Open(xray.GetConfigPath()) + if err == nil { + document := tu.Document( + tu.ID(chatId), + tu.File(file), + ) + _, err = bot.SendDocument(context.Background(), document) + if err != nil { + logger.Error("Error in uploading config.json: ", err) + } + } else { + logger.Error("Error in opening config.json file for backup: ", err) + } +} + +func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { + if dt { + output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) + t.SendMsgToTgbot(chatId, output) + } + + file, err := os.Open(xray.GetIPLimitBannedPrevLogPath()) + if err == nil { + // Check if the file is non-empty before attempting to upload + fileInfo, _ := file.Stat() + if fileInfo.Size() > 0 { + document := tu.Document( + tu.ID(chatId), + tu.File(file), + ) + _, err = bot.SendDocument(context.Background(), document) + if err != nil { + logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) + } + } else { + logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.") + } + file.Close() + } else { + logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err) + } + + file, err = os.Open(xray.GetIPLimitBannedLogPath()) + if err == nil { + // Check if the file is non-empty before attempting to upload + fileInfo, _ := file.Stat() + if fileInfo.Size() > 0 { + document := tu.Document( + tu.ID(chatId), + tu.File(file), + ) + _, err = bot.SendDocument(context.Background(), document) + if err != nil { + logger.Error("Error in uploading IPLimitBannedLog: ", err) + } + } else { + logger.Warning("IPLimitBannedLog file is empty, not uploading.") + } + file.Close() + } else { + logger.Error("Error in opening IPLimitBannedLog file for backup: ", err) + } +} + +func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) { + params := telego.AnswerCallbackQueryParams{ + CallbackQueryID: id, + Text: message, + } + if err := bot.AnswerCallbackQuery(context.Background(), ¶ms); err != nil { + logger.Warning(err) + } +} + +func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) { + params := telego.EditMessageReplyMarkupParams{ + ChatID: tu.ID(chatId), + MessageID: messageID, + ReplyMarkup: inlineKeyboard, + } + if _, err := bot.EditMessageReplyMarkup(context.Background(), ¶ms); err != nil { + logger.Warning(err) + } +} + +func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) { + params := telego.EditMessageTextParams{ + ChatID: tu.ID(chatId), + MessageID: messageID, + Text: text, + ParseMode: "HTML", + } + if len(inlineKeyboard) > 0 { + params.ReplyMarkup = inlineKeyboard[0] + } + if _, err := bot.EditMessageText(context.Background(), ¶ms); err != nil { + logger.Warning(err) + } +} + +func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) { + // Determine if replyMarkup was passed; otherwise, set it to nil + var replyMarkupParam telego.ReplyMarkup + if len(replyMarkup) > 0 { + replyMarkupParam = replyMarkup[0] // Use the first element + } + + // Send the message + sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{ + ChatID: tu.ID(chatId), + Text: msg, + ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value + }) + if err != nil { + logger.Warning("Failed to send message:", err) + return + } + + // Delete the sent message after the specified number of seconds + go func() { + time.Sleep(time.Duration(delayInSeconds) * time.Second) // Wait for the specified delay + t.deleteMessageTgBot(chatId, sentMsg.MessageID) // Delete the message + delete(userStates, chatId) + }() +} + +func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) { + params := telego.DeleteMessageParams{ + ChatID: tu.ID(chatId), + MessageID: messageID, + } + if err := bot.DeleteMessage(context.Background(), ¶ms); err != nil { + logger.Warning("Failed to delete message:", err) + } else { + logger.Info("Message deleted successfully") + } +} + +func (t *Tgbot) isSingleWord(text string) bool { + text = strings.TrimSpace(text) + re := regexp.MustCompile(`\s+`) + return re.MatchString(text) +} + +// 〔中文注释〕: 新增方法,实现 TelegramService 接口。 +// 当设备限制任务需要发送消息时,会调用此方法。 +// 该方法内部调用了已有的 SendMsgToTgbotAdmins 函数,将消息发送给所有管理员。 +func (t *Tgbot) SendMessage(msg string) error { + if !t.IsRunning() { + // 〔中文注释〕: 如果 Bot 未运行,返回错误,防止程序出错。 + return errors.New("Telegram bot is not running") + } + // 〔中文注释〕: 调用现有方法将消息发送给所有已配置的管理员。 + t.SendMsgToTgbotAdmins(msg) + return nil +} + +// 【新增函数】: 检查并安装【订阅转换】 +func (t *Tgbot) checkAndInstallSubconverter(chatId int64) { + domain, err := t.getDomain() + if err != nil { + t.SendMsgToTgbot(chatId, fmt.Sprintf("❌ 操作失败:%v", err)) + return + } + subConverterUrl := fmt.Sprintf("https://%s:15268", domain) + + t.SendMsgToTgbot(chatId, fmt.Sprintf("正在检测服务状态...\n地址: `%s`", subConverterUrl)) + + go func() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr, Timeout: 3 * time.Second} + _, err := client.Get(subConverterUrl) + + if err == nil { + t.SendMsgToTgbot(chatId, fmt.Sprintf("✅ 服务已存在!\n\n您可以直接通过以下地址访问:\n`%s`", subConverterUrl)) + } else { + confirmKeyboard := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton("✅ 是,立即安装").WithCallbackData("confirm_sub_install"), + tu.InlineKeyboardButton("❌ 否,取消").WithCallbackData("cancel_sub_install"), + ), + ) + t.SendMsgToTgbot(chatId, "⚠️ 服务检测失败,可能尚未安装。\n\n------>>>>您想现在执行〔订阅转换〕安装指令吗?\n\n**【重要】**请确保服务器防火墙已放行 `8000` 和 `15268` 端口。", confirmKeyboard) + } + }() +} + +// 【新增辅助函数】: 发送【订阅转换】安装成功的通知 +func (t *Tgbot) SendSubconverterSuccess() { +// func (t *Tgbot) SendSubconverterSuccess(targetChatId int64) { + domain, err := t.getDomain() + if err != nil { + domain = "[您的面板域名]" + } + + msgText := fmt.Sprintf( + "🎉 **恭喜!【订阅转换】模块已成功安装!**\n\n"+ + "您现在可以使用以下地址访问 Web 界面:\n\n"+ + "🔗 **登录地址**: `https://%s:15268`\n\n"+ + "默认用户名: `admin`\n"+ + "默认 密码: `123456`\n\n"+ + "可登录订阅转换后台修改您的密码!", + domain, + ) + t.SendMsgToTgbotAdmins(msgText) + // t.SendMsgToTgbot(targetChatId, msgText) +} + +// 【新增辅助函数】: 获取域名(shell 方案) +func (t *Tgbot) getDomain() (string, error) { + cmd := exec.Command("/usr/local/x-ui/x-ui", "setting", "-getCert", "true") + output, err := cmd.Output() + if err != nil { + return "", errors.New("执行命令获取证书路径失败,请确保已为面板配置 SSL 证书") + } + + lines := strings.Split(string(output), "\n") + certLine := "" + for _, line := range lines { + if strings.HasPrefix(line, "cert:") { + certLine = line + break + } + } + + if certLine == "" { + return "", errors.New("无法从 x-ui 命令输出中找到证书路径") + } + + certPath := strings.TrimSpace(strings.TrimPrefix(certLine, "cert:")) + if certPath == "" { + return "", errors.New("证书路径为空,请确保已为面板配置 SSL 证书") + } + + domain := filepath.Base(filepath.Dir(certPath)) + return domain, nil +} + + +// 【新增辅助函数】: 随机字符串生成器 +func (t *Tgbot) randomString(length int, charset string) string { + bytes := make([]byte, length) + for i := range bytes { + randomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + bytes[i] = charset[randomIndex.Int64()] + } + return string(bytes) +} + + +func (t *Tgbot) handleCallbackQuery(ctx *th.Context, cq telego.CallbackQuery) error { + // 1) 确保 Message 可访问 —— 注意必须调用 cq.Message.Message() 而不是直接访问 .Message + if cq.Message == nil || cq.Message.Message == nil { + _ = ctx.Bot().AnswerCallbackQuery(ctx, tu.CallbackQuery(cq.ID).WithText("消息对象不存在")) + return nil + } + + // 关键修正:这里要调用方法 Message() + msg := cq.Message.Message() // <- 调用方法,返回 *telego.Message + // 现在 msg 是 *telego.Message,可以访问 Chat / MessageID + chatIDInt64 := msg.Chat.ID + messageID := msg.MessageID + + // 解码回调数据(沿用你已有函数) + data, err := t.decodeQuery(cq.Data) + if err != nil { + _ = ctx.Bot().AnswerCallbackQuery(ctx, tu.CallbackQuery(cq.ID).WithText("回调数据解析失败")) + return nil + } + + // 移除内联键盘(telegoutil 构造 params) + if _, err := ctx.Bot().EditMessageReplyMarkup(ctx, tu.EditMessageReplyMarkup(tu.ID(chatIDInt64), messageID, nil)); err != nil { + logger.Warningf("TG Bot: 移除内联键盘失败: %v", err) + } + + // ---------- oneclick_ 分支 ---------- + if strings.HasPrefix(data, "oneclick_") { + configType := strings.TrimPrefix(data, "oneclick_") + + var creationMessage string + switch configType { + case "reality": + creationMessage = "🚀 Vless + TCP + Reality + Vision" + case "xhttp_reality": + creationMessage = "⚡ Vless + XHTTP + Reality" + case "tls": + creationMessage = "🛡️ Vless Encryption + XHTTP + TLS" + case "switch_vision": // 【新增】: 为占位按钮提供单独的提示 + t.SendMsgToTgbot(chatIDInt64, "此协议组合的功能还在开发中 ............暂不可用...") + _ = ctx.Bot().AnswerCallbackQuery(ctx, tu.CallbackQuery(cq.ID).WithText("开发中...")) + return nil + default: + creationMessage = strings.ToUpper(configType) + } + + // 注意:不要把无返回值函数当作表达式使用,直接调用即可 + t.SendMsgToTgbot(chatIDInt64, fmt.Sprintf("🛠️ 正在为您远程创建 %s 配置,请稍候...", creationMessage)) + _ = ctx.Bot().AnswerCallbackQuery(ctx, tu.CallbackQuery(cq.ID).WithText("配置已创建,请查收管理员私信。")) + return nil + } + + // ---------- confirm_sub_install 分支 ---------- + if data == "confirm_sub_install" { + t.SendMsgToTgbot(chatIDInt64, "🛠️ **已接收到订阅转换安装指令,** 后台正在异步执行...") + + if err := t.serverService.InstallSubconverter(); err != nil { + // 直接调用发送函数(无返回值) + t.SendMsgToTgbot(chatIDInt64, fmt.Sprintf("❌ **安装指令启动失败:**\n`%v`", err)) + } else { + t.SendMsgToTgbot(chatIDInt64, "✅ **安装指令已成功发送到后台。**\n\n请等待安装完成的管理员通知。") + } + + _ = ctx.Bot().AnswerCallbackQuery(ctx, tu.CallbackQuery(cq.ID)) + return nil + } + + // 默认回答,避免用户界面卡住 + _ = ctx.Bot().AnswerCallbackQuery(ctx, tu.CallbackQuery(cq.ID).WithText("操作已完成。")) + return nil +} + +// 新增一个公共方法 (大写 G) 来包装私有方法 +func (t *Tgbot) GetDomain() (string, error) { + return t.getDomain() +} + +// openPortWithUFW 检查/安装 ufw,放行一系列默认端口,并放行指定的端口 +func (t *Tgbot) openPortWithUFW(port int) error { + // 【中文注释】: 将所有 Shell 逻辑整合为一个命令。 + // 新增了对默认端口列表 (22, 80, 443, 13688, 8443) 的放行逻辑。 + shellCommand := fmt.Sprintf(` + # 定义需要放行的指定端口和一系列默认端口 + PORT_TO_OPEN=%d + DEFAULT_PORTS="22 80 443 13688 8443" + + echo "脚本开始:准备配置 ufw 防火墙..." + + # 1. 检查/安装 ufw + if ! command -v ufw &> /dev/null; then + echo "ufw 防火墙未安装,正在自动安装..." + # 使用绝对路径执行 apt-get,避免 PATH 问题,并抑制不必要的输出 + DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -qq >/dev/null + DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install -y -qq ufw >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 安装失败。"; exit 1; fi + echo "✅ ufw 安装成功。" + fi + + # 2. 【新增】循环放行所有默认端口 + echo "正在检查并放行基础服务端口: $DEFAULT_PORTS" + for p in $DEFAULT_PORTS; do + # 使用静默模式检查规则是否存在,如果不存在则添加 + if ! ufw status | grep -qw "$p/tcp"; then + echo "端口 $p/tcp 未放行,正在执行 ufw allow $p/tcp..." + ufw allow $p/tcp >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 端口 $p 放行失败。"; exit 1; fi + else + echo "端口 $p/tcp 规则已存在,跳过。" + fi + done + echo "✅ 基础服务端口检查/放行完毕。" + + # 3. 放行指定的端口 + echo "正在为当前【入站配置】放行指定端口 $PORT_TO_OPEN..." + if ! ufw status | grep -qw "$PORT_TO_OPEN/tcp"; then + ufw allow $PORT_TO_OPEN/tcp >/dev/null + if [ $? -ne 0 ]; then echo "❌ ufw 端口 $PORT_TO_OPEN 放行失败。"; exit 1; fi + echo "✅ 端口 $PORT_TO_OPEN 已成功放行。" + else + echo "端口 $PORT_TO_OPEN 规则已存在,跳过。" + fi + + + # 4. 检查/激活防火墙 + if ! ufw status | grep -q "Status: active"; then + echo "ufw 状态:未激活。正在强制激活..." + # --force 选项可以无需交互直接激活 + ufw --force enable + if [ $? -ne 0 ]; then echo "❌ ufw 激活失败。"; exit 1; fi + echo "✅ ufw 已成功激活。" + else + echo "ufw 状态已经是激活状态。" + fi + + echo "🎉 所有防火墙配置已完成。" + + `, port) // 将函数传入的 port 参数填充到 Shell 脚本中 + + // 使用 exec.CommandContext 运行完整的 shell 脚本 + cmd := exec.CommandContext(context.Background(), "/bin/bash", "-c", shellCommand) + + // 捕获命令的标准输出和标准错误 + output, err := cmd.CombinedOutput() + + // 无论成功与否,都记录完整的 Shell 执行日志,便于调试 + logOutput := string(output) + logger.Infof("执行 ufw 端口放行脚本(目标端口 %d)的完整输出:\n%s", port, logOutput) + + if err != nil { + // 如果脚本执行出错 (例如 exit 1),则返回包含详细输出的错误信息 + return fmt.Errorf("执行 ufw 端口放行脚本时发生错误: %v, Shell 输出: %s", err, logOutput) + } + + return nil +} + +// ========================================================================================= +// 【核心数据结构:XML 解析专用】 +// ========================================================================================= + +// 〔中文注释〕: 内部通用的新闻数据结构,用于避免类型不匹配错误。 +type NewsItem struct { + Title string + Description string // 用于链接或 GitHub 描述 +} + +// 用于解析 Google News 或通用 RSS 格式 +type RssFeed struct { + XMLName xml.Name `xml:"rss"` + Channel RssChannel `xml:"channel"` +} + +type RssChannel struct { + Title string `xml:"title"` + Items []RssItem `xml:"item"` +} + +type RssItem struct { + Title string `xml:"title"` + Link string `xml:"link"` +} + +// 用于解析 YouTube 官方 Atom Feed 格式 +type AtomFeed struct { + XMLName xml.Name `xml:"feed"` + Entries []AtomEntry `xml:"entry"` +} + +type AtomEntry struct { + Title string `xml:"title"` + Link struct { + Href string `xml:"href,attr"` + } `xml:"link"` +} + +// 〔中文注释〕: 内部辅助函数:生成一个安全的随机数。 +func safeRandomInt(max int) int { + if max <= 0 { + return 0 + } + result, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + return time.Now().Nanosecond() % max + } + return int(result.Int64()) +} + +// ========================================================================================= +// 【辅助函数:每日一语】 (最终修复:严格遵循官方文档 Token 机制,增强健壮性) +// ========================================================================================= + +// 〔中文注释〕: 辅助函数:获取完整的古诗词。严格遵循官方 Token 文档,确保稳定性。 +func (t *Tgbot) getDailyVerse() (string, error) { + client := &http.Client{Timeout: 8 * time.Second} + + // 1. 获取 Token + tokenResp, err := client.Get("https://v2.jinrishici.com/token") + if err != nil { + return "", fmt.Errorf("步骤 1: 请求 Token API 失败: %v", err) + } + defer tokenResp.Body.Close() + + tokenBody, err := ioutil.ReadAll(tokenResp.Body) + if err != nil { + return "", fmt.Errorf("步骤 1: 读取 Token 响应失败: %v", err) + } + + var tokenResult struct { + Status string `json:"status"` + Token string `json:"data"` + } + + if json.Unmarshal(tokenBody, &tokenResult) != nil || tokenResult.Status != "success" || tokenResult.Token == "" { + return "", fmt.Errorf("步骤 1: 解析 Token JSON 失败或状态异常: %s", string(tokenBody)) + } + + // 2. 使用 Token 获取诗句 + sentenceURL := "https://v2.jinrishici.com/sentence" // 简化 URL + req, err := http.NewRequest("GET", sentenceURL, nil) + if err != nil { + return "", fmt.Errorf("步骤 2: 创建请求失败: %v", err) + } + // 严格按照文档,将 Token 放在 X-User-Token Header 中 + req.Header.Add("X-User-Token", tokenResult.Token) + // 增加 User-Agent 伪装成浏览器请求 + req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + + sentenceResp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("步骤 2: 请求诗句 API 失败: %v", err) + } + defer sentenceResp.Body.Close() + + sentenceBody, err := ioutil.ReadAll(sentenceResp.Body) + if err != nil { + return "", fmt.Errorf("步骤 2: 读取诗句响应失败: %v", err) + } + + var result struct { + Status string `json:"status"` + Data struct { + Content string `json:"content"` + Origin struct { + Title string `json:"title"` + Author string `json:"author"` + } `json:"origin"` + } `json:"data"` + } + + if json.Unmarshal(sentenceBody, &result) != nil || result.Status != "success" || result.Data.Content == "" { + // 如果失败,记录完整的 JSON 响应,便于调试 + return "", fmt.Errorf("步骤 2: 解析诗句 JSON 失败或内容为空。返回状态码: %d, 响应体: %s", sentenceResp.StatusCode, string(sentenceBody)) + } + + poemContent := strings.ReplaceAll(result.Data.Content, ",", ",\n") + return fmt.Sprintf("📜 **【每日一语】**\n\n%s\n\n`—— %s ·《%s》`", poemContent, result.Data.Origin.Author, result.Data.Origin.Title), nil +} + +// ========================================================================================= +// 【辅助函数:图片发送】 (随机打乱 + 冗余尝试 + 播种修复) +// ========================================================================================= + +// 〔中文注释〕: 【最终重构】图片发送函数:按随机顺序尝试3个不同的图片源。 +func (t *Tgbot) sendRandomImageWithFallback() { + + // 强制使用动态种子,确保每次调用时随机序列都不同 + r := rng.New(rng.NewSource(time.Now().UnixNano())) + + // 定义所有可用的图片源及其标题 + imageSources := []struct { + Name string + API string + Caption string + }{ + { + Name: "waifu.pics (动漫/科技)", + API: "https://api.waifu.pics/sfw/waifu", + Caption: "🖼️ **【今日美图】**\n(来源:waifu.pics 动漫)", + }, + { + Name: "Picsum Photos (唯美风景)", + // Picsum 获取图片列表,随机选择一张。r.Intn(10)+1 用于随机选择页码。 + API: fmt.Sprintf("https://picsum.photos/v2/list?page=%d&limit=100", r.Intn(10)+1), + Caption: "🏞️ **【今日美图】**\n(来源:Picsum Photos 唯美风景)", + }, + { + Name: "Bing 每日图片 (高清/自然)", + API: "https://api.adicw.cn/api/images/bing", + Caption: "🌄 **【今日美图】**\n(来源:Bing 每日图片)", + }, + } + + // 随机打乱数组顺序 + sourceCount := len(imageSources) + for i := sourceCount - 1; i > 0; i-- { + j := r.Intn(i + 1) + imageSources[i], imageSources[j] = imageSources[j], imageSources[i] + } + + var imageURL string + var caption string + var found bool + + // 逐个尝试所有来源,直到成功 + for i, source := range imageSources { + logger.Infof("图片获取:开始尝试来源 (随机顺序 [%d/%d]): %s", i+1, len(imageSources), source.Name) + + tempURL, err := t.fetchImageFromAPI(source.API, source.Name) + + if err == nil && tempURL != "" { + imageURL = tempURL + caption = source.Caption + found = true + // 日志直接使用 source.Name + logger.Infof("图片获取:来源 [%s] 成功,URL: %s", source.Name, imageURL) + break // 找到一个成功的就退出循环 + } + logger.Warningf("图片来源 [%s] 尝试失败: %v", source.Name, err) + } + + if !found { + logger.Warning("所有图片来源均失败,跳过图片发送。") + return + } + + // --- SEND_IMAGE 逻辑 --- + // 假设 bot 和 adminIds 是可用的全局或结构体变量 + for _, adminId := range adminIds { + photo := tu.Photo( + tu.ID(adminId), + tu.FileFromURL(imageURL), + ).WithCaption(caption).WithParseMode(telego.ModeMarkdown) + + _, err := bot.SendPhoto(context.Background(), photo) + if err != nil { + logger.Warningf("发送图片给管理员 %d 失败: %v", adminId, err) + } + time.Sleep(300 * time.Millisecond) + } +} + +// ========================================================================================= +// 【新的辅助函数:封装图片获取逻辑】 (用于清理 sendRandomImageWithFallback 函数体) +// ========================================================================================= + +// 〔中文注释〕: 辅助函数:根据不同的 API 逻辑获取图片 URL。 +func (t *Tgbot) fetchImageFromAPI(apiURL string, sourceName string) (string, error) { + client := &http.Client{ + Timeout: 15 * time.Second, + // 确保 client 遵循重定向 + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return nil + }, + } + + // 伪装 User-Agent + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return "", err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") + + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound && resp.StatusCode != http.StatusSeeOther { + return "", fmt.Errorf("API 返回非 200/302 状态码: %d", resp.StatusCode) + } + + if strings.Contains(sourceName, "waifu.pics") { + // waifu.pics (JSON API) + body, _ := ioutil.ReadAll(resp.Body) + var res struct{ URL string `json:"url"` } + if json.Unmarshal(body, &res) == nil && res.URL != "" { + return res.URL, nil + } + return "", errors.New("waifu.pics JSON 解析失败") + } else if strings.Contains(sourceName, "Picsum Photos") { + // Picsum Photos (列表 JSON API) + body, _ := ioutil.ReadAll(resp.Body) + var list []struct{ ID string `json:"id"` } + if json.Unmarshal(body, &list) == nil && len(list) > 0 { + // 这里我们不能使用 safeRandomInt,因为 safeRandomInt 也在依赖 rng + // 我们需要使用一个新的随机源或者将 r 传入 + // 为了简化,这里直接返回一个固定的格式化URL,让用户看到 Picsum 的图 + return fmt.Sprintf("https://picsum.photos/id/%s/1024/768", list[0].ID), nil + } + return "", errors.New("Picsum Photos 列表解析失败或列表为空") + } else if strings.Contains(sourceName, "Bing 每日图片") { + // Bing 每日图片 (重定向或直接图片 URL) + // 检查是否有重定向(例如 Unsplash, Bing) + if resp.Request.URL.String() != apiURL { + return resp.Request.URL.String(), nil + } + // 如果 API 返回的是 200,但其响应体内容就是图片数据, + // 我们可以返回原始 URL,让 Telegram 自己处理。 + return apiURL, nil + } + + return "", errors.New("未知图片源处理逻辑") +} + +// ========================================================================================= +// 【辅助函数:新闻资讯核心抓取逻辑】 (已重构,逻辑更清晰) +// ========================================================================================= + +// 【中文注释】: 新闻源的数据结构,增加 Type 字段用于区分解析方式 +type NewsSource struct { + Name string + API string + Type string // "RSS2JSON" 或 "DirectJSON" +} + + +// 〔中文注释〕: 辅助函数:核心逻辑,从给定的 API 获取新闻简报。 +// 此函数现在依赖传入的 source.Type 来决定如何解析数据,不再使用模糊的字符串匹配。 +func fetchNewsFromGlobalAPI(source NewsSource, limit int) (string, error) { + client := &http.Client{Timeout: 15 * time.Second} + var newsItems []NewsItem + var err error + + // --- 步骤 1: 发起网络请求 --- + req, reqErr := http.NewRequest("GET", source.API, nil) + if reqErr != nil { + return "", fmt.Errorf("创建请求失败: %v", reqErr) + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + + resp, respErr := client.Do(req) + if respErr != nil { + return "", fmt.Errorf("请求 %s API 失败: %v", source.Name, respErr) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("请求 %s API 返回非 200 状态码: %d", source.Name, resp.StatusCode) + } + + body, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + return "", fmt.Errorf("读取 %s 响应失败: %v", source.Name, readErr) + } + + // --- 步骤 2: 根据来源类型解析响应 --- + switch source.Type { + case "RSS2JSON": + // 【修复】: 专门处理来自 api.rss2json.com 的数据,适用于 YouTube, Google News 和新的币圈新闻源 + var result struct { + Status string `json:"status"` + Items []struct { + Title string `json:"title"` + Link string `json:"link"` + } `json:"items"` + } + if jsonErr := json.Unmarshal(body, &result); jsonErr == nil && result.Status == "ok" { + for _, item := range result.Items { + newsItems = append(newsItems, NewsItem{ + Title: item.Title, + Description: item.Link, + }) + } + } else { + err = fmt.Errorf("解析 %s 的 RSS2JSON 响应失败: %v。响应体: %s", source.Name, jsonErr, string(body)) + } + + case "DirectJSON": + // 【保留】: 处理直接返回 JSON 的 API,例如 GitHub Trending + if strings.Contains(source.Name, "GitHub") { + var result []struct { + RepoName string `json:"repo_name"` + Desc string `json:"desc"` + } + if jsonErr := json.Unmarshal(body, &result); jsonErr == nil { + for _, item := range result { + newsItems = append(newsItems, NewsItem{ + Title: fmt.Sprintf("⭐ %s", item.RepoName), + Description: item.Desc, + }) + } + } else { + err = fmt.Errorf("解析 GitHub Trending JSON 失败: %v", jsonErr) + } + } + // 这里可以为其他 DirectJSON 类型的源添加更多的 else if + default: + err = fmt.Errorf("未知的源类型: %s", source.Type) + } + + if err != nil { + return "", err + } + + if len(newsItems) == 0 { + return "", errors.New(source.Name + " 简报内容为空") + } + + // --- 步骤 3: 最终消息构建 --- + var builder strings.Builder + builder.WriteString(fmt.Sprintf("📰 **【%s 简报】**\n", source.Name)) + + for i, item := range newsItems { + if i >= limit { + break + } + if item.Title != "" { + // 移除 RSS 源标题中可能包含的来源信息,让内容更整洁 + cleanTitle := strings.ReplaceAll(item.Title, " - YouTube", "") + // 移除 HTML 标签(RSS/Atom Title中常见) + cleanTitle = regexp.MustCompile("<[^>]*>").ReplaceAllString(cleanTitle, "") + // 对 Google News 的标题做特殊清理 + if strings.Contains(source.Name, "Google News") { + parts := strings.Split(cleanTitle, " - ") + if len(parts) > 1 { + cleanTitle = strings.Join(parts[:len(parts)-1], " - ") + } + } + + // 【排版修复】: 使用 \n%d. %s 开始新的一条新闻 + builder.WriteString(fmt.Sprintf("\n%d. %s", i+1, cleanTitle)) + + // 链接/描述只有在特定源时才显示 + if item.Description != "" && (source.Type == "RSS2JSON" || strings.Contains(source.Name, "GitHub")) { + builder.WriteString(fmt.Sprintf("\n `%s`", item.Description)) + } + + // 【排版修复】: 在每条新闻项的末尾添加额外的空行,确保分隔清晰 + builder.WriteString("\n") + } + } + + return builder.String(), nil +} + + +// ========================================================================================= +// 【核心函数:getNewsBriefingWithFallback】 (已重构,确保随机性和来源有效性) +// ========================================================================================= + +// 〔中文注释〕: 【最终重构】新闻资讯获取函数:随机排列源并逐个尝试,直到成功或全部失败。 +func (t *Tgbot) getNewsBriefingWithFallback() (string, error) { + + // 强制使用动态种子,确保每次调用时随机序列都不同 + r := rng.New(rng.NewSource(time.Now().UnixNano())) + + // Google News 的 URL 计算 + rssQueryGoogle := url.QueryEscape("AI 科技 OR 区块链 OR IT OR 国际时事") + rssURLGoogle := fmt.Sprintf("https://news.google.com/rss/search?q=%s&hl=zh-CN&gl=CN", rssQueryGoogle) + + // 【修复】: 定义所有可用的新闻源,并明确指定其 Type + newsSources := []NewsSource{ + { + Name: "YouTube 中文热搜 (AI/IT/科技)", + API: fmt.Sprintf("https://api.rss2json.com/v1/api.json?rss_url=%s", url.QueryEscape("https://www.youtube.com/feeds/videos.xml?channel_id=UCaT8sendP_s_U4L_D3q_V-g")), // 使用一个科技频道的Feed作为示例 + Type: "RSS2JSON", + }, + { + Name: "Google News 中文资讯", + API: fmt.Sprintf("https://api.rss2json.com/v1/api.json?rss_url=%s", url.QueryEscape(rssURLGoogle)), + Type: "RSS2JSON", + }, + { + Name: "币圈头条 (Cointelegraph)", + // 【修复】: 替换了失效的 coinmarketcap.cn API,改用更稳定的 Cointelegraph 中文 RSS Feed + API: fmt.Sprintf("https://api.rss2json.com/v1/api.json?rss_url=%s", url.QueryEscape("https://cointelegraph.com/rss/category/china")), + Type: "RSS2JSON", + }, + } + + // 解决 rand.Shuffle 兼容性问题:手动实现 Fisher-Yates 洗牌算法 + sourceCount := len(newsSources) + + // 执行洗牌 (使用前面初始化的 r) + for i := sourceCount - 1; i > 0; i-- { + // 在 [0, i] 范围内随机选择一个索引 + j := r.Intn(i + 1) + // 交换元素 + newsSources[i], newsSources[j] = newsSources[j], newsSources[i] + } + + // 逐个尝试所有来源,直到成功 + for i, source := range newsSources { + logger.Infof("新闻资讯:开始尝试来源 (随机顺序 [%d/%d]): %s", i+1, len(newsSources), source.Name) + + // 调用核心抓取逻辑 + newsMsg, err := fetchNewsFromGlobalAPI(source, 5) // 直接传递 source 结构体 + + if err == nil && newsMsg != "" { + // 成功获取到内容 + logger.Infof("新闻资讯:来源 [%s] 成功获取内容。", source.Name) + return newsMsg, nil + } + + // 失败,记录警告,继续尝试下一个 + logger.Warningf("新闻资讯来源 [%s] 尝试失败: %v", source.Name, err) + } + + // 所有来源都失败,返回一个友好的错误信息 + return "", errors.New("所有新闻来源均获取失败,请检查网络或 API 状态") +} + +// 【新增的辅助函数】: 发送贴纸到指定的聊天 ID,并返回消息对象(用于获取 ID) +func (t *Tgbot) SendStickerToTgbot(chatId int64, fileId string) (*telego.Message, error) { + // 必须使用 SendStickerParams 结构体,并传入 context + params := telego.SendStickerParams{ + ChatID: tu.ID(chatId), + // 对于现有 File ID 字符串,必须封装在 telego.InputFile 结构中。 + Sticker: telego.InputFile{FileID: fileId}, + } + + // 使用全局变量 bot 调用 SendSticker,并传入 context.Background() 和参数指针 + msg, err := bot.SendSticker(context.Background(), ¶ms) + + if err != nil { + logger.Errorf("发送贴纸失败到聊天 ID %d: %v", chatId, err) + return nil, err + } + + // 成功返回 *telego.Message 对象 + return msg, nil +} diff --git a/web/service/user.go b/web/service/user.go new file mode 100644 index 0000000..ff5b853 --- /dev/null +++ b/web/service/user.go @@ -0,0 +1,123 @@ +package service + +import ( + "errors" + + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/crypto" + + "github.com/xlzd/gotp" + "gorm.io/gorm" +) + +type UserService struct { + settingService SettingService +} + +func (s *UserService) GetFirstUser() (*model.User, error) { + db := database.GetDB() + + user := &model.User{} + err := db.Model(model.User{}). + First(user). + Error + if err != nil { + return nil, err + } + return user, nil +} + +func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User { + db := database.GetDB() + + user := &model.User{} + + err := db.Model(model.User{}). + Where("username = ?", username). + First(user). + Error + if err == gorm.ErrRecordNotFound { + return nil + } else if err != nil { + logger.Warning("check user err:", err) + return nil + } + + if !crypto.CheckPasswordHash(user.Password, password) { + return nil + } + + twoFactorEnable, err := s.settingService.GetTwoFactorEnable() + if err != nil { + logger.Warning("check two factor err:", err) + return nil + } + + if twoFactorEnable { + twoFactorToken, err := s.settingService.GetTwoFactorToken() + + if err != nil { + logger.Warning("check two factor token err:", err) + return nil + } + + if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { + return nil + } + } + + return user +} + +func (s *UserService) UpdateUser(id int, username string, password string) error { + db := database.GetDB() + hashedPassword, err := crypto.HashPasswordAsBcrypt(password) + + if err != nil { + return err + } + + twoFactorEnable, err := s.settingService.GetTwoFactorEnable() + if err != nil { + return err + } + + if twoFactorEnable { + s.settingService.SetTwoFactorEnable(false) + s.settingService.SetTwoFactorToken("") + } + + return db.Model(model.User{}). + Where("id = ?", id). + Updates(map[string]any{"username": username, "password": hashedPassword}). + Error +} + +func (s *UserService) UpdateFirstUser(username string, password string) error { + if username == "" { + return errors.New("username can not be empty") + } else if password == "" { + return errors.New("password can not be empty") + } + hashedPassword, er := crypto.HashPasswordAsBcrypt(password) + + if er != nil { + return er + } + + db := database.GetDB() + user := &model.User{} + err := db.Model(model.User{}).First(user).Error + if database.IsNotFound(err) { + user.Username = username + user.Password = hashedPassword + return db.Model(model.User{}).Create(user).Error + } else if err != nil { + return err + } + user.Username = username + user.Password = hashedPassword + return db.Save(user).Error +} diff --git a/web/service/warp.go b/web/service/warp.go new file mode 100644 index 0000000..0ed8bee --- /dev/null +++ b/web/service/warp.go @@ -0,0 +1,170 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + "x-ui/logger" + "x-ui/util/common" +) + +type WarpService struct { + SettingService +} + +func (s *WarpService) GetWarpData() (string, error) { + warp, err := s.SettingService.GetWarp() + if err != nil { + return "", err + } + return warp, nil +} + +func (s *WarpService) DelWarpData() error { + err := s.SettingService.SetWarp("") + if err != nil { + return err + } + return nil +} + +func (s *WarpService) GetWarpConfig() (string, error) { + var warpData map[string]string + warp, err := s.SettingService.GetWarp() + if err != nil { + return "", err + } + err = json.Unmarshal([]byte(warp), &warpData) + if err != nil { + return "", err + } + + url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"]) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + buffer := &bytes.Buffer{} + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + return buffer.String(), nil +} + +func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) { + tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") + hostName, _ := os.Hostname() + data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName) + + url := "https://api.cloudflareclient.com/v0a2158/reg" + + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) + if err != nil { + return "", err + } + + req.Header.Add("CF-Client-Version", "a-7.21-0721") + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + buffer := &bytes.Buffer{} + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + var rspData map[string]any + err = json.Unmarshal(buffer.Bytes(), &rspData) + if err != nil { + return "", err + } + + deviceId := rspData["id"].(string) + token := rspData["token"].(string) + license, ok := rspData["account"].(map[string]any)["license"].(string) + if !ok { + logger.Debug("Error accessing license value.") + return "", err + } + + warpData := fmt.Sprintf("{\n \"access_token\": \"%s\",\n \"device_id\": \"%s\",", token, deviceId) + warpData += fmt.Sprintf("\n \"license_key\": \"%s\",\n \"private_key\": \"%s\"\n}", license, secretKey) + + s.SettingService.SetWarp(warpData) + + result := fmt.Sprintf("{\n \"data\": %s,\n \"config\": %s\n}", warpData, buffer.String()) + + return result, nil +} + +func (s *WarpService) SetWarpLicense(license string) (string, error) { + var warpData map[string]string + warp, err := s.SettingService.GetWarp() + if err != nil { + return "", err + } + err = json.Unmarshal([]byte(warp), &warpData) + if err != nil { + return "", err + } + + url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"]) + data := fmt.Sprintf(`{"license": "%s"}`, license) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data))) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + buffer := &bytes.Buffer{} + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + var response map[string]any + err = json.Unmarshal(buffer.Bytes(), &response) + if err != nil { + return "", err + } + if response["success"] == false { + errorArr, _ := response["errors"].([]any) + errorObj := errorArr[0].(map[string]any) + return "", common.NewError(errorObj["code"], errorObj["message"]) + } + + warpData["license_key"] = license + newWarpData, err := json.MarshalIndent(warpData, "", " ") + if err != nil { + return "", err + } + s.SettingService.SetWarp(string(newWarpData)) + + return string(newWarpData), nil +} diff --git a/web/service/xray.go b/web/service/xray.go new file mode 100644 index 0000000..be5edef --- /dev/null +++ b/web/service/xray.go @@ -0,0 +1,481 @@ +package service + +import ( + "encoding/json" + "errors" + "runtime" + "sync" + "strconv" + + "x-ui/logger" + "x-ui/xray" + json_util "x-ui/util/json_util" + + "go.uber.org/atomic" +) + +var ( + p *xray.Process + lock sync.Mutex + isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray + isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel + result string +) + +type XrayService struct { + inboundService InboundService + settingService SettingService + xrayAPI xray.XrayAPI +} + +// SetXrayAPI 用于从外部注入 XrayAPI 实例 +func (s *XrayService) SetXrayAPI(api xray.XrayAPI) { + s.xrayAPI = api +} + +// IsXrayRunning 检查 Xray 是否正在运行 +func (s *XrayService) IsXrayRunning() bool { + return p != nil && p.IsRunning() +} + +// 中文注释: +// 新增 GetApiPort 函数。 +// 这个函数的作用是安全地返回当前 Xray 进程正在监听的 API 端口号。 +// 如果 Xray 没有运行 (p == nil),则返回 0。 +// 我们的后台任务将调用这个函数来获取端口号。 +func (s *XrayService) GetApiPort() int { + if p == nil { + return 0 + } + return p.GetAPIPort() +} + + +func (s *XrayService) GetXrayErr() error { + if p == nil { + return nil + } + + err := p.GetErr() + + if runtime.GOOS == "windows" && err.Error() == "exit status 1" { + // exit status 1 on Windows means that Xray process was killed + // as we kill process to stop in on Windows, this is not an error + return nil + } + + return err +} + +func (s *XrayService) GetXrayResult() string { + if result != "" { + return result + } + if s.IsXrayRunning() { + return "" + } + if p == nil { + return "" + } + + result = p.GetResult() + + if runtime.GOOS == "windows" && result == "exit status 1" { + // exit status 1 on Windows means that Xray process was killed + // as we kill process to stop in on Windows, this is not an error + return "" + } + + return result +} + +func (s *XrayService) GetXrayVersion() string { + if p == nil { + return "Unknown" + } + return p.GetVersion() +} + +func RemoveIndex(s []any, index int) []any { + return append(s[:index], s[index+1:]...) +} + +func (s *XrayService) GetXrayConfig() (*xray.Config, error) { + templateConfig, err := s.settingService.GetXrayConfigTemplate() + if err != nil { + return nil, err + } + + xrayConfig := &xray.Config{} + if err := json.Unmarshal([]byte(templateConfig), xrayConfig); err != nil { + return nil, err + } + + + inbounds, err := s.inboundService.GetAllInbounds() + if err != nil { + return nil, err + } + + // ================================================================= + // 中文注释: 动态限速核心逻辑 - 第一步: 收集所有限速值 + // ================================================================= + // 创建一个 map 用于存储所有出现过的、不为0的限速值 + uniqueSpeeds := make(map[int]bool) + for _, inbound := range inbounds { + if !inbound.Enable { + continue + } + + // 获取该入站下的所有客户端设置 + dbClients, _ := s.inboundService.GetClients(inbound) + for _, dbClient := range dbClients { + if dbClient.SpeedLimit > 0 { + uniqueSpeeds[dbClient.SpeedLimit] = true + } + } + } + + // ================================================================= + // 中文注释: 动态限速核心逻辑 - 第二步: 根据收集到的限速值,动态生成 Policy Levels + // ================================================================= + + // 1. 先从模板中解析出已有的 policy 对象 + var finalPolicy map[string]interface{} + if xrayConfig.Policy != nil { + if err := json.Unmarshal(xrayConfig.Policy, &finalPolicy); err != nil { + logger.Warningf("无法解析模板中的 policy: %v", err) + finalPolicy = make(map[string]interface{}) + } + } else { + finalPolicy = make(map[string]interface{}) + } + + // 2. 初始化 policy levels,获取或创建 policy中的 levels map + var policyLevels map[string]interface{} + if levels, ok := finalPolicy["levels"].(map[string]interface{}); ok { + policyLevels = levels + } else { + policyLevels = make(map[string]interface{}) + } + + // 3. 〔重要修改〕: 确保 level 0 策略的完整性,这是让设备限制和默认用户统计生效的关键 + var level0 map[string]interface{} + if l0, ok := policyLevels["0"].(map[string]interface{}); ok { + // 〔中文注释〕: 如果模板中已存在 level 0,使用它作为基础进行修改。 + level0 = l0 + } else { + // 〔中文注释〕: 如果模板中不存在,则创建一个全新的 map。 + level0 = make(map[string]interface{}) + } + // 〔中文注释〕: 无论 level 0 是否存在,都为其补充或覆盖以下关键参数。 + // handshake 和 connIdle 是激活 Xray 连接统计的前提, + // uplinkOnly 和 downlinkOnly 设置为 0 代表不限速,这是 level 0 用户的默认行为。 + // statsUserUplink 和 statsUserDownlink 确保用户的流量能够被统计。 + level0["handshake"] = 4 + level0["connIdle"] = 300 + level0["uplinkOnly"] = 0 + level0["downlinkOnly"] = 0 + level0["statsUserUplink"] = true + level0["statsUserDownlink"] = true + // 〔新增〕: 增加此关键选项以启用 Xray-core 的在线 IP 统计功能。 + // 这是让【设备限制】功能正常工作的前提。 + level0["statsUserOnline"] = true + + // 〔中文注释〕: 将完整配置好的 level 0 写回 policyLevels,确保最终生成的 config.json 是正确的。 + policyLevels["0"] = level0 + + // 4. 遍历所有收集到的限速值,为每个独立的限速值创建对应的 level + for speed := range uniqueSpeeds { + // 为每个速率创建一个 level,level 的名字就是速率的字符串形式 + // 例如,速率 1024 KB/s 对应 level "1024" + policyLevels[strconv.Itoa(speed)] = map[string]interface{}{ + "downlinkOnly": speed, + "uplinkOnly": speed, + "handshake": 4, + "connIdle": 300, + "statsUserUplink": true, + "statsUserDownlink": true, + "statsUserOnline": true, + } + } + + // 5. 将修改后的 levels 写回 policy 对象,并序列化回 xrayConfig.Policy,将生成的 policy 应用到 Xray 配置中 + finalPolicy["levels"] = policyLevels + policyJSON, err := json.Marshal(finalPolicy) + if err != nil { + return nil, err + } + xrayConfig.Policy = json_util.RawMessage(policyJSON) + + // ================================================================= + // 中文注释: 在这里增加日志,打印最终生成的限速策略 + // ================================================================= + if len(uniqueSpeeds) > 0 { + finalPolicyLog, _ := json.Marshal(policyLevels) + logger.Infof("已为Xray动态生成〔限速策略〕: %s", string(finalPolicyLog)) + } + + // ================================================================= + // 中文注释: 动态限速核心逻辑 - 第三步: 为设置了限速的用户分配对应的 Level,逐个 inbound 构建 inboundConfig + // ================================================================= + // 触发一次空调用以处理可能的残留任务 + s.inboundService.AddTraffic(nil, nil) + + for _, inbound := range inbounds { + if !inbound.Enable { + continue + } + + // 先生成一个 inboundConfig(后面会覆盖 Settings/StreamSettings) + inboundConfig := inbound.GenXrayInboundConfig() + + // 从 DB clients 建立 email/id -> speedLimit 映射(优先使用 DB 的值) + speedByEmail := make(map[string]int) + speedById := make(map[string]int) + dbClients, _ := s.inboundService.GetClients(inbound) + for _, dbc := range dbClients { + if dbc.Email != "" { + speedByEmail[dbc.Email] = dbc.SpeedLimit + } + // 如果有 id 字段也建立映射(以防 email 不存在) + if dbc.ID != "" { + speedById[dbc.ID] = dbc.SpeedLimit + } + } + + // 解析 inbound.Settings + var settings map[string]interface{} + if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil { + logger.Warningf("无法解析 inbound.Settings (inbound %d): %v ,跳过该入站", inbound.Id, err) + continue + } + + originalClients, ok := settings["clients"].([]interface{}) + if ok { + clientStats := inbound.ClientStats + + var xrayClients []interface{} + for _, clientRaw := range originalClients { + c, ok := clientRaw.(map[string]interface{}) + if !ok { + continue + } + + // ----------------------------------------------------------------- + // 中文注释: 用户过滤 - 1) settings 中的 enable 字段检查 + // ----------------------------------------------------------------- + if en, ok := c["enable"].(bool); ok && !en { + if em, _ := c["email"].(string); em != "" { + logger.Infof("已从Xray配置中移除被settings标记为禁用的用户: %s", em) + } + continue + } + + // ----------------------------------------------------------------- + // 中文注释: 用户过滤 - 2) inbound.ClientStats 检查 (DB/流量层禁用) + // ----------------------------------------------------------------- + email, _ := c["email"].(string) + idStr, _ := c["id"].(string) + disabledByStat := false + for _, stat := range clientStats { + if stat.Email == email && !stat.Enable { + disabledByStat = true + break + } + } + if disabledByStat { + logger.Infof("已从Xray配置中移除被禁用的用户: %s", email) + continue + } + + // ----------------------------------------------------------------- + // 中文注释: 构建干净的 xrayClient(只保留白名单字段) + // ----------------------------------------------------------------- + xrayClient := make(map[string]interface{}) + if id, ok := c["id"]; ok { xrayClient["id"] = id } + if email != "" { xrayClient["email"] = email } + + // 规范化 flow + if flow, ok := c["flow"]; ok { + if fs, ok2 := flow.(string); ok2 && fs == "xtls-rprx-vision-udp443" { + xrayClient["flow"] = "xtls-rprx-vision" + } else { + xrayClient["flow"] = flow + } + } + if password, ok := c["password"]; ok { xrayClient["password"] = password } + if method, ok := c["method"]; ok { xrayClient["method"] = method } + + // ⚠️ security 字段已移除,不再加入到 xrayClient + + // ----------------------------------------------------------------- + // 中文注释: 限速等级映射(优先 DB,再回退 settings.speedLimit) + // ----------------------------------------------------------------- + + // ================================================================= + // 这里的逻辑是准备将 client 对象提交给 Xray-core。 + // 我们需要将 speedLimit 转换为 Xray 认识的 level 字段。 + // 这样可以确保包含 speedLimit 的完整用户信息被用于生成配置。 + // ================================================================= + level := 0 + if email != "" { + if v, ok := speedByEmail[email]; ok && v > 0 { + level = v + } + } + if level == 0 && idStr != "" { + if v, ok := speedById[idStr]; ok && v > 0 { + level = v + } + } + if level == 0 { + if sl, ok := c["speedLimit"]; ok { + switch vv := sl.(type) { + case float64: + level = int(vv) + case int: + level = vv + case int64: + level = int(vv) + case string: + if n, err := strconv.Atoi(vv); err == nil { + level = n + } + } + } + } + + // 【新增功能】在这里添加日志记录 + // 只有当最终计算出的 level 大于 0,且 email 存在时,才记录日志 + if level > 0 && email != "" { + logger.Infof("为用户 %s 应用〔独立限速〕: %d KB/s", email, level) + } + // ================================================================= + + xrayClient["level"] = level + + xrayClients = append(xrayClients, xrayClient) + } + + // 把纯净的 clients 应用到 settings,并写入 inboundConfig.Settings + settings["clients"] = xrayClients + finalSettingsForXray, err := json.Marshal(settings) + if err != nil { + logger.Warningf("无法序列化用于Xray的入站设置 in GetXrayConfig for inbound %d: %v,跳过该入站", inbound.Id, err) + continue + } + inboundConfig.Settings = json_util.RawMessage(finalSettingsForXray) + } + + // ----------------------------------------------------------------- + // 中文注释: 处理 StreamSettings(清理敏感字段) + // ----------------------------------------------------------------- + if len(inbound.StreamSettings) > 0 { + var stream map[string]interface{} + if err := json.Unmarshal([]byte(inbound.StreamSettings), &stream); err != nil { + logger.Warningf("无法解析 StreamSettings (inbound %d): %v ,跳过该入站", inbound.Id, err) + continue + } + + if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok { + delete(tlsSettings, "settings") + } + if realitySettings, ok := stream["realitySettings"].(map[string]interface{}); ok { + delete(realitySettings, "settings") + } + delete(stream, "externalProxy") + + newStream, err := json.Marshal(stream) + if err != nil { + return nil, err + } + inboundConfig.StreamSettings = json_util.RawMessage(newStream) + } + + xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) + } + + return xrayConfig, nil +} + + +func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) { + if !s.IsXrayRunning() { + err := errors.New("xray is not running") + logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err) + return nil, nil, err + } + apiPort := p.GetAPIPort() + s.xrayAPI.Init(apiPort) + defer s.xrayAPI.Close() + + traffic, clientTraffic, err := s.xrayAPI.GetTraffic(true) + if err != nil { + logger.Debug("Failed to fetch Xray traffic:", err) + return nil, nil, err + } + return traffic, clientTraffic, nil +} + +func (s *XrayService) RestartXray(isForce bool) error { + lock.Lock() + defer lock.Unlock() + logger.Debug("restart Xray, force:", isForce) + isManuallyStopped.Store(false) + + xrayConfig, err := s.GetXrayConfig() + if err != nil { + return err + } + + // 【新功能】重启时,将完整配置打印到 Debug 日志以供验证 + configBytes, jsonErr := json.MarshalIndent(xrayConfig, "", " ") + if jsonErr == nil { + logger.Debugf("使用新配置重启 Xray:\n%s", string(configBytes)) + } else { + logger.Warning("无法将 Xray 配置编组以进行日志记录:", jsonErr) + } + + + if s.IsXrayRunning() { + if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() { + logger.Debug("It does not need to restart Xray") + return nil + } + p.Stop() + } + + p = xray.NewProcess(xrayConfig) + result = "" + err = p.Start() + if err != nil { + return err + } + + return nil +} + +func (s *XrayService) StopXray() error { + lock.Lock() + defer lock.Unlock() + isManuallyStopped.Store(true) + logger.Debug("Attempting to stop Xray...") + if s.IsXrayRunning() { + return p.Stop() + } + return errors.New("xray is not running") +} + +func (s *XrayService) SetToNeedRestart() { + isNeedXrayRestart.Store(true) +} + +func (s *XrayService) IsNeedRestartAndSetFalse() bool { + return isNeedXrayRestart.CompareAndSwap(true, false) +} + +// Check if Xray is not running and wasn't stopped manually, i.e. crashed +func (s *XrayService) DidXrayCrash() bool { + return !s.IsXrayRunning() && !isManuallyStopped.Load() +} diff --git a/web/service/xray_setting.go b/web/service/xray_setting.go new file mode 100644 index 0000000..f497bf8 --- /dev/null +++ b/web/service/xray_setting.go @@ -0,0 +1,29 @@ +package service + +import ( + _ "embed" + "encoding/json" + + "x-ui/util/common" + "x-ui/xray" +) + +type XraySettingService struct { + SettingService +} + +func (s *XraySettingService) SaveXraySetting(newXraySettings string) error { + if err := s.CheckXrayConfig(newXraySettings); err != nil { + return err + } + return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings) +} + +func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { + xrayConfig := &xray.Config{} + err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig) + if err != nil { + return common.NewError("xray template config invalid:", err) + } + return nil +} diff --git a/web/session/session.go b/web/session/session.go new file mode 100644 index 0000000..13aedad --- /dev/null +++ b/web/session/session.go @@ -0,0 +1,65 @@ +package session + +import ( + "encoding/gob" + + "x-ui/database/model" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +const ( + loginUserKey = "LOGIN_USER" + defaultPath = "/" +) + +func init() { + gob.Register(model.User{}) +} + +func SetLoginUser(c *gin.Context, user *model.User) { + if user == nil { + return + } + s := sessions.Default(c) + s.Set(loginUserKey, *user) +} + +func SetMaxAge(c *gin.Context, maxAge int) { + s := sessions.Default(c) + s.Options(sessions.Options{ + Path: defaultPath, + MaxAge: maxAge, + HttpOnly: true, + }) +} + +func GetLoginUser(c *gin.Context) *model.User { + s := sessions.Default(c) + obj := s.Get(loginUserKey) + if obj == nil { + return nil + } + user, ok := obj.(model.User) + if !ok { + + s.Delete(loginUserKey) + return nil + } + return &user +} + +func IsLogin(c *gin.Context) bool { + return GetLoginUser(c) != nil +} + +func ClearSession(c *gin.Context) { + s := sessions.Default(c) + s.Clear() + s.Options(sessions.Options{ + Path: defaultPath, + MaxAge: -1, + HttpOnly: true, + }) +} diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml new file mode 100644 index 0000000..f97b6b5 --- /dev/null +++ b/web/translation/translate.ar_EG.toml @@ -0,0 +1,739 @@ +"username" = "اسم المستخدم" +"password" = "الباسورد" +"login" = "تسجيل الدخول" +"confirm" = "تأكيد" +"cancel" = "إلغاء" +"close" = "إغلاق" +"create" = "إنشاء" +"update" = "تحديث" +"copy" = "نسخ" +"copied" = "اتنسخ" +"download" = "تحميل" +"remark" = "ملاحظة" +"enable" = "مفعل" +"protocol" = "بروتوكول" +"search" = "بحث" +"filter" = "فلترة" +"loading" = "جاري التحميل..." +"second" = "ثانية" +"minute" = "دقيقة" +"hour" = "ساعة" +"day" = "يوم" +"check" = "شيك" +"indefinite" = "غير محدد" +"unlimited" = "غير محدود" +"none" = "مفيش" +"qrCode" = "كود QR" +"info" = "معلومات أكتر" +"edit" = "تعديل" +"delete" = "مسح" +"reset" = "إعادة ضبط" +"noData" = "لا توجد بيانات." +"copySuccess" = "اتنسخ بنجاح" +"sure" = "متأكد؟" +"encryption" = "تشفير" +"useIPv4ForHost" = "استخدم IPv4 للمضيف" +"transmission" = "نقل" +"host" = "المستضيف" +"path" = "مسار" +"camouflage" = "تمويه" +"status" = "الحالة" +"enabled" = "مفعل" +"disabled" = "معطل" +"depleted" = "خلص" +"depletingSoon" = "هينتهي قريب" +"offline" = "أوفلاين" +"online" = "أونلاين" +"domainName" = "اسم الدومين" +"monitor" = "المسمع IP" +"certificate" = "شهادة رقمية" +"fail" = "فشل" +"comment" = "تعليق" +"success" = "تم بنجاح" +"lastOnline" = "آخر متصل" +"getVersion" = "جيب النسخة" +"install" = "تثبيت" +"clients" = "عملاء" +"usage" = "استخدام" +"twoFactorCode" = "الكود" +"remained" = "المتبقي" +"security" = "أمان" +"secAlertTitle" = "تنبيه أمني" +"secAlertSsl" = "الاتصال ده مش آمن. ابعد عن إدخال معلومات حساسة لغاية ما تشغل TLS لحماية البيانات." +"secAlertConf" = "بعض الإعدادات معرضة لهجمات. ينصح بتعزيز بروتوكولات الأمان عشان تمنع الاختراقات المحتملة." +"secAlertSSL" = "البانل مش مؤمن. حمّل شهادة TLS لحماية البيانات." +"secAlertPanelPort" = "بورت البانل الافتراضي معرض للخطر. ياريت تغير لبورت عشوائي أو محدد." +"secAlertPanelURI" = "مسار URI الافتراضي للبانل مش آمن. ياريت تضبط مسار URI معقد." +"secAlertSubURI" = "مسار URI الافتراضي للاشتراك مش آمن. ياريت تضبط مسار URI معقد." +"secAlertSubJsonURI" = "مسار URI الافتراضي لاشتراك JSON مش آمن. ياريت تضبط مسار URI معقد." +"emptyDnsDesc" = "مفيش سيرفر DNS مضاف." +"emptyFakeDnsDesc" = "مفيش سيرفر Fake DNS مضاف." +"emptyBalancersDesc" = "مفيش موازن تحميل مضاف." +"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف." +"somethingWentWrong" = "حدث خطأ ما" + +[menu] +"theme" = "الثيم" +"dark" = "داكن" +"ultraDark" = "داكن جدًا" +"dashboard" = "نظرة عامة" +"inbounds" = "الإدخالات" +"settings" = "إعدادات البانل" +"xray" = "إعدادات Xray" +"logout" = "تسجيل خروج" +"link" = "إدارة" + +[pages.login] +"hello" = "أهلا" +"title" = "أهلاً وسهلاً" +"loginAgain" = "انتهت صلاحية الجلسة، سجل دخول تاني" + +[pages.login.toasts] +"invalidFormData" = "تنسيق البيانات المدخلة مش صحيح." +"emptyUsername" = "اسم المستخدم مطلوب" +"emptyPassword" = "الباسورد مطلوب" +"wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح." +"successLogin" = "لقد تم تسجيل الدخول إلى حسابك بنجاح." + +[pages.index] +"title" = "نظرة عامة" +"cpu" = "المعالج" +"logicalProcessors" = "المعالجات المنطقية" +"frequency" = "التردد" +"swap" = "Swap" +"storage" = "تخزين" +"memory" = "رام" +"threads" = "خيوط المعالجة" +"xrayStatus" = "Xray" +"stopXray" = "إيقاف" +"restartXray" = "إعادة تشغيل" +"xraySwitch" = "النسخة" +"xraySwitchClick" = "اختار النسخة اللي عايز تتحول لها." +"xraySwitchClickDesk" = "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية." +"xrayStatusUnknown" = "مش معروف" +"xrayStatusRunning" = "شغالة" +"xrayStatusStop" = "متوقفة" +"xrayStatusError" = "فيها غلطة" +"xrayErrorPopoverTitle" = "حصل خطأ أثناء تشغيل Xray" +"operationHours" = "مدة التشغيل" +"systemLoad" = "تحميل النظام" +"systemLoadDesc" = "متوسط تحميل النظام في الدقائق 1, 5, و15" +"connectionCount" = "إحصائيات الاتصال" +"ipAddresses" = "عناوين IP" +"toggleIpVisibility" = "بدل إظهار IP" +"overallSpeed" = "السرعة الكلية" +"upload" = "رفع" +"download" = "تنزيل" +"totalData" = "إجمالي البيانات" +"sent" = "مرسل" +"received" = "مستقبل" +"documentation" = "التوثيق" +"xraySwitchVersionDialog" = "هل تريد حقًا تغيير إصدار Xray؟" +"xraySwitchVersionDialogDesc" = "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#." +"xraySwitchVersionPopover" = "تم تحديث Xray بنجاح" +"geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟" +"geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#." +"geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات." +"geofilesUpdateAll" = "تحديث الكل" +"geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح" +"dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة" +"logs" = "السجلات" +"config" = "الإعدادات" +"backup" = "نسخة احتياطية" +"backupTitle" = "نسخة احتياطية واسترجاع قاعدة البيانات" +"exportDatabase" = "اخزن نسخة" +"exportDatabaseDesc" = "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك." +"importDatabase" = "استرجاع" +"importDatabaseDesc" = "اضغط عشان تختار وتحمل ملف .db من جهازك لاسترجاع قاعدة البيانات من نسخة احتياطية." +"importDatabaseSuccess" = "تم استيراد قاعدة البيانات بنجاح" +"importDatabaseError" = "حدث خطأ أثناء استيراد قاعدة البيانات" +"readDatabaseError" = "حدث خطأ أثناء قراءة قاعدة البيانات" +"getDatabaseError" = "حدث خطأ أثناء استرجاع قاعدة البيانات" +"getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات" + +[pages.inbounds] +"allTimeTraffic" = "إجمالي حركة المرور" +"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت" +"title" = "الإدخالات" +"totalDownUp" = "إجمالي المرسل/المستقبل" +"totalUsage" = "إجمالي الاستخدام" +"inboundCount" = "عدد الإدخالات" +"operate" = "القائمة" +"enable" = "مفعل" +"remark" = "ملاحظة" +"protocol" = "بروتوكول" +"port" = "بورت" +"portMap" = "خريطة البورت" +"traffic" = "الترافيك" +"details" = "تفاصيل" +"transportConfig" = "نقل" +"expireDate" = "المدة" +"createdAt" = "تاريخ الإنشاء" +"updatedAt" = "تاريخ التحديث" +"resetTraffic" = "إعادة ضبط الترافيك" +"addInbound" = "أضف إدخال" +"generalActions" = "إجراءات عامة" +"autoRefresh" = "تحديث تلقائي" +"autoRefreshInterval" = "الفاصل" +"modifyInbound" = "تعديل الإدخال" +"deleteInbound" = "حذف الإدخال" +"deleteInboundContent" = "متأكد إنك عايز تحذف الإدخال؟" +"deleteClient" = "حذف العميل" +"deleteClientContent" = "متأكد إنك عايز تحذف العميل؟" +"resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟" +"copyLink" = "انسخ الرابط" +"address" = "العنوان" +"network" = "الشبكة" +"destinationPort" = "بورت الوجهة" +"targetAddress" = "عنوان الهدف" +"monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs" +"meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)" +"totalFlow" = "إجمالي التدفق" +"leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش" +"noRecommendKeepDefault" = "ننصح باستخدام الافتراضي" +"certificatePath" = "مسار الملف" +"certificateContent" = "محتوى الملف" +"publicKey" = "المفتاح العام" +"privatekey" = "المفتاح الخاص" +"clickOnQRcode" = "اضغط على كود QR للنسخ" +"client" = "عميل" +"export" = "تصدير كل الروابط" +"clone" = "استنساخ" +"cloneInbound" = "استنساخ الإدخال" +"cloneInboundContent" = "كل إعدادات الإدخال ده، غير البورت، IP الاستماع، والعملاء، هتتطبق على الاستنساخ." +"cloneInboundOk" = "استنساخ" +"resetAllTraffic" = "إعادة ضبط ترافيك كل الإدخالات" +"resetAllTrafficTitle" = "إعادة ضبط ترافيك كل الإدخالات" +"resetAllTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك لكل الإدخالات؟" +"resetInboundClientTraffics" = "إعادة ضبط ترافيك العملاء" +"resetInboundClientTrafficTitle" = "إعادة ضبط ترافيك العملاء" +"resetInboundClientTrafficContent" = "متأكد إنك عايز تعيد ضبط ترافيك عملاء الإدخال ده؟" +"resetAllClientTraffics" = "إعادة ضبط ترافيك كل العملاء" +"resetAllClientTrafficTitle" = "إعادة ضبط ترافيك كل العملاء" +"resetAllClientTrafficContent" = "متأكد إنك عايز تعيد ضبط ترافيك كل العملاء؟" +"delDepletedClients" = "حذف العملاء اللي خلصت" +"delDepletedClientsTitle" = "حذف العملاء اللي خلصت" +"delDepletedClientsContent" = "متأكد إنك عايز تحذف كل العملاء اللي خلصت؟" +"email" = "الإيميل" +"emailDesc" = "ادخل إيميل فريد." +"IPLimit" = "تحديد IP" +"IPLimitDesc" = "بيعطل الإدخال لو العدد زاد عن القيمة المحددة. (0 = تعطيل)" +"IPLimitlog" = "سجل IP" +"IPLimitlogDesc" = "سجل تاريخ الـ IPs. (عشان تفعل الإدخال بعد التعطيل، امسح السجل)" +"IPLimitlogclear" = "امسح السجل" +"setDefaultCert" = "استخدم شهادة البانل" +"telegramDesc" = "ادخل ID شات Telegram. (استخدم '/id' في البوت) أو (@userinfobot)" +"subscriptionDesc" = "عشان تلاقي رابط الاشتراك، ادخل على 'التفاصيل'. وكمان ممكن تستخدم نفس الاسم لعدة عملاء." +"info" = "معلومات" +"same" = "نفسه" +"inboundData" = "بيانات الإدخال" +"exportInbound" = "تصدير الإدخال" +"import" = "استيراد" +"importInbound" = "استيراد إدخال" + +[pages.client] +"add" = "أضف عميل" +"edit" = "تعديل عميل" +"submitAdd" = "أضف العميل" +"submitEdit" = "احفظ التعديلات" +"clientCount" = "عدد العملاء" +"bulk" = "إضافة بالجملة" +"method" = "طريقة" +"first" = "أول واحد" +"last" = "آخر واحد" +"prefix" = "بادئة" +"postfix" = "لاحقة" +"delayedStart" = "ابدأ بعد أول استخدام" +"expireDays" = "المدة" +"days" = "يوم/أيام" +"renew" = "تجديد تلقائي" +"renewDesc" = "تجديد تلقائي بعد انتهاء الصلاحية. (0 = تعطيل)(الوحدة: يوم)" + +[pages.inbounds.toasts] +"obtain" = "تم الحصول عليه" +"updateSuccess" = "تم التحديث بنجاح" +"logCleanSuccess" = "تم مسح السجل" +"inboundsUpdateSuccess" = "تم تحديث الواردات بنجاح" +"inboundUpdateSuccess" = "تم تحديث الوارد بنجاح" +"inboundCreateSuccess" = "تم إنشاء الوارد بنجاح" +"inboundDeleteSuccess" = "تم حذف الوارد بنجاح" +"inboundClientAddSuccess" = "تمت إضافة عميل(عملاء) وارد" +"inboundClientDeleteSuccess" = "تم حذف عميل وارد" +"inboundClientUpdateSuccess" = "تم تحديث عميل وارد" +"delDepletedClientsSuccess" = "تم حذف جميع العملاء المستنفذين" +"resetAllClientTrafficSuccess" = "تم إعادة تعيين كل حركة المرور من العميل" +"resetAllTrafficSuccess" = "تم إعادة تعيين كل حركة المرور" +"resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور" +"trafficGetError" = "خطأ في الحصول على حركات المرور" +"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519." +"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65." + +[pages.inbounds.stream.general] +"request" = "طلب" +"response" = "رد" +"name" = "اسم" +"value" = "قيمة" + +[pages.inbounds.stream.tcp] +"version" = "نسخة" +"method" = "طريقة" +"path" = "مسار" +"status" = "الحالة" +"statusDescription" = "وصف الحالة" +"requestHeader" = "رأس الطلب" +"responseHeader" = "رأس الرد" + +[pages.settings] +"title" = "إعدادات البانل" +"save" = "حفظ" +"infoDesc" = "كل تغيير هتعمله هنا لازم يتخزن. ياريت تعيد تشغيل البانل عشان التعديلات تتفعل." +"restartPanel" = "إعادة تشغيل البانل" +"restartPanelDesc" = "متأكد إنك عايز تعيد تشغيل البانل؟ لو ماقدرتش تدخل بعد إعادة التشغيل، شوف سجل البانل على السيرفر." +"restartPanelSuccess" = "تم إعادة تشغيل اللوحة بنجاح" +"actions" = "إجراءات" +"resetDefaultConfig" = "استرجاع الافتراضي" +"panelSettings" = "عام" +"securitySettings" = "المصادقة" +"TGBotSettings" = "بوت Telegram" +"panelListeningIP" = "IP الاستماع" +"panelListeningIPDesc" = "عنوان IP للبانل. (سيبه فاضي عشان يستمع على كل الـ IPs)" +"panelListeningDomain" = "دومين الاستماع" +"panelListeningDomainDesc" = "اسم الدومين للبانل. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)" +"panelPort" = "بورت الاستماع" +"panelPortDesc" = "رقم البورت للبانل. (لازم يكون بورت فاضي)" +"publicKeyPath" = "مسار المفتاح العام" +"publicKeyPathDesc" = "مسار ملف المفتاح العام للبانل. (يبدأ بـ '/')" +"privateKeyPath" = "مسار المفتاح الخاص" +"privateKeyPathDesc" = "مسار ملف المفتاح الخاص للبانل. (يبدأ بـ '/')" +"panelUrlPath" = "مسار URI" +"panelUrlPathDesc" = "مسار URI للبانل. (يبدأ بـ '/' وبينتهي بـ '/')" +"pageSize" = "حجم الصفحة" +"pageSizeDesc" = "حدد حجم الصفحة لجدول الإدخالات. (0 = تعطيل)" +"remarkModel" = "نموذج الملاحظة وحرف الفصل" +"datepicker" = "نوع التقويم" +"datepickerPlaceholder" = "اختار التاريخ" +"datepickerDescription" = "المهام المجدولة هتشتغل بناءً على التقويم ده." +"sampleRemark" = "مثال للملاحظة" +"oldUsername" = "اسم المستخدم الحالي" +"currentPassword" = "الباسورد الحالي" +"newUsername" = "اسم المستخدم الجديد" +"newPassword" = "الباسورد الجديد" +"telegramBotEnable" = "تفعيل بوت Telegram" +"telegramBotEnableDesc" = "يفعل بوت Telegram." +"telegramToken" = "توكن Telegram" +"telegramTokenDesc" = "توكن البوت اللي جبت من '@BotFather'." +"telegramProxy" = "بروكسي SOCKS" +"telegramProxyDesc" = "يفعل بروكسي SOCKS5 للاتصال بـ Telegram. (اضبط الإعدادات حسب الدليل)" +"telegramAPIServer" = "سيرفر Telegram API" +"telegramAPIServerDesc" = "سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي." +"telegramChatId" = "ID شات الأدمن" +"telegramChatIdDesc" = "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من @userinfobot) أو (استخدم '/id' في البوت)" +"telegramNotifyTime" = "وقت الإشعار" +"telegramNotifyTimeDesc" = "وقت إشعار البوت للتقارير الدورية. (استخدم صيغة وقت crontab)" +"tgNotifyBackup" = "نسخة احتياطية لقاعدة البيانات" +"tgNotifyBackupDesc" = "ابعت ملف النسخة الاحتياطية لقاعدة البيانات مع التقرير." +"tgNotifyLogin" = "إشعار بتسجيل الدخول" +"tgNotifyLoginDesc" = "استقبل إشعار بكل محاولة تسجيل دخول للبانل مع اسم المستخدم، الـ IP، والوقت." +"sessionMaxAge" = "مدة الجلسة" +"sessionMaxAgeDesc" = "المدة اللي تفضل فيها مسجل دخول. (الوحدة: دقيقة)" +"expireTimeDiff" = "تنبيه بتاريخ الانتهاء" +"expireTimeDiffDesc" = "استقبل تنبيه قبل ما توصل لتاريخ الانتهاء بالمدة المحددة. (الوحدة: يوم)" +"trafficDiff" = "تنبيه حد الترافيك" +"trafficDiffDesc" = "استقبل تنبيه عند وصول الترافيك للحد المحدد. (الوحدة: جيجابايت)" +"tgNotifyCpu" = "تنبيه حمل المعالج" +"tgNotifyCpuDesc" = "استقبل تنبيه لو حمل المعالج عدى الحد المحدد. (الوحدة: %)" +"timeZone" = "المنطقة الزمنية" +"timeZoneDesc" = "المهام المجدولة هتشتغل بناءً على المنطقة الزمنية دي." +"subSettings" = "الاشتراك" +"subEnable" = "تفعيل خدمة الاشتراك" +"subEnableDesc" = "يفعل خدمة الاشتراك." +"subTitle" = "عنوان الاشتراك" +"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN" +"subListen" = "IP الاستماع" +"subListenDesc" = "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)" +"subPort" = "بورت الاستماع" +"subPortDesc" = "رقم البورت لخدمة الاشتراك. (لازم يكون بورت فاضي)" +"subCertPath" = "مسار المفتاح العام" +"subCertPathDesc" = "مسار ملف المفتاح العام لخدمة الاشتراك. (يبدأ بـ '/')" +"subKeyPath" = "مسار المفتاح الخاص" +"subKeyPathDesc" = "مسار ملف المفتاح الخاص لخدمة الاشتراك. (يبدأ بـ '/')" +"subPath" = "مسار URI" +"subPathDesc" = "مسار URI لخدمة الاشتراك. (يبدأ بـ '/' وبينتهي بـ '/')" +"subDomain" = "دومين الاستماع" +"subDomainDesc" = "اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)" +"subUpdates" = "فترات التحديث" +"subUpdatesDesc" = "فترات تحديث رابط الاشتراك في تطبيقات العملاء. (الوحدة: ساعة)" +"subEncrypt" = "تشفير" +"subEncryptDesc" = "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64." +"subShowInfo" = "اظهر معلومات الاستخدام" +"subShowInfoDesc" = "هيظهر الترافيك المتبقي والتاريخ في تطبيقات العملاء." +"subURI" = "مسار البروكسي العكسي" +"subURIDesc" = "مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي." +"externalTrafficInformEnable" = "تنبيه الترافيك الخارجي" +"externalTrafficInformEnableDesc" = "يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك." +"externalTrafficInformURI" = "مسار تنبيه الترافيك الخارجي" +"externalTrafficInformURIDesc" = "تحديثات الترافيك هتتبعت للمسار ده." +"fragment" = "تجزئة" +"fragmentDesc" = "يفعل تجزئة لحزمة TLS hello." +"fragmentSett" = "إعدادات التجزئة" +"noisesDesc" = "يفعل التشويش." +"noisesSett" = "إعدادات التشويش" +"mux" = "MUX" +"muxDesc" = "ينقل أكثر من تيار بيانات مستقل خلال تيار بيانات واحد قائم." +"muxSett" = "إعدادات MUX" +"direct" = "اتصال مباشر" +"directDesc" = "ينشئ اتصال مباشر مع الدومينات أو نطاقات IP لدولة معينة." +"notifications" = "الإشعارات" +"certs" = "الشهادات" +"externalTraffic" = "الترافيك الخارجي" +"dateAndTime" = "التاريخ والوقت" +"proxyAndServer" = "البروكسي والسيرفر" +"intervals" = "الفترات" +"information" = "المعلومات" +"language" = "اللغة" +"telegramBotLanguage" = "لغة بوت Telegram" + +[pages.xray] +"title" = "إعدادات Xray" +"save" = "احفظ" +"restart" = "أعد تشغيل Xray" +"restartSuccess" = "تم إعادة تشغيل Xray بنجاح" +"stopSuccess" = "تم إيقاف Xray بنجاح" +"restartError" = "حدث خطأ أثناء إعادة تشغيل Xray." +"stopError" = "حدث خطأ أثناء إيقاف Xray." +"basicTemplate" = "أساسي" +"advancedTemplate" = "متقدم" +"generalConfigs" = "إعدادات عامة" +"generalConfigsDesc" = "الخيارات دي هتحدد التعديلات العامة." +"logConfigs" = "السجلات" +"logConfigsDesc" = "السجلات ممكن تأثر على كفاءة السيرفر. ننصح بتفعيلها بحكمة لما تكون محتاجها." +"blockConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على بروتوكولات ومواقع محددة." +"basicRouting" = "توجيه أساسي" +"blockConnectionsConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على الدولة المطلوبة." +"directConnectionsConfigsDesc" = "الاتصال المباشر بيضمن إن الترافيك المعين مايمرش من سيرفر تاني." +"blockips" = "حظر IPs" +"blockdomains" = "حظر دومينات" +"directips" = "اتصالات مباشرة لـ IPs" +"directdomains" = "اتصالات مباشرة للدومينات" +"ipv4Routing" = "توجيه IPv4" +"ipv4RoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4." +"warpRouting" = "توجيه WARP" +"warpRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر WARP." +"Template" = "قالب إعدادات Xray المتقدم" +"TemplateDesc" = "ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده." +"FreedomStrategy" = "استراتيجية بروتوكول الحرية" +"FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية." +"RoutingStrategy" = "استراتيجية التوجيه العامة" +"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات." +"Torrent" = "حظر بروتوكول التورنت" +"Inbounds" = "الإدخالات" +"InboundsDesc" = "قبول العملاء المعينين." +"Outbounds" = "المخرجات" +"Balancers" = "موازنات التحميل" +"OutboundsDesc" = "حدد مسار الترافيك الصادر." +"Routings" = "قواعد التوجيه" +"RoutingsDesc" = "أولوية كل قاعدة مهمة جداً!" +"completeTemplate" = "الكل" +"logLevel" = "مستوى السجلات" +"logLevelDesc" = "مستوى السجل الخاص بالأخطاء، اللي بيوضح المعلومات المطلوبة للتسجيل." +"accessLog" = "سجل الوصول" +"accessLogDesc" = "مسار ملف سجل الوصول. القيمة الخاصة 'none' بتعطل سجل الوصول." +"errorLog" = "سجل الأخطاء" +"errorLogDesc" = "مسار ملف سجل الأخطاء. القيمة الخاصة 'none' بتعطل سجل الأخطاء." +"dnsLog" = "سجل DNS" +"dnsLogDesc" = "لو هتسجل استعلامات DNS." +"maskAddress" = "إخفاء العنوان" +"maskAddressDesc" = "إخفاء عنوان الـ IP؛ لو مفعل، هيستبدل تلقائياً عنوان IP اللي بيظهر في السجل." +"statistics" = "إحصائيات" +"statsInboundUplink" = "إحصائيات رفع الإدخال" +"statsInboundUplinkDesc" = "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من الإدخالات." +"statsInboundDownlink" = "إحصائيات تنزيل الإدخال" +"statsInboundDownlinkDesc" = "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من الإدخالات." +"statsOutboundUplink" = "إحصائيات رفع المخرجات" +"statsOutboundUplinkDesc" = "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات." +"statsOutboundDownlink" = "إحصائيات تنزيل المخرجات" +"statsOutboundDownlinkDesc" = "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات." + +[pages.xray.rules] +"first" = "أول" +"last" = "آخر" +"up" = "فوق" +"down" = "تحت" +"source" = "المصدر" +"dest" = "الوجهة" +"inbound" = "إدخال" +"outbound" = "مخرج" +"balancer" = "موازن" +"info" = "معلومات" +"add" = "أضف قاعدة" +"edit" = "عدل القاعدة" +"useComma" = "عناصر مفصولة بفواصل" + +[pages.xray.outbound] +"addOutbound" = "أضف مخرج" +"addReverse" = "أضف عكسي" +"editOutbound" = "عدل المخرج" +"editReverse" = "عدل العكسي" +"tag" = "تاج" +"tagDesc" = "تاج فريد" +"address" = "العنوان" +"reverse" = "عكسي" +"domain" = "دومين" +"type" = "النوع" +"bridge" = "جسر" +"portal" = "بوابة" +"link" = "رابط" +"intercon" = "تواصل" +"settings" = "إعدادات" +"accountInfo" = "معلومات الحساب" +"outboundStatus" = "حالة المخرج" +"sendThrough" = "أرسل من خلال" + +[pages.xray.balancer] +"addBalancer" = "أضف موازن تحميل" +"editBalancer" = "عدل موازن التحميل" +"balancerStrategy" = "استراتيجية الموازن" +"balancerSelectors" = "المحددات" +"tag" = "تاج" +"tagDesc" = "تاج فريد" +"balancerDesc" = "ماينفعش تستخدم balancerTag و outboundTag مع بعض. لو اتستخدموا مع بعض، outboundTag هو اللي هيشتغل." + +[pages.xray.wireguard] +"secretKey" = "المفتاح السري" +"publicKey" = "المفتاح العام" +"allowedIPs" = "عناوين IP المسموح بها" +"endpoint" = "النهاية" +"psk" = "المفتاح المشترك" +"domainStrategy" = "استراتيجية الدومين" + +[pages.xray.dns] +"enable" = "فعل DNS" +"enableDesc" = "فعل سيرفر DNS المدمج" +"tag" = "تاج إدخال DNS" +"tagDesc" = "التاج ده هيبقى متاح كإدخال في قواعد التوجيه." +"clientIp" = "IP العميل" +"clientIpDesc" = "بيحدد موقع العميل خلال استعلامات DNS" +"disableCache" = "تعطيل الكاش" +"disableCacheDesc" = "بيعطل تخزين نتائج DNS مؤقتاً" +"disableFallback" = "تعطيل النسخ الاحتياطي" +"disableFallbackDesc" = "بيعطل استعلامات DNS الاحتياطية" +"disableFallbackIfMatch" = "تعطيل النسخ الاحتياطي عند التطابق" +"disableFallbackIfMatchDesc" = "بيعطل استعلامات DNS الاحتياطية لما يتحقق تطابق مع قائمة الدومينات" +"strategy" = "استراتيجية الاستعلام" +"strategyDesc" = "الاستراتيجية العامة لحل أسماء الدومين" +"add" = "أضف سيرفر" +"edit" = "عدل السيرفر" +"domains" = "الدومينات" +"expectIPs" = "العناوين المتوقعة" +"unexpectIPs" = "عناوين IP غير متوقعة" +"useSystemHosts" = "استخدام ملف Hosts الخاص بالنظام" +"useSystemHostsDesc" = "استخدام ملف hosts من نظام مثبت" +"usePreset" = "استخدام النموذج" +"dnsPresetTitle" = "قوالب DNS" +"dnsPresetFamily" = "العائلي" + +[pages.xray.fakedns] +"add" = "أضف Fake DNS" +"edit" = "عدل Fake DNS" +"ipPool" = "نطاق IP Pool" +"poolSize" = "حجم المجموعة" + +[pages.settings.security] +"admin" = "بيانات الأدمن" +"twoFactor" = "المصادقة الثنائية" +"twoFactorEnable" = "تفعيل المصادقة الثنائية" +"twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان." +"twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية" +"twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية" +"twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:" +"twoFactorModalFirstStep" = "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق" +"twoFactorModalSecondStep" = "2. أدخل الرمز من التطبيق" +"twoFactorModalRemoveStep" = "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية." +"twoFactorModalChangeCredentialsTitle" = "تغيير بيانات الاعتماد" +"twoFactorModalChangeCredentialsStep" = "أدخل الرمز من التطبيق لتغيير بيانات اعتماد المسؤول." +"twoFactorModalSetSuccess" = "تم إنشاء المصادقة الثنائية بنجاح" +"twoFactorModalDeleteSuccess" = "تم حذف المصادقة الثنائية بنجاح" +"twoFactorModalError" = "رمز خاطئ" + +[pages.settings.toasts] +"modifySettings" = "تم تغيير المعلمات." +"getSettings" = "حدث خطأ أثناء استرداد المعلمات." +"modifyUserError" = "حدث خطأ أثناء تغيير بيانات اعتماد المسؤول." +"modifyUser" = "لقد قمت بتغيير بيانات اعتماد المسؤول بنجاح." +"originalUserPassIncorrect" = "اسم المستخدم أو الباسورد الحالي غير صحيح" +"userPassMustBeNotEmpty" = "اسم المستخدم والباسورد الجديدين فاضيين" +"getOutboundTrafficError" = "خطأ في الحصول على حركات المرور الصادرة" +"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة" + +[tgbot] +"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!" +"noResult" = "❗ لا يوجد نتائج!" +"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!" +"wentWrong" = "❌ حدث خطأ ما!" +"noIpRecord" = "❗ لا يوجد سجل IP!" +"noInbounds" = "❗ لم يتم العثور على أي وارد!" +"unlimited" = "♾ غير محدود (إعادة تعيين)" +"add" = "إضافة" +"month" = "شهر" +"months" = "أشهر" +"day" = "يوم" +"days" = "أيام" +"hours" = "ساعات" +"minutes" = "دقائق" +"unknown" = "غير معروف" +"inbounds" = "الواردات" +"clients" = "العملاء" +"offline" = "🔴 غير متصل" +"online" = "🟢 متصل" + +[tgbot.commands] +"unknown" = "❗ أمر مش معروف." +"pleaseChoose" = "👇 من فضلك اختار:\r\n" +"help" = "🤖 أهلا بيك في البوت! البوت ده معمول عشان يديك بيانات معينة من البانل ويسمحلك بالتعديلات." +"start" = "👋 أهلا {{ .Firstname }}.\r\n" +"welcome" = "🤖 أهلا بيك في بوت إدارة {{ .Hostname }}.\r\n" +"status" = "✅ البوت شغال!" +"usage" = "❗ من فضلك ادخل نص للتبحث عنه!" +"getID" = "🆔 الـ ID بتاعك: {{ .ID }}" +"helpAdminCommands" = "عشان تعيد تشغيل Xray Core:\r\n/restart\r\n\r\nعشان تدور على إيميل عميل:\r\n/usage [Email]\r\n\r\nعشان تدور على إدخالات (مع إحصائيات العملاء):\r\n/inbound [Remark]\r\n\r\nID شات Telegram:\r\n/id" +"helpClientCommands" = "عشان تدور على الإحصائيات، استخدم الأمر ده:\r\n\r\n/usage [Email]\r\n\r\nID شات Telegram:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ العملية نجحت!" +"restartFailed" = "❗ حصل خطأ في العملية.\r\n\r\nError: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core مش شغال." +"startDesc" = "عرض القائمة الرئيسية" +"helpDesc" = "مساعدة البوت" +"statusDesc" = "التحقق من حالة البوت" +"idDesc" = "عرض معرف Telegram الخاص بك" + +[tgbot.messages] +"cpuThreshold" = "🔴 حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)" +"selectUserFailed" = "❌ حصل خطأ في اختيار المستخدم!" +"userSaved" = "✅ حفظت بيانات مستخدم Telegram." +"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n" +"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n" +"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n" +"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n" +"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n" +"version" = "🚀 X-Panel: {{ .Version }}\r\n" +"xrayVersion" = "📡 نسخة Xray: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 عناوين IP:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ وقت التشغيل: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 تحميل النظام: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 الرام: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 الترافيك: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ الحالة: {{ .State }}\r\n" +"username" = "👤 اسم المستخدم: {{ .Username }}\r\n" +"password" = "👤 الباسورد: {{ .Password }}\r\n" +"time" = "⏰ الوقت: {{ .Time }}\r\n" +"inbound" = "📍 الإدخال: {{ .Remark }}\r\n" +"port" = "🔌 البورت: {{ .Port }}\r\n" +"expire" = "📅 تاريخ الانتهاء: {{ .Time }}\r\n" +"expireIn" = "📅 هيخلص بعد: {{ .Time }}\r\n" +"active" = "💡 مفعل: {{ .Enable }}\r\n" +"enabled" = "🚨 مفعل: {{ .Enable }}\r\n" +"online" = "🌐 حالة الاتصال: {{ .Status }}\r\n" +"email" = "📧 الإيميل: {{ .Email }}\r\n" +"upload" = "🔼 رفع: ↑{{ .Upload }}\r\n" +"download" = "🔽 تنزيل: ↓{{ .Download }}\r\n" +"total" = "📊 الإجمالي: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 مستخدم Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 نفذ {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 عدد النفاذ لـ {{ .Type }}:\r\n" +"onlinesCount" = "🌐 العملاء الأونلاين: {{ .Count }}\r\n" +"disabled" = "🛑 معطل: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 هينتهي قريب: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 وقت النسخة الاحتياطية: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n" +"yes" = "✅ أيوه" +"no" = "❌ لأ" +"received_id" = "🔑📥 الـ ID اتحدث." +"received_password" = "🔑📥 الباسورد اتحدث." +"received_email" = "📧📥 الإيميل اتحدث." +"received_comment" = "💬📥 التعليق اتحدث." +"id_prompt" = "🔑 الـ ID الافتراضي: {{ .ClientId }}\n\nادخل الـ ID بتاعك." +"pass_prompt" = "🔑 الباسورد الافتراضي: {{ .ClientPassword }}\n\nادخل الباسورد بتاعك." +"email_prompt" = "📧 الإيميل الافتراضي: {{ .ClientEmail }}\n\nادخل الإيميل بتاعك." +"comment_prompt" = "💬 التعليق الافتراضي: {{ .ClientComment }}\n\nادخل تعليقك." +"inbound_client_data_id" = "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 المعرف: {{ .ClientId }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!" +"inbound_client_data_pass" = "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 كلمة المرور: {{ .ClientPass }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!" +"cancel" = "❌ العملية اتلغت! \n\nممكن تبدأ من /start في أي وقت. 🔄" +"error_add_client" = "⚠️ حصل خطأ:\n\n {{ .error }}" +"using_default_value" = "تمام، هشيل على القيمة الافتراضية. 😊" +"incorrect_input" = "المدخلات مش صحيحة.\nالكلمات لازم تكون متصلة من غير فراغات.\nمثال صحيح: aaaaaa\nمثال غلط: aaa aaa 🚫" +"AreYouSure" = "إنت متأكد؟ 🤔" +"SuccessResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ✅ تم بنجاح" +"FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠️ الخطأ: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء." + +[tgbot.buttons] +"closeKeyboard" = "❌ اقفل الكيبورد" +"cancel" = "❌ إلغاء" +"cancelReset" = "❌ إلغاء إعادة الضبط" +"cancelIpLimit" = "❌ إلغاء حد الـ IP" +"confirmResetTraffic" = "✅ تأكيد إعادة ضبط الترافيك؟" +"confirmClearIps" = "✅ تأكيد مسح الـ IPs؟" +"confirmRemoveTGUser" = "✅ تأكيد حذف مستخدم Telegram؟" +"confirmToggle" = "✅ تأكيد تفعيل/تعطيل المستخدم؟" +"dbBackup" = "احصل على نسخة DB" +"serverUsage" = "استخدام السيرفر" +"getInbounds" = "احصل على الإدخالات" +"depleteSoon" = "هينتهي قريب" +"clientUsage" = "استخدام العميل" +"onlines" = "العملاء الأونلاين" +"commands" = "الأوامر" +"refresh" = "🔄 تجديد" +"clearIPs" = "❌ مسح الـ IPs" +"removeTGUser" = "❌ حذف مستخدم Telegram" +"selectTGUser" = "👤 اختار مستخدم Telegram" +"selectOneTGUser" = "👤 اختار مستخدم Telegram:" +"resetTraffic" = "📈 إعادة ضبط الترافيك" +"resetExpire" = "📅 تغيير تاريخ الانتهاء" +"ipLog" = "🔢 سجل الـ IP" +"ipLimit" = "🔢 حد الـ IP" +"setTGUser" = "👤 ضبط مستخدم Telegram" +"toggle" = "🔘 تفعيل / تعطيل" +"custom" = "🔢 مخصص" +"confirmNumber" = "✅ تأكيد: {{ .Num }}" +"confirmNumberAdd" = "✅ تأكيد إضافة: {{ .Num }}" +"limitTraffic" = "🚧 حد الترافيك" +"getBanLogs" = "احصل على سجلات الحظر" +"allClients" = "كل العملاء" +"addClient" = "إضافة عميل" +"submitDisable" = "إرسال كمعطّل ☑️" +"submitEnable" = "إرسال كمفعّل ✅" +"use_default" = "🏷️ استخدام الإعدادات الافتراضية" +"change_id" = "⚙️🔑 المعرّف" +"change_password" = "⚙️🔑 كلمة السر" +"change_email" = "⚙️📧 البريد الإلكتروني" +"change_comment" = "⚙️💬 تعليق" +"ResetAllTraffics" = "إعادة ضبط جميع الترافيك" +"SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب" + +[tgbot.answers] +"successfulOperation" = "✅ العملية نجحت!" +"errorOperation" = "❗ حصل خطأ في العملية." +"getInboundsFailed" = "❌ فشل الحصول على الإدخالات." +"getClientsFailed" = "❌ فشل الحصول على العملاء." +"canceled" = "❌ {{ .Email }}: العملية اتلغت." +"clientRefreshSuccess" = "✅ {{ .Email }}: العميل اتحدث بنجاح." +"IpRefreshSuccess" = "✅ {{ .Email }}: الـ IPs اتحدثت بنجاح." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: مستخدم Telegram اتحدث بنجاح." +"resetTrafficSuccess" = "✅ {{ .Email }}: الترافيك اتظبط بنجاح." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: حد الترافيك اتسجل بنجاح." +"expireResetSuccess" = "✅ {{ .Email }}: أيام الانتهاء اتظبطت بنجاح." +"resetIpSuccess" = "✅ {{ .Email }}: حد الـ IP ({{ .Count }}) اتسجل بنجاح." +"clearIpSuccess" = "✅ {{ .Email }}: الـ IPs اتمسحت بنجاح." +"getIpLog" = "✅ {{ .Email }}: سجل الـ IP اتجاب." +"getUserInfo" = "✅ {{ .Email }}: بيانات مستخدم Telegram اتجاب." +"removedTGUserSuccess" = "✅ {{ .Email }}: مستخدم Telegram اتحذف بنجاح." +"enableSuccess" = "✅ {{ .Email }}: اتفعل بنجاح." +"disableSuccess" = "✅ {{ .Email }}: اتعطل بنجاح." +"askToAddUserId" = "مافيش إعدادات ليك!\r\nاطلب من الأدمن يضيف الـ Telegram ChatID الخاص بيك في إعداداتك.\r\n\r\nالـ ChatID بتاعك: {{ .TgUserID }}" +"chooseClient" = "اختار عميل للإدخال {{ .Inbound }}" +"chooseInbound" = "اختار الإدخال" diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml new file mode 100644 index 0000000..9b93b1c --- /dev/null +++ b/web/translation/translate.en_US.toml @@ -0,0 +1,813 @@ +"username" = "Username" +"password" = "Password" +"login" = "Log In" +"confirm" = "Confirm" +"cancel" = "Cancel" +"close" = "Close" +"create" = "Create" +"update" = "Update" +"copy" = "Copy" +"copied" = "Copied" +"download" = "Download" +"remark" = "Remark" +"enable" = "Enabled" +"protocol" = "Protocol" +"search" = "Search" +"filter" = "Filter" +"loading" = "Loading..." +"second" = "Second" +"minute" = "Minute" +"hour" = "Hour" +"day" = "Day" +"check" = "Check" +"indefinite" = "Indefinite" +"unlimited" = "Unlimited" +"none" = "None" +"qrCode" = "QR Code" +"info" = "More Information" +"edit" = "Edit" +"delete" = "Delete" +"reset" = "Reset" +"noData" = "No data." +"copySuccess" = "Copied Successful" +"sure" = "Sure" +"encryption" = "Encryption" +"useIPv4ForHost" = "Use IPv4 for host" +"transmission" = "Transmission" +"host" = "Host" +"path" = "Path" +"camouflage" = "Obfuscation" +"status" = "Status" +"enabled" = "Enabled" +"disabled" = "Disabled" +"depleted" = "Ended" +"depletingSoon" = "Depleting" +"offline" = "Offline" +"online" = "Online" +"domainName" = "Domain Name" +"monitor" = "Listen IP" +"certificate" = "Digital Certificate" +"fail" = "Failed" +"comment" = "Comment" +"success" = "Successfully" +"lastOnline" = "Last Online" +"getVersion" = "Get Version" +"install" = "Install" +"clients" = "Clients" +"usage" = "Usage" +"twoFactorCode" = "TwoStepCode" +"remained" = "Remained" +"security" = "Security" +"secAlertTitle" = "Security Alert" +"secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection." +"secAlertConf" = "Certain settings are vulnerable to attacks. It is recommended to reinforce security protocols to prevent potential breaches." +"secAlertSSL" = "Panel lacks secure connection. Please install TLS certificate for data protection." +"secAlertPanelPort" = "Panel default port is vulnerable. Please configure a random or specific port." +"secAlertPanelURI" = "Panel default URI path is insecure. Please configure a complex URI path." +"secAlertSubURI" = "Subscription default URI path is insecure. Please configure a complex URI path." +"secAlertSubJsonURI" = "Subscription JSON default URI path is insecure. Please configure a complex URI path." +"emptyDnsDesc" = "No added DNS servers." +"emptyFakeDnsDesc" = "No added Fake DNS servers." +"emptyBalancersDesc" = "No added balancers." +"emptyReverseDesc" = "No added reverse proxies." +"somethingWentWrong" = "Something went wrong" + +[menu] +"theme" = "Theme" +"dark" = "Dark" +"ultraDark" = "Ultra Dark" +"dashboard" = "Overview" +"inbounds" = "Inbounds" +"settings" = "Panel Settings" +"xray" = "Xray Configs" +"logout" = "Log Out" +"link" = "Manage/Configure Database" +"navigation" = "Practical Navigation" + +[pages.login] +"hello" = "Hello" +"XPanelSystem" = "Management System" +"title" = "Welcome to Use" +"loginAgain" = "Your session has expired, please log in again" + +[pages.login.toasts] +"invalidFormData" = "The Input data format is invalid." +"emptyUsername" = "Username is required" +"emptyPassword" = "Password is required" +"wrongUsernameOrPassword" = "Invalid username or password or two-factor code." +"successLogin" = " You have successfully logged into your account." + +[pages.index] +"title" = "System Status" +"cpu" = "CPU" +"logicalProcessors" = "Logical Processors" +"frequency" = "Frequency" +"swap" = "Swap" +"storage" = "Storage" +"memory" = "RAM" +"threads" = "Threads" +"xrayStatus" = "Xray Running Status" +"stopXray" = "Stop" +"restartXray" = "Restart" +"xraySwitch" = "Version" +"xraySwitchClick" = "Choose the version you want to switch to." +"xraySwitchClickDesk" = "Choose carefully, as older versions may not be compatible with current configurations." +"xrayStatusUnknown" = "Unknown" +"xrayStatusRunning" = "Running" +"xrayStatusStop" = "Stop" +"xrayStatusError" = "Error" +"xrayErrorPopoverTitle" = "An error occurred while running Xray" +"operationHours" = "Uptime" +"systemLoad" = "System Load" +"systemLoadDesc" = "System load average for the past 1, 5, and 15 minutes" +"connectionCount" = "Connection Stats" +"ipAddresses" = "IP Addresses" +"toggleIpVisibility" = "Toggle visibility of the IP" +"overallSpeed" = "Overall Speed" +"upload" = "Upload" +"download" = "Download" +"totalData" = "Total Data" +"sent" = "Sent" +"received" = "Received" +"documentation" = "Documentation" +"xraySwitchVersionDialog" = "Do you really want to change the Xray version?" +"xraySwitchVersionDialogDesc" = "This will change the Xray version to #version#." +"xraySwitchVersionPopover" = "Xray updated successfully" +"geofileUpdateDialog" = "Do you really want to update the geofile?" +"geofileUpdateDialogDesc" = "This will update the #filename# file." +"geofilesUpdateDialogDesc" = "This will update all geofiles." +"geofilesUpdateAll" = "Update all" +"geofileUpdatePopover" = "Geofile updated successfully" +"dontRefresh" = "Installation is in progress, please do not refresh this page" +"logs" = "Logs" +"config" = "Config" +"backup" = "Backup" +"backupTitle" = "Database Backup & Restore" +"exportDatabase" = "Local Back Up" +"exportDatabaseDesc" = "Click to download a .db file containing a backup of your current database to your device." +"importDatabase" = "Local Restore" +"importDatabaseDesc" = "Click to select and upload a .db file from your device to restore your database from a backup." +"importDatabaseSuccess" = "The database has been successfully imported." +"importDatabaseError" = "An error occurred while importing the database." +"readDatabaseError" = "An error occurred while reading the database." +"getDatabaseError" = "An error occurred while retrieving the database." +"getConfigError" = "An error occurred while retrieving the config file." +"betterPanel" = "A Better Panel" +"builtOnXray" = "Built On Xray Core" +"xpanelTitle" = "〔X-Panel〕Dashboard" +"tgPrivateChat" = "TG Private Chat" +"tgGroupChat" = "〔X-Panel〕Group Chat" +"portCheck" = "Port Check" +"speedTest" = "Speed Test" +"datasnapshot" = "Data Snapshot" +"emergencyrecovery" = "Emergency Recovery" + +[pages.inbounds] +"allTimeTraffic" = "All-time Traffic" +"allTimeTrafficUsage" = "All Time Total Usage" +"title" = "Inbounds" +"totalDownUp" = "Total Sent/Received" +"totalUsage" = "Total Usage" +"inboundCount" = "Total Inbounds" +"operate" = "Menu" +"enable" = "Enabled" +"remark" = "Remark" +"protocol" = "Protocol" +"port" = "Port" +"portMap" = "Port Mapping" +"traffic" = "Traffic" +"details" = "Details" +"transportConfig" = "Transport" +"expireDate" = "Duration" +"createdAt" = "Created" +"updatedAt" = "Updated" +"resetTraffic" = "Reset Traffic" +"addInbound" = "Add Inbound" +"generalActions" = "General Actions" +"autoRefresh" = "Auto-refresh" +"autoRefreshInterval" = "Interval" +"modifyInbound" = "Modify Inbound" +"deleteInbound" = "Delete Inbound" +"deleteInboundContent" = "Are you sure you want to delete inbound?" +"deleteClient" = "Delete Client" +"deleteClientContent" = "Are you sure you want to delete client?" +"resetTrafficContent" = "Are you sure you want to reset traffic?" +"copyLink" = "Copy URL" +"address" = "Address" +"network" = "Network" +"destinationPort" = "Destination Port" +"targetAddress" = "Target Address" +"monitorDesc" = "Leave blank to listen on all IPs" +"meansNoLimit" = "= Unlimited. (unit: GB)" +"totalFlow" = "Total Flow" +"leaveBlankToNeverExpire" = "Leave blank to never expire" +"noRecommendKeepDefault" = "It is recommended to keep the default" +"certificatePath" = "File Path" +"certificateContent" = "File Content" +"publicKey" = "Public Key" +"privatekey" = "Private Key" +"clickOnQRcode" = "Click on QR Code to Copy" +"client" = "Client" +"clients" = "All Clients" +"export" = "Export All URLs" +"clone" = "Clone" +"cloneInbound" = "Clone" +"cloneInboundContent" = "All settings of this inbound, except Port, Listening IP, and Clients, will be applied to the clone." +"cloneInboundOk" = "Clone" +"resetAllTraffic" = "Reset All Inbounds Traffic" +"resetAllTrafficTitle" = "Reset All Inbounds Traffic" +"resetAllTrafficContent" = "Are you sure you want to reset the traffic of all inbounds?" +"resetInboundClientTraffics" = "Reset Clients Traffic" +"resetInboundClientTrafficTitle" = "Reset Clients Traffic" +"resetInboundClientTrafficContent" = "Are you sure you want to reset the traffic of this inbound's clients?" +"resetAllClientTraffics" = "Reset All Clients Traffic" +"resetAllClientTrafficTitle" = "Reset All Clients Traffic" +"resetAllClientTrafficContent" = "Are you sure you want to reset the traffic of all clients?" +"delDepletedClients" = "Delete Depleted Clients" +"delDepletedClientsTitle" = "Delete Depleted Clients" +"delDepletedClientsContent" = "Are you sure you want to delete all the depleted clients?" +"email" = "Email" +"emailDesc" = "Please provide a unique email address." +"IPLimit" = "IP Limit" +"IPLimitDesc" = "Disables inbound if the count exceeds the set value. (0 = disable)" +"IPLimitlog" = "IP Log" +"IPLimitlogDesc" = "The IPs history log. (to enable inbound after disabling, clear the log)" +"IPLimitlogclear" = "Clear The Log" +"setDefaultCert" = "Set Cert from Panel" +"telegramDesc" = "Please provide Telegram Chat ID. (use '/id' command in the bot) or (@userinfobot)" +"subscriptionDesc" = "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients." +"info" = "Info" +"same" = "Same" +"inboundData" = "Inbound's Data" +"exportInbound" = "Export Inbound" +"import" = "Import" +"importInbound" = "Import an Inbound" +"reviewTitle"="Please confirm the following configuration information" +"neverExpire"="Never expires" +"reviewHint"="If you need to modify, please return to the previous step" +"unlimited"="No restrictions" +"deviceLimit"="Device restrictions" +"deviceLimitDesc"="Please enter the specific quantity, \r\n0 means no limit (leaving it blank also means no limit)" +"speedLimit"="Independent speedLimit" +"speedLimitDesc"="Set the maximum upload/download speed for this user in KB/s. 0 means unlimited speed." +"oneClickConfig"="One-click configuration" +"is_subConversion"="Subscription Conversion" +"confirmCreate"="Confirm submission creation" + +[pages.client] +"add" = "Add Client" +"edit" = "Edit Client" +"submitAdd" = "Add Client" +"submitEdit" = "Save Changes" +"clientCount" = "Number of Clients" +"bulk" = "Add Bulk" +"method" = "Method" +"first" = "First" +"last" = "Last" +"prefix" = "Prefix" +"postfix" = "Postfix" +"delayedStart" = "Start After First Use" +"expireDays" = "Duration" +"days" = "Day(s)" +"renew" = "Auto Renew" +"renewDesc" = "Auto-renewal after expiration. (0 = disable)(unit: day)" + +[pages.inbounds.toasts] +"obtain" = "Obtain" +"updateSuccess" = "The update was successful." +"logCleanSuccess" = "The log has been cleared." +"inboundsUpdateSuccess" = "Inbounds have been successfully updated." +"inboundUpdateSuccess" = "Inbound has been successfully updated." +"inboundCreateSuccess" = "Inbound has been successfully created." +"inboundDeleteSuccess" = "Inbound has been successfully deleted." +"inboundClientAddSuccess" = "Inbound client(s) have been added." +"inboundClientDeleteSuccess" = "Inbound client has been deleted." +"inboundClientUpdateSuccess" = "Inbound client has been updated." +"delDepletedClientsSuccess" = "All depleted clients are deleted." +"resetAllClientTrafficSuccess" = "All traffic from the client has been reset." +"resetAllTrafficSuccess" = "All traffic has been reset." +"resetInboundClientTrafficSuccess" = "Traffic has been reset." +"trafficGetError" = "Error getting traffics." +"getNewX25519CertError" = "Error while obtaining the X25519 certificate." +"getNewmldsa65Error" = "Error while obtaining mldsa65." + +[pages.inbounds.stream.general] +"request" = "Request" +"response" = "Response" +"name" = "Name" +"value" = "Value" + +[pages.inbounds.stream.tcp] +"version" = "Version" +"method" = "Method" +"path" = "Path" +"status" = "Status" +"statusDescription" = "Status Desc" +"requestHeader" = "Request Header" +"responseHeader" = "Response Header" + +[pages.inbounds.oneClick] +"title" = "One-click configuration" +"prompt" = "Select a preset configuration to quickly create an inbound connection." +"presetType" = "Preset protocol configuration type" +"generateTab" = "Generate Configuration" +"historyTab" = "History" +"historyType" = "Generate Type" +"historyLink" = "Links" +"historyTime" = "Generated Time" +"clickToCopyTitle" = "Click to copy link" +"scanQrcodeTitle" = "Scan QR code" +"clickToCopyHint" = "(Click on the image to copy)" +"linkCreated" = "The inbound link has been created, please copy the following address:" +"descriptionTitle" = "\r\n\r\nUsage notes" +"descriptionContent" = "1.This function will automatically generate inbound connections for the VLESS + TCP + Reality (xtls-rprx-vision) and VLESS + XHTTP + Reality protocols. The first two combinations are suitable for direct connections.\r\n2.Or for VLESS Encryption + XHTTP + TLS (with optional CDN) protocols.\r\n3.And randomly assign an available port. Please ensure that this port is open. After generating, please copy the [Link Address] directly.\r\n4.The [One-Click Configuration] generation function here is connected to the [Inbound] data.\r\n5.After one-click creation, you can manually view/copy or edit the details in the list to add other parameters." + +[pages.inbounds.oneClick.preset] +"vless_reality" = "VLESS + TCP + Reality + Vision" +"vless_xhttp_reality" = "VLESS + XHTTP + Reality" +"vless_tls_encryption" = "VLESS Encryption + TLS (CDN optional)" +"switch_vision_seed_dev" = "Switch + Vision Seed (in development)" + +[pages.inbounds.subConversion] +"modalTitle" = "Jump to tip..." +"modalContent" = "We are about to detect and redirect you to your subscription conversion service page. Do you want to continue?" +"modalOk" = "continue" +"notFoundTitle" = "[Subscription Conversion Service] is not installed or cannot be accessed" +"notFoundContent" = "Please enter the VPS server terminal, enter the x-ui command, and select option [25] to install." + +[pages.settings] +"title" = "Panel Settings" +"save" = "Save" +"infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes." +"restartPanel" = "Restart Panel" +"restartPanelDesc" = "Are you sure you want to restart the panel? If you cannot access the panel after restarting, please view the panel log info on the server." +"restartPanelSuccess" = "The panel was successfully restarted." +"actions" = "Actions" +"resetDefaultConfig" = "Reset to Default" +"panelSettings" = "General" +"securitySettings" = "Authentication" +"TGBotSettings" = "Telegram Bot" +"panelListeningIP" = "Listen IP" +"panelListeningIPDesc" = "The IP address for the web panel. (leave blank to listen on all IPs)" +"panelListeningDomain" = "Listen Domain" +"panelListeningDomainDesc" = "The domain name for the web panel. (leave blank to listen on all domains and IPs)" +"panelPort" = "Listen Port" +"panelPortDesc" = "The port number for the web panel. (must be an unused port)" +"publicKeyPath" = "Public Key Path" +"publicKeyPathDesc" = "The public key file path for the web panel. (begins with ‘/‘)" +"privateKeyPath" = "Private Key Path" +"privateKeyPathDesc" = "The private key file path for the web panel. (begins with ‘/‘)" +"panelUrlPath" = "URI Path" +"panelUrlPathDesc" = "The URI path for the web panel. (begins with ‘/‘ and concludes with ‘/‘)" +"pageSize" = "Pagination Size" +"pageSizeDesc" = "Define page size for inbounds table. (0 = disable)" +"remarkModel" = "Remark Model & Separation Character" +"datepicker" = "Calendar Type" +"datepickerPlaceholder" = "Select date" +"datepickerDescription" = "Scheduled tasks will run based on this calendar." +"sampleRemark" = "Sample Remark" +"oldUsername" = "Current Username" +"currentPassword" = "Current Password" +"newUsername" = "New Username" +"newPassword" = "New Password" +"telegramBotEnable" = "Enable Telegram Bot" +"telegramBotEnableDesc" = "Enables the Telegram bot." +"telegramToken" = "Telegram Token" +"telegramTokenDesc" = "The Telegram bot token obtained from '@BotFather'." +"telegramProxy" = "SOCKS Proxy" +"telegramProxyDesc" = "Enables SOCKS5 proxy for connecting to Telegram. (adjust settings as per guide)" +"telegramAPIServer" = "Telegram API Server" +"telegramAPIServerDesc" = "The Telegram API server to use. Leave blank to use the default server." +"telegramChatId" = "Admin Chat ID" +"telegramChatIdDesc" = "The Telegram Admin Chat ID(s). (comma-separated)(get it here @userinfobot) or (use '/id' command in the bot)" +"telegramNotifyTime" = "Notification Time" +"telegramNotifyTimeDesc" = "The Telegram bot notification time set for periodic reports. (use the crontab time format)" +"tgNotifyBackup" = "Database Backup" +"tgNotifyBackupDesc" = "Send a database backup file with a report." +"tgNotifyLogin" = "Login Notification" +"tgNotifyLoginDesc" = "Get notified about the username, IP address, and time whenever someone attempts to log into your web panel." +"sessionMaxAge" = "Session Duration" +"sessionMaxAgeDesc" = "The duration for which you can stay logged in. (unit: minute)" +"expireTimeDiff" = "Expiration Date Notification" +"expireTimeDiffDesc" = "Get notified about expiration date when reaching this threshold. (unit: day)" +"trafficDiff" = "Traffic Cap Notification" +"trafficDiffDesc" = "Get notified about traffic cap when reaching this threshold. (unit: GB)" +"tgNotifyCpu" = "CPU Load Notification" +"tgNotifyCpuDesc" = "Get notified if CPU load exceeds this threshold. (unit: %)" +"timeZone" = "Time Zone" +"timeZoneDesc" = "Scheduled tasks will run based on this time zone." +"subSettings" = "Subscription" +"subEnable" = "Enable Subscription Service" +"subEnableDesc" = "Enables the subscription service." +"subTitle" = "Subscription Title" +"subTitleDesc" = "Title shown in VPN client" +"subListen" = "Listen IP" +"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)" +"subPort" = "Listen Port" +"subPortDesc" = "The port number for the subscription service. (must be an unused port)" +"subCertPath" = "Public Key Path" +"subCertPathDesc" = "The public key file path for the subscription service. (begins with ‘/‘)" +"subKeyPath" = "Private Key Path" +"subKeyPathDesc" = "The private key file path for the subscription service. (begins with ‘/‘)" +"subPath" = "URI Path" +"subPathDesc" = "The URI path for the subscription service. (begins with ‘/‘ and concludes with ‘/‘)" +"subDomain" = "Listen Domain" +"subDomainDesc" = "The domain name for the subscription service. (leave blank to listen on all domains and IPs)" +"subUpdates" = "Update Intervals" +"subUpdatesDesc" = "The update intervals of the subscription URL in the client apps. (unit: hour)" +"subEncrypt" = "Encode" +"subEncryptDesc" = "The returned content of subscription service will be Base64 encoded." +"subShowInfo" = "Show Usage Info" +"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps." +"subURI" = "Reverse Proxy URI" +"subURIDesc" = "The URI path of the subscription URL for use behind proxies." +"externalTrafficInformEnable" = "External Traffic Inform" +"externalTrafficInformEnableDesc" = "Inform external API on every traffic update." +"externalTrafficInformURI" = "External Traffic Inform URI" +"externalTrafficInformURIDesc" = "Traffic updates are sent to this URI." +"fragment" = "Fragmentation" +"fragmentDesc" = "Enable fragmentation for TLS hello packet." +"fragmentSett" = "Fragmentation Settings" +"noisesDesc" = "Enable Noises." +"noisesSett" = "Noises Settings" +"mux" = "Mux" +"muxDesc" = "Transmit multiple independent data streams within an established data stream." +"muxSett" = "Mux Settings" +"direct" = "Direct Connection" +"directDesc" = "Directly establishes connections with domains or IP ranges of a specific country." +"notifications" = "Notifications" +"certs" = "Certificaties" +"externalTraffic" = "External Traffic" +"dateAndTime" = "Date and Time" +"proxyAndServer" = "Proxy and Server" +"intervals" = "Intervals" +"information" = "Information" +"language" = "Language" +"telegramBotLanguage" = "Telegram Bot Language" + +[pages.xray] +"title" = "Xray Configs" +"save" = "Save" +"restart" = "Restart Xray" +"restartSuccess" = "Xray has been successfully relaunched." +"stopSuccess" = "Xray has been successfully stopped." +"restartError" = "There was an error when rebooting the Xray." +"stopError" = "There was an error when stopping the Xray." +"basicTemplate" = "Basics" +"advancedTemplate" = "Advanced" +"generalConfigs" = "General" +"generalConfigsDesc" = "These options will determine general adjustments." +"logConfigs" = "Log" +"logConfigsDesc" = "Logs may affect your server's efficiency. It is recommended to enable it wisely only in case of your needs" +"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites." +"basicRouting" = "Basic Routing" +"blockConnectionsConfigsDesc" = "These options will block traffic based on the specific requested country." +"directConnectionsConfigsDesc" = "A direct connection ensures that specific traffic is not routed through another server." +"blockips" = "Block IPs" +"blockdomains" = "Block Domains" +"directips" = "Direct IPs" +"directdomains" = "Direct Domains" +"ipv4Routing" = "IPv4 Routing" +"ipv4RoutingDesc" = "These options will route traffic based on a specific destination via IPv4." +"warpRouting" = "WARP Routing" +"warpRoutingDesc" = "These options will route traffic based on a specific destination via WARP." +"Template" = "Advanced Xray Configuration Template" +"TemplateDesc" = "The final Xray config file will be generated based on this template." +"FreedomStrategy" = "Freedom Protocol Strategy" +"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol." +"RoutingStrategy" = "Overall Routing Strategy" +"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests." +"Torrent" = "Block BitTorrent Protocol" +"Inbounds" = "Inbounds" +"InboundsDesc" = "Accepting the specific clients." +"Outbounds" = "Outbounds" +"Balancers" = "Balancers" +"OutboundsDesc" = "Set the outgoing traffic pathway." +"Routings" = "Routing Rules" +"RoutingsDesc" = "The priority of each rule is important!" +"completeTemplate" = "All" +"logLevel" = "Log Level" +"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded." +"accessLog" = "Access Log" +"accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs" +"errorLog" = "Error Log" +"errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs" +"dnsLog" = "DNS Log" +"dnsLogDesc" = "Whether to enable DNS query logs" +"maskAddress" = "Mask Address" +"maskAddressDesc" = "IP address mask, when enabled, will automatically replace the IP address that appears in the log." +"statistics" = "Statistics" +"statsInboundUplink" = "Inbound Upload Statistics" +"statsInboundUplinkDesc" = "Enables the statistics collection for upstream traffic of all inbound proxies." +"statsInboundDownlink" = "Inbound Download Statistics" +"statsInboundDownlinkDesc" = "Enables the statistics collection for downstream traffic of all inbound proxies." +"statsOutboundUplink" = "Outbound Upload Statistics" +"statsOutboundUplinkDesc" = "Enables the statistics collection for upstream traffic of all outbound proxies." +"statsOutboundDownlink" = "Outbound Download Statistics" +"statsOutboundDownlinkDesc" = "Enables the statistics collection for downstream traffic of all outbound proxies." + +[pages.navigation] +"title" = "Practical Navigation" + +[pages.controlledmanagement] +"title" = "Controlled Management" + +[pages.xray.rules] +"first" = "First" +"last" = "Last" +"up" = "Up" +"down" = "Down" +"source" = "Source" +"dest" = "Destination" +"inbound" = "Inbound" +"outbound" = "Outbound" +"balancer" = "Balancer" +"info" = "Info" +"add" = "Add Rule" +"edit" = "Edit Rule" +"useComma" = "Comma-separated items" +"DomainMatcher" = "Domain Match Type" +"SourceIPs" = "Source IP" +"SourcePort" = "Source Port" +"Network" = "Network Type" +"Protocol" = "Transport Protocol" +"Attributes" = "Attributes" +"Domain" = "Domain Address" +"User" = "User" +"Port" = "Port" +"InboundTag" = "Inbound Tag" +"OutboundTag" = "Outbound Tag" +"BalancerTag" = "Load Balancing Tag" + +[pages.xray.outbound] +"addOutbound" = "Add Outbound" +"addReverse" = "Add Reverse" +"editOutbound" = "Edit Outbound" +"editReverse" = "Edit Reverse" +"tag" = "Tag" +"tagDesc" = "Unique Tag" +"address" = "Address" +"reverse" = "Reverse" +"domain" = "Domain" +"type" = "Type" +"bridge" = "Bridge" +"portal" = "Portal" +"link" = "Link" +"intercon" = "Interconnection" +"settings" = "Settings" +"accountInfo" = "Account Information" +"outboundStatus" = "Outbound Status" +"sendThrough" = "Send Through" + +[pages.xray.balancer] +"addBalancer" = "Add Balancer" +"editBalancer" = "Edit Balancer" +"balancerStrategy" = "Strategy" +"balancerSelectors" = "Selectors" +"tag" = "Tag" +"tagDesc" = "Unique Tag" +"balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work." + +[pages.xray.wireguard] +"secretKey" = "Secret Key" +"publicKey" = "Public Key" +"allowedIPs" = "Allowed IPs" +"endpoint" = "Endpoint" +"psk" = "PreShared Key" +"domainStrategy" = "Domain Strategy" + +[pages.xray.dns] +"enable" = "Enable DNS" +"enableDesc" = "Enable built-in DNS server" +"tag" = "DNS Inbound Tag" +"tagDesc" = "This tag will be available as an Inbound tag in routing rules." +"clientIp" = "Client IP" +"clientIpDesc" = "Used to notify the server of the specified IP location during DNS queries" +"disableCache" = "Disable cache" +"disableCacheDesc" = "Disables DNS caching" +"disableFallback" = "Disable Fallback" +"disableFallbackDesc" = "Disables fallback DNS queries" +"disableFallbackIfMatch" = "Disable Fallback If Match" +"disableFallbackIfMatchDesc" = "Disables fallback DNS queries when the matching domain list of the DNS server is hit" +"enableParallelQuery" = "Enable Parallel Query" +"enableParallelQueryDesc" = "Enable parallel DNS queries to multiple servers for faster resolution" +"strategy" = "Query Strategy" +"strategyDesc" = "Overall strategy to resolve domain names" +"add" = "Add Server" +"edit" = "Edit Server" +"domains" = "Domains" +"expectIPs" = "Expect IPs" +"unexpectIPs" = "Unexpect IPs" +"useSystemHosts" = "Use System Hosts" +"useSystemHostsDesc" = "Use the hosts file from an installed system" +"usePreset" = "Use Preset" +"dnsPresetTitle" = "DNS Presets" +"dnsPresetFamily" = "Family" + +[pages.xray.fakedns] +"add" = "Add Fake DNS" +"edit" = "Edit Fake DNS" +"ipPool" = "IP Pool Subnet" +"poolSize" = "Pool Size" + +[pages.settings.security] +"admin" = "Admin credentials" +"twoFactor" = "Two-factor authentication" +"twoFactorEnable" = "Enable 2FA" +"twoFactorEnableDesc" = "Adds an additional layer of authentication to provide more security." +"twoFactorModalSetTitle" = "Enable two-factor authentication" +"twoFactorModalDeleteTitle" = "Disable two-factor authentication" +"twoFactorModalSteps" = "To set up two-factor authentication, perform a few steps:" +"twoFactorModalFirstStep" = "1. Scan this QR code in the app for authentication or copy the token near the QR code and paste it into the app" +"twoFactorModalSecondStep" = "2. Enter the code from the app" +"twoFactorModalRemoveStep" = "Enter the code from the application to remove two-factor authentication." +"twoFactorModalChangeCredentialsTitle" = "Change credentials" +"twoFactorModalChangeCredentialsStep" = "Enter the code from the application to change administrator credentials." +"twoFactorModalSetSuccess" = "Two-factor authentication has been successfully established" +"twoFactorModalDeleteSuccess" = "Two-factor authentication has been successfully deleted" +"twoFactorModalError" = "Wrong code" + +[pages.settings.toasts] +"modifySettings" = "The parameters have been changed." +"getSettings" = "An error occurred while retrieving parameters." +"modifyUserError" = "An error occurred while changing administrator credentials." +"modifyUser" = "You have successfully changed the credentials of the administrator." +"originalUserPassIncorrect" = "The сurrent username or password is invalid" +"userPassMustBeNotEmpty" = "The new username and password is empty" +"getOutboundTrafficError" = "Error getting traffics" +"resetOutboundTrafficError" = "Error in reset outbound traffics" + +[tgbot] +"keyboardClosed" = "❌ Custom keyboard closed!" +"noResult" = "❗ No result!" +"noQuery" = "❌ Query not found! Please use the command again!" +"wentWrong" = "❌ Something went wrong!" +"noIpRecord" = "❗ No IP Record!" +"noInbounds" = "❗ No inbound found!" +"unlimited" = "♾ Unlimited(Reset)" +"add" = "Add" +"month" = "Month" +"months" = "Months" +"day" = "Day" +"days" = "Days" +"hours" = "Hours" +"minutes" = "Minutes" +"unknown" = "Unknown" +"inbounds" = "Inbounds" +"clients" = "Clients" +"offline" = "🔴 Offline" +"online" = "🟢 Online" + +[tgbot.commands] +"unknown" = "❗ Unknown command." +"pleaseChoose" = "👇 Please choose:\r\n" +"help" = "🤖 Welcome to this bot! It's designed to offer specific data from the web panel and allows you to make modifications as needed.\r\n\r\n" +"start" = "👋 Hello {{ .Firstname }}.\r\n" +"welcome" = "🤖 Welcome to {{ .Hostname }} management bot.\r\n" +"status" = "✅ Bot is OK!" +"usage" = "❗ Please provide a text to search!" +"getID" = "🆔 Your ID: {{ .ID }}" +"helpAdminCommands" = "To restart Xray Core:\r\n/restart\r\n\r\nTo search for customer emails:\r\n/usage [email]\r\n\r\nTo search inbound (with customer statistics):\r\n/inbound [notes]\r\n\r\nTelegram chat ID:\r\n/id\r\n\r\nOne-click configuration:\r\n/oneclick\r\n\r\nSubscription conversion:\r\n/subconverter\r\n\r\nRestart〔X-Panel〕:\r\n/restartx" +"helpClientCommands" = "To search for statistics, use the following command:\r\n\r\n/usage [Email]\r\n\r\nTelegram Chat ID:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ Operation successful!" +"restartFailed" = "❗ Error in operation.\r\n\r\nError: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core is not running." +"startDesc" = "Show the main menu" +"helpDesc" = "Bot help" +"statusDesc" = "Check bot status" +"idDesc" = "Show your Telegram ID" + +[tgbot.messages] +"cpuThreshold" = "🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%" +"selectUserFailed" = "❌ Error in user selection!" +"userSaved" = "✅ Telegram User saved." +"loginSuccess" = "✅ Logged in to the panel successfully.\r\n" +"loginFailed" = "❗️Login attempt to the panel failed.\r\n" +"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n" +"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n" +"hostname" = "💻 Host: {{ .Hostname }}\r\n" +"version" = "🚀 X-Panel Version: v{{ .Version }}\r\n" +"xrayVersion" = "📡 Xray Core Version: v{{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Uptime: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 System Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" +"username" = "👤 Username: {{ .Username }}\r\n" +"password" = "👤 Password: {{ .Password }}\r\n" +"time" = "⏰ Time: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Port: {{ .Port }}\r\n" +"expire" = "📅 Expire Date: {{ .Time }}\r\n" +"expireIn" = "📅 Expire In: {{ .Time }}\r\n" +"active" = "💡 Active: {{ .Enable }}\r\n" +"enabled" = "🚨 Enabled: {{ .Enable }}\r\n" +"online" = "🌐 Connection status: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" +"download" = "🔽 Download: ↓{{ .Download }}\r\n" +"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n" +"onlinesCount" = "🌐 Online Clients: {{ .Count }}\r\n" +"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n" +"yes" = "✅ Yes" +"no" = "❌ No" +"received_id" = "🔑📥 ID updated." +"received_password" = "🔑📥 Password updated." +"received_email" = "📧📥 Email updated." +"received_comment" = "💬📥 Comment updated." +"id_prompt" = "🔑 Default ID: {{ .ClientId }}\n\nEnter your id." +"pass_prompt" = "🔑 Default Password: {{ .ClientPassword }}\n\nEnter your password." +"email_prompt" = "📧 Default Email: {{ .ClientEmail }}\n\nEnter your email." +"comment_prompt" = "💬 Default Comment: {{ .ClientComment }}\n\nEnter your Comment." +"inbound_client_data_id" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!" +"inbound_client_data_pass" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 Password: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!" +"cancel" = "❌ Process Canceled! \n\nYou can /start again anytime. 🔄" +"error_add_client" = "⚠️ Error:\n\n {{ .error }}" +"using_default_value" = "Okay, I'll stick with the default value. 😊" +"incorrect_input" ="Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫" +"AreYouSure" = "Are you sure? 🤔" +"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ✅ Success" +"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠️ Error: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Traffic reset process finished for all clients." + +[tgbot.buttons] +"closeKeyboard" = "❌ Close Keyboard" +"cancel" = "❌ Cancel" +"cancelReset" = "❌ Cancel Reset" +"cancelIpLimit" = "❌ Cancel IP Limit" +"confirmResetTraffic" = "✅ Confirm Reset Traffic?" +"confirmClearIps" = "✅ Confirm Clear IPs?" +"confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?" +"confirmToggle" = "✅ Confirm Enable/Disable User?" +"dbBackup" = "Get DB Backup" +"serverUsage" = "🌐Server Usage" +"getInbounds" = "Get Inbounds" +"depleteSoon" = "Deplete Soon" +"clientUsage" = "Get Usage" +"onlines" = "Online Clients" +"commands" = "Commands" +"refresh" = "🔄 Refresh" +"clearIPs" = "❌ Clear IPs" +"removeTGUser" = "❌ Remove Telegram User" +"selectTGUser" = "👤 Select Telegram User" +"selectOneTGUser" = "👤 Select a Telegram User:" +"resetTraffic" = "📈 Reset Traffic" +"resetExpire" = "📅 Change Expiry Date" +"ipLog" = "🔢 IP Log" +"ipLimit" = "🔢 IP Limit" +"setTGUser" = "👤 Set Telegram User" +"toggle" = "🔘 Enable / Disable" +"custom" = "🔢 Custom" +"confirmNumber" = "✅ Confirm: {{ .Num }}" +"confirmNumberAdd" = "✅ Confirm adding: {{ .Num }}" +"limitTraffic" = "🚧 Traffic Limit" +"getBanLogs" = "Get Ban Logs" +"allClients" = "All Clients" +"addClient" = "Add Client" +"submitDisable" = "Submit As Disable ☑️" +"submitEnable" = "Submit As Enable ✅" +"use_default" = "🏷️ Use default" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Password" +"change_email" = "⚙️📧 Email" +"change_comment" = "⚙️💬 Comment" +"ResetAllTraffics" = "Reset All Traffics" +"SortedTrafficUsageReport" = "Traffic Usage Report" +"oneClick" = "🚀 One-click configuration" +"subconverter" = "🔄 Subscription conversion" + +[tgbot.answers] +"successfulOperation" = "✅ Operation successful!" +"errorOperation" = "❗ Error in operation." +"getInboundsFailed" = "❌ Failed to get inbounds." +"getClientsFailed" = "❌ Failed to get clients." +"canceled" = "❌ {{ .Email }}: Operation canceled." +"clientRefreshSuccess" = "✅ {{ .Email }}: Client refreshed successfully." +"IpRefreshSuccess" = "✅ {{ .Email }}: IPs refreshed successfully." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Client's Telegram User refreshed successfully." +"resetTrafficSuccess" = "✅ {{ .Email }}: Traffic reset successfully." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Traffic limit saved successfully." +"expireResetSuccess" = "✅ {{ .Email }}: Expire days reset successfully." +"resetIpSuccess" = "✅ {{ .Email }}: IP limit {{ .Count }} saved successfully." +"clearIpSuccess" = "✅ {{ .Email }}: IPs cleared successfully." +"getIpLog" = "✅ {{ .Email }}: Get IP Log." +"getUserInfo" = "✅ {{ .Email }}: Get Telegram User Info." +"removedTGUserSuccess" = "✅ {{ .Email }}: Telegram User removed successfully." +"enableSuccess" = "✅ {{ .Email }}: Enabled successfully." +"disableSuccess" = "✅ {{ .Email }}: Disabled successfully." +"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your admin to use your Telegram ChatID in your configuration(s).\r\n\r\nYour ChatID: {{ .TgUserID }}" +"chooseClient" = "Choose a Client for Inbound {{ .Inbound }}" +"chooseInbound" = "Choose an Inbound" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml new file mode 100644 index 0000000..cf743b4 --- /dev/null +++ b/web/translation/translate.es_ES.toml @@ -0,0 +1,740 @@ +"username" = "Nombre de Usuario" +"password" = "Contraseña" +"login" = "Acceder" +"confirm" = "Confirmar" +"cancel" = "Cancelar" +"close" = "Cerrar" +"create" = "Crear" +"update" = "Actualizar" +"copy" = "Copiar" +"copied" = "Copiado" +"download" = "Descargar" +"remark" = "Nota" +"enable" = "Habilitar" +"protocol" = "Protocolo" +"search" = "Buscar" +"filter" = "Filtrar" +"loading" = "Cargando..." +"second" = "Segundo" +"minute" = "Minuto" +"hour" = "Hora" +"day" = "Día" +"check" = "Verificar" +"indefinite" = "Indefinido" +"unlimited" = "Ilimitado" +"none" = "None" +"qrCode" = "Código QR" +"info" = "Más Información" +"edit" = "Editar" +"delete" = "Eliminar" +"reset" = "Restablecer" +"noData" = "Sin datos." +"copySuccess" = "Copiado exitosamente" +"sure" = "Seguro" +"encryption" = "Encriptación" +"useIPv4ForHost" = "Usar IPv4 para el host" +"transmission" = "Transmisión" +"host" = "Anfitrión" +"path" = "Ruta" +"camouflage" = "Camuflaje" +"status" = "Estado" +"enabled" = "Habilitado" +"disabled" = "Deshabilitado" +"depleted" = "Agotado" +"depletingSoon" = "Agotándose" +"offline" = "fuera de línea" +"online" = "en línea" +"domainName" = "Nombre de dominio" +"monitor" = "Listening IP" +"certificate" = "Certificado Digital" +"fail" = "Falló" +"comment" = "Comentario" +"success" = "Éxito" +"lastOnline" = "Última conexión" +"getVersion" = "Obtener versión" +"install" = "Instalar" +"clients" = "Clientes" +"usage" = "Uso" +"twoFactorCode" = "Código" +"remained" = "Restante" +"security" = "Seguridad" +"secAlertTitle" = "Alerta de Seguridad" +"secAlertSsl" = "Esta conexión no es segura. Por favor, evite ingresar información sensible hasta que se active TLS para la protección de datos." +"secAlertConf" = "Ciertas configuraciones son vulnerables a ataques. Se recomienda reforzar los protocolos de seguridad para prevenir posibles violaciones." +"secAlertSSL" = "El panel carece de una conexión segura. Por favor, instale un certificado TLS para la protección de datos." +"secAlertPanelPort" = "El puerto predeterminado del panel es vulnerable. Por favor, configure un puerto aleatorio o específico." +"secAlertPanelURI" = "La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja." +"secAlertSubURI" = "La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja." +"secAlertSubJsonURI" = "La ruta URI JSON predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja." +"emptyDnsDesc" = "No hay servidores DNS añadidos." +"emptyFakeDnsDesc" = "No hay servidores Fake DNS añadidos." +"emptyBalancersDesc" = "No hay balanceadores añadidos." +"emptyReverseDesc" = "No hay proxies inversos añadidos." +"somethingWentWrong" = "Algo salió mal" + +[menu] +"theme" = "Tema" +"dark" = "Oscuro" +"ultraDark" = "Ultra Oscuro" +"dashboard" = "Estado del Sistema" +"inbounds" = "Entradas" +"settings" = "Configuraciones" +"xray" = "Ajustes Xray" +"logout" = "Cerrar Sesión" +"link" = "Gestionar" + +[pages.login] +"hello" = "Hola" +"title" = "Bienvenido" +"loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente." + +[pages.login.toasts] +"invalidFormData" = "El formato de los datos de entrada es inválido." +"emptyUsername" = "Por favor ingresa el nombre de usuario." +"emptyPassword" = "Por favor ingresa la contraseña." +"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto." +"successLogin" = "Has iniciado sesión en tu cuenta correctamente." + +[pages.index] +"title" = "Estado del Sistema" +"cpu" = "CPU" +"logicalProcessors" = "Procesadores lógicos" +"frequency" = "Frecuencia" +"swap" = "Intercambio" +"storage" = "Almacenamiento" +"memory" = "RAM" +"threads" = "Hilos" +"xrayStatus" = "Xray" +"stopXray" = "Detener" +"restartXray" = "Reiniciar" +"xraySwitch" = "Versión" +"xraySwitchClick" = "Elige la versión a la que deseas cambiar." +"xraySwitchClickDesk" = "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales." +"xrayStatusUnknown" = "Desconocido" +"xrayStatusRunning" = "En ejecución" +"xrayStatusStop" = "Detenido" +"xrayStatusError" = "Error" +"xrayErrorPopoverTitle" = "Se produjo un error al ejecutar Xray" +"operationHours" = "Tiempo de Funcionamiento" +"systemLoad" = "Carga del Sistema" +"systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos" +"connectionCount" = "Número de Conexiones" +"ipAddresses" = "Direcciones IP" +"toggleIpVisibility" = "Alternar visibilidad de la IP" +"overallSpeed" = "Velocidad general" +"upload" = "Subida" +"download" = "Descarga" +"totalData" = "Datos totales" +"sent" = "Enviado" +"received" = "Recibido" +"documentation" = "Documentación" +"xraySwitchVersionDialog" = "¿Realmente deseas cambiar la versión de Xray?" +"xraySwitchVersionDialogDesc" = "Esto cambiará la versión de Xray a #version#." +"xraySwitchVersionPopover" = "Xray se actualizó correctamente" +"geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?" +"geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#." +"geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos." +"geofilesUpdateAll" = "Actualizar todo" +"geofileUpdatePopover" = "Geofichero actualizado correctamente" +"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." +"logs" = "Registros" +"config" = "Configuración" +"backup" = "Сopia de Seguridad" +"backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos" +"exportDatabase" = "Copia de seguridad" +"exportDatabaseDesc" = "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo." +"importDatabase" = "Restaurar" +"importDatabaseDesc" = "Haz clic para seleccionar y cargar un archivo .db desde tu dispositivo para restaurar tu base de datos desde una copia de seguridad." +"importDatabaseSuccess" = "La base de datos se ha importado correctamente" +"importDatabaseError" = "Ocurrió un error al importar la base de datos" +"readDatabaseError" = "Ocurrió un error al leer la base de datos" +"getDatabaseError" = "Ocurrió un error al obtener la base de datos" +"getConfigError" = "Ocurrió un error al obtener el archivo de configuración" + +[pages.inbounds] +"allTimeTraffic" = "Tráfico Total" +"allTimeTrafficUsage" = "Uso total de todos los tiempos" +"title" = "Entradas" +"totalDownUp" = "Subidas/Descargas Totales" +"totalUsage" = "Uso Total" +"inboundCount" = "Número de Entradas" +"operate" = "Menú" +"enable" = "Habilitar" +"remark" = "Notas" +"protocol" = "Protocolo" +"port" = "Puerto" +"portMap" = "Puertos de Destino" +"traffic" = "Tráfico" +"details" = "Detalles" +"transportConfig" = "Transporte" +"expireDate" = "Fecha de Expiración" +"createdAt" = "Creado" +"updatedAt" = "Actualizado" +"resetTraffic" = "Restablecer Tráfico" +"addInbound" = "Agregar Entrada" +"generalActions" = "Acciones Generales" +"autoRefresh" = "Auto-actualizar" +"autoRefreshInterval" = "Intervalo" +"modifyInbound" = "Modificar Entrada" +"deleteInbound" = "Eliminar Entrada" +"deleteInboundContent" = "¿Confirmar eliminación de entrada?" +"deleteClient" = "Eliminar cliente" +"deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?" +"resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?" +"copyLink" = "Copiar Enlace" +"address" = "Dirección" +"network" = "Red" +"destinationPort" = "Puerto de Destino" +"targetAddress" = "Dirección de Destino" +"monitorDesc" = "Dejar en blanco por defecto" +"meansNoLimit" = "= illimitata. (unidad: GB)" +"totalFlow" = "Flujo Total" +"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar" +"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada" +"certificatePath" = "Ruta Cert" +"certificateContent" = "Datos Cert" +"publicKey" = "Clave Pública" +"privatekey" = "Clave Privada" +"clickOnQRcode" = "Haz clic en el Código QR para Copiar" +"client" = "Cliente" +"export" = "Exportar Enlaces" +"clone" = "Clonar" +"cloneInbound" = "Clonar Entradas" +"cloneInboundContent" = "Se aplicarán todas las configuraciones de esta entrada, excepto el Puerto, la IP de Escucha y los Clientes, al clon." +"cloneInboundOk" = "Clonar" +"resetAllTraffic" = "Restablecer Tráfico de Todas las Entradas" +"resetAllTrafficTitle" = "Restablecer tráfico de todas las entradas" +"resetAllTrafficContent" = "¿Estás seguro de que deseas restablecer el tráfico de todas las entradas?" +"resetInboundClientTraffics" = "Restablecer Tráfico de Clientes" +"resetInboundClientTrafficTitle" = "Restablecer todo el tráfico de clientes" +"resetInboundClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para los clientes de esta entrada?" +"resetAllClientTraffics" = "Restablecer Tráfico de Todos los Clientes" +"resetAllClientTrafficTitle" = "Restablecer todo el tráfico de clientes" +"resetAllClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para todos los clientes?" +"delDepletedClients" = "Eliminar Clientes Agotados" +"delDepletedClientsTitle" = "Eliminar clientes agotados" +"delDepletedClientsContent" = "¿Estás seguro de que deseas eliminar todos los clientes agotados?" +"email" = "Email" +"emailDesc" = "Por favor proporciona una dirección de correo electrónico única." +"IPLimit" = "Límite de IP" +"IPLimitDesc" = "Desactiva la entrada si la cantidad supera el valor ingresado (ingresa 0 para desactivar el límite de IP)." +"IPLimitlog" = "Registro de IP" +"IPLimitlogDesc" = "Registro de historial de IPs (antes de habilitar la entrada después de que haya sido desactivada por el límite de IP, debes borrar el registro)." +"IPLimitlogclear" = "Limpiar el Registro" +"setDefaultCert" = "Establecer certificado desde el panel" +"telegramDesc" = "Por favor, proporciona el ID de Chat de Telegram. (usa el comando '/id' en el bot) o (@userinfobot)" +"subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones." +"info" = "Info" +"same" = "misma" +"inboundData" = "Datos de entrada" +"exportInbound" = "Exportación entrante" +"import" = "Importar" +"importInbound" = "Importar un entrante" + +[pages.client] +"add" = "Agregar Cliente" +"edit" = "Editar Cliente" +"submitAdd" = "Agregar Cliente" +"submitEdit" = "Guardar Cambios" +"clientCount" = "Número de Clientes" +"bulk" = "Agregar en Lote" +"method" = "Método" +"first" = "Primero" +"last" = "Último" +"prefix" = "Prefijo" +"postfix" = "Sufijo" +"delayedStart" = "Iniciar después del primer uso" +"expireDays" = "Duración" +"days" = "Día(s)" +"renew" = "Renovación automática" +"renewDesc" = "Renovación automática después de la expiración. (0 = desactivar) (unidad: día)" + +[pages.inbounds.toasts] +"obtain" = "Recibir" +"updateSuccess" = "La actualización fue exitosa" +"logCleanSuccess" = "El registro ha sido limpiado" +"inboundsUpdateSuccess" = "Entradas actualizadas correctamente" +"inboundUpdateSuccess" = "Entrada actualizada correctamente" +"inboundCreateSuccess" = "Entrada creada correctamente" +"inboundDeleteSuccess" = "Entrada eliminada correctamente" +"inboundClientAddSuccess" = "Cliente(s) de entrada añadido(s)" +"inboundClientDeleteSuccess" = "Cliente de entrada eliminado" +"inboundClientUpdateSuccess" = "Cliente de entrada actualizado" +"delDepletedClientsSuccess" = "Todos los clientes agotados fueron eliminados" +"resetAllClientTrafficSuccess" = "Todo el tráfico del cliente ha sido reiniciado" +"resetAllTrafficSuccess" = "Todo el tráfico ha sido reiniciado" +"resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado" +"trafficGetError" = "Error al obtener los tráficos" +"getNewX25519CertError" = "Error al obtener el certificado X25519." +"getNewmldsa65Error" = "Error al obtener el certificado mldsa65." + +[pages.inbounds.stream.general] +"request" = "Pedido" +"response" = "Respuesta" +"name" = "Nombre" +"value" = "Valor" + +[pages.inbounds.stream.tcp] +"version" = "Versión" +"method" = "Método" +"path" = "Camino" +"status" = "Estado" +"statusDescription" = "Descripción de la Situación" +"requestHeader" = "Encabezado de solicitud" +"responseHeader" = "Encabezado de respuesta" + +[pages.settings] +"title" = "Configuraciones" +"save" = "Guardar" +"infoDesc" = "Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios." +"restartPanel" = "Reiniciar Panel" +"restartPanelDesc" = "¿Está seguro de que desea reiniciar el panel? Haga clic en Aceptar para reiniciar después de 3 segundos. Si no puede acceder al panel después de reiniciar, por favor, consulte la información de registro del panel en el servidor." +"restartPanelSuccess" = "El panel se reinició correctamente" +"actions" = "Acciones" +"resetDefaultConfig" = "Restablecer a Configuración Predeterminada" +"panelSettings" = "Configuraciones del Panel" +"securitySettings" = "Configuraciones de Seguridad" +"TGBotSettings" = "Configuraciones de Bot de Telegram" +"panelListeningIP" = "IP de Escucha del Panel" +"panelListeningIPDesc" = "Dejar en blanco por defecto para monitorear todas las IPs." +"panelListeningDomain" = "Dominio de Escucha del Panel" +"panelListeningDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs." +"panelPort" = "Puerto del Panel" +"panelPortDesc" = "El puerto utilizado para mostrar este panel." +"publicKeyPath" = "Ruta del Archivo de Clave Pública del Certificado del Panel" +"publicKeyPathDesc" = "Complete con una ruta absoluta que comience con." +"privateKeyPath" = "Ruta del Archivo de Clave Privada del Certificado del Panel" +"privateKeyPathDesc" = "Complete con una ruta absoluta que comience con." +"panelUrlPath" = "Ruta Raíz de la URL del Panel" +"panelUrlPathDesc" = "Debe empezar con '/' y terminar con." +"pageSize" = "Tamaño de paginación" +"pageSizeDesc" = "Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar" +"remarkModel" = "Modelo de observación y carácter de separación" +"datepicker" = "selector de fechas" +"datepickerPlaceholder" = "Seleccionar fecha" +"datepickerDescription" = "El tipo de calendario selector especifica la fecha de vencimiento" +"sampleRemark" = "Observación de muestra" +"oldUsername" = "Nombre de Usuario Actual" +"currentPassword" = "Contraseña Actual" +"newUsername" = "Nuevo Nombre de Usuario" +"newPassword" = "Nueva Contraseña" +"telegramBotEnable" = "Habilitar bot de Telegram" +"telegramBotEnableDesc" = "Conéctese a las funciones de este panel a través del bot de Telegram." +"telegramToken" = "Token de Telegram" +"telegramTokenDesc" = "Debe obtener el token del administrador de bots de Telegram @botfather." +"telegramProxy" = "Socks5 Proxy" +"telegramProxyDesc" = "Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía." +"telegramAPIServer" = "API Server de Telegram" +"telegramAPIServerDesc" = "El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado." +"telegramChatId" = "IDs de Chat de Telegram para Administradores" +"telegramChatIdDesc" = "IDs de Chat múltiples separados por comas. Use @userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat." +"telegramNotifyTime" = "Hora de Notificación del Bot de Telegram" +"telegramNotifyTimeDesc" = "Usar el formato de tiempo de Crontab." +"tgNotifyBackup" = "Respaldo de Base de Datos" +"tgNotifyBackupDesc" = "Incluir archivo de respaldo de base de datos con notificación de informe." +"tgNotifyLogin" = "Notificación de Inicio de Sesión" +"tgNotifyLoginDesc" = "Muestra el nombre de usuario, dirección IP y hora cuando alguien intenta iniciar sesión en su panel." +"sessionMaxAge" = "Edad Máxima de Sesión" +"sessionMaxAgeDesc" = "La duración de una sesión de inicio de sesión (unidad: minutos)." +"expireTimeDiff" = "Umbral de Expiración para Notificación" +"expireTimeDiffDesc" = "Reciba notificaciones sobre la expiración de la cuenta antes del umbral (unidad: días)." +"trafficDiff" = "Umbral de Tráfico para Notificación" +"trafficDiffDesc" = "Reciba notificaciones sobre el agotamiento del tráfico antes de alcanzar el umbral (unidad: GB)." +"tgNotifyCpu" = "Umbral de Alerta de Porcentaje de CPU" +"tgNotifyCpuDesc" = "Reciba notificaciones si el uso de la CPU supera este umbral (unidad: %)." +"timeZone" = "Zona Horaria" +"timeZoneDesc" = "Las tareas programadas se ejecutan de acuerdo con la hora en esta zona horaria." +"subSettings" = "Suscripción" +"subEnable" = "Habilitar Servicio" +"subEnableDesc" = "Función de suscripción con configuración separada." +"subTitle" = "Título de la Suscripción" +"subTitleDesc" = "Título mostrado en el cliente de VPN" +"subListen" = "Listening IP" +"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs." +"subPort" = "Puerto de Suscripción" +"subPortDesc" = "El número de puerto para el servicio de suscripción debe estar sin usar en el servidor." +"subCertPath" = "Ruta del Archivo de Clave Pública del Certificado de Suscripción" +"subCertPathDesc" = "Complete con una ruta absoluta que comience con '/'" +"subKeyPath" = "Ruta del Archivo de Clave Privada del Certificado de Suscripción" +"subKeyPathDesc" = "Complete con una ruta absoluta que comience con '/'" +"subPath" = "Ruta Raíz de la URL de Suscripción" +"subPathDesc" = "Debe empezar con '/' y terminar con '/'" +"subDomain" = "Dominio de Escucha" +"subDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs." +"subUpdates" = "Intervalos de Actualización de Suscripción" +"subUpdatesDesc" = "Horas de intervalo entre actualizaciones en la aplicación del cliente." +"subEncrypt" = "Encriptar configuraciones" +"subEncryptDesc" = "Encriptar las configuraciones devueltas en la suscripción." +"subShowInfo" = "Mostrar información de uso" +"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración." +"subURI" = "URI de proxy inverso" +"externalTrafficInformEnable" = "Informe de tráfico externo" +"externalTrafficInformEnableDesc" = "Informar a la API externa sobre cada actualización de tráfico." +"externalTrafficInformURI" = "URI de información de tráfico externo" +"externalTrafficInformURIDesc" = "Las actualizaciones de tráfico se envían a este URI." +"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy" +"fragment" = "Fragmentación" +"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS" +"fragmentSett" = "Configuración de Fragmentación" +"noisesDesc" = "Activar Noises." +"noisesSett" = "Configuración de Noises" +"mux" = "Mux" +"muxDesc" = "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido." +"muxSett" = "Configuración Mux" +"direct" = "Conexión Directa" +"directDesc" = "Establece conexiones directas con dominios o rangos de IP de un país específico." +"notifications" = "Notificaciones" +"certs" = "Certificados" +"externalTraffic" = "Tráfico Externo" +"dateAndTime" = "Fecha y Hora" +"proxyAndServer" = "Proxy y Servidor" +"intervals" = "Intervalos" +"information" = "Información" +"language" = "Idioma" +"telegramBotLanguage" = "Idioma del Bot de Telegram" + +[pages.xray] +"title" = "Xray Configuración" +"save" = "Guardar configuración" +"restart" = "Reiniciar Xray" +"restartSuccess" = "Xray se ha reiniciado correctamente" +"stopSuccess" = "Xray se ha detenido correctamente" +"restartError" = "Ocurrió un error al reiniciar Xray." +"stopError" = "Ocurrió un error al detener Xray." +"basicTemplate" = "Plantilla Básica" +"advancedTemplate" = "Plantilla Avanzada" +"generalConfigs" = "Configuraciones Generales" +"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales." +"logConfigs" = "Registro" +"logConfigsDesc" = "Los registros pueden afectar la eficiencia de su servidor. Se recomienda habilitarlos sabiamente solo en caso de sus necesidades." +"blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos." +"basicRouting" = "Enrutamiento Básico" +"blockConnectionsConfigsDesc" = "Estas opciones bloquearán el tráfico según el país solicitado específico." +"directConnectionsConfigsDesc" = "Una conexión directa asegura que el tráfico específico no sea enrutado a través de otro servidor." +"blockips" = "Bloquear IPs" +"blockdomains" = "Bloquear Dominios" +"directips" = "IPs Directas" +"directdomains" = "Dominios Directos" +"ipv4Routing" = "Enrutamiento IPv4" +"ipv4RoutingDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4." +"warpRouting" = "Enrutamiento WARP" +"warpRoutingDesc" = "Precaución: Antes de usar estas opciones, instale WARP en modo de proxy socks5 en su servidor siguiendo los pasos en el GitHub del panel. WARP enrutará el tráfico a los sitios web a través de los servidores de Cloudflare." +"Template" = "Plantilla de Configuración de Xray" +"TemplateDesc" = "Genera el archivo de configuración final de Xray basado en esta plantilla." +"FreedomStrategy" = "Configurar Estrategia para el Protocolo Freedom" +"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom." +"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios" +"RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS." +"Torrent" = "Prohibir Uso de BitTorrent" +"Inbounds" = "Entrante" +"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos." +"Outbounds" = "Salidas" +"Balancers" = "Equilibradores" +"OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor." +"Routings" = "Reglas de enrutamiento" +"RoutingsDesc" = "¡La prioridad de cada regla es importante!" +"completeTemplate" = "Todos" +"logLevel" = "Nivel de registro" +"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse." +"accessLog" = "Registro de acceso" +"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso" +"errorLog" = "Registro de Errores" +"errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores." +"dnsLog" = "Registro DNS" +"dnsLogDesc" = "Si habilitar los registros de consulta DNS" +"maskAddress" = "Enmascarar Dirección" +"maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro." +"statistics" = "Estadísticas" +"statsInboundUplink" = "Estadísticas de Subida de Entrada" +"statsInboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de entrada." +"statsInboundDownlink" = "Estadísticas de Bajada de Entrada" +"statsInboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de entrada." +"statsOutboundUplink" = "Estadísticas de Subida de Salida" +"statsOutboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida." +"statsOutboundDownlink" = "Estadísticas de Bajada de Salida" +"statsOutboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida." + +[pages.xray.rules] +"first" = "Primero" +"last" = "Último" +"up" = "Arriba" +"down" = "Abajo" +"source" = "Fuente" +"dest" = "Destino" +"inbound" = "Entrante" +"outbound" = "Saliente" +"balancer" = "Equilibrador" +"info" = "Información" +"add" = "Agregar Regla" +"edit" = "Editar Regla" +"useComma" = "Elementos separados por comas" + +[pages.xray.outbound] +"addOutbound" = "Agregar salida" +"addReverse" = "Agregar reverso" +"editOutbound" = "Editar salida" +"editReverse" = "Editar reverso" +"tag" = "Etiqueta" +"tagDesc" = "etiqueta única" +"address" = "Dirección" +"reverse" = "Reverso" +"domain" = "Dominio" +"type" = "Tipo" +"bridge" = "puente" +"portal" = "portal" +"link" = "Enlace" +"intercon" = "Interconexión" +"settings" = "Configuración" +"accountInfo" = "Información de la Cuenta" +"outboundStatus" = "Estado de Salida" +"sendThrough" = "Enviar a través de" + +[pages.xray.balancer] +"addBalancer" = "Agregar equilibrador" +"editBalancer" = "Editar balanceador" +"balancerStrategy" = "Estrategia" +"balancerSelectors" = "Selectores" +"tag" = "Etiqueta" +"tagDesc" = "etiqueta única" +"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag." + +[pages.xray.wireguard] +"secretKey" = "Llave secreta" +"publicKey" = "Llave pública" +"allowedIPs" = "IP permitidas" +"endpoint" = "Punto final" +"psk" = "Clave precompartida" +"domainStrategy" = "Estrategia de dominio" + +[pages.xray.dns] +"enable" = "Habilitar DNS" +"enableDesc" = "Habilitar servidor DNS incorporado" +"tag" = "Etiqueta de Entrada DNS" +"tagDesc" = "Esta etiqueta estará disponible como una etiqueta de entrada en las reglas de enrutamiento." +"clientIp" = "IP del cliente" +"clientIpDesc" = "Se utiliza para notificar al servidor la ubicación IP especificada durante las consultas DNS" +"disableCache" = "Desactivar caché" +"disableCacheDesc" = "Desactiva el almacenamiento en caché de DNS" +"disableFallback" = "Desactivar respaldo" +"disableFallbackDesc" = "Desactiva las consultas DNS de respaldo" +"disableFallbackIfMatch" = "Desactivar respaldo si coincide" +"disableFallbackIfMatchDesc" = "Desactiva las consultas DNS de respaldo cuando se acierta en la lista de dominios coincidentes del servidor DNS" +"strategy" = "Estrategia de Consulta" +"strategyDesc" = "Estrategia general para resolver nombres de dominio" +"add" = "Agregar Servidor" +"edit" = "Editar Servidor" +"domains" = "Dominios" +"expectIPs" = "IPs esperadas" +"unexpectIPs" = "IPs inesperadas" +"useSystemHosts" = "Usar Hosts del sistema" +"useSystemHostsDesc" = "Usar el archivo hosts de un sistema instalado" +"usePreset" = "Usar plantilla" +"dnsPresetTitle" = "Plantillas DNS" +"dnsPresetFamily" = "Familiar" + +[pages.xray.fakedns] +"add" = "Agregar DNS Falso" +"edit" = "Editar DNS Falso" +"ipPool" = "Subred del grupo de IP" +"poolSize" = "Tamaño del grupo" + +[pages.settings.security] +"admin" = "Credenciales de administrador" +"twoFactor" = "Autenticación de dos factores" +"twoFactorEnable" = "Habilitar 2FA" +"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad." +"twoFactorModalSetTitle" = "Activar autenticación de dos factores" +"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores" +"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:" +"twoFactorModalFirstStep" = "1. Escanea este código QR en la aplicación de autenticación o copia el token cerca del código QR y pégalo en la aplicación" +"twoFactorModalSecondStep" = "2. Ingresa el código de la aplicación" +"twoFactorModalRemoveStep" = "Ingresa el código de la aplicación para eliminar la autenticación de dos factores." +"twoFactorModalChangeCredentialsTitle" = "Cambiar credenciales" +"twoFactorModalChangeCredentialsStep" = "Ingrese el código de la aplicación para cambiar las credenciales del administrador." +"twoFactorModalSetSuccess" = "La autenticación de dos factores se ha establecido con éxito" +"twoFactorModalDeleteSuccess" = "La autenticación de dos factores se ha eliminado con éxito" +"twoFactorModalError" = "Código incorrecto" + +[pages.settings.toasts] +"modifySettings" = "Los parámetros han sido modificados." +"getSettings" = "Ocurrió un error al obtener los parámetros." +"modifyUserError" = "Ocurrió un error al cambiar las credenciales del administrador." +"modifyUser" = "Has cambiado exitosamente las credenciales del administrador." +"originalUserPassIncorrect" = "Nombre de usuario o contraseña original incorrectos" +"userPassMustBeNotEmpty" = "El nuevo nombre de usuario y la nueva contraseña no pueden estar vacíos" +"getOutboundTrafficError" = "Error al obtener el tráfico saliente" +"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente" + +[tgbot] +"keyboardClosed" = "❌ Teclado cerrado!" +"noResult" = "❗ ¡No hay resultados!" +"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!" +"wentWrong" = "❌ ¡Algo salió mal!" +"noIpRecord" = "❗ ¡No hay registro de IP!" +"noInbounds" = "❗ ¡No se encontraron entradas!" +"unlimited" = "♾ Ilimitado (Restablecer)" +"add" = "Añadir" +"month" = "Mes" +"months" = "Meses" +"day" = "Día" +"days" = "Días" +"hours" = "Horas" +"minutes" = "Minutos" +"unknown" = "Desconocido" +"inbounds" = "Entradas" +"clients" = "Clientes" +"offline" = "🔴 Desconectado" +"online" = "🟢 En línea" + +[tgbot.commands] +"unknown" = "❗ Comando desconocido" +"pleaseChoose" = "👇 Por favor elige:\r\n" +"help" = "🤖 ¡Bienvenido a este bot! Está diseñado para ofrecerte datos específicos del servidor y te permite hacer modificaciones según sea necesario.\r\n\r\n" +"start" = "👋 Hola {{ .Firstname }}.\r\n" +"welcome" = "🤖 Bienvenido al bot de gestión de {{ .Hostname }}.\r\n" +"status" = "✅ ¡El bot está bien!" +"usage" = "❗ ¡Por favor proporciona un texto para buscar!" +"getID" = "🆔 Tu ID: {{ .ID }}" +"helpAdminCommands" = "Para reiniciar Xray Core:\r\n/restart\r\n\r\nPara buscar un correo electrónico de cliente:\r\n/usage [Correo electrónico]\r\n\r\nPara buscar entradas (con estadísticas de cliente):\r\n/inbound [Observación]\r\n\r\nID de Chat de Telegram:\r\n/id" +"helpClientCommands" = "Para buscar estadísticas, utiliza el siguiente comando:\r\n/usage [Correo electrónico]\r\n\r\nID de Chat de Telegram:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ ¡Operación exitosa!" +"restartFailed" = "❗ Error en la operación.\r\n\r\nError: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core no está en ejecución." +"startDesc" = "Mostrar el menú principal" +"helpDesc" = "Ayuda del bot" +"statusDesc" = "Comprobar el estado del bot" +"idDesc" = "Mostrar tu ID de Telegram" + +[tgbot.messages] +"cpuThreshold" = "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%" +"selectUserFailed" = "❌ ¡Error al seleccionar usuario!" +"userSaved" = "✅ Usuario de Telegram guardado." +"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n" +"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n" +"report" = "🕰 Informes programados: {{ .RunTime }}\r\n" +"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n" +"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n" +"version" = "🚀 Versión de X-Panel: {{ .Version }}\r\n" +"xrayVersion" = "📡 Versión de Xray: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 Conteo de TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 Conteo de UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Estado de Xray: {{ .State }}\r\n" +"username" = "👤 Nombre de usuario: {{ .Username }}\r\n" +"password" = "👤 Contraseña: {{ .Password }}\r\n" +"time" = "⏰ Hora: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Puerto: {{ .Port }}\r\n" +"expire" = "📅 Fecha de Vencimiento: {{ .Time }}\r\n" +"expireIn" = "📅 Vence en: {{ .Time }}\r\n" +"active" = "💡 Activo: {{ .Enable }}\r\n" +"enabled" = "🚨 Habilitado: {{ .Enable }}\r\n" +"online" = "🌐 Estado de conexión: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Subida: ↑{{ .Upload }}\r\n" +"download" = "🔽 Bajada: ↓{{ .Download }}\r\n" +"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Usuario de Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Agotado {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}:\r\n" +"onlinesCount" = "🌐 Clientes en línea: {{ .Count }}\r\n" +"disabled" = "🛑 Desactivado: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n" +"yes" = "✅ Sí" +"no" = "❌ No" +"received_id" = "🔑📥 ID actualizado." +"received_password" = "🔑📥 Contraseña actualizada." +"received_email" = "📧📥 Correo electrónico actualizado." +"received_comment" = "💬📥 Comentario actualizado." +"id_prompt" = "🔑 ID predeterminado: {{ .ClientId }}\n\nIntroduce tu ID." +"pass_prompt" = "🔑 Contraseña predeterminada: {{ .ClientPassword }}\n\nIntroduce tu contraseña." +"email_prompt" = "📧 Correo electrónico predeterminado: {{ .ClientEmail }}\n\nIntroduce tu correo electrónico." +"comment_prompt" = "💬 Comentario predeterminado: {{ .ClientComment }}\n\nIntroduce tu comentario." +"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!" +"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Contraseña: {{ .ClientPass }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!" +"cancel" = "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄" +"error_add_client" = "⚠️ Error:\n\n {{ .error }}" +"using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊" +"incorrect_input" ="Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫" +"AreYouSure" = "¿Estás seguro? 🤔" +"SuccessResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito" +"FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠️ Error: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes." + +[tgbot.buttons] +"closeKeyboard" = "❌ Cerrar Teclado" +"cancel" = "❌ Cancelar" +"cancelReset" = "❌ Cancelar Reinicio" +"cancelIpLimit" = "❌ Cancelar Límite de IP" +"confirmResetTraffic" = "✅ ¿Confirmar Reinicio de Tráfico?" +"confirmClearIps" = "✅ ¿Confirmar Limpiar IPs?" +"confirmRemoveTGUser" = "✅ ¿Confirmar Eliminar Usuario de Telegram?" +"confirmToggle" = "✅ ¿Confirmar habilitar/deshabilitar usuario?" +"dbBackup" = "Obtener Copia de Seguridad de BD" +"serverUsage" = "Uso del Servidor" +"getInbounds" = "Obtener Entradas" +"depleteSoon" = "Pronto se Agotará" +"clientUsage" = "Obtener Uso" +"onlines" = "Clientes en línea" +"commands" = "Comandos" +"refresh" = "🔄 Actualizar" +"clearIPs" = "❌ Limpiar IPs" +"removeTGUser" = "❌ Eliminar Usuario de Telegram" +"selectTGUser" = "👤 Seleccionar Usuario de Telegram" +"selectOneTGUser" = "👤 Selecciona un usuario de telegram:" +"resetTraffic" = "📈 Reiniciar Tráfico" +"resetExpire" = "📅 Cambiar fecha de Vencimiento" +"ipLog" = "🔢 Registro de IP" +"ipLimit" = "🔢 Límite de IP" +"setTGUser" = "👤 Establecer Usuario de Telegram" +"toggle" = "🔘 Habilitar / Deshabilitar" +"custom" = "🔢 Costumbre" +"confirmNumber" = "✅ Confirmar: {{ .Num }}" +"confirmNumberAdd" = "✅ Confirmar agregando: {{ .Num }}" +"limitTraffic" = "🚧 Límite de tráfico" +"getBanLogs" = "Registros de prohibición" +"allClients" = "Todos los Clientes" +"addClient" = "Añadir cliente" +"submitDisable" = "Enviar como deshabilitado ☑️" +"submitEnable" = "Enviar como habilitado ✅" +"use_default" = "🏷️ Usar por defecto" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Contraseña" +"change_email" = "⚙️📧 Correo electrónico" +"change_comment" = "⚙️💬 Comentario" +"ResetAllTraffics" = "Reiniciar todo el tráfico" +"SortedTrafficUsageReport" = "Informe de uso de tráfico ordenado" + +[tgbot.answers] +"successfulOperation" = "✅ ¡Exitosa!" +"errorOperation" = "❗ Error en la Operación." +"getInboundsFailed" = "❌ Error al obtener las entradas" +"getClientsFailed" = "❌ No se pudo obtener los clientes." +"canceled" = "❌ {{ .Email }} : Operación cancelada." +"clientRefreshSuccess" = "✅ {{ .Email }} : Cliente actualizado exitosamente." +"IpRefreshSuccess" = "✅ {{ .Email }} : IPs actualizadas exitosamente." +"TGIdRefreshSuccess" = "✅ {{ .Email }} : Usuario de Telegram del cliente actualizado exitosamente." +"resetTrafficSuccess" = "✅ {{ .Email }} : Tráfico reiniciado exitosamente." +"setTrafficLimitSuccess" = "✅ {{ .Email }} : Límite de Tráfico guardado exitosamente." +"expireResetSuccess" = "✅ {{ .Email }} : Días de vencimiento reiniciados exitosamente." +"resetIpSuccess" = "✅ {{ .Email }} : Límite de IP {{ .Count }} guardado exitosamente." +"clearIpSuccess" = "✅ {{ .Email }} : IPs limpiadas exitosamente." +"getIpLog" = "✅ {{ .Email }} : Obtener Registro de IP." +"getUserInfo" = "✅ {{ .Email }} : Obtener Información de Usuario de Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente." +"enableSuccess" = "✅ {{ .Email }} : Habilitado exitosamente." +"disableSuccess" = "✅ {{ .Email }} : Deshabilitado exitosamente." +"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ChatID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ChatID de usuario: {{ .TgUserID }}" +"chooseClient" = "Elige un Cliente para Inbound {{ .Inbound }}" +"chooseInbound" = "Elige un Inbound" + diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml new file mode 100644 index 0000000..5c94992 --- /dev/null +++ b/web/translation/translate.fa_IR.toml @@ -0,0 +1,739 @@ +"username" = "نام‌کاربری" +"password" = "رمزعبور" +"login" = "ورود" +"confirm" = "تایید" +"cancel" = "انصراف" +"close" = "بستن" +"create" = "ایجاد" +"update" = "به‌روزرسانی" +"copy" = "کپی" +"copied" = "کپی شد" +"download" = "دانلود" +"remark" = "نام" +"enable" = "فعال" +"protocol" = "پروتکل" +"search" = "جستجو" +"filter" = "فیلتر" +"loading" = "...در حال بارگذاری" +"second" = "ثانیه" +"minute" = "دقیقه" +"hour" = "ساعت" +"day" = "روز" +"check" = "چک کردن" +"indefinite" = "نامحدود" +"unlimited" = "نامحدود" +"none" = "هیچ" +"qrCode" = "QRکد" +"info" = "اطلاعات بیشتر" +"edit" = "ویرایش" +"delete" = "حذف" +"reset" = "ریست" +"noData" = "داده‌ای وجود ندارد." +"copySuccess" = "باموفقیت کپی‌شد" +"sure" = "مطمئن" +"encryption" = "رمزگذاری" +"useIPv4ForHost" = "از IPv4 برای میزبان استفاده کنید" +"transmission" = "راه‌اتصال" +"host" = "آدرس" +"path" = "مسیر" +"camouflage" = "مبهم‌سازی" +"status" = "وضعیت" +"enabled" = "فعال" +"disabled" = "غیرفعال" +"depleted" = "منقضی" +"depletingSoon" = "در‌حال‌انقضا" +"offline" = "آفلاین" +"online" = "آنلاین" +"domainName" = "آدرس دامنه" +"monitor" = "آی‌پی اتصال" +"certificate" = "گواهی دیجیتال" +"fail" = "ناموفق" +"comment" = "توضیحات" +"success" = "موفق" +"lastOnline" = "آخرین فعالیت" +"getVersion" = "دریافت نسخه" +"install" = "نصب" +"clients" = "کاربران" +"usage" = "استفاده" +"twoFactorCode" = "کد" +"remained" = "باقی‌مانده" +"security" = "امنیت" +"secAlertTitle" = "هشدار‌امنیتی" +"secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید" +"secAlertConf" = "تنظیمات خاصی در برابر حملات آسیب پذیر هستند. توصیه می‌شود پروتکل‌های امنیتی را برای جلوگیری از نفوذ احتمالی تقویت کنید" +"secAlertSSL" = "پنل فاقد ارتباط امن است. لطفاً یک گواهینامه تی‌ال‌اس برای محافظت از داده‌ها نصب کنید" +"secAlertPanelPort" = "استفاده از پورت پیش‌فرض پنل ناامن است. لطفاً یک پورت تصادفی یا خاص تنظیم کنید" +"secAlertPanelURI" = "مسیر پیش‌فرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید" +"secAlertSubURI" = "مسیر پیش‌فرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید" +"secAlertSubJsonURI" = "مسیر پیش‌فرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید" +"emptyDnsDesc" = "هیچ سرور DNS اضافه نشده است." +"emptyFakeDnsDesc" = "هیچ سرور Fake DNS اضافه نشده است." +"emptyBalancersDesc" = "هیچ بالانسر اضافه نشده است." +"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است." +"somethingWentWrong" = "مشکلی پیش آمد" + +[menu] +"theme" = "تم" +"dark" = "تیره" +"ultraDark" = "فوق تیره" +"dashboard" = "نمای کلی" +"inbounds" = "ورودی‌ها" +"settings" = "تنظیمات پنل" +"xray" = "پیکربندی ایکس‌ری" +"logout" = "خروج" +"link" = "مدیریت" + +[pages.login] +"hello" = "سلام" +"title" = "خوش‌آمدید" +"loginAgain" = "مدت زمان استفاده به‌اتمام‌رسیده، لطفا دوباره وارد شوید" + +[pages.login.toasts] +"invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است" +"emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌" +"emptyPassword" = "لطفا یک رمزعبور وارد کنید" +"wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است." +"successLogin" = "شما با موفقیت به حساب کاربری خود وارد شدید." + +[pages.index] +"title" = "نمای کلی" +"cpu" = "پردازنده" +"logicalProcessors" = "پردازنده‌های منطقی" +"frequency" = "فرکانس" +"swap" = "سواپ" +"storage" = "ذخیره‌سازی" +"memory" = "حافظه رم" +"threads" = "رشته‌ها" +"xrayStatus" = "ایکس‌ری" +"stopXray" = "توقف" +"restartXray" = "شروع‌مجدد" +"xraySwitch" = "‌نسخه" +"xraySwitchClick" = "نسخه مورد نظر را انتخاب کنید" +"xraySwitchClickDesk" = "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد" +"xrayStatusUnknown" = "ناشناخته" +"xrayStatusRunning" = "در حال اجرا" +"xrayStatusStop" = "متوقف" +"xrayStatusError" = "خطا" +"xrayErrorPopoverTitle" = "خطا در هنگام اجرای Xray رخ داد" +"operationHours" = "مدت‌کارکرد" +"systemLoad" = "بارسیستم" +"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته" +"connectionCount" = "تعداد کانکشن ها" +"ipAddresses" = "آدرس‌های IP" +"toggleIpVisibility" = "تغییر وضعیت نمایش IP" +"overallSpeed" = "سرعت کلی" +"upload" = "آپلود" +"download" = "دانلود" +"totalData" = "داده‌های کل" +"sent" = "ارسال شده" +"received" = "دریافت شده" +"documentation" = "مستندات" +"xraySwitchVersionDialog" = "آیا واقعاً می‌خواهید نسخه Xray را تغییر دهید؟" +"xraySwitchVersionDialogDesc" = "این کار نسخه Xray را به #version# تغییر می‌دهد." +"xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد" +"geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟" +"geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند." +"geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند." +"geofilesUpdateAll" = "همه را به‌روزرسانی کنید" +"geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد" +"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" +"logs" = "گزارش‌ها" +"config" = "پیکربندی" +"backup" = "پشتیبان‌گیری" +"backupTitle" = "پشتیبان‌گیری دیتابیس" +"exportDatabase" = "پشتیبان‌گیری" +"exportDatabaseDesc" = "برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید." +"importDatabase" = "بازیابی" +"importDatabaseDesc" = "برای انتخاب و آپلود یک فایل .db از دستگاهتان و بازیابی پایگاه داده از یک پشتیبان کلیک کنید." +"importDatabaseSuccess" = "پایگاه داده با موفقیت وارد شد" +"importDatabaseError" = "خطا در وارد کردن پایگاه داده" +"readDatabaseError" = "خطا در خواندن پایگاه داده" +"getDatabaseError" = "خطا در دریافت پایگاه داده" +"getConfigError" = "خطا در دریافت فایل پیکربندی" + +[pages.inbounds] +"allTimeTraffic" = "کل ترافیک" +"allTimeTrafficUsage" = "کل استفاده در تمام مدت" +"title" = "کاربران" +"totalDownUp" = "دریافت/ارسال کل" +"totalUsage" = "‌‌‌مصرف کل" +"inboundCount" = "کل ورودی‌ها" +"operate" = "عملیات" +"enable" = "فعال" +"remark" = "نام" +"protocol" = "پروتکل" +"port" = "پورت" +"portMap" = "پورت‌های نظیر" +"traffic" = "ترافیک" +"details" = "توضیحات" +"transportConfig" = "نحوه اتصال" +"expireDate" = "مدت زمان" +"createdAt" = "ایجاد" +"updatedAt" = "به‌روزرسانی" +"resetTraffic" = "ریست ترافیک" +"addInbound" = "افزودن ورودی" +"generalActions" = "عملیات کلی" +"autoRefresh" = "تازه‌سازی خودکار" +"autoRefreshInterval" = "فاصله" +"modifyInbound" = "ویرایش ورودی" +"deleteInbound" = "حذف ورودی" +"deleteInboundContent" = "آیا مطمئن به حذف ورودی هستید؟" +"deleteClient" = "حذف کاربر" +"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟" +"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟" +"copyLink" = "کپی لینک" +"address" = "آدرس" +"network" = "شبکه" +"destinationPort" = "پورت مقصد" +"targetAddress" = "آدرس مقصد" +"monitorDesc" = "به‌طور پیش‌فرض خالی‌بگذارید" +"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)" +"totalFlow" = "ترافیک کل" +"leaveBlankToNeverExpire" = "برای منقضی‌نشدن خالی‌بگذارید" +"noRecommendKeepDefault" = "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود" +"certificatePath" = "مسیر فایل" +"certificateContent" = "محتوای فایل" +"publicKey" = "کلید عمومی" +"privatekey" = "کلید خصوصی" +"clickOnQRcode" = "برای کپی بر روی کدتصویری کلیک کنید" +"client" = "کاربر" +"export" = "استخراج لینک‌ها" +"clone" = "شبیه‌سازی" +"cloneInbound" = "شبیه‌سازی ورودی" +"cloneInboundContent" = "همه موارد این ورودی بجز پورت، آی‌پی و کاربر‌ها شبیه‌سازی خواهند شد" +"cloneInboundOk" = "ساختن شبیه ساز" +"resetAllTraffic" = "ریست ترافیک کل ورودی‌ها" +"resetAllTrafficTitle" = "ریست ترافیک کل ورودی‌ها" +"resetAllTrafficContent" = "آیا مطمئن به ریست ترافیک تمام ورودی‌ها هستید؟" +"resetInboundClientTraffics" = "ریست ترافیک کاربران" +"resetInboundClientTrafficTitle" = "ریست ترافیک کاربران" +"resetInboundClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران این‌ ورودی هستید؟" +"resetAllClientTraffics" = "ریست ترافیک کل کاربران" +"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران" +"resetAllClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران هستید؟" +"delDepletedClients" = "حذف کاربران منقضی" +"delDepletedClientsTitle" = "حذف کاربران منقضی" +"delDepletedClientsContent" = "آیا مطمئن به حذف تمام کاربران منقضی‌شده ‌هستید؟" +"email" = "ایمیل" +"emailDesc" = "باید یک ایمیل یکتا باشد" +"IPLimit" = "محدودیت آی‌پی" +"IPLimitDesc" = "(اگر تعداد از مقدار تنظیم شده بیشتر شود، ورودی را غیرفعال می کند. (0 = غیرفعال" +"IPLimitlog" = "گزارش‌ها" +"IPLimitlogDesc" = "گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید" +"IPLimitlogclear" = "پاک کردن گزارش‌ها" +"setDefaultCert" = "استفاده از گواهی پنل" +"telegramDesc" = "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا (@userinfobot)" +"subscriptionDesc" = "شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید" +"info" = "اطلاعات" +"same" = "همسان" +"inboundData" = "داده‌های ورودی" +"exportInbound" = "استخراج ورودی" +"import" = "افزودن" +"importInbound" = "افزودن یک ورودی" + +[pages.client] +"add" = "کاربر جدید" +"edit" = "ویرایش کاربر" +"submitAdd" = "اضافه کردن" +"submitEdit" = "ذخیره تغییرات" +"clientCount" = "تعداد کاربران" +"bulk" = "انبوه‌سازی" +"method" = "روش" +"first" = "از" +"last" = "تا" +"prefix" = "پیشوند" +"postfix" = "پسوند" +"delayedStart" = "شروع‌پس‌از‌اولین‌استفاده" +"expireDays" = "مدت زمان" +"days" = "(روز)" +"renew" = "تمدید خودکار" +"renewDesc" = "(تمدید خودکار پس‌از ‌انقضا. (0 = غیرفعال)(واحد: روز" + +[pages.inbounds.toasts] +"obtain" = "فراهم‌سازی" +"updateSuccess" = "بروزرسانی با موفقیت انجام شد" +"logCleanSuccess" = "لاگ پاکسازی شد" +"inboundsUpdateSuccess" = "ورودی‌ها با موفقیت به‌روزرسانی شدند" +"inboundUpdateSuccess" = "ورودی با موفقیت به‌روزرسانی شد" +"inboundCreateSuccess" = "ورودی با موفقیت ایجاد شد" +"inboundDeleteSuccess" = "ورودی با موفقیت حذف شد" +"inboundClientAddSuccess" = "کلاینت(های) ورودی اضافه شدند" +"inboundClientDeleteSuccess" = "کلاینت ورودی حذف شد" +"inboundClientUpdateSuccess" = "کلاینت ورودی به‌روزرسانی شد" +"delDepletedClientsSuccess" = "تمام کلاینت‌های مصرف شده حذف شدند" +"resetAllClientTrafficSuccess" = "تمام ترافیک کلاینت بازنشانی شد" +"resetAllTrafficSuccess" = "تمام ترافیک‌ها بازنشانی شدند" +"resetInboundClientTrafficSuccess" = "ترافیک بازنشانی شد" +"trafficGetError" = "خطا در دریافت ترافیک‌ها" +"getNewX25519CertError" = "خطا در دریافت گواهی X25519." +"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65." + +[pages.inbounds.stream.general] +"request" = "درخواست" +"response" = "پاسخ" +"name" = "نام" +"value" = "مقدار" + +[pages.inbounds.stream.tcp] +"version" = "نسخه" +"method" = "متد" +"path" = "مسیر" +"status" = "وضعیت" +"statusDescription" = "توضیحات وضعیت" +"requestHeader" = "سربرگ درخواست" +"responseHeader" = "سربرگ پاسخ" + +[pages.settings] +"title" = "تنظیمات پنل" +"save" = "ذخیره" +"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید" +"restartPanel" = "ریستارت پنل" +"restartPanelDesc" = "آیا مطمئن به ریستارت پنل هستید؟ اگر پس‌از ریستارت نمی‌توانید به پنل دسترسی پیدا کنید، لطفاً گزارش‌های موجود در اسکریپت پنل را بررسی کنید" +"restartPanelSuccess" = "پنل با موفقیت راه‌اندازی مجدد شد" +"actions" = "عملیات ها" +"resetDefaultConfig" = "برگشت به پیش‌فرض" +"panelSettings" = "پیکربندی" +"securitySettings" = "احرازهویت" +"TGBotSettings" = "ربات تلگرام" +"panelListeningIP" = "آدرس آی‌پی" +"panelListeningIPDesc" = "آدرس آی‌پی برای وب پنل. برای گوش‌دادن به‌تمام آی‌پی‌ها خالی‌بگذارید" +"panelListeningDomain" = "نام دامنه" +"panelListeningDomainDesc" = "آدرس دامنه برای وب پنل. برای گوش دادن به‌تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید" +"panelPort" = "پورت" +"panelPortDesc" = "شماره پورت برای وب پنل. باید پورت استفاده نشده‌باشد" +"publicKeyPath" = "مسیر کلید عمومی" +"publicKeyPathDesc" = "مسیر فایل کلیدعمومی برای وب پنل. با '/' شروع‌می‌شود" +"privateKeyPath" = "مسیر کلید خصوصی" +"privateKeyPathDesc" = "مسیر فایل کلیدخصوصی برای وب پنل. با '/' شروع‌می‌شود" +"panelUrlPath" = "URI مسیر" +"panelUrlPathDesc" = "برای وب پنل. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر" +"pageSize" = "اندازه صفحه بندی جدول" +"pageSizeDesc" = "(اندازه صفحه برای جدول ورودی‌ها.(0 = غیرفعال" +"remarkModel" = "نام‌کانفیگ و جداکننده" +"datepicker" = "نوع تقویم" +"datepickerPlaceholder" = "انتخاب تاریخ" +"datepickerDescription" = "وظایف برنامه ریزی شده بر اساس این تقویم اجرا می‌شود" +"sampleRemark" = "نمونه‌نام" +"oldUsername" = "نام‌کاربری فعلی" +"currentPassword" = "رمز‌عبور فعلی" +"newUsername" = "نام‌کاربری جدید" +"newPassword" = "رمزعبور جدید" +"telegramBotEnable" = "فعال‌سازی ربات تلگرام" +"telegramBotEnableDesc" = "ربات تلگرام را فعال می‌کند" +"telegramToken" = "توکن تلگرام" +"telegramTokenDesc" = "دریافت کنید @botfather توکن را می‌توانید از" +"telegramProxy" = "SOCKS پراکسی" +"telegramProxyDesc" = "را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی" +"telegramAPIServer" = "سرور API تلگرام" +"telegramAPIServerDesc" = "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید" +"telegramChatId" = "آی‌دی چت مدیر" +"telegramChatIdDesc" = "دریافت ‌کنید ('/id'یا (دستور (@userinfobot) آی‌دی(های) چت تلگرام مدیر، از" +"telegramNotifyTime" = "زمان نوتیفیکیشن" +"telegramNotifyTimeDesc" = "زمان‌اطلاع‌رسانی ربات تلگرام برای گزارش های دوره‌ای. از فرمت زمانبندی لینوکس استفاده‌کنید‌" +"tgNotifyBackup" = "پشتیبان‌گیری از دیتابیس" +"tgNotifyBackupDesc" = "فایل پشتیبان‌دیتابیس را به‌همراه گزارش ارسال می‌کند" +"tgNotifyLogin" = "اعلان ورود" +"tgNotifyLoginDesc" = "نام‌کاربری، آدرس آی‌پی، و زمان ورود، فردی که سعی می‌کند وارد پنل شود را نمایش می‌دهد" +"sessionMaxAge" = "بیشینه زمان جلسه وب" +"sessionMaxAgeDesc" = "(بیشینه زمانی که می‌توانید لاگین بمانید. (واحد: دقیقه" +"expireTimeDiff" = "آستانه زمان باقی مانده" +"expireTimeDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به زمان انقضا. (واحد: روز" +"trafficDiff" = "آستانه ترافیک باقی مانده" +"trafficDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به اتمام ترافیک. (واحد: گیگابایت" +"tgNotifyCpu" = "آستانه هشدار بار پردازنده" +"tgNotifyCpuDesc" = "(اگر بار روی پردازنده ازاین آستانه فراتر رفت، برای شما پیام ارسال می‌شود. (واحد: درصد" +"timeZone" = "منطقه زمانی" +"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه‌زمانی اجرا می‌شود" +"subSettings" = "سابسکریپشن" +"subEnable" = "فعال‌سازی سرویس سابسکریپشن" +"subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند" +"subTitle" = "عنوان اشتراک" +"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN" +"subListen" = "آدرس آی‌پی" +"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید" +"subPort" = "پورت" +"subPortDesc" = "شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده نشده‌باشد" +"subCertPath" = "مسیر کلید عمومی" +"subCertPathDesc" = "مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروع‌می‌شود" +"subKeyPath" = "مسیر کلید خصوصی" +"subKeyPathDesc" = "مسیر فایل کلیدخصوصی برای سرویس سابسکریپشن. با '/' شروع‌می‌شود" +"subPath" = "URI مسیر" +"subPathDesc" = "برای سرویس سابسکریپشن. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر" +"subDomain" = "نام دامنه" +"subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌" +"subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن" +"subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت" +"subEncrypt" = "کدگذاری" +"subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه" +"subShowInfo" = "نمایش اطلاعات مصرف" +"subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد" +"subURI" = "پروکسی معکوس URI مسیر" +"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر" +"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک" +"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود" +"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک" +"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود" +"fragment" = "فرگمنت" +"fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس" +"fragmentSett" = "تنظیمات فرگمنت" +"noisesDesc" = "فعال کردن Noises." +"noisesSett" = "تنظیمات Noises" +"mux" = "ماکس" +"muxDesc" = "چندین جریان داده مستقل را در یک جریان داده ثابت منتقل می کند" +"muxSett" = "تنظیمات ماکس" +"direct" = "اتصال مستقیم" +"directDesc" = "به طور مستقیم با دامنه ها یا محدوده آی‌پی یک کشور خاص ارتباط برقرار می کند" +"notifications" = "اعلان‌ها" +"certs" = "گواهی‌ها" +"externalTraffic" = "ترافیک خارجی" +"dateAndTime" = "تاریخ و زمان" +"proxyAndServer" = "پراکسی و سرور" +"intervals" = "فواصل" +"information" = "اطلاعات" +"language" = "زبان" +"telegramBotLanguage" = "زبان ربات تلگرام" + +[pages.xray] +"title" = "پیکربندی ایکس‌ری" +"save" = "ذخیره" +"restart" = "ریستارت ایکس‌ری" +"restartSuccess" = "Xray با موفقیت راه‌اندازی مجدد شد" +"stopSuccess" = "Xray با موفقیت متوقف شد" +"restartError" = "خطا در راه‌اندازی مجدد Xray." +"stopError" = "خطا در توقف Xray." +"basicTemplate" = "پایه" +"advancedTemplate" = "پیشرفته" +"generalConfigs" = "استراتژی‌ کلی" +"generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند" +"logConfigs" = "گزارش" +"logConfigsDesc" = "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید" +"blockConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند" +"basicRouting" = "مسیریابی پایه" +"blockConnectionsConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس کشور درخواست‌شده خاص مسدود می‌کنند." +"directConnectionsConfigsDesc" = "یک اتصال مستقیم تضمین می‌کند که ترافیک خاص از طریق سرور دیگری مسیریابی نشود." +"blockips" = "مسدود کردن آی‌پی‌ها" +"blockdomains" = "مسدود کردن دامنه‌ها" +"directips" = "آی‌پی‌های مستقیم" +"directdomains" = "دامنه‌های مستقیم" +"ipv4Routing" = "IPv4 مسیریابی" +"ipv4RoutingDesc" = "این گزینه‌ها ترافیک را از طریق آی‌پی نسخه4 سرور، به مقصد هدایت می‌کند" +"warpRouting" = "WARP مسیریابی" +"warpRoutingDesc" = "این گزینه‌ها ترافیک‌ را از طریق وارپ کلادفلر به مقصد هدایت می‌کند" +"Template" = "‌پیکربندی پیشرفته الگو ایکس‌ری" +"TemplateDesc" = "فایل پیکربندی نهایی ایکس‌ری بر اساس این الگو ایجاد می‌شود" +"FreedomStrategy" = "Freedom استراتژی پروتکل" +"FreedomStrategyDesc" = "تعیین می‌کند Freedom استراتژی خروجی شبکه را برای پروتکل" +"RoutingStrategy" = "استراتژی کلی مسیریابی" +"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند" +"Torrent" = "مسدودسازی پروتکل بیت‌تورنت" +"Inbounds" = "ورودی‌ها" +"InboundsDesc" = "پذیرش کلاینت خاص" +"Outbounds" = "خروجی‌ها" +"Balancers" = "بالانسرها" +"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید" +"Routings" = "قوانین مسیریابی" +"RoutingsDesc" = "اولویت هر قانون مهم است" +"completeTemplate" = "کامل" +"logLevel" = "سطح گزارش" +"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند." +"accessLog" = "مسیر گزارش" +"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند." +"errorLog" = "گزارش خطا" +"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند" +"dnsLog" = "گزارش DNS" +"dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید" +"maskAddress" = "پنهان کردن آدرس" +"maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند." +"statistics" = "آمار" +"statsInboundUplink" = "آمار آپلود ورودی" +"statsInboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های ورودی را فعال می‌کند." +"statsInboundDownlink" = "آمار دانلود ورودی" +"statsInboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های ورودی را فعال می‌کند." +"statsOutboundUplink" = "آمار آپلود خروجی" +"statsOutboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند." +"statsOutboundDownlink" = "آمار دانلود خروجی" +"statsOutboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند." + +[pages.xray.rules] +"first" = "اولین" +"last" = "آخرین" +"up" = "بالا" +"down" = "پایین" +"source" = "مبدا" +"dest" = "مقصد" +"inbound" = "ورودی" +"outbound" = "خروجی" +"balancer" = "بالانسر" +"info" = "اطلاعات" +"add" = "افزودن قانون" +"edit" = "ویرایش قانون" +"useComma" = "موارد جدا شده با کاما" + +[pages.xray.outbound] +"addOutbound" = "افزودن خروجی" +"addReverse" = "افزودن معکوس" +"editOutbound" = "ویرایش خروجی" +"editReverse" = "ویرایش معکوس" +"tag" = "برچسب" +"tagDesc" = "برچسب یگانه" +"address" = "آدرس" +"reverse" = "معکوس" +"domain" = "دامنه" +"type" = "نوع" +"bridge" = "پل" +"portal" = "پورتال" +"link" = "لینک" +"intercon" = "اتصال میانی" +"settings" = "تنظیمات" +"accountInfo" = "اطلاعات حساب" +"outboundStatus" = "وضعیت خروجی" +"sendThrough" = "ارسال با" + +[pages.xray.balancer] +"addBalancer" = "افزودن بالانسر" +"editBalancer" = "ویرایش بالانسر" +"balancerStrategy" = "استراتژی" +"balancerSelectors" = "انتخاب‌گرها" +"tag" = "برچسب" +"tagDesc" = "برچسب یگانه" +"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد." + +[pages.xray.wireguard] +"secretKey" = "کلید شخصی" +"publicKey" = "کلید عمومی" +"allowedIPs" = "آی‌پی‌های مجاز" +"endpoint" = "نقطه پایانی" +"psk" = "کلید مشترک" +"domainStrategy" = "استراتژی حل دامنه" + +[pages.xray.dns] +"enable" = "فعال کردن حل دامنه" +"enableDesc" = "سرور حل دامنه داخلی را فعال کنید" +"tag" = "برچسب" +"tagDesc" = "این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود" +"clientIp" = "آی‌پی کلاینت" +"clientIpDesc" = "برای اطلاع‌رسانی به سرور درباره مکان IP مشخص‌شده در طول درخواست‌های DNS استفاده می‌شود" +"disableCache" = "غیرفعال‌سازی کش" +"disableCacheDesc" = "کش DNS را غیرفعال می‌کند" +"disableFallback" = "غیرفعال‌سازی Fallback" +"disableFallbackDesc" = "درخواست‌های DNS Fallback را غیرفعال می‌کند" +"disableFallbackIfMatch" = "غیرفعال‌سازی Fallback در صورت تطابق" +"disableFallbackIfMatchDesc" = "درخواست‌های DNS Fallback را زمانی که لیست دامنه‌های مطابقت‌یافته سرور DNS فعال است، غیرفعال می‌کند" +"strategy" = "استراتژی پرس‌وجو" +"strategyDesc" = "استراتژی کلی برای حل نام دامنه" +"add" = "افزودن سرور" +"edit" = "ویرایش سرور" +"domains" = "دامنه‌ها" +"expectIPs" = "آی‌پی‌های مورد انتظار" +"unexpectIPs" = "آی‌پی‌های غیرمنتظره" +"useSystemHosts" = "استفاده از Hosts سیستم" +"useSystemHostsDesc" = "استفاده از فایل hosts یک سیستم نصب‌شده" +"usePreset" = "استفاده از پیش‌تنظیم" +"dnsPresetTitle" = "پیش‌تنظیم‌های DNS" +"dnsPresetFamily" = "خانوادگی" + +[pages.xray.fakedns] +"add" = "افزودن دی‌ان‌اس جعلی" +"edit" = "ویرایش دی‌ان‌اس جعلی" +"ipPool" = "زیرشبکه استخر آی‌پی" +"poolSize" = "اندازه استخر" + +[pages.settings.security] +"admin" = "اعتبارنامه‌های ادمین" +"twoFactor" = "احراز هویت دو مرحله‌ای" +"twoFactorEnable" = "فعال‌سازی 2FA" +"twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند." +"twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای" +"twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای" +"twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:" +"twoFactorModalFirstStep" = "1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید" +"twoFactorModalSecondStep" = "2. کد را از برنامه وارد کنید" +"twoFactorModalRemoveStep" = "برای حذف احراز هویت دو مرحله‌ای، کد را از برنامه وارد کنید." +"twoFactorModalChangeCredentialsTitle" = "تغییر اعتبارنامه‌ها" +"twoFactorModalChangeCredentialsStep" = "برای تغییر اعتبارنامه‌های مدیر، کد را از برنامه وارد کنید." +"twoFactorModalSetSuccess" = "احراز هویت دو مرحله‌ای با موفقیت برقرار شد" +"twoFactorModalDeleteSuccess" = "احراز هویت دو مرحله‌ای با موفقیت حذف شد" +"twoFactorModalError" = "کد نادرست" + +[pages.settings.toasts] +"modifySettings" = "پارامترها تغییر کرده‌اند." +"getSettings" = "خطا در دریافت پارامترها" +"modifyUserError" = "خطا در تغییر اعتبارنامه‌های مدیر سیستم." +"modifyUser" = "شما با موفقیت اعتبارنامه‌های مدیر سیستم را تغییر دادید." +"originalUserPassIncorrect" = "نام‌کاربری یا رمزعبور فعلی اشتباه‌است" +"userPassMustBeNotEmpty" = "نام‌کاربری یا رمزعبور جدید خالی‌است" +"getOutboundTrafficError" = "خطا در دریافت ترافیک خروجی" +"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی" + +[tgbot] +"keyboardClosed" = "❌ صفحه کلید بسته شد!" +"noResult" = "❗ نتیجه ای یافت نشد!" +"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!" +"wentWrong" = "❌ مشکلی پیش آمد!" +"noIpRecord" = "❗ رکورد آی پی وجود ندارد!" +"noInbounds" = "❗ هیچ ورودی یافت نشد!" +"unlimited" = "♾ نامحدود(ریست)" +"add" = "افزودن" +"month" = "ماه" +"months" = "ماه" +"day" = "روز" +"days" = "روز" +"hours" = "ساعت" +"minutes" = "دقیقه" +"unknown" = "نامشخص" +"inbounds" = "ورودی ها" +"clients" = "کاربران" +"offline" = "🔴 آفلاین" +"online" = "🟢 آنلاین" + +[tgbot.commands] +"unknown" = "❗ دستور ناشناخته" +"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n" +"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n" +"start" = "👋 سلام {{ .Firstname }}.\r\n" +"welcome" = "🤖 به ربات مدیریت {{ .Hostname }} خوش آمدید.\r\n" +"status" = "✅ ربات در حالت عادی است!" +"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!" +"getID" = "🆔 شناسه شما: {{ .ID }}" +"helpAdminCommands" = "برای راه‌اندازی مجدد Xray Core:\r\n/restart\r\n\r\nبرای جستجوی ایمیل مشتری:\r\n/usage [ایمیل]\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n/inbound [توضیحات]\r\n\r\nشناسه گفتگوی تلگرام:\r\n/id" +"helpClientCommands" = "برای جستجوی آمار، از دستور زیر استفاده کنید:\r\n/usage [ایمیل]\r\n\r\nشناسه گفتگوی تلگرام:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ عملیات با موفقیت انجام شد!" +"restartFailed" = "❗ خطا در عملیات.\r\n\r\nخطا: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core در حال اجرا نیست." +"startDesc" = "نمایش منوی اصلی" +"helpDesc" = "راهنمای ربات" +"statusDesc" = "بررسی وضعیت ربات" +"idDesc" = "نمایش شناسه تلگرام شما" + +[tgbot.messages] +"cpuThreshold" = "🔴 بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%" +"selectUserFailed" = "❌ خطا در انتخاب کاربر!" +"userSaved" = "✅ کاربر تلگرام ذخیره شد." +"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n" +"loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n" +"report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n" +"datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n" +"hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n" +"version" = "🚀 نسخه‌پنل: {{ .Version }}\r\n" +"xrayVersion" = "📡 نسخه‌هسته: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 آدرس‌آی‌پی: {{ .IP }}\r\n" +"ips" = "🔢 آدرس‌های آی‌پی:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ مدت‌کارکردسیستم: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ وضعیت‌ایکس‌ری: {{ .State }}\r\n" +"username" = "👤 نام‌کاربری: {{ .Username }}\r\n" +"password" = "👤 رمز عبور: {{ .Password }}\r\n" +"time" = "⏰ زمان: {{ .Time }}\r\n" +"inbound" = "📍 نام‌ورودی: {{ .Remark }}\r\n" +"port" = "🔌 پورت: {{ .Port }}\r\n" +"expire" = "📅 تاریخ‌انقضا: {{ .Time }}\r\n\r\n" +"expireIn" = "📅 باقی‌ مانده‌ تا انقضا: {{ .Time }}\r\n\r\n" +"active" = "💡 فعال: {{ .Enable }}\r\n" +"enabled" = "🚨 وضعیت: {{ .Enable }}\r\n" +"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n" +"email" = "📧 ایمیل: {{ .Email }}\r\n" +"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n" +"download" = "🔽 دانلود↓: {{ .Download }}\r\n" +"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 {{ .Type }} به‌اتمام‌رسیده‌است:\r\n" +"exhaustedCount" = "🚨 تعداد {{ .Type }} به‌اتمام‌رسیده‌است:\r\n" +"onlinesCount" = "🌐 کاربران‌آنلاین: {{ .Count }}\r\n" +"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 به‌زودی‌به‌پایان‌خواهدرسید: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 زمان‌پشتیبان‌گیری: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n" +"yes" = "✅ بله" +"no" = "❌ خیر" +"received_id" = "🔑📥 شناسه به‌روزرسانی شد." +"received_password" = "🔑📥 رمز عبور به‌روزرسانی شد." +"received_email" = "📧📥 ایمیل به‌روزرسانی شد." +"received_comment" = "💬📥 نظر به‌روزرسانی شد." +"id_prompt" = "🔑 شناسه پیش‌فرض: {{ .ClientId }}\n\nشناسه خود را وارد کنید." +"pass_prompt" = "🔑 رمز عبور پیش‌فرض: {{ .ClientPassword }}\n\nرمز عبور خود را وارد کنید." +"email_prompt" = "📧 ایمیل پیش‌فرض: {{ .ClientEmail }}\n\nایمیل خود را وارد کنید." +"comment_prompt" = "💬 نظر پیش‌فرض: {{ .ClientComment }}\n\nنظر خود را وارد کنید." +"inbound_client_data_id" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 شناسه: {{ .ClientId }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!" +"inbound_client_data_pass" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 رمز عبور: {{ .ClientPass }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!" +"cancel" = "❌ فرآیند لغو شد! \n\nمی‌توانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄" +"error_add_client" = "⚠️ خطا:\n\n {{ .error }}" +"using_default_value" = "باشه، از مقدار پیش‌فرض استفاده می‌کنم. 😊" +"incorrect_input" ="ورودی شما معتبر نیست.\nعبارت‌ها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫" +"AreYouSure" = "مطمئنی؟ 🤔" +"SuccessResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیت‌آمیز" +"FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠️ خطا: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید." + +[tgbot.buttons] +"closeKeyboard" = "❌ بستن کیبورد" +"cancel" = "❌ لغو" +"cancelReset" = "❌ لغو تنظیم مجدد" +"cancelIpLimit" = "❌ لغو محدودیت آی‌پی" +"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟" +"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های آی‌پی؟" +"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟" +"confirmToggle" = "✅ تایید فعال/غیرفعال کردن کاربر؟" +"dbBackup" = "دریافت پشتیبان" +"serverUsage" = "استفاده از سیستم" +"getInbounds" = "دریافت ورودی‌ها" +"depleteSoon" = "به‌زودی به پایان خواهد رسید" +"clientUsage" = "دریافت آمار کاربر" +"onlines" = "کاربران آنلاین" +"commands" = "دستورات" +"refresh" = "🔄 تازه‌سازی" +"clearIPs" = "❌ پاک‌سازی آدرس‌ها" +"removeTGUser" = "❌ حذف کاربر تلگرام" +"selectTGUser" = "👤 انتخاب کاربر تلگرام" +"selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:" +"resetTraffic" = "📈 تنظیم مجدد ترافیک" +"resetExpire" = "📅 تنظیم مجدد تاریخ انقضا" +"ipLog" = "🔢 لاگ آدرس‌های IP" +"ipLimit" = "🔢 محدودیت IP" +"setTGUser" = "👤 تنظیم کاربر تلگرام" +"toggle" = "🔘 فعال / غیرفعال" +"custom" = "🔢 سفارشی" +"confirmNumber" = "✅ تایید: {{ .Num }}" +"confirmNumberAdd" = "✅ تایید اضافه کردن: {{ .Num }}" +"limitTraffic" = "🚧 محدودیت ترافیک" +"getBanLogs" = "گزارش های بلوک را دریافت کنید" +"allClients" = "همه مشتریان" +"addClient" = "افزودن مشتری" +"submitDisable" = "ارسال به عنوان غیرفعال ☑️" +"submitEnable" = "ارسال به عنوان فعال ✅" +"use_default" = "🏷️ استفاده از پیش‌فرض" +"change_id" = "⚙️🔑 شناسه" +"change_password" = "⚙️🔑 گذرواژه" +"change_email" = "⚙️📧 ایمیل" +"change_comment" = "⚙️💬 نظر" +"ResetAllTraffics" = "بازنشانی همه ترافیک‌ها" +"SortedTrafficUsageReport" = "گزارش استفاده از ترافیک مرتب‌شده" + +[tgbot.answers] +"successfulOperation" = "✅ انجام شد!" +"errorOperation" = "❗ خطا در عملیات." +"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد." +"getClientsFailed" = "❌ دریافت مشتریان با شکست مواجه شد." +"canceled" = "❌ {{ .Email }} : عملیات لغو شد." +"clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد." +"IpRefreshSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند." +"TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد." +"resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد." +"setTrafficLimitSuccess" = "✅ {{ .Email }} : محدودیت ترافیک با موفقیت ذخیره شد." +"expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد." +"resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد." +"clearIpSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند." +"getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرس‌های IP." +"getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام." +"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد." +"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد." +"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد." +"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: {{ .TgUserID }}" +"chooseClient" = "یک مشتری برای ورودی {{ .Inbound }} انتخاب کنید" +"chooseInbound" = "یک ورودی انتخاب کنید" diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml new file mode 100644 index 0000000..8b80945 --- /dev/null +++ b/web/translation/translate.id_ID.toml @@ -0,0 +1,739 @@ +"username" = "Nama Pengguna" +"password" = "Kata Sandi" +"login" = "Masuk" +"confirm" = "Konfirmasi" +"cancel" = "Batal" +"close" = "Tutup" +"create" = "Buat" +"update" = "Perbarui" +"copy" = "Salin" +"copied" = "Tersalin" +"download" = "Unduh" +"remark" = "Catatan" +"enable" = "Aktifkan" +"protocol" = "Protokol" +"search" = "Cari" +"filter" = "Filter" +"loading" = "Memuat..." +"second" = "Detik" +"minute" = "Menit" +"hour" = "Jam" +"day" = "Hari" +"check" = "Centang" +"indefinite" = "Tak Terbatas" +"unlimited" = "Tanpa Batas" +"none" = "None" +"qrCode" = "Kode QR" +"info" = "Informasi Lebih Lanjut" +"edit" = "Edit" +"delete" = "Hapus" +"reset" = "Reset" +"noData" = "Tidak ada data." +"copySuccess" = "Berhasil Disalin" +"sure" = "Yakin" +"encryption" = "Enkripsi" +"useIPv4ForHost" = "Gunakan IPv4 untuk host" +"transmission" = "Transmisi" +"host" = "Host" +"path" = "Jalur" +"camouflage" = "Obfuscation" +"status" = "Status" +"enabled" = "Aktif" +"disabled" = "Nonaktif" +"depleted" = "Habis" +"depletingSoon" = "Akan Habis" +"offline" = "Offline" +"online" = "Online" +"domainName" = "Nama Domain" +"monitor" = "IP Pemantauan" +"certificate" = "Sertifikat Digital" +"fail" = "Gagal" +"comment" = "Komentar" +"success" = "Berhasil" +"lastOnline" = "Terakhir online" +"getVersion" = "Dapatkan Versi" +"install" = "Instal" +"clients" = "Klien" +"usage" = "Penggunaan" +"twoFactorCode" = "Kode" +"remained" = "Tersisa" +"security" = "Keamanan" +"secAlertTitle" = "Peringatan keamanan" +"secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data." +"secAlertConf" = "Beberapa pengaturan rentan terhadap serangan. Disarankan untuk memperkuat protokol keamanan guna mencegah pelanggaran potensial." +"secAlertSSL" = "Panel kekurangan koneksi yang aman. Harap instal sertifikat TLS untuk perlindungan data." +"secAlertPanelPort" = "Port default panel rentan. Harap konfigurasi port acak atau tertentu." +"secAlertPanelURI" = "Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks." +"secAlertSubURI" = "Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks." +"secAlertSubJsonURI" = "Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks." +"emptyDnsDesc" = "Tidak ada server DNS yang ditambahkan." +"emptyFakeDnsDesc" = "Tidak ada server Fake DNS yang ditambahkan." +"emptyBalancersDesc" = "Tidak ada penyeimbang yang ditambahkan." +"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan." +"somethingWentWrong" = "Terjadi kesalahan" + +[menu] +"theme" = "Tema" +"dark" = "Gelap" +"ultraDark" = "Sangat Gelap" +"dashboard" = "Ikhtisar" +"inbounds" = "Masuk" +"settings" = "Pengaturan Panel" +"xray" = "Konfigurasi Xray" +"logout" = "Keluar" +"link" = "Kelola" + +[pages.login] +"hello" = "Halo" +"title" = "Selamat Datang" +"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali" + +[pages.login.toasts] +"invalidFormData" = "Format data input tidak valid." +"emptyUsername" = "Nama Pengguna diperlukan" +"emptyPassword" = "Kata Sandi diperlukan" +"wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid." +"successLogin" = "Anda telah berhasil masuk ke akun Anda." + +[pages.index] +"title" = "Ikhtisar" +"cpu" = "CPU" +"logicalProcessors" = "Prosesor logis" +"frequency" = "Frekuensi" +"swap" = "Swap" +"storage" = "Penyimpanan" +"memory" = "RAM" +"threads" = "Thread" +"xrayStatus" = "Xray" +"stopXray" = "Stop" +"restartXray" = "Restart" +"xraySwitch" = "Versi" +"xraySwitchClick" = "Pilih versi yang ingin Anda pindah." +"xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini." +"xrayStatusUnknown" = "Tidak diketahui" +"xrayStatusRunning" = "Berjalan" +"xrayStatusStop" = "Berhenti" +"xrayStatusError" = "Kesalahan" +"xrayErrorPopoverTitle" = "Terjadi kesalahan saat menjalankan Xray" +"operationHours" = "Waktu Aktif" +"systemLoad" = "Beban Sistem" +"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir" +"connectionCount" = "Statistik Koneksi" +"ipAddresses" = "Alamat IP" +"toggleIpVisibility" = "Alihkan visibilitas IP" +"overallSpeed" = "Kecepatan keseluruhan" +"upload" = "Unggah" +"download" = "Unduh" +"totalData" = "Total data" +"sent" = "Dikirim" +"received" = "Diterima" +"documentation" = "Dokumentasi" +"xraySwitchVersionDialog" = "Apakah Anda yakin ingin mengubah versi Xray?" +"xraySwitchVersionDialogDesc" = "Ini akan mengubah versi Xray ke #version#." +"xraySwitchVersionPopover" = "Xray berhasil diperbarui" +"geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?" +"geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#." +"geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas." +"geofilesUpdateAll" = "Perbarui semua" +"geofileUpdatePopover" = "Geofile berhasil diperbarui" +"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" +"logs" = "Log" +"config" = "Konfigurasi" +"backup" = "Cadangan" +"backupTitle" = "Cadangan & Pulihkan Database" +"exportDatabase" = "Cadangkan" +"exportDatabaseDesc" = "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda." +"importDatabase" = "Pulihkan" +"importDatabaseDesc" = "Klik untuk memilih dan mengunggah file .db dari perangkat Anda untuk memulihkan database dari cadangan." +"importDatabaseSuccess" = "Database berhasil diimpor" +"importDatabaseError" = "Terjadi kesalahan saat mengimpor database" +"readDatabaseError" = "Terjadi kesalahan saat membaca database" +"getDatabaseError" = "Terjadi kesalahan saat mengambil database" +"getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi" + +[pages.inbounds] +"allTimeTraffic" = "Total Lalu Lintas" +"allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu" +"title" = "Masuk" +"totalDownUp" = "Total Terkirim/Diterima" +"totalUsage" = "Penggunaan Total" +"inboundCount" = "Total Masuk" +"operate" = "Menu" +"enable" = "Aktifkan" +"remark" = "Catatan" +"protocol" = "Protokol" +"port" = "Port" +"portMap" = "Port Mapping" +"traffic" = "Traffic" +"details" = "Rincian" +"transportConfig" = "Transport" +"expireDate" = "Durasi" +"createdAt" = "Dibuat" +"updatedAt" = "Diperbarui" +"resetTraffic" = "Reset Traffic" +"addInbound" = "Tambahkan Masuk" +"generalActions" = "Tindakan Umum" +"autoRefresh" = "Pembaruan otomatis" +"autoRefreshInterval" = "Interval" +"modifyInbound" = "Ubah Masuk" +"deleteInbound" = "Hapus Masuk" +"deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?" +"deleteClient" = "Hapus Klien" +"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?" +"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?" +"copyLink" = "Salin URL" +"address" = "Alamat" +"network" = "Jaringan" +"destinationPort" = "Port Tujuan" +"targetAddress" = "Alamat Target" +"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP" +"meansNoLimit" = "= Unlimited. (unit: GB)" +"totalFlow" = "Total Aliran" +"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa" +"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default" +"certificatePath" = "Path Berkas" +"certificateContent" = "Konten Berkas" +"publicKey" = "Kunci Publik" +"privatekey" = "Kunci Pribadi" +"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin" +"client" = "Klien" +"export" = "Ekspor Semua URL" +"clone" = "Duplikat" +"cloneInbound" = "Duplikat" +"cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat." +"cloneInboundOk" = "Duplikat" +"resetAllTraffic" = "Reset Semua Traffic Masuk" +"resetAllTrafficTitle" = "Reset Semua Traffic Masuk" +"resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?" +"resetInboundClientTraffics" = "Reset Traffic Klien Masuk" +"resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk" +"resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?" +"resetAllClientTraffics" = "Reset Traffic Semua Klien" +"resetAllClientTrafficTitle" = "Reset Traffic Semua Klien" +"resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?" +"delDepletedClients" = "Hapus Klien Habis" +"delDepletedClientsTitle" = "Hapus Klien Habis" +"delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?" +"email" = "Email" +"emailDesc" = "Harap berikan alamat email yang unik." +"IPLimit" = "Batas IP" +"IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)" +"IPLimitlog" = "Log IP" +"IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)" +"IPLimitlogclear" = "Hapus Log" +"setDefaultCert" = "Atur Sertifikat dari Panel" +"telegramDesc" = "Harap berikan ID Obrolan Telegram. (gunakan perintah '/id' di bot) atau (@userinfobot)" +"subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien." +"info" = "Info" +"same" = "Sama" +"inboundData" = "Data Masuk" +"exportInbound" = "Ekspor Masuk" +"import" = "Impor" +"importInbound" = "Impor Masuk" + +[pages.client] +"add" = "Tambah Klien" +"edit" = "Edit Klien" +"submitAdd" = "Tambah Klien" +"submitEdit" = "Simpan Perubahan" +"clientCount" = "Jumlah Klien" +"bulk" = "Tambahkan Massal" +"method" = "Metode" +"first" = "Pertama" +"last" = "Terakhir" +"prefix" = "Awalan" +"postfix" = "Akhiran" +"delayedStart" = "Mulai Awal" +"expireDays" = "Durasi" +"days" = "Hari" +"renew" = "Perpanjang Otomatis" +"renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)" + +[pages.inbounds.toasts] +"obtain" = "Dapatkan" +"updateSuccess" = "Pembaruan berhasil" +"logCleanSuccess" = "Log telah dibersihkan" +"inboundsUpdateSuccess" = "Inbound berhasil diperbarui" +"inboundUpdateSuccess" = "Inbound berhasil diperbarui" +"inboundCreateSuccess" = "Inbound berhasil dibuat" +"inboundDeleteSuccess" = "Inbound berhasil dihapus" +"inboundClientAddSuccess" = "Klien inbound telah ditambahkan" +"inboundClientDeleteSuccess" = "Klien inbound telah dihapus" +"inboundClientUpdateSuccess" = "Klien inbound telah diperbarui" +"delDepletedClientsSuccess" = "Semua klien yang habis telah dihapus" +"resetAllClientTrafficSuccess" = "Semua lalu lintas klien telah direset" +"resetAllTrafficSuccess" = "Semua lalu lintas telah direset" +"resetInboundClientTrafficSuccess" = "Lalu lintas telah direset" +"trafficGetError" = "Gagal mendapatkan data lalu lintas" +"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519." +"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65." + +[pages.inbounds.stream.general] +"request" = "Permintaan" +"response" = "Respons" +"name" = "Nama" +"value" = "Nilai" + +[pages.inbounds.stream.tcp] +"version" = "Versi" +"method" = "Metode" +"path" = "Path" +"status" = "Status" +"statusDescription" = "Deskripsi Status" +"requestHeader" = "Header Permintaan" +"responseHeader" = "Header Respons" + +[pages.settings] +"title" = "Pengaturan Panel" +"save" = "Simpan" +"infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan." +"restartPanel" = "Restart Panel" +"restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server." +"restartPanelSuccess" = "Panel berhasil dimulai ulang" +"actions" = "Tindakan" +"resetDefaultConfig" = "Reset ke Default" +"panelSettings" = "Umum" +"securitySettings" = "Otentikasi" +"TGBotSettings" = "Bot Telegram" +"panelListeningIP" = "IP Pendengar" +"panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)" +"panelListeningDomain" = "Domain Pendengar" +"panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)" +"panelPort" = "Port Pendengar" +"panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)" +"publicKeyPath" = "Path Kunci Publik" +"publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan ‘/‘)" +"privateKeyPath" = "Path Kunci Privat" +"privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan ‘/‘)" +"panelUrlPath" = "URI Path" +"panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)" +"pageSize" = "Ukuran Halaman" +"pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)" +"remarkModel" = "Model Catatan & Karakter Pemisah" +"datepicker" = "Jenis Kalender" +"datepickerPlaceholder" = "Pilih tanggal" +"datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini." +"sampleRemark" = "Contoh Catatan" +"oldUsername" = "Username Saat Ini" +"currentPassword" = "Kata Sandi Saat Ini" +"newUsername" = "Username Baru" +"newPassword" = "Kata Sandi Baru" +"telegramBotEnable" = "Aktifkan Bot Telegram" +"telegramBotEnableDesc" = "Mengaktifkan bot Telegram." +"telegramToken" = "Token Telegram" +"telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'." +"telegramProxy" = "Proxy SOCKS" +"telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)" +"telegramAPIServer" = "Telegram API Server" +"telegramAPIServerDesc" = "Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default." +"telegramChatId" = "ID Obrolan Admin" +"telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)" +"telegramNotifyTime" = "Waktu Notifikasi" +"telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)" +"tgNotifyBackup" = "Cadangan Database" +"tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan." +"tgNotifyLogin" = "Notifikasi Login" +"tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda." +"sessionMaxAge" = "Durasi Sesi" +"sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)" +"expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa" +"expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)" +"trafficDiff" = "Notifikasi Batas Traffic" +"trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)" +"tgNotifyCpu" = "Notifikasi Beban CPU" +"tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)" +"timeZone" = "Zone Waktu" +"timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini." +"subSettings" = "Langganan" +"subEnable" = "Aktifkan Layanan Langganan" +"subEnableDesc" = "Mengaktifkan layanan langganan." +"subTitle" = "Judul Langganan" +"subTitleDesc" = "Judul yang ditampilkan di klien VPN" +"subListen" = "IP Pendengar" +"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)" +"subPort" = "Port Pendengar" +"subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)" +"subCertPath" = "Path Kunci Publik" +"subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)" +"subKeyPath" = "Path Kunci Privat" +"subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan ‘/‘)" +"subPath" = "URI Path" +"subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)" +"subDomain" = "Domain Pendengar" +"subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)" +"subUpdates" = "Interval Pembaruan" +"subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)" +"subEncrypt" = "Encode" +"subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64." +"subShowInfo" = "Tampilkan Info Penggunaan" +"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien." +"subURI" = "URI Proxy Terbalik" +"subURIDesc" = "Path URI dari URL langganan untuk digunakan di belakang proxy." +"externalTrafficInformEnable" = "Informasikan API eksternal pada setiap pembaruan lalu lintas." +"externalTrafficInformEnableDesc" = "Inform external API on every traffic update." +"externalTrafficInformURI" = "Lalu Lintas Eksternal Menginformasikan URI" +"externalTrafficInformURIDesc" = "Pembaruan lalu lintas dikirim ke URI ini." +"fragment" = "Fragmentasi" +"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS" +"fragmentSett" = "Pengaturan Fragmentasi" +"noisesDesc" = "Aktifkan Noises." +"noisesSett" = "Pengaturan Noises" +"mux" = "Mux" +"muxDesc" = "Mengirimkan beberapa aliran data independen dalam aliran data yang sudah ada." +"muxSett" = "Pengaturan Mux" +"direct" = "Koneksi langsung" +"directDesc" = "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu." +"notifications" = "Notifikasi" +"certs" = "Sertifikat" +"externalTraffic" = "Lalu Lintas Eksternal" +"dateAndTime" = "Tanggal dan Waktu" +"proxyAndServer" = "Proxy dan Server" +"intervals" = "Interval" +"information" = "Informasi" +"language" = "Bahasa" +"telegramBotLanguage" = "Bahasa Bot Telegram" + +[pages.xray] +"title" = "Konfigurasi Xray" +"save" = "Simpan" +"restart" = "Restart Xray" +"restartSuccess" = "Xray berhasil diluncurkan ulang" +"stopSuccess" = "Xray telah berhasil dihentikan" +"restartError" = "Terjadi kesalahan saat memulai ulang Xray." +"stopError" = "Terjadi kesalahan saat menghentikan Xray." +"basicTemplate" = "Dasar" +"advancedTemplate" = "Lanjutan" +"generalConfigs" = "Strategi Umum" +"generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum." +"logConfigs" = "Catatan" +"logConfigsDesc" = "Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan" +"blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta." +"basicRouting" = "Perutean Dasar" +"blockConnectionsConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta." +"directConnectionsConfigsDesc" = "Koneksi langsung memastikan bahwa lalu lintas tertentu tidak dialihkan melalui server lain." +"blockips" = "Blokir IP" +"blockdomains" = "Blokir Domain" +"directips" = "IP Langsung" +"directdomains" = "Domain Langsung" +"ipv4Routing" = "Perutean IPv4" +"ipv4RoutingDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4." +"warpRouting" = "Perutean WARP" +"warpRoutingDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP." +"Template" = "Template Konfigurasi Xray Lanjutan" +"TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini." +"FreedomStrategy" = "Strategi Protokol Freedom" +"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom." +"RoutingStrategy" = "Strategi Pengalihan Keseluruhan" +"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan." +"Torrent" = "Blokir Protokol BitTorrent" +"Inbounds" = "Masuk" +"InboundsDesc" = "Menerima klien tertentu." +"Outbounds" = "Keluar" +"Balancers" = "Penyeimbang" +"OutboundsDesc" = "Atur jalur lalu lintas keluar." +"Routings" = "Aturan Pengalihan" +"RoutingsDesc" = "Prioritas setiap aturan penting!" +"completeTemplate" = "Semua" +"logLevel" = "Tingkat Log" +"logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat." +"accessLog" = "Log Akses" +"accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses" +"errorLog" = "Catatan eror" +"errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan" +"dnsLog" = "Log DNS" +"dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS" +"maskAddress" = "Alamat Masker" +"maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log." +"statistics" = "Statistik" +"statsInboundUplink" = "Statistik Unggah Masuk" +"statsInboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy masuk." +"statsInboundDownlink" = "Statistik Unduh Masuk" +"statsInboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy masuk." +"statsOutboundUplink" = "Statistik Unggah Keluar" +"statsOutboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar." +"statsOutboundDownlink" = "Statistik Unduh Keluar" +"statsOutboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar." + +[pages.xray.rules] +"first" = "Pertama" +"last" = "Terakhir" +"up" = "Naik" +"down" = "Turun" +"source" = "Sumber" +"dest" = "Tujuan" +"inbound" = "Masuk" +"outbound" = "Keluar" +"balancer" = "Pengimbang" +"info" = "Info" +"add" = "Tambahkan Aturan" +"edit" = "Edit Aturan" +"useComma" = "Item yang dipisahkan koma" + +[pages.xray.outbound] +"addOutbound" = "Tambahkan Keluar" +"addReverse" = "Tambahkan Revers" +"editOutbound" = "Edit Keluar" +"editReverse" = "Edit Revers" +"tag" = "Tag" +"tagDesc" = "Tag Unik" +"address" = "Alamat" +"reverse" = "Revers" +"domain" = "Domain" +"type" = "Tipe" +"bridge" = "Jembatan" +"portal" = "Portal" +"link" = "Tautan" +"intercon" = "Interkoneksi" +"settings" = "Pengaturan" +"accountInfo" = "Informasi Akun" +"outboundStatus" = "Status Keluar" +"sendThrough" = "Kirim Melalui" + +[pages.xray.balancer] +"addBalancer" = "Tambahkan Penyeimbang" +"editBalancer" = "Sunting Penyeimbang" +"balancerStrategy" = "Strategi" +"balancerSelectors" = "Penyeleksi" +"tag" = "Menandai" +"tagDesc" = "Label Unik" +"balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi." + +[pages.xray.wireguard] +"secretKey" = "Kunci Rahasia" +"publicKey" = "Kunci Publik" +"allowedIPs" = "IP yang Diizinkan" +"endpoint" = "Titik Akhir" +"psk" = "Kunci Pra-Bagi" +"domainStrategy" = "Strategi Domain" + +[pages.xray.dns] +"enable" = "Aktifkan DNS" +"enableDesc" = "Aktifkan server DNS bawaan" +"tag" = "Tanda DNS Masuk" +"tagDesc" = "Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan." +"clientIp" = "IP Klien" +"clientIpDesc" = "Digunakan untuk memberi tahu server tentang lokasi IP yang ditentukan selama kueri DNS" +"disableCache" = "Nonaktifkan cache" +"disableCacheDesc" = "Menonaktifkan caching DNS" +"disableFallback" = "Nonaktifkan Fallback" +"disableFallbackDesc" = "Menonaktifkan kueri DNS fallback" +"disableFallbackIfMatch" = "Nonaktifkan Fallback Jika Cocok" +"disableFallbackIfMatchDesc" = "Menonaktifkan kueri DNS fallback ketika daftar domain yang cocok dari server DNS terpenuhi" +"strategy" = "Strategi Kueri" +"strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain" +"add" = "Tambahkan Server" +"edit" = "Sunting Server" +"domains" = "Domains" +"expectIPs" = "IP yang Diharapkan" +"unexpectIPs" = "IP tak terduga" +"useSystemHosts" = "Gunakan Hosts Sistem" +"useSystemHostsDesc" = "Gunakan file hosts dari sistem yang terinstal" +"usePreset" = "Gunakan templat" +"dnsPresetTitle" = "Templat DNS" +"dnsPresetFamily" = "Keluarga" + +[pages.xray.fakedns] +"add" = "Tambahkan DNS Palsu" +"edit" = "Edit DNS Palsu" +"ipPool" = "Subnet Kumpulan IP" +"poolSize" = "Ukuran Kolam" + +[pages.settings.security] +"admin" = "Kredensial admin" +"twoFactor" = "Autentikasi dua faktor" +"twoFactorEnable" = "Aktifkan 2FA" +"twoFactorEnableDesc" = "Menambahkan lapisan autentikasi tambahan untuk keamanan lebih." +"twoFactorModalSetTitle" = "Aktifkan autentikasi dua faktor" +"twoFactorModalDeleteTitle" = "Nonaktifkan autentikasi dua faktor" +"twoFactorModalSteps" = "Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:" +"twoFactorModalFirstStep" = "1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi" +"twoFactorModalSecondStep" = "2. Masukkan kode dari aplikasi" +"twoFactorModalRemoveStep" = "Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor." +"twoFactorModalChangeCredentialsTitle" = "Ubah kredensial" +"twoFactorModalChangeCredentialsStep" = "Masukkan kode dari aplikasi untuk mengubah kredensial administrator." +"twoFactorModalSetSuccess" = "Autentikasi dua faktor telah berhasil dibuat" +"twoFactorModalDeleteSuccess" = "Autentikasi dua faktor telah berhasil dihapus" +"twoFactorModalError" = "Kode salah" + +[pages.settings.toasts] +"modifySettings" = "Parameter telah diubah." +"getSettings" = "Terjadi kesalahan saat mengambil parameter." +"modifyUserError" = "Terjadi kesalahan saat mengubah kredensial administrator." +"modifyUser" = "Anda telah berhasil mengubah kredensial administrator." +"originalUserPassIncorrect" = "Username atau password saat ini tidak valid" +"userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong" +"getOutboundTrafficError" = "Gagal mendapatkan lalu lintas keluar" +"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar" + +[tgbot] +"keyboardClosed" = "❌ Keyboard ditutup!" +"noResult" = "❗ Tidak ada hasil!" +"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!" +"wentWrong" = "❌ Terjadi kesalahan!" +"noIpRecord" = "❗ Tidak ada Catatan IP!" +"noInbounds" = "❗ Tidak ada inbound yang ditemukan!" +"unlimited" = "♾ Tidak terbatas (Reset)" +"add" = "Tambah" +"month" = "Bulan" +"months" = "Bulan" +"day" = "Hari" +"days" = "Hari" +"hours" = "Jam" +"minutes" = "Menit" +"unknown" = "Tidak diketahui" +"inbounds" = "Inbound" +"clients" = "Klien" +"offline" = "🔴 Offline" +"online" = "🟢 Online" + +[tgbot.commands] +"unknown" = "❗ Perintah tidak dikenal." +"pleaseChoose" = "👇 Harap pilih:\r\n" +"help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n" +"start" = "👋 Halo {{ .Firstname }}.\r\n" +"welcome" = "🤖 Selamat datang di {{.Hostname }} bot managemen.\r\n" +"status" = "✅ Bot dalam keadaan baik!" +"usage" = "❗ Harap berikan teks untuk mencari!" +"getID" = "🆔 ID Anda: {{ .ID }}" +"helpAdminCommands" = "Untuk memulai ulang Xray Core:\r\n/restart\r\n\r\nUntuk mencari email klien:\r\n/usage [Email]\r\n\r\nUntuk mencari inbound (dengan statistik klien):\r\n/inbound [Catatan]\r\n\r\nID Obrolan Telegram:\r\n/id" +"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n/usage [Email]\r\n\r\nID Obrolan Telegram:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ Operasi berhasil!" +"restartFailed" = "❗ Kesalahan dalam operasi.\r\n\r\nError: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core tidak berjalan." +"startDesc" = "Tampilkan menu utama" +"helpDesc" = "Bantuan bot" +"statusDesc" = "Periksa status bot" +"idDesc" = "Tampilkan ID Telegram Anda" + +[tgbot.messages] +"cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%" +"selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!" +"userSaved" = "✅ Pengguna Telegram tersimpan." +"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n" +"loginFailed" = "❗️ Gagal masuk ke panel.\r\n" +"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n" +"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n" +"hostname" = "💻 Host: {{ .Hostname }}\r\n" +"version" = "🚀 Versi X-Panel: {{ .Version }}\r\n" +"xrayVersion" = "📡 Versi Xray: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IP:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" +"username" = "👤 Nama Pengguna: {{ .Username }}\r\n" +"password" = "👤 Kata Sandi: {{ .Password }}\r\n" +"time" = "⏰ Waktu: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Port: {{ .Port }}\r\n" +"expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n" +"expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n" +"active" = "💡 Aktif: {{ .Enable }}\r\n" +"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n" +"online" = "🌐 Status Koneksi: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n" +"download" = "🔽 Unduh: ↓{{ .Download }}\r\n" +"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n" +"onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n" +"disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n" +"yes" = "✅ Ya" +"no" = "❌ Tidak" +"received_id" = "🔑📥 ID diperbarui." +"received_password" = "🔑📥 Kata sandi diperbarui." +"received_email" = "📧📥 Email diperbarui." +"received_comment" = "💬📥 Komentar diperbarui." +"id_prompt" = "🔑 ID Default: {{ .ClientId }}\n\nMasukkan ID Anda." +"pass_prompt" = "🔑 Kata Sandi Default: {{ .ClientPassword }}\n\nMasukkan kata sandi Anda." +"email_prompt" = "📧 Email Default: {{ .ClientEmail }}\n\nMasukkan email Anda." +"comment_prompt" = "💬 Komentar Default: {{ .ClientComment }}\n\nMasukkan komentar Anda." +"inbound_client_data_id" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!" +"inbound_client_data_pass" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!" +"cancel" = "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄" +"error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}" +"using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊" +"incorrect_input" ="Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫" +"AreYouSure" = "Apakah kamu yakin? 🤔" +"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil" +"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠️ Kesalahan: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Proses reset traffic selesai untuk semua klien." + +[tgbot.buttons] +"closeKeyboard" = "❌ Tutup Papan Ketik" +"cancel" = "❌ Batal" +"cancelReset" = "❌ Batal Reset" +"cancelIpLimit" = "❌ Batal Batas IP" +"confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?" +"confirmClearIps" = "✅ Konfirmasi Hapus IPs?" +"confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?" +"confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?" +"dbBackup" = "Dapatkan Cadangan DB" +"serverUsage" = "Penggunaan Server" +"getInbounds" = "Dapatkan Inbounds" +"depleteSoon" = "Habis Sebentar" +"clientUsage" = "Dapatkan Penggunaan" +"onlines" = "Klien Online" +"commands" = "Perintah" +"refresh" = "🔄 Perbarui" +"clearIPs" = "❌ Hapus IPs" +"removeTGUser" = "❌ Hapus Pengguna Telegram" +"selectTGUser" = "👤 Pilih Pengguna Telegram" +"selectOneTGUser" = "👤 Pilih Pengguna Telegram:" +"resetTraffic" = "📈 Reset Lalu Lintas" +"resetExpire" = "📅 Ubah Tanggal Kadaluarsa" +"ipLog" = "🔢 Log IP" +"ipLimit" = "🔢 Batas IP" +"setTGUser" = "👤 Set Pengguna Telegram" +"toggle" = "🔘 Aktifkan / Nonaktifkan" +"custom" = "🔢 Kustom" +"confirmNumber" = "✅ Konfirmasi: {{ .Num }}" +"confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}" +"limitTraffic" = "🚧 Batas Lalu Lintas" +"getBanLogs" = "Dapatkan Log Pemblokiran" +"allClients" = "Semua Klien" +"addClient" = "Tambah Klien" +"submitDisable" = "Kirim Sebagai Nonaktif ☑️" +"submitEnable" = "Kirim Sebagai Aktif ✅" +"use_default" = "🏷️ Gunakan Default" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Kata Sandi" +"change_email" = "⚙️📧 Email" +"change_comment" = "⚙️💬 Komentar" +"ResetAllTraffics" = "Reset Semua Lalu Lintas" +"SortedTrafficUsageReport" = "Laporan Penggunaan Lalu Lintas yang Terurut" + +[tgbot.answers] +"successfulOperation" = "✅ Operasi berhasil!" +"errorOperation" = "❗ Kesalahan dalam operasi." +"getInboundsFailed" = "❌ Gagal mendapatkan inbounds." +"getClientsFailed" = "❌ Gagal mendapatkan klien." +"canceled" = "❌ {{ .Email }}: Operasi dibatalkan." +"clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil." +"IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil." +"resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil." +"expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil." +"resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil." +"clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil." +"getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP." +"getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil." +"enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil." +"disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil." +"askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ChatID Telegram Anda dalam konfigurasi Anda.\r\n\r\nChatID Pengguna Anda: {{ .TgUserID }}" +"chooseClient" = "Pilih Klien untuk Inbound {{ .Inbound }}" +"chooseInbound" = "Pilih Inbound" diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml new file mode 100644 index 0000000..6edd659 --- /dev/null +++ b/web/translation/translate.ja_JP.toml @@ -0,0 +1,739 @@ +"username" = "ユーザー名" +"password" = "パスワード" +"login" = "ログイン" +"confirm" = "確認" +"cancel" = "キャンセル" +"close" = "閉じる" +"create" = "作成" +"update" = "更新" +"copy" = "コピー" +"copied" = "コピー済み" +"download" = "ダウンロード" +"remark" = "備考" +"enable" = "有効化" +"protocol" = "プロトコル" +"search" = "検索" +"filter" = "フィルター" +"loading" = "読み込み中..." +"second" = "秒" +"minute" = "分" +"hour" = "時間" +"day" = "日" +"check" = "確認" +"indefinite" = "無期限" +"unlimited" = "無制限" +"none" = "なし" +"qrCode" = "QRコード" +"info" = "詳細情報" +"edit" = "編集" +"delete" = "削除" +"reset" = "リセット" +"noData" = "データなし。" +"copySuccess" = "コピー成功" +"sure" = "確定" +"encryption" = "暗号化" +"useIPv4ForHost" = "ホストにIPv4を使用" +"transmission" = "伝送" +"host" = "ホスト" +"path" = "パス" +"camouflage" = "偽装" +"status" = "ステータス" +"enabled" = "有効" +"disabled" = "無効" +"depleted" = "消耗済み" +"depletingSoon" = "間もなく消耗" +"offline" = "オフライン" +"online" = "オンライン" +"domainName" = "ドメイン名" +"monitor" = "監視" +"certificate" = "証明書" +"fail" = "失敗" +"comment" = "コメント" +"success" = "成功" +"lastOnline" = "最終オンライン" +"getVersion" = "バージョン取得" +"install" = "インストール" +"clients" = "クライアント" +"usage" = "利用状況" +"twoFactorCode" = "コード" +"remained" = "残り" +"security" = "セキュリティ" +"secAlertTitle" = "セキュリティアラート" +"secAlertSsl" = "この接続は安全ではありません。TLSを有効にしてデータ保護を行うまで、機密情報を入力しないでください。" +"secAlertConf" = "一部の設定は脆弱です。潜在的な脆弱性を防ぐために、セキュリティプロトコルを強化することをお勧めします。" +"secAlertSSL" = "セキュアな接続がありません。データ保護のためにTLS証明書をインストールしてください。" +"secAlertPanelPort" = "デフォルトのポートにはセキュリティリスクがあります。ランダムなポートまたは特定のポートを設定してください。" +"secAlertPanelURI" = "デフォルトのURIパスは安全ではありません。複雑なURIパスを設定してください。" +"secAlertSubURI" = "サブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。" +"secAlertSubJsonURI" = "JSONサブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。" +"emptyDnsDesc" = "追加されたDNSサーバーはありません。" +"emptyFakeDnsDesc" = "追加されたFake DNSサーバーはありません。" +"emptyBalancersDesc" = "追加されたバランサーはありません。" +"emptyReverseDesc" = "追加されたリバースプロキシはありません。" +"somethingWentWrong" = "エラーが発生しました" + +[menu] +"theme" = "テーマ" +"dark" = "ダーク" +"ultraDark" = "ウルトラダーク" +"dashboard" = "ダッシュボード" +"inbounds" = "インバウンド一覧" +"settings" = "パネル設定" +"xray" = "Xray設定" +"logout" = "ログアウト" +"link" = "リンク管理" + +[pages.login] +"hello" = "こんにちは" +"title" = "ようこそ" +"loginAgain" = "ログインセッションが切れました。再度ログインしてください。" + +[pages.login.toasts] +"invalidFormData" = "データ形式エラー" +"emptyUsername" = "ユーザー名を入力してください" +"emptyPassword" = "パスワードを入力してください" +"wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。" +"successLogin" = "アカウントに正常にログインしました。" + +[pages.index] +"title" = "システムステータス" +"cpu" = "CPU" +"logicalProcessors" = "論理プロセッサ" +"frequency" = "周波数" +"swap" = "スワップ" +"storage" = "ストレージ" +"memory" = "RAM" +"threads" = "スレッド" +"xrayStatus" = "Xray" +"stopXray" = "停止" +"restartXray" = "再起動" +"xraySwitch" = "バージョン" +"xraySwitchClick" = "切り替えるバージョンを選択してください" +"xraySwitchClickDesk" = "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。" +"xrayStatusUnknown" = "不明" +"xrayStatusRunning" = "実行中" +"xrayStatusStop" = "停止" +"xrayStatusError" = "エラー" +"xrayErrorPopoverTitle" = "Xrayの実行中にエラーが発生しました" +"operationHours" = "システム稼働時間" +"systemLoad" = "システム負荷" +"systemLoadDesc" = "過去1、5、15分間のシステム平均負荷" +"connectionCount" = "接続数" +"ipAddresses" = "IPアドレス" +"toggleIpVisibility" = "IPの表示を切り替える" +"overallSpeed" = "全体の速度" +"upload" = "アップロード" +"download" = "ダウンロード" +"totalData" = "総データ量" +"sent" = "送信" +"received" = "受信" +"documentation" = "ドキュメント" +"xraySwitchVersionDialog" = "Xrayのバージョンを本当に変更しますか?" +"xraySwitchVersionDialogDesc" = "Xrayのバージョンが#version#に変更されます。" +"xraySwitchVersionPopover" = "Xrayの更新が成功しました" +"geofileUpdateDialog" = "ジオファイルを本当に更新しますか?" +"geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。" +"geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。" +"geofilesUpdateAll" = "すべて更新" +"geofileUpdatePopover" = "ジオファイルの更新が成功しました" +"dontRefresh" = "インストール中、このページをリロードしないでください" +"logs" = "ログ" +"config" = "設定" +"backup" = "バックアップ" +"backupTitle" = "データベースのバックアップと復元" +"exportDatabase" = "バックアップ" +"exportDatabaseDesc" = "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。" +"importDatabase" = "復元" +"importDatabaseDesc" = "クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。" +"importDatabaseSuccess" = "データベースのインポートに成功しました" +"importDatabaseError" = "データベースのインポート中にエラーが発生しました" +"readDatabaseError" = "データベースの読み取り中にエラーが発生しました" +"getDatabaseError" = "データベースの取得中にエラーが発生しました" +"getConfigError" = "設定ファイルの取得中にエラーが発生しました" + +[pages.inbounds] +"allTimeTraffic" = "総トラフィック" +"allTimeTrafficUsage" = "これまでの総使用量" +"title" = "インバウンド一覧" +"totalDownUp" = "総アップロード / ダウンロード" +"totalUsage" = "総使用量" +"inboundCount" = "インバウンド数" +"operate" = "メニュー" +"enable" = "有効化" +"remark" = "備考" +"protocol" = "プロトコル" +"port" = "ポート" +"portMap" = "ポートマッピング" +"traffic" = "トラフィック" +"details" = "詳細情報" +"transportConfig" = "トランスポート設定" +"expireDate" = "有効期限" +"createdAt" = "作成" +"updatedAt" = "更新" +"resetTraffic" = "トラフィックリセット" +"addInbound" = "インバウンド追加" +"generalActions" = "一般操作" +"autoRefresh" = "自動更新" +"autoRefreshInterval" = "間隔" +"modifyInbound" = "インバウンド修正" +"deleteInbound" = "インバウンド削除" +"deleteInboundContent" = "インバウンドを削除してもよろしいですか?" +"deleteClient" = "クライアント削除" +"deleteClientContent" = "クライアントを削除してもよろしいですか?" +"resetTrafficContent" = "トラフィックをリセットしてもよろしいですか?" +"copyLink" = "リンクをコピー" +"address" = "アドレス" +"network" = "ネットワーク" +"destinationPort" = "宛先ポート" +"targetAddress" = "宛先アドレス" +"monitorDesc" = "空白にするとすべてのIPを監視" +"meansNoLimit" = "= 無制限(単位:GB)" +"totalFlow" = "総トラフィック" +"leaveBlankToNeverExpire" = "空白にすると期限なし" +"noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします" +"certificatePath" = "ファイルパス" +"certificateContent" = "ファイル内容" +"publicKey" = "公開鍵" +"privatekey" = "秘密鍵" +"clickOnQRcode" = "QRコードをクリックしてコピー" +"client" = "クライアント" +"export" = "リンクエクスポート" +"clone" = "複製" +"cloneInbound" = "複製" +"cloneInboundContent" = "このインバウンドルールは、ポート(Port)、リスニングIP(Listening IP)、クライアント(Clients)を除くすべての設定がクローンされます" +"cloneInboundOk" = "クローン作成" +"resetAllTraffic" = "すべてのインバウンドトラフィックをリセット" +"resetAllTrafficTitle" = "すべてのインバウンドトラフィックをリセット" +"resetAllTrafficContent" = "すべてのインバウンドトラフィックをリセットしてもよろしいですか?" +"resetInboundClientTraffics" = "クライアントトラフィックをリセット" +"resetInboundClientTrafficTitle" = "すべてのクライアントトラフィックをリセット" +"resetInboundClientTrafficContent" = "このインバウンドクライアントのすべてのトラフィックをリセットしてもよろしいですか?" +"resetAllClientTraffics" = "すべてのクライアントトラフィックをリセット" +"resetAllClientTrafficTitle" = "すべてのクライアントトラフィックをリセット" +"resetAllClientTrafficContent" = "すべてのクライアントのトラフィックをリセットしてもよろしいですか?" +"delDepletedClients" = "トラフィックが尽きたクライアントを削除" +"delDepletedClientsTitle" = "トラフィックが尽きたクライアントを削除" +"delDepletedClientsContent" = "トラフィックが尽きたすべてのクライアントを削除してもよろしいですか?" +"email" = "メールアドレス" +"emailDesc" = "メールアドレスは一意でなければなりません" +"IPLimit" = "IP制限" +"IPLimitDesc" = "設定値を超えるとインバウンドトラフィックが無効になります。(0 = 無効)" +"IPLimitlog" = "IPログ" +"IPLimitlogDesc" = "IP履歴ログ(無効なインバウンドトラフィックを有効にするには、ログをクリアしてください)" +"IPLimitlogclear" = "ログをクリア" +"setDefaultCert" = "パネル設定から証明書を設定" +"telegramDesc" = "TelegramチャットIDを提供してください。(ボットで'/id'コマンドを使用)または(@userinfobot)" +"subscriptionDesc" = "サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。" +"info" = "情報" +"same" = "同じ" +"inboundData" = "インバウンドデータ" +"exportInbound" = "インバウンドルールをエクスポート" +"import" = "インポート" +"importInbound" = "インバウンドルールをインポート" + +[pages.client] +"add" = "クライアント追加" +"edit" = "クライアント編集" +"submitAdd" = "クライアント追加" +"submitEdit" = "変更を保存" +"clientCount" = "クライアント数" +"bulk" = "一括作成" +"method" = "方法" +"first" = "最初" +"last" = "最後" +"prefix" = "プレフィックス" +"postfix" = "サフィックス" +"delayedStart" = "初回使用後に開始" +"expireDays" = "期間" +"days" = "日" +"renew" = "自動更新" +"renewDesc" = "期限が切れた後に自動更新。(0 = 無効)(単位:日)" + +[pages.inbounds.toasts] +"obtain" = "取得" +"updateSuccess" = "更新が成功しました" +"logCleanSuccess" = "ログがクリアされました" +"inboundsUpdateSuccess" = "インバウンドが正常に更新されました" +"inboundUpdateSuccess" = "インバウンドが正常に更新されました" +"inboundCreateSuccess" = "インバウンドが正常に作成されました" +"inboundDeleteSuccess" = "インバウンドが正常に削除されました" +"inboundClientAddSuccess" = "インバウンドクライアントが追加されました" +"inboundClientDeleteSuccess" = "インバウンドクライアントが削除されました" +"inboundClientUpdateSuccess" = "インバウンドクライアントが更新されました" +"delDepletedClientsSuccess" = "すべての枯渇したクライアントが削除されました" +"resetAllClientTrafficSuccess" = "クライアントのすべてのトラフィックがリセットされました" +"resetAllTrafficSuccess" = "すべてのトラフィックがリセットされました" +"resetInboundClientTrafficSuccess" = "トラフィックがリセットされました" +"trafficGetError" = "トラフィックの取得中にエラーが発生しました" +"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。" +"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。" + +[pages.inbounds.stream.general] +"request" = "リクエスト" +"response" = "レスポンス" +"name" = "名前" +"value" = "値" + +[pages.inbounds.stream.tcp] +"version" = "バージョン" +"method" = "方法" +"path" = "パス" +"status" = "ステータス" +"statusDescription" = "ステータス説明" +"requestHeader" = "リクエストヘッダー" +"responseHeader" = "レスポンスヘッダー" + +[pages.settings] +"title" = "パネル設定" +"save" = "保存" +"infoDesc" = "ここでのすべての変更は、保存してパネルを再起動する必要があります" +"restartPanel" = "パネル再起動" +"restartPanelDesc" = "パネルを再起動してもよろしいですか?再起動後にパネルにアクセスできない場合は、サーバーでパネルログを確認してください" +"restartPanelSuccess" = "パネルの再起動に成功しました" +"actions" = "操作" +"resetDefaultConfig" = "デフォルト設定にリセット" +"panelSettings" = "一般" +"securitySettings" = "セキュリティ設定" +"TGBotSettings" = "Telegramボット設定" +"panelListeningIP" = "パネル監視IP" +"panelListeningIPDesc" = "デフォルトではすべてのIPを監視する" +"panelListeningDomain" = "パネル監視ドメイン" +"panelListeningDomainDesc" = "デフォルトで空白の場合、すべてのドメインとIPアドレスを監視する" +"panelPort" = "パネル監視ポート" +"panelPortDesc" = "再起動で有効" +"publicKeyPath" = "パネル証明書公開鍵ファイルパス" +"publicKeyPathDesc" = "'/'で始まる絶対パスを入力" +"privateKeyPath" = "パネル証明書秘密鍵ファイルパス" +"privateKeyPathDesc" = "'/'で始まる絶対パスを入力" +"panelUrlPath" = "パネルURLルートパス" +"panelUrlPathDesc" = "'/'で始まり、'/'で終わる必要があります" +"pageSize" = "ページサイズ" +"pageSizeDesc" = "インバウンドテーブルのページサイズを定義します。0を設定すると無効化されます" +"remarkModel" = "備考モデルと区切り記号" +"datepicker" = "日付ピッカー" +"datepickerPlaceholder" = "日付を選択" +"datepickerDescription" = "日付選択カレンダーで有効期限を指定する" +"sampleRemark" = "備考の例" +"oldUsername" = "旧ユーザー名" +"currentPassword" = "旧パスワード" +"newUsername" = "新しいユーザー名" +"newPassword" = "新しいパスワード" +"telegramBotEnable" = "Telegramボットを有効にする" +"telegramBotEnableDesc" = "Telegramボット機能を有効にする" +"telegramToken" = "Telegramボットトークン" +"telegramTokenDesc" = "'@BotFather'から取得したTelegramボットトークン" +"telegramProxy" = "SOCKS5プロキシ" +"telegramProxyDesc" = "SOCKS5プロキシを有効にしてTelegramに接続する(ガイドに従って設定を調整)" +"telegramAPIServer" = "Telegram APIサーバー" +"telegramAPIServerDesc" = "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する" +"telegramChatId" = "管理者チャットID" +"telegramChatIdDesc" = "Telegram管理者チャットID(複数の場合はカンマで区切る)@userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する" +"telegramNotifyTime" = "通知時間" +"telegramNotifyTimeDesc" = "定期的なTelegramボット通知時間を設定する(crontab時間形式を使用)" +"tgNotifyBackup" = "データベースバックアップ" +"tgNotifyBackupDesc" = "レポート付きのデータベースバックアップファイルを送信" +"tgNotifyLogin" = "ログイン通知" +"tgNotifyLoginDesc" = "誰かがパネルにログインしようとしたときに、ユーザー名、IPアドレス、時間を表示する" +"sessionMaxAge" = "セッション期間" +"sessionMaxAgeDesc" = "ログイン状態を保持する期間(単位:分)" +"expireTimeDiff" = "有効期限通知のしきい値" +"expireTimeDiffDesc" = "このしきい値に達した場合、有効期限に関する通知を受け取る(単位:日)" +"trafficDiff" = "トラフィック消耗しきい値" +"trafficDiffDesc" = "このしきい値に達した場合、トラフィック消耗に関する通知を受け取る(単位:GB)" +"tgNotifyCpu" = "CPU負荷通知しきい値" +"tgNotifyCpuDesc" = "CPU負荷がこのしきい値を超えた場合、通知を受け取る(単位:%)" +"timeZone" = "タイムゾーン" +"timeZoneDesc" = "定時タスクはこのタイムゾーンの時間に従って実行される" +"subSettings" = "サブスクリプション設定" +"subEnable" = "サブスクリプションサービスを有効にする" +"subEnableDesc" = "サブスクリプションサービス機能を有効にする" +"subTitle" = "サブスクリプションタイトル" +"subTitleDesc" = "VPNクライアントに表示されるタイトル" +"subListen" = "監視IP" +"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)" +"subPort" = "監視ポート" +"subPortDesc" = "サブスクリプションサービスが監視するポート番号(使用されていないポートである必要があります)" +"subCertPath" = "公開鍵パス" +"subCertPathDesc" = "サブスクリプションサービスで使用する公開鍵ファイルのパス('/'で始まる)" +"subKeyPath" = "秘密鍵パス" +"subKeyPathDesc" = "サブスクリプションサービスで使用する秘密鍵ファイルのパス('/'で始まる)" +"subPath" = "URIパス" +"subPathDesc" = "サブスクリプションサービスで使用するURIパス('/'で始まり、'/'で終わる)" +"subDomain" = "監視ドメイン" +"subDomainDesc" = "サブスクリプションサービスが監視するドメイン(空白にするとすべてのドメインとIPを監視)" +"subUpdates" = "更新間隔" +"subUpdatesDesc" = "クライアントアプリケーションでサブスクリプションURLの更新間隔(単位:時間)" +"subEncrypt" = "エンコード" +"subEncryptDesc" = "サブスクリプションサービスが返す内容をBase64エンコードする" +"subShowInfo" = "利用情報を表示" +"subShowInfoDesc" = "クライアントアプリで残りのトラフィックと日付情報を表示する" +"subURI" = "リバースプロキシURI" +"subURIDesc" = "プロキシ後ろのサブスクリプションURLのURIパスに使用する" +"externalTrafficInformEnable" = "外部トラフィック情報" +"externalTrafficInformEnableDesc" = "トラフィックの更新ごとに外部 API に通知します。" +"externalTrafficInformURI" = "外部トラフィック通知 URI" +"externalTrafficInformURIDesc" = "トラフィックの更新ごとに外部 API に通知します。" +"fragment" = "フラグメント" +"fragmentDesc" = "TLS helloパケットのフラグメントを有効にする" +"fragmentSett" = "設定" +"noisesDesc" = "Noisesを有効にする" +"noisesSett" = "Noises設定" +"mux" = "マルチプレクサ" +"muxDesc" = "確立されたストリーム内で複数の独立したストリームを伝送する" +"muxSett" = "マルチプレクサ設定" +"direct" = "直接接続" +"directDesc" = "特定の国のドメインまたはIP範囲に直接接続する" +"notifications" = "通知" +"certs" = "証明書" +"externalTraffic" = "外部トラフィック" +"dateAndTime" = "日付と時刻" +"proxyAndServer" = "プロキシとサーバー" +"intervals" = "間隔" +"information" = "情報" +"language" = "言語" +"telegramBotLanguage" = "Telegram Botの言語" + +[pages.xray] +"title" = "Xray 設定" +"save" = "保存" +"restart" = "Xray 再起動" +"restartSuccess" = "Xrayの再起動に成功しました" +"stopSuccess" = "Xrayが正常に停止しました" +"restartError" = "Xrayの再起動中にエラーが発生しました。" +"stopError" = "Xrayの停止中にエラーが発生しました。" +"basicTemplate" = "基本設定" +"advancedTemplate" = "高度な設定" +"generalConfigs" = "一般設定" +"generalConfigsDesc" = "これらのオプションは一般設定を決定します" +"logConfigs" = "ログ" +"logConfigsDesc" = "ログはサーバーのパフォーマンスに影響を与える可能性があるため、必要な場合にのみ有効にすることをお勧めします" +"blockConfigsDesc" = "これらのオプションは、特定のプロトコルやウェブサイトへのユーザー接続をブロックします" +"basicRouting" = "基本ルーティング" +"blockConnectionsConfigsDesc" = "これらのオプションにより、特定のリクエスト元の国に基づいてトラフィックをブロックします。" +"directConnectionsConfigsDesc" = "直接接続により、特定のトラフィックが他のサーバーを経由しないようにします。" +"blockips" = "IPをブロック" +"blockdomains" = "ドメインをブロック" +"directips" = "直接IP" +"directdomains" = "直接ドメイン" +"ipv4Routing" = "IPv4 ルーティング" +"ipv4RoutingDesc" = "このオプションはIPv4のみを介してターゲットドメインへルーティングします" +"warpRouting" = "WARP ルーティング" +"warpRoutingDesc" = "注意:これらのオプションを使用する前に、パネルのGitHubの手順に従って、サーバーにsocks5プロキシモードでWARPをインストールしてください。WARPはCloudflareサーバー経由でトラフィックをウェブサイトにルーティングします。" +"Template" = "高度なXray設定テンプレート" +"TemplateDesc" = "最終的なXray設定ファイルはこのテンプレートに基づいて生成されます" +"FreedomStrategy" = "Freedom プロトコル戦略" +"FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する" +"RoutingStrategy" = "ルーティングドメイン戦略設定" +"RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する" +"Torrent" = "BitTorrent プロトコルをブロック" +"Inbounds" = "インバウンドルール" +"InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる" +"Outbounds" = "アウトバウンドルール" +"Balancers" = "負荷分散" +"OutboundsDesc" = "アウトバウンドトラフィックの送信方法を設定する" +"Routings" = "ルーティングルール" +"RoutingsDesc" = "各ルールの優先順位が重要です" +"completeTemplate" = "すべて" +"logLevel" = "ログレベル" +"logLevelDesc" = "エラーログのレベルを指定し、記録する情報を示します" +"accessLog" = "アクセスログ" +"accessLogDesc" = "アクセスログのファイルパス。特殊値 'none' はアクセスログを無効にします" +"errorLog" = "エラーログ" +"errorLogDesc" = "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします" +"dnsLog" = "DNS ログ" +"dnsLogDesc" = "DNSクエリのログを有効にするかどうか" +"maskAddress" = "アドレスをマスク" +"maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます" +"statistics" = "統計" +"statsInboundUplink" = "インバウンドアップロード統計" +"statsInboundUplinkDesc" = "すべてのインバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。" +"statsInboundDownlink" = "インバウンドダウンロード統計" +"statsInboundDownlinkDesc" = "すべてのインバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。" +"statsOutboundUplink" = "アウトバウンドアップロード統計" +"statsOutboundUplinkDesc" = "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。" +"statsOutboundDownlink" = "アウトバウンドダウンロード統計" +"statsOutboundDownlinkDesc" = "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。" + +[pages.xray.rules] +"first" = "最初" +"last" = "最後" +"up" = "上へ" +"down" = "下へ" +"source" = "ソース" +"dest" = "宛先アドレス" +"inbound" = "インバウンド" +"outbound" = "アウトバウンド" +"balancer" = "負荷分散" +"info" = "情報" +"add" = "ルール追加" +"edit" = "ルール編集" +"useComma" = "カンマ区切りの項目" + +[pages.xray.outbound] +"addOutbound" = "アウトバウンド追加" +"addReverse" = "リバース追加" +"editOutbound" = "アウトバウンド編集" +"editReverse" = "リバース編集" +"tag" = "タグ" +"tagDesc" = "一意のタグ" +"address" = "アドレス" +"reverse" = "リバース" +"domain" = "ドメイン" +"type" = "タイプ" +"bridge" = "ブリッジ" +"portal" = "ポータル" +"link" = "リンク" +"intercon" = "インターコネクション" +"settings" = "設定" +"accountInfo" = "アカウント情報" +"outboundStatus" = "アウトバウンドステータス" +"sendThrough" = "送信経路" + +[pages.xray.balancer] +"addBalancer" = "負荷分散追加" +"editBalancer" = "負荷分散編集" +"balancerStrategy" = "戦略" +"balancerSelectors" = "セレクター" +"tag" = "タグ" +"tagDesc" = "一意のタグ" +"balancerDesc" = "balancerTagとoutboundTagは同時に使用できません。同時に使用された場合、outboundTagのみが有効になります。" + +[pages.xray.wireguard] +"secretKey" = "シークレットキー" +"publicKey" = "公開鍵" +"allowedIPs" = "許可されたIP" +"endpoint" = "エンドポイント" +"psk" = "共有キー" +"domainStrategy" = "ドメイン戦略" + +[pages.xray.dns] +"enable" = "DNSを有効にする" +"enableDesc" = "組み込みDNSサーバーを有効にする" +"tag" = "DNSインバウンドタグ" +"tagDesc" = "このタグはルーティングルールでインバウンドタグとして使用できます" +"clientIp" = "クライアントIP" +"clientIpDesc" = "DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます" +"disableCache" = "キャッシュを無効にする" +"disableCacheDesc" = "DNSキャッシュを無効にします" +"disableFallback" = "フォールバックを無効にする" +"disableFallbackDesc" = "フォールバックDNSクエリを無効にします" +"disableFallbackIfMatch" = "一致した場合にフォールバックを無効にする" +"disableFallbackIfMatchDesc" = "DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします" +"strategy" = "クエリ戦略" +"strategyDesc" = "ドメイン名解決の全体的な戦略" +"add" = "サーバー追加" +"edit" = "サーバー編集" +"domains" = "ドメイン" +"expectIPs" = "期待されるIP" +"unexpectIPs" = "予期しないIP" +"useSystemHosts" = "システムのHostsを使用" +"useSystemHostsDesc" = "インストール済みシステムのhostsファイルを使用する" +"usePreset" = "テンプレートを使用" +"dnsPresetTitle" = "DNSテンプレート" +"dnsPresetFamily" = "ファミリー" + +[pages.xray.fakedns] +"add" = "フェイクDNS追加" +"edit" = "フェイクDNS編集" +"ipPool" = "IPプールサブネット" +"poolSize" = "プールサイズ" + +[pages.settings.security] +"admin" = "管理者の資格情報" +"twoFactor" = "二段階認証" +"twoFactorEnable" = "2FAを有効化" +"twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。" +"twoFactorModalSetTitle" = "二段階認証を有効にする" +"twoFactorModalDeleteTitle" = "二段階認証を無効にする" +"twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:" +"twoFactorModalFirstStep" = "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます" +"twoFactorModalSecondStep" = "2. アプリからコードを入力してください" +"twoFactorModalRemoveStep" = "二段階認証を削除するには、アプリからコードを入力してください。" +"twoFactorModalChangeCredentialsTitle" = "認証情報の変更" +"twoFactorModalChangeCredentialsStep" = "管理者の認証情報を変更するには、アプリケーションからコードを入力してください。" +"twoFactorModalSetSuccess" = "二要素認証が正常に設定されました" +"twoFactorModalDeleteSuccess" = "二要素認証が正常に削除されました" +"twoFactorModalError" = "コードが間違っています" + +[pages.settings.toasts] +"modifySettings" = "パラメーターが変更されました。" +"getSettings" = "パラメーターの取得中にエラーが発生しました" +"modifyUserError" = "管理者認証情報の変更中にエラーが発生しました。" +"modifyUser" = "管理者の認証情報を正常に変更しました。" +"originalUserPassIncorrect" = "旧ユーザー名または旧パスワードが間違っています" +"userPassMustBeNotEmpty" = "新しいユーザー名と新しいパスワードは空にできません" +"getOutboundTrafficError" = "送信トラフィックの取得エラー" +"resetOutboundTrafficError" = "送信トラフィックのリセットエラー" + +[tgbot] +"keyboardClosed" = "❌ キーボードを閉じました!" +"noResult" = "❗ 結果がありません!" +"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!" +"wentWrong" = "❌ 何かがうまくいかなかった!" +"noIpRecord" = "❗ IPレコードがありません!" +"noInbounds" = "❗ インバウンドが見つかりません!" +"unlimited" = "♾ 無制限(リセット)" +"add" = "追加" +"month" = "月" +"months" = "ヶ月" +"day" = "日" +"days" = "日間" +"hours" = "時間" +"minutes" = "分" +"unknown" = "不明" +"inbounds" = "インバウンド" +"clients" = "クライアント" +"offline" = "🔴 オフライン" +"online" = "🟢 オンライン" + +[tgbot.commands] +"unknown" = "❗ 不明なコマンド" +"pleaseChoose" = "👇 選択してください:\r\n" +"help" = "🤖 このボットをご利用いただきありがとうございます!サーバーから特定のデータを提供し、必要な変更を行うことができます。\r\n\r\n" +"start" = "👋 こんにちは、{{ .Firstname }}。\r\n" +"welcome" = "🤖 {{ .Hostname }} 管理ボットへようこそ。\r\n" +"status" = "✅ ボットは正常に動作しています!" +"usage" = "❗ 検索するテキストを入力してください!" +"getID" = "🆔 あなたのIDは:{{ .ID }}" +"helpAdminCommands" = "Xray Coreを再起動するには:\r\n/restart\r\n\r\nクライアントの電子メールを検索するには:\r\n/usage [電子メール]\r\n\r\nインバウンド(クライアントの統計情報を含む)を検索するには:\r\n/inbound [備考]\r\n\r\nTelegramチャットID:\r\n/id" +"helpClientCommands" = "統計情報を検索するには、次のコマンドを使用してください:\r\n/usage [電子メール]\r\n\r\nTelegramチャットID:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ 操作成功!" +"restartFailed" = "❗ 操作エラー。\r\n\r\nエラー: {{ .Error }}" +"xrayNotRunning" = "❗ Xray Core は動作していません。" +"startDesc" = "メインメニューを表示" +"helpDesc" = "ボットのヘルプ" +"statusDesc" = "ボットの状態を確認" +"idDesc" = "Telegram IDを表示" + +[tgbot.messages] +"cpuThreshold" = "🔴 CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました" +"selectUserFailed" = "❌ ユーザーの選択に失敗しました!" +"userSaved" = "✅ Telegramユーザーが保存されました。" +"loginSuccess" = "✅ パネルに正常にログインしました。\r\n" +"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n" +"report" = "🕰 定期報告:{{ .RunTime }}\r\n" +"datetime" = "⏰ 日時:{{ .DateTime }}\r\n" +"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n" +"version" = "🚀 X-Panel バージョン:{{ .Version }}\r\n" +"xrayVersion" = "📡 Xray バージョン: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" +"ip" = "🌐 IP:{{ .IP }}\r\n" +"ips" = "🔢 IPアドレス:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ サーバー稼働時間:{{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 サーバー負荷:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 サーバーメモリ:{{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP接続数:{{ .Count }}\r\n" +"udpCount" = "🔸 UDP接続数:{{ .Count }}\r\n" +"traffic" = "🚦 トラフィック:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Xrayステータス:{{ .State }}\r\n" +"username" = "👤 ユーザー名:{{ .Username }}\r\n" +"password" = "👤 パスワード: {{ .Password }}\r\n" +"time" = "⏰ 時間:{{ .Time }}\r\n" +"inbound" = "📍 インバウンド:{{ .Remark }}\r\n" +"port" = "🔌 ポート:{{ .Port }}\r\n" +"expire" = "📅 有効期限:{{ .Time }}\r\n" +"expireIn" = "📅 残り時間:{{ .Time }}\r\n" +"active" = "💡 有効:{{ .Enable }}\r\n" +"enabled" = "🚨 有効化済み:{{ .Enable }}\r\n" +"online" = "🌐 接続ステータス:{{ .Status }}\r\n" +"email" = "📧 メール:{{ .Email }}\r\n" +"upload" = "🔼 アップロード↑:{{ .Upload }}\r\n" +"download" = "🔽 ダウンロード↓:{{ .Download }}\r\n" +"total" = "📊 合計:{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Telegramユーザー:{{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 消耗済みの {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 消耗済みの {{ .Type }} 数量:\r\n" +"onlinesCount" = "🌐 オンラインクライアント:{{ .Count }}\r\n" +"disabled" = "🛑 無効化:{{ .Disabled }}\r\n" +"depleteSoon" = "🔜 間もなく消耗:{{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 バックアップ時間:{{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n" +"yes" = "✅ はい" +"no" = "❌ いいえ" +"received_id" = "🔑📥 IDが更新されました。" +"received_password" = "🔑📥 パスワードが更新されました。" +"received_email" = "📧📥 メールが更新されました。" +"received_comment" = "💬📥 コメントが更新されました。" +"id_prompt" = "🔑 デフォルトID: {{ .ClientId }}\n\nIDを入力してください。" +"pass_prompt" = "🔑 デフォルトパスワード: {{ .ClientPassword }}\n\nパスワードを入力してください。" +"email_prompt" = "📧 デフォルトメール: {{ .ClientEmail }}\n\nメールを入力してください。" +"comment_prompt" = "💬 デフォルトコメント: {{ .ClientComment }}\n\nコメントを入力してください。" +"inbound_client_data_id" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます!" +"inbound_client_data_pass" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 パスワード: {{ .ClientPass }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます!" +"cancel" = "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄" +"error_add_client" = "⚠️ エラー:\n\n {{ .error }}" +"using_default_value" = "わかりました、デフォルト値を使用します。 😊" +"incorrect_input" ="入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫" +"AreYouSure" = "本当にいいですか?🤔" +"SuccessResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功" +"FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ エラー: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 すべてのクライアントのトラフィックリセットが完了しました。" + +[tgbot.buttons] +"closeKeyboard" = "❌ キーボードを閉じる" +"cancel" = "❌ キャンセル" +"cancelReset" = "❌ リセットをキャンセル" +"cancelIpLimit" = "❌ IP制限をキャンセル" +"confirmResetTraffic" = "✅ トラフィックをリセットしますか?" +"confirmClearIps" = "✅ IPをクリアしますか?" +"confirmRemoveTGUser" = "✅ Telegramユーザーを削除しますか?" +"confirmToggle" = "✅ ユーザーを有効/無効にしますか?" +"dbBackup" = "データベースバックアップを取得" +"serverUsage" = "サーバーの使用状況" +"getInbounds" = "インバウンド情報を取得" +"depleteSoon" = "間もなく消耗" +"clientUsage" = "使用状況を取得" +"onlines" = "オンラインクライアント" +"commands" = "コマンド" +"refresh" = "🔄 更新" +"clearIPs" = "❌ IPをクリア" +"removeTGUser" = "❌ Telegramユーザーを削除" +"selectTGUser" = "👤 Telegramユーザーを選択" +"selectOneTGUser" = "👤 1人のTelegramユーザーを選択:" +"resetTraffic" = "📈 トラフィックをリセット" +"resetExpire" = "📅 有効期限を変更" +"ipLog" = "🔢 IPログ" +"ipLimit" = "🔢 IP制限" +"setTGUser" = "👤 Telegramユーザーを設定" +"toggle" = "🔘 有効/無効" +"custom" = "🔢 カスタム" +"confirmNumber" = "✅ 確認: {{ .Num }}" +"confirmNumberAdd" = "✅ 追加を確認:{{ .Num }}" +"limitTraffic" = "🚧 トラフィック制限" +"getBanLogs" = "禁止ログ" +"allClients" = "すべてのクライアント" +"addClient" = "クライアントを追加" +"submitDisable" = "無効として送信 ☑️" +"submitEnable" = "有効として送信 ✅" +"use_default" = "🏷️ デフォルトを使用" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 パスワード" +"change_email" = "⚙️📧 メールアドレス" +"change_comment" = "⚙️💬 コメント" +"ResetAllTraffics" = "すべてのトラフィックをリセット" +"SortedTrafficUsageReport" = "ソートされたトラフィック使用レポート" + +[tgbot.answers] +"successfulOperation" = "✅ 成功!" +"errorOperation" = "❗ 操作エラー。" +"getInboundsFailed" = "❌ インバウンド情報の取得に失敗しました。" +"getClientsFailed" = "❌ クライアントの取得に失敗しました。" +"canceled" = "❌ {{ .Email }}:操作がキャンセルされました。" +"clientRefreshSuccess" = "✅ {{ .Email }}:クライアントが正常に更新されました。" +"IpRefreshSuccess" = "✅ {{ .Email }}:IPが正常に更新されました。" +"TGIdRefreshSuccess" = "✅ {{ .Email }}:クライアントのTelegramユーザーが正常に更新されました。" +"resetTrafficSuccess" = "✅ {{ .Email }}:トラフィックが正常にリセットされました。" +"setTrafficLimitSuccess" = "✅ {{ .Email }}:トラフィック制限が正常に保存されました。" +"expireResetSuccess" = "✅ {{ .Email }}:有効期限の日数が正常にリセットされました。" +"resetIpSuccess" = "✅ {{ .Email }}:IP制限数が正常に保存されました:{{ .Count }}。" +"clearIpSuccess" = "✅ {{ .Email }}:IPが正常にクリアされました。" +"getIpLog" = "✅ {{ .Email }}:IPログの取得。" +"getUserInfo" = "✅ {{ .Email }}:Telegramユーザー情報の取得。" +"removedTGUserSuccess" = "✅ {{ .Email }}:Telegramユーザーが正常に削除されました。" +"enableSuccess" = "✅ {{ .Email }}:正常に有効化されました。" +"disableSuccess" = "✅ {{ .Email }}:正常に無効化されました。" +"askToAddUserId" = "設定が見つかりませんでした!\r\n管理者に問い合わせて、設定にTelegramユーザーのChatIDを使用してください。\r\n\r\nあなたのユーザーChatID:{{ .TgUserID }}" +"chooseClient" = "インバウンド {{ .Inbound }} のクライアントを選択" +"chooseInbound" = "インバウンドを選択" diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml new file mode 100644 index 0000000..62dbf0a --- /dev/null +++ b/web/translation/translate.pt_BR.toml @@ -0,0 +1,739 @@ +"username" = "Nome de Usuário" +"password" = "Senha" +"login" = "Entrar" +"confirm" = "Confirmar" +"cancel" = "Cancelar" +"close" = "Fechar" +"create" = "Criar" +"update" = "Atualizar" +"copy" = "Copiar" +"copied" = "Copiado" +"download" = "Baixar" +"remark" = "Observação" +"enable" = "Ativado" +"protocol" = "Protocolo" +"search" = "Pesquisar" +"filter" = "Filtrar" +"loading" = "Carregando..." +"second" = "Segundo" +"minute" = "Minuto" +"hour" = "Hora" +"day" = "Dia" +"check" = "Verificar" +"indefinite" = "Indeterminado" +"unlimited" = "Ilimitado" +"none" = "Nada" +"qrCode" = "Código QR" +"info" = "Mais Informações" +"edit" = "Editar" +"delete" = "Excluir" +"reset" = "Redefinir" +"noData" = "Sem dados." +"copySuccess" = "Copiado com Sucesso" +"sure" = "Certo" +"encryption" = "Criptografia" +"useIPv4ForHost" = "Usar IPv4 para o host" +"transmission" = "Transmissão" +"host" = "Servidor" +"path" = "Caminho" +"camouflage" = "Ofuscação" +"status" = "Status" +"enabled" = "Ativado" +"disabled" = "Desativado" +"depleted" = "Encerrado" +"depletingSoon" = "Esgotando" +"offline" = "Offline" +"online" = "Online" +"domainName" = "Nome de Domínio" +"monitor" = "IP de Escuta" +"certificate" = "Certificado Digital" +"fail" = "Falhou" +"comment" = "Comentário" +"success" = "Com Sucesso" +"lastOnline" = "Última vez online" +"getVersion" = "Obter Versão" +"install" = "Instalar" +"clients" = "Clientes" +"usage" = "Uso" +"twoFactorCode" = "Código" +"remained" = "Restante" +"security" = "Segurança" +"secAlertTitle" = "Alerta de Segurança" +"secAlertSsl" = "Esta conexão não é segura. Evite inserir informações confidenciais até que o TLS seja ativado para proteção de dados." +"secAlertConf" = "Algumas configurações estão vulneráveis a ataques. Recomenda-se reforçar os protocolos de segurança para evitar possíveis violações." +"secAlertSSL" = "O painel não possui uma conexão segura. Instale o certificado TLS para proteção de dados." +"secAlertPanelPort" = "A porta padrão do painel é vulnerável. Configure uma porta aleatória ou específica." +"secAlertPanelURI" = "O caminho URI padrão do painel não é seguro. Configure um caminho URI complexo." +"secAlertSubURI" = "O caminho URI padrão de inscrição não é seguro. Configure um caminho URI complexo." +"secAlertSubJsonURI" = "O caminho URI JSON de inscrição padrão não é seguro. Configure um caminho URI complexo." +"emptyDnsDesc" = "Nenhum servidor DNS adicionado." +"emptyFakeDnsDesc" = "Nenhum servidor Fake DNS adicionado." +"emptyBalancersDesc" = "Nenhum balanceador adicionado." +"emptyReverseDesc" = "Nenhum proxy reverso adicionado." +"somethingWentWrong" = "Algo deu errado" + +[menu] +"theme" = "Tema" +"dark" = "Escuro" +"ultraDark" = "Ultra Escuro" +"dashboard" = "Visão Geral" +"inbounds" = "Inbounds" +"settings" = "Panel Settings" +"xray" = "Xray Configs" +"logout" = "Sair" +"link" = "Gerenciar" + +[pages.login] +"hello" = "Olá" +"title" = "Bem-vindo" +"loginAgain" = "Sua sessão expirou, faça login novamente" + +[pages.login.toasts] +"invalidFormData" = "O formato dos dados de entrada é inválido." +"emptyUsername" = "Nome de usuário é obrigatório" +"emptyPassword" = "Senha é obrigatória" +"wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido." +"successLogin" = "Você entrou na sua conta com sucesso." + +[pages.index] +"title" = "Visão Geral" +"cpu" = "CPU" +"logicalProcessors" = "Processadores lógicos" +"frequency" = "Frequência" +"swap" = "Swap" +"storage" = "Armazenamento" +"memory" = "RAM" +"threads" = "Threads" +"xrayStatus" = "Xray" +"stopXray" = "Parar" +"restartXray" = "Reiniciar" +"xraySwitch" = "Versão" +"xraySwitchClick" = "Escolha a versão para a qual deseja alternar." +"xraySwitchClickDesk" = "Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais." +"xrayStatusUnknown" = "Desconhecido" +"xrayStatusRunning" = "Em execução" +"xrayStatusStop" = "Parado" +"xrayStatusError" = "Erro" +"xrayErrorPopoverTitle" = "Ocorreu um erro ao executar o Xray" +"operationHours" = "Tempo de Atividade" +"systemLoad" = "Carga do Sistema" +"systemLoadDesc" = "Média de carga do sistema nos últimos 1, 5 e 15 minutos" +"connectionCount" = "Estatísticas de Conexão" +"ipAddresses" = "Endereços IP" +"toggleIpVisibility" = "Alternar visibilidade do IP" +"overallSpeed" = "Velocidade geral" +"upload" = "Upload" +"download" = "Download" +"totalData" = "Dados totais" +"sent" = "Enviado" +"received" = "Recebido" +"documentation" = "Documentação" +"xraySwitchVersionDialog" = "Você realmente deseja alterar a versão do Xray?" +"xraySwitchVersionDialogDesc" = "Isso mudará a versão do Xray para #version#." +"xraySwitchVersionPopover" = "Xray atualizado com sucesso" +"geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?" +"geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#." +"geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos." +"geofilesUpdateAll" = "Atualizar tudo" +"geofileUpdatePopover" = "Geofile atualizado com sucesso" +"dontRefresh" = "Instalação em andamento, por favor não atualize a página" +"logs" = "Logs" +"config" = "Configuração" +"backup" = "Backup" +"backupTitle" = "Backup e Restauração do Banco de Dados" +"exportDatabase" = "Backup" +"exportDatabaseDesc" = "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo." +"importDatabase" = "Restaurar" +"importDatabaseDesc" = "Clique para selecionar e enviar um arquivo .db do seu dispositivo para restaurar seu banco de dados a partir de um backup." +"importDatabaseSuccess" = "O banco de dados foi importado com sucesso" +"importDatabaseError" = "Ocorreu um erro ao importar o banco de dados" +"readDatabaseError" = "Ocorreu um erro ao ler o banco de dados" +"getDatabaseError" = "Ocorreu um erro ao recuperar o banco de dados" +"getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração" + +[pages.inbounds] +"allTimeTraffic" = "Tráfego Total" +"allTimeTrafficUsage" = "Uso total de todos os tempos" +"title" = "Inbounds" +"totalDownUp" = "Total Enviado/Recebido" +"totalUsage" = "Uso Total" +"inboundCount" = "Total de Inbounds" +"operate" = "Menu" +"enable" = "Ativado" +"remark" = "Observação" +"protocol" = "Protocolo" +"port" = "Porta" +"portMap" = "Porta Mapeada" +"traffic" = "Tráfego" +"details" = "Detalhes" +"transportConfig" = "Transporte" +"expireDate" = "Duração" +"createdAt" = "Criado" +"updatedAt" = "Atualizado" +"resetTraffic" = "Redefinir Tráfego" +"addInbound" = "Adicionar Inbound" +"generalActions" = "Ações Gerais" +"autoRefresh" = "Atualização automática" +"autoRefreshInterval" = "Intervalo" +"modifyInbound" = "Modificar Inbound" +"deleteInbound" = "Excluir Inbound" +"deleteInboundContent" = "Tem certeza de que deseja excluir o inbound?" +"deleteClient" = "Excluir Cliente" +"deleteClientContent" = "Tem certeza de que deseja excluir o cliente?" +"resetTrafficContent" = "Tem certeza de que deseja redefinir o tráfego?" +"copyLink" = "Copiar URL" +"address" = "Endereço" +"network" = "Rede" +"destinationPort" = "Porta de Destino" +"targetAddress" = "Endereço de Destino" +"monitorDesc" = "Deixe em branco para ouvir todos os IPs" +"meansNoLimit" = "= Ilimitado. (unidade: GB)" +"totalFlow" = "Fluxo Total" +"leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar" +"noRecommendKeepDefault" = "Recomenda-se manter o padrão" +"certificatePath" = "Caminho" +"certificateContent" = "Conteúdo" +"publicKey" = "Chave Pública" +"privatekey" = "Chave Privada" +"clickOnQRcode" = "Clique no Código QR para Copiar" +"client" = "Cliente" +"export" = "Exportar Todos os URLs" +"clone" = "Clonar" +"cloneInbound" = "Clonar" +"cloneInboundContent" = "Todas as configurações deste inbound, exceto Porta, IP de Escuta e Clientes, serão aplicadas ao clone." +"cloneInboundOk" = "Clonar" +"resetAllTraffic" = "Redefinir Tráfego de Todos os Inbounds" +"resetAllTrafficTitle" = "Redefinir Tráfego de Todos os Inbounds" +"resetAllTrafficContent" = "Tem certeza de que deseja redefinir o tráfego de todos os inbounds?" +"resetInboundClientTraffics" = "Redefinir Tráfego dos Clientes" +"resetInboundClientTrafficTitle" = "Redefinir Tráfego dos Clientes" +"resetInboundClientTrafficContent" = "Tem certeza de que deseja redefinir o tráfego dos clientes deste inbound?" +"resetAllClientTraffics" = "Redefinir Tráfego de Todos os Clientes" +"resetAllClientTrafficTitle" = "Redefinir Tráfego de Todos os Clientes" +"resetAllClientTrafficContent" = "Tem certeza de que deseja redefinir o tráfego de todos os clientes?" +"delDepletedClients" = "Excluir Clientes Esgotados" +"delDepletedClientsTitle" = "Excluir Clientes Esgotados" +"delDepletedClientsContent" = "Tem certeza de que deseja excluir todos os clientes esgotados?" +"email" = "Email" +"emailDesc" = "Por favor, forneça um endereço de e-mail único." +"IPLimit" = "Limite de IP" +"IPLimitDesc" = "Desativa o inbound se o número ultrapassar o valor definido. (0 = desativar)" +"IPLimitlog" = "Log de IP" +"IPLimitlogDesc" = "O histórico de IPs. (para ativar o inbound após a desativação, limpe o log)" +"IPLimitlogclear" = "Limpar o Log" +"setDefaultCert" = "Definir Certificado pelo Painel" +"telegramDesc" = "Por favor, forneça o ID do Chat do Telegram. (use o comando '/id' no bot) ou (@userinfobot)" +"subscriptionDesc" = "Para encontrar seu URL de assinatura, navegue até 'Detalhes'. Além disso, você pode usar o mesmo nome para vários clientes." +"info" = "Informações" +"same" = "Igual" +"inboundData" = "Dados do Inbound" +"exportInbound" = "Exportar Inbound" +"import" = "Importar" +"importInbound" = "Importar um Inbound" + +[pages.client] +"add" = "Adicionar Cliente" +"edit" = "Editar Cliente" +"submitAdd" = "Adicionar Cliente" +"submitEdit" = "Salvar Alterações" +"clientCount" = "Número de Clientes" +"bulk" = "Adicionar Vários" +"method" = "Método" +"first" = "Primeiro" +"last" = "Último" +"prefix" = "Prefixo" +"postfix" = "Sufixo" +"delayedStart" = "Iniciar Após Primeiro Uso" +"expireDays" = "Duração" +"days" = "Dia(s)" +"renew" = "Renovação Automática" +"renewDesc" = "Renovação automática após expiração. (0 = desativado)(unidade: dia)" + +[pages.inbounds.toasts] +"obtain" = "Obter" +"updateSuccess" = "A atualização foi bem-sucedida" +"logCleanSuccess" = "O log foi limpo" +"inboundsUpdateSuccess" = "Entradas atualizadas com sucesso" +"inboundUpdateSuccess" = "Entrada atualizada com sucesso" +"inboundCreateSuccess" = "Entrada criada com sucesso" +"inboundDeleteSuccess" = "Entrada excluída com sucesso" +"inboundClientAddSuccess" = "Cliente(s) de entrada adicionado(s)" +"inboundClientDeleteSuccess" = "Cliente de entrada excluído" +"inboundClientUpdateSuccess" = "Cliente de entrada atualizado" +"delDepletedClientsSuccess" = "Todos os clientes esgotados foram excluídos" +"resetAllClientTrafficSuccess" = "Todo o tráfego do cliente foi reiniciado" +"resetAllTrafficSuccess" = "Todo o tráfego foi reiniciado" +"resetInboundClientTrafficSuccess" = "O tráfego foi reiniciado" +"trafficGetError" = "Erro ao obter tráfegos" +"getNewX25519CertError" = "Erro ao obter o certificado X25519." +"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65." + +[pages.inbounds.stream.general] +"request" = "Requisição" +"response" = "Resposta" +"name" = "Nome" +"value" = "Valor" + +[pages.inbounds.stream.tcp] +"version" = "Versão" +"method" = "Método" +"path" = "Caminho" +"status" = "Status" +"statusDescription" = "Descrição do Status" +"requestHeader" = "Cabeçalho da Requisição" +"responseHeader" = "Cabeçalho da Resposta" + +[pages.settings] +"title" = "Configurações do Painel" +"save" = "Salvar" +"infoDesc" = "Toda alteração feita aqui precisa ser salva. Reinicie o painel para aplicar as alterações." +"restartPanel" = "Reiniciar Painel" +"restartPanelDesc" = "Tem certeza de que deseja reiniciar o painel? Se não conseguir acessar o painel após reiniciar, consulte os logs do painel no servidor." +"restartPanelSuccess" = "O painel foi reiniciado com sucesso" +"actions" = "Ações" +"resetDefaultConfig" = "Redefinir para Padrão" +"panelSettings" = "Geral" +"securitySettings" = "Autenticação" +"TGBotSettings" = "Bot do Telegram" +"panelListeningIP" = "IP de Escuta" +"panelListeningIPDesc" = "O endereço IP para o painel web. (deixe em branco para escutar em todos os IPs)" +"panelListeningDomain" = "Domínio de Escuta" +"panelListeningDomainDesc" = "O nome de domínio para o painel web. (deixe em branco para escutar em todos os domínios e IPs)" +"panelPort" = "Porta de Escuta" +"panelPortDesc" = "O número da porta para o painel web. (deve ser uma porta não usada)" +"publicKeyPath" = "Caminho da Chave Pública" +"publicKeyPathDesc" = "O caminho do arquivo de chave pública para o painel web. (começa com ‘/‘)" +"privateKeyPath" = "Caminho da Chave Privada" +"privateKeyPathDesc" = "O caminho do arquivo de chave privada para o painel web. (começa com ‘/‘)" +"panelUrlPath" = "Caminho URI" +"panelUrlPathDesc" = "O caminho URI para o painel web. (começa com ‘/‘ e termina com ‘/‘)" +"pageSize" = "Tamanho da Paginação" +"pageSizeDesc" = "Definir o tamanho da página para a tabela de entradas. (0 = desativado)" +"remarkModel" = "Modelo de Observação & Caractere de Separação" +"datepicker" = "Tipo de Calendário" +"datepickerPlaceholder" = "Selecionar data" +"datepickerDescription" = "Tarefas agendadas serão executadas com base neste calendário." +"sampleRemark" = "Exemplo de Observação" +"oldUsername" = "Nome de Usuário Atual" +"currentPassword" = "Senha Atual" +"newUsername" = "Novo Nome de Usuário" +"newPassword" = "Nova Senha" +"telegramBotEnable" = "Ativar Bot do Telegram" +"telegramBotEnableDesc" = "Ativa o bot do Telegram." +"telegramToken" = "Token do Telegram" +"telegramTokenDesc" = "O token do bot do Telegram obtido de '@BotFather'." +"telegramProxy" = "Proxy SOCKS" +"telegramProxyDesc" = "Ativa o proxy SOCKS5 para conectar ao Telegram. (ajuste as configurações conforme o guia)" +"telegramAPIServer" = "API Server do Telegram" +"telegramAPIServerDesc" = "O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão." +"telegramChatId" = "ID de Chat do Administrador" +"telegramChatIdDesc" = "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui @userinfobot) ou (use o comando '/id' no bot)" +"telegramNotifyTime" = "Hora da Notificação" +"telegramNotifyTimeDesc" = "O horário de notificação do bot do Telegram configurado para relatórios periódicos. (use o formato de tempo do crontab)" +"tgNotifyBackup" = "Backup do Banco de Dados" +"tgNotifyBackupDesc" = "Enviar arquivo de backup do banco de dados junto com o relatório." +"tgNotifyLogin" = "Notificação de Login" +"tgNotifyLoginDesc" = "Receba notificações sobre o nome de usuário, endereço IP e horário sempre que alguém tentar fazer login no seu painel web." +"sessionMaxAge" = "Duração da Sessão" +"sessionMaxAgeDesc" = "A duração pela qual você pode permanecer logado. (unidade: minuto)" +"expireTimeDiff" = "Notificação de Expiração" +"expireTimeDiffDesc" = "Receba notificações sobre a data de expiração ao atingir esse limite. (unidade: dia)" +"trafficDiff" = "Notificação de Limite de Tráfego" +"trafficDiffDesc" = "Receba notificações sobre o limite de tráfego ao atingir esse limite. (unidade: GB)" +"tgNotifyCpu" = "Notificação de Carga da CPU" +"tgNotifyCpuDesc" = "Receba notificações se a carga da CPU ultrapassar esse limite. (unidade: %)" +"timeZone" = "Fuso Horário" +"timeZoneDesc" = "As tarefas agendadas serão executadas com base nesse fuso horário." +"subSettings" = "Assinatura" +"subEnable" = "Ativar Serviço de Assinatura" +"subEnableDesc" = "Ativa o serviço de assinatura." +"subTitle" = "Título da Assinatura" +"subTitleDesc" = "Título exibido no cliente VPN" +"subListen" = "IP de Escuta" +"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)" +"subPort" = "Porta de Escuta" +"subPortDesc" = "O número da porta para o serviço de assinatura. (deve ser uma porta não usada)" +"subCertPath" = "Caminho da Chave Pública" +"subCertPathDesc" = "O caminho do arquivo de chave pública para o serviço de assinatura. (começa com ‘/‘)" +"subKeyPath" = "Caminho da Chave Privada" +"subKeyPathDesc" = "O caminho do arquivo de chave privada para o serviço de assinatura. (começa com ‘/‘)" +"subPath" = "Caminho URI" +"subPathDesc" = "O caminho URI para o serviço de assinatura. (começa com ‘/‘ e termina com ‘/‘)" +"subDomain" = "Domínio de Escuta" +"subDomainDesc" = "O nome de domínio para o serviço de assinatura. (deixe em branco para escutar em todos os domínios e IPs)" +"subUpdates" = "Intervalos de Atualização" +"subUpdatesDesc" = "Os intervalos de atualização da URL de assinatura nos aplicativos de cliente. (unidade: hora)" +"subEncrypt" = "Codificar" +"subEncryptDesc" = "O conteúdo retornado pelo serviço de assinatura será codificado em Base64." +"subShowInfo" = "Mostrar Informações de Uso" +"subShowInfoDesc" = "O tráfego restante e a data serão exibidos nos aplicativos de cliente." +"subURI" = "URI de Proxy Reverso" +"subURIDesc" = "O caminho URI da URL de assinatura para uso por trás de proxies." +"externalTrafficInformEnable" = "Informações de tráfego externo" +"externalTrafficInformEnableDesc" = "Informar a API externa sobre cada atualização de tráfego." +"externalTrafficInformURI" = "URI de informação de tráfego externo" +"externalTrafficInformURIDesc" = "As atualizações de tráfego são enviadas para este URI." +"fragment" = "Fragmentação" +"fragmentDesc" = "Ativa a fragmentação para o pacote TLS hello." +"fragmentSett" = "Configurações de Fragmentação" +"noisesDesc" = "Ativar Noises." +"noisesSett" = "Configurações de Noises" +"mux" = "Mux" +"muxDesc" = "Transmitir múltiplos fluxos de dados independentes dentro de um fluxo de dados estabelecido." +"muxSett" = "Configurações de Mux" +"direct" = "Conexão Direta" +"directDesc" = "Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico." +"notifications" = "Notificações" +"certs" = "Certificados" +"externalTraffic" = "Tráfego Externo" +"dateAndTime" = "Data e Hora" +"proxyAndServer" = "Proxy e Servidor" +"intervals" = "Intervalos" +"information" = "Informação" +"language" = "Idioma" +"telegramBotLanguage" = "Idioma do Bot do Telegram" + +[pages.xray] +"title" = "Configurações Xray" +"save" = "Salvar" +"restart" = "Reiniciar Xray" +"restartSuccess" = "Xray foi reiniciado com sucesso" +"stopSuccess" = "Xray foi interrompido com sucesso" +"restartError" = "Ocorreu um erro ao reiniciar o Xray." +"stopError" = "Ocorreu um erro ao parar o Xray." +"basicTemplate" = "Básico" +"advancedTemplate" = "Avançado" +"generalConfigs" = "Geral" +"generalConfigsDesc" = "Essas opções determinam ajustes gerais." +"logConfigs" = "Log" +"logConfigsDesc" = "Os logs podem afetar a eficiência do servidor. É recomendável habilitá-los com sabedoria apenas se necessário." +"blockConfigsDesc" = "Essas opções bloqueiam tráfego com base em protocolos e sites específicos solicitados." +"basicRouting" = "Roteamento Básico" +"blockConnectionsConfigsDesc" = "Essas opções bloquearão o tráfego com base no país solicitado." +"directConnectionsConfigsDesc" = "Uma conexão direta garante que o tráfego específico não seja roteado por outro servidor." +"blockips" = "Bloquear IPs" +"blockdomains" = "Bloquear Domínios" +"directips" = "IPs Diretos" +"directdomains" = "Domínios Diretos" +"ipv4Routing" = "Roteamento IPv4" +"ipv4RoutingDesc" = "Essas opções roteam o tráfego para um destino específico via IPv4." +"warpRouting" = "Roteamento WARP" +"warpRoutingDesc" = "Essas opções roteam o tráfego para um destino específico via WARP." +"Template" = "Modelo de Configuração Avançada do Xray" +"TemplateDesc" = "O arquivo final de configuração do Xray será gerado com base neste modelo." +"FreedomStrategy" = "Estratégia do Protocolo Freedom" +"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom." +"RoutingStrategy" = "Estratégia Geral de Roteamento" +"RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações." +"Torrent" = "Bloquear Protocolo BitTorrent" +"Inbounds" = "Inbounds" +"InboundsDesc" = "Aceitar clientes específicos." +"Outbounds" = "Outbounds" +"Balancers" = "Balanceadores" +"OutboundsDesc" = "Definir o caminho de saída do tráfego." +"Routings" = "Regras de Roteamento" +"RoutingsDesc" = "A prioridade de cada regra é importante!" +"completeTemplate" = "Todos" +"logLevel" = "Nível de Log" +"logLevelDesc" = "O nível de log para erros, indicando a informação que precisa ser registrada." +"accessLog" = "Log de Acesso" +"accessLogDesc" = "O caminho do arquivo para o log de acesso. O valor especial 'none' desativa os logs de acesso." +"errorLog" = "Log de Erros" +"errorLogDesc" = "O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro." +"dnsLog" = "Log DNS" +"dnsLogDesc" = "Se ativar logs de consulta DNS" +"maskAddress" = "Mascarar Endereço" +"maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log." +"statistics" = "Estatísticas" +"statsInboundUplink" = "Estatísticas de Upload de Entrada" +"statsInboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de entrada." +"statsInboundDownlink" = "Estatísticas de Download de Entrada" +"statsInboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de entrada." +"statsOutboundUplink" = "Estatísticas de Upload de Saída" +"statsOutboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída." +"statsOutboundDownlink" = "Estatísticas de Download de Saída" +"statsOutboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída." + +[pages.xray.rules] +"first" = "Primeiro" +"last" = "Último" +"up" = "Cima" +"down" = "Baixo" +"source" = "Fonte" +"dest" = "Destino" +"inbound" = "Entrada" +"outbound" = "Saída" +"balancer" = "Balanceador" +"info" = "Info" +"add" = "Adicionar Regra" +"edit" = "Editar Regra" +"useComma" = "Itens separados por vírgula" + +[pages.xray.outbound] +"addOutbound" = "Adicionar Saída" +"addReverse" = "Adicionar Reverso" +"editOutbound" = "Editar Saída" +"editReverse" = "Editar Reverso" +"tag" = "Tag" +"tagDesc" = "Tag Única" +"address" = "Endereço" +"reverse" = "Reverso" +"domain" = "Domínio" +"type" = "Tipo" +"bridge" = "Ponte" +"portal" = "Portal" +"link" = "Link" +"intercon" = "Interconexão" +"settings" = "Configurações" +"accountInfo" = "Informações da Conta" +"outboundStatus" = "Status de Saída" +"sendThrough" = "Enviar Através de" + +[pages.xray.balancer] +"addBalancer" = "Adicionar Balanceador" +"editBalancer" = "Editar Balanceador" +"balancerStrategy" = "Estratégia" +"balancerSelectors" = "Seletores" +"tag" = "Tag" +"tagDesc" = "Tag Única" +"balancerDesc" = "Não é possível usar balancerTag e outboundTag ao mesmo tempo. Se usados simultaneamente, apenas outboundTag funcionará." + +[pages.xray.wireguard] +"secretKey" = "Chave Secreta" +"publicKey" = "Chave Pública" +"allowedIPs" = "IPs Permitidos" +"endpoint" = "Ponto Final" +"psk" = "Chave Pré-Compartilhada" +"domainStrategy" = "Estratégia de Domínio" + +[pages.xray.dns] +"enable" = "Ativar DNS" +"enableDesc" = "Ativar o servidor DNS integrado" +"tag" = "Tag de Entrada DNS" +"tagDesc" = "Esta tag estará disponível como uma tag de Entrada nas regras de roteamento." +"clientIp" = "IP do Cliente" +"clientIpDesc" = "Usado para notificar o servidor sobre a localização IP especificada durante consultas DNS" +"disableCache" = "Desativar cache" +"disableCacheDesc" = "Desativa o cache de DNS" +"disableFallback" = "Desativar Fallback" +"disableFallbackDesc" = "Desativa consultas DNS de fallback" +"disableFallbackIfMatch" = "Desativar Fallback Se Corresponder" +"disableFallbackIfMatchDesc" = "Desativa consultas DNS de fallback quando a lista de domínios correspondentes do servidor DNS é atingida" +"strategy" = "Estratégia de Consulta" +"strategyDesc" = "Estratégia geral para resolver nomes de domínio" +"add" = "Adicionar Servidor" +"edit" = "Editar Servidor" +"domains" = "Domínios" +"expectIPs" = "IPs Esperadas" +"unexpectIPs" = "IPs inesperados" +"useSystemHosts" = "Usar Hosts do sistema" +"useSystemHostsDesc" = "Usar o arquivo hosts de um sistema instalado" +"usePreset" = "Usar modelo" +"dnsPresetTitle" = "Modelos DNS" +"dnsPresetFamily" = "Familiar" + +[pages.xray.fakedns] +"add" = "Adicionar Fake DNS" +"edit" = "Editar Fake DNS" +"ipPool" = "Sub-rede do Pool de IP" +"poolSize" = "Tamanho do Pool" + +[pages.settings.security] +"admin" = "Credenciais de administrador" +"twoFactor" = "Autenticação de dois fatores" +"twoFactorEnable" = "Ativar 2FA" +"twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança." +"twoFactorModalSetTitle" = "Ativar autenticação de dois fatores" +"twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores" +"twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:" +"twoFactorModalFirstStep" = "1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo" +"twoFactorModalSecondStep" = "2. Digite o código do aplicativo" +"twoFactorModalRemoveStep" = "Digite o código do aplicativo para remover a autenticação de dois fatores." +"twoFactorModalChangeCredentialsTitle" = "Alterar credenciais" +"twoFactorModalChangeCredentialsStep" = "Insira o código do aplicativo para alterar as credenciais do administrador." +"twoFactorModalSetSuccess" = "A autenticação de dois fatores foi estabelecida com sucesso" +"twoFactorModalDeleteSuccess" = "A autenticação de dois fatores foi excluída com sucesso" +"twoFactorModalError" = "Código incorreto" + +[pages.settings.toasts] +"modifySettings" = "Os parâmetros foram alterados." +"getSettings" = "Ocorreu um erro ao recuperar os parâmetros." +"modifyUserError" = "Ocorreu um erro ao alterar as credenciais do administrador." +"modifyUser" = "Você alterou com sucesso as credenciais do administrador." +"originalUserPassIncorrect" = "O nome de usuário ou senha atual é inválido" +"userPassMustBeNotEmpty" = "O novo nome de usuário e senha não podem estar vazios" +"getOutboundTrafficError" = "Erro ao obter tráfego de saída" +"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída" + +[tgbot] +"keyboardClosed" = "❌ Teclado fechado!" +"noResult" = "❗ Nenhum resultado!" +"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!" +"wentWrong" = "❌ Algo deu errado!" +"noIpRecord" = "❗ Nenhum registro de IP!" +"noInbounds" = "❗ Nenhum inbound encontrado!" +"unlimited" = "♾ Ilimitado (Reset)" +"add" = "Adicionar" +"month" = "Mês" +"months" = "Meses" +"day" = "Dia" +"days" = "Dias" +"hours" = "Horas" +"minutes" = "Minutos" +"unknown" = "Desconhecido" +"inbounds" = "Inbounds" +"clients" = "Clientes" +"offline" = "🔴 Offline" +"online" = "🟢 Online" + +[tgbot.commands] +"unknown" = "❗ Comando desconhecido." +"pleaseChoose" = "👇 Escolha:\r\n" +"help" = "🤖 Bem-vindo a este bot! Ele foi projetado para oferecer dados específicos do painel da web e permite que você faça as modificações necessárias.\r\n\r\n" +"start" = "👋 Olá {{ .Firstname }}.\r\n" +"welcome" = "🤖 Bem-vindo ao bot de gerenciamento do {{ .Hostname }}.\r\n" +"status" = "✅ Bot está OK!" +"usage" = "❗ Por favor, forneça um texto para pesquisar!" +"getID" = "🆔 Seu ID: {{ .ID }}" +"helpAdminCommands" = "Para reiniciar o Xray Core:\r\n/restart\r\n\r\nPara pesquisar por um email de cliente:\r\n/usage [Email]\r\n\r\nPara pesquisar por inbounds (com estatísticas do cliente):\r\n/inbound [Remark]\r\n\r\nTelegram Chat ID:\r\n/id" +"helpClientCommands" = "Para pesquisar por estatísticas, use o seguinte comando:\r\n\r\n/usage [Email]\r\n\r\nTelegram Chat ID:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ Operação bem-sucedida!" +"restartFailed" = "❗ Erro na operação.\r\n\r\nErro: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core não está em execução." +"startDesc" = "Mostrar menu principal" +"helpDesc" = "Ajuda do bot" +"statusDesc" = "Verificar status do bot" +"idDesc" = "Mostrar seu ID do Telegram" + +[tgbot.messages] +"cpuThreshold" = "🔴 A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%" +"selectUserFailed" = "❌ Erro na seleção do usuário!" +"userSaved" = "✅ Usuário do Telegram salvo." +"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n" +"loginFailed" = "❗️Tentativa de login no painel falhou.\r\n" +"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n" +"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n" +"hostname" = "💻 Host: {{ .Hostname }}\r\n" +"version" = "🚀 Versão X-Panel: {{ .Version }}\r\n" +"xrayVersion" = "📡 Versão Xray: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Tempo de atividade: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Carga do sistema: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Tráfego: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" +"username" = "👤 Nome de usuário: {{ .Username }}\r\n" +"password" = "👤 Senha: {{ .Password }}\r\n" +"time" = "⏰ Hora: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Porta: {{ .Port }}\r\n" +"expire" = "📅 Data de expiração: {{ .Time }}\r\n" +"expireIn" = "📅 Expira em: {{ .Time }}\r\n" +"active" = "💡 Ativo: {{ .Enable }}\r\n" +"enabled" = "🚨 Ativado: {{ .Enable }}\r\n" +"online" = "🌐 Status da conexão: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" +"download" = "🔽 Download: ↓{{ .Download }}\r\n" +"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Usuário do Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 {{ .Type }} esgotado:\r\n" +"exhaustedCount" = "🚨 Contagem de {{ .Type }} esgotado:\r\n" +"onlinesCount" = "🌐 Clientes online: {{ .Count }}\r\n" +"disabled" = "🛑 Desativado: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Esgotar em breve: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Hora do backup: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n" +"yes" = "✅ Sim" +"no" = "❌ Não" +"received_id" = "🔑📥 ID atualizado." +"received_password" = "🔑📥 Senha atualizada." +"received_email" = "📧📥 E-mail atualizado." +"received_comment" = "💬📥 Comentário atualizado." +"id_prompt" = "🔑 ID Padrão: {{ .ClientId }}\n\nDigite seu ID." +"pass_prompt" = "🔑 Senha Padrão: {{ .ClientPassword }}\n\nDigite sua senha." +"email_prompt" = "📧 E-mail Padrão: {{ .ClientEmail }}\n\nDigite seu e-mail." +"comment_prompt" = "💬 Comentário Padrão: {{ .ClientComment }}\n\nDigite seu comentário." +"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!" +"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Senha: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!" +"cancel" = "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄" +"error_add_client" = "⚠️ Erro:\n\n {{ .error }}" +"using_default_value" = "Tudo bem, vou manter o valor padrão. 😊" +"incorrect_input" ="Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫" +"AreYouSure" = "Você tem certeza? 🤔" +"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso" +"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠️ Erro: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Processo de redefinição de tráfego concluído para todos os clientes." + +[tgbot.buttons] +"closeKeyboard" = "❌ Fechar teclado" +"cancel" = "❌ Cancelar" +"cancelReset" = "❌ Cancelar redefinição" +"cancelIpLimit" = "❌ Cancelar limite de IP" +"confirmResetTraffic" = "✅ Confirmar redefinição de tráfego?" +"confirmClearIps" = "✅ Confirmar limpar IPs?" +"confirmRemoveTGUser" = "✅ Confirmar remover usuário do Telegram?" +"confirmToggle" = "✅ Confirmar ativar/desativar usuário?" +"dbBackup" = "Obter backup do DB" +"serverUsage" = "Uso do servidor" +"getInbounds" = "Obter Inbounds" +"depleteSoon" = "Esgotar em breve" +"clientUsage" = "Obter uso" +"onlines" = "Clientes online" +"commands" = "Comandos" +"refresh" = "🔄 Atualizar" +"clearIPs" = "❌ Limpar IPs" +"removeTGUser" = "❌ Remover usuário do Telegram" +"selectTGUser" = "👤 Selecionar usuário do Telegram" +"selectOneTGUser" = "👤 Selecione um usuário do Telegram:" +"resetTraffic" = "📈 Redefinir tráfego" +"resetExpire" = "📅 Alterar data de expiração" +"ipLog" = "🔢 Log de IP" +"ipLimit" = "🔢 Limite de IP" +"setTGUser" = "👤 Definir usuário do Telegram" +"toggle" = "🔘 Ativar / Desativar" +"custom" = "🔢 Personalizado" +"confirmNumber" = "✅ Confirmar: {{ .Num }}" +"confirmNumberAdd" = "✅ Confirmar adicionar: {{ .Num }}" +"limitTraffic" = "🚧 Limite de tráfego" +"getBanLogs" = "Obter logs de banimento" +"allClients" = "Todos os clientes" +"addClient" = "Adicionar Cliente" +"submitDisable" = "Enviar como Desativado ☑️" +"submitEnable" = "Enviar como Ativado ✅" +"use_default" = "🏷️ Usar padrão" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Senha" +"change_email" = "⚙️📧 E-mail" +"change_comment" = "⚙️💬 Comentário" +"ResetAllTraffics" = "Redefinir Todo o Tráfego" +"SortedTrafficUsageReport" = "Relatório de Uso de Tráfego Ordenado" + +[tgbot.answers] +"successfulOperation" = "✅ Operação bem-sucedida!" +"errorOperation" = "❗ Erro na operação." +"getInboundsFailed" = "❌ Falha ao obter inbounds." +"getClientsFailed" = "❌ Falha ao obter clientes." +"canceled" = "❌ {{ .Email }}: Operação cancelada." +"clientRefreshSuccess" = "✅ {{ .Email }}: Cliente atualizado com sucesso." +"IpRefreshSuccess" = "✅ {{ .Email }}: IPs atualizados com sucesso." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Usuário do Telegram do cliente atualizado com sucesso." +"resetTrafficSuccess" = "✅ {{ .Email }}: Tráfego redefinido com sucesso." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Limite de tráfego salvo com sucesso." +"expireResetSuccess" = "✅ {{ .Email }}: Dias de expiração redefinidos com sucesso." +"resetIpSuccess" = "✅ {{ .Email }}: Limite de IP {{ .Count }} salvo com sucesso." +"clearIpSuccess" = "✅ {{ .Email }}: IPs limpos com sucesso." +"getIpLog" = "✅ {{ .Email }}: Obter log de IP." +"getUserInfo" = "✅ {{ .Email }}: Obter informações do usuário do Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }}: Usuário do Telegram removido com sucesso." +"enableSuccess" = "✅ {{ .Email }}: Ativado com sucesso." +"disableSuccess" = "✅ {{ .Email }}: Desativado com sucesso." +"askToAddUserId" = "Sua configuração não foi encontrada!\r\nPeça ao seu administrador para usar seu Telegram ChatID em suas configurações.\r\n\r\nSeu ChatID: {{ .TgUserID }}" +"chooseClient" = "Escolha um cliente para Inbound {{ .Inbound }}" +"chooseInbound" = "Escolha um Inbound" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml new file mode 100644 index 0000000..f4a8eb6 --- /dev/null +++ b/web/translation/translate.ru_RU.toml @@ -0,0 +1,739 @@ +"username" = "Имя пользователя" +"password" = "Пароль" +"login" = "Войти" +"confirm" = "Подтвердить" +"cancel" = "Отмена" +"close" = "Закрыть" +"create" = "Создать" +"update" = "Обновить" +"copy" = "Копировать" +"copied" = "Скопировано" +"download" = "Скачать" +"remark" = "Примечание" +"enable" = "Включить" +"protocol" = "Протокол" +"search" = "Поиск" +"filter" = "Фильтр" +"loading" = "Загрузка..." +"second" = "Секунда" +"minute" = "Минута" +"hour" = "Час" +"day" = "День" +"check" = "Проверить" +"indefinite" = "Бесконечно" +"unlimited" = "Безлимит" +"none" = "Пусто" +"qrCode" = "QR-код" +"info" = "Информация" +"edit" = "Изменить" +"delete" = "Удалить" +"reset" = "Сбросить" +"noData" = "Нет данных." +"copySuccess" = "Скопировано" +"sure" = "Да" +"encryption" = "Шифрование" +"useIPv4ForHost" = "Использовать IPv4 для хоста" +"transmission" = "Транспорт" +"host" = "Хост" +"path" = "Путь" +"camouflage" = "Маскировка" +"status" = "Статус" +"enabled" = "Включено" +"disabled" = "Отключено" +"depleted" = "Исчерпано" +"depletingSoon" = "Почти исчерпано" +"offline" = "Офлайн" +"online" = "Онлайн" +"domainName" = "Домен" +"monitor" = "Мониторинг IP" +"certificate" = "SSL сертификат" +"fail" = "Ошибка" +"comment" = "Комментарий" +"success" = "Успешно" +"lastOnline" = "Был(а) в сети" +"getVersion" = "Узнать версию" +"install" = "Установка" +"clients" = "Клиенты" +"usage" = "Использование" +"twoFactorCode" = "Код" +"remained" = "Остаток" +"security" = "Безопасность" +"secAlertTitle" = "Предупреждение системы безопасности" +"secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения" +"secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту." +"secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных." +"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт." +"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным." +"secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." +"secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес." +"emptyDnsDesc" = "Нет добавленных DNS-серверов." +"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов." +"emptyBalancersDesc" = "Нет добавленных балансировщиков." +"emptyReverseDesc" = "Нет добавленных реверс-прокси." +"somethingWentWrong" = "Что-то пошло не так" + +[menu] +"theme" = "Тема" +"dark" = "Темная" +"ultraDark" = "Очень темная" +"dashboard" = "Дашборд" +"inbounds" = "Инбаунды" +"settings" = "Настройки" +"xray" = "Настройки Xray" +"logout" = "Выход" +"link" = "Управление" + +[pages.login] +"hello" = "Привет!" +"title" = "Приветствие!" +"loginAgain" = "Сессия истекла. Войдите в систему снова" + +[pages.login.toasts] +"invalidFormData" = "Недопустимый формат данных" +"emptyUsername" = "Введите имя пользователя" +"emptyPassword" = "Введите пароль" +"wrongUsernameOrPassword" = "Неверные данные учетной записи." +"successLogin" = "Вы успешно вошли в аккаунт" + +[pages.index] +"title" = "Дашборд" +"cpu" = "ЦП" +"logicalProcessors" = "Логические процессоры" +"frequency" = "Частота" +"swap" = "Файл подкачки" +"storage" = "Диск" +"memory" = "ОЗУ" +"threads" = "Потоки" +"xrayStatus" = "Xray" +"stopXray" = "Остановить" +"restartXray" = "Перезапустить" +"xraySwitch" = "Выбор версии" +"xraySwitchClick" = "Выберите желаемую версию" +"xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки" +"xrayStatusUnknown" = "Неизвестно" +"xrayStatusRunning" = "Запущен" +"xrayStatusStop" = "Остановлен" +"xrayStatusError" = "Ошибка" +"xrayErrorPopoverTitle" = "Ошибка при запуске Xray" +"operationHours" = "Время работы системы" +"systemLoad" = "Нагрузка на систему" +"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут" +"connectionCount" = "Количество соединений" +"ipAddresses" = "IP-адреса сервера" +"toggleIpVisibility" = "Переключить видимость IP-адресов сервера" +"overallSpeed" = "Общая скорость передачи трафика" +"upload" = "Отправка" +"download" = "Загрузка" +"totalData" = "Общий объем трафика" +"sent" = "Отправлено" +"received" = "Получено" +"documentation" = "Документация" +"xraySwitchVersionDialog" = "Переключить версию Xray" +"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" +"xraySwitchVersionPopover" = "Xray успешно обновлён" +"geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?" +"geofileUpdateDialogDesc" = "Это обновит файл #filename#." +"geofilesUpdateDialogDesc" = "Это обновит все геофайлы." +"geofilesUpdateAll" = "Обновить все" +"geofileUpdatePopover" = "Геофайл успешно обновлён" +"dontRefresh" = "Установка в процессе. Не обновляйте страницу" +"logs" = "Журнал" +"config" = "Конфигурация" +"backup" = "Резервная копия" +"backupTitle" = "Резервная копия базы данных" +"exportDatabase" = "Экспорт базы данных" +"exportDatabaseDesc" = "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство." +"importDatabase" = "Импорт базы данных" +"importDatabaseDesc" = "Нажмите, чтобы выбрать и загрузить файл .db с вашего устройства для восстановления базы данных из резервной копии." +"importDatabaseSuccess" = "База данных успешно импортирована" +"importDatabaseError" = "Произошла ошибка при импорте базы данных" +"readDatabaseError" = "Произошла ошибка при чтении базы данных" +"getDatabaseError" = "Произошла ошибка при получении базы данных" +"getConfigError" = "Произошла ошибка при получении конфигурационного файла" + +[pages.inbounds] +"allTimeTraffic" = "Общий трафик" +"allTimeTrafficUsage" = "Общее использование за все время" +"title" = "Инбаунды" +"totalDownUp" = "Объем отправленного/полученного трафика" +"totalUsage" = "Всего трафика" +"inboundCount" = "Всего инбаундов" +"operate" = "Меню" +"enable" = "Включить" +"remark" = "Примечание" +"protocol" = "Протокол" +"port" = "Порт" +"portMap" = "Порт-маппинг" +"traffic" = "Трафик" +"details" = "Подробнее" +"transportConfig" = "Транспорт" +"expireDate" = "Дата окончания" +"createdAt" = "Создано" +"updatedAt" = "Обновлено" +"resetTraffic" = "Сброс трафика" +"addInbound" = "Создать инбаунд" +"generalActions" = "Общие действия" +"autoRefresh" = "Автообновление" +"autoRefreshInterval" = "Интервал" +"modifyInbound" = "Изменить инбаунд" +"deleteInbound" = "Удалить инбаунд" +"deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?" +"deleteClient" = "Удалить клиента" +"deleteClientContent" = "Вы уверены, что хотите удалить клиента?" +"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" +"copyLink" = "Копировать ссылку" +"address" = "Адрес" +"network" = "Сеть" +"destinationPort" = "Порт назначения" +"targetAddress" = "Целевой адрес" +"monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов" +"meansNoLimit" = "= Без ограничений (значение: ГБ)" +"totalFlow" = "Общий расход" +"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным" +"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию" +"certificatePath" = "Путь к сертификату" +"certificateContent" = "Содержимое сертификата" +"publicKey" = "Публичный ключ" +"privatekey" = "Приватный ключ" +"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать" +"client" = "Клиент" +"export" = "Экспорт ссылок" +"clone" = "Клонировать" +"cloneInbound" = "Клонировать" +"cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания" +"cloneInboundOk" = "Клонировано" +"resetAllTraffic" = "Сброс трафика всех инбаундов" +"resetAllTrafficTitle" = "Сброс трафика всех инбаундов" +"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?" +"resetInboundClientTraffics" = "Сброс трафика клиента" +"resetInboundClientTrafficTitle" = "Сброс трафика клиентов" +"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" +"resetAllClientTraffics" = "Сброс трафика всех клиентов" +"resetAllClientTrafficTitle" = "Сброс трафика всех клиентов" +"resetAllClientTrafficContent" = "Вы уверены, что хотите сбросить трафик всех клиентов?" +"delDepletedClients" = "Удалить отключенных клиентов" +"delDepletedClientsTitle" = "Удаление отключенных клиентов" +"delDepletedClientsContent" = "Вы уверены, что хотите удалить всех отключенных клиентов?" +"email" = "Email" +"emailDesc" = "Пожалуйста, укажите уникальный Email" +"IPLimit" = "Лимит по количеству IP" +"IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 – отключить)" +"IPLimitlog" = "Лог IP-адресов" +"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)" +"IPLimitlogclear" = "Очистить лог" +"setDefaultCert" = "Установить сертификат панели" +"telegramDesc" = "Пожалуйста, укажите Chat ID Telegram. (используйте команду '/id' в боте) или (@userinfobot)" +"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" +"info" = "Информация" +"same" = "Тот же" +"inboundData" = "Данные инбаундов" +"exportInbound" = "Экспорт инбаундов" +"import" = "Импортировать" +"importInbound" = "Импорт инбаундов" + +[pages.client] +"add" = "Создать клиента" +"edit" = "Редактировать клиента" +"submitAdd" = "Добавить" +"submitEdit" = "Сохранить" +"clientCount" = "Количество клиентов" +"bulk" = "Добавить несколько" +"method" = "Метод" +"first" = "Первый" +"last" = "Последний" +"prefix" = "Префикс" +"postfix" = "Постфикс" +"delayedStart" = "Начало использования" +"expireDays" = "Длительность" +"days" = "дней" +"renew" = "Автопродление" +"renewDesc" = "Автопродление после истечения срока действия. (0 = отключить)(единица: день)" + +[pages.inbounds.toasts] +"obtain" = "Получить" +"updateSuccess" = "Обновление прошло успешно" +"logCleanSuccess" = "Лог был очищен" +"inboundsUpdateSuccess" = "Инбаунды успешно обновлены" +"inboundUpdateSuccess" = "Инбаунд успешно обновлено" +"inboundCreateSuccess" = "Инбаунд успешно создано" +"inboundDeleteSuccess" = "Инбаунд успешно удалено" +"inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)" +"inboundClientDeleteSuccess" = "Клиент инбаунда удалён" +"inboundClientUpdateSuccess" = "Клиент инбаунда обновлён" +"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" +"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" +"resetAllTrafficSuccess" = "Весь трафик сброшен" +"resetInboundClientTrafficSuccess" = "Трафик сброшен" +"trafficGetError" = "Ошибка получения данных о трафике" +"getNewX25519CertError" = "Ошибка при получении сертификата X25519." +"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65." + +[pages.inbounds.stream.general] +"request" = "Запрос" +"response" = "Ответ" +"name" = "Имя" +"value" = "Значение" + +[pages.inbounds.stream.tcp] +"version" = "Версия" +"method" = "Метод" +"path" = "Путь" +"status" = "Статус" +"statusDescription" = "Описание статуса" +"requestHeader" = "Заголовок запроса" +"responseHeader" = "Заголовок ответа" + +[pages.settings] +"title" = "Настройки" +"save" = "Сохранить" +"infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу." +"restartPanel" = "Перезапуск панели" +"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" +"restartPanelSuccess" = "Панель успешно перезапущена" +"actions" = "Действия" +"resetDefaultConfig" = "Восстановить настройки по умолчанию" +"panelSettings" = "Панель" +"securitySettings" = "Учетная запись" +"TGBotSettings" = "Telegram" +"panelListeningIP" = "IP-адрес для управления панелью" +"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" +"panelListeningDomain" = "Домен панели" +"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов" +"panelPort" = "Порт панели" +"panelPortDesc" = "Порт, на котором работает панель" +"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" +"publicKeyPathDesc" = "Введите полный путь, начинающийся с '/'" +"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели" +"privateKeyPathDesc" = "Введите полный путь, начинающийся с '/'" +"panelUrlPath" = "Корневой путь URL адреса панели" +"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" +"pageSize" = "Размер нумерации страниц" +"pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить" +"remarkModel" = "Модель примечания и символ разделения" +"datepicker" = "Выбор даты" +"datepickerPlaceholder" = "Выберите дату" +"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время" +"sampleRemark" = "Пример примечания" +"oldUsername" = "Текущий логин" +"currentPassword" = "Текущий пароль" +"newUsername" = "Новый логин" +"newPassword" = "Новый пароль" +"telegramBotEnable" = "Включить Telegram бота" +"telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота" +"telegramToken" = "Токен Telegram бота" +"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" +"telegramProxy" = "Прокси Socks5" +"telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству." +"telegramAPIServer" = "API-сервер Telegram" +"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию." +"telegramChatId" = "User ID администратора бота" +"telegramChatIdDesc" = "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте @userinfobot или команду '/id' в боте." +"telegramNotifyTime" = "Частота уведомлений для администраторов от бота" +"telegramNotifyTimeDesc" = "Укажите интервал уведомлений в формате Crontab" +"tgNotifyBackup" = "Резервное копирование базы данных" +"tgNotifyBackupDesc" = "Отправлять уведомление с файлом резервной копии базы данных" +"tgNotifyLogin" = "Уведомление о входе" +"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель." +"sessionMaxAge" = "Продолжительность сессии" +"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)" +"expireTimeDiff" = "Задержка уведомления об истечении сессии" +"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)" +"trafficDiff" = "Порог трафика для уведомления" +"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)" +"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления" +"tgNotifyCpuDesc" = "Уведомление администраторов в Telegram, если нагрузка на ЦП превышает этот порог (значение: %)" +"timeZone" = "Часовой пояс" +"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе" +"subSettings" = "Подписка" +"subEnable" = "Включить подписку" +"subEnableDesc" = "Функция подписки с отдельной конфигурацией" +"subTitle" = "Заголовок подписки" +"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте" +"subListen" = "Прослушивание IP" +"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса" +"subPort" = "Порт подписки" +"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере" +"subCertPath" = "Путь к файлу публичного ключа сертификата подписки" +"subCertPathDesc" = "Введите полный путь, начинающийся с '/'" +"subKeyPath" = "Путь к файлу приватного ключа сертификата подписки" +"subKeyPathDesc" = "Введите полный путь, начинающийся с '/'" +"subPath" = "Корневой путь URL-адреса подписки" +"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'" +"subDomain" = "Домен прослушивания" +"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса" +"subUpdates" = "Интервалы обновления подписки" +"subUpdatesDesc" = "Интервал между обновлениями в клиентском приложении (в часах)" +"subEncrypt" = "Шифровать конфиги" +"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке" +"subShowInfo" = "Показать информацию об использовании" +"subShowInfoDesc" = "Отображать остаток трафика и дату окончания после имени конфигурации" +"subURI" = "URI обратного прокси" +"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами" +"externalTrafficInformEnable" = "Информация о внешнем трафике" +"externalTrafficInformEnableDesc" = "Информировать внешний API о каждом обновлении трафика" +"externalTrafficInformURI" = "URI информации о внешнем трафике" +"externalTrafficInformURIDesc" = "Обновления трафика отправляются на этот URI" +"fragment" = "Фрагментация" +"fragmentDesc" = "Включить фрагментацию TLS-хэндшейка" +"fragmentSett" = "Настройки фрагментации" +"noisesDesc" = "Включить Noises." +"noisesSett" = "Настройки Noises" +"mux" = "Mux" +"muxDesc" = "Передача нескольких независимых потоков данных в одном соединении." +"muxSett" = "Настройки Mux" +"direct" = "Прямое подключение" +"directDesc" = "Устанавливает прямые соединения с доменами или IP-адресами определённой страны." +"notifications" = "Уведомления" +"certs" = "Сертификаты" +"externalTraffic" = "Внешний трафик" +"dateAndTime" = "Дата и время" +"proxyAndServer" = "Прокси и сервер" +"intervals" = "Интервалы" +"information" = "Информация" +"language" = "Язык интерфейса" +"telegramBotLanguage" = "Язык Telegram-бота" + +[pages.xray] +"title" = "Настройки Xray" +"save" = "Сохранить" +"restart" = "Перезапуск Xray" +"restartSuccess" = "Xray успешно перезапущен" +"stopSuccess" = "Xray успешно остановлен" +"restartError" = "Произошла ошибка при перезапуске Xray." +"stopError" = "Произошла ошибка при остановке Xray." +"basicTemplate" = "Основное" +"advancedTemplate" = "Расширенный шаблон" +"generalConfigs" = "Основные настройки" +"generalConfigsDesc" = "Эти параметры описывают общие настройки" +"logConfigs" = "Логи" +"logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!" +"blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам" +"basicRouting" = "Базовые соединения" +"blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от страны назначения." +"directConnectionsConfigsDesc" = "Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер." +"blockips" = "Заблокированные IP-адреса" +"blockdomains" = "Заблокированные домены" +"directips" = "Прямые IP-адреса" +"directdomains" = "Прямые домены" +"ipv4Routing" = "Правила IPv4" +"ipv4RoutingDesc" = "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4" +"warpRouting" = "Правила WARP" +"warpRoutingDesc" = " Эти опции будут направлять трафик в зависимости от конкретного пункта назначения через WARP." +"Template" = "Шаблон конфигурации Xray" +"TemplateDesc" = "На основе шаблона создаётся конфигурационный файл Xray." +"FreedomStrategy" = "Настройка стратегии протокола Freedom" +"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom" +"RoutingStrategy" = "Настройка маршрутизации доменов" +"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" +"Torrent" = "Заблокировать BitTorrent" +"Inbounds" = "Инбаунды" +"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" +"Outbounds" = "Аутбаунды" +"Balancers" = "Балансировщик" +"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера" +"Routings" = "Маршрутизация" +"RoutingsDesc" = "Важен приоритет каждого правила!" +"completeTemplate" = "Все" +"logLevel" = "Уровень логов" +"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать." +"accessLog" = "Логи доступа" +"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключает логи доступа." +"errorLog" = "Логи ошибок" +"errorLogDesc" = "Путь к файлу логов ошибок. Специальное значение «none» отключает логи ошибок." +"dnsLog" = "Логи DNS" +"dnsLogDesc" = "Включить логи запросов DNS" +"maskAddress" = "Маскировка адреса" +"maskAddressDesc" = "При активации реальный IP-адрес заменяется на маскировочный в логах." +"statistics" = "Статистика" +"statsInboundUplink" = "Статистика входящего аплинка" +"statsInboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех входящих прокси." +"statsInboundDownlink" = "Статистика входящего даунлинка" +"statsInboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех входящих прокси." +"statsOutboundUplink" = "Статистика исходящего аплинка" +"statsOutboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех исходящих прокси." +"statsOutboundDownlink" = "Статистика исходящего даунлинка" +"statsOutboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех исходящих прокси." + +[pages.xray.rules] +"first" = "Первый" +"last" = "Последний" +"up" = "Поднять вверх" +"down" = "Опустить вниз" +"source" = "Источник" +"dest" = "Пункт назначения" +"inbound" = "Инбаунд" +"outbound" = "Аутбаунд" +"balancer" = "Балансировщик" +"info" = "Информация" +"add" = "Создать правило" +"edit" = "Редактировать правило" +"useComma" = "Элементы, разделённые запятыми" + +[pages.xray.outbound] +"addOutbound" = "Создать аутбаунд" +"addReverse" = "Создать реверс-прокси" +"editOutbound" = "Изменить аутбаунд" +"editReverse" = "Редактировать реверс-прокси" +"tag" = "Тег" +"tagDesc" = "Уникальный тег" +"address" = "Адрес" +"reverse" = "Реверс-прокси" +"domain" = "Домен" +"type" = "Тип" +"bridge" = "Мост" +"portal" = "Портал" +"link" = "Ссылка" +"intercon" = "Соединение" +"settings" = "Настройки" +"accountInfo" = "Информация об учетной записи" +"outboundStatus" = "Статус аутбаунда" +"sendThrough" = "Отправить через" + +[pages.xray.balancer] +"addBalancer" = "Создать балансировщик" +"editBalancer" = "Редактировать балансировщик" +"balancerStrategy" = "Стратегия" +"balancerSelectors" = "Селекторы" +"tag" = "Тег" +"tagDesc" = "Уникальный тег" +"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag." + +[pages.xray.wireguard] +"secretKey" = "Секретный ключ" +"publicKey" = "Публичный ключ" +"allowedIPs" = "Разрешенные IP-адреса" +"endpoint" = "Конечная точка" +"psk" = "Общий ключ" +"domainStrategy" = "Стратегия домена" + +[pages.xray.dns] +"enable" = "Включить DNS" +"enableDesc" = "Включить встроенный DNS-сервер" +"tag" = "Название тега DNS" +"tagDesc" = "Этот тег будет доступен как входящий тег в правилах маршрутизации." +"clientIp" = "IP клиента" +"clientIpDesc" = "Используется для уведомления сервера о указанном местоположении IP во время DNS-запросов" +"disableCache" = "Отключить кэш" +"disableCacheDesc" = "Отключает кэширование DNS" +"disableFallback" = "Отключить резервный DNS" +"disableFallbackDesc" = "Отключает резервные DNS-запросы" +"disableFallbackIfMatch" = "Отключить резервный DNS при совпадении" +"disableFallbackIfMatchDesc" = "Отключает резервные DNS-запросы при совпадении списка доменов DNS-сервера" +"strategy" = "Стратегия запроса" +"strategyDesc" = "Общая стратегия разрешения доменных имен" +"add" = "Создать DNS" +"edit" = "Редактировать DNS" +"domains" = "Домены" +"expectIPs" = "Ожидаемые IP" +"unexpectIPs" = "Неожидаемые IP" +"useSystemHosts" = "Использовать системные Hosts" +"useSystemHostsDesc" = "Использовать файл hosts из установленной системы" +"usePreset" = "Использовать шаблон" +"dnsPresetTitle" = "Шаблоны DNS" +"dnsPresetFamily" = "Семейный" + +[pages.xray.fakedns] +"add" = "Создать Fake DNS" +"edit" = "Редактировать Fake DNS" +"ipPool" = "Подсеть пула IP" +"poolSize" = "Размер пула" + +[pages.settings.security] +"admin" = "Учетные данные администратора" +"twoFactor" = "Двухфакторная аутентификация" +"twoFactorEnable" = "Включить 2FA" +"twoFactorEnableDesc" = "Добавляет дополнительный уровень аутентификации для повышения безопасности." +"twoFactorModalSetTitle" = "Включить двухфакторную аутентификацию" +"twoFactorModalDeleteTitle" = "Отключить двухфакторную аутентификацию" +"twoFactorModalSteps" = "Для настройки двухфакторной аутентификации выполните несколько шагов:" +"twoFactorModalFirstStep" = "1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение" +"twoFactorModalSecondStep" = "2. Введите код из приложения" +"twoFactorModalRemoveStep" = "Введите код из приложения, чтобы отключить двухфакторную аутентификацию." +"twoFactorModalChangeCredentialsTitle" = "Изменить учетные данные" +"twoFactorModalChangeCredentialsStep" = "Введите код из приложения, чтобы изменить учетные данные администратора." +"twoFactorModalSetSuccess" = "Двухфакторная аутентификация была успешно установлена" +"twoFactorModalDeleteSuccess" = "Двухфакторная аутентификация была успешно удалена" +"twoFactorModalError" = "Неверный код" + +[pages.settings.toasts] +"modifySettings" = "Настройки изменены" +"getSettings" = "Произошла ошибка при получении параметров." +"modifyUserError" = "Произошла ошибка при изменении учетных данных администратора." +"modifyUser" = "Вы успешно изменили учетные данные администратора." +"originalUserPassIncorrect" = "Неверное имя пользователя или пароль" +"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" +"getOutboundTrafficError" = "Ошибка получения трафика аутбаунда" +"resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда" + +[tgbot] +"keyboardClosed" = "❌ Клавиатура закрыта." +"noResult" = "❗ Нет результатов." +"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." +"wentWrong" = "❌ Что-то пошло не так..." +"noIpRecord" = "❗ Нет записей об IP-адресе." +"noInbounds" = "❗ У вас не настроено ни одного инбаунда." +"unlimited" = "♾ Безлимит" +"add" = "Добавить" +"month" = "Месяц" +"months" = "Месяцев" +"day" = "День" +"days" = "Дней" +"hours" = "Часов" +"minutes" = "Минуты" +"unknown" = "Неизвестно" +"inbounds" = "Инбаунды" +"clients" = "Клиенты" +"offline" = "🔴 Офлайн" +"online" = "🟢 Онлайн" + +[tgbot.commands] +"unknown" = "❗ Неизвестная команда" +"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n" +"help" = "🤖 Добро пожаловать! Этот бот предназначен для предоставления вам данных с сервера и позволяет вносить изменения на него.\r\n\r\n" +"start" = "👋 Привет, {{ .Firstname }}.\r\n" +"welcome" = "🤖 Добро пожаловать в бота управления {{ .Hostname }}!\r\n" +"status" = "✅ Бот функционирует нормально." +"usage" = "❗ Пожалуйста, укажите email для поиска." +"getID" = "🆔 Ваш User ID: {{ .ID }}" +"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n/restart\r\n\r\n🔎 Для поиска клиента по email:\r\n/usage [Email]\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n/inbound [имя подключения]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" +"helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n/usage [Email]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ Ядро Xray успешно перезапущено." +"restartFailed" = "❗ Ошибка при перезапуске Xray-core.\r\n\r\nОшибка: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core не запущен." +"startDesc" = "Показать главное меню" +"helpDesc" = "Справка по боту" +"statusDesc" = "Проверить статус бота" +"idDesc" = "Показать ваш Telegram ID" + +[tgbot.messages] +"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%" +"selectUserFailed" = "❌ Ошибка при выборе пользователя." +"userSaved" = "✅ Пользователь Telegram сохранен." +"loginSuccess" = "✅ Успешный вход в панель.\r\n" +"loginFailed" = "❗️ Ошибка входа в панель.\r\n" +"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n" +"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n" +"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n" +"version" = "🚀 Версия X-Panel: {{ .Version }}\r\n" +"xrayVersion" = "📡 Версия Xray: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 Диск сервера: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n" +"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n" +"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Состояние Xray: {{ .State }}\r\n" +"username" = "👤 Имя пользователя: {{ .Username }}\r\n" +"password" = "👤 Пароль: {{ .Password }}\r\n" +"time" = "⏰ Время: {{ .Time }}\r\n" +"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n" +"port" = "🔌 Порт: {{ .Port }}\r\n" +"expire" = "📅 Дата окончания: {{ .Time }}\r\n" +"expireIn" = "📅 Окончание через: {{ .Time }}\r\n" +"active" = "💡 Активен: {{ .Enable }}\r\n" +"enabled" = "🚨 Активен: {{ .Enable }}\r\n" +"online" = "🌐 Статус соединения: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n" +"download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n" +"total" = "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n" +"TGUser" = "👤 Telegram User ID: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n" +"onlinesCount" = "🌐 Клиентов онлайн: {{ .Count }}\r\n" +"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n" +"yes" = "✅ Да" +"no" = "❌ Нет" +"received_id" = "🔑📥 ID обновлён." +"received_password" = "🔑📥 Пароль обновлён." +"received_email" = "📧📥 Email обновлен." +"received_comment" = "💬📥 Комментарий обновлён." +"id_prompt" = "🔑 Стандартный ID: {{ .ClientId }}\n\nВведите ваш ID." +"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." +"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." +"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." +"inbound_client_data_id" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" +"inbound_client_data_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" +"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" +"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" +"using_default_value" = "Используется значение по умолчанию👌" +"incorrect_input" ="Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫" +"AreYouSure" = "Вы уверены? 🤔" +"SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно" +"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠️ Ошибка: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Сброс трафика завершён для всех клиентов." + +[tgbot.buttons] +"closeKeyboard" = "❌ Закрыть клавиатуру" +"cancel" = "❌ Отмена" +"cancelReset" = "❌ Отменить сброс" +"cancelIpLimit" = "❌ Отменить лимит IP" +"confirmResetTraffic" = "✅ Подтвердить сброс трафика?" +"confirmClearIps" = "✅ Подтвердить очистку IP?" +"confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?" +"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" +"dbBackup" = "📂 Бэкап БД" +"serverUsage" = "💻 Состояние сервера" +"getInbounds" = "🔌 Инбаунды" +"depleteSoon" = "⚠️ Скоро конец" +"clientUsage" = "Статистика клиента" +"onlines" = "🟢 Онлайн" +"commands" = "🖱️ Команды" +"refresh" = "🔄 Обновить" +"clearIPs" = "❌ Очистить IP" +"removeTGUser" = "❌ Удалить пользователя Telegram" +"selectTGUser" = "👤 Выбрать пользователя Telegram" +"selectOneTGUser" = "👤 Выберите пользователя Telegram:" +"resetTraffic" = "📈 Сбросить трафик" +"resetExpire" = "📅 Изменить дату окончания" +"ipLog" = "🔢 Лог IP" +"ipLimit" = "🔢 Лимит IP" +"setTGUser" = "👤 Установить пользователя Telegram" +"toggle" = "🔘 Вкл./Выкл." +"custom" = "🔢 Свой" +"confirmNumber" = "✅ Подтвердить: {{ .Num }}" +"confirmNumberAdd" = "✅ Подтвердить добавление: {{ .Num }}" +"limitTraffic" = "🚧 Лимит трафика" +"getBanLogs" = "📄 Лог банов" +"allClients" = "👥 Все клиенты" +"addClient" = "➕ Новый клиент" +"submitDisable" = "Добавить отключенным ☑️" +"submitEnable" = "Добавить включенныи ✅" +"use_default" = "🏷️ Использовать по умолчанию" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Пароль" +"change_email" = "⚙️📧 Email" +"change_comment" = "⚙️💬 Комментарий" +"ResetAllTraffics" = "Сбросить весь трафик" +"SortedTrafficUsageReport" = "Отсортированный отчет об использовании трафика" + +[tgbot.answers] +"successfulOperation" = "✅ Успешно!" +"errorOperation" = "❗ Ошибка в операции." +"getInboundsFailed" = "❌ Не удалось получить инбаунды." +"getClientsFailed" = "❌ Не удалось получить клиентов." +"canceled" = "❌ {{ .Email }}: Операция отменена." +"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." +"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен." +"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Лимит трафика успешно установлен." +"expireResetSuccess" = "✅ {{ .Email }}: Срок действия успешно сброшен." +"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен." +"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены." +"getIpLog" = "✅ {{ .Email }}: Получен лог IP." +"getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален." +"enableSuccess" = "✅ {{ .Email }}: Включено успешно." +"disableSuccess" = "✅ {{ .Email }}: Отключено успешно." +"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: {{ .TgUserID }}" +"chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}" +"chooseInbound" = "Выберите инбаунд" diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml new file mode 100644 index 0000000..d942426 --- /dev/null +++ b/web/translation/translate.tr_TR.toml @@ -0,0 +1,739 @@ +"username" = "Kullanıcı Adı" +"password" = "Şifre" +"login" = "Giriş Yap" +"confirm" = "Onayla" +"cancel" = "İptal" +"close" = "Kapat" +"create" = "Oluştur" +"update" = "Güncelle" +"copy" = "Kopyala" +"copied" = "Kopyalandı" +"download" = "İndir" +"remark" = "Açıklama" +"enable" = "Etkin" +"protocol" = "Protokol" +"search" = "Ara" +"filter" = "Filtrele" +"loading" = "Yükleniyor..." +"second" = "Saniye" +"minute" = "Dakika" +"hour" = "Saat" +"day" = "Gün" +"check" = "Kontrol Et" +"indefinite" = "Belirsiz" +"unlimited" = "Sınırsız" +"none" = "Hiçbiri" +"qrCode" = "QR Kod" +"info" = "Daha Fazla Bilgi" +"edit" = "Düzenle" +"delete" = "Sil" +"reset" = "Sıfırla" +"noData" = "Veri yok." +"copySuccess" = "Başarıyla Kopyalandı" +"sure" = "Emin misiniz" +"encryption" = "Şifreleme" +"useIPv4ForHost" = "Ana bilgisayar için IPv4 kullan" +"transmission" = "İletim" +"host" = "Sunucu" +"path" = "Yol" +"camouflage" = "Kandırma" +"status" = "Durum" +"enabled" = "Etkin" +"disabled" = "Devre Dışı" +"depleted" = "Bitti" +"depletingSoon" = "Bitmek Üzere" +"offline" = "Çevrimdışı" +"online" = "Çevrimiçi" +"domainName" = "Alan Adı" +"monitor" = "Dinleme IP" +"certificate" = "Dijital Sertifika" +"fail" = "Başarısız" +"comment" = "Yorum" +"success" = "Başarılı" +"lastOnline" = "Son çevrimiçi" +"getVersion" = "Sürümü Al" +"install" = "Yükle" +"clients" = "Müşteriler" +"usage" = "Kullanım" +"twoFactorCode" = "Kod" +"remained" = "Kalan" +"security" = "Güvenlik" +"secAlertTitle" = "Güvenlik Uyarısı" +"secAlertSsl" = "Bu bağlantı güvenli değil. Verilerin korunması için TLS etkinleştirilene kadar hassas bilgiler girmekten kaçının." +"secAlertConf" = "Bazı ayarlar saldırılara açıktır. Olası ihlalleri önlemek için güvenlik protokollerini güçlendirmeniz önerilir." +"secAlertSSL" = "Panelde güvenli bağlantı yok. Verilerin korunması için TLS sertifikası yükleyin." +"secAlertPanelPort" = "Panel varsayılan portu savunmasız. Rastgele veya belirli bir port yapılandırın." +"secAlertPanelURI" = "Panel varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın." +"secAlertSubURI" = "Abonelik varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın." +"secAlertSubJsonURI" = "Abonelik JSON varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın." +"emptyDnsDesc" = "Eklenmiş DNS sunucusu yok." +"emptyFakeDnsDesc" = "Eklenmiş Fake DNS sunucusu yok." +"emptyBalancersDesc" = "Eklenmiş dengeleyici yok." +"emptyReverseDesc" = "Eklenmiş ters proxy yok." +"somethingWentWrong" = "Bir şeyler yanlış gitti" + +[menu] +"theme" = "Tema" +"dark" = "Koyu" +"ultraDark" = "Ultra Koyu" +"dashboard" = "Genel Bakış" +"inbounds" = "Gelenler" +"settings" = "Panel Ayarları" +"xray" = "Xray Yapılandırmaları" +"logout" = "Çıkış Yap" +"link" = "Yönet" + +[pages.login] +"hello" = "Merhaba" +"title" = "Hoş Geldiniz" +"loginAgain" = "Oturum süreniz doldu, lütfen tekrar giriş yapın" + +[pages.login.toasts] +"invalidFormData" = "Girdi verisi formatı geçersiz." +"emptyUsername" = "Kullanıcı adı gerekli" +"emptyPassword" = "Şifre gerekli" +"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu." +"successLogin" = "Hesabınıza başarıyla giriş yaptınız." + +[pages.index] +"title" = "Genel Bakış" +"cpu" = "İşlemci" +"logicalProcessors" = "Mantıksal işlemciler" +"frequency" = "Frekans" +"swap" = "Takas" +"storage" = "Depolama" +"memory" = "RAM" +"threads" = "İş parçacıkları" +"xrayStatus" = "Xray" +"stopXray" = "Durdur" +"restartXray" = "Yeniden Başlat" +"xraySwitch" = "Sürüm" +"xraySwitchClick" = "Geçiş yapmak istediğiniz sürümü seçin." +"xraySwitchClickDesk" = "Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir." +"xrayStatusUnknown" = "Bilinmiyor" +"xrayStatusRunning" = "Çalışıyor" +"xrayStatusStop" = "Durduruldu" +"xrayStatusError" = "Hata" +"xrayErrorPopoverTitle" = "Xray çalıştırılırken bir hata oluştu" +"operationHours" = "Çalışma Süresi" +"systemLoad" = "Sistem Yükü" +"systemLoadDesc" = "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması" +"connectionCount" = "Bağlantı İstatistikleri" +"ipAddresses" = "IP adresleri" +"toggleIpVisibility" = "IP görünürlüğünü değiştir" +"overallSpeed" = "Genel hız" +"upload" = "Yükleme" +"download" = "İndirme" +"totalData" = "Toplam veri" +"sent" = "Gönderilen" +"received" = "Alınan" +"documentation" = "Dokümantasyon" +"xraySwitchVersionDialog" = "Xray sürümünü gerçekten değiştirmek istiyor musunuz?" +"xraySwitchVersionDialogDesc" = "Bu işlem Xray sürümünü #version# olarak değiştirecektir." +"xraySwitchVersionPopover" = "Xray başarıyla güncellendi" +"geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?" +"geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir." +"geofilesUpdateDialogDesc" = "Bu, tüm dosyaları güncelleyecektir." +"geofilesUpdateAll" = "Tümünü güncelle" +"geofileUpdatePopover" = "Geofile başarıyla güncellendi" +"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" +"logs" = "Günlükler" +"config" = "Yapılandırma" +"backup" = "Yedek" +"backupTitle" = "Veritabanı Yedekleme & Geri Yükleme" +"exportDatabase" = "Yedekle" +"exportDatabaseDesc" = "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın." +"importDatabase" = "Geri Yükle" +"importDatabaseDesc" = "Cihazınızdan bir .db dosyası seçip yükleyerek veritabanınızı yedekten geri yüklemek için tıklayın." +"importDatabaseSuccess" = "Veritabanı başarıyla içe aktarıldı" +"importDatabaseError" = "Veritabanı içe aktarılırken bir hata oluştu" +"readDatabaseError" = "Veritabanı okunurken bir hata oluştu" +"getDatabaseError" = "Veritabanı alınırken bir hata oluştu" +"getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu" + +[pages.inbounds] +"allTimeTraffic" = "Toplam Trafik" +"allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı" +"title" = "Gelenler" +"totalDownUp" = "Toplam Gönderilen/Alınan" +"totalUsage" = "Toplam Kullanım" +"inboundCount" = "Toplam Gelen" +"operate" = "Menü" +"enable" = "Etkin" +"remark" = "Açıklama" +"protocol" = "Protokol" +"port" = "Port" +"portMap" = "Port Atama" +"traffic" = "Trafik" +"details" = "Detaylar" +"transportConfig" = "Taşıma" +"expireDate" = "Süre" +"createdAt" = "Oluşturuldu" +"updatedAt" = "Güncellendi" +"resetTraffic" = "Trafiği Sıfırla" +"addInbound" = "Gelen Ekle" +"generalActions" = "Genel Eylemler" +"autoRefresh" = "Otomatik yenileme" +"autoRefreshInterval" = "Aralık" +"modifyInbound" = "Geleni Düzenle" +"deleteInbound" = "Geleni Sil" +"deleteInboundContent" = "Geleni silmek istediğinizden emin misiniz?" +"deleteClient" = "Müşteriyi Sil" +"deleteClientContent" = "Müşteriyi silmek istediğinizden emin misiniz?" +"resetTrafficContent" = "Trafiği sıfırlamak istediğinizden emin misiniz?" +"copyLink" = "URL'yi Kopyala" +"address" = "Adres" +"network" = "Ağ" +"destinationPort" = "Hedef Port" +"targetAddress" = "Hedef Adres" +"monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın" +"meansNoLimit" = "= Sınırsız. (birim: GB)" +"totalFlow" = "Toplam Akış" +"leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın" +"noRecommendKeepDefault" = "Varsayılanı korumanız önerilir" +"certificatePath" = "Dosya Yolu" +"certificateContent" = "Dosya İçeriği" +"publicKey" = "Genel Anahtar" +"privatekey" = "Özel Anahtar" +"clickOnQRcode" = "Kopyalamak için QR Kodu Tıklayın" +"client" = "Müşteri" +"export" = "Tüm URL'leri Dışa Aktar" +"clone" = "Klonla" +"cloneInbound" = "Klonla" +"cloneInboundContent" = "Bu gelenin tüm ayarları, Port, Dinleme IP ve Müşteriler hariç, klona uygulanacaktır." +"cloneInboundOk" = "Klonla" +"resetAllTraffic" = "Tüm Gelen Trafiğini Sıfırla" +"resetAllTrafficTitle" = "Tüm Gelen Trafiğini Sıfırla" +"resetAllTrafficContent" = "Tüm gelenlerin trafiğini sıfırlamak istediğinizden emin misiniz?" +"resetInboundClientTraffics" = "Müşteri Trafiklerini Sıfırla" +"resetInboundClientTrafficTitle" = "Müşteri Trafiklerini Sıfırla" +"resetInboundClientTrafficContent" = "Bu gelenin müşterilerinin trafiğini sıfırlamak istediğinizden emin misiniz?" +"resetAllClientTraffics" = "Tüm Müşteri Trafiklerini Sıfırla" +"resetAllClientTrafficTitle" = "Tüm Müşteri Trafiklerini Sıfırla" +"resetAllClientTrafficContent" = "Tüm müşterilerin trafiğini sıfırlamak istediğinizden emin misiniz?" +"delDepletedClients" = "Bitmiş Müşterileri Sil" +"delDepletedClientsTitle" = "Bitmiş Müşterileri Sil" +"delDepletedClientsContent" = "Tüm bitmiş müşterileri silmek istediğinizden emin misiniz?" +"email" = "E-posta" +"emailDesc" = "Lütfen benzersiz bir e-posta adresi sağlayın." +"IPLimit" = "IP Limiti" +"IPLimitDesc" = "Sayının aşılması durumunda gelen devre dışı bırakılır. (0 = devre dışı)" +"IPLimitlog" = "IP Günlüğü" +"IPLimitlogDesc" = "IP geçmiş günlüğü. (devre dışı bırakıldıktan sonra gelini etkinleştirmek için günlüğü temizleyin)" +"IPLimitlogclear" = "Günlüğü Temizle" +"setDefaultCert" = "Panelden Sertifikayı Ayarla" +"telegramDesc" = "Lütfen Telegram Sohbet Kimliği sağlayın. (botta '/id' komutunu kullanın) veya (@userinfobot)" +"subscriptionDesc" = "Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla müşteri için kullanabilirsiniz." +"info" = "Bilgi" +"same" = "Aynı" +"inboundData" = "Gelenin Verileri" +"exportInbound" = "Geleni Dışa Aktar" +"import" = "İçe Aktar" +"importInbound" = "Bir Gelen İçe Aktar" + +[pages.client] +"add" = "Müşteri Ekle" +"edit" = "Müşteriyi Düzenle" +"submitAdd" = "Müşteri Ekle" +"submitEdit" = "Değişiklikleri Kaydet" +"clientCount" = "Müşteri Sayısı" +"bulk" = "Toplu Ekle" +"method" = "Yöntem" +"first" = "İlk" +"last" = "Son" +"prefix" = "Önek" +"postfix" = "Sonek" +"delayedStart" = "İlk Kullanımdan Sonra Başlat" +"expireDays" = "Süre" +"days" = "Gün" +"renew" = "Otomatik Yenile" +"renewDesc" = "Süresi dolduktan sonra otomatik yenileme. (0 = devre dışı)(birim: gün)" + +[pages.inbounds.toasts] +"obtain" = "Elde Et" +"updateSuccess" = "Güncelleme başarılı oldu" +"logCleanSuccess" = "Günlük temizlendi" +"inboundsUpdateSuccess" = "Gelen bağlantılar başarıyla güncellendi" +"inboundUpdateSuccess" = "Gelen bağlantı başarıyla güncellendi" +"inboundCreateSuccess" = "Gelen bağlantı başarıyla oluşturuldu" +"inboundDeleteSuccess" = "Gelen bağlantı başarıyla silindi" +"inboundClientAddSuccess" = "Gelen bağlantı istemci(leri) eklendi" +"inboundClientDeleteSuccess" = "Gelen bağlantı istemcisi silindi" +"inboundClientUpdateSuccess" = "Gelen bağlantı istemcisi güncellendi" +"delDepletedClientsSuccess" = "Tüm tükenmiş istemciler silindi" +"resetAllClientTrafficSuccess" = "İstemcinin tüm trafiği sıfırlandı" +"resetAllTrafficSuccess" = "Tüm trafik sıfırlandı" +"resetInboundClientTrafficSuccess" = "Trafik sıfırlandı" +"trafficGetError" = "Trafik bilgisi alınırken hata oluştu" +"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu." +"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu." + +[pages.inbounds.stream.general] +"request" = "İstek" +"response" = "Yanıt" +"name" = "Ad" +"value" = "Değer" + +[pages.inbounds.stream.tcp] +"version" = "Sürüm" +"method" = "Yöntem" +"path" = "Yol" +"status" = "Durum" +"statusDescription" = "Durum Açıklaması" +"requestHeader" = "İstek Başlığı" +"responseHeader" = "Yanıt Başlığı" + +[pages.settings] +"title" = "Panel Ayarları" +"save" = "Kaydet" +"infoDesc" = "Burada yapılan her değişikliğin kaydedilmesi gerekir. Değişikliklerin uygulanması için paneli yeniden başlatın." +"restartPanel" = "Paneli Yeniden Başlat" +"restartPanelDesc" = "Paneli yeniden başlatmak istediğinizden emin misiniz? Yeniden başlattıktan sonra panele erişemezseniz, sunucudaki panel günlük bilgilerini görüntüleyin." +"restartPanelSuccess" = "Panel başarıyla yeniden başlatıldı" +"actions" = "Eylemler" +"resetDefaultConfig" = "Varsayılana Sıfırla" +"panelSettings" = "Genel" +"securitySettings" = "Kimlik Doğrulama" +"TGBotSettings" = "Telegram Bot" +"panelListeningIP" = "Dinleme IP" +"panelListeningIPDesc" = "Web paneli için IP adresi. (tüm IP'leri dinlemek için boş bırakın)" +"panelListeningDomain" = "Dinleme Alan Adı" +"panelListeningDomainDesc" = "Web paneli için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)" +"panelPort" = "Dinleme Portu" +"panelPortDesc" = "Web paneli için port numarası. (kullanılmayan bir port olmalıdır)" +"publicKeyPath" = "Genel Anahtar Yolu" +"publicKeyPathDesc" = "Web paneli için genel anahtar dosya yolu. ('/' ile başlar)" +"privateKeyPath" = "Özel Anahtar Yolu" +"privateKeyPathDesc" = "Web paneli için özel anahtar dosya yolu. ('/' ile başlar)" +"panelUrlPath" = "URI Yolu" +"panelUrlPathDesc" = "Web paneli için URI yolu. ('/' ile başlar ve '/' ile biter)" +"pageSize" = "Sayfa Boyutu" +"pageSizeDesc" = "Gelenler tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)" +"remarkModel" = "Açıklama Modeli & Ayırma Karakteri" +"datepicker" = "Takvim Türü" +"datepickerPlaceholder" = "Tarih Seçin" +"datepickerDescription" = "Planlanmış görevler bu takvime göre çalışacaktır." +"sampleRemark" = "Örnek Açıklama" +"oldUsername" = "Mevcut Kullanıcı Adı" +"currentPassword" = "Mevcut Şifre" +"newUsername" = "Yeni Kullanıcı Adı" +"newPassword" = "Yeni Şifre" +"telegramBotEnable" = "Telegram Botunu Etkinleştir" +"telegramBotEnableDesc" = "Telegram botunu etkinleştirir." +"telegramToken" = "Telegram Token" +"telegramTokenDesc" = "'@BotFather'dan alınan Telegram bot token." +"telegramProxy" = "SOCKS Proxy" +"telegramProxyDesc" = "Telegram'a bağlanmak için SOCKS5 proxy'sini etkinleştirir. (ayarları kılavuzda belirtilen şekilde ayarlayın)" +"telegramAPIServer" = "Telegram API Server" +"telegramAPIServerDesc" = "Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın." +"telegramChatId" = "Yönetici Sohbet Kimliği" +"telegramChatIdDesc" = "Telegram Yönetici Sohbet Kimliği(leri). (virgülle ayrılmış)(buradan alın @userinfobot) veya (botta '/id' komutunu kullanın)" +"telegramNotifyTime" = "Bildirim Zamanı" +"telegramNotifyTimeDesc" = "Periyodik raporlar için ayarlanan Telegram bot bildirim zamanı. (crontab zaman formatını kullanın)" +"tgNotifyBackup" = "Veritabanı Yedeği" +"tgNotifyBackupDesc" = "Bir rapor ile birlikte veritabanı yedek dosyasını gönder." +"tgNotifyLogin" = "Giriş Bildirimi" +"tgNotifyLoginDesc" = "Birisi web panelinize giriş yapmaya çalıştığında kullanıcı adı, IP adresi ve zaman hakkında bildirim alın." +"sessionMaxAge" = "Oturum Süresi" +"sessionMaxAgeDesc" = "Giriş yaptıktan sonra oturum süresi. (birim: dakika)" +"expireTimeDiff" = "Son Kullanma Tarihi Bildirimi" +"expireTimeDiffDesc" = "Bu eşik seviyesine ulaşıldığında son kullanma tarihi hakkında bildirim alın. (birim: gün)" +"trafficDiff" = "Trafik Sınırı Bildirimi" +"trafficDiffDesc" = "Bu eşik seviyesine ulaşıldığında trafik sınırı hakkında bildirim alın. (birim: GB)" +"tgNotifyCpu" = "CPU Yükü Bildirimi" +"tgNotifyCpuDesc" = "CPU yükü bu eşik seviyesini aşarsa bildirim alın. (birim: %)" +"timeZone" = "Saat Dilimi" +"timeZoneDesc" = "Planlanmış görevler bu saat dilimine göre çalışacaktır." +"subSettings" = "Abonelik" +"subEnable" = "Abonelik Hizmetini Etkinleştir" +"subEnableDesc" = "Abonelik hizmetini etkinleştirir." +"subTitle" = "Abonelik Başlığı" +"subTitleDesc" = "VPN istemcisinde gösterilen başlık" +"subListen" = "Dinleme IP" +"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)" +"subPort" = "Dinleme Portu" +"subPortDesc" = "Abonelik hizmeti için port numarası. (kullanılmayan bir port olmalıdır)" +"subCertPath" = "Genel Anahtar Yolu" +"subCertPathDesc" = "Abonelik hizmeti için genel anahtar dosya yolu. ('/' ile başlar)" +"subKeyPath" = "Özel Anahtar Yolu" +"subKeyPathDesc" = "Abonelik hizmeti için özel anahtar dosya yolu. ('/' ile başlar)" +"subPath" = "URI Yolu" +"subPathDesc" = "Abonelik hizmeti için URI yolu. ('/' ile başlar ve '/' ile biter)" +"subDomain" = "Dinleme Alan Adı" +"subDomainDesc" = "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)" +"subUpdates" = "Güncelleme Aralıkları" +"subUpdatesDesc" = "Müşteri uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)" +"subEncrypt" = "Şifrele" +"subEncryptDesc" = "Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir." +"subShowInfo" = "Kullanım Bilgisini Göster" +"subShowInfoDesc" = "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir." +"subURI" = "Ters Proxy URI" +"subURIDesc" = "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu." +"externalTrafficInformEnable" = "Harici Trafik Bilgisi" +"externalTrafficInformEnableDesc" = "Her trafik güncellemesinde harici API'yi bilgilendirin." +"externalTrafficInformURI" = "Harici Trafik Bilgisi URI'si" +"externalTrafficInformURIDesc" = "Trafik güncellemeleri bu URI'ye gönderildi." +"fragment" = "Parçalama" +"fragmentDesc" = "TLS merhaba paketinin parçalanmasını etkinleştir." +"fragmentSett" = "Parçalama Ayarları" +"noisesDesc" = "Noises'i Etkinleştir." +"noisesSett" = "Noises Ayarları" +"mux" = "Mux" +"muxDesc" = "Kurulmuş bir veri akışında birden çok bağımsız veri akışını iletir." +"muxSett" = "Mux Ayarları" +"direct" = "Doğrudan Bağlantı" +"directDesc" = "Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar." +"notifications" = "Bildirimler" +"certs" = "Sertifikalar" +"externalTraffic" = "Harici Trafik" +"dateAndTime" = "Tarih ve Saat" +"proxyAndServer" = "Proxy ve Sunucu" +"intervals" = "Aralıklar" +"information" = "Bilgi" +"language" = "Dil" +"telegramBotLanguage" = "Telegram Bot Dili" + +[pages.xray] +"title" = "Xray Yapılandırmaları" +"save" = "Kaydet" +"restart" = "Xray'i Yeniden Başlat" +"restartSuccess" = "Xray başarıyla yeniden başlatıldı" +"stopSuccess" = "Xray başarıyla durduruldu" +"restartError" = "Xray yeniden başlatılırken bir hata oluştu." +"stopError" = "Xray durdurulurken bir hata oluştu." +"basicTemplate" = "Temeller" +"advancedTemplate" = "Gelişmiş" +"generalConfigs" = "Genel" +"generalConfigsDesc" = "Bu seçenekler genel ayarlamaları belirler." +"logConfigs" = "Günlük" +"logConfigsDesc" = "Günlükler sunucunuzun verimliliğini etkileyebilir. Yalnızca ihtiyaç durumunda akıllıca etkinleştirmeniz önerilir" +"blockConfigsDesc" = "Bu seçenekler belirli istek protokolleri ve web siteleri temelinde trafiği engeller." +"basicRouting" = "Temel Yönlendirme" +"blockConnectionsConfigsDesc" = "Bu seçenekler belirli bir istenen ülkeye göre trafiği engelleyecektir." +"directConnectionsConfigsDesc" = "Doğrudan bağlantı, belirli bir trafiğin başka bir sunucu üzerinden yönlendirilmediğini sağlar." +"blockips" = "IP'leri Engelle" +"blockdomains" = "Alan Adlarını Engelle" +"directips" = "Doğrudan IP'ler" +"directdomains" = "Doğrudan Alan Adları" +"ipv4Routing" = "IPv4 Yönlendirme" +"ipv4RoutingDesc" = "Bu seçenekler belirli bir varış yerine IPv4 üzerinden trafiği yönlendirir." +"warpRouting" = "WARP Yönlendirme" +"warpRoutingDesc" = "Bu seçenekler belirli bir varış yerine WARP üzerinden trafiği yönlendirir." +"Template" = "Gelişmiş Xray Yapılandırma Şablonu" +"TemplateDesc" = "Nihai Xray yapılandırma dosyası bu şablona göre oluşturulacaktır." +"FreedomStrategy" = "Freedom Protokol Stratejisi" +"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın." +"RoutingStrategy" = "Genel Yönlendirme Stratejisi" +"RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın." +"Torrent" = "BitTorrent Protokolünü Engelle" +"Inbounds" = "Gelenler" +"InboundsDesc" = "Belirli müşterileri kabul eder." +"Outbounds" = "Gidenler" +"Balancers" = "Dengeler" +"OutboundsDesc" = "Giden trafiğin yolunu ayarlayın." +"Routings" = "Yönlendirme Kuralları" +"RoutingsDesc" = "Her kuralın önceliği önemlidir!" +"completeTemplate" = "Tümü" +"logLevel" = "Günlük Seviyesi" +"logLevelDesc" = "Hata günlükleri için günlük seviyesi, kaydedilmesi gereken bilgileri belirtir." +"accessLog" = "Erişim Günlüğü" +"accessLogDesc" = "Erişim günlüğü için dosya yolu. 'none' özel değeri erişim günlüklerini devre dışı bırakır" +"errorLog" = "Hata Günlüğü" +"errorLogDesc" = "Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır" +"dnsLog" = "DNS Günlüğü" +"dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin" +"maskAddress" = "Adres Maskesi" +"maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir." +"statistics" = "İstatistikler" +"statsInboundUplink" = "Gelen Yükleme İstatistikleri" +"statsInboundUplinkDesc" = "Tüm gelen proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir." +"statsInboundDownlink" = "Gelen İndirme İstatistikleri" +"statsInboundDownlinkDesc" = "Tüm gelen proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir." +"statsOutboundUplink" = "Giden Yükleme İstatistikleri" +"statsOutboundUplinkDesc" = "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir." +"statsOutboundDownlink" = "Giden İndirme İstatistikleri" +"statsOutboundDownlinkDesc" = "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir." + +[pages.xray.rules] +"first" = "İlk" +"last" = "Son" +"up" = "Yukarı" +"down" = "Aşağı" +"source" = "Kaynak" +"dest" = "Hedef" +"inbound" = "Gelen" +"outbound" = "Giden" +"balancer" = "Dengeler" +"info" = "Bilgi" +"add" = "Kural Ekle" +"edit" = "Kuralı Düzenle" +"useComma" = "Virgülle ayrılmış öğeler" + +[pages.xray.outbound] +"addOutbound" = "Giden Ekle" +"addReverse" = "Ters Ekle" +"editOutbound" = "Gideni Düzenle" +"editReverse" = "Tersi Düzenle" +"tag" = "Etiket" +"tagDesc" = "Benzersiz Etiket" +"address" = "Adres" +"reverse" = "Ters" +"domain" = "Alan Adı" +"type" = "Tür" +"bridge" = "Köprü" +"portal" = "Portal" +"link" = "Bağlantı" +"intercon" = "Bağlantı" +"settings" = "Ayarlar" +"accountInfo" = "Hesap Bilgileri" +"outboundStatus" = "Giden Durumu" +"sendThrough" = "Üzerinden Gönder" + +[pages.xray.balancer] +"addBalancer" = "Dengeleyici Ekle" +"editBalancer" = "Dengeleyiciyi Düzenle" +"balancerStrategy" = "Strateji" +"balancerSelectors" = "Seçiciler" +"tag" = "Etiket" +"tagDesc" = "Benzersiz Etiket" +"balancerDesc" = "Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca giden etiketi çalışır." + +[pages.xray.wireguard] +"secretKey" = "Gizli Anahtar" +"publicKey" = "Genel Anahtar" +"allowedIPs" = "İzin Verilen IP'ler" +"endpoint" = "Uç Nokta" +"psk" = "Ön Paylaşılan Anahtar" +"domainStrategy" = "Alan Adı Stratejisi" + +[pages.xray.dns] +"enable" = "DNS'yi Etkinleştir" +"enableDesc" = "Dahili DNS sunucusunu etkinleştir" +"tag" = "DNS Gelen Etiketi" +"tagDesc" = "Bu etiket, yönlendirme kurallarında Gelen etiketi olarak kullanılabilir." +"clientIp" = "İstemci IP" +"clientIpDesc" = "DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır" +"disableCache" = "Önbelleği devre dışı bırak" +"disableCacheDesc" = "DNS önbelleğini devre dışı bırakır" +"disableFallback" = "Yedeklemeyi devre dışı bırak" +"disableFallbackDesc" = "Yedek DNS sorgularını devre dışı bırakır" +"disableFallbackIfMatch" = "Eşleşirse Yedeklemeyi Devre Dışı Bırak" +"disableFallbackIfMatchDesc" = "DNS sunucusunun eşleşen alan adı listesi vurulduğunda yedek DNS sorgularını devre dışı bırakır" +"strategy" = "Sorgu Stratejisi" +"strategyDesc" = "Alan adlarını çözmek için genel strateji" +"add" = "Sunucu Ekle" +"edit" = "Sunucuyu Düzenle" +"domains" = "Alan Adları" +"expectIPs" = "Beklenen IP'ler" +"unexpectIPs" = "Beklenmeyen IP'ler" +"useSystemHosts" = "Sistem Hosts'larını Kullan" +"useSystemHostsDesc" = "Yüklü bir sistemden hosts dosyasını kullan" +"usePreset" = "Şablon kullan" +"dnsPresetTitle" = "DNS Şablonları" +"dnsPresetFamily" = "Aile" + +[pages.xray.fakedns] +"add" = "Sahte DNS Ekle" +"edit" = "Sahte DNS'i Düzenle" +"ipPool" = "IP Havuzu Alt Ağı" +"poolSize" = "Havuz Boyutu" + +[pages.settings.security] +"admin" = "Yönetici kimlik bilgileri" +"twoFactor" = "İki adımlı doğrulama" +"twoFactorEnable" = "2FA'yı Etkinleştir" +"twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler." +"twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir" +"twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak" +"twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:" +"twoFactorModalFirstStep" = "1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki token'ı kopyalayıp uygulamaya yapıştırın" +"twoFactorModalSecondStep" = "2. Uygulamadaki kodu girin" +"twoFactorModalRemoveStep" = "İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin." +"twoFactorModalChangeCredentialsTitle" = "Kimlik bilgilerini değiştir" +"twoFactorModalChangeCredentialsStep" = "Yönetici kimlik bilgilerini değiştirmek için uygulamadaki kodu girin." +"twoFactorModalSetSuccess" = "İki faktörlü kimlik doğrulama başarıyla kuruldu" +"twoFactorModalDeleteSuccess" = "İki faktörlü kimlik doğrulama başarıyla silindi" +"twoFactorModalError" = "Yanlış kod" + +[pages.settings.toasts] +"modifySettings" = "Parametreler değiştirildi." +"getSettings" = "Parametreler alınırken bir hata oluştu." +"modifyUserError" = "Yönetici kimlik bilgileri değiştirilirken bir hata oluştu." +"modifyUser" = "Yönetici kimlik bilgilerini başarıyla değiştirdiniz." +"originalUserPassIncorrect" = "Mevcut kullanıcı adı veya şifre geçersiz" +"userPassMustBeNotEmpty" = "Yeni kullanıcı adı ve şifre boş olamaz" +"getOutboundTrafficError" = "Giden trafik alınırken hata" +"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata" + +[tgbot] +"keyboardClosed" = "❌ Klavye kapatıldı!" +"noResult" = "❗ Sonuç yok!" +"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!" +"wentWrong" = "❌ Bir şeyler yanlış gitti!" +"noIpRecord" = "❗ IP Kaydı Yok!" +"noInbounds" = "❗ Gelen bağlantı bulunamadı!" +"unlimited" = "♾ Sınırsız (Sıfırla)" +"add" = "Ekle" +"month" = "Ay" +"months" = "Aylar" +"day" = "Gün" +"days" = "Günler" +"hours" = "Saatler" +"minutes" = "Dakika" +"unknown" = "Bilinmeyen" +"inbounds" = "Gelenler" +"clients" = "İstemciler" +"offline" = "🔴 Çevrimdışı" +"online" = "🟢 Çevrimiçi" + +[tgbot.commands] +"unknown" = "❗ Bilinmeyen komut." +"pleaseChoose" = "👇 Lütfen seçin:\r\n" +"help" = "🤖 Bu bota hoş geldiniz! Web panelinden belirli verileri sunmak ve gerektiğinde değişiklik yapmanıza olanak tanımak için tasarlanmıştır.\r\n\r\n" +"start" = "👋 Merhaba {{ .Firstname }}.\r\n" +"welcome" = "🤖 {{ .Hostname }} yönetim botuna hoş geldiniz.\r\n" +"status" = "✅ Bot çalışıyor!" +"usage" = "❗ Lütfen aramak için bir metin sağlayın!" +"getID" = "🆔 Kimliğiniz: {{ .ID }}" +"helpAdminCommands" = "Xray Core'u yeniden başlatmak için:\r\n/restart\r\n\r\nBir müşteri e-postasını aramak için:\r\n/usage [E-posta]\r\n\r\nGelenleri aramak için (müşteri istatistikleri ile):\r\n/inbound [Açıklama]\r\n\r\nTelegram Sohbet Kimliği:\r\n/id" +"helpClientCommands" = "İstatistikleri aramak için şu komutu kullanın:\r\n\r\n/usage [E-posta]\r\n\r\nTelegram Sohbet Kimliği:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ İşlem başarılı!" +"restartFailed" = "❗ İşlem hatası.\r\n\r\nHata: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core çalışmıyor." +"startDesc" = "Ana menüyü göster" +"helpDesc" = "Bot yardımı" +"statusDesc" = "Bot durumunu kontrol et" +"idDesc" = "Telegram ID'nizi göster" + +[tgbot.messages] +"cpuThreshold" = "🔴 CPU Yükü {{ .Percent }}% eşiği {{ .Threshold }}%'yi aşıyor" +"selectUserFailed" = "❌ Kullanıcı seçiminde hata!" +"userSaved" = "✅ Telegram Kullanıcısı kaydedildi." +"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n" +"loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n" +"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n" +"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n" +"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n" +"version" = "🚀 X-Panel Sürümü: {{ .Version }}\r\n" +"xrayVersion" = "📡 Xray Sürümü: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IP'ler:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Çalışma Süresi: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Sistem Yükü: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Trafik: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Durum: {{ .State }}\r\n" +"username" = "👤 Kullanıcı Adı: {{ .Username }}\r\n" +"password" = "👤 Şifre: {{ .Password }}\r\n" +"time" = "⏰ Zaman: {{ .Time }}\r\n" +"inbound" = "📍 Gelen: {{ .Remark }}\r\n" +"port" = "🔌 Port: {{ .Port }}\r\n" +"expire" = "📅 Son Kullanma Tarihi: {{ .Time }}\r\n" +"expireIn" = "📅 Sona Erecek: {{ .Time }}\r\n" +"active" = "💡 Aktif: {{ .Enable }}\r\n" +"enabled" = "🚨 Etkin: {{ .Enable }}\r\n" +"online" = "🌐 Bağlantı durumu: {{ .Status }}\r\n" +"email" = "📧 E-posta: {{ .Email }}\r\n" +"upload" = "🔼 Yükleme: ↑{{ .Upload }}\r\n" +"download" = "🔽 İndirme: ↓{{ .Download }}\r\n" +"total" = "📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Telegram Kullanıcısı: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Tükenmiş {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Tükenmiş {{ .Type }} sayısı:\r\n" +"onlinesCount" = "🌐 Çevrimiçi Müşteriler: {{ .Count }}\r\n" +"disabled" = "🛑 Devre Dışı: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Yakında Tükenecek: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Yedekleme Zamanı: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n" +"yes" = "✅ Evet" +"no" = "❌ Hayır" +"received_id" = "🔑📥 Kimlik güncellendi." +"received_password" = "🔑📥 Şifre güncellendi." +"received_email" = "📧📥 E-posta güncellendi." +"received_comment" = "💬📥 Yorum güncellendi." +"id_prompt" = "🔑 Varsayılan Kimlik: {{ .ClientId }}\n\nKimliğinizi girin." +"pass_prompt" = "🔑 Varsayılan Şifre: {{ .ClientPassword }}\n\nŞifrenizi girin." +"email_prompt" = "📧 Varsayılan E-posta: {{ .ClientEmail }}\n\nE-postanızı girin." +"comment_prompt" = "💬 Varsayılan Yorum: {{ .ClientComment }}\n\nYorumunuzu girin." +"inbound_client_data_id" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!" +"inbound_client_data_pass" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!" +"cancel" = "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄" +"error_add_client" = "⚠️ Hata:\n\n {{ .error }}" +"using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊" +"incorrect_input" ="Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫" +"AreYouSure" = "Emin misin? 🤔" +"SuccessResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı" +"FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠️ Hata: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı." + +[tgbot.buttons] +"closeKeyboard" = "❌ Klavyeyi Kapat" +"cancel" = "❌ İptal" +"cancelReset" = "❌ Sıfırlamayı İptal Et" +"cancelIpLimit" = "❌ IP Limitini İptal Et" +"confirmResetTraffic" = "✅ Trafiği Sıfırlamayı Onayla?" +"confirmClearIps" = "✅ IP'leri Temizlemeyi Onayla?" +"confirmRemoveTGUser" = "✅ Telegram Kullanıcısını Kaldırmayı Onayla?" +"confirmToggle" = "✅ Kullanıcıyı Etkinleştirme/Devre Dışı Bırakmayı Onayla?" +"dbBackup" = "Veritabanı Yedeği Al" +"serverUsage" = "Sunucu Kullanımı" +"getInbounds" = "Gelenleri Al" +"depleteSoon" = "Yakında Tükenecek" +"clientUsage" = "Kullanımı Al" +"onlines" = "Çevrimiçi Müşteriler" +"commands" = "Komutlar" +"refresh" = "🔄 Yenile" +"clearIPs" = "❌ IP'leri Temizle" +"removeTGUser" = "❌ Telegram Kullanıcısını Kaldır" +"selectTGUser" = "👤 Telegram Kullanıcısını Seç" +"selectOneTGUser" = "👤 Bir Telegram Kullanıcısını Seçin:" +"resetTraffic" = "📈 Trafiği Sıfırla" +"resetExpire" = "📅 Son Kullanma Tarihini Değiştir" +"ipLog" = "🔢 IP Günlüğü" +"ipLimit" = "🔢 IP Limiti" +"setTGUser" = "👤 Telegram Kullanıcısını Ayarla" +"toggle" = "🔘 Etkinleştir / Devre Dışı Bırak" +"custom" = "🔢 Özel" +"confirmNumber" = "✅ Onayla: {{ .Num }}" +"confirmNumberAdd" = "✅ Ekleme onayı: {{ .Num }}" +"limitTraffic" = "🚧 Trafik Sınırı" +"getBanLogs" = "Yasak Günlüklerini Al" +"allClients" = "Tüm Müşteriler" +"addClient" = "Müşteri Ekle" +"submitDisable" = "Devre Dışı Olarak Gönder ☑️" +"submitEnable" = "Etkin Olarak Gönder ✅" +"use_default" = "🏷️ Varsayılanı Kullan" +"change_id" = "⚙️🔑 Kimlik" +"change_password" = "⚙️🔑 Şifre" +"change_email" = "⚙️📧 E-posta" +"change_comment" = "⚙️💬 Yorum" +"ResetAllTraffics" = "Tüm Trafikleri Sıfırla" +"SortedTrafficUsageReport" = "Sıralı Trafik Kullanım Raporu" + +[tgbot.answers] +"successfulOperation" = "✅ İşlem başarılı!" +"errorOperation" = "❗ İşlemde hata." +"getInboundsFailed" = "❌ Gelenler alınamadı." +"getClientsFailed" = "❌ Müşteriler alınamadı." +"canceled" = "❌ {{ .Email }}: İşlem iptal edildi." +"clientRefreshSuccess" = "✅ {{ .Email }}: Müşteri başarıyla yenilendi." +"IpRefreshSuccess" = "✅ {{ .Email }}: IP'ler başarıyla yenilendi." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Müşterinin Telegram Kullanıcısı başarıyla yenilendi." +"resetTrafficSuccess" = "✅ {{ .Email }}: Trafik başarıyla sıfırlandı." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Trafik limiti başarıyla kaydedildi." +"expireResetSuccess" = "✅ {{ .Email }}: Son kullanma günleri başarıyla sıfırlandı." +"resetIpSuccess" = "✅ {{ .Email }}: IP limiti {{ .Count }} başarıyla kaydedildi." +"clearIpSuccess" = "✅ {{ .Email }}: IP'ler başarıyla temizlendi." +"getIpLog" = "✅ {{ .Email }}: IP Günlüğü alındı." +"getUserInfo" = "✅ {{ .Email }}: Telegram Kullanıcı Bilgisi alındı." +"removedTGUserSuccess" = "✅ {{ .Email }}: Telegram Kullanıcısı başarıyla kaldırıldı." +"enableSuccess" = "✅ {{ .Email }}: Başarıyla etkinleştirildi." +"disableSuccess" = "✅ {{ .Email }}: Başarıyla devre dışı bırakıldı." +"askToAddUserId" = "Yapılandırmanız bulunamadı!\r\nLütfen yöneticinizden yapılandırmalarınıza Telegram ChatID'nizi eklemesini isteyin.\r\n\r\nKullanıcı ChatID'niz: {{ .TgUserID }}" +"chooseClient" = "Gelen {{ .Inbound }} için bir Müşteri Seçin" +"chooseInbound" = "Bir Gelen Seçin" diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml new file mode 100644 index 0000000..483e478 --- /dev/null +++ b/web/translation/translate.uk_UA.toml @@ -0,0 +1,739 @@ +"username" = "Ім'я користувача" +"password" = "Пароль" +"login" = "Увійти" +"confirm" = "Підтвердити" +"cancel" = "Скасувати" +"close" = "Закрити" +"create" = "Створити" +"update" = "Оновити" +"copy" = "Копіювати" +"copied" = "Скопійовано" +"download" = "Завантажити" +"remark" = "Примітка" +"enable" = "Увімкнути" +"protocol" = "Протокол" +"search" = "Пошук" +"filter" = "Фільтр" +"loading" = "Завантаження..." +"second" = "Секунда" +"minute" = "Хвилина" +"hour" = "Година" +"day" = "День" +"check" = "Перевірка" +"indefinite" = "Безстроково" +"unlimited" = "Безлімітний" +"none" = "Немає" +"qrCode" = "QR-Код" +"info" = "Більше інформації" +"edit" = "Редагувати" +"delete" = "Видалити" +"reset" = "Скидання" +"noData" = "Немає даних." +"copySuccess" = "Скопійовано успішно" +"sure" = "Звичайно" +"encryption" = "Шифрування" +"useIPv4ForHost" = "Використовувати IPv4 для хоста" +"transmission" = "Протокол передачи" +"host" = "Хост" +"path" = "Шлях" +"camouflage" = "Маскування" +"status" = "Статус" +"enabled" = "Увімкнено" +"disabled" = "Вимкнено" +"depleted" = "Вичерпано" +"depletingSoon" = "Вичерпується" +"offline" = "Офлайн" +"online" = "Онлайн" +"domainName" = "Доменне ім`я" +"monitor" = "Слухати IP" +"certificate" = "Цифровий сертифікат" +"fail" = "Помилка" +"comment" = "Коментар" +"success" = "Успішно" +"lastOnline" = "Був(ла) онлайн" +"getVersion" = "Отримати версію" +"install" = "Встановити" +"clients" = "Клієнти" +"usage" = "Використання" +"twoFactorCode" = "Код" +"remained" = "Залишилося" +"security" = "Беспека" +"secAlertTitle" = "Попередження системи безпеки" +"secAlertSsl" = "Це з'єднання не є безпечним. Будь ласка, уникайте введення конфіденційної інформації, поки TLS не буде активовано для захисту даних." +"secAlertConf" = "Деякі налаштування вразливі до атак. Рекомендується посилити протоколи безпеки, щоб запобігти можливим порушенням." +"secAlertSSL" = "Панель не має безпечного з'єднання. Будь ласка, встановіть сертифікат TLS для захисту даних." +"secAlertPanelPort" = "Стандартний порт панелі вразливий. Будь ласка, сконфігуруйте випадковий або конкретний порт." +"secAlertPanelURI" = "Стандартний URI-шлях панелі небезпечний. Будь ласка, сконфігуруйте складний URI-шлях." +"secAlertSubURI" = "Стандартний URI-шлях підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях." +"secAlertSubJsonURI" = "Стандартний URI-шлях JSON підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях." +"emptyDnsDesc" = "Немає доданих DNS-серверів." +"emptyFakeDnsDesc" = "Немає доданих Fake DNS-серверів." +"emptyBalancersDesc" = "Немає доданих балансувальників." +"emptyReverseDesc" = "Немає доданих зворотних проксі." +"somethingWentWrong" = "Щось пішло не так" + +[menu] +"theme" = "Тема" +"dark" = "Темна" +"ultraDark" = "Ультра темна" +"dashboard" = "Огляд" +"inbounds" = "Вхідні" +"settings" = "Параметри панелі" +"xray" = "Конфігурації Xray" +"logout" = "Вийти" +"link" = "Керувати" + +[pages.login] +"hello" = "Привіт" +"title" = "Привітання!" +"loginAgain" = "Ваш сеанс закінчився, увійдіть знову" + +[pages.login.toasts] +"invalidFormData" = "Формат вхідних даних недійсний." +"emptyUsername" = "Потрібне ім'я користувача" +"emptyPassword" = "Потрібен пароль" +"wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації." +"successLogin" = "Ви успішно увійшли до свого облікового запису." + +[pages.index] +"title" = "Огляд" +"cpu" = "ЦП" +"logicalProcessors" = "Логічні процесори" +"frequency" = "Частота" +"swap" = "Своп" +"storage" = "Сховище" +"memory" = "ОЗП" +"threads" = "Потоки" +"xrayStatus" = "Xray" +"stopXray" = "Зупинити" +"restartXray" = "Перезапустити" +"xraySwitch" = "Версія" +"xraySwitchClick" = "Виберіть версію, на яку ви хочете перейти." +"xraySwitchClickDesk" = "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями." +"xrayStatusUnknown" = "Невідомо" +"xrayStatusRunning" = "Запущено" +"xrayStatusStop" = "Зупинено" +"xrayStatusError" = "Помилка" +"xrayErrorPopoverTitle" = "Під час роботи Xray сталася помилка" +"operationHours" = "Час роботи" +"systemLoad" = "Завантаження системи" +"systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин" +"connectionCount" = "Статистика з'єднання" +"ipAddresses" = "IP-адреси" +"toggleIpVisibility" = "Перемкнути видимість IP" +"overallSpeed" = "Загальна швидкість" +"upload" = "Відправка" +"download" = "Завантаження" +"totalData" = "Загальний обсяг даних" +"sent" = "Відправлено" +"received" = "Отримано" +"documentation" = "Документація" +"xraySwitchVersionDialog" = "Ви дійсно хочете змінити версію Xray?" +"xraySwitchVersionDialogDesc" = "Це змінить версію Xray на #version#." +"xraySwitchVersionPopover" = "Xray успішно оновлено" +"geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?" +"geofileUpdateDialogDesc" = "Це оновить файл #filename#." +"geofilesUpdateDialogDesc" = "Це оновить усі геофайли." +"geofilesUpdateAll" = "Оновити все" +"geofileUpdatePopover" = "Геофайл успішно оновлено" +"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" +"logs" = "Журнали" +"config" = "Конфігурація" +"backup" = "Резервна копія" +"backupTitle" = "Резервне копіювання та відновлення бази даних" +"exportDatabase" = "Резервна копія" +"exportDatabaseDesc" = "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій." +"importDatabase" = "Відновити" +"importDatabaseDesc" = "Натисніть, щоб вибрати та завантажити файл .db з вашого пристрою для відновлення бази даних з резервної копії." +"importDatabaseSuccess" = "Базу даних успішно імпортовано" +"importDatabaseError" = "Виникла помилка під час імпорту бази даних" +"readDatabaseError" = "Виникла помилка під час читання бази даних" +"getDatabaseError" = "Виникла помилка під час отримання бази даних" +"getConfigError" = "Виникла помилка під час отримання файлу конфігурації" + +[pages.inbounds] +"allTimeTraffic" = "Загальний трафік" +"allTimeTrafficUsage" = "Загальне використання за весь час" +"title" = "Вхідні" +"totalDownUp" = "Всього надісланих/отриманих" +"totalUsage" = "Всього використанно" +"inboundCount" = "Загальна кількість вхідних" +"operate" = "Меню" +"enable" = "Увімкнено" +"remark" = "Примітка" +"protocol" = "Протокол" +"port" = "Порт" +"portMap" = "Порт-перехід" +"traffic" = "Трафік" +"details" = "Деталі" +"transportConfig" = "Транспорт" +"expireDate" = "Тривалість" +"createdAt" = "Створено" +"updatedAt" = "Оновлено" +"resetTraffic" = "Скинути трафік" +"addInbound" = "Додати вхідний" +"generalActions" = "Загальні дії" +"autoRefresh" = "Автооновлення" +"autoRefreshInterval" = "Інтервал" +"modifyInbound" = "Змінити вхідний" +"deleteInbound" = "Видалити вхідні" +"deleteInboundContent" = "Ви впевнені, що хочете видалити вхідні?" +"deleteClient" = "Видалити клієнта" +"deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?" +"resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?" +"copyLink" = "Копіювати URL" +"address" = "Адреса" +"network" = "Мережа" +"destinationPort" = "Порт призначення" +"targetAddress" = "Цільова адреса" +"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси" +"meansNoLimit" = "= Необмежено. (одиниця: ГБ)" +"totalFlow" = "Загальна витрата" +"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався" +"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням" +"certificatePath" = "Шлях до файлу" +"certificateContent" = "Вміст файлу" +"publicKey" = "Публічний ключ" +"privatekey" = "Закритий ключ" +"clickOnQRcode" = "Натисніть QR-код, щоб скопіювати" +"client" = "Клієнт" +"export" = "Експортувати всі URL-адреси" +"clone" = "Клон" +"cloneInbound" = "Клонувати" +"cloneInboundContent" = "Усі налаштування цього вхідного потоку, крім порту, IP-адреси прослуховування та клієнтів, будуть застосовані до клону." +"cloneInboundOk" = "Клонувати" +"resetAllTraffic" = "Скинути весь вхідний трафік" +"resetAllTrafficTitle" = "Скинути весь вхідний трафік" +"resetAllTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх вхідних?" +"resetInboundClientTraffics" = "Скинути трафік клієнтів" +"resetInboundClientTrafficTitle" = "Скинути трафік клієнтів" +"resetInboundClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік клієнтів цього вхідного потоку?" +"resetAllClientTraffics" = "Скинути весь трафік клієнтів" +"resetAllClientTrafficTitle" = "Скинути весь трафік клієнтів" +"resetAllClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх клієнтів?" +"delDepletedClients" = "Видалити вичерпані клієнти" +"delDepletedClientsTitle" = "Видалити вичерпані клієнти" +"delDepletedClientsContent" = "Ви впевнені, що хочете видалити всі вичерпані клієнти?" +"email" = "Електронна пошта" +"emailDesc" = "Будь ласка, надайте унікальну адресу електронної пошти." +"IPLimit" = "Обмеження IP" +"IPLimitDesc" = "Вимикає вхідний, якщо кількість перевищує встановлене значення. (0 = вимкнено)" +"IPLimitlog" = "Журнал IP" +"IPLimitlogDesc" = "Журнал історії IP-адрес. (щоб увімкнути вхідну після вимкнення, очистіть журнал)" +"IPLimitlogclear" = "Очистити журнал" +"setDefaultCert" = "Установити сертифікат з панелі" +"telegramDesc" = "Будь ласка, вкажіть ID чату Telegram. (використовуйте команду '/id' у боті) або (@userinfobot)" +"subscriptionDesc" = "Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів." +"info" = "Інформація" +"same" = "Те саме" +"inboundData" = "Вхідні дані" +"exportInbound" = "Експортувати вхідні" +"import" = "Імпорт" +"importInbound" = "Імпортувати вхідний" + +[pages.client] +"add" = "Додати клієнта" +"edit" = "Редагувати клієнта" +"submitAdd" = "Додати клієнта" +"submitEdit" = "Зберегти зміни" +"clientCount" = "Кількість клієнтів" +"bulk" = "Додати групу" +"method" = "Метод" +"first" = "Перший" +"last" = "Останній" +"prefix" = "Префікс" +"postfix" = "Постфікс" +"delayedStart" = "Початок використання" +"expireDays" = "Тривалість" +"days" = "Дні(в)" +"renew" = "Автоматичне оновлення" +"renewDesc" = "Автоматичне поновлення після закінчення терміну дії. (0 = вимкнено)(одиниця: день)" + +[pages.inbounds.toasts] +"obtain" = "Отримати" +"updateSuccess" = "Оновлення пройшло успішно" +"logCleanSuccess" = "Журнал очищено" +"inboundsUpdateSuccess" = "Вхідні підключення успішно оновлено" +"inboundUpdateSuccess" = "Вхідне підключення успішно оновлено" +"inboundCreateSuccess" = "Вхідне підключення успішно створено" +"inboundDeleteSuccess" = "Вхідне підключення успішно видалено" +"inboundClientAddSuccess" = "Клієнт(и) вхідного підключення додано" +"inboundClientDeleteSuccess" = "Клієнта вхідного підключення видалено" +"inboundClientUpdateSuccess" = "Клієнта вхідного підключення оновлено" +"delDepletedClientsSuccess" = "Усі вичерпані клієнти видалені" +"resetAllClientTrafficSuccess" = "Весь трафік клієнта скинуто" +"resetAllTrafficSuccess" = "Весь трафік скинуто" +"resetInboundClientTrafficSuccess" = "Трафік скинуто" +"trafficGetError" = "Помилка отримання даних про трафік" +"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519." +"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65." + +[pages.inbounds.stream.general] +"request" = "Запит" +"response" = "Відповідь" +"name" = "Ім'я" +"value" = "Значення" + +[pages.inbounds.stream.tcp] +"version" = "Версія" +"method" = "Метод" +"path" = "Шлях" +"status" = "Статус" +"statusDescription" = "Опис стану" +"requestHeader" = "Заголовок запиту" +"responseHeader" = "Заголовок відповіді" + +[pages.settings] +"title" = "Параметри панелі" +"save" = "Зберегти" +"infoDesc" = "Кожна внесена тут зміна повинна бути збережена. Перезапустіть панель, щоб застосувати зміни." +"restartPanel" = "Перезапустити панель" +"restartPanelDesc" = "Ви впевнені, що бажаєте перезапустити панель? Якщо ви не можете отримати доступ до панелі після перезапуску, будь ласка, перегляньте інформацію журналу панелі на сервері." +"restartPanelSuccess" = "Панель успішно перезапущено" +"actions" = "Дії" +"resetDefaultConfig" = "Відновити значення за замовчуванням" +"panelSettings" = "Загальні" +"securitySettings" = "Автентифікація" +"TGBotSettings" = "Telegram Бот" +"panelListeningIP" = "Слухати IP" +"panelListeningIPDesc" = "IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)" +"panelListeningDomain" = "Домен прослуховування" +"panelListeningDomainDesc" = "Доменне ім'я для веб-панелі. (залиште порожнім, щоб слухати всі домени та IP-адреси)" +"panelPort" = "Порт прослуховування" +"panelPortDesc" = "Номер порту для веб-панелі. (має бути невикористаний порт)" +"publicKeyPath" = "Шлях відкритого ключа" +"publicKeyPathDesc" = "Шлях до файлу відкритого ключа для веб-панелі. (починається з ‘/‘)" +"privateKeyPath" = "Шлях приватного ключа" +"privateKeyPathDesc" = "Шлях до файлу приватного ключа для веб-панелі. (починається з ‘/‘)" +"panelUrlPath" = "Шлях URL" +"panelUrlPathDesc" = "Шлях URL для веб-панелі. (починається з ‘/‘ і закінчується ‘/‘)" +"pageSize" = "Розмір сторінки" +"pageSizeDesc" = "Визначити розмір сторінки для вхідної таблиці. (0 = вимкнено)" +"remarkModel" = "Модель зауваження та роздільний символ" +"datepicker" = "Тип календаря" +"datepickerPlaceholder" = "Виберіть дату" +"datepickerDescription" = "Заплановані завдання виконуватимуться на основі цього календаря." +"sampleRemark" = "Зразок зауваження" +"oldUsername" = "Поточне ім'я користувача" +"currentPassword" = "Поточний пароль" +"newUsername" = "Нове ім'я користувача" +"newPassword" = "Новий пароль" +"telegramBotEnable" = "Увімкнути Telegram Bot" +"telegramBotEnableDesc" = "Вмикає бота Telegram." +"telegramToken" = "Telegram Токен" +"telegramTokenDesc" = "Токен бота Telegram, отриманий від '@BotFather'." +"telegramProxy" = "SOCKS Проксі" +"telegramProxyDesc" = "Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)" +"telegramAPIServer" = "Сервер Telegram API" +"telegramAPIServerDesc" = "Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням." +"telegramChatId" = "Ідентифікатор чату адміністратора" +"telegramChatIdDesc" = "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут @userinfobot) або (використовуйте команду '/id' у боті)" +"telegramNotifyTime" = "Час сповіщення" +"telegramNotifyTimeDesc" = "Час повідомлення бота Telegram, встановлений для періодичних звітів. (використовуйте формат часу crontab)" +"tgNotifyBackup" = "Резервне копіювання бази даних" +"tgNotifyBackupDesc" = "Надіслати файл резервної копії бази даних зі звітом." +"tgNotifyLogin" = "Сповіщення про вхід" +"tgNotifyLoginDesc" = "Отримувати сповіщення про ім'я користувача, IP-адресу та час щоразу, коли хтось намагається увійти у вашу веб-панель." +"sessionMaxAge" = "Тривалість сеансу" +"sessionMaxAgeDesc" = "Тривалість, протягом якої ви можете залишатися в системі. (одиниця: хвилина)" +"expireTimeDiff" = "Повідомлення про дату закінчення" +"expireTimeDiffDesc" = "Отримувати сповіщення про термін дії при досягненні цього порогу. (одиниця: день)" +"trafficDiff" = "Повідомлення про обмеження трафіку" +"trafficDiffDesc" = "Отримувати сповіщення про обмеження трафіку при досягненні цього порогу. (одиниця: ГБ)" +"tgNotifyCpu" = "Сповіщення про завантаження ЦП" +"tgNotifyCpuDesc" = "Отримувати сповіщення, якщо навантаження ЦП перевищує це порогове значення. (одиниця: %)" +"timeZone" = "Часовий пояс" +"timeZoneDesc" = "Заплановані завдання виконуватимуться на основі цього часового поясу." +"subSettings" = "Підписка" +"subEnable" = "Увімкнути службу підписки" +"subEnableDesc" = "Вмикає службу підписки." +"subTitle" = "Назва Підписки" +"subTitleDesc" = "Назва, яка відображається у VPN-клієнті" +"subListen" = "Слухати IP" +"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)" +"subPort" = "Слухати порт" +"subPortDesc" = "Номер порту для служби підписки. (має бути невикористаний порт)" +"subCertPath" = "Шлях відкритого ключа" +"subCertPathDesc" = "Шлях до файлу відкритого ключа для служби підписки. (починається з ‘/‘)" +"subKeyPath" = "Шлях приватного ключа" +"subKeyPathDesc" = "Шлях до файлу приватного ключа для служби підписки. (починається з ‘/‘)" +"subPath" = "Шлях URI" +"subPathDesc" = "Шлях URI для служби підписки. (починається з ‘/‘ і закінчується ‘/‘)" +"subDomain" = "Домен прослуховування" +"subDomainDesc" = "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)" +"subUpdates" = "Інтервали оновлення" +"subUpdatesDesc" = "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)" +"subEncrypt" = "Закодувати" +"subEncryptDesc" = "Повернений вміст послуги підписки матиме кодування Base64." +"subShowInfo" = "Показати інформацію про використання" +"subShowInfoDesc" = "Залишок трафіку та дата відображатимуться в клієнтських програмах." +"subURI" = "URI зворотного проксі" +"subURIDesc" = "URI до URL-адреси підписки для використання за проксі." +"externalTrafficInformEnable" = "Інформація про зовнішній трафік" +"externalTrafficInformEnableDesc" = "Інформувати зовнішній API про кожне оновлення трафіку." +"externalTrafficInformURI" = "Інформаційний URI зовнішнього трафіку" +"externalTrafficInformURIDesc" = "Оновлення трафіку надсилаються на цей URI." +"fragment" = "Фрагментація" +"fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS" +"fragmentSett" = "Параметри фрагментації" +"noisesDesc" = "Увімкнути Noises." +"noisesSett" = "Налаштування Noises" +"mux" = "Mux" +"muxDesc" = "Передавати кілька незалежних потоків даних у межах встановленого потоку даних." +"muxSett" = "Налаштування Mux" +"direct" = "Пряме підключення" +"directDesc" = "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни." +"notifications" = "Сповіщення" +"certs" = "Сертифікати" +"externalTraffic" = "Зовнішній трафік" +"dateAndTime" = "Дата та час" +"proxyAndServer" = "Проксі та сервер" +"intervals" = "Інтервали" +"information" = "Інформація" +"language" = "Мова" +"telegramBotLanguage" = "Мова Telegram-бота" + +[pages.xray] +"title" = "Xray конфігурації" +"save" = "Зберегти" +"restart" = "Перезапустити Xray" +"restartSuccess" = "Xray успішно перезапущено" +"stopSuccess" = "Xray успішно зупинено" +"restartError" = "Виникла помилка під час перезапуску Xray." +"stopError" = "Виникла помилка під час зупинки Xray." +"basicTemplate" = "Базовий шаблон" +"advancedTemplate" = "Додатково" +"generalConfigs" = "Загальні конфігурації" +"generalConfigsDesc" = "Ці параметри визначатимуть загальні налаштування." +"logConfigs" = "Журнал" +"logConfigsDesc" = "Журнали можуть вплинути на ефективність вашого сервера. Рекомендується вмикати його з розумом лише у випадку ваших потреб" +"blockConfigsDesc" = "Ці параметри блокуватимуть трафік на основі конкретних запитуваних протоколів і веб-сайтів." +"basicRouting" = "Основна Маршрутизація" +"blockConnectionsConfigsDesc" = "Ці параметри блокуватимуть трафік на основі запитаних країн." +"directConnectionsConfigsDesc" = "Пряме з'єднання гарантує, що певний трафік не буде маршрутизовано через інший сервер." +"blockips" = "Блокувати IP" +"blockdomains" = "Блокувати домени" +"directips" = "Прямі IP" +"directdomains" = "Прямі домени" +"ipv4Routing" = "Маршрутизація IPv4" +"ipv4RoutingDesc" = "Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4." +"warpRouting" = "WARP Маршрутизація" +"warpRoutingDesc" = "Ці параметри маршрутизуватимуть трафік на основі певного пункту призначення через WARP." +"Template" = "Шаблон розширеної конфігурації Xray" +"TemplateDesc" = "Остаточний конфігураційний файл Xray буде створено на основі цього шаблону." +"FreedomStrategy" = "Стратегія протоколу свободи" +"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи." +"RoutingStrategy" = "Загальна стратегія маршрутизації" +"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів." +"Torrent" = "Блокувати протокол BitTorrent" +"Inbounds" = "Вхідні" +"InboundsDesc" = "Прийняття певних клієнтів." +"Outbounds" = "Вихід" +"Balancers" = "Балансери" +"OutboundsDesc" = "Встановити шлях вихідного трафіку." +"Routings" = "Правила маршрутизації" +"RoutingsDesc" = "Пріоритет кожного правила важливий!" +"completeTemplate" = "Усі" +"logLevel" = "Рівень журналу" +"logLevelDesc" = "Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати." +"accessLog" = "Журнал доступу" +"accessLogDesc" = "Шлях до файлу журналу доступу. Спеціальне значення 'none' вимикає журнали доступу" +"errorLog" = "Журнал помилок" +"errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок" +"dnsLog" = "Журнал DNS" +"dnsLogDesc" = "Чи включити журнали запитів DNS" +"maskAddress" = "Маскувати Адресу" +"maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі." +"statistics" = "Статистика" +"statsInboundUplink" = "Статистика вхідного аплінку" +"statsInboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вхідних проксі." +"statsInboundDownlink" = "Статистика вхідного даунлінку" +"statsInboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вхідних проксі." +"statsOutboundUplink" = "Статистика вихідного аплінку" +"statsOutboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі." +"statsOutboundDownlink" = "Статистика вихідного даунлінку" +"statsOutboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі." + +[pages.xray.rules] +"first" = "Перший" +"last" = "Останній" +"up" = "Вгору" +"down" = "Вниз" +"source" = "Джерело" +"dest" = "Пункт призначення" +"inbound" = "Вхідний" +"outbound" = "Вихідний" +"balancer" = "Балансувальник" +"info" = "Інформація" +"add" = "Додати правило" +"edit" = "Редагувати правило" +"useComma" = "Елементи, розділені комами" + +[pages.xray.outbound] +"addOutbound" = "Додати вихідний" +"addReverse" = "Додати реверс" +"editOutbound" = "Редагувати вихідні" +"editReverse" = "Редагувати реверс" +"tag" = "Тег" +"tagDesc" = "Унікальний тег" +"address" = "Адреса" +"reverse" = "Зворотний" +"domain" = "Домен" +"type" = "Тип" +"bridge" = "Міст" +"portal" = "Портал" +"link" = "Посилання" +"intercon" = "Взаємозв'язок" +"settings" = "Налаштування" +"accountInfo" = "Інформація про обліковий запис" +"outboundStatus" = "Статус виходу" +"sendThrough" = "Надіслати через" + +[pages.xray.balancer] +"addBalancer" = "Додати балансир" +"editBalancer" = "Редагувати балансир" +"balancerStrategy" = "Стратегія" +"balancerSelectors" = "Селектори" +"tag" = "Тег" +"tagDesc" = "Унікальний тег" +"balancerDesc" = "Неможливо використовувати balancerTag і outboundTag одночасно. Якщо використовувати одночасно, працюватиме лише outboundTag." + +[pages.xray.wireguard] +"secretKey" = "Приватний ключ" +"publicKey" = "Публічний ключ" +"allowedIPs" = "Дозволені IP-адреси" +"endpoint" = "Кінцева точка" +"psk" = "Спільний ключ" +"domainStrategy" = "Стратегія домену" + +[pages.xray.dns] +"enable" = "Увімкнути DNS" +"enableDesc" = "Увімкнути вбудований DNS-сервер" +"tag" = "Мітка вхідного DNS" +"tagDesc" = "Ця мітка буде доступна як вхідна мітка в правилах маршрутизації." +"clientIp" = "IP клієнта" +"clientIpDesc" = "Використовується для повідомлення серверу про вказане місцезнаходження IP під час DNS-запитів" +"disableCache" = "Вимкнути кеш" +"disableCacheDesc" = "Вимкнути кешування DNS" +"disableFallback" = "Вимкнути резервний DNS" +"disableFallbackDesc" = "Вимкнути резервні DNS-запити" +"disableFallbackIfMatch" = "Вимкнути резервний DNS при збігу" +"disableFallbackIfMatchDesc" = "Вимкнути резервні DNS-запити при збігу списку доменів DNS-сервера" +"strategy" = "Стратегія запиту" +"strategyDesc" = "Загальна стратегія вирішення доменних імен" +"add" = "Додати сервер" +"edit" = "Редагувати сервер" +"domains" = "Домени" +"expectIPs" = "Очікувані IP" +"unexpectIPs" = "Неочікувані IP" +"useSystemHosts" = "Використовувати системні Hosts" +"useSystemHostsDesc" = "Використовувати файл hosts з встановленої системи" +"usePreset" = "Використати шаблон" +"dnsPresetTitle" = "Шаблони DNS" +"dnsPresetFamily" = "Сімейний" + +[pages.xray.fakedns] +"add" = "Додати підроблений DNS" +"edit" = "Редагувати підроблений DNS" +"ipPool" = "Підмережа IP-пулу" +"poolSize" = "Розмір пулу" + +[pages.settings.security] +"admin" = "Облікові дані адміністратора" +"twoFactor" = "Двофакторна аутентифікація" +"twoFactorEnable" = "Увімкнути 2FA" +"twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки." +"twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію" +"twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію" +"twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:" +"twoFactorModalFirstStep" = "1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму" +"twoFactorModalSecondStep" = "2. Введіть код з програми" +"twoFactorModalRemoveStep" = "Введіть код з програми, щоб вимкнути двофакторну аутентифікацію." +"twoFactorModalChangeCredentialsTitle" = "Змінити облікові дані" +"twoFactorModalChangeCredentialsStep" = "Введіть код з додатку, щоб змінити облікові дані адміністратора." +"twoFactorModalSetSuccess" = "Двофакторна аутентифікація була успішно встановлена" +"twoFactorModalDeleteSuccess" = "Двофакторна аутентифікація була успішно видалена" +"twoFactorModalError" = "Невірний код" + +[pages.settings.toasts] +"modifySettings" = "Параметри було змінено." +"getSettings" = "Виникла помилка під час отримання параметрів." +"modifyUserError" = "Виникла помилка під час зміни облікових даних адміністратора." +"modifyUser" = "Ви успішно змінили облікові дані адміністратора." +"originalUserPassIncorrect" = "Поточне ім'я користувача або пароль недійсні" +"userPassMustBeNotEmpty" = "Нове ім'я користувача та пароль порожні" +"getOutboundTrafficError" = "Помилка отримання вихідного трафіку" +"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку" + +[tgbot] +"keyboardClosed" = "❌ Клавіатуру закрито!" +"noResult" = "❗ Немає результату!" +"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!" +"wentWrong" = "❌ Щось пішло не так!" +"noIpRecord" = "❗ Немає запису IP!" +"noInbounds" = "❗ Вхідні не знайдені!" +"unlimited" = "♾ Необмежено (Скинути)" +"add" = "Додати" +"month" = "Місяць" +"months" = "Місяці" +"day" = "День" +"days" = "Дні" +"hours" = "Години" +"minutes" = "Хвилини" +"unknown" = "Невідомо" +"inbounds" = "Вхідні" +"clients" = "Клієнти" +"offline" = "🔴 Офлайн" +"online" = "🟢 Онлайн" + +[tgbot.commands] +"unknown" = "❗ Невідома команда." +"pleaseChoose" = "👇 Будь ласка, виберіть:\r\n" +"help" = "🤖 Ласкаво просимо до цього бота! Він розроблений, щоб надавати певні дані з веб-панелі та дозволяє вносити зміни за потреби.\r\n\r\n" +"start" = "👋 Привіт {{ .Firstname }}.\r\n" +"welcome" = "🤖 Ласкаво просимо до {{ .Hostname }} бота керування.\r\n" +"status" = "✅ Бот в порядку!" +"usage" = "❗ Введіть текст для пошуку!" +"getID" = "🆔 Ваш ідентифікатор: {{ .ID }}" +"helpAdminCommands" = "Для перезапуску Xray Core:\r\n/restart\r\n\r\nДля пошуку електронної пошти клієнта:\r\n/usage [Електронна пошта]\r\n\r\nДля пошуку вхідних (зі статистикою клієнта):\r\n/inbound [Примітка]\r\n\r\nID чату Telegram:\r\n/id" +"helpClientCommands" = "Для пошуку статистики використовуйте наступну команду:\r\n/usage [Електронна пошта]\r\n\r\nID чату Telegram:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ Операція успішна!" +"restartFailed" = "❗ Помилка в операції.\r\n\r\nПомилка: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core не запущений." +"startDesc" = "Показати головне меню" +"helpDesc" = "Довідка по боту" +"statusDesc" = "Перевірити статус бота" +"idDesc" = "Показати ваш Telegram ID" + +[tgbot.messages] +"cpuThreshold" = "🔴 Навантаження ЦП {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%" +"selectUserFailed" = "❌ Помилка під час вибору користувача!" +"userSaved" = "✅ Користувача Telegram збережено." +"loginSuccess" = "✅ Успішно ввійшли в панель\r\n" +"loginFailed" = "❗️ Помилка входу в панель.\r\n" +"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n" +"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n" +"hostname" = "💻 Хост: {{ .Hostname }}\r\n" +"version" = "🚀 X-Panel Версія: {{ .Version }}\r\n" +"xrayVersion" = "📡 Xray Версія: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IP-адреси:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Трафік: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Статус: {{ .State }}\r\n" +"username" = "👤 Ім'я користувача: {{ .Username }}\r\n" +"password" = "👤 Пароль: {{ .Password }}\r\n" +"time" = "⏰ Час: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Порт: {{ .Port }}\r\n" +"expire" = "📅 Дата закінчення: {{ .Time }}\r\n" +"expireIn" = "📅 Термін дії: {{ .Time }}\r\n" +"active" = "💡 Активний: {{ .Enable }}\r\n" +"enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n" +"online" = "🌐 Стан підключення: {{ .Status }}\r\n" +"email" = "📧 Електронна пошта: {{ .Email }}\r\n" +"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" +"download" = "🔽 Download: ↓{{ .Download }}\r\n" +"total" = "📊 Всього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Користувач Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Вичерпано {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Вичерпано кількість {{ .Type }} count:\r\n" +"onlinesCount" = "🌐 Онлайн-клієнти: {{ .Count }}\r\n" +"disabled" = "🛑 Вимкнено: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Скоро вичерпається: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Час резервного копіювання: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n" +"yes" = "✅ Так" +"no" = "❌ Ні" +"received_id" = "🔑📥 ID оновлено." +"received_password" = "🔑📥 Пароль оновлено." +"received_email" = "📧📥 Електронна пошта оновлена." +"received_comment" = "💬📥 Коментар оновлено." +"id_prompt" = "🔑 Стандартний ID: {{ .ClientId }}\n\nВведіть ваш ID." +"pass_prompt" = "🔑 Стандартний пароль: {{ .ClientPassword }}\n\nВведіть ваш пароль." +"email_prompt" = "📧 Стандартний email: {{ .ClientEmail }}\n\nВведіть ваш email." +"comment_prompt" = "💬 Стандартний коментар: {{ .ClientComment }}\n\nВведіть ваш коментар." +"inbound_client_data_id" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!" +"inbound_client_data_pass" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!" +"cancel" = "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄" +"error_add_client" = "⚠️ Помилка:\n\n {{ .error }}" +"using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊" +"incorrect_input" ="Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫" +"AreYouSure" = "Ви впевнені? 🤔" +"SuccessResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно" +"FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠️ Помилка: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Процес скидання трафіку завершено для всіх клієнтів." + +[tgbot.buttons] +"closeKeyboard" = "❌ Закрити клавіатуру" +"cancel" = "❌ Скасувати" +"cancelReset" = "❌ Скасувати скидання" +"cancelIpLimit" = "❌ Скасувати обмеження IP" +"confirmResetTraffic" = "✅ Підтвердити скидання трафіку?" +"confirmClearIps" = "✅ Підтвердити очищення IP-адрес?" +"confirmRemoveTGUser" = "✅ Підтвердити видалення користувача Telegram?" +"confirmToggle" = "✅ Підтвердити ввімкнути/вимкнути користувача?" +"dbBackup" = "Отримати резервну копію БД" +"serverUsage" = "Використання сервера" +"getInbounds" = "Отримати вхідні" +"depleteSoon" = "Скоро вичерпати" +"clientUsage" = "Отримати використання" +"onlines" = "Онлайн-клієнти" +"commands" = "Команди" +"refresh" = "🔄 Оновити" +"clearIPs" = "❌ Очистити IP-адреси" +"removeTGUser" = "❌ Видалити користувача Telegram" +"selectTGUser" = "👤 Виберіть користувача Telegram" +"selectOneTGUser" = "👤 Виберіть користувача Telegram:" +"resetTraffic" = "📈 Скинути трафік" +"resetExpire" = "📅 Змінити термін дії" +"ipLog" = "🔢 IP журнал" +"ipLimit" = "🔢 IP Ліміт" +"setTGUser" = "👤 Встановити користувача Telegram" +"toggle" = "🔘 Увімкнути / Вимкнути" +"custom" = "🔢 Custom" +"confirmNumber" = "✅ Підтвердити: {{ .Num }}" +"confirmNumberAdd" = "✅ Підтвердити додавання: {{ .Num }}" +"limitTraffic" = "🚧 Ліміт трафіку" +"getBanLogs" = "Отримати журнали заборон" +"allClients" = "Всі Клієнти" +"addClient" = "Додати клієнта" +"submitDisable" = "Надіслати як вимкнено ☑️" +"submitEnable" = "Надіслати як увімкнено ✅" +"use_default" = "🏷️ Використати типове" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Пароль" +"change_email" = "⚙️📧 Електронна пошта" +"change_comment" = "⚙️💬 Коментар" +"ResetAllTraffics" = "Скинути весь трафік" +"SortedTrafficUsageReport" = "Відсортований звіт про використання трафіку" + +[tgbot.answers] +"successfulOperation" = "✅ Операція успішна!" +"errorOperation" = "❗ Помилка в роботі." +"getInboundsFailed" = "❌ Не вдалося отримати вхідні повідомлення." +"getClientsFailed" = "❌ Не вдалося отримати клієнтів." +"canceled" = "❌ {{ .Email }}: Операцію скасовано." +"clientRefreshSuccess" = "✅ {{ .Email }}: Клієнт успішно оновлено." +"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреси успішно оновлено." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Користувач Telegram клієнта успішно оновлено." +"resetTrafficSuccess" = "✅ {{ .Email }}: Трафік скинуто успішно." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Ліміт трафіку успішно збережено." +"expireResetSuccess" = "✅ {{ .Email }}: Успішно скинуто дні закінчення терміну дії." +"resetIpSuccess" = "✅ {{ .Email }}: IP обмеження {{ .Count }} успішно збережено." +"clearIpSuccess" = "✅ {{ .Email }}: IP успішно очищено." +"getIpLog" = "✅ {{ .Email }}: Отримати IP-журнал." +"getUserInfo" = "✅ {{ .Email }}: Отримати інформацію про користувача Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }}: Користувача Telegram видалено успішно." +"enableSuccess" = "✅ {{ .Email }}: Увімкнути успішно." +"disableSuccess" = "✅ {{ .Email }}: Успішно вимкнено." +"askToAddUserId" = "Вашу конфігурацію не знайдено!\r\nБудь ласка, попросіть свого адміністратора використовувати ваш ідентифікатор Telegram у вашій конфігурації.\r\n\r\nВаш ідентифікатор користувача: {{ .TgUserID }}" +"chooseClient" = "Виберіть клієнта для Вхідного {{ .Inbound }}" +"chooseInbound" = "Виберіть Вхідний" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml new file mode 100644 index 0000000..c04f864 --- /dev/null +++ b/web/translation/translate.vi_VN.toml @@ -0,0 +1,740 @@ +"username" = "Tên người dùng" +"password" = "Mật khẩu" +"login" = "Đăng nhập" +"confirm" = "Xác nhận" +"cancel" = "Hủy bỏ" +"close" = "Đóng" +"create" = "Tạo" +"update" = "Cập nhật" +"copy" = "Sao chép" +"copied" = "Đã sao chép" +"download" = "Tải xuống" +"remark" = "Ghi chú" +"enable" = "Kích hoạt" +"protocol" = "Giao thức" +"search" = "Tìm kiếm" +"filter" = "Bộ lọc" +"loading" = "Đang tải" +"second" = "Giây" +"minute" = "Phút" +"hour" = "Giờ" +"day" = "Ngày" +"check" = "Kiểm tra" +"indefinite" = "Không xác định" +"unlimited" = "Không giới hạn" +"none" = "None" +"qrCode" = "Mã QR" +"info" = "Thông tin thêm" +"edit" = "Chỉnh sửa" +"delete" = "Xóa" +"reset" = "Đặt lại" +"noData" = "Không có dữ liệu." +"copySuccess" = "Đã sao chép thành công" +"sure" = "Chắc chắn" +"encryption" = "Mã hóa" +"useIPv4ForHost" = "Sử dụng IPv4 cho máy chủ" +"transmission" = "Truyền tải" +"host" = "Máy chủ" +"path" = "Đường dẫn" +"camouflage" = "Ngụy trang" +"status" = "Trạng thái" +"enabled" = "Đã kích hoạt" +"disabled" = "Đã tắt" +"depleted" = "Depleted" +"depletingSoon" = "Depleting..." +"offline" = "Ngoại tuyến" +"online" = "Trực tuyến" +"domainName" = "Tên miền" +"monitor" = "Listening IP" +"certificate" = "Chứng chỉ số" +"fail" = "Thất bại" +"comment" = "Bình luận" +"success" = "Thành công" +"lastOnline" = "Lần online gần nhất" +"getVersion" = "Lấy phiên bản" +"install" = "Cài đặt" +"clients" = "Các khách hàng" +"usage" = "Sử dụng" +"twoFactorCode" = "Mã" +"remained" = "Còn lại" +"security" = "Bảo vệ" +"secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7" +"secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn" +"secAlertConf" = "Một số cài đặt có thể dễ bị tấn công. Đề xuất tăng cường các giao thức bảo mật để ngăn chặn các vi phạm tiềm ẩn." +"secAlertSSL" = "Bảng điều khiển thiếu kết nối an toàn. Vui lòng cài đặt chứng chỉ TLS để bảo vệ dữ liệu." +"secAlertPanelPort" = "Cổng mặc định của bảng điều khiển có thể dễ bị tấn công. Vui lòng cấu hình một cổng ngẫu nhiên hoặc cụ thể." +"secAlertPanelURI" = "Đường dẫn URI mặc định của bảng điều khiển không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp." +"secAlertSubURI" = "Đường dẫn URI mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp." +"secAlertSubJsonURI" = "Đường dẫn URI JSON mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp." +"emptyDnsDesc" = "Không có máy chủ DNS nào được thêm." +"emptyFakeDnsDesc" = "Không có máy chủ Fake DNS nào được thêm." +"emptyBalancersDesc" = "Không có bộ cân bằng tải nào được thêm." +"emptyReverseDesc" = "Không có proxy ngược nào được thêm." +"somethingWentWrong" = "Đã xảy ra lỗi" + +[menu] +"theme" = "Chủ đề" +"dark" = "Tối" +"ultraDark" = "Siêu tối" +"dashboard" = "Trạng thái hệ thống" +"inbounds" = "Đầu vào khách hàng" +"settings" = "Cài đặt bảng điều khiển" +"logout" = "Đăng xuất" +"xray" = "Cài đặt Xray" +"link" = "Quản lý" + +[pages.login] +"hello" = "Xin chào" +"title" = "Chào mừng" +"loginAgain" = "Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại." + +[pages.login.toasts] +"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ." +"emptyUsername" = "Vui lòng nhập tên người dùng." +"emptyPassword" = "Vui lòng nhập mật khẩu." +"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ." +"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công." + +[pages.index] +"title" = "Trạng thái hệ thống" +"cpu" = "CPU" +"logicalProcessors" = "Bộ xử lý logic" +"frequency" = "Tần số" +"swap" = "Swap" +"storage" = "Lưu trữ" +"memory" = "RAM" +"threads" = "Luồng" +"xrayStatus" = "Xray" +"stopXray" = "Dừng lại" +"restartXray" = "Khởi động lại" +"xraySwitch" = "Phiên bản" +"xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang." +"xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại." +"xrayStatusUnknown" = "Không xác định" +"xrayStatusRunning" = "Đang chạy" +"xrayStatusStop" = "Dừng" +"xrayStatusError" = "Lỗi" +"xrayErrorPopoverTitle" = "Đã xảy ra lỗi khi chạy Xray" +"operationHours" = "Thời gian hoạt động" +"systemLoad" = "Tải hệ thống" +"systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua" +"connectionCount" = "Số lượng kết nối" +"ipAddresses" = "Địa chỉ IP" +"toggleIpVisibility" = "Chuyển đổi hiển thị IP" +"overallSpeed" = "Tốc độ tổng thể" +"upload" = "Tải lên" +"download" = "Tải xuống" +"totalData" = "Tổng dữ liệu" +"sent" = "Đã gửi" +"received" = "Đã nhận" +"documentation" = "Tài liệu" +"xraySwitchVersionDialog" = "Bạn có chắc chắn muốn thay đổi phiên bản Xray không?" +"xraySwitchVersionDialogDesc" = "Hành động này sẽ thay đổi phiên bản Xray thành #version#." +"xraySwitchVersionPopover" = "Xray đã được cập nhật thành công" +"geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?" +"geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#." +"geofilesUpdateDialogDesc" = "Thao tác này sẽ cập nhật tất cả các tập tin." +"geofilesUpdateAll" = "Cập nhật tất cả" +"geofileUpdatePopover" = "Geofile đã được cập nhật thành công" +"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." +"logs" = "Nhật ký" +"config" = "Cấu hình" +"backup" = "Sao lưu" +"backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu" +"exportDatabase" = "Sao lưu" +"exportDatabaseDesc" = "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị." +"importDatabase" = "Khôi phục" +"importDatabaseDesc" = "Nhấp để chọn và tải lên tệp .db từ thiết bị của bạn để khôi phục cơ sở dữ liệu từ bản sao lưu." +"importDatabaseSuccess" = "Đã nhập cơ sở dữ liệu thành công" +"importDatabaseError" = "Lỗi xảy ra khi nhập cơ sở dữ liệu" +"readDatabaseError" = "Lỗi xảy ra khi đọc cơ sở dữ liệu" +"getDatabaseError" = "Lỗi xảy ra khi truy xuất cơ sở dữ liệu" +"getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình" + +[pages.inbounds] +"allTimeTraffic" = "Tổng Lưu Lượng" +"allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc" +"title" = "Điểm vào (Inbounds)" +"totalDownUp" = "Tổng tải lên/tải xuống" +"totalUsage" = "Tổng sử dụng" +"inboundCount" = "Số lượng điểm vào" +"operate" = "Thao tác" +"enable" = "Kích hoạt" +"remark" = "Chú thích" +"protocol" = "Giao thức" +"port" = "Cổng" +"portMap" = "Cổng tạo" +"traffic" = "Lưu lượng" +"details" = "Chi tiết" +"transportConfig" = "Giao vận" +"expireDate" = "Ngày hết hạn" +"createdAt" = "Tạo lúc" +"updatedAt" = "Cập nhật" +"resetTraffic" = "Đặt lại lưu lượng" +"addInbound" = "Thêm điểm vào" +"generalActions" = "Hành động chung" +"autoRefresh" = "Tự động làm mới" +"autoRefreshInterval" = "Khoảng thời gian" +"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)" +"deleteInbound" = "Xóa điểm vào (Inbound)" +"deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)" +"deleteClient" = "Xóa người dùng" +"deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?" +"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?" +"copyLink" = "Sao chép liên kết" +"address" = "Địa chỉ" +"network" = "Mạng" +"destinationPort" = "Cổng đích" +"targetAddress" = "Địa chỉ mục tiêu" +"monitorDesc" = "Mặc định để trống" +"meansNoLimit" = "= Không giới hạn (đơn vị: GB)" +"totalFlow" = "Tổng lưu lượng" +"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn" +"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định" +"certificatePath" = "Đường dẫn tập" +"certificateContent" = "Nội dung tập" +"publicKey" = "Khóa công khai" +"privatekey" = "Khóa cá nhân" +"clickOnQRcode" = "Nhấn vào Mã QR để sao chép" +"client" = "Người dùng" +"export" = "Xuất liên kết" +"clone" = "Sao chép" +"cloneInbound" = "Sao chép điểm vào (Inbound)" +"cloneInboundContent" = "Tất cả cài đặt của điểm vào này, trừ Cổng, IP nghe và máy khách, sẽ được áp dụng cho bản sao." +"cloneInboundOk" = "Sao chép" +"resetAllTraffic" = "Đặt lại lưu lượng cho tất cả điểm vào" +"resetAllTrafficTitle" = "Đặt lại lưu lượng cho tất cả điểm vào" +"resetAllTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng cho tất cả điểm vào không?" +"resetInboundClientTraffics" = "Đặt lại lưu lượng toàn bộ người dùng của điểm vào" +"resetInboundClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng của điểm vào" +"resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các người dùng của điểm vào này không?" +"resetAllClientTraffics" = "Đặt lại lưu lượng cho toàn bộ người dùng" +"resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng" +"resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho toàn bộ người dùng không?" +"delDepletedClients" = "Xóa các người dùng đã cạn kiệt" +"delDepletedClientsTitle" = "Xóa các người dùng đã cạn kiệt" +"delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa toàn bộ người dùng đã cạn kiệt không?" +"email" = "Email" +"emailDesc" = "Vui lòng cung cấp một địa chỉ email duy nhất." +"IPLimit" = "Giới hạn IP" +"IPLimitDesc" = "Vô hiệu hóa điểm vào nếu số lượng vượt quá giá trị đã nhập (nhập 0 để vô hiệu hóa giới hạn IP)." +"IPLimitlog" = "Lịch sử IP" +"IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)." +"IPLimitlogclear" = "Xóa Lịch sử" +"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển" +"telegramDesc" = "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)" +"subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau" +"info" = "Thông tin" +"same" = "Giống nhau" +"inboundData" = "Dữ liệu gửi đến" +"exportInbound" = "Xuất nhập khẩu" +"import" = "Nhập" +"importInbound" = "Nhập inbound" + +[pages.client] +"add" = "Thêm người dùng" +"edit" = "Chỉnh sửa người dùng" +"submitAdd" = "Thêm" +"submitEdit" = "Lưu thay đổi" +"clientCount" = "Số lượng người dùng" +"bulk" = "Thêm hàng loạt" +"method" = "Phương pháp" +"first" = "Đầu tiên" +"last" = "Cuối cùng" +"prefix" = "Tiền tố" +"postfix" = "Hậu tố" +"delayedStart" = "Bắt đầu ở Lần Đầu" +"expireDays" = "Khoảng thời gian" +"days" = "ngày" +"renew" = "Tự động gia hạn" +"renewDesc" = "Tự động gia hạn sau khi hết hạn. (0 = tắt)(đơn vị: ngày)" + +[pages.inbounds.toasts] +"obtain" = "Nhận" +"updateSuccess" = "Cập nhật thành công" +"logCleanSuccess" = "Đã xóa nhật ký" +"inboundsUpdateSuccess" = "Đã cập nhật thành công các kết nối inbound" +"inboundUpdateSuccess" = "Đã cập nhật thành công kết nối inbound" +"inboundCreateSuccess" = "Đã tạo thành công kết nối inbound" +"inboundDeleteSuccess" = "Đã xóa thành công kết nối inbound" +"inboundClientAddSuccess" = "Đã thêm client inbound" +"inboundClientDeleteSuccess" = "Đã xóa client inbound" +"inboundClientUpdateSuccess" = "Đã cập nhật client inbound" +"delDepletedClientsSuccess" = "Đã xóa tất cả client hết hạn" +"resetAllClientTrafficSuccess" = "Đã đặt lại toàn bộ lưu lượng client" +"resetAllTrafficSuccess" = "Đã đặt lại toàn bộ lưu lượng" +"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng" +"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng" +"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519." +"getNewmldsa65Error" = "Lỗi khi lấy chúng tôi mldsa65." + +[pages.inbounds.stream.general] +"request" = "Lời yêu cầu" +"response" = "Phản ứng" +"name" = "Tên" +"value" = "Giá trị" + +[pages.inbounds.stream.tcp] +"version" = "Phiên bản" +"method" = "Phương pháp" +"path" = "Đường dẫn" +"status" = "Trạng thái" +"statusDescription" = "Tình trạng Mô tả" +"requestHeader" = "Header yêu cầu" +"responseHeader" = "Header phản hồi" + +[pages.settings] +"title" = "Cài đặt" +"save" = "Lưu" +"infoDesc" = "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi." +"restartPanel" = "Khởi động lại bảng điều khiển" +"restartPanelDesc" = "Bạn có chắc chắn muốn khởi động lại bảng điều khiển? Nhấn OK để khởi động lại sau 3 giây. Nếu bạn không thể truy cập bảng điều khiển sau khi khởi động lại, vui lòng xem thông tin nhật ký của bảng điều khiển trên máy chủ." +"restartPanelSuccess" = "Đã khởi động lại bảng điều khiển thành công" +"actions" = "Hành động" +"resetDefaultConfig" = "Đặt lại cấu hình mặc định" +"panelSettings" = "Bảng điều khiển" +"securitySettings" = "Bảo mật" +"TGBotSettings" = "Bot Telegram" +"panelListeningIP" = "IP Nghe của bảng điều khiển" +"panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP." +"panelListeningDomain" = "Tên miền của nghe bảng điều khiển" +"panelListeningDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP" +"panelPort" = "Cổng bảng điều khiển" +"panelPortDesc" = "Cổng được sử dụng để kết nối với bảng điều khiển này" +"publicKeyPath" = "Đường dẫn file chứng chỉ bảng điều khiển" +"publicKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')" +"privateKeyPath" = "Đường dẫn file khóa của chứng chỉ bảng điều khiển" +"privateKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')" +"panelUrlPath" = "Đường dẫn gốc URL bảng điều khiển" +"panelUrlPathDesc" = "Phải bắt đầu và kết thúc bằng '/'" +"pageSize" = "Kích thước phân trang" +"pageSizeDesc" = "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt" +"remarkModel" = "Ghi chú mô hình và ký tự phân tách" +"datepicker" = "Kiểu lịch" +"datepickerPlaceholder" = "Chọn ngày" +"datepickerDescription" = "Tác vụ chạy theo lịch trình sẽ chạy theo kiểu lịch này." +"sampleRemark" = "Nhận xét mẫu" +"oldUsername" = "Tên người dùng hiện tại" +"currentPassword" = "Mật khẩu hiện tại" +"newUsername" = "Tên người dùng mới" +"newPassword" = "Mật khẩu mới" +"telegramBotEnable" = "Bật Bot Telegram" +"telegramBotEnableDesc" = "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram" +"telegramToken" = "Token Telegram" +"telegramTokenDesc" = "Bạn phải nhận token từ quản lý bot Telegram @botfather" +"telegramProxy" = "Socks5 Proxy" +"telegramProxyDesc" = "Nếu bạn cần socks5 proxy để kết nối với Telegram. Điều chỉnh cài đặt của nó theo hướng dẫn." +"telegramAPIServer" = "Telegram API Server" +"telegramAPIServerDesc" = "Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định." +"telegramChatId" = "Chat ID Telegram của quản trị viên" +"telegramChatIdDesc" = "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng @userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn." +"telegramNotifyTime" = "Thời gian thông báo của bot Telegram" +"telegramNotifyTimeDesc" = "Sử dụng định dạng thời gian Crontab." +"tgNotifyBackup" = "Sao lưu Cơ sở dữ liệu" +"tgNotifyBackupDesc" = "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo." +"tgNotifyLogin" = "Thông báo Đăng nhập" +"tgNotifyLoginDesc" = "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn." +"sessionMaxAge" = "Thời gian tối đa của phiên" +"sessionMaxAgeDesc" = "Thời gian của phiên đăng nhập (đơn vị: phút)" +"expireTimeDiff" = "Ngưỡng hết hạn cho thông báo" +"expireTimeDiffDesc" = "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)" +"trafficDiff" = "Ngưỡng lưu lượng cho thông báo" +"trafficDiffDesc" = "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)" +"tgNotifyCpu" = "Ngưỡng cảnh báo tỷ lệ CPU" +"tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)" +"timeZone" = "Múi giờ" +"timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này." +"subSettings" = "Gói đăng ký" +"subEnable" = "Bật dịch vụ" +"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng" +"subTitle" = "Tiêu đề Đăng ký" +"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN" +"subListen" = "Listening IP" +"subListenDesc" = "Mặc định để trống để nghe tất cả các IP" +"subPort" = "Cổng gói đăng ký" +"subPortDesc" = "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ" +"subCertPath" = "Đường dẫn file chứng chỉ gói đăng ký" +"subCertPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')" +"subKeyPath" = "Đường dẫn file khóa của chứng chỉ gói đăng ký" +"subKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')" +"subPath" = "Đường dẫn gốc URL gói đăng ký" +"subPathDesc" = "Phải bắt đầu và kết thúc bằng '/'" +"subDomain" = "Tên miền con" +"subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP" +"subUpdates" = "Khoảng thời gian cập nhật gói đăng ký" +"subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách" +"subEncrypt" = "Mã hóa cấu hình" +"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong gói đăng ký" +"subShowInfo" = "Hiển thị thông tin sử dụng" +"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình" +"subURI" = "URI proxy trung gian" +"subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian" +"externalTrafficInformEnable" = "Thông báo giao thông bên ngoài" +"externalTrafficInformEnableDesc" = "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập." +"externalTrafficInformURI" = "URI thông báo lưu lượng truy cập bên ngoài" +"externalTrafficInformURIDesc" = "Cập nhật lưu lượng truy cập được gửi tới URI này." +"fragment" = "Sự phân mảnh" +"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello" +"fragmentSett" = "Cài đặt phân mảnh" +"noisesDesc" = "Bật Noises." +"noisesSett" = "Cài đặt Noises" +"mux" = "Mux" +"muxDesc" = "Truyền nhiều luồng dữ liệu độc lập trong luồng dữ liệu đã thiết lập." +"muxSett" = "Mux Cài đặt" +"direct" = "Kết nối trực tiếp" +"directDesc" = "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể." +"notifications" = "Thông báo" +"certs" = "Chứng chỉ" +"externalTraffic" = "Lưu lượng bên ngoài" +"dateAndTime" = "Ngày và giờ" +"proxyAndServer" = "Proxy và máy chủ" +"intervals" = "Khoảng thời gian" +"information" = "Thông tin" +"language" = "Ngôn ngữ" +"telegramBotLanguage" = "Ngôn ngữ của Bot Telegram" + +[pages.xray] +"title" = "Cài đặt Xray" +"save" = "Lưu cài đặt" +"restart" = "Khởi động lại Xray" +"restartSuccess" = "Đã khởi động lại Xray thành công" +"stopSuccess" = "Xray đã được dừng thành công" +"restartError" = "Đã xảy ra lỗi khi khởi động lại Xray." +"stopError" = "Đã xảy ra lỗi khi dừng Xray." +"basicTemplate" = "Mẫu Cơ bản" +"advancedTemplate" = "Mẫu Nâng cao" +"generalConfigs" = "Cấu hình Chung" +"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát." +"logConfigs" = "Nhật ký" +"logConfigsDesc" = "Nhật ký có thể ảnh hưởng đến hiệu suất máy chủ của bạn. Bạn chỉ nên kích hoạt nó một cách khôn ngoan trong trường hợp bạn cần" +"blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể." +"basicRouting" = "Định tuyến Cơ bản" +"blockConnectionsConfigsDesc" = "Các tùy chọn này sẽ chặn lưu lượng truy cập dựa trên quốc gia được yêu cầu cụ thể." +"directConnectionsConfigsDesc" = "Kết nối trực tiếp đảm bảo rằng lưu lượng truy cập cụ thể không được định tuyến qua máy chủ khác." +"blockips" = "Chặn IP" +"blockdomains" = "Chặn Tên Miền" +"directips" = "IP Trực Tiếp" +"directdomains" = "Tên Miền Trực Tiếp" +"ipv4Routing" = "Định tuyến IPv4" +"ipv4RoutingDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4." +"warpRouting" = "Định tuyến WARP" +"warpRoutingDesc" = "Cảnh báo: Trước khi sử dụng những tùy chọn này, hãy cài đặt WARP ở chế độ proxy socks5 trên máy chủ của bạn bằng cách làm theo các bước trên GitHub của bảng điều khiển. WARP sẽ định tuyến lưu lượng đến các trang web qua máy chủ Cloudflare." +"Template" = "Mẫu Cấu hình Xray" +"TemplateDesc" = "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này." +"FreedomStrategy" = "Cấu hình Chiến lược cho Giao thức Freedom" +"FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom." +"RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền" +"RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS." +"Torrent" = "Cấu hình sử dụng BitTorrent" +"Inbounds" = "Đầu vào" +"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể." +"Outbounds" = "Đầu ra" +"Balancers" = "Cân bằng" +"OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này." +"Routings" = "Quy tắc định tuyến" +"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!" +"completeTemplate" = "All" +"logLevel" = "Mức đăng nhập" +"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại." +"accessLog" = "Nhật ký truy cập" +"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'" +"errorLog" = "Nhật ký lỗi" +"errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'" +"dnsLog" = "Nhật ký DNS" +"dnsLogDesc" = "Có bật nhật ký truy vấn DNS không" +"maskAddress" = "Ẩn Địa Chỉ" +"maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký." +"statistics" = "Thống kê" +"statsInboundUplink" = "Thống kê tải lên đầu vào" +"statsInboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu vào." +"statsInboundDownlink" = "Thống kê tải xuống đầu vào" +"statsInboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu vào." +"statsOutboundUplink" = "Thống kê tải lên đầu ra" +"statsOutboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu ra." +"statsOutboundDownlink" = "Thống kê tải xuống đầu ra" +"statsOutboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu ra." + +[pages.xray.rules] +"first" = "Đầu tiên" +"last" = "Cuối cùng" +"up" = "Lên" +"down" = "Xuống" +"source" = "Nguồn" +"dest" = "Đích" +"inbound" = "Vào" +"outbound" = "Ra" +"balancer" = "Cân bằng" +"info" = "Thông tin" +"add" = "Thêm quy tắc" +"edit" = "Chỉnh sửa quy tắc" +"useComma" = "Các mục được phân tách bằng dấu phẩy" + +[pages.xray.outbound] +"addOutbound" = "Thêm thư đi" +"addReverse" = "Thêm đảo ngược" +"editOutbound" = "Chỉnh sửa gửi đi" +"editReverse" = "Chỉnh sửa ngược lại" +"tag" = "Thẻ" +"tagDesc" = "thẻ duy nhất" +"address" = "Địa chỉ" +"reverse" = "Đảo ngược" +"domain" = "Miền" +"type" = "Loại" +"bridge" = "Cầu" +"portal" = "Cổng thông tin" +"link" = "Liên kết" +"intercon" = "Kết nối" +"settings" = "cài đặt" +"accountInfo" = "Thông tin tài khoản" +"outboundStatus" = "Trạng thái đầu ra" +"sendThrough" = "Gửi qua" + +[pages.xray.balancer] +"addBalancer" = "Thêm cân bằng" +"editBalancer" = "Chỉnh sửa cân bằng" +"balancerStrategy" = "Chiến lược" +"balancerSelectors" = "Bộ chọn" +"tag" = "Thẻ" +"tagDesc" = "thẻ duy nhất" +"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động." + +[pages.xray.wireguard] +"secretKey" = "Khoá bí mật" +"publicKey" = "Khóa công khai" +"allowedIPs" = "IP được phép" +"endpoint" = "Điểm cuối" +"psk" = "Khóa chia sẻ" +"domainStrategy" = "Chiến lược tên miền" + +[pages.xray.dns] +"enable" = "Kích hoạt DNS" +"enableDesc" = "Kích hoạt máy chủ DNS tích hợp" +"tag" = "Thẻ gửi đến DNS" +"tagDesc" = "Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến." +"clientIp" = "IP khách hàng" +"clientIpDesc" = "Được sử dụng để thông báo cho máy chủ về vị trí IP được chỉ định trong các truy vấn DNS" +"disableCache" = "Tắt bộ nhớ đệm" +"disableCacheDesc" = "Tắt bộ nhớ đệm DNS" +"disableFallback" = "Tắt Fallback" +"disableFallbackDesc" = "Tắt các truy vấn DNS Fallback" +"disableFallbackIfMatch" = "Tắt Fallback Nếu Khớp" +"disableFallbackIfMatchDesc" = "Tắt các truy vấn DNS Fallback khi danh sách tên miền khớp của máy chủ DNS được kích hoạt" +"strategy" = "Chiến lược truy vấn" +"strategyDesc" = "Chiến lược tổng thể để phân giải tên miền" +"add" = "Thêm máy chủ" +"edit" = "Chỉnh sửa máy chủ" +"domains" = "Tên miền" +"expectIPs" = "Các IP Dự Kiến" +"unexpectIPs" = "IP không mong muốn" +"useSystemHosts" = "Sử dụng Hosts hệ thống" +"useSystemHostsDesc" = "Sử dụng file hosts từ hệ thống đã cài đặt" +"usePreset" = "Dùng mẫu" +"dnsPresetTitle" = "Mẫu DNS" +"dnsPresetFamily" = "Gia đình" + +[pages.xray.fakedns] +"add" = "Thêm DNS giả" +"edit" = "Chỉnh sửa DNS giả" +"ipPool" = "Mạng con nhóm IP" +"poolSize" = "Kích thước bể bơi" + +[pages.settings.security] +"admin" = "Thông tin đăng nhập quản trị viên" +"twoFactor" = "Xác thực hai yếu tố" +"twoFactorEnable" = "Bật 2FA" +"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn." +"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố" +"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố" +"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:" +"twoFactorModalFirstStep" = "1. Quét mã QR này trong ứng dụng xác thực hoặc sao chép mã token gần mã QR và dán vào ứng dụng" +"twoFactorModalSecondStep" = "2. Nhập mã từ ứng dụng" +"twoFactorModalRemoveStep" = "Nhập mã từ ứng dụng để xóa xác thực hai yếu tố." +"twoFactorModalChangeCredentialsTitle" = "Thay đổi thông tin xác thực" +"twoFactorModalChangeCredentialsStep" = "Nhập mã từ ứng dụng để thay đổi thông tin xác thực quản trị viên." +"twoFactorModalSetSuccess" = "Xác thực hai yếu tố đã được thiết lập thành công" +"twoFactorModalDeleteSuccess" = "Xác thực hai yếu tố đã được xóa thành công" +"twoFactorModalError" = "Mã sai" + +[pages.settings.toasts] +"modifySettings" = "Các tham số đã được thay đổi." +"getSettings" = "Lỗi xảy ra khi truy xuất tham số." +"modifyUserError" = "Đã xảy ra lỗi khi thay đổi thông tin đăng nhập quản trị viên." +"modifyUser" = "Bạn đã thay đổi thông tin đăng nhập quản trị viên thành công." +"originalUserPassIncorrect" = "Tên người dùng hoặc mật khẩu gốc không đúng" +"userPassMustBeNotEmpty" = "Tên người dùng mới và mật khẩu mới không thể để trống" +"getOutboundTrafficError" = "Lỗi khi lấy lưu lượng truy cập đi" +"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi" + +[tgbot] +"keyboardClosed" = "❌ Bàn phím đã đóng!" +"noResult" = "❗ Không có kết quả!" +"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!" +"wentWrong" = "❌ Đã xảy ra lỗi!" +"noIpRecord" = "❗ Không có bản ghi IP!" +"noInbounds" = "❗ Không tìm thấy inbound!" +"unlimited" = "♾ Không giới hạn (Đặt lại)" +"add" = "Thêm" +"month" = "Tháng" +"months" = "Tháng" +"day" = "Ngày" +"days" = "Ngày" +"hours" = "Giờ" +"minutes" = "Phút" +"unknown" = "Không xác định" +"inbounds" = "Inbound" +"clients" = "Client" +"offline" = "🔴 Ngoại tuyến" +"online" = "🟢 Trực tuyến" + +[tgbot.commands] +"unknown" = "❗ Lệnh không rõ" +"pleaseChoose" = "👇 Vui lòng chọn:\r\n" +"help" = "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n" +"start" = "👋 Xin chào {{ .Firstname }}.\r\n" +"welcome" = "🤖 Chào mừng đến với bot quản lý của {{ .Hostname }}.\r\n" +"status" = "✅ Bot hoạt động bình thường!" +"usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!" +"getID" = "🆔 ID của bạn: {{ .ID }}" +"helpAdminCommands" = "Để khởi động lại Xray Core:\r\n/restart\r\n\r\nĐể tìm kiếm email của khách hàng:\r\n/usage [Email]\r\n\r\nĐể tìm kiếm các nhập (với số liệu thống kê của khách hàng):\r\n/inbound [Ghi chú]\r\n\r\nID Trò chuyện Telegram:\r\n/id" +"helpClientCommands" = "Để tìm kiếm thống kê, sử dụng lệnh sau:\r\n/usage [Email]\r\n\r\nID Trò chuyện Telegram:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ Hoạt động thành công!" +"restartFailed" = "❗ Lỗi trong quá trình hoạt động.\r\n\r\nLỗi: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core không chạy." +"startDesc" = "Hiển thị menu chính" +"helpDesc" = "Trợ giúp bot" +"statusDesc" = "Kiểm tra trạng thái bot" +"idDesc" = "Hiển thị ID Telegram của bạn" + +[tgbot.messages] +"cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%" +"selectUserFailed" = "❌ Lỗi khi chọn người dùng!" +"userSaved" = "✅ Người dùng Telegram đã được lưu." +"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n" +"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n" +"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n" +"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n" +"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n" +"version" = "🚀 Phiên bản X-Panel: {{ .Version }}\r\n" +"xrayVersion" = "📡 Phiên bản Xray: {{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 Các IP:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 Số lượng kết nối TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 Số lượng kết nối UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Trạng thái Xray: {{ .State }}\r\n" +"username" = "👤 Tên người dùng: {{ .Username }}\r\n" +"password" = "👤 Mật khẩu: {{ .Password }}\r\n" +"time" = "⏰ Thời gian: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Cổng: {{ .Port }}\r\n" +"expire" = "📅 Ngày hết hạn: {{ .Time }}\r\n" +"expireIn" = "📅 Hết hạn sau: {{ .Time }}\r\n" +"active" = "💡 Đang hoạt động: {{ .Enable }}\r\n" +"enabled" = "🚨 Đã bật: {{ .Enable }}\r\n" +"online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n" +"download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n" +"total" = "📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Người dùng Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Sự cạn kiệt {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Số lần cạn kiệt {{ .Type }}:\r\n" +"onlinesCount" = "🌐 Khách hàng trực tuyến: {{ .Count }}\r\n" +"disabled" = "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n" +"yes" = "✅ Có" +"no" = "❌ Không" +"received_id" = "🔑📥 ID đã được cập nhật." +"received_password" = "🔑📥 Mật khẩu đã được cập nhật." +"received_email" = "📧📥 Email đã được cập nhật." +"received_comment" = "💬📥 Bình luận đã được cập nhật." +"id_prompt" = "🔑 ID mặc định: {{ .ClientId }}\n\nVui lòng nhập ID của bạn." +"pass_prompt" = "🔑 Mật khẩu mặc định: {{ .ClientPassword }}\n\nVui lòng nhập mật khẩu của bạn." +"email_prompt" = "📧 Email mặc định: {{ .ClientEmail }}\n\nVui lòng nhập email của bạn." +"comment_prompt" = "💬 Bình luận mặc định: {{ .ClientComment }}\n\nVui lòng nhập bình luận của bạn." +"inbound_client_data_id" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!" +"inbound_client_data_pass" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 Mật khẩu: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!" +"cancel" = "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄" +"error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}" +"using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊" +"incorrect_input" ="Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫" +"AreYouSure" = "Bạn có chắc không? 🤔" +"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công" +"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠️ Lỗi: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng." + +[tgbot.buttons] +"closeKeyboard" = "❌ Đóng Bàn Phím" +"cancel" = "❌ Hủy" +"cancelReset" = "❌ Hủy Đặt Lại" +"cancelIpLimit" = "❌ Hủy Giới Hạn IP" +"confirmResetTraffic" = "✅ Xác Nhận Đặt Lại Lưu Lượng?" +"confirmClearIps" = "✅ Xác Nhận Xóa Các IP?" +"confirmRemoveTGUser" = "✅ Xác Nhận Xóa Người Dùng Telegram?" +"confirmToggle" = "✅ Xác nhận Bật/Tắt người dùng?" +"dbBackup" = "Tải bản sao lưu cơ sở dữ liệu" +"serverUsage" = "Sử Dụng Máy Chủ" +"getInbounds" = "Lấy cổng vào" +"depleteSoon" = "Depleted Soon" +"clientUsage" = "Lấy Sử Dụng" +"onlines" = "Khách hàng trực tuyến" +"commands" = "Lệnh" +"refresh" = "🔄 Cập Nhật" +"clearIPs" = "❌ Xóa IP" +"removeTGUser" = "❌ Xóa Người Dùng Telegram" +"selectTGUser" = "👤 Chọn Người Dùng Telegram" +"selectOneTGUser" = "👤 Chọn một người dùng telegram:" +"resetTraffic" = "📈 Đặt Lại Lưu Lượng" +"resetExpire" = "📅 Thay đổi ngày hết hạn" +"ipLog" = "🔢 Nhật ký địa chỉ IP" +"ipLimit" = "🔢 Giới Hạn địa chỉ IP" +"setTGUser" = "👤 Đặt Người Dùng Telegram" +"toggle" = "🔘 Bật / Tắt" +"custom" = "🔢 Tùy chỉnh" +"confirmNumber" = "✅ Xác nhận: {{ .Num }}" +"confirmNumberAdd" = "✅ Xác nhận thêm: {{ .Num }}" +"limitTraffic" = "🚧 Giới hạn lưu lượng" +"getBanLogs" = "Cấm nhật ký" +"allClients" = "Tất cả Khách hàng" +"addClient" = "Thêm Khách Hàng" +"submitDisable" = "Gửi Dưới Dạng Vô Hiệu ☑️" +"submitEnable" = "Gửi Dưới Dạng Kích Hoạt ✅" +"use_default" = "🏷️ Sử Dụng Mặc Định" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 Mật Khẩu" +"change_email" = "⚙️📧 Email" +"change_comment" = "⚙️💬 Bình Luận" +"ResetAllTraffics" = "Đặt lại tất cả lưu lượng" +"SortedTrafficUsageReport" = "Báo cáo sử dụng lưu lượng đã sắp xếp" + +[tgbot.answers] +"successfulOperation" = "✅ Thành công!" +"errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện." +"getInboundsFailed" = "❌ Không Thể Lấy Được Inbounds" +"getClientsFailed" = "❌ Không thể lấy khách hàng." +"canceled" = "❌ {{ .Email }} : Thao Tác Đã Bị Hủy." +"clientRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng." +"IpRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs." +"TGIdRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram." +"resetTrafficSuccess" = "✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công." +"setTrafficLimitSuccess" = "✅ {{ .Email }} : Đã lưu thành công giới hạn lưu lượng." +"expireResetSuccess" = "✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công." +"resetIpSuccess" = "✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công." +"clearIpSuccess" = "✅ {{ .Email }} : IP Đã Được Xóa Thành Công." +"getIpLog" = "✅ {{ .Email }} : Lấy nhật ký IP Thành Công." +"getUserInfo" = "✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công." +"removedTGUserSuccess" = "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công." +"enableSuccess" = "✅ {{ .Email }} : Đã Bật Thành Công." +"disableSuccess" = "✅ {{ .Email }} : Đã Tắt Thành Công." +"askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: {{ .TgUserID }}" +"chooseClient" = "Chọn một Khách hàng cho Inbound {{ .Inbound }}" +"chooseInbound" = "Chọn một Inbound" + diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml new file mode 100644 index 0000000..f5bcd59 --- /dev/null +++ b/web/translation/translate.zh_CN.toml @@ -0,0 +1,848 @@ +"username" = "用户名" +"password" = "密码" +"login" = "登录" +"confirm" = "确定" +"cancel" = "取消" +"close" = "关闭" +"create" = "创建" +"update" = "更新" +"copy" = "复制" +"copied" = "已复制" +"download" = "下载" +"remark" = "备注" +"enable" = "启用" +"protocol" = "协议" +"search" = "搜索" +"filter" = "筛选" +"loading" = "加载中..." +"second" = "秒" +"minute" = "分钟" +"hour" = "小时" +"day" = "天" +"check" = "查看" +"indefinite" = "无限期" +"unlimited" = "无限制" +"none" = "无" +"qrCode" = "二维码" +"info" = "更多信息" +"edit" = "编辑" +"delete" = "删除" +"reset" = "重置" +"noData" = "无数据" +"copySuccess" = "复制成功" +"sure" = "确定" +"encryption" = "加密" +"useIPv4ForHost" = "使用 IPv4 连接主机" +"transmission" = "传输" +"host" = "主机" +"path" = "路径" +"camouflage" = "伪装" +"status" = "状态" +"enabled" = "开启" +"disabled" = "关闭" +"depleted" = "耗尽" +"depletingSoon" = "即将耗尽" +"offline" = "离线" +"online" = "在线" +"domainName" = "域名" +"monitor" = "监听" +"certificate" = "数字证书" +"fail" = "失败" +"comment" = "评论" +"success" = "成功" +"lastOnline" = "上次在线" +"getVersion" = "获取版本" +"install" = "安装" +"clients" = "客户端" +"usage" = "使用情况" +"secretToken" = "安全密钥" +"twoFactorCode" = "动态验证码" +"remained" = "剩余" +"security" = "安全" +"secAlertTitle" = "安全警报" +"secAlertSsl" = "此连接不安全!在激活 TLS 进行数据保护之前,请勿输入敏感信息。" +"secAlertConf" = "某些设置易受攻击!建议加强安全协议以防止潜在漏洞。" +"secAlertSSL" = "面板缺少安全连接!请安装 TLS 证书以保护数据安全。" +"secAlertPanelPort" = "面板默认端口存在安全风险!请配置随机端口或特定端口。" +"secAlertPanelURI" = "面板默认 URI 路径不安全!请配置复杂的 URI 路径。" +"secAlertSubURI" = "订阅默认 URI 路径不安全!请配置复杂的 URI 路径。" +"secAlertSubJsonURI" = "订阅 JSON 默认 URI 路径不安全!请配置复杂的 URI 路径。" +"emptyDnsDesc" = "未添加DNS服务器。" +"emptyFakeDnsDesc" = "未添加Fake DNS服务器。" +"emptyBalancersDesc" = "未添加负载均衡器。" +"emptyReverseDesc" = "未添加反向代理。" +"somethingWentWrong" = "出了点问题" + +[menu] +"theme" = "主题" +"dark" = "暗色" +"ultraDark" = "黑色" +"dashboard" = "系统状态" +"inbounds" = "入站列表" +"settings" = "面板设置" +"xray" = "Xray设置" +"logout" = "退出登录" +"link" = "管理/配置数据库" +"navigation" = "实用导航" + +[pages.login] +"hello" = "你好" +"XPanelSystem" = "管理系统" +"title" = "欢迎使用" +"loginAgain" = "登录时效已过,请重新登录" + +[pages.login.toasts] +"invalidFormData" = "数据格式错误" +"emptyUsername" = "请输入用户名" +"emptyPassword" = "请输入密码" +"wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。" +"successLogin" = "您已成功登录您的账户。" + +[pages.index] +"title" = "系统状态" +"cpu" = "CPU" +"logicalProcessors" = "逻辑处理器" +"frequency" = "频率" +"swap" = "交换分区" +"storage" = "存储" +"memory" = "内存" +"hard" = "磁盘" +"threads" = "线程" +"xrayStatus" = "Xray 运行状态" +"stopXray" = "停止" +"restartXray" = "重启" +"xraySwitch" = "版本" +"xraySwitchClick" = "选择你要切换到的版本" +"xraySwitchClickDesk" = "请谨慎选择,因为较旧版本可能与当前配置不兼容" +"xrayStatusUnknown" = "未知" +"xrayStatusRunning" = "运行中" +"xrayStatusStop" = "停止" +"xrayStatusError" = "错误" +"xrayErrorPopoverTitle" = "运行Xray时发生错误" +"operationHours" = "系统正常运行时间" +"systemLoad" = "系统负载" +"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载" +"connectionCount" = "连接数" +"ipAddresses" = "IP地址" +"toggleIpVisibility" = "切换IP可见性" +"overallSpeed" = "整体速度" +"upload" = "上传" +"download" = "下载" +"totalData" = "总数据" +"sent" = "已发送" +"received" = "已接收" +"documentation" = "文档" +"xraySwitchVersionDialog" = "您确定要更改Xray版本吗?" +"xraySwitchVersionDialogDesc" = "这将把Xray版本更改为#version#。" +"xraySwitchVersionPopover" = "Xray 更新成功" +"geofileUpdateDialog" = "您确定要更新地理文件吗?" +"geofileUpdateDialogDesc" = "这将更新 #filename# 文件。" +"geofilesUpdateDialogDesc" = "这将更新所有文件。" +"geofilesUpdateAll" = "全部更新" +"geofileUpdatePopover" = "地理文件更新成功" +"dontRefresh" = "安装中,请勿刷新此页面" +"logs" = "日志" +"config" = "配置" +"backup" = "备份和恢复" +"backupTitle" = "备份和恢复数据库" +"exportDatabase" = "本地备份" +"exportDatabaseDesc" = "点击下载包含当前数据库备份的 .db 文件到您的设备。" +"importDatabase" = "本地恢复" +"importDatabaseDesc" = "点击选择并上传设备中的 .db 文件以从备份恢复数据库。" +"importDatabaseSuccess" = "数据库导入成功" +"importDatabaseError" = "导入数据库时出错" +"readDatabaseError" = "读取数据库时出错" +"getDatabaseError" = "检索数据库时出错" +"getConfigError" = "检索配置文件时出错" +"betterPanel" = "一个更好的面板" +"builtOnXray" = "基于 Xray Core 构建" +"xpanelTitle" = "〔X-Panel面板〕" +"tgPrivateChat" = "TG私聊交流" +"tgGroupChat" = "〔X-Panel面板〕交流群" +"portCheck" = "端口检测" +"speedTest" = "网络测速" +"datasnapshot" = "创建数据快照" +"emergencyrecovery" = "远程急救还原" + +[pages.inbounds] +"allTimeTraffic" = "累计总流量" +"allTimeTrafficUsage" = "所有时间总使用量" +"title" = "入站列表" +"totalDownUp" = "总上传 / 下载" +"totalUsage" = "总用量" +"inboundCount" = "入站数量" +"operate" = "菜单" +"enable" = "启用" +"remark" = "备注" +"protocol" = "协议" +"port" = "端口" +"portMap" = "端口映射" +"traffic" = "流量" +"details" = "详细信息" +"transportConfig" = "传输配置" +"expireDate" = "到期时间" +"createdAt" = "创建时间" +"updatedAt" = "更新时间" +"resetTraffic" = "重置流量" +"addInbound" = "添加入站" +"generalActions" = "通用操作" +"autoRefresh" = "自动刷新" +"autoRefreshInterval" = "间隔" +"create" = "添加" +"update" = "修改" +"modifyInbound" = "修改入站" +"deleteInbound" = "删除入站" +"deleteInboundContent" = "确定要删除入站吗?" +"deleteClient" = "删除客户端" +"deleteClientContent" = "确定要删除客户端吗?" +"resetTrafficContent" = "确定要重置流量吗?" +"copyLink" = "复制链接" +"address" = "地址" +"network" = "网络" +"destinationPort" = "目标端口" +"targetAddress" = "目标地址" +"monitorDesc" = "留空表示监听所有 IP" +"meansNoLimit" = "= 无限制(单位:GB)" +"totalFlow" = "总流量" +"leaveBlankToNeverExpire" = "留空表示永不过期" +"noRecommendKeepDefault" = "建议保留默认值" +"certificatePath" = "文件路径" +"certificateContent" = "文件内容" +"publicKey" = "公钥" +"privatekey" = "私钥" +"clickOnQRcode" = "点击二维码复制" +"client" = "客户/用户" +"clients" = "所有客户端" +"export" = "导出链接" +"clone" = "克隆" +"cloneInbound" = "克隆" +"cloneInboundContent" = "此入站规则除端口(Port)、监听 IP(Listening IP)和客户端(Clients)以外的所有配置都将应用于克隆" +"cloneInboundOk" = "创建克隆" +"resetAllTraffic" = "重置所有入站流量" +"resetAllTrafficTitle" = "重置所有入站流量" +"resetAllTrafficContent" = "确定要重置所有入站流量吗?" +"resetInboundClientTraffics" = "重置客户端流量" +"resetInboundClientTrafficTitle" = "重置所有客户端流量" +"resetInboundClientTrafficContent" = "确定要重置此入站客户端的所有流量吗?" +"resetAllClientTraffics" = "重置所有客户端流量" +"resetAllClientTrafficTitle" = "重置所有客户端流量" +"resetAllClientTrafficContent" = "确定要重置所有客户端的所有流量吗?" +"delDepletedClients" = "删除流量耗尽的客户端" +"delDepletedClientsTitle" = "删除流量耗尽的客户端" +"delDepletedClientsContent" = "确定要删除所有流量耗尽的客户端吗?" +"email" = "电子邮件" +"emailDesc" = "电子邮件必须确保唯一" +"IPLimit" = "IP 限制" +"IPLimitDesc" = "如果数量超过设置值,则禁用入站流量。(0 = 禁用)" +"IPLimitlog" = "IP 日志" +"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)" +"IPLimitlogclear" = "清除日志" +"setDefaultCert" = "从面板设置证书" +"telegramDesc" = "请提供Telegram聊天ID。(在机器人中使用'/id'命令或跟@userinfobot机器人对话获取)" +"subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。" +"info" = "信息" +"same" = "相同" +"inboundData" = "入站数据" +"exportInbound" = "导出入站规则" +"import"="导入" +"importInbound" = "导入入站规则" +"reviewTitle"="请确认以下配置信息" +"neverExpire"="永不过期" +"reviewHint"="如需修改请返回上一步" +"unlimited"="无限制" +"deviceLimit"="设备限制" +"deviceLimitDesc"="请输入具体数量,\r\n0表示不限制(留空也表示不限制)" +"speedLimit"="独立限速" +"speedLimitDesc"="设置该用户的最大〔上传/下载速度〕,\r\n单位 KB/s,0 表示不限速" +"oneClickConfig"="一键配置" +"is_subConversion"="订阅转换" +"confirmCreate"="确认提交创建" + + +[pages.client] +"add" = "添加客户端" +"edit" = "编辑客户端" +"submitAdd" = "添加客户端" +"submitEdit" = "保存修改" +"clientCount" = "客户端数量" +"bulk" = "批量创建" +"method" = "方法" +"first" = "置顶" +"last" = "置底" +"prefix" = "前缀" +"postfix" = "后缀" +"delayedStart" = "首次使用后开始" +"expireDays" = "到期天数/期间" +"days" = "天" +"renew" = "自动续订" +"renewDesc" = "到期后自动续订。(0 = 禁用)(单位: 天)" + +[pages.inbounds.toasts] +"obtain" = "获取" +"updateSuccess" = "更新成功" +"logCleanSuccess" = "日志已清除" +"inboundsUpdateSuccess" = "入站连接已成功更新" +"inboundUpdateSuccess" = "入站连接已成功更新" +"inboundCreateSuccess" = "入站连接已成功创建" +"inboundDeleteSuccess" = "入站连接已成功删除" +"inboundClientAddSuccess" = "已添加入站客户端" +"inboundClientDeleteSuccess" = "入站客户端已删除" +"inboundClientUpdateSuccess" = "入站客户端已更新" +"delDepletedClientsSuccess" = "所有耗尽客户端已删除" +"resetAllClientTrafficSuccess" = "客户端所有流量已重置" +"resetAllTrafficSuccess" = "所有流量已重置" +"resetInboundClientTrafficSuccess" = "流量已重置" +"trafficGetError" = "获取流量数据时出错" +"getNewX25519CertError" = "获取X25519证书时出错。" +"getNewmldsa65Error" = "获取mldsa65证书时出错。" +"getNewVlessEncError" = "获取VlessEnc证书时出错。" + +[pages.inbounds.stream.general] +"request" = "请求" +"response" = "响应" +"name" = "名称" +"value" = "值" + +[pages.inbounds.stream.tcp] +"version" = "版本" +"method" = "方法" +"path" = "路径" +"status" = "状态" +"statusDescription" = "状态说明" +"requestHeader" = "请求头" +"responseHeader" = "响应头" + +[pages.inbounds.oneClick] +"title" = "一键配置" +"prompt" = "选择一个预设配置快速创建入站连接。" +"presetType" = "预设协议配置类型" +"generateTab" = "生成配置" +"historyTab" = "历史记录" +"historyType" = "生成类型" +"historyLink" = "链接" +"historyTime" = "生成时间" +"loading" = "正在生成高清二维码..." +"success" = "高清二维码生成成功!" +"warning" = "连接转换服务失败,二维码可能难以扫描。" +"clickToCopyTitle" = "点击复制链接" +"scanQrcodeTitle" = "扫描二维码" +"clickToCopyHint" = "(点击图片也可复制)" +"linkCreated" = "入站链接已创建,请复制以下地址:" +"descriptionTitle" = "\r\n\r\n用法说明:" +"descriptionContent" = "1、该功能将自动生成 VLESS + TCP + Reality (xtls-rprx-vision) 和 VLESS + XHTTP + Reality 协议的入站,以上最前面两种协议组合适合直连,\r\n2、或 VLESS Encryption + XHTTP + TLS(可选套CDN)协议的入站,\r\n3、并随机分配一个可用端口,请确保此端口放行,生成后请直接复制【链接地址】,\r\n4、此处的【一键配置】生成功能,跟【入站】的数据是打通的,\r\n5、你可以在一键创建后于列表中,手动查看/复制或编辑详细信息,以便添加其他参数。" + +[pages.inbounds.oneClick.preset] +"vless_reality" = "VLESS + TCP + Reality + Vision" +"vless_xhttp_reality" = "VLESS + XHTTP + Reality" +"vless_tls_encryption" = "VLESS Encryption + TLS(可选套CDN)" +"switch_vision_seed_dev" = "Switch + Vision Seed (开发中)" + +[pages.inbounds.subConversion] +"modalTitle" = "通用订阅转换" +"ok" = "开始转换" +"urlLabel" = "链接地址输入框" +"urlPlaceholder" = "请粘贴原始的链接地址" +"targetLabel" = "目标客户端" +"success" = "转换成功!" +"errorInput" = "请输入链接地址!" +"errorShort" = "生成分享链接失败" +"modalContent" = "\r\n即将检测并跳转至:\r\n您的订阅转换服务页面,是否继续?" +"modalOk" = "继续" +"notFoundTitle" = "【订阅转换服务】未安装或无法访问" +"notFoundContent" = "\r\n请进入VPS服务器终端,输入 x-ui 命令,\r\n并选择选项【25】进行安装。" + +[pages.settings] +"title" = "面板设置" +"save" = "保存" +"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效" +"restartPanel" = "重启面板" +"restartPanelDesc" = "确定要重启面板吗?若重启后无法访问面板,请前往服务器查看面板日志信息" +"restartPanelSuccess" = "面板已成功重启" +"actions" = "操作" +"resetDefaultConfig" = "重置为默认配置" +"panelSettings" = "常规" +"securitySettings" = "安全设定" +"TGBotSettings" = "Telegram机器人配置" +"panelListeningIP" = "面板监听 IP" +"panelListeningIPDesc" = "默认留空监听所有 IP" +"panelListeningDomain" = "面板监听域名" +"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址" +"panelPort" = "面板监听端口" +"panelPortDesc" = "重启面板生效" +"publicKeyPath" = "面板证书公钥文件路径" +"DefaultpublicKeyPath" = "/root/.acme.sh/域名_ecc/域名.cer" +"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,〔acme方式〕请自行在填入时修改域名" +"privateKeyPath" = "面板证书密钥文件路径" +"DefaultprivateKeyPath" = "/root/.acme.sh/域名_ecc/域名.key" +"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,〔acme方式〕请自行在填入时修改域名" +"panelUrlPath" = "面板登录访问路径" +"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾" +"pageSize" = "分页大小" +"pageSizeDesc" = "定义入站表的页面大小。设置 0 表示禁用" +"remarkModel" = "备注模型和分隔符" +"datepicker" = "日期选择器" +"datepickerPlaceholder" = "选择日期" +"datepickerDescription" = "选择器日历类型指定到期日期" +"sampleRemark" = "备注示例" +"oldUsername" = "原用户名" +"currentPassword" = "原密码" +"newUsername" = "新用户名" +"newPassword" = "新密码" +"telegramBotEnable" = "启用 Telegram 机器人" +"telegramBotEnableDesc" = "启用 Telegram 机器人功能" +"telegramToken" = "Telegram 机器人令牌(token)" +"telegramTokenDesc" = "跟 '@BotFather' 对话获取的 Telegram 机器人令牌" +"telegramProxy" = "SOCKS5 Proxy" +"telegramProxyDesc" = "启用 SOCKS5 代理连接到 Telegram(根据指南调整设置)" +"telegramAPIServer" = "Telegram API Server" +"telegramAPIServerDesc" = "要使用的 Telegram API 服务器。留空以使用默认服务器。" +"telegramChatId" = "管理员聊天 ID" +"telegramChatIdDesc" = "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 @userinfobot 获取,或在机器人中使用 '/id' 命令获取)" +"telegramNotifyTime" = "通知时间" +"telegramNotifyTimeDesc" = "设置周期性的 Telegram 机器人通知时间(使用 crontab 时间格式)" +"tgNotifyBackup" = "数据库备份" +"tgNotifyBackupDesc" = "发送带有报告的数据库备份文件" +"tgNotifyLogin" = "登录通知" +"tgNotifyLoginDesc" = "当有人试图登录你的面板时显示用户名、IP 地址和时间" +"sessionMaxAge" = "会话时长" +"sessionMaxAgeDesc" = "保持登录状态的时长(单位:分钟)" +"expireTimeDiff" = "到期通知阈值" +"expireTimeDiffDesc" = "达到此阈值时,将收到有关到期时间的通知(单位:天)" +"trafficDiff" = "流量耗尽阈值" +"trafficDiffDesc" = "达到此阈值时,将收到有关流量耗尽的通知(单位:GB)" +"tgNotifyCpu" = "CPU 负载通知阈值" +"tgNotifyCpuDesc" = "CPU 负载超过此阈值时,将收到通知(单位:%)" +"timeZone" = "时区" +"timeZoneDesc" = "定时任务将按照该时区的时间运行" +"subSettings" = "订阅设置" +"subEnable" = "启用订阅服务" +"subEnableDesc" = "启用订阅服务功能" +"subTitle" = "订阅标题" +"subTitleDesc" = "在VPN客户端中显示的标题" +"subListen" = "监听 IP" +"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP)" +"subPort" = "监听端口" +"subPortDesc" = "订阅服务监听的端口号(必须是未使用的端口)" +"subCertPath" = "公钥路径" +"subCertPathDesc" = "订阅服务使用的公钥文件路径(以 '/' 开头)" +"subKeyPath" = "私钥路径" +"subKeyPathDesc" = "订阅服务使用的私钥文件路径(以 '/' 开头)" +"subPath" = "URI 路径" +"subPathDesc" = "订阅服务使用的 URI 路径(以 '/' 开头,以 '/' 结尾)" +"subDomain" = "监听域名" +"subDomainDesc" = "订阅服务监听的域名(留空表示监听所有域名和 IP)" +"subUpdates" = "更新间隔" +"subUpdatesDesc" = "客户端应用中订阅 URL 的更新间隔(单位:小时)" +"subEncrypt" = "编码" +"subEncryptDesc" = "订阅服务返回的内容将采用 Base64 编码" +"subShowInfo" = "显示使用信息" +"subShowInfoDesc" = "客户端应用中将显示剩余流量和日期信息" +"subURI" = "反向代理 URI" +"subURIDesc" = "用于代理后面的订阅 URL 的 URI 路径" +"externalTrafficInformEnable" = "外部交通通知" +"externalTrafficInformEnableDesc" = "每次流量更新时通知外部 API" +"externalTrafficInformURI" = "外部流量通知 URI" +"externalTrafficInformURIDesc" = "流量更新将发送到此 URI" +"fragment" = "分片" +"fragmentDesc" = "启用 TLS hello 数据包分片" +"fragmentSett" = "设置" +"noisesDesc" = "启用 Noises." +"noisesSett" = "Noises 设置" +"mux" = "多路复用器" +"muxDesc" = "在已建立的数据流内传输多个独立的数据流" +"muxSett" = "复用器设置" +"direct" = "直接连接" +"directDesc" = "直接与特定国家的域或IP范围建立连接" +"notifications" = "通知" +"certs" = "证书" +"externalTraffic" = "外部流量" +"dateAndTime" = "日期和时间" +"proxyAndServer" = "代理和服务器" +"intervals" = "间隔" +"information" = "信息" +"language" = "语言" +"telegramBotLanguage" = "Telegram 机器人语言" + +[pages.xray] +"title" = "Xray 配置" +"save" = "保存" +"restart" = "重新启动 Xray" +"restartSuccess" = "Xray 已成功重新启动" +"stopSuccess" = "Xray 已成功停止" +"restartError" = "重启Xray时发生错误。" +"stopError" = "停止Xray时发生错误。" +"basicTemplate" = "基础配置" +"advancedTemplate" = "高级配置" +"generalConfigs" = "常规配置" +"generalConfigsDesc" = "这些选项将决定常规配置" +"logConfigs" = "日志" +"logConfigsDesc" = "日志可能会影响服务器的性能,建议仅在需要时启用" +"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站" +"basicRouting" = "基本路由" +"blockConnectionsConfigsDesc" = "这些选项将根据特定的请求国家阻止流量。" +"directConnectionsConfigsDesc" = "直接连接确保特定的流量不会通过其他服务器路由。" +"blockips" = "阻止IP" +"blockdomains" = "阻止域名" +"directips" = "直接IP" +"directdomains" = "直接域名" +"ipv4Routing" = "IPv4 路由" +"ipv4RoutingDesc" = "此选项将仅通过 IPv4 路由到目标域" +"warpRouting" = "WARP 路由" +"warpRoutingDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在你的服务器上以 socks5 代理模式安装 WARP。WARP 将通过 Cloudflare 服务器将流量路由到网站。" +"Template" = "高级 Xray 配置模板" +"TemplateDesc" = "最终的 Xray 配置文件将基于此模板生成" +"FreedomStrategy" = "Freedom 协议策略" +"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略" +"RoutingStrategy" = "配置路由域策略" +"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略" +"Torrent" = "屏蔽 BitTorrent 协议" +"Inbounds" = "入站规则" +"InboundsDesc" = "接受来自特定客户端的流量" +"Outbounds" = "出站规则" +"Balancers" = "负载均衡" +"OutboundsDesc" = "设置出站流量传出方式" +"Routings" = "路由规则" +"RoutingsDesc" = "每条规则的优先级都很重要" +"completeTemplate" = "全部" +"logLevel" = "日志级别" +"logLevelDesc" = "错误日志的日志级别,用于指示需要记录的信息" +"accessLog" = "访问日志" +"accessLogDesc" = "访问日志的文件路径。特殊值 'none' 禁用访问日志" +"errorLog" = "错误日志" +"errorLogDesc" = "错误日志的文件路径。特殊值 'none' 禁用错误日志" +"dnsLog" = "DNS 日志" +"dnsLogDesc" = "是否启用 DNS 查询日志" +"maskAddress" = "隐藏地址" +"maskAddressDesc" = "IP 地址掩码,启用时会自动替换日志中出现的 IP 地址。" +"statistics" = "统计" +"statsInboundUplink" = "入站上传统计" +"statsInboundUplinkDesc" = "启用所有入站代理的上行流量统计收集。" +"statsInboundDownlink" = "入站下载统计" +"statsInboundDownlinkDesc" = "启用所有入站代理的下行流量统计收集。" +"statsOutboundUplink" = "出站上传统计" +"statsOutboundUplinkDesc" = "启用所有出站代理的上行流量统计收集。" +"statsOutboundDownlink" = "出站下载统计" +"statsOutboundDownlinkDesc" = "启用所有出站代理的下行流量统计收集。" + +[pages.navigation] +"title" = "实用导航" + +[pages.controlledmanagement] +"title" = "主从管理" + +[pages.xray.rules] +"first" = "置顶" +"last" = "置底" +"up" = "向上" +"down" = "向下" +"source" = "来源" +"dest" = "目标地址" +"inbound" = "入站" +"outbound" = "出站" +"balancer" = "负载均衡" +"info" = "信息" +"add" = "添加规则" +"edit" = "编辑规则" +"useComma" = "逗号分隔的项目" +"DomainMatcher" = "域匹配类型" +"SourceIPs" = "源IP" +"SourcePort" = "源端口" +"Network" = "网络类型" +"Protocol" = "传输协议" +"Attributes" = "属性" +"Domain" = "域地址" +"User" = "用户" +"Port" = "端口" +"InboundTag" = "入站 Tag" +"OutboundTag" = "出站 Tag" +"BalancerTag" = "负载均衡 Tag" + +[pages.xray.outbound] +"addOutbound" = "添加出站" +"addReverse" = "添加反向" +"editOutbound" = "编辑出站" +"editReverse" = "编辑反向" +"tag" = "标签" +"tagDesc" = "唯一标签" +"address" = "地址" +"reverse" = "反向" +"domain" = "域名" +"type" = "类型" +"bridge" = "Bridge" +"portal" = "Portal" +"link" = "链接" +"intercon" = "互连" +"settings" = "设置" +"accountInfo" = "帐户信息" +"outboundStatus" = "出站状态" +"sendThrough" = "发送通过" + +[pages.xray.balancer] +"addBalancer" = "添加负载均衡" +"editBalancer" = "编辑负载均衡" +"balancerStrategy" = "策略" +"balancerSelectors" = "选择器" +"tag" = "标签" +"tagDesc" = "唯一标签" +"balancerDesc" = "无法同时使用 balancerTag 和 outboundTag。如果同时使用,则只有 outboundTag 会生效。" + +[pages.xray.wireguard] +"secretKey" = "密钥" +"publicKey" = "公钥" +"allowedIPs" = "允许的 IP" +"endpoint" = "端点" +"psk" = "共享密钥" +"domainStrategy" = "域策略" + +[pages.xray.dns] +"enable" = "启用 DNS" +"enableDesc" = "启用内置 DNS 服务器" +"tag" = "DNS 入站标签" +"tagDesc" = "此标签将在路由规则中可用作入站标签" +"clientIp" = "客户端IP" +"clientIpDesc" = "用于在DNS查询期间通知服务器指定的IP位置" +"disableCache" = "禁用缓存" +"disableCacheDesc" = "禁用DNS缓存" +"disableFallback" = "禁用回退" +"disableFallbackDesc" = "禁用回退DNS查询" +"disableFallbackIfMatch" = "匹配时禁用回退" +"disableFallbackIfMatchDesc" = "当DNS服务器的匹配域名列表命中时,禁用回退DNS查询" +"enableParallelQuery" = "启用并行查询" +"enableParallelQueryDesc" = "启用并行“DNS查询”到多个服务器以实现更快的解析" +"strategy" = "查询策略" +"strategyDesc" = "解析域名的总体策略" +"add" = "添加服务器" +"edit" = "编辑服务器" +"domains" = "域" +"expectIPs" = "预期 IP" +"unexpectIPs" = "意外IP" +"useSystemHosts" = "使用系统Hosts" +"useSystemHostsDesc" = "使用已安装系统的hosts文件" +"usePreset" = "使用模板" +"dnsPresetTitle" = "DNS模板" +"dnsPresetFamily" = "家庭" + +[pages.xray.fakedns] +"add" = "添加假 DNS" +"edit" = "编辑假 DNS" +"ipPool" = "IP 池子网" +"poolSize" = "池大小" + +[pages.settings.security] +"admin" = "管理员凭据" +"twoFactor" = "双重验证" +"twoFactorEnable" = "启用2FA" +"twoFactorEnableDesc" = "增加额外的验证层以提高安全性。" +"twoFactorModalSetTitle" = "启用双重认证" +"twoFactorModalDeleteTitle" = "停用双重认证" +"twoFactorModalSteps" = "要设定双重认证,请执行以下步骤:" +"twoFactorModalFirstStep" = "1. 在认证应用程序中扫描此QR码,或复制QR码附近的令牌并粘贴到应用程序中" +"twoFactorModalSecondStep" = "2. 输入应用程序中的验证码" +"twoFactorModalRemoveStep" = "输入应用程序中的验证码以移除双重认证。" +"twoFactorModalChangeCredentialsTitle" = "更改凭据" +"twoFactorModalChangeCredentialsStep" = "输入应用程序中的代码以更改管理员凭据。" +"twoFactorModalSetSuccess" = "双因素认证已成功建立" +"twoFactorModalDeleteSuccess" = "双因素认证已成功删除" +"twoFactorModalError" = "验证码错误" + +[pages.settings.toasts] +"modifySettings" = "参数已更改。" +"getSettings" = "获取参数时发生错误" +"modifyUserError" = "更改管理员凭据时发生错误。" +"modifyUser" = "您已成功更改管理员凭据。" +"originalUserPassIncorrect" = "原用户名或原密码错误" +"userPassMustBeNotEmpty" = "新用户名和新密码不能为空" +"getOutboundTrafficError" = "获取出站流量错误" +"resetOutboundTrafficError" = "重置出站流量错误" + +[tgbot] +"keyboardClosed" = "❌ 自定义键盘已关闭!" +"noResult" = "❗ 没有结果!" +"noQuery" = "❌ 未找到查询!请重新使用命令!" +"wentWrong" = "❌ 出了点问题!" +"noIpRecord" = "❗ 没有 IP 记录!" +"noInbounds" = "❗ 没有找到入站连接!" +"unlimited" = "♾ 无限制" +"add" = "添加" +"month" = "月" +"months" = "月" +"day" = "天" +"days" = "天" +"hours" = "小时" +"minutes" = "分钟" +"unknown" = "未知" +"inbounds" = "入站连接" +"clients" = "客户端" +"offline" = "🔴 离线" +"online" = "🟢 在线" + +[tgbot.commands] +"unknown" = "❗ 未知命令" +"pleaseChoose" = "👇请〔按照需求〕选择下方按钮 :\r\n" +"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n" +"start" = "👋 你好,{{ .Firstname }}。\r\n" +"welcome" = "🤖 欢迎来到 {{ .Hostname }} 管理机器人。\r\n" +"status" = "✅ 机器人正常运行!" +"usage" = "❗ 请输入要搜索的文本!" +"getID" = "🆔 您的 ID 为:{{ .ID }}" +"helpAdminCommands" = "要重新启动 Xray Core:\r\n/restart\r\n\r\n要搜索客户电子邮件:\r\n/usage [电子邮件]\r\n\r\n要搜索入站(带有客户统计数据):\r\n/inbound [备注]\r\n\r\nTelegram聊天ID:\r\n/id\r\n\r\n一键配置:\r\n/oneclick\r\n\r\n订阅转换:\r\n/subconverter\r\n\r\n重启〔X-Panel 面板〕:\r\n/restartx" +"helpClientCommands" = "要搜索统计数据,请使用以下命令:\r\n/usage [电子邮件]\r\n\r\nTelegram聊天ID:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ 操作成功!" +"restartFailed" = "❗ 操作错误。\r\n\r\n错误: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core 未运行。" +"startDesc" = "显示主菜单" +"helpDesc" = "机器人帮助" +"statusDesc" = "检查机器人状态" +"idDesc" = "显示您的 Telegram ID" + +[tgbot.messages] +"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%" +"selectUserFailed" = "❌ 用户选择错误!" +"userSaved" = "✅ 电报用户已保存。" +"loginSuccess" = "✅ 成功登录到面板。\r\n" +"loginFailed" = "❗️ 面板登录失败。\r\n" +"report" = "🕰 定时报告:{{ .RunTime }}\r\n" +"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n" +"hostname" = "💻 主机名:{{ .Hostname }}\r\n" +"version" = "🚀 X-Panel 版本:v{{ .Version }}\r\n" +"xrayVersion" = "📡 Xray Core 版本:v{{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" +"ip" = "🌐 IP:{{ .IP }}\r\n" +"ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n" +"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n" +"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Xray 状态:{{ .State }}\r\n" +"username" = "👤 用户名:{{ .Username }}\r\n" +"password" = "👤 密码: {{ .Password }}\r\n" +"time" = "⏰ 时间:{{ .Time }}\r\n" +"inbound" = "📍 入站:{{ .Remark }}\r\n" +"port" = "🔌 端口:{{ .Port }}\r\n" +"expire" = "📅 过期日期:{{ .Time }}\r\n" +"expireIn" = "📅 剩余时间:{{ .Time }}\r\n" +"active" = "💡 激活:{{ .Enable }}\r\n" +"enabled" = "🚨 已启用:{{ .Enable }}\r\n" +"online" = "🌐 连接状态:{{ .Status }}\r\n" +"email" = "📧 邮箱(用户):{{ .Email }}\r\n" +"upload" = "🔼 上传↑:{{ .Upload }}\r\n" +"download" = "🔽 下载↓:{{ .Download }}\r\n" +"total" = "📊 总计:{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 耗尽的 {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 耗尽的 {{ .Type }} 数量:\r\n" +"onlinesCount" = "🌐 在线客户:{{ .Count }}\r\n" +"disabled" = "🛑 禁用:{{ .Disabled }}\r\n" +"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 备份时间:{{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n" +"yes" = "✅ 是的" +"no" = "❌ 没有" +"received_id" = "🔑📥 ID 已更新。" +"received_password" = "🔑📥 密码已更新。" +"received_email" = "📧📥 邮箱已更新。" +"received_comment" = "💬📥 评论已更新。" +"id_prompt" = "🔑 默认 ID: {{ .ClientId }}\n\n请输入您的 ID。" +"pass_prompt" = "🔑 默认密码: {{ .ClientPassword }}\n\n请输入您的密码。" +"email_prompt" = "📧 默认邮箱: {{ .ClientEmail }}\n\n请输入您的邮箱。" +"comment_prompt" = "💬 默认评论: {{ .ClientComment }}\n\n请输入您的评论。" +"inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了!" +"inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密码: {{ .ClientPass }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了!" +"cancel" = "❌ 进程已取消!\n\n您可以随时使用 /start 重新开始。 🔄" +"error_add_client" = "⚠️ 错误:\n\n {{ .error }}" +"using_default_value" = "好的,我会使用默认值。 😊" +"incorrect_input" ="您的输入无效。\n短语应连续输入,不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫" +"AreYouSure" = "你确定吗?🤔" +"SuccessResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ✅ 成功" +"FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠️ 错误: [ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 所有客户的流量重置已完成。" +"updateConfirm" = "🤔 您确定要更新面板吗?\n此操作将自动从 GitHub 拉取最新代码并重启服务,期间面板将无法访问。" +"restartPanelConfirm" = "🤔 您确定要重启面板服务吗?\n这也会重启Xray-core,会使面板在短时间内无法访问。" +"updateFailed" = "❌ 面板更新命令执行失败!错误信息已记录到日志,请检查命令或权限。" +"restartPanelFailed" = "❌ 面板重启命令执行失败!错误信息已记录到日志,请检查命令或权限。" +"updateRestartWait" = "⏳ 更新命令已执行,正在等待面板重启(约50秒),并进行验证检查..." +"restartPanelWait" = "⏳ 重启命令已执行,正在等待面板恢复(约20秒),并进行验证检查..." +"updateRestartFailed" = "⚠️ 面板更新命令执行成功,但重启后服务未在预期时间内恢复,请手动检查面板状态。" +"restartPanelHealthFailed" = "⚠️ 面板重启命令执行成功,但服务未在预期时间内恢复,请手动检查面板状态。" +"updateSuccess" = "✅ 面板更新成功!服务已成功重启!" +"restartPanelSuccess" = "🚀 面板重启成功!服务已成功恢复!" + + +[tgbot.buttons] +"closeKeyboard" = "❌ 关闭键盘" +"cancel" = "❌ 取消" +"cancelReset" = "❌ 取消重置" +"cancelIpLimit" = "❌ 取消 IP 限制" +"confirmResetTraffic" = "✅ 确认重置流量?" +"confirmClearIps" = "✅ 确认清除 IP?" +"confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?" +"confirmToggle" = "✅ 确认启用/禁用用户?" +"dbBackup" = "获取数据库备份" +"serverUsage" = "🌐服务器状态" +"getInbounds" = "获取入站信息" +"depleteSoon" = "即将耗尽" +"clientUsage" = "获取使用情况" +"onlines" = "在线客户端" +"commands" = "常用命令" +"refresh" = "🔄 刷新" +"clearIPs" = "❌ 清除 IP" +"removeTGUser" = "❌ 移除 Telegram 用户" +"selectTGUser" = "👤 选择 Telegram 用户" +"selectOneTGUser" = "👤 选择一个 Telegram 用户:" +"resetTraffic" = "📈 重置流量" +"resetExpire" = "📅 更改到期日期" +"ipLog" = "🔢 IP 日志" +"ipLimit" = "🔢 IP 限制" +"setTGUser" = "👤 设置 Telegram 用户" +"toggle" = "🔘 启用/禁用" +"custom" = "🔢 自定义输入" +"confirmNumber" = "✅ 确认: {{ .Num }}" +"confirmNumberAdd" = "✅ 确认添加:{{ .Num }}" +"limitTraffic" = "🚧 流量限制" +"getBanLogs" = "禁止日志" +"allClients" = "所有客户" +"addClient" = "添加客户" +"submitDisable" = "提交为禁用 ☑️" +"submitEnable" = "提交为启用 ✅" +"use_default" = "🏷️ 使用默认" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 密码" +"change_email" = "⚙️📧 邮箱" +"change_comment" = "⚙️💬 评论" +"ResetAllTraffics" = "重置所有流量" +"SortedTrafficUsageReport" = "流量使用报告" +"updatePanel" = "🔄 更新面板" +"restartPanel" = "🚀 重启面板" +"oneClick" = "🚀 一键配置" +"subconverter" = "🔄 订阅转换" + + +[tgbot.answers] +"successfulOperation" = "✅ 成功!" +"errorOperation" = "❗ 操作错误。" +"getInboundsFailed" = "❌ 获取入站信息失败。" +"getClientsFailed" = "❌ 获取客户失败。" +"canceled" = "❌ {{ .Email }}:操作已取消。" +"clientRefreshSuccess" = "✅ {{ .Email }}:客户端刷新成功。" +"IpRefreshSuccess" = "✅ {{ .Email }}:IP 刷新成功。" +"TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。" +"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。" +"setTrafficLimitSuccess" = "✅ {{ .Email }}: 流量限制保存成功。" +"expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。" +"resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。" +"clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。" +"getIpLog" = "✅ {{ .Email }}:获取 IP 日志。" +"getUserInfo" = "✅ {{ .Email }}:获取 Telegram 用户信息。" +"removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。" +"enableSuccess" = "✅ {{ .Email }}:已成功启用。" +"disableSuccess" = "✅ {{ .Email }}:已成功禁用。" +"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户 ChatID。\r\n\r\n您的用户 ChatID:{{ .TgUserID }}" +"chooseClient" = "为入站 {{ .Inbound }} 选择一个客户" +"chooseInbound" = "选择一个入站" +"panelUpdating" = "正在发送更新命令..." +"panelRestarting" = "正在发送重启命令..." +"actionCancelled" = "操作已取消" diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml new file mode 100644 index 0000000..cf9b96e --- /dev/null +++ b/web/translation/translate.zh_TW.toml @@ -0,0 +1,820 @@ +"username" = "用戶名" +"password" = "密碼" +"login" = "登入" +"confirm" = "確定" +"cancel" = "取消" +"close" = "關閉" +"create" = "創建" +"update" = "更新" +"copy" = "複製" +"copied" = "已複製" +"download" = "下載" +"remark" = "備註" +"enable" = "啟用" +"protocol" = "協議" +"search" = "搜尋" +"filter" = "篩選" +"loading" = "載入中..." +"second" = "秒" +"minute" = "分鐘" +"hour" = "小時" +"day" = "天" +"check" = "檢視" +"indefinite" = "無限期" +"unlimited" = "無限制" +"none" = "無" +"qrCode" = "二維碼" +"info" = "更多資訊" +"edit" = "編輯" +"delete" = "刪除" +"reset" = "重設" +"noData" = "無資料" +"copySuccess" = "複製成功" +"sure" = "確定" +"encryption" = "加密" +"useIPv4ForHost" = "使用 IPv4 連接主機" +"transmission" = "傳輸" +"host" = "主機" +"path" = "路徑" +"camouflage" = "偽裝" +"status" = "狀態" +"enabled" = "開啟" +"disabled" = "關閉" +"depleted" = "耗盡" +"depletingSoon" = "即將耗盡" +"offline" = "離線" +"online" = "在線" +"domainName" = "網域名稱" +"monitor" = "監聽" +"certificate" = "數位憑證" +"fail" = "失敗" +"comment" = "評論" +"success" = "成功" +"lastOnline" = "上次上線" +"getVersion" = "獲取版本" +"install" = "安裝" +"clients" = "客戶端" +"usage" = "使用情況" +"secretToken" = "安全金鑰" +"twoFactorCode" = "動態驗證碼" +"remained" = "剩餘" +"security" = "安全" +"secAlertTitle" = "安全警報" +"secAlertSsl" = "此連線不安全!在啟用 TLS 進行資料保護之前,請勿輸入敏感資訊。" +"secAlertConf" = "某些設定易受攻擊!建議加強安全協議以防止潛在漏洞。" +"secAlertSSL" = "面板缺少安全連線!請安裝 TLS 憑證以保護資料安全。" +"secAlertPanelPort" = "面板預設連接埠存在安全風險!請設定隨機連接埠或特定連接埠。" +"secAlertPanelURI" = "面板預設 URI 路徑不安全!請設定複雜的 URI 路徑。" +"secAlertSubURI" = "訂閱預設 URI 路徑不安全!請設定複雜的 URI 路徑。" +"secAlertSubJsonURI" = "訂閱 JSON 預設 URI 路徑不安全!請設定複雜的 URI 路徑。" +"emptyDnsDesc" = "未新增 DNS 伺服器。" +"emptyFakeDnsDesc" = "未新增 Fake DNS 伺服器。" +"emptyBalancersDesc" = "未新增負載平衡器。" +"emptyReverseDesc" = "未新增反向代理。" +"somethingWentWrong" = "出了一點問題" + +[menu] +"theme" = "主題" +"dark" = "暗色" +"ultraDark" = "黑色" +"dashboard" = "系統狀態" +"inbounds" = "入站列表" +"settings" = "面板設定" +"xray" = "Xray 設定" +"logout" = "登出" +"link" = "管理/設定資料庫" +"navigation" = "實用導覽" + +[pages.login] +"hello" = "你好" +"XPanelSystem" = "管理系統" +"title" = "歡迎使用" +"loginAgain" = "登入時效已過,請重新登入" + +[pages.login.toasts] +"invalidFormData" = "資料格式錯誤" +"emptyUsername" = "請輸入用戶名" +"emptyPassword" = "請輸入密碼" +"wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。" +"successLogin" = "您已成功登入您的帳戶。" + +[pages.index] +"title" = "系統狀態" +"cpu" = "CPU" +"logicalProcessors" = "邏輯處理器" +"frequency" = "頻率" +"swap" = "交換分區" +"storage" = "儲存" +"memory" = "記憶體" +"hard" = "磁碟" +"threads" = "執行緒" +"xrayStatus" = "Xray 運作狀態" +"stopXray" = "停止" +"restartXray" = "重啟" +"xraySwitch" = "版本" +"xraySwitchClick" = "選擇您要切換到的版本" +"xraySwitchClickDesk" = "請謹慎選擇,因為較舊版本可能與目前設定不相容" +"xrayStatusUnknown" = "未知" +"xrayStatusRunning" = "運行中" +"xrayStatusStop" = "停止" +"xrayStatusError" = "錯誤" +"xrayErrorPopoverTitle" = "執行 Xray 時發生錯誤" +"operationHours" = "系統正常執行時間" +"systemLoad" = "系統負載" +"systemLoadDesc" = "過去 1、5 和 15 分鐘的系統平均負載" +"connectionCount" = "連線數" +"ipAddresses" = "IP 位址" +"toggleIpVisibility" = "切換 IP 可見性" +"overallSpeed" = "整體速度" +"upload" = "上傳" +"download" = "下載" +"totalData" = "總數據" +"sent" = "已傳送" +"received" = "已接收" +"documentation" = "文件" +"xraySwitchVersionDialog" = "您確定要變更 Xray 版本嗎?" +"xraySwitchVersionDialogDesc" = "這將把 Xray 版本變更為 #version#。" +"xraySwitchVersionPopover" = "Xray 更新成功" +"geofileUpdateDialog" = "您確定要更新地理檔案嗎?" +"geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。" +"geofilesUpdateDialogDesc" = "這將更新所有檔案。" +"geofilesUpdateAll" = "全部更新" +"geofileUpdatePopover" = "地理檔案更新成功" +"dontRefresh" = "安裝中,請勿重新整理此頁面" +"logs" = "日誌" +"config" = "設定" +"backup" = "備份與還原" +"backupTitle" = "備份與還原資料庫" +"exportDatabase" = "本地備份" +"exportDatabaseDesc" = "點擊下載包含目前資料庫備份的 .db 檔案到您的裝置。" +"importDatabase" = "本地還原" +"importDatabaseDesc" = "點擊選擇並上傳裝置中的 .db 檔案以從備份還原資料庫。" +"importDatabaseSuccess" = "資料庫匯入成功" +"importDatabaseError" = "匯入資料庫時發生錯誤" +"readDatabaseError" = "讀取資料庫時發生錯誤" +"getDatabaseError" = "擷取資料庫時發生錯誤" +"getConfigError" = "擷取設定檔時發生錯誤" +"betterPanel" = "一個更好的面板" +"builtOnXray" = "基於 Xray Core 建置" +"xpanelTitle" = "〔X-Panel面板〕" +"tgPrivateChat" = "TG私聊交流" +"tgGroupChat" = "〔X-Panel面板〕交流群" +"portCheck" = "連接埠偵測" +"speedTest" = "網路測速" +"datasnapshot" = "建立資料快照" +"emergencyrecovery" = "遠端急救還原" + +[pages.inbounds] +"allTimeTraffic" = "累計總流量" +"allTimeTrafficUsage" = "所有时间总使用量" +"title" = "入站列表" +"totalDownUp" = "總上傳 / 下載" +"totalUsage" = "總用量" +"inboundCount" = "入站數量" +"operate" = "選單" +"enable" = "啟用" +"remark" = "備註" +"protocol" = "協議" +"port" = "連接埠" +"portMap" = "連接埠映射" +"traffic" = "流量" +"details" = "詳細資訊" +"transportConfig" = "傳輸設定" +"expireDate" = "到期時間" +"createdAt" = "建立時間" +"updatedAt" = "更新時間" +"resetTraffic" = "重置流量" +"addInbound" = "新增入站" +"generalActions" = "通用操作" +"autoRefresh" = "自動重新整理" +"autoRefreshInterval" = "間隔" +"create" = "新增" +"update" = "修改" +"modifyInbound" = "修改入站" +"deleteInbound" = "刪除入站" +"deleteInboundContent" = "確定要刪除入站嗎?" +"deleteClient" = "刪除客戶端" +"deleteClientContent" = "確定要刪除客戶端嗎?" +"resetTrafficContent" = "確定要重設流量嗎?" +"copyLink" = "複製連結" +"address" = "位址" +"network" = "網路" +"destinationPort" = "目標連接埠" +"targetAddress" = "目標位址" +"monitorDesc" = "留空表示監聽所有 IP" +"meansNoLimit" = "= 無限制(單位:GB)" +"totalFlow" = "總流量" +"leaveBlankToNeverExpire" = "留空表示永不過期" +"noRecommendKeepDefault" = "建議保留預設值" +"certificatePath" = "檔案路徑" +"certificateContent" = "檔案內容" +"publicKey" = "公鑰" +"privatekey" = "私鑰" +"clickOnQRcode" = "點擊二維碼複製" +"client" = "客戶/用戶" +"clients" = "所有客戶端" +"export" = "匯出連結" +"clone" = "克隆" +"cloneInbound" = "克隆" +"cloneInboundContent" = "此入站規則除連接埠(Port)、監聽 IP(Listening IP)和客戶端(Clients)以外的所有設定都將應用於克隆" +"cloneInboundOk" = "創建克隆" +"resetAllTraffic" = "重設所有入站流量" +"resetAllTrafficTitle" = "重設所有入站流量" +"resetAllTrafficContent" = "確定要重設所有入站流量嗎?" +"resetInboundClientTraffics" = "重設客戶端流量" +"resetInboundClientTrafficTitle" = "重設所有客戶端流量" +"resetInboundClientTrafficContent" = "確定要重設此入站客戶端的所有流量嗎?" +"resetAllClientTraffics" = "重設所有客戶端流量" +"resetAllClientTrafficTitle" = "重設所有客戶端流量" +"resetAllClientTrafficContent" = "確定要重設所有客戶端的所有流量嗎?" +"delDepletedClients" = "刪除流量耗盡的客戶端" +"delDepletedClientsTitle" = "刪除流量耗盡的客戶端" +"delDepletedClientsContent" = "確定要刪除所有流量耗盡的客戶端嗎?" +"email" = "電子郵件" +"emailDesc" = "電子郵件必須確保唯一" +"IPLimit" = "IP 限制" +"IPLimitDesc" = "如果數量超過設定值,則停用入站流量。(0 = 停用)" +"IPLimitlog" = "IP 日誌" +"IPLimitlogDesc" = "IP 歷史日誌(要啟用被停用的入站流量,請清除日誌)" +"IPLimitlogclear" = "清除日誌" +"setDefaultCert" = "從面板設定憑證" +"telegramDesc" = "請提供 Telegram 聊天 ID。(在機器人中使用 '/id' 指令或跟 @userinfobot 機器人對話獲取)" +"subscriptionDesc" = "要找到您的訂閱 URL,請導覽至「詳細資訊」。此外,您可以為多個客戶端使用相同的名稱。" +"info" = "資訊" +"same" = "相同" +"inboundData" = "入站資料" +"exportInbound" = "匯出入站規則" +"import" = "匯入" +"importInbound" = "匯入入站規則" +"reviewTitle" = "請確認以下設定資訊" +"neverExpire" = "永不過期" +"reviewHint" = "如需修改請返回上一步" +"unlimited" = "無限制" +"deviceLimit" = "裝置限制" +"deviceLimitDesc"="請輸入具體數量,\r\n0表示不限制(留空也表示不限制)" +"speedLimit"="獨立限速" +"speedLimitDesc"="設定該使用者的最大〔上傳/下載速度〕,\r\n單位 KB/s,0 表示不限速" +"oneClickConfig"="一鍵配置" +"is_subConversion"="訂閱轉換" +"confirmCreate"="確認提交創建" + +[pages.client] +"add" = "新增客戶端" +"edit" = "編輯客戶端" +"submitAdd" = "新增客戶端" +"submitEdit" = "儲存修改" +"clientCount" = "客戶端數量" +"bulk" = "批次創建" +"method" = "方法" +"first" = "置頂" +"last" = "置底" +"prefix" = "前綴" +"postfix" = "後綴" +"delayedStart" = "首次使用後開始" +"expireDays" = "到期天數/期間" +"days" = "天" +"renew" = "自動續約" +"renewDesc" = "到期後自動續約。(0 = 停用)(單位: 天)" + +[pages.inbounds.toasts] +"obtain" = "獲取" +"updateSuccess" = "更新成功" +"logCleanSuccess" = "日誌已清除" +"inboundsUpdateSuccess" = "入站連線已成功更新" +"inboundUpdateSuccess" = "入站連線已成功更新" +"inboundCreateSuccess" = "入站連線已成功創建" +"inboundDeleteSuccess" = "入站連線已成功刪除" +"inboundClientAddSuccess" = "已新增入站客戶端" +"inboundClientDeleteSuccess" = "入站客戶端已刪除" +"inboundClientUpdateSuccess" = "入站客戶端已更新" +"delDepletedClientsSuccess" = "所有耗盡客戶端已刪除" +"resetAllClientTrafficSuccess" = "客戶端所有流量已重設" +"resetAllTrafficSuccess" = "所有流量已重設" +"resetInboundClientTrafficSuccess" = "流量已重設" +"trafficGetError" = "獲取流量資料時發生錯誤" +"getNewX25519CertError" = "獲取 X25519 憑證時發生錯誤。" +"getNewmldsa65Error" = "獲取 mldsa65 憑證時發生錯誤。" +"getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。" + +[pages.inbounds.stream.general] +"request" = "請求" +"response" = "回應" +"name" = "名稱" +"value" = "值" + +[pages.inbounds.stream.tcp] +"version" = "版本" +"method" = "方法" +"path" = "路徑" +"status" = "狀態" +"statusDescription" = "狀態說明" +"requestHeader" = "請求標頭" +"responseHeader" = "回應標頭" + +[pages.inbounds.oneClick] +"title" = "一鍵配置" +"prompt" = "選擇一個預設配置快速建立入站連線。" +"presetType" = "預設協定配置類型" +"generateTab" = "產生配置" +"historyTab" = "歷史記錄" +"historyType" = "生成類型" +"historyLink" = "連結" +"historyTime" = "生成時間" +"clickToCopyTitle" = "點擊複製連結" +"scanQrcodeTitle" = "掃描二維碼" +"clickToCopyHint" = "(點圖也可複製)" +"linkCreated" = "入站連結已創建,請複製以下地址:" +"descriptionTitle" = "\r\n\r\n用法說明:" +"descriptionContent" = "1、此功能將自動產生 VLESS + TCP + Reality (xtls-rprx-vision) 和 VLESS + XHTTP + Reality 協定的入站,以上最前面兩種協定組合適合直連,\r\n2、或 VLESS Encryption + XHTTP + TLS(可選套CDN)協定的入站,\r\n3、並隨機分配一個可用端口,請確保此端口放行,生成後請直接複製【鏈接地址】,\r\n4、此處的【一鍵配置】生成功能,跟【入站】的數據是打通的,\r\n5、你可以在一鍵創建後於列表中,手動查看/複製或編輯信息,以便添加其他參數。" + +[pages.inbounds.oneClick.preset] +"vless_reality" = "VLESS + TCP + Reality + Vision" +"vless_xhttp_reality" = "VLESS + XHTTP + Reality" +"vless_tls_encryption" = "VLESS Encryption + TLS(可選套CDN)" +"switch_vision_seed_dev" = "Switch + Vision Seed (開發中)" + +[pages.inbounds.subConversion] +"modalTitle" = "跳轉提示......" +"modalContent" = "\r\n即將偵測並跳轉至:\r\n您的訂閱轉換服務頁面,是否繼續?" +"modalOk" = "繼續" +"notFoundTitle" = "【訂閱轉換服務】未安裝或無法存取" +"notFoundContent" = "\r\n請進入VPS伺服器終端,輸入 x-ui 指令,\r\n選擇選項【25】進行安裝。" + +[pages.settings] +"title" = "面板設定" +"save" = "儲存" +"infoDesc" = "此處的所有變更都需要儲存並重啟面板才能生效" +"restartPanel" = "重啟面板" +"restartPanelDesc" = "確定要重啟面板嗎?若重啟後無法存取面板,請前往伺服器查看面板日誌資訊" +"restartPanelSuccess" = "面板已成功重啟" +"actions" = "操作" +"resetDefaultConfig" = "重設為預設設定" +"panelSettings" = "一般" +"securitySettings" = "安全設定" +"TGBotSettings" = "Telegram 機器人設定" +"panelListeningIP" = "面板監聽 IP" +"panelListeningIPDesc" = "預設留空監聽所有 IP" +"panelListeningDomain" = "面板監聽網域" +"panelListeningDomainDesc" = "預設情況下留空以監視所有網域和 IP 位址" +"panelPort" = "面板監聽連接埠" +"panelPortDesc" = "重啟面板生效" +"publicKeyPath" = "面板憑證公鑰檔案路徑" +"DefaultpublicKeyPath" = "/root/.acme.sh/網域_ecc/網域.cer" +"publicKeyPathDesc" = "填寫一個 '/' 開頭的絕對路徑,〔acme 方式〕請自行在填入時修改網域" +"privateKeyPath" = "面板憑證金鑰檔案路徑" +"DefaultprivateKeyPath" = "/root/.acme.sh/網域_ecc/網域.key" +"privateKeyPathDesc" = "填寫一個 '/' 開頭的絕對路徑,〔acme 方式〕請自行在填入時修改網域" +"panelUrlPath" = "面板登入存取路徑" +"panelUrlPathDesc" = "必須以 '/' 開頭,以 '/' 結尾" +"pageSize" = "分頁大小" +"pageSizeDesc" = "定義入站列表的頁面大小。設定 0 表示停用" +"remarkModel" = "備註模型與分隔符" +"datepicker" = "日期選擇器" +"datepickerPlaceholder" = "選擇日期" +"datepickerDescription" = "選擇器日曆類型指定到期日期" +"sampleRemark" = "備註範例" +"oldUsername" = "原用戶名" +"currentPassword" = "原密碼" +"newUsername" = "新用戶名" +"newPassword" = "新密碼" +"telegramBotEnable" = "啟用 Telegram 機器人" +"telegramBotEnableDesc" = "啟用 Telegram 機器人功能" +"telegramToken" = "Telegram 機器人權杖(token)" +"telegramTokenDesc" = "跟 '@BotFather' 對話獲取的 Telegram 機器人權杖" +"telegramProxy" = "SOCKS5 Proxy" +"telegramProxyDesc" = "啟用 SOCKS5 代理連線到 Telegram(根據指南調整設定)" +"telegramAPIServer" = "Telegram API Server" +"telegramAPIServerDesc" = "要使用的 Telegram API 伺服器。留空以使用預設伺服器。" +"telegramChatId" = "管理員聊天 ID" +"telegramChatIdDesc" = "Telegram 管理員聊天 ID (多個以逗號分隔)(可透過 @userinfobot 獲取,或在機器人中使用 '/id' 指令獲取)" +"telegramNotifyTime" = "通知時間" +"telegramNotifyTimeDesc" = "設定週期性的 Telegram 機器人通知時間(使用 crontab 時間格式)" +"tgNotifyBackup" = "資料庫備份" +"tgNotifyBackupDesc" = "傳送帶有報告的資料庫備份檔案" +"tgNotifyLogin" = "登入通知" +"tgNotifyLoginDesc" = "當有人試圖登入您的面板時顯示用戶名、IP 位址和時間" +"sessionMaxAge" = "會話時長" +"sessionMaxAgeDesc" = "保持登入狀態的時長(單位:分鐘)" +"expireTimeDiff" = "到期通知閾值" +"expireTimeDiffDesc" = "達到此閾值時,將收到有關到期時間的通知(單位:天)" +"trafficDiff" = "流量耗盡閾值" +"trafficDiffDesc" = "達到此閾值時,將收到有關流量耗盡的通知(單位:GB)" +"tgNotifyCpu" = "CPU 負載通知閾值" +"tgNotifyCpuDesc" = "CPU 負載超過此閾值時,將收到通知(單位:%)" +"timeZone" = "時區" +"timeZoneDesc" = "定時任務將按照該時區的時間執行" +"subSettings" = "訂閱設定" +"subEnable" = "啟用訂閱服務" +"subEnableDesc" = "啟用訂閱服務功能" +"subTitle" = "訂閱標題" +"subTitleDesc" = "在 VPN 客戶端中顯示的標題" +"subListen" = "監聽 IP" +"subListenDesc" = "訂閱服務監聽的 IP 位址(留空表示監聽所有 IP)" +"subPort" = "監聽連接埠" +"subPortDesc" = "訂閱服務監聽的連接埠號(必須是未使用的連接埠)" +"subCertPath" = "公鑰路徑" +"subCertPathDesc" = "訂閱服務使用的公鑰檔案路徑(以 '/' 開頭)" +"subKeyPath" = "私鑰路徑" +"subKeyPathDesc" = "訂閱服務使用的私鑰檔案路徑(以 '/' 開頭)" +"subPath" = "URI 路徑" +"subPathDesc" = "訂閱服務使用的 URI 路徑(以 '/' 開頭,以 '/' 結尾)" +"subDomain" = "監聽網域" +"subDomainDesc" = "訂閱服務監聽的網域(留空表示監聽所有網域和 IP)" +"subUpdates" = "更新間隔" +"subUpdatesDesc" = "客戶端應用中訂閱 URL 的更新間隔(單位:小時)" +"subEncrypt" = "編碼" +"subEncryptDesc" = "訂閱服務返回的內容將採用 Base64 編碼" +"subShowInfo" = "顯示使用資訊" +"subShowInfoDesc" = "客戶端應用中將顯示剩餘流量和日期資訊" +"subURI" = "反向代理 URI" +"subURIDesc" = "用於代理後面的訂閱 URL 的 URI 路徑" +"externalTrafficInformEnable" = "外部流量通知" +"externalTrafficInformEnableDesc" = "每次流量更新時通知外部 API" +"externalTrafficInformURI" = "外部流量通知 URI" +"externalTrafficInformURIDesc" = "流量更新將傳送到此 URI" +"fragment" = "分片" +"fragmentDesc" = "啟用 TLS hello 封包分片" +"fragmentSett" = "設定" +"noisesDesc" = "啟用 Noises." +"noisesSett" = "Noises 設定" +"mux" = "多路複用器" +"muxDesc" = "在已建立的資料流內傳輸多個獨立的資料流" +"muxSett" = "複用器設定" +"direct" = "直接連線" +"directDesc" = "直接與特定國家的網域或 IP 範圍建立連線" +"notifications" = "通知" +"certs" = "憑證" +"externalTraffic" = "外部流量" +"dateAndTime" = "日期與時間" +"proxyAndServer" = "代理與伺服器" +"intervals" = "間隔" +"information" = "資訊" +"language" = "語言" +"telegramBotLanguage" = "Telegram 機器人語言" + +[pages.xray] +"title" = "Xray 設定" +"save" = "儲存" +"restart" = "重新啟動 Xray" +"restartSuccess" = "Xray 已成功重新啟動" +"stopSuccess" = "Xray 已成功停止" +"restartError" = "重啟 Xray 時發生錯誤。" +"stopError" = "停止 Xray 時發生錯誤。" +"basicTemplate" = "基礎設定" +"advancedTemplate" = "進階設定" +"generalConfigs" = "一般設定" +"generalConfigsDesc" = "這些選項將決定一般設定" +"logConfigs" = "日誌" +"logConfigsDesc" = "日誌可能會影響伺服器的效能,建議僅在需要時啟用" +"blockConfigsDesc" = "這些選項將阻止使用者連線到特定協議和網站" +"basicRouting" = "基本路由" +"blockConnectionsConfigsDesc" = "這些選項將根據特定的請求國家阻止流量。" +"directConnectionsConfigsDesc" = "直接連線確保特定的流量不會透過其他伺服器路由。" +"blockips" = "阻止 IP" +"blockdomains" = "阻止網域" +"directips" = "直接 IP" +"directdomains" = "直接網域" +"ipv4Routing" = "IPv4 路由" +"ipv4RoutingDesc" = "此選項將僅透過 IPv4 路由到目標網域" +"warpRouting" = "WARP 路由" +"warpRoutingDesc" = "注意:在使用這些選項之前,請按照面板 GitHub 上的步驟在您的伺服器上以 socks5 代理模式安裝 WARP。WARP 將透過 Cloudflare 伺服器將流量路由到網站。" +"Template" = "進階 Xray 設定模板" +"TemplateDesc" = "最終的 Xray 設定檔將基於此模板產生" +"FreedomStrategy" = "Freedom 協議策略" +"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略" +"RoutingStrategy" = "設定路由網域策略" +"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略" +"Torrent" = "封鎖 BitTorrent 協議" +"Inbounds" = "入站規則" +"InboundsDesc" = "接受來自特定客戶端的流量" +"Outbounds" = "出站規則" +"Balancers" = "負載平衡" +"OutboundsDesc" = "設定出站流量傳出方式" +"Routings" = "路由規則" +"RoutingsDesc" = "每條規則的優先級都很重要" +"completeTemplate" = "全部" +"logLevel" = "日誌級別" +"logLevelDesc" = "錯誤日誌的日誌級別,用於指示需要記錄的資訊" +"accessLog" = "存取日誌" +"accessLogDesc" = "存取日誌的檔案路徑。特殊值 'none' 停用存取日誌" +"errorLog" = "錯誤日誌" +"errorLogDesc" = "錯誤日誌的檔案路徑。特殊值 'none' 停用錯誤日誌" +"dnsLog" = "DNS 日誌" +"dnsLogDesc" = "是否啟用 DNS 查詢日誌" +"maskAddress" = "隱藏位址" +"maskAddressDesc" = "IP 位址遮罩,啟用時會自動替換日誌中出現的 IP 位址。" +"statistics" = "統計" +"statsInboundUplink" = "入站上傳統計" +"statsInboundUplinkDesc" = "啟用所有入站代理的上行流量統計收集。" +"statsInboundDownlink" = "入站下載統計" +"statsInboundDownlinkDesc" = "啟用所有入站代理的下行流量統計收集。" +"statsOutboundUplink" = "出站上傳統計" +"statsOutboundUplinkDesc" = "啟用所有出站代理的上行流量統計收集。" +"statsOutboundDownlink" = "出站下載統計" +"statsOutboundDownlinkDesc" = "啟用所有出站代理的下行流量統計收集。" + +[pages.navigation] +"title" = "實用導覽" + +[pages.controlledmanagement] +"title" = "主從管理" + +[pages.xray.rules] +"first" = "置頂" +"last" = "置底" +"up" = "向上" +"down" = "向下" +"source" = "來源" +"dest" = "目標位址" +"inbound" = "入站" +"outbound" = "出站" +"balancer" = "負載平衡" +"info" = "資訊" +"add" = "新增規則" +"edit" = "編輯規則" +"useComma" = "逗號分隔的項目" +"DomainMatcher" = "網域匹配類型" +"SourceIPs" = "來源 IP" +"SourcePort" = "來源連接埠" +"Network" = "網路類型" +"Protocol" = "傳輸協議" +"Attributes" = "屬性" +"Domain" = "網域" +"User" = "使用者" +"Port" = "連接埠" +"InboundTag" = "入站 Tag" +"OutboundTag" = "出站 Tag" +"BalancerTag" = "負載平衡 Tag" + +[pages.xray.outbound] +"addOutbound" = "新增出站" +"addReverse" = "新增反向" +"editOutbound" = "編輯出站" +"editReverse" = "編輯反向" +"tag" = "標籤" +"tagDesc" = "唯一標籤" +"address" = "位址" +"reverse" = "反向" +"domain" = "網域" +"type" = "類型" +"bridge" = "Bridge" +"portal" = "Portal" +"link" = "連結" +"intercon" = "互連" +"settings" = "設定" +"accountInfo" = "帳戶資訊" +"outboundStatus" = "出站狀態" +"sendThrough" = "傳送通過" + +[pages.xray.balancer] +"addBalancer" = "新增負載平衡" +"editBalancer" = "編輯負載平衡" +"balancerStrategy" = "策略" +"balancerSelectors" = "選擇器" +"tag" = "標籤" +"tagDesc" = "唯一標籤" +"balancerDesc" = "無法同時使用 balancerTag 和 outboundTag。如果同時使用,則只有 outboundTag 會生效。" + +[pages.xray.wireguard] +"secretKey" = "金鑰" +"publicKey" = "公鑰" +"allowedIPs" = "允許的 IP" +"endpoint" = "端點" +"psk" = "共享金鑰" +"domainStrategy" = "網域策略" + +[pages.xray.dns] +"enable" = "啟用 DNS" +"enableDesc" = "啟用內建 DNS 伺服器" +"tag" = "DNS 入站標籤" +"tagDesc" = "此標籤將在路由規則中可用作入站標籤" +"clientIp" = "客戶端 IP" +"clientIpDesc" = "用於在 DNS 查詢期間通知伺服器指定的 IP 位置" +"disableCache" = "停用快取" +"disableCacheDesc" = "停用 DNS 快取" +"disableFallback" = "停用回退" +"disableFallbackDesc" = "停用回退 DNS 查詢" +"disableFallbackIfMatch" = "匹配時停用回退" +"disableFallbackIfMatchDesc" = "當 DNS 伺服器的匹配網域列表命中時,停用回退 DNS 查詢" +"enableParallelQuery" = "啟用並行查詢" +"enableParallelQueryDesc" = "啟用並行“DNS查詢”到多個伺服器以實現更快的解析" +"strategy" = "查詢策略" +"strategyDesc" = "解析網域的總體策略" +"add" = "新增伺服器" +"edit" = "編輯伺服器" +"domains" = "網域" +"expectIPs" = "預期 IP" +"unexpectIPs" = "意外 IP" +"useSystemHosts" = "使用系統 Hosts" +"useSystemHostsDesc" = "使用已安裝系統的 hosts 檔案" +"usePreset" = "使用模板" +"dnsPresetTitle" = "DNS 模板" +"dnsPresetFamily" = "家庭" + +[pages.xray.fakedns] +"add" = "新增假 DNS" +"edit" = "編輯假 DNS" +"ipPool" = "IP 池子網路" +"poolSize" = "池大小" + +[pages.settings.security] +"admin" = "管理員憑證" +"twoFactor" = "雙重驗證" +"twoFactorEnable" = "啟用 2FA" +"twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。" +"twoFactorModalSetTitle" = "啟用雙重認證" +"twoFactorModalDeleteTitle" = "停用雙重認證" +"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:" +"twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此 QR 碼,或複製 QR 碼附近的權杖並貼到應用程式中" +"twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼" +"twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。" +"twoFactorModalChangeCredentialsTitle" = "變更憑證" +"twoFactorModalChangeCredentialsStep" = "輸入應用程式中的代碼以變更管理員憑證。" +"twoFactorModalSetSuccess" = "雙因素認證已成功建立" +"twoFactorModalDeleteSuccess" = "雙因素認證已成功刪除" +"twoFactorModalError" = "驗證碼錯誤" + +[pages.settings.toasts] +"modifySettings" = "參數已變更。" +"getSettings" = "獲取參數時發生錯誤" +"modifyUserError" = "變更管理員憑證時發生錯誤。" +"modifyUser" = "您已成功變更管理員憑證。" +"originalUserPassIncorrect" = "原用戶名或原密碼錯誤" +"userPassMustBeNotEmpty" = "新用戶名和新密碼不能為空" +"getOutboundTrafficError" = "獲取出站流量錯誤" +"resetOutboundTrafficError" = "重設出站流量錯誤" + +[tgbot] +"keyboardClosed" = "❌ 自訂鍵盤已關閉!" +"noResult" = "❗ 沒有結果!" +"noQuery" = "❌ 找不到查詢!請重新使用指令!" +"wentWrong" = "❌ 出了一點問題!" +"noIpRecord" = "❗ 沒有 IP 記錄!" +"noInbounds" = "❗ 找不到入站連線!" +"unlimited" = "♾ 無限制" +"add" = "新增" +"month" = "月" +"months" = "月" +"day" = "天" +"days" = "天" +"hours" = "小時" +"minutes" = "分鐘" +"unknown" = "未知" +"inbounds" = "入站連線" +"clients" = "客戶端" +"offline" = "🔴 離線" +"online" = "🟢 在線" + +[tgbot.commands] +"unknown" = "❗ 未知指令" +"pleaseChoose" = "👇請〔按照需求〕選擇下方按鈕 :\r\n" +"help" = "🤖 歡迎使用本機器人!它旨在為您提供來自伺服器的特定資料,並允許您進行必要的修改。\r\n\r\n" +"start" = "👋 你好,{{ .Firstname }}。\r\n" +"welcome" = "🤖 歡迎來到 {{ .Hostname }} 管理機器人。\r\n" +"status" = "✅ 機器人正常運作!" +"usage" = "❗ 請輸入要搜尋的文字!" +"getID" = "🆔 您的 ID 為:{{ .ID }}" +"helpAdminCommands" = "要重新啟動 Xray Core:\r\n/restart\r\n\n要搜尋客戶電子郵件:\r\n/usage [電子郵件]\r\n\r\n要搜尋入站(有客戶統計資料):\r\n\r\n\r\n要搜尋入站(有客戶統計資料):\r\n [備註]\r\n\r\nTelegram聊天ID:\r\n/id\r\n\r\n一鍵設定:\r\n/oneclick\r\n\r\n訂閱轉換:\r\n/subconverter\r\n\r\n重啟〔X-Panel 面板〕:\r\n/restartx" +"helpClientCommands" = "要搜尋統計資料,請使用以下指令:\r\n/usage [電子郵件]\r\n\r\nTelegram 聊天 ID:\r\n/id" +"restartUsage" = "\r\n\r\n/restart" +"restartSuccess" = "✅ 操作成功!" +"restartFailed" = "❗ 操作錯誤。\r\n\r\n錯誤: {{ .Error }}." +"xrayNotRunning" = "❗ Xray Core 未運作。" +"startDesc" = "顯示主選單" +"helpDesc" = "機器人幫助" +"statusDesc" = "檢查機器人狀態" +"idDesc" = "顯示您的 Telegram ID" + +[tgbot.messages] +"cpuThreshold" = "🔴 CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%" +"selectUserFailed" = "❌ 使用者選擇錯誤!" +"userSaved" = "✅ Telegram 用戶已儲存。" +"loginSuccess" = "✅ 成功登入到面板。\r\n" +"loginFailed" = "❗️ 面板登入失敗。\r\n" +"report" = "🕰 定時報告:{{ .RunTime }}\r\n" +"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n" +"hostname" = "💻 主機名稱:{{ .Hostname }}\r\n" +"version" = "🚀 X-Panel 版本:v{{ .Version }}\r\n" +"xrayVersion" = "📡 Xray Core 版本:v{{ .XrayVersion }}\r\n" +"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" +"ip" = "🌐 IP:{{ .IP }}\r\n" +"ips" = "🔢 IP 位址:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ 伺服器運作時間:{{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 伺服器記憶體:{{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP 連線數:{{ .Count }}\r\n" +"udpCount" = "🔸 UDP 連線數:{{ .Count }}\r\n" +"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Xray 狀態:{{ .State }}\r\n" +"username" = "👤 用戶名:{{ .Username }}\r\n" +"password" = "👤 密碼:{{ .Password }}\r\n" +"time" = "⏰ 時間:{{ .Time }}\r\n" +"inbound" = "📍 入站:{{ .Remark }}\r\n" +"port" = "🔌 連接埠:{{ .Port }}\r\n" +"expire" = "📅 過期日期:{{ .Time }}\r\n" +"expireIn" = "📅 剩餘時間:{{ .Time }}\r\n" +"active" = "💡 啟用:{{ .Enable }}\r\n" +"enabled" = "🚨 已啟用:{{ .Enable }}\r\n" +"online" = "🌐 連線狀態:{{ .Status }}\r\n" +"email" = "📧 電子郵件(用戶):{{ .Email }}\r\n" +"upload" = "🔼 上傳↑:{{ .Upload }}\r\n" +"download" = "🔽 下載↓:{{ .Download }}\r\n" +"total" = "📊 總計:{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Telegram 用戶:{{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 耗盡的 {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 耗盡的 {{ .Type }} 數量:\r\n" +"onlinesCount" = "🌐 在線客戶:{{ .Count }}\r\n" +"disabled" = "🛑 停用:{{ .Disabled }}\r\n" +"depleteSoon" = "🔜 即將耗盡:{{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 備份時間:{{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n" +"yes" = "✅ 是的" +"no" = "❌ 沒有" +"received_id" = "🔑📥 ID 已更新。" +"received_password" = "🔑📥 密碼已更新。" +"received_email" = "📧📥 電子郵件已更新。" +"received_comment" = "💬📥 評論已更新。" +"id_prompt" = "🔑 預設 ID:{{ .ClientId }}\n\n請輸入您的 ID。" +"pass_prompt" = "🔑 預設密碼:{{ .ClientPassword }}\n\n請輸入您的密碼。" +"email_prompt" = "📧 預設電子郵件:{{ .ClientEmail }}\n\n請輸入您的電子郵件。" +"comment_prompt" = "💬 預設評論:{{ .ClientComment }}\n\n請輸入您的評論。" +"inbound_client_data_id" = "🔄 入站:{{ .InboundRemark }}\n\n🔑 ID:{{ .ClientId }}\n📧 電子郵件:{{ .ClientEmail }}\n📊 流量:{{ .ClientTraffic }}\n📅 到期日期:{{ .ClientExp }}\n🌐 IP 限制:{{ .IpLimit }}\n💬 備註:{{ .ClientComment }}\n\n您現在可以將客戶新增到入站了!" +"inbound_client_data_pass" = "🔄 入站:{{ .InboundRemark }}\n\n🔑 密碼:{{ .ClientPass }}\n📧 電子郵件:{{ .ClientEmail }}\n📊 流量:{{ .ClientTraffic }}\n📅 到期日期:{{ .ClientExp }}\n🌐 IP 限制:{{ .IpLimit }}\n💬 備註:{{ .ClientComment }}\n\n您現在可以將客戶新增到入站了!" +"cancel" = "❌ 流程已取消!\n\n您可以隨時使用 /start 重新開始。 🔄" +"error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}" +"using_default_value" = "好的,我會使用預設值。 😊" +"incorrect_input" ="您的輸入無效。\n片語應連續輸入,不能有空格。\n正確範例:aaaaaa\n錯誤範例:aaa aaa 🚫" +"AreYouSure" = "您確定嗎?🤔" +"SuccessResetTraffic" = "📧 電子郵件:{{ .ClientEmail }}\n🏁 結果:✅ 成功" +"FailedResetTraffic" = "📧 電子郵件:{{ .ClientEmail }}\n🏁 結果:❌ 失敗 \n\n🛠️ 錯誤:[ {{ .ErrorMessage }} ]" +"FinishProcess" = "🔚 所有客戶的流量重設已完成。" + +[tgbot.buttons] +"closeKeyboard" = "❌ 關閉鍵盤" +"cancel" = "❌ 取消" +"cancelReset" = "❌ 取消重設" +"cancelIpLimit" = "❌ 取消 IP 限制" +"confirmResetTraffic" = "✅ 確認重設流量?" +"confirmClearIps" = "✅ 確認清除 IP?" +"confirmRemoveTGUser" = "✅ 確認移除 Telegram 用戶?" +"confirmToggle" = "✅ 確認啟用/停用用戶?" +"dbBackup" = "獲取資料庫備份" +"serverUsage" = "🌐伺服器狀態" +"getInbounds" = "獲取入站資訊" +"depleteSoon" = "即將耗盡" +"clientUsage" = "獲取使用情況" +"onlines" = "在線客戶端" +"commands" = "常用指令" +"refresh" = "🔄 重新整理" +"clearIPs" = "❌ 清除 IP" +"removeTGUser" = "❌ 移除 Telegram 用戶" +"selectTGUser" = "👤 選擇 Telegram 用戶" +"selectOneTGUser" = "👤 選擇一個 Telegram 用戶:" +"resetTraffic" = "📈 重設流量" +"resetExpire" = "📅 變更到期日期" +"ipLog" = "🔢 IP 日誌" +"ipLimit" = "🔢 IP 限制" +"setTGUser" = "👤 設定 Telegram 用戶" +"toggle" = "🔘 啟用/停用" +"custom" = "🔢 自訂輸入" +"confirmNumber" = "✅ 確認:{{ .Num }}" +"confirmNumberAdd" = "✅ 確認新增:{{ .Num }}" +"limitTraffic" = "🚧 流量限制" +"getBanLogs" = "封鎖日誌" +"allClients" = "所有客戶" +"addClient" = "新增客戶" +"submitDisable" = "提交為停用 ☑️" +"submitEnable" = "提交為啟用 ✅" +"use_default" = "🏷️ 使用預設" +"change_id" = "⚙️🔑 ID" +"change_password" = "⚙️🔑 密碼" +"change_email" = "⚙️📧 電子郵件" +"change_comment" = "⚙️💬 評論" +"ResetAllTraffics" = "重設所有流量" +"SortedTrafficUsageReport" = "流量使用報告" +"oneClick" = "🚀 一鍵配置" +"subconverter" = "🔄 訂閱轉換" + +[tgbot.answers] +"successfulOperation" = "✅ 成功!" +"errorOperation" = "❗ 操作錯誤。" +"getInboundsFailed" = "❌ 獲取入站資訊失敗。" +"getClientsFailed" = "❌ 獲取客戶失敗。" +"canceled" = "❌ {{ .Email }}:操作已取消。" +"clientRefreshSuccess" = "✅ {{ .Email }}:客戶端重新整理成功。" +"IpRefreshSuccess" = "✅ {{ .Email }}:IP 重新整理成功。" +"TGIdRefreshSuccess" = "✅ {{ .Email }}:客戶端的 Telegram 用戶重新整理成功。" +"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重設成功。" +"setTrafficLimitSuccess" = "✅ {{ .Email }}:流量限制儲存成功。" +"expireResetSuccess" = "✅ {{ .Email }}:過期天數已重設成功。" +"resetIpSuccess" = "✅ {{ .Email }}:成功儲存 IP 限制數量為 {{ .Count }}。" +"clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。" +"getIpLog" = "✅ {{ .Email }}:獲取 IP 日誌。" +"getUserInfo" = "✅ {{ .Email }}:獲取 Telegram 用戶資訊。" +"removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用戶已成功移除。" +"enableSuccess" = "✅ {{ .Email }}:已成功啟用。" +"disableSuccess" = "✅ {{ .Email }}:已成功停用。" +"askToAddUserId" = "找不到您的設定!\r\n請向管理員詢問,在您的設定中使用您的 Telegram 用戶 ChatID。\r\n\r\n您的用戶 ChatID:{{ .TgUserID }}" +"chooseClient" = "為入站 {{ .Inbound }} 選擇一個客戶" +"chooseInbound" = "選擇一個入站" diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..ddec611 --- /dev/null +++ b/web/web.go @@ -0,0 +1,567 @@ +package web + +import ( + "context" + "crypto/tls" + "embed" + "html/template" + "io" + "io/fs" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" + + "x-ui/config" + "x-ui/logger" + "x-ui/util/common" + "x-ui/web/controller" + "x-ui/web/job" + "x-ui/web/locale" + "x-ui/web/middleware" + "x-ui/web/network" + "x-ui/web/service" + + "github.com/gin-contrib/gzip" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/robfig/cron/v3" +) + +//go:embed assets/* +var assetsFS embed.FS + +//go:embed html/* +var htmlFS embed.FS + +//go:embed translation/* +var i18nFS embed.FS + +var startTime = time.Now() + +// 预定义 IPv4 私网和回环网段 +var privateIPv4Nets []*net.IPNet + +type wrapAssetsFS struct { + embed.FS +} + +// Keep-Alive 监听器包装器:用于拦截新连接并设置 Keep-Alive 选项 +type keepAliveListener struct { + *net.TCPListener + KeepAlivePeriod time.Duration +} + +// Accept 方法:拦截连接并设置 Keep-Alive +func (l keepAliveListener) Accept() (net.Conn, error) { + // 1. 接受底层 TCP 连接 + tc, err := l.TCPListener.AcceptTCP() + if err != nil { + return nil, err + } + + // 2. 在 *net.TCPConn 上设置 Keep-Alive 属性 (这里的方法是正确的) + if err := tc.SetKeepAlive(true); err != nil { + logger.Warning("Failed to set KeepAlive:", err) + } + // 设置心跳包周期为 5 秒 + if err := tc.SetKeepAlivePeriod(l.KeepAlivePeriod); err != nil { + logger.Warning("Failed to set KeepAlivePeriod:", err) + } + + return tc, nil +} + +func (f *wrapAssetsFS) Open(name string) (fs.File, error) { + file, err := f.FS.Open("assets/" + name) + if err != nil { + return nil, err + } + return &wrapAssetsFile{ + File: file, + }, nil +} + +type wrapAssetsFile struct { + fs.File +} + +func (f *wrapAssetsFile) Stat() (fs.FileInfo, error) { + info, err := f.File.Stat() + if err != nil { + return nil, err + } + return &wrapAssetsFileInfo{ + FileInfo: info, + }, nil +} + +type wrapAssetsFileInfo struct { + fs.FileInfo +} + +func (f *wrapAssetsFileInfo) ModTime() time.Time { + return startTime +} + +type Server struct { + httpServer *http.Server + listener net.Listener + + index *controller.IndexController + server *controller.ServerController + panel *controller.XUIController + api *controller.APIController + + xrayService service.XrayService + settingService service.SettingService + tgbotService service.TelegramService + // 〔中文注释〕: 添加这个字段,用来“持有”从 main.go 传递过来的 serverService 实例。 + serverService service.ServerService + + cron *cron.Cron + + ctx context.Context + cancel context.CancelFunc +} + + +// 【新增方法】:用于 main.go 将创建好的 tgBotService 注入进来 +func (s *Server) SetTelegramService(tgService service.TelegramService) { + s.tgbotService = tgService +} + +// 〔中文注释〕: 1. 让 NewServer 能够接收一个 serverService 实例作为参数。 +func NewServer(serverService service.ServerService) *Server { + ctx, cancel := context.WithCancel(context.Background()) + return &Server{ + ctx: ctx, + cancel: cancel, + // 〔中文注释〕: 2. 将传入的 serverService 存储到 Server 结构体的字段中。 + serverService: serverService, + } +} + + +func (s *Server) getHtmlFiles() ([]string, error) { + files := make([]string, 0) + dir, _ := os.Getwd() + err := fs.WalkDir(os.DirFS(dir), "web/html", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + files = append(files, path) + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) { + // 这里用 htmlFS(//go:embed html/*)而不是“templates” + t := template.New("").Funcs(funcMap) + + // 递归遍历 embed 的 html 目录,解析所有 .html 模板 + err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".html") { + return nil + } + + // 读出模板内容 + b, err := htmlFS.ReadFile(path) + if err != nil { + return err + } + + // 去掉前缀“html/”,让 {{template "form/inbound"}} 这种名字能被正确找到 + name := strings.TrimPrefix(path, "html/") + _, err = t.New(name).Parse(string(b)) + return err + }) + if err != nil { + return nil, err + } + return t, nil +} + + +func (s *Server) initRouter() (*gin.Engine, error) { + if config.IsDebug() { + gin.SetMode(gin.DebugMode) + } else { + gin.DefaultWriter = io.Discard + gin.DefaultErrorWriter = io.Discard + gin.SetMode(gin.ReleaseMode) + } + + engine := gin.Default() + + webDomain, err := s.settingService.GetWebDomain() + if err != nil { + return nil, err + } + + if webDomain != "" { + engine.Use(middleware.DomainValidatorMiddleware(webDomain)) + } + + secret, err := s.settingService.GetSecret() + if err != nil { + return nil, err + } + + basePath, err := s.settingService.GetBasePath() + if err != nil { + return nil, err + } + engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"}))) + assetsBasePath := basePath + "assets/" + + store := cookie.NewStore(secret) + engine.Use(sessions.Sessions("3x-ui", store)) + engine.Use(func(c *gin.Context) { + c.Set("base_path", basePath) + }) + engine.Use(func(c *gin.Context) { + uri := c.Request.RequestURI + if strings.HasPrefix(uri, assetsBasePath) { + c.Header("Cache-Control", "max-age=31536000") + } + }) + + // init i18n + err = locale.InitLocalizer(i18nFS, &s.settingService) + if err != nil { + return nil, err + } + + // Apply locale middleware for i18n + i18nWebFunc := func(key string, params ...string) string { + return locale.I18n(locale.Web, key, params...) + } + engine.FuncMap["i18n"] = i18nWebFunc + engine.Use(locale.LocalizerMiddleware()) + + // set static files and template + if config.IsDebug() { + // for development + files, err := s.getHtmlFiles() + if err != nil { + return nil, err + } + engine.LoadHTMLFiles(files...) + engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets"))) + } else { + // for production + template, err := s.getHtmlTemplate(engine.FuncMap) + if err != nil { + return nil, err + } + engine.SetHTMLTemplate(template) + engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) + } + + // Apply the redirect middleware (`/xui` to `/panel`) + engine.Use(middleware.RedirectMiddleware(basePath)) + + g := engine.Group(basePath) + + s.index = controller.NewIndexController(g) + // 〔中文注释〕: 调用我们刚刚改造过的 NewServerController,并将 s.serverService 作为参数传进去。 + s.server = controller.NewServerController(g, s.serverService) + s.panel = controller.NewXUIController(g) + s.api = controller.NewAPIController(g) + + return engine, nil +} + +func (s *Server) startTask() { + err := s.xrayService.RestartXray(true) + if err != nil { + logger.Warning("start xray failed:", err) + } + // Check whether xray is running every second + s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob()) + + // Check if xray needs to be restarted every 30 seconds + s.cron.AddFunc("@every 30s", func() { + if s.xrayService.IsNeedRestartAndSetFalse() { + err := s.xrayService.RestartXray(false) + if err != nil { + logger.Error("restart xray failed:", err) + } + } + }) + + go func() { + time.Sleep(time.Second * 5) + // Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray + s.cron.AddJob("@every 10s", job.NewXrayTrafficJob()) + }() + + // check client ips from log file every 10 sec + s.cron.AddJob("@every 10s", job.NewCheckClientIpJob()) + + // check client ips from log file every day + s.cron.AddJob("@daily", job.NewClearLogsJob()) + + // Make a traffic condition every day, 8:30 + var entry cron.EntryID + isTgbotenabled, err := s.settingService.GetTgbotEnabled() + if (err == nil) && (isTgbotenabled) { + runtime, err := s.settingService.GetTgbotRuntime() + if err != nil || runtime == "" { + logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime) + runtime = "@daily" + } + logger.Infof("Tg notify enabled,run at %s", runtime) + _, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob()) + if err != nil { + logger.Warning("Add NewStatsNotifyJob error", err) + return + } + + // check for Telegram bot callback query hash storage reset + s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob()) + + // Check CPU load and alarm to TgBot if threshold passes + cpuThreshold, err := s.settingService.GetTgCpu() + if (err == nil) && (cpuThreshold > 0) { + s.cron.AddJob("@every 10s", job.NewCheckCpuJob()) + } + } else { + s.cron.Remove(entry) + } +} + +func (s *Server) Start() (err error) { + // This is an anonymous function, no function name + defer func() { + if err != nil { + s.Stop() + } + }() + + loc, err := s.settingService.GetTimeLocation() + if err != nil { + return err + } + s.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds()) + s.cron.Start() + + engine, err := s.initRouter() + if err != nil { + return err + } + + certFile, err := s.settingService.GetCertFile() + if err != nil { + return err + } + keyFile, err := s.settingService.GetKeyFile() + if err != nil { + return err + } + listen, err := s.settingService.GetListen() + if err != nil { + return err + } + port, err := s.settingService.GetPort() + if err != nil { + return err + } + var listenAddr string + + if certFile != "" && keyFile != "" { + // 方式一:配置了证书,启用 HTTPS + // 检查证书是否有效,如果无效则直接报错退出,不允许回退到 HTTP + _, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + logger.Errorf("Error loading certificates, please check the file path and content: %v", err) + return err + } + // 监听用户配置的地址 + listenAddr = net.JoinHostPort(listen, strconv.Itoa(port)) + } else { + // 方式二:未配置证书,强制监听在本地回环地址,仅供 SSH 转发使用 + logger.Info("No certificate configured. Forcing listen address to localhost for security.") + logger.Info("Access is only possible via SSH tunnel (e.g., http://127.0.0.1).") + + // 无论用户在 listen 中填写什么,都强制使用回环地址 + listen = fallbackToLocalhost(listen) + listenAddr = net.JoinHostPort(listen, strconv.Itoa(port)) + } + + // 1. 使用 baseListener 临时变量接收 net.Listen 的结果,这是底层的 TCP 监听器 + baseListener, err := net.Listen("tcp", listenAddr) + if err != nil { + return err + } + + // 声明最终要使用的 listener 变量 + var listener net.Listener + + // 2. 尝试将 net.Listener 断言为 *net.TCPListener,以便进行更底层的设置 + tcpListener, ok := baseListener.(*net.TCPListener) + if !ok { + // 如果断言失败 (例如在某些特殊环境或测试中),则直接使用原始的 listener,不设置 Keep-Alive + logger.Warning("监听器不是 TCPListener 类型, 无法设置 Keep-Alive。") + listener = baseListener + } else { + // 3. 【核心功能】: 使用自定义的包装器为每一个新的连接设置 Keep-Alive 属性 + kaListener := &keepAliveListener{ + TCPListener: tcpListener, + KeepAlivePeriod: 5 * time.Second, // 将 Keep-Alive 探测周期设置为 5 秒 + } + // 将包装后的监听器赋值给最终的 listener 变量,后续流程将使用这个新的 listener + listener = net.Listener(kaListener) + } + + // 再次检查证书,配置 TLS Listener + if certFile != "" && keyFile != "" { + cert, _ := tls.LoadX509KeyPair(certFile, keyFile) // 这里我们忽略错误,因为上面已经检查过了 + c := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + listener = network.NewAutoHttpsListener(listener) + listener = tls.NewListener(listener, c) + logger.Info("Web server running HTTPS on", listener.Addr()) + } else { + logger.Info("Web server running HTTP on", listener.Addr()) + } + s.listener = listener + + // 修改 s.httpServer 的初始化代码 + s.httpServer = &http.Server{ + Handler: engine, + // 【新增】:设置 120 秒的读写超时,确保 ufw 命令有足够的时间完成 + ReadTimeout: 120 * time.Second, + WriteTimeout: 120 * time.Second, + } + + + go func() { + s.httpServer.Serve(listener) + }() + + s.startTask() + + // 启动 TG Bot + isTgbotenabled, err := s.settingService.GetTgbotEnabled() + if (err == nil) && (isTgbotenabled) { + // 现在直接在注入的实例上调用 Start 方法,而不是 NewTgbot() + // 因为 main.go 已经注入了完整的实例 + if tgbot, ok := s.tgbotService.(*service.Tgbot); ok { + tgbot.Start(i18nFS) + } else { + logger.Warning("Telegram Bot 已启用,但注入的实例类型不正确或为 nil,无法启动。") + } + } + + return nil +} + +func (s *Server) Stop() error { + s.cancel() + s.xrayService.StopXray() + if s.cron != nil { + s.cron.Stop() + } + // 只有在断言成功后,才能调用只在 *service.Tgbot 上定义的 Stop() 和 IsRunning() 方法。 + if tgBot, ok := s.tgbotService.(*service.Tgbot); ok { + if tgBot.IsRunning() { + tgBot.Stop() + } + } + var err1 error + var err2 error + if s.httpServer != nil { + err1 = s.httpServer.Shutdown(s.ctx) + } + if s.listener != nil { + err2 = s.listener.Close() + } + return common.Combine(err1, err2) +} + +func (s *Server) GetCtx() context.Context { + return s.ctx +} + +func (s *Server) GetCron() *cron.Cron { + return s.cron +} + +// isInternalIP 判断是否为私网或回环IP(支持IPv4和IPv6) +func isInternalIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + + if ip4 := ip.To4(); ip4 != nil { + // IPv4 判断是否在私网/回环网段内 + for _, privateNet := range privateIPv4Nets { + if privateNet.Contains(ip4) { + return true + } + } + return false + } + + // IPv6 判断回环或链路本地地址 + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return true + } + + // 判断 IPv6 fc00::/7 私网地址段 + if ip[0]&0xfe == 0xfc { + return true + } + + return false +} + +// fallbackToLocalhost 根据传入地址返回对应的本地回环地址 +func fallbackToLocalhost(listen string) string { + ip := net.ParseIP(listen) + if ip == nil { + // 无法解析则默认回退 IPv4 回环 + return "127.0.0.1" + } + if ip.To4() != nil { + // IPv4 回退 IPv4 回环 + return "127.0.0.1" + } + // IPv6 回退 IPv6 回环 + return "::1" +} + +func init() { + for _, cidr := range []string{ + "10.0.0.0/8", // A类私网 + "172.16.0.0/12", // B类私网 + "192.168.0.0/16", // C类私网 + "100.64.0.0/10", // CGNAT地址段 + "127.0.0.0/8", // 回环 + } { + _, netw, err := net.ParseCIDR(cidr) + if err == nil { + privateIPv4Nets = append(privateIPv4Nets, netw) + } + } +} diff --git a/windows_files/xray_no_window.ps1 b/windows_files/xray_no_window.ps1 new file mode 100644 index 0000000..abe6608 --- /dev/null +++ b/windows_files/xray_no_window.ps1 @@ -0,0 +1 @@ +Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden diff --git a/windows_files/xray_no_window.vbs b/windows_files/xray_no_window.vbs new file mode 100644 index 0000000..ab6cf14 --- /dev/null +++ b/windows_files/xray_no_window.vbs @@ -0,0 +1 @@ +CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0 diff --git a/x-ui.service b/x-ui.service new file mode 100644 index 0000000..24294a1 --- /dev/null +++ b/x-ui.service @@ -0,0 +1,18 @@ +[Unit] +Description=x-ui Service +Documentation=https://github.com/xeefei/X-Panel +After=network.target +Wants=network.target + +[Service] +Restart=always +TimeoutStopSec=20s + +Environment="XRAY_VMESS_AEAD_FORCED=false" +Type=simple +WorkingDirectory=/usr/local/x-ui/ +ExecStart=/usr/local/x-ui/x-ui +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/x-ui.sh b/x-ui.sh new file mode 100644 index 0000000..923f393 --- /dev/null +++ b/x-ui.sh @@ -0,0 +1,2259 @@ +#!/bin/bash + +red='\033[0;31m' +green='\033[0;32m' +blue='\033[0;34m' +yellow='\033[0;33m' +plain='\033[0m' + +#Add some basic function here +function LOGD() { + echo -e "${yellow}[DEG] $* ${plain}" +} + +function LOGE() { + echo -e "${red}[ERR] $* ${plain}" +} + +function LOGI() { + echo -e "${green}[INF] $* ${plain}" +} + +# check root +[[ $EUID -ne 0 ]] && echo -e "${red}致命错误: ${plain} 请使用 root 权限运行此脚本\n" && exit 1 + +# Check OS and set release variable +if [[ -f /etc/os-release ]]; then + source /etc/os-release + release=$ID +elif [[ -f /usr/lib/os-release ]]; then + source /usr/lib/os-release + release=$ID +else + echo -e "${red}检查服务器操作系统失败,请联系作者!${plain}" >&2 + exit 1 +fi + +echo -e "——————————————————————" +echo -e "当前服务器的操作系统为:${red} $release${plain}" +echo "" +xui_version=$(/usr/local/x-ui/x-ui -v) +last_version=$(curl -Ls "https://api.github.com/repos/xeefei/x-panel/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +echo -e "${green}当前代理面板的版本为: ${red}〔X-Panel面板〕v${xui_version}${plain}" +echo "" +echo -e "${yellow}〔X-Panel面板〕最新版为---------->>> ${last_version}${plain}" + +os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1) + +if [[ "${release}" == "centos" ]]; then + if [[ ${os_version} -lt 8 ]]; then + echo -e "${red} 请使用 CentOS 8 或更高版本 ${plain}\n" && exit 1 + fi +elif [[ "${release}" == "ubuntu" ]]; then + if [[ ${os_version} -lt 20 ]]; then + echo -e "${red} 请使用 Ubuntu 20 或更高版本!${plain}\n" && exit 1 + fi + +elif [[ "${release}" == "fedora" ]]; then + if [[ ${os_version} -lt 36 ]]; then + echo -e "${red} 请使用 Fedora 36 或更高版本!${plain}\n" && exit 1 + fi + +elif [[ "${release}" == "debian" ]]; then + if [[ ${os_version} -lt 11 ]]; then + echo -e "${red} 请使用 Debian 11 或更高版本 ${plain}\n" && exit 1 + fi + +elif [[ "${release}" == "almalinux" ]]; then + if [[ ${os_version} -lt 9 ]]; then + echo -e "${red} 请使用 AlmaLinux 9 或更高版本 ${plain}\n" && exit 1 + fi + +elif [[ "${release}" == "rocky" ]]; then + if [[ ${os_version} -lt 9 ]]; then + echo -e "${red} 请使用 RockyLinux 9 或更高版本 ${plain}\n" && exit 1 + fi +elif [[ "${release}" == "arch" ]]; then + echo "您的操作系统是 ArchLinux" +elif [[ "${release}" == "manjaro" ]]; then + echo "您的操作系统是 Manjaro" +elif [[ "${release}" == "armbian" ]]; then + echo "您的操作系统是 Armbian" +elif [[ "${release}" == "alpine" ]]; then + echo "您的操作系统是 Alpine Linux" +elif [[ "${release}" == "opensuse-tumbleweed" ]]; then + echo "您的操作系统是 OpenSUSE Tumbleweed" +elif [[ "${release}" == "oracle" ]]; then + if [[ ${os_version} -lt 8 ]]; then + echo -e "${red} 请使用 Oracle Linux 8 或更高版本 ${plain}\n" && exit 1 + fi +else + echo -e "${red}此脚本不支持您的操作系统。${plain}\n" + echo "请确保您使用的是以下受支持的操作系统之一:" + echo "- Ubuntu 20.04+" + echo "- Debian 11+" + echo "- CentOS 8+" + echo "- Fedora 36+" + echo "- Arch Linux" + echo "- Parch Linux" + echo "- Manjaro" + echo "- Armbian" + echo "- Alpine Linux" + echo "- AlmaLinux 9+" + echo "- Rocky Linux 9+" + echo "- Oracle Linux 8+" + echo "- OpenSUSE Tumbleweed" + exit 1 + +fi + +# Declare Variables +log_folder="${XUI_LOG_FOLDER:=/var/log}" +iplimit_log_path="${log_folder}/3xipl.log" +iplimit_banned_log_path="${log_folder}/3xipl-banned.log" + +confirm() { + if [[ $# > 1 ]]; then + echo && read -p "$1 [Default $2]: " temp + if [[ "${temp}" == "" ]]; then + temp=$2 + fi + else + read -p "$1 [y/n]: " temp + fi + if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then + return 0 + else + return 1 + fi +} + +confirm_restart() { + confirm "重启面板,注意:重启面板也会重启 Xray" "y" + if [[ $? == 0 ]]; then + restart + else + show_menu + fi +} + +before_show_menu() { + echo && echo -n -e "${yellow}按 Enter 键返回主菜单:${plain}" && read temp + show_menu +} + +install() { + bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/main/install.sh) + if [[ $? == 0 ]]; then + if [[ $# == 0 ]]; then + start + else + start 0 + fi + fi +} + +update() { + confirm "$(echo -e "${green}该功能将强制安装最新版本,并且数据不会丢失。${red}你想继续吗?${plain}---->>请输入")" "y" + if [[ $? != 0 ]]; then + LOGE "已取消" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/main/install.sh) + if [[ $? == 0 ]]; then + LOGI "更新完成,面板已自动重启" + exit 0 + fi +} + +update_menu() { + echo -e "${yellow}更新菜单项${plain}" + confirm "此功能会将所有菜单项更新为最新显示状态" "y" + if [[ $? != 0 ]]; then + LOGE "Cancelled" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + + wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/xeefei/x-panel/main/x-ui.sh + chmod +x /usr/local/x-ui/x-ui.sh + chmod +x /usr/bin/x-ui + + if [[ $? == 0 ]]; then + echo -e "${green}更新成功,面板已自动重启${plain}" + exit 0 + else + echo -e "${red}更新菜单项失败${plain}" + return 1 + fi +} + +custom_version() { + echo "输入面板版本 (例: 2.3.8):" + read panel_version + + if [ -z "$panel_version" ]; then + echo "面板版本不能为空。" + exit 1 + fi + + download_link="https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh" + + # Use the entered panel version in the download link + install_command="bash <(curl -Ls $download_link) v$panel_version" + + echo "下载并安装面板版本 $panel_version..." + eval $install_command +} + +# Function to handle the deletion of the script file +delete_script() { + rm "$0" # Remove the script file itself + exit 1 +} + +uninstall() { + confirm "您确定要卸载面板吗? Xray 也将被卸载!" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + systemctl stop x-ui + systemctl disable x-ui + rm /etc/systemd/system/x-ui.service -f + systemctl daemon-reload + systemctl reset-failed + rm /etc/x-ui/ -rf + rm /usr/local/x-ui/ -rf + + echo "" + echo -e "卸载成功\n" + echo "如果您需要再次安装此面板,可以使用以下命令:" + echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/xeefei/x-panel/master/install.sh)${plain}" + echo "" + # Trap the SIGTERM signal + trap delete_script SIGTERM + delete_script +} + +reset_user() { + confirm "您确定重置面板的用户名和密码吗?" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + read -rp "请设置用户名 [默认为随机用户名]: " config_account + [[ -z $config_account ]] && config_account=$(date +%s%N | md5sum | cut -c 1-8) + read -rp "请设置密码 [默认为随机密码]: " config_password + [[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8) + /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} >/dev/null 2>&1 + /usr/local/x-ui/x-ui setting -remove_secret >/dev/null 2>&1 + echo -e "面板登录用户名已重置为:${green} ${config_account} ${plain}" + echo -e "面板登录密码已重置为:${green} ${config_password} ${plain}" + echo -e "${yellow} 面板 Secret Token 已禁用 ${plain}" + echo -e "${green} 请使用新的登录用户名和密码访问 X-Panel 面板。也请记住它们!${plain}" + confirm_restart +} + +gen_random_string() { + local length="$1" + local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 + systemctl restart x-ui + + # Display confirmation message + echo -e "面板访问路径已重置为: ${green}${config_webBasePath}${plain}" + echo -e "${green}请使用新的路径登录访问面板${plain}" +} + +reset_config() { + confirm "您确定要重置所有面板设置,帐户数据不会丢失,用户名和密码不会更改" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + /usr/local/x-ui/x-ui setting -reset + echo -e "所有面板设置已重置为默认,请立即重新启动面板,并使用默认的${green}13688${plain}端口访问网页面板" + confirm_restart +} + +check_config() { + info=$(/usr/local/x-ui/x-ui setting -show true) + if [[ $? != 0 ]]; then + LOGE "获取当前设置错误,请检查日志" + show_menu + fi + echo -e "${info}${plain}" + echo "" + + # 获取 IPv4 和 IPv6 地址 + v4=$(curl -s4m8 http://ip.sb -k) + v6=$(curl -s6m8 http://ip.sb -k) + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath(访问路径): .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port(端口号): .+' | awk '{print $2}') + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + local existing_key=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') + + if [[ -n "$existing_cert" && -n "$existing_key" ]]; then + echo -e "${green}面板已安装证书采用SSL保护${plain}" + echo "" + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + domain=$(basename "$(dirname "$existing_cert")") + echo -e "${green}登录访问面板URL: https://${domain}:${existing_port}${green}${existing_webBasePath}${plain}" + fi + echo "" + if [[ -z "$existing_cert" && -z "$existing_key" ]]; then + echo -e "${red}警告:未找到证书和密钥,面板不安全!${plain}" + echo "" + echo -e "${green}------->>>>请按照下述方法设置〔ssh转发〕<<<<-------${plain}" + echo "" + + # 检查 IP 并输出相应的 SSH 和浏览器访问信息 + if [[ -z $v4 ]]; then + echo -e "${green}1、本地电脑客户端转发命令:${plain} ${blue}ssh -L [::]:15208:127.0.0.1:${existing_port}${blue} root@[$v6]${plain}" + echo "" + echo -e "${green}2、请通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务${plain}" + echo "" + echo -e "${green}3、请在终端中成功输入服务器的〔root密码〕,注意区分大小写,用以上命令进行转发${plain}" + echo "" + echo -e "${green}4、请在浏览器地址栏复制${plain} ${blue}[::1]:15208${existing_webBasePath}${plain} ${green}进入〔X-Panel面板〕登录界面" + echo "" + echo -e "${red}注意:若不使用〔ssh转发〕请为X-Panel面板配置安装证书再行登录管理后台${plain}" + elif [[ -n $v4 && -n $v6 ]]; then + echo -e "${green}1、本地电脑客户端转发命令:${plain} ${blue}ssh -L 15208:127.0.0.1:${existing_port}${blue} root@$v4${plain} ${yellow}或者 ${blue}ssh -L [::]:15208:127.0.0.1:${existing_port}${blue} root@[$v6]${plain}" + echo "" + echo -e "${green}2、请通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务${plain}" + echo "" + echo -e "${green}3、请在终端中成功输入服务器的〔root密码〕,注意区分大小写,用以上命令进行转发${plain}" + echo "" + echo -e "${green}4、请在浏览器地址栏复制${plain} ${blue}127.0.0.1:15208${existing_webBasePath}${plain} ${yellow}或者${plain} ${blue}[::1]:15208${existing_webBasePath}${plain} ${green}进入〔X-Panel面板〕登录界面" + echo "" + echo -e "${red}注意:若不使用〔ssh转发〕请为X-Panel面板配置安装证书再行登录管理后台${plain}" + else + echo -e "${green}1、本地电脑客户端转发命令:${plain} ${blue}ssh -L 15208:127.0.0.1:${existing_port}${blue} root@$v4${plain}" + echo "" + echo -e "${green}2、请通过快捷键【Win + R】调出运行窗口,在里面输入【cmd】打开本地终端服务${plain}" + echo "" + echo -e "${green}3、请在终端中成功输入服务器的〔root密码〕,注意区分大小写,用以上命令进行转发${plain}" + echo "" + echo -e "${green}4、请在浏览器地址栏复制${plain} ${blue}127.0.0.1:15208${existing_webBasePath}${plain} ${green}进入〔X-Panel面板〕登录界面" + echo "" + echo -e "${red}注意:若不使用〔ssh转发〕请为X-Panel面板配置安装证书再行登录管理后台${plain}" + echo "" + fi + fi +} + +set_port() { + echo && echo -n -e "输入端口号 [1-65535]: " && read port + if [[ -z "${port}" ]]; then + LOGD "Cancelled" + before_show_menu + else + /usr/local/x-ui/x-ui setting -port ${port} + echo -e "端口已设置,请立即重启面板,并使用新端口 ${green}${port}${plain} 以访问面板" + confirm_restart + fi +} + +start() { + check_status + if [[ $? == 0 ]]; then + echo "" + LOGI "面板正在运行,无需再次启动,如需重新启动,请选择重新启动" + else + systemctl start x-ui + sleep 2 + check_status + if [[ $? == 0 ]]; then + LOGI "X-Panel 已成功启动" + else + LOGE "面板启动失败,可能是启动时间超过两秒,请稍后查看日志信息" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +stop() { + check_status + if [[ $? == 1 ]]; then + echo "" + LOGI "面板已关闭,无需再次关闭!" + else + systemctl stop x-ui + sleep 2 + check_status + if [[ $? == 1 ]]; then + LOGI "X-Panel 和 Xray 已成功关闭" + else + LOGE "面板关闭失败,可能是停止时间超过两秒,请稍后查看日志信息" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +restart() { + systemctl restart x-ui + sleep 2 + check_status + if [[ $? == 0 ]]; then + LOGI "X-Panel 和 Xray 已成功重启" + else + LOGE "面板重启失败,可能是启动时间超过两秒,请稍后查看日志信息" + fi + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +status() { + systemctl status x-ui -l + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +enable() { + systemctl enable x-ui + if [[ $? == 0 ]]; then + LOGI "x-ui 已成功设置开机启动" + else + LOGE "x-ui 设置开机启动失败" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +disable() { + systemctl disable x-ui + if [[ $? == 0 ]]; then + LOGI "x-ui 已成功取消开机启动" + else + LOGE "x-ui 取消开机启动失败" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +show_log() { + journalctl -u x-ui.service -e --no-pager -f + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +bbr_menu() { + echo -e "${green}\t1.${plain} 启用 BBR" + echo -e "${green}\t2.${plain} 禁用 BBR" + echo -e "${green}\t0.${plain} 返回主菜单" + read -p "请输入选项: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + enable_bbr + ;; + 2) + disable_bbr + ;; + *) echo "无效选项" ;; + esac +} + +disable_bbr() { + if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${yellow}BBR 当前未启用${plain}" + exit 0 + fi + + # Replace BBR with CUBIC configurations + sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf + sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf + + # Apply changes + sysctl -p + + # Verify that BBR is replaced with CUBIC + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then + echo -e "${green}BBR 已成功替换为 CUBIC${plain}" + else + echo -e "${red}用 CUBIC 替换 BBR 失败,请检查您的系统配置。${plain}" + fi +} + +enable_bbr() { + if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${green}BBR 已经启用!${plain}" + exit 0 + fi + + # Check the OS and install necessary packages + case "${release}" in + ubuntu | debian | armbian) + apt-get update && apt-get install -yqq --no-install-recommends ca-certificates + ;; + centos | almalinux | rocky | oracle) + yum -y update && yum -y install ca-certificates + ;; + fedora) + dnf -y update && dnf -y install ca-certificates + ;; + arch | manjaro) + pacman -Sy --noconfirm ca-certificates + ;; + *) + echo -e "${red}不支持的操作系统。请检查脚本并手动安装必要的软件包${plain}\n" + exit 1 + ;; + esac + + # Enable BBR + echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf + echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf + + # Apply changes + sysctl -p + + # Verify that BBR is enabled + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then + echo -e "${green}BBR 已成功启用${plain}" + else + echo -e "${red}启用 BBR 失败,请检查您的系统配置${plain}" + fi +} + +update_shell() { + wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/xeefei/x-panel/raw/main/x-ui.sh + if [[ $? != 0 ]]; then + echo "" + LOGE "下载脚本失败,请检查机器是否可以连接至 GitHub" + before_show_menu + else + chmod +x /usr/bin/x-ui + LOGI "升级脚本成功,请重新运行脚本" && exit 0 + fi +} + +# 0: running, 1: not running, 2: not installed +check_status() { + if [[ ! -f /etc/systemd/system/x-ui.service ]]; then + return 2 + fi + temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) + if [[ "${temp}" == "running" ]]; then + return 0 + else + return 1 + fi +} + +check_enabled() { + temp=$(systemctl is-enabled x-ui) + if [[ "${temp}" == "enabled" ]]; then + return 0 + else + return 1 + fi +} + +check_uninstall() { + check_status + if [[ $? != 2 ]]; then + echo "" + LOGE "面板已安装,请勿重新安装" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +check_install() { + check_status + if [[ $? == 2 ]]; then + echo "" + LOGE "请先安装面板" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +show_status() { + check_status + case $? in + 0) + echo -e "面板状态: ${green}运行中${plain}" + show_enable_status + ;; + 1) + echo -e "面板状态: ${yellow}未运行${plain}" + show_enable_status + ;; + 2) + echo -e "面板状态: ${red}未安装${plain}" + ;; + esac + show_xray_status +} + +show_enable_status() { + check_enabled + if [[ $? == 0 ]]; then + echo -e "开机启动: ${green}是${plain}" + else + echo -e "开机启动: ${red}否${plain}" + fi +} + +check_xray_status() { + count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) + if [[ count -ne 0 ]]; then + return 0 + else + return 1 + fi +} + +show_xray_status() { + check_xray_status + if [[ $? == 0 ]]; then + echo -e "Xray状态: ${green}运行中${plain}" + else + echo -e "Xray状态: ${red}未运行${plain}" + fi +} + +firewall_menu() { + echo -e "${green}\t1.${plain} 安装防火墙并开放端口" + echo -e "${green}\t2.${plain} 允许列表" + echo -e "${green}\t3.${plain} 从列表中删除端口" + echo -e "${green}\t4.${plain} 禁用防火墙" + echo -e "${green}\t0.${plain} 返回主菜单" + read -p "请输入选项: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + open_ports + ;; + 2) + sudo ufw status + ;; + 3) + delete_ports + ;; + 4) + sudo ufw disable + ;; + *) echo "无效选项" ;; + esac +} + +open_ports() { + if ! command -v ufw &>/dev/null; then + echo "ufw 防火墙未安装,正在安装..." + apt-get update + apt-get install -y ufw + else + echo "ufw 防火墙已安装" + fi + + # Check if the firewall is inactive + if ufw status | grep -q "Status: active"; then + echo "防火墙已经激活" + else + # Open the necessary ports + ufw allow ssh + ufw allow http + ufw allow https + ufw allow 13688/tcp + + # Enable the firewall + ufw --force enable + fi + + # Prompt the user to enter a list of ports + read -p "输入您要打开的端口(例如 80,443,13688 或端口范围 400-500): " ports + + # Check if the input is valid + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "错误:输入无效。请输入以英文逗号分隔的端口列表或端口范围(例如 80,443,13688 或 400-500)" >&2 + exit 1 + fi + + # Open the specified ports using ufw + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the range into start and end ports + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Loop through the range and open each port + for ((i = start_port; i <= end_port; i++)); do + ufw allow $i + done + else + ufw allow "$port" + fi + done + + # Confirm that the ports are open + ufw status | grep $ports +} + +delete_ports() { + # Prompt the user to enter the ports they want to delete + read -p "输入要删除的端口(例如 80,443,13688 或范围 400-500): " ports + + # Check if the input is valid + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "错误:输入无效。请输入以英文逗号分隔的端口列表或端口范围(例如 80,443,13688 或 400-500)" >&2 + exit 1 + fi + + # Delete the specified ports using ufw + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the range into start and end ports + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Loop through the range and delete each port + for ((i = start_port; i <= end_port; i++)); do + ufw delete allow $i + done + else + ufw delete allow "$port" + fi + done + + # Confirm that the ports are deleted + echo "删除指定端口:" + ufw status | grep $ports +} + +update_geo() { + local defaultBinFolder="/usr/local/x-ui/bin" + read -p "请输入 x-ui bin 文件夹路径,默认留空。(默认值:'${defaultBinFolder}')" binFolder + binFolder=${binFolder:-${defaultBinFolder}} + if [[ ! -d ${binFolder} ]]; then + LOGE "文件夹 ${binFolder} 不存在!" + LOGI "制作 bin 文件夹:${binFolder}..." + mkdir -p ${binFolder} + fi + + systemctl stop x-ui + cd ${binFolder} + rm -f geoip.dat geosite.dat geoip_IR.dat geosite_IR.dat geoip_VN.dat geosite_VN.dat + wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat + wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat + wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat + wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat + wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat + wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat + systemctl start x-ui + echo -e "${green}Geosite.dat + Geoip.dat + geoip_IR.dat + geosite_IR.dat 在 bin 文件夹: '${binfolder}' 中已经更新成功 !${plain}" + before_show_menu +} + +install_acme() { + # 检查是否已安装 acme.sh + if command -v ~/.acme.sh/acme.sh &>/dev/null; then + LOGI "acme.sh 已经安装。" + return 0 + fi + + LOGI "正在安装 acme.sh..." + cd ~ || return 1 # 确保可以切换到主目录 + + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + LOGE "安装 acme.sh 失败。" + return 1 + else + LOGI "安装 acme.sh 成功。" + fi + + return 0 +} + +# 【中文注释】:“备用方式申请证书”函数 +ssl_cert_issue_standalone_embedded() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath(访问路径): .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port(端口号): .+' | awk '{print $2}') + echo "" + echo -e "${yellow}=== 备用方式申请 SSL 证书 (Standalone 模式) ===${plain}" + echo "" + echo -e "${yellow}说明:此模式使用 80 端口申请证书,成功率高,但需要占用 80 端口。${plain}" + echo "" + + # --- 1. 用户输入信息 --- + read -rp "请输入域名: " DOMAIN + if [ -z "$DOMAIN" ]; then + LOGE "域名不能为空" + echo "" + return 1 + fi + + read -rp "请输入电子邮件地址 (用于注册账户): " EMAIL + if [ -z "$EMAIL" ]; then + LOGE "邮箱不能为空" + return 1 + fi + + echo -e "请选择证书颁发机构(CA):" + echo "" + echo -e "${green}1)${plain} Let's Encrypt" + echo "" + echo -e "${green}2)${plain} Buypass" + echo "" + echo -e "${green}3)${plain} ZeroSSL" + echo "" + read -rp "输入选项(1-3): " CA_OPTION + case $CA_OPTION in + 1) CA_SERVER="letsencrypt" ;; + 2) CA_SERVER="buypass" ;; + 3) CA_SERVER="zerossl" ;; + *) echo "❌ 无效选项"; return 1 ;; + esac + + # --- 2. 防火墙与端口处理 --- + echo "" + echo -e "${yellow}注意:Standalone 模式必须使用 80 端口。${plain}" + echo "" + echo -e "脚本可以尝试临时放行端口或关闭防火墙,申请结束后您可以手动恢复。" + echo "" + echo -e "${green}1)${plain} 尝试关闭防火墙 (成功率最高)" + echo "" + echo -e "${green}2)${plain} 尝试仅放行 80 端口" + echo "" + echo -e "${green}3)${plain} 跳过防火墙设置 (如果您已确认 80 端口开放)" + echo "" + read -rp "输入选项(1-3):" FIREWALL_OPTION + + # 检测系统类型 + if [[ -f /etc/os-release ]]; then + source /etc/os-release + OS=$ID + else + echo "❌ 无法识别操作系统,跳过依赖安装优化。" + fi + + # 执行防火墙操作 + if [ "$FIREWALL_OPTION" -eq 1 ]; then + LOGI "正在尝试关闭防火墙..." + if command -v ufw >/dev/null 2>&1; then + sudo ufw disable + elif command -v systemctl >/dev/null 2>&1; then + sudo systemctl stop firewalld 2>/dev/null + sudo systemctl stop iptables 2>/dev/null + fi + elif [ "$FIREWALL_OPTION" -eq 2 ]; then + LOGI "正在尝试放行 80 端口..." + if command -v ufw >/dev/null 2>&1; then + sudo ufw allow 80 + elif command -v firewall-cmd >/dev/null 2>&1; then + sudo firewall-cmd --permanent --add-port=80/tcp + sudo firewall-cmd --reload + fi + fi + + # --- 3. 安装依赖 (socat 是 standalone 模式必须的) --- + LOGI "正在检查并安装必要依赖 (socat)..." + case $OS in + ubuntu|debian) + sudo apt update -y + sudo apt install -y curl socat cron + ;; + centos|almalinux|rocky) + sudo yum update -y + sudo yum install -y curl socat cronie + sudo systemctl start crond + sudo systemctl enable crond + ;; + *) + # 其他系统尝试通用安装 + if ! command -v socat >/dev/null 2>&1; then + echo "❌ 无法识别系统包管理器,请手动安装 socat。" + return 1 + fi + ;; + esac + + # --- 4. 安装/检查 acme.sh --- + if ! command -v ~/.acme.sh/acme.sh >/dev/null 2>&1; then + LOGI "未检测到 acme.sh,正在安装..." + curl https://get.acme.sh | sh + if [ $? -ne 0 ]; then + LOGE "acme.sh 安装失败!" + return 1 + fi + export PATH="$HOME/.acme.sh:$PATH" + ~/.acme.sh/acme.sh --upgrade + else + LOGI "acme.sh 已安装。" + fi + + # --- 5. 注册账户 --- + LOGI "正在注册 ACME 账户..." + ~/.acme.sh/acme.sh --register-account -m $EMAIL --server $CA_SERVER + + # --- 6. 申请证书 (Standalone 模式) --- + LOGI "正在使用 Standalone 模式申请证书 (占用 80 端口)..." + # 如果 80 端口被 nginx 或 x-ui 占用,先尝试停止它们 + systemctl stop nginx >/dev/null 2>&1 + systemctl stop x-ui >/dev/null 2>&1 + + if ! ~/.acme.sh/acme.sh --issue --standalone -d $DOMAIN --server $CA_SERVER --force; then + LOGE "❌ 证书申请失败!" + LOGE "请检查:1、域名解析是否正确指向本机 IP。2、80 端口是否开放且未被占用。" + # 失败尝试重启 x-ui + systemctl start x-ui >/dev/null 2>&1 + return 1 + fi + +# --- 7. 安装证书到标准路径 --- +# 直接创建 x-ui 标准目录结构, + local certPath="/root/cert/${DOMAIN}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + fi + + LOGI "申请成功!正在安装证书到: $certPath" + + # x-ui 是系统服务,用 systemctl restart x-ui 更稳妥 + local reloadCmd="systemctl restart x-ui" + + ~/.acme.sh/acme.sh --installcert -d $DOMAIN \ + --key-file "${certPath}/privkey.pem" \ + --fullchain-file "${certPath}/fullchain.pem" \ + --reloadcmd "${reloadCmd}" + + if [ $? -eq 0 ]; then + LOGI "✅ 证书安装命令执行成功!" + # 启用 acme.sh 自动升级 + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + + # --- 8. 自动应用到面板设置 (包含文件检查) --- + local webCertFile="${certPath}/fullchain.pem" + local webKeyFile="${certPath}/privkey.pem" + + # === 文件检查判断 === + if [[ -f "${webCertFile}" && -f "${webKeyFile}" && -s "${webCertFile}" && -s "${webKeyFile}" ]]; then + + LOGI "检测到证书文件存在且有效,正在应用到 X-Panel 面板..." + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + + echo -e "${green}恭喜!备用方式证书申请并配置成功!${plain}" + echo "" + echo " - 域名:$DOMAIN" + echo "" + echo " - 证书路径:$webCertFile" + echo "" + echo " - 私钥路径:$webKeyFile" + echo "" + echo -e "${green}登录访问面板URL: https://${domain}:${existing_port}${green}${existing_webBasePath}${plain}" + echo "" + echo -e "${green}PS:若您要登录访问面板,请复制上面的地址到浏览器打开即可${plain}" + echo "" + + # 重启面板以生效 + if command -v restart >/dev/null 2>&1; then + restart + else + systemctl restart x-ui + fi + else + LOGE "❌ 严重错误:证书安装显示成功,但未找到证书文件或文件为空!" + LOGE "请检查目录权限或磁盘空间:$certPath" + # 恢复 x-ui 运行,避免断联 + systemctl start x-ui >/dev/null 2>&1 + return 1 + fi + else + LOGE "证书安装步骤失败,请检查日志。" + systemctl start x-ui >/dev/null 2>&1 + fi +} + +ssl_cert_issue_main() { + echo -e "${green}\t1.${plain} 获取 SSL 证书" + echo -e "${green}\t2.${plain} 撤销证书" + echo -e "${green}\t3.${plain} 强制更新证书" + echo -e "${green}\t4.${plain} 自定义证书路径" + echo -e "${green}\t5.${plain} 备用方式申请证书" + echo -e "${green}\t6.${plain} 显示现有域名" + echo -e "${green}\t7.${plain} 为面板设置证书路径" + echo -e "${green}\t0.${plain} 返回主菜单" + echo "" + + read -rp "请选择一个选项:" choice + case "$choice" in + 0) + show_menu + ;; + 1) + ssl_cert_issue + ssl_cert_issue_main + ;; + 2) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "未找到可撤销的证书。" + else + echo "现有域名:" + echo "$domains" + read -rp "请从列表中输入要撤销证书的域名:" domain + if echo "$domains" | grep -qw "$domain"; then + ~/.acme.sh/acme.sh --revoke -d ${domain} + LOGI "已撤销域名的证书:$domain" + else + echo "输入的域名无效。" + fi + fi + ssl_cert_issue_main + ;; + 3) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "未找到可更新的证书。" + else + echo "现有域名:" + echo "$domains" + read -rp "请从列表中输入要强制更新 SSL 证书的域名:" domain + if echo "$domains" | grep -qw "$domain"; then + ~/.acme.sh/acme.sh --renew -d ${domain} --force + LOGI "已强制更新域名的证书:$domain" + else + echo "输入的域名无效。" + fi + fi + ssl_cert_issue_main + ;; + 4) + # 【功能:自定义证书路径】 + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath(访问路径): .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port(端口号): .+' | awk '{print $2}') + echo "" + echo -e "${yellow}您选择了“手动上传证书”去自定义路径${plain}" + echo "" + echo -e "${red}请您务必先以〔“root”用户〕上传您的证书文件${plain}" + echo "" + read -rp "请输入您的域名: " domain + echo "" + read -rp "请输入公钥文件(Fullchain/Cert)的绝对路径: " user_cert + echo "" + read -rp "请输入私钥文件(Key)的绝对路径: " user_key + + # 源文件检查,增加 -s 判断,防止用户上传了空文件 + if [[ -f "$user_cert" && -f "$user_key" && -s "$user_cert" && -s "$user_key" ]]; then + + # 创建标准存放目录 + local certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + fi + + # 复制文件到 X-Panel 标准目录,统一命名 + # 使用 \cp -f 强制覆盖,不提示 + \cp -f "$user_cert" "${certPath}/fullchain.pem" + \cp -f "$user_key" "${certPath}/privkey.pem" + + local webCertFile="${certPath}/fullchain.pem" + local webKeyFile="${certPath}/privkey.pem" + + # 检查复制后的目标文件是否存在且不为空 + if [[ -f "${webCertFile}" && -f "${webKeyFile}" && -s "${webCertFile}" && -s "${webKeyFile}" ]]; then + + # 应用到面板设置 + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + + echo -e "${green}已成功导入证书并设置应用到面板路径!${plain}" + echo "" + echo " - 域名:$domain" + echo "" + echo " - 证书文件:$webCertFile" + echo "" + echo " - 私钥文件:$webKeyFile" + echo "" + echo -e "${green}登录访问面板URL: https://${domain}:${existing_port}${green}${existing_webBasePath}${plain}" + echo "" + echo -e "${green}PS:若您要登录访问面板,请复制上面的地址到浏览器打开即可${plain}" + echo "" + + # 只有文件确认无误,才执行重启 + restart + else + # 复制失败的处理 + LOGE "❌ 严重错误:文件复制失败或目标文件为空!" + echo "可能是磁盘空间不足或权限问题,面板设置未更改。" + fi + else + echo -e "${red}错误:找不到文件或文件为空 (0KB),请检查后重试!${plain}" + echo "" + echo "公钥路径: $user_cert" + echo "" + echo "私钥路径: $user_key" + fi + + # 返回主菜单 + ssl_cert_issue_main + ;; + 5) + # 【功能:备用方式申请证书】 + ssl_cert_issue_standalone_embedded + ssl_cert_issue_main + ;; + 6) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "未找到证书。" + else + echo "现有域名及其路径:" + for domain in $domains; do + local cert_path="/root/cert/${domain}/fullchain.pem" + local key_path="/root/cert/${domain}/privkey.pem" + if [[ -f "${cert_path}" && -f "${key_path}" ]]; then + echo -e "域名:${domain}" + echo "" + echo -e "\t证书路径:${cert_path}" + echo "" + echo -e "\t私钥路径:${key_path}" + else + echo -e "域名:${domain} - 证书或私钥文件缺失。" + fi + done + fi + ssl_cert_issue_main + ;; + 7) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "未找到证书。" + else + echo "可用域名:" + echo "$domains" + read -rp "请选择要为面板设置证书路径的域名:" domain + + if echo "$domains" | grep -qw "$domain"; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo "已为域名设置面板证书路径:$domain" + echo "" + echo " - 证书文件:$webCertFile" + echo "" + echo " - 私钥文件:$webKeyFile" + restart + else + echo "未找到域名的证书或私钥:$domain" + fi + else + echo "输入的域名无效。" + fi + fi + ssl_cert_issue_main + ;; + + *) + echo -e "${red}无效选项,请选择有效的数字。${plain}\n" + ssl_cert_issue_main + ;; + esac +} + +ssl_cert_issue() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath(访问路径): .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port(端口号): .+' | awk '{print $2}') + # 首先检查 acme.sh + echo "" + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "未找到 acme.sh,将进行安装" + install_acme + if [ $? -ne 0 ]; then + LOGE "安装 acme 失败,请检查日志" + exit 1 + fi + fi + + # 安装 socat 和 dnsutils/bind-utils (用于 dig) + case "${release}" in + ubuntu | debian | armbian) + # 添加了 dnsutils 用于 dig 命令 + apt update && apt install socat dnsutils -y + ;; + centos | rhel | almalinux | rocky | ol) + # 添加了 bind-utils 用于 dig 命令 + yum -y update && yum -y install socat bind-utils + ;; + fedora | amzn | virtuozzo) + # 添加了 bind-utils 用于 dig 命令 + dnf -y update && dnf -y install socat bind-utils + ;; + arch | manjaro | parch) + # 添加了 dnsutils 用于 dig 命令 + pacman -Sy --noconfirm socat dnsutils + ;; + *) + echo "" + echo -e "${red}不支持的操作系统。请检查脚本并手动安装必要的软件包。${plain}\n" + exit 1 + ;; + esac + if [ $? -ne 0 ]; then + LOGE "安装 socat 或 dig 工具 失败,请检查日志" + echo "" + exit 1 + else + LOGI "安装 socat 和 dig 工具 成功..." + echo "" + fi + + # 在这里获取域名,我们需要验证它 + local domain="" + # 强制从终端读取输入,避免被管道跳过 + read -rp "请输入您的域名: " domain >> A 记录..." + # 确保只获取A记录,并取第一个 + domain_ip=$(dig +short $domain A | head -n 1) + + if [ -z "$domain_ip" ]; then + echo "" + LOGE "未能查询到域名 ${domain} 的 “A 记录”。" + echo "" + LOGE "请确保您的域名已在 DNS 服务商处添加了〔A 记录〕,并指向本机 IP。" + echo "" + LOGE "提示:如果您使用的是 Cloudflare,请确保【小黄云】(代理)已关闭。" + exit 1 + fi + LOGI "域名 ${domain} 解析到 IP: ${domain_ip}" + echo "" + + if [ "$public_ip" != "$domain_ip" ]; then + echo "" + LOGE "域名 ${domain} 解析的 IP (${domain_ip}) 与本机 IP (${public_ip}) 不符!" + echo "" + LOGE "请检查您的 DNS 中 的〔A 记录〕设置是否正确。" + echo "" + LOGE "提示:如果您使用的是 Cloudflare,请确保【小黄云】(代理)已关闭。" + exit 1 + fi + + LOGI "域名解析验证成功,继续执行证书申请......" + echo "" + + # 检查是否已存在证书 + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${domain}" ]; then + local certInfo=$(~/.acme.sh/acme.sh --list) + LOGE "系统已存在此域名的证书。无法再次签发。当前证书详情:" + LOGI "$certInfo" + exit 1 + else + LOGI "您的域名现在可以签发证书了......" + fi + + # 为证书创建一个目录 + echo "" + certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + # 获取独立服务器的端口号 + local WebPort=80 + read -rp "请选择要使用的端口 (默认为 80): " WebPort + if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then + LOGE "您输入的 ${WebPort} 无效,将使用默认端口 80。" + WebPort=80 + fi + LOGI "将使用端口: ${WebPort} 来签发证书。请确保此端口已开放。" + + # 签发证书 + echo "" + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + LOGE "签发证书失败,请检查日志。" + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGE "签发证书成功,正在安装证书..." + fi + + # --- 自动设置 reloadCmd --- + echo "" + reloadCmd="systemctl restart x-ui" + LOGI "ACME 的 --reloadcmd 已自动设置为: ${yellow}systemctl restart x-ui" + + # 安装证书 + echo "" + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem \ + --reloadcmd "${reloadCmd}" + + echo "" + if [ $? -ne 0 ]; then + LOGE "安装证书失败,正在退出。" + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGI "安装证书成功,正在启用自动续订..." + fi + + # 启用自动续订 + echo "" + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "自动续订失败,证书详情:" + ls -lah cert/* + chmod 755 $certPath/* + exit 1 + else + LOGI "自动续订成功,证书详情:" + ls -lah cert/* + chmod 755 $certPath/* + fi + + # --- 自动为面板设置证书路径 --- + echo "" + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "已为域名自动设置面板证书路径: $domain" + echo "" + LOGI " - 证书文件: $webCertFile" + echo "" + LOGI " - 私钥文件: $webKeyFile" + echo "" + echo -e "${green}登录访问面板URL: https://${domain}:${existing_port}${green}${existing_webBasePath}${plain}" + echo "" + echo -e "${green}PS:若您要登录访问面板,请复制上面的地址到浏览器打开即可${plain}" + echo "" + restart + else + LOGE "错误:未找到域名的证书或私钥文件: $domain。" + fi +} + +ssl_cert_issue_CF() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath(访问路径): .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port(端口号): .+' | awk '{print $2}') + LOGI "****** 使用说明 ******" + echo "" + LOGI "请按照以下步骤完成操作:" + echo "" + LOGI "1. 准备好在 Cloudflare 注册的电子邮箱。" + echo "" + LOGI "2. 准备好 Cloudflare Global API 密钥。" + echo "" + LOGI "3. 准备好一个可用的域名。" + echo "" + LOGI "4. 证书颁发后,系统将自动为您设置证书路径。" + echo "" + LOGI "5. 安装后,脚本还支持自动续订 SSL 证书。" + echo "" + + confirm "您确认信息并希望继续吗?[y/n]" "y" + + if [ $? -eq 0 ]; then + # 首先检查 acme.sh + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "未找到 acme.sh。我们将为您安装。" + install_acme + if [ $? -ne 0 ]; then + LOGE "安装 acme 失败,请检查日志。" + exit 1 + fi + fi + + CF_Domain="" + + LOGD "请设置一个域名:" + # 强制从终端读取输入 + read -rp "在此输入您的域名: " CF_Domain >>>接下来进行sublink订阅转换服务的安装 ........${plain}" + sleep 3 + echo "" +else + echo -e "${red}警告:未找到证书和密钥,面板不安全!${plain}" + echo "" + echo -e "${green}------->>>>且不能安装sublink订阅转换服务<<<<-------${plain}" + echo "" + sleep 5 + exit 1 +fi + +# --------- 安装/部署sublink服务 ---------- + +bash <(curl -Ls https://raw.githubusercontent.com/xeefei/sublink/main/install.sh) + + +# --------- 安装 Nginx ---------- +if ! command -v nginx &>/dev/null; then + echo -e "${yellow}-------------->>>>>>>>未检测到 Nginx,正在安装...${plain}" + apt update && apt install -y nginx + systemctl enable nginx + systemctl start nginx +else + echo -e "${green}检测到 Nginx 已安装,跳过安装步骤${plain}" +fi + +# --------- 拷贝X-Panel已有证书到 Nginx ---------- +mkdir -p /etc/nginx/ssl +acme_path="/root/.acme.sh/${domain}_ecc" + +cp "${acme_path}/fullchain.cer" "/etc/nginx/ssl/${domain}.cer" +cp "${acme_path}/${domain}.key" "/etc/nginx/ssl/${domain}.key" + + +# --------- 配置 Nginx 反向代理 ---------- +NGINX_CONF="/etc/nginx/conf.d/sublink.conf" +cat > $NGINX_CONF </dev/null; then + # If not installed, install it + local pkg_manager="" + local speedtest_install_script="" + + if command -v dnf &>/dev/null; then + pkg_manager="dnf" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" + elif command -v yum &>/dev/null; then + pkg_manager="yum" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" + elif command -v apt-get &>/dev/null; then + pkg_manager="apt-get" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" + elif command -v apt &>/dev/null; then + pkg_manager="apt" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" + fi + + if [[ -z $pkg_manager ]]; then + echo "错误:找不到包管理器。 您可能需要手动安装 Speedtest" + return 1 + else + curl -s $speedtest_install_script | bash + $pkg_manager install -y speedtest + fi + fi + + # Run Speedtest + speedtest +} + + +iplimit_main() { + echo -e "\n${green}\t1.${plain} 安装 Fail2ban 并配置 IP 限制" + echo -e "${green}\t2.${plain} 更改禁止期限" + echo -e "${green}\t3.${plain} 解禁所有 IP" + echo -e "${green}\t4.${plain} 查看日志" + echo -e "${green}\t5.${plain} Fail2ban 状态" + echo -e "${green}\t6.${plain} 重启 Fail2ban" + echo -e "${green}\t7.${plain} 卸载 Fail2ban" + echo -e "${green}\t0.${plain} 返回主菜单" + read -p "请输入选项: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + confirm "继续安装 Fail2ban 和 IP 限制?" "y" + if [[ $? == 0 ]]; then + install_iplimit + else + iplimit_main + fi + ;; + 2) + read -rp "请输入新的禁令持续时间(以分钟为单位)[默认 30]: " NUM + if [[ $NUM =~ ^[0-9]+$ ]]; then + create_iplimit_jails ${NUM} + systemctl restart fail2ban + else + echo -e "${red}${NUM} 不是一个数字! 请再试一次.${plain}" + fi + iplimit_main + ;; + 3) + confirm "继续解除所有人的 IP 限制禁令?" "y" + if [[ $? == 0 ]]; then + fail2ban-client reload --restart --unban 3x-ipl + truncate -s 0 "${iplimit_banned_log_path}" + echo -e "${green}所有用户已成功解封${plain}" + iplimit_main + else + echo -e "${yellow}已取消${plain}" + fi + iplimit_main + ;; + 4) + show_banlog + ;; + 5) + service fail2ban status + ;; + 6) + systemctl restart fail2ban + ;; + 7) + remove_iplimit + ;; + *) echo "无效选项" ;; + esac +} + +install_iplimit() { + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${green}未安装 Fail2ban。正在安装...!${plain}\n" + + # Check the OS and install necessary packages + case "${release}" in + ubuntu) + apt-get update + if [[ "${os_version}" -ge 24 ]]; then + apt-get install python3-pip -y + python3 -m pip install pyasynchat --break-system-packages + fi + apt-get install fail2ban -y + ;; + debian) + apt-get update + if [ "$os_version" -ge 12 ]; then + apt-get install -y python3-systemd + fi + apt-get install -y fail2ban + ;; + armbian) + apt-get update && apt-get install fail2ban -y + ;; + centos | almalinux | rocky | oracle) + yum update -y && yum install epel-release -y + yum -y install fail2ban + ;; + fedora) + dnf -y update && dnf -y install fail2ban + ;; + arch | manjaro | parch) + pacman -Syu --noconfirm fail2ban + ;; + *) + echo -e "${red}不支持的操作系统,请检查脚本并手动安装必要的软件包.${plain}\n" + exit 1 + ;; + esac + + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${red}Fail2ban 安装失败${plain}\n" + exit 1 + fi + + echo -e "${green}Fail2ban 安装成功!${plain}\n" + else + echo -e "${yellow}Fail2ban 已安装${plain}\n" + fi + + echo -e "${green}配置 IP 限制中...${plain}\n" + + # make sure there's no conflict for jail files + iplimit_remove_conflicts + + # Check if log file exists + if ! test -f "${iplimit_banned_log_path}"; then + touch ${iplimit_banned_log_path} + fi + + # Check if service log file exists so fail2ban won't return error + if ! test -f "${iplimit_log_path}"; then + touch ${iplimit_log_path} + fi + + # Create the iplimit jail files + # we didn't pass the bantime here to use the default value + create_iplimit_jails + + # Launching fail2ban + if ! systemctl is-active --quiet fail2ban; then + systemctl start fail2ban + systemctl enable fail2ban + else + systemctl restart fail2ban + fi + systemctl enable fail2ban + + echo -e "${green}IP 限制安装并配置成功!${plain}\n" + before_show_menu +} + +remove_iplimit() { + echo -e "${green}\t1.${plain} 仅删除 IP 限制配置" + echo -e "${green}\t2.${plain} 卸载 Fail2ban 和 IP 限制" + echo -e "${green}\t0.${plain} 终止" + read -p "请输入选项: " num + case "$num" in + 1) + rm -f /etc/fail2ban/filter.d/3x-ipl.conf + rm -f /etc/fail2ban/action.d/3x-ipl.conf + rm -f /etc/fail2ban/jail.d/3x-ipl.conf + systemctl restart fail2ban + echo -e "${green}IP 限制成功解除!${plain}\n" + before_show_menu + ;; + 2) + rm -rf /etc/fail2ban + systemctl stop fail2ban + case "${release}" in + ubuntu | debian | armbian) + apt-get remove -y fail2ban + apt-get purge -y fail2ban -y + apt-get autoremove -y + ;; + centos | almalinux | rocky | oracle) + yum remove fail2ban -y + yum autoremove -y + ;; + fedora) + dnf remove fail2ban -y + dnf autoremove -y + ;; + arch | manjaro) + pacman -Rns --noconfirm fail2ban + ;; + *) + echo -e "${red}不支持的操作系统,请手动卸载 Fail2ban.${plain}\n" + exit 1 + ;; + esac + echo -e "${green}Fail2ban 和 IP 限制已成功删除!${plain}\n" + before_show_menu + ;; + 0) + echo -e "${yellow}已取消${plain}\n" + iplimit_main + ;; + *) + echo -e "${red}无效选项。 请选择一个有效的选项。${plain}\n" + remove_iplimit + ;; + esac +} + +show_banlog() { + local system_log="/var/log/fail2ban.log" + + echo -e "${green}正在检查禁止日志...${plain}\n" + + if ! systemctl is-active --quiet fail2ban; then + echo -e "${red}Fail2ban 服务未运行!${plain}\n" + return 1 + fi + + if [[ -f "$system_log" ]]; then + echo -e "${green}来自 fail2ban.log 的最近系统禁止活动:${plain}" + grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}未发现近期系统禁止活动${plain}" + echo "" + fi + + if [[ -f "${iplimit_banned_log_path}" ]]; then + echo -e "${green}3X-IPL禁止日志文件条目:${plain}" + if [[ -s "${iplimit_banned_log_path}" ]]; then + grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}未找到禁止条目${plain}" + else + echo -e "${yellow}禁止日志文件为空${plain}" + fi + else + echo -e "${red}未找到禁止日志文件: ${iplimit_banned_log_path}${plain}" + fi + + echo -e "\n${green}目前的限制情况:${plain}" + fail2ban-client status 3x-ipl || echo -e "${yellow}无法获取限制状态${plain}" +} + +create_iplimit_jails() { + # Use default bantime if not passed => 30 minutes + local bantime="${1:-30}" + + # Uncomment 'allowipv6 = auto' in fail2ban.conf + sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf + + # On Debian 12+ fail2ban's default backend should be changed to systemd + if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then + sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf + fi + + cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf +[3x-ipl] +enabled=true +backend=auto +filter=3x-ipl +action=3x-ipl +logpath=${iplimit_log_path} +maxretry=2 +findtime=32 +bantime=${bantime}m +EOF + + cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf +[Definition] +datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S +failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* +ignoreregex = +EOF + + cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf +[INCLUDES] +before = iptables-allports.conf + +[Definition] +actionstart = -N f2b- + -A f2b- -j + -I -p -j f2b- + +actionstop = -D -p -j f2b- + + -X f2b- + +actioncheck = -n -L | grep -q 'f2b-[ \t]' + +actionban = -I f2b- 1 -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} + +actionunban = -D f2b- -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} + +[Init] +name = default +protocol = tcp +chain = INPUT +EOF + + echo -e "${green}创建的 IP Limit 限制文件禁止时间为 ${bantime} 分钟。${plain}" +} + +iplimit_remove_conflicts() { + local jail_files=( + /etc/fail2ban/jail.conf + /etc/fail2ban/jail.local + ) + + for file in "${jail_files[@]}"; do + # Check for [3x-ipl] config in jail file then remove it + if test -f "${file}" && grep -qw '3x-ipl' ${file}; then + sed -i "/\[3x-ipl\]/,/^$/d" ${file} + echo -e "${yellow}消除系统环境中 [3x-ipl] 的冲突 (${file})!${plain}\n" + fi + done +} + +show_usage() { + echo -e " ---------------------" + echo -e " |${green}X-Panel 控制菜单用法 ${plain}|${plain}" + echo -e " | ${yellow}一个更好的面板 ${plain}|${plain}" + echo -e " | ${yellow}基于Xray Core构建 ${plain}|${plain}" + echo -e "--------------------------------------------" + echo -e "x-ui - 进入管理脚本" + echo -e "x-ui start - 启动 X-Panel 面板" + echo -e "x-ui stop - 关闭 X-Panel 面板" + echo -e "x-ui restart - 重启 X-Panel 面板" + echo -e "x-ui status - 查看 X-Panel 状态" + echo -e "x-ui settings - 查看当前设置信息" + echo -e "x-ui enable - 启用 X-Panel 开机启动" + echo -e "x-ui disable - 禁用 X-Panel 开机启动" + echo -e "x-ui log - 查看 X-Panel 运行日志" + echo -e "x-ui banlog - 检查 Fail2ban 禁止日志" + echo -e "x-ui update - 更新 X-Panel 面板" + echo -e "x-ui custom - 自定义 X-Panel 版本" + echo -e "x-ui install - 安装 X-Panel 面板" + echo -e "x-ui uninstall - 卸载 X-Panel 面板" + echo -e "--------------------------------------------" +} + +show_menu() { + echo -e " +—————————————————————— + ${green}X-Panel 面板管理脚本${plain} + ${yellow} 一个更好的面板${plain} + ${yellow} 基于Xray Core构建${plain} +—————————————————————— + ${green}0.${plain} 退出脚本 + ${green}1.${plain} 安装面板 + ${green}2.${plain} 更新面板 + ${green}3.${plain} 更新菜单项 + ${green}4.${plain} 自定义版本 + ${green}5.${plain} 卸载面板 +—————————————————————— + ${green}6.${plain} 重置用户名、密码 + ${green}7.${plain} 修改访问路径 + ${green}8.${plain} 重置面板设置 + ${green}9.${plain} 修改面板端口 + ${green}10.${plain} 查看面板设置 +—————————————————————— + ${green}11.${plain} 启动面板 + ${green}12.${plain} 关闭面板 + ${green}13.${plain} 重启面板 + ${green}14.${plain} 检查面板状态 + ${green}15.${plain} 检查面板日志 +—————————————————————— + ${green}16.${plain} 启用开机启动 + ${green}17.${plain} 禁用开机启动 +—————————————————————— + ${green}18.${plain} SSL 证书管理 + ${green}19.${plain} CF SSL 证书 + ${green}20.${plain} IP 限制管理 + ${green}21.${plain} 防火墙管理 +—————————————————————— + ${green}22.${plain} 启用 BBR + ${green}23.${plain} 更新 Geo 文件 + ${green}24.${plain} Speedtest by Ookla + ${green}25.${plain} 安装订阅转换 +—————————————————————— + ${green}若在使用过程中有任何问题${plain} + ${yellow}请加入〔X-Panel面板〕交流群${plain} + ${red}https://t.me/XUI_CN ${yellow}截图进行反馈${plain} + ${green}〔X-Panel面板〕项目地址${plain} + ${yellow}https://github.com/xeefei/x-panel${plain} + ${green}详细〔安装配置〕教程${plain} + ${yellow}https://xeefei.blogspot.com/2025/09/x-panel.html${plain} +—————————————————————— + +-------------->>>>>>>赞 助 推 广 区<<<<<<<<------------------- + +${green}1、搬瓦工GIA高端线路:${yellow}https://bandwagonhost.com/aff.php?aff=75015${plain} + +${green}2、Dmit高端GIA线路:${yellow}https://www.dmit.io/aff.php?aff=9326${plain} + +${green}3、Gomami亚太顶尖优化线路:${yellow}https://gomami.io/aff.php?aff=174${plain} + +${green}4、ISIF优质亚太优化线路:${yellow}https://cloud.isif.net/login?affiliation_code=333${plain} + +${green}5、ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选:${yellow}https://my.zorocloud.com/aff.php?aff=1072${plain} + +${green}6、三网直连 IEPL / IPLC 直播流量转发:${yellow}https://idc333.top/#register/BCUZXNELNO${plain} + +${green}7、Bagevm优质落地鸡(原生IP全解锁):${yellow}https://www.bagevm.com/aff.php?aff=754${plain} + +${green}8、白丝云〔4837线路〕实惠量大管饱:${yellow}https://cloudsilk.io/aff.php?aff=706${plain} + +${green}9、RackNerd极致性价比机器:${yellow}https://my.racknerd.com/aff.php?aff=15268&pid=912${plain} + +---------------------------------------------- +" + show_status + echo && read -p "请输入选项 [0-25]: " num + + case "${num}" in + 0) + exit 0 + ;; + 1) + check_uninstall && install + ;; + 2) + check_install && update + ;; + 3) + check_install && update_menu + ;; + 4) + check_install && custom_version + ;; + 5) + check_install && uninstall + ;; + 6) + check_install && reset_user + ;; + 7) + check_install && reset_webbasepath + ;; + 8) + check_install && reset_config + ;; + 9) + check_install && set_port + ;; + 10) + check_install && check_config + ;; + 11) + check_install && start + ;; + 12) + check_install && stop + ;; + 13) + check_install && restart + ;; + 14) + check_install && status + ;; + 15) + check_install && show_log + ;; + 16) + check_install && enable + ;; + 17) + check_install && disable + ;; + 18) + ssl_cert_issue_main + ;; + 19) + ssl_cert_issue_CF + ;; + 20) + iplimit_main + ;; + 21) + firewall_menu + ;; + 22) + bbr_menu + ;; + 23) + update_geo + ;; + 24) + run_speedtest + ;; + 25) + subconverter + ;; + *) + LOGE "请输入正确的数字选项 [0-25]" + ;; + esac +} + +if [[ $# > 0 ]]; then + case $1 in + "start") + check_install 0 && start 0 + ;; + "stop") + check_install 0 && stop 0 + ;; + "restart") + check_install 0 && restart 0 + ;; + "status") + check_install 0 && status 0 + ;; + "settings") + check_install 0 && check_config 0 + ;; + "enable") + check_install 0 && enable 0 + ;; + "disable") + check_install 0 && disable 0 + ;; + "log") + check_install 0 && show_log 0 + ;; + "banlog") + check_install 0 && show_banlog 0 + ;; + "update") + check_install 0 && update 0 + ;; + "custom") + check_install 0 && custom_version 0 + ;; + "install") + check_uninstall 0 && install 0 + ;; + "uninstall") + check_install 0 && uninstall 0 + ;; + "subconverter") + check_install 0 && subconverter 0 + ;; + *) show_usage ;; + esac +else + show_menu +fi diff --git a/xray/api.go b/xray/api.go new file mode 100644 index 0000000..7ae5e32 --- /dev/null +++ b/xray/api.go @@ -0,0 +1,294 @@ +package xray + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "time" + "math" + + "x-ui/logger" + "x-ui/util/common" + + "github.com/xtls/xray-core/app/proxyman/command" + statsService "github.com/xtls/xray-core/app/stats/command" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/shadowsocks" + "github.com/xtls/xray-core/proxy/shadowsocks_2022" + "github.com/xtls/xray-core/proxy/trojan" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vmess" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type XrayAPI struct { + HandlerServiceClient *command.HandlerServiceClient + StatsServiceClient *statsService.StatsServiceClient + grpcClient *grpc.ClientConn + isConnected bool +} + +func (x *XrayAPI) Init(apiPort int) error { + if apiPort <= 0 || apiPort > math.MaxUint16 { + return fmt.Errorf("invalid Xray API port: %d", apiPort) + } + + addr := fmt.Sprintf("127.0.0.1:%d", apiPort) + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("failed to connect to Xray API: %w", err) + } + + x.grpcClient = conn + x.isConnected = true + + hsClient := command.NewHandlerServiceClient(conn) + ssClient := statsService.NewStatsServiceClient(conn) + + x.HandlerServiceClient = &hsClient + x.StatsServiceClient = &ssClient + + return nil +} + +func (x *XrayAPI) Close() { + if x.grpcClient != nil { + x.grpcClient.Close() + } + x.HandlerServiceClient = nil + x.StatsServiceClient = nil + x.isConnected = false +} + +func (x *XrayAPI) AddInbound(inbound []byte) error { + client := *x.HandlerServiceClient + + conf := new(conf.InboundDetourConfig) + err := json.Unmarshal(inbound, conf) + if err != nil { + logger.Debug("Failed to unmarshal inbound:", err) + return err + } + config, err := conf.Build() + if err != nil { + logger.Debug("Failed to build inbound Detur:", err) + return err + } + inboundConfig := command.AddInboundRequest{Inbound: config} + + _, err = client.AddInbound(context.Background(), &inboundConfig) + + return err +} + +func (x *XrayAPI) DelInbound(tag string) error { + client := *x.HandlerServiceClient + _, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ + Tag: tag, + }) + return err +} + +func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error { + var account *serial.TypedMessage + switch Protocol { + case "vmess": + account = serial.ToTypedMessage(&vmess.Account{ + Id: user["id"].(string), + }) + case "vless": + vlessAccount := &vless.Account{ + Id: user["id"].(string), + Flow: user["flow"].(string), + } + // Add testseed if provided + if testseedVal, ok := user["testseed"]; ok { + if testseedArr, ok := testseedVal.([]interface{}); ok && len(testseedArr) >= 4 { + testseed := make([]uint32, len(testseedArr)) + for i, v := range testseedArr { + if num, ok := v.(float64); ok { + testseed[i] = uint32(num) + } + } + vlessAccount.Testseed = testseed + } else if testseedArr, ok := testseedVal.([]uint32); ok && len(testseedArr) >= 4 { + vlessAccount.Testseed = testseedArr + } + } + // Add testpre if provided (for outbound, but can be in user for compatibility) + if testpreVal, ok := user["testpre"]; ok { + if testpre, ok := testpreVal.(float64); ok && testpre > 0 { + vlessAccount.Testpre = uint32(testpre) + } else if testpre, ok := testpreVal.(uint32); ok && testpre > 0 { + vlessAccount.Testpre = testpre + } + } + account = serial.ToTypedMessage(vlessAccount) + case "trojan": + account = serial.ToTypedMessage(&trojan.Account{ + Password: user["password"].(string), + }) + case "shadowsocks": + var ssCipherType shadowsocks.CipherType + switch user["cipher"].(string) { + case "aes-128-gcm": + ssCipherType = shadowsocks.CipherType_AES_128_GCM + case "aes-256-gcm": + ssCipherType = shadowsocks.CipherType_AES_256_GCM + case "chacha20-poly1305", "chacha20-ietf-poly1305": + ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305 + case "xchacha20-poly1305", "xchacha20-ietf-poly1305": + ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305 + default: + ssCipherType = shadowsocks.CipherType_NONE + } + + if ssCipherType != shadowsocks.CipherType_NONE { + account = serial.ToTypedMessage(&shadowsocks.Account{ + Password: user["password"].(string), + CipherType: ssCipherType, + }) + } else { + account = serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{ + Key: user["password"].(string), + Email: user["email"].(string), + }) + } + default: + return nil + } + + // 〔中文注释〕: (修改点) 创建一个有5秒超时限制的上下文(Context)。 + // 这确保了如果 Xray-Core API 因为某些原因没有及时响应, + // 这个操作不会永久阻塞,而是在5秒后自动失败,从而提高程序的健壮性。 + // 这与 RemoveUser 函数中的超时设置保持了一致。 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + client := *x.HandlerServiceClient + + _, err := client.AlterInbound(ctx, &command.AlterInboundRequest{ // 〔中文注释〕: (修改点) 使用上面创建的带超时的 ctx + Tag: inboundTag, + Operation: serial.ToTypedMessage(&command.AddUserOperation{ + User: &protocol.User{ + Email: user["email"].(string), + Account: account, + }, + }), + }) + + // 〔中文注释〕: (修改点) 增加更详细的错误日志,方便排查问题。 + if err != nil { + emailStr, _ := user["email"].(string) + return fmt.Errorf("failed to add user '%s' to inbound '%s': %w", emailStr, inboundTag, err) + } + + return nil +} + +func (x *XrayAPI) RemoveUser(inboundTag, email string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + op := &command.RemoveUserOperation{Email: email} + req := &command.AlterInboundRequest{ + Tag: inboundTag, + Operation: serial.ToTypedMessage(op), + } + + _, err := (*x.HandlerServiceClient).AlterInbound(ctx, req) + if err != nil { + return fmt.Errorf("failed to remove user: %w", err) + } + + return nil +} + +func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { + if x.grpcClient == nil { + return nil, nil, common.NewError("xray api is not initialized") + } + + trafficRegex := regexp.MustCompile(`(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)`) + clientTrafficRegex := regexp.MustCompile(`user>>>([^>]+)>>>traffic>>>(downlink|uplink)`) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + if x.StatsServiceClient == nil { + return nil, nil, common.NewError("xray StatusServiceClient is not initialized") + } + + resp, err := (*x.StatsServiceClient).QueryStats(ctx, &statsService.QueryStatsRequest{Reset_: reset}) + if err != nil { + logger.Debug("Failed to query Xray stats:", err) + return nil, nil, err + } + + tagTrafficMap := make(map[string]*Traffic) + emailTrafficMap := make(map[string]*ClientTraffic) + + for _, stat := range resp.GetStat() { + if matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 { + processTraffic(matches, stat.Value, tagTrafficMap) + } else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 { + processClientTraffic(matches, stat.Value, emailTrafficMap) + } + } + return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil +} + +func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) { + isInbound := matches[1] == "inbound" + tag := matches[2] + isDown := matches[3] == "downlink" + + if tag == "api" { + return + } + + traffic, ok := trafficMap[tag] + if !ok { + traffic = &Traffic{ + IsInbound: isInbound, + IsOutbound: !isInbound, + Tag: tag, + } + trafficMap[tag] = traffic + } + + if isDown { + traffic.Down = value + } else { + traffic.Up = value + } +} + +func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) { + email := matches[1] + isDown := matches[2] == "downlink" + + traffic, ok := clientTrafficMap[email] + if !ok { + traffic = &ClientTraffic{Email: email} + clientTrafficMap[email] = traffic + } + + if isDown { + traffic.Down = value + } else { + traffic.Up = value + } +} + +func mapToSlice[T any](m map[string]*T) []*T { + result := make([]*T, 0, len(m)) + for _, v := range m { + result = append(result, v) + } + return result +} diff --git a/xray/client_traffic.go b/xray/client_traffic.go new file mode 100644 index 0000000..fe527d5 --- /dev/null +++ b/xray/client_traffic.go @@ -0,0 +1,15 @@ +package xray + +type ClientTraffic struct { + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + InboundId int `json:"inboundId" form:"inboundId"` + Enable bool `json:"enable" form:"enable"` + Email string `json:"email" form:"email" gorm:"unique"` + Up int64 `json:"up" form:"up"` + Down int64 `json:"down" form:"down"` + AllTime int64 `json:"allTime" form:"allTime"` + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` + Total int64 `json:"total" form:"total"` + Reset int `json:"reset" form:"reset" gorm:"default:0"` + LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"` +} diff --git a/xray/config.go b/xray/config.go new file mode 100644 index 0000000..a246b84 --- /dev/null +++ b/xray/config.go @@ -0,0 +1,69 @@ +package xray + +import ( + "bytes" + + "x-ui/util/json_util" +) + +type Config struct { + LogConfig json_util.RawMessage `json:"log"` + RouterConfig json_util.RawMessage `json:"routing"` + DNSConfig json_util.RawMessage `json:"dns"` + InboundConfigs []InboundConfig `json:"inbounds"` + OutboundConfigs json_util.RawMessage `json:"outbounds"` + Transport json_util.RawMessage `json:"transport"` + Policy json_util.RawMessage `json:"policy"` + API json_util.RawMessage `json:"api"` + Stats json_util.RawMessage `json:"stats"` + Reverse json_util.RawMessage `json:"reverse"` + FakeDNS json_util.RawMessage `json:"fakedns"` + Observatory json_util.RawMessage `json:"observatory"` + BurstObservatory json_util.RawMessage `json:"burstObservatory"` + Metrics json_util.RawMessage `json:"metrics"` +} + +func (c *Config) Equals(other *Config) bool { + if len(c.InboundConfigs) != len(other.InboundConfigs) { + return false + } + for i, inbound := range c.InboundConfigs { + if !inbound.Equals(&other.InboundConfigs[i]) { + return false + } + } + if !bytes.Equal(c.LogConfig, other.LogConfig) { + return false + } + if !bytes.Equal(c.RouterConfig, other.RouterConfig) { + return false + } + if !bytes.Equal(c.DNSConfig, other.DNSConfig) { + return false + } + if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) { + return false + } + if !bytes.Equal(c.Transport, other.Transport) { + return false + } + if !bytes.Equal(c.Policy, other.Policy) { + return false + } + if !bytes.Equal(c.API, other.API) { + return false + } + if !bytes.Equal(c.Stats, other.Stats) { + return false + } + if !bytes.Equal(c.Reverse, other.Reverse) { + return false + } + if !bytes.Equal(c.FakeDNS, other.FakeDNS) { + return false + } + if !bytes.Equal(c.Metrics, other.Metrics) { + return false + } + return true +} diff --git a/xray/inbound.go b/xray/inbound.go new file mode 100644 index 0000000..ea11449 --- /dev/null +++ b/xray/inbound.go @@ -0,0 +1,42 @@ +package xray + +import ( + "bytes" + + "x-ui/util/json_util" +) + +type InboundConfig struct { + Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string + Port int `json:"port"` + Protocol string `json:"protocol"` + Settings json_util.RawMessage `json:"settings"` + StreamSettings json_util.RawMessage `json:"streamSettings"` + Tag string `json:"tag"` + Sniffing json_util.RawMessage `json:"sniffing"` +} + +func (c *InboundConfig) Equals(other *InboundConfig) bool { + if !bytes.Equal(c.Listen, other.Listen) { + return false + } + if c.Port != other.Port { + return false + } + if c.Protocol != other.Protocol { + return false + } + if !bytes.Equal(c.Settings, other.Settings) { + return false + } + if !bytes.Equal(c.StreamSettings, other.StreamSettings) { + return false + } + if c.Tag != other.Tag { + return false + } + if !bytes.Equal(c.Sniffing, other.Sniffing) { + return false + } + return true +} diff --git a/xray/log_writer.go b/xray/log_writer.go new file mode 100644 index 0000000..b6e04db --- /dev/null +++ b/xray/log_writer.go @@ -0,0 +1,90 @@ +package xray + +import ( + "regexp" + "strings" + + "x-ui/logger" +) + +func NewLogWriter() *LogWriter { + return &LogWriter{} +} + +type LogWriter struct { + lastLine string +} + +func (lw *LogWriter) Write(m []byte) (n int, err error) { + crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`) + + // Convert the data to a string + message := strings.TrimSpace(string(m)) + + // Check if the message contains a crash + if crashRegex.MatchString(message) { + logger.Debug("Core crash detected:\n", message) + lw.lastLine = message + err1 := writeCrashReport(m) + if err1 != nil { + logger.Error("Unable to write crash report:", err1) + } + return len(m), nil + } + + regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) \[([^\]]+)\] (.+)$`) + messages := strings.SplitSeq(message, "\n") + + for msg := range messages { + matches := regex.FindStringSubmatch(msg) + + if len(matches) > 3 { + level := matches[2] + msgBody := matches[3] + msgBodyLower := strings.ToLower(msgBody) + + if strings.Contains(msgBodyLower, "tls handshake error") || + strings.Contains(msgBodyLower, "connection ends") { + logger.Debug("XRAY: " + msgBody) + lw.lastLine = "" + continue + } + + if strings.Contains(msgBodyLower, "failed") { + logger.Error("XRAY: " + msgBody) + } else { + switch level { + case "Debug": + logger.Debug("XRAY: " + msgBody) + case "Info": + logger.Info("XRAY: " + msgBody) + case "Warning": + logger.Warning("XRAY: " + msgBody) + case "Error": + logger.Error("XRAY: " + msgBody) + default: + logger.Debug("XRAY: " + msg) + } + } + lw.lastLine = "" + } else if msg != "" { + msgLower := strings.ToLower(msg) + + if strings.Contains(msgLower, "tls handshake error") || + strings.Contains(msgLower, "connection ends") { + logger.Debug("XRAY: " + msg) + lw.lastLine = msg + continue + } + + if strings.Contains(msgLower, "failed") { + logger.Error("XRAY: " + msg) + } else { + logger.Debug("XRAY: " + msg) + } + lw.lastLine = msg + } + } + + return len(m), nil +} diff --git a/xray/process.go b/xray/process.go new file mode 100644 index 0000000..33700aa --- /dev/null +++ b/xray/process.go @@ -0,0 +1,261 @@ +package xray + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "runtime" + "sync" + "syscall" + "time" + + "x-ui/config" + "x-ui/logger" + "x-ui/util/common" +) + +func GetBinaryName() string { + return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) +} + +func GetBinaryPath() string { + return config.GetBinFolderPath() + "/" + GetBinaryName() +} + +func GetConfigPath() string { + return config.GetBinFolderPath() + "/config.json" +} + +func GetGeositePath() string { + return config.GetBinFolderPath() + "/geosite.dat" +} + +func GetGeoipPath() string { + return config.GetBinFolderPath() + "/geoip.dat" +} + +func GetIPLimitLogPath() string { + return config.GetLogFolder() + "/3xipl.log" +} + +func GetIPLimitBannedLogPath() string { + return config.GetLogFolder() + "/3xipl-banned.log" +} + +func GetIPLimitBannedPrevLogPath() string { + return config.GetLogFolder() + "/3xipl-banned.prev.log" +} + +func GetAccessPersistentLogPath() string { + return config.GetLogFolder() + "/3xipl-ap.log" +} + +func GetAccessPersistentPrevLogPath() string { + return config.GetLogFolder() + "/3xipl-ap.prev.log" +} + +func GetAccessLogPath() (string, error) { + config, err := os.ReadFile(GetConfigPath()) + if err != nil { + logger.Warningf("Failed to read configuration file: %s", err) + return "", err + } + + jsonConfig := map[string]any{} + err = json.Unmarshal([]byte(config), &jsonConfig) + if err != nil { + logger.Warningf("Failed to parse JSON configuration: %s", err) + return "", err + } + + if jsonConfig["log"] != nil { + jsonLog := jsonConfig["log"].(map[string]any) + if jsonLog["access"] != nil { + accessLogPath := jsonLog["access"].(string) + return accessLogPath, nil + } + } + return "", err +} + +func stopProcess(p *Process) { + p.Stop() +} + +type Process struct { + *process +} + +func NewProcess(xrayConfig *Config) *Process { + p := &Process{newProcess(xrayConfig)} + runtime.SetFinalizer(p, stopProcess) + return p +} + +type process struct { + cmd *exec.Cmd + + version string + apiPort int + + onlineClients []string + mutex sync.RWMutex + + config *Config + logWriter *LogWriter + exitErr error + startTime time.Time +} + +func newProcess(config *Config) *process { + return &process{ + version: "Unknown", + config: config, + logWriter: NewLogWriter(), + startTime: time.Now(), + } +} + +func (p *process) IsRunning() bool { + if p.cmd == nil || p.cmd.Process == nil { + return false + } + if p.cmd.ProcessState == nil { + return true + } + return false +} + +func (p *process) GetErr() error { + return p.exitErr +} + +func (p *process) GetResult() string { + if len(p.logWriter.lastLine) == 0 && p.exitErr != nil { + return p.exitErr.Error() + } + return p.logWriter.lastLine +} + +func (p *process) GetVersion() string { + return p.version +} + +func (p *Process) GetAPIPort() int { + return p.apiPort +} + +func (p *Process) GetConfig() *Config { + return p.config +} + +func (p *Process) GetOnlineClients() []string { + p.mutex.RLock() + defer p.mutex.RUnlock() + clientsCopy := make([]string, len(p.onlineClients)) + copy(clientsCopy, p.onlineClients) + return clientsCopy +} + +func (p *Process) SetOnlineClients(clients []string) { + p.mutex.Lock() + defer p.mutex.Unlock() + p.onlineClients = clients +} + +func (p *Process) GetUptime() uint64 { + return uint64(time.Since(p.startTime).Seconds()) +} + +func (p *process) refreshAPIPort() { + for _, inbound := range p.config.InboundConfigs { + if inbound.Tag == "api" { + p.apiPort = inbound.Port + break + } + } +} + +func (p *process) refreshVersion() { + cmd := exec.Command(GetBinaryPath(), "-version") + data, err := cmd.Output() + if err != nil { + p.version = "Unknown" + } else { + datas := bytes.Split(data, []byte(" ")) + if len(datas) <= 1 { + p.version = "Unknown" + } else { + p.version = string(datas[1]) + } + } +} + +func (p *process) Start() (err error) { + if p.IsRunning() { + return errors.New("xray is already running") + } + + defer func() { + if err != nil { + logger.Error("Failure in running xray-core process: ", err) + p.exitErr = err + } + }() + + data, err := json.MarshalIndent(p.config, "", " ") + if err != nil { + return common.NewErrorf("Failed to generate XRAY configuration files: %v", err) + } + + err = os.MkdirAll(config.GetLogFolder(), 0o770) + if err != nil { + logger.Warningf("Failed to create log folder: %s", err) + } + + configPath := GetConfigPath() + err = os.WriteFile(configPath, data, fs.ModePerm) + if err != nil { + return common.NewErrorf("Failed to write configuration file: %v", err) + } + + cmd := exec.Command(GetBinaryPath(), "-c", configPath) + p.cmd = cmd + + cmd.Stdout = p.logWriter + cmd.Stderr = p.logWriter + + go func() { + err := cmd.Run() + if err != nil { + logger.Error("Failure in running xray-core:", err) + p.exitErr = err + } + }() + + p.refreshVersion() + p.refreshAPIPort() + + return nil +} + +func (p *process) Stop() error { + if !p.IsRunning() { + return errors.New("xray is not running") + } + + if runtime.GOOS == "windows" { + return p.cmd.Process.Kill() + } else { + return p.cmd.Process.Signal(syscall.SIGTERM) + } +} + +func writeCrashReport(m []byte) error { + crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log" + return os.WriteFile(crashReportPath, m, os.ModePerm) +} diff --git a/xray/traffic.go b/xray/traffic.go new file mode 100644 index 0000000..7b907ba --- /dev/null +++ b/xray/traffic.go @@ -0,0 +1,9 @@ +package xray + +type Traffic struct { + IsInbound bool + IsOutbound bool + Tag string + Up int64 + Down int64 +}