From 796b5b3c57dd970c54070fa360912fc63f7e9dd0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Sep 2026 04:42:56 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=88=20Sync=202026-09-29=2004:42:56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-netwiz/LICENSE | 674 ++ luci-app-netwiz/README.md | 357 + luci-app-netwiz/install.sh | 230 + luci-app-netwiz/luci-app-netwiz/LICENSE | 674 ++ luci-app-netwiz/luci-app-netwiz/Makefile | 17 + luci-app-netwiz/luci-app-netwiz/README.md | 139 + .../luci-static/resources/view/netwiz.css | 715 ++ .../luci-static/resources/view/netwiz.js | 6289 +++++++++++++++++ .../luci-static/resources/view/netwiz_dev.js | 2707 +++++++ .../luci-static/resources/view/netwiz_tmpl.js | 1115 +++ .../luci-static/resources/view/qrcode.min.js | 1 + .../luci-app-netwiz/po/zh_Hans/netwiz.po | 2353 ++++++ .../luci-app-netwiz/po/zh_Hant/netwiz.po | 2353 ++++++ .../luci-app-netwiz/root/etc/config/netwiz | 5 + .../root/etc/hotplug.d/dhcp/99-netwiz-guard | 46 + .../root/etc/init.d/netwiz-monitor | 148 + .../root/etc/init.d/netwiz-recovery | 128 + .../root/etc/init.d/netwiz-watchdog | 29 + .../root/usr/libexec/netwiz-autodetect.sh | 172 + .../root/usr/libexec/netwiz-monitor-loop.sh | 134 + .../root/usr/libexec/rpcd/netwiz | 2648 +++++++ .../root/usr/libexec/rpcd/netwiz_dev | 1320 ++++ .../share/luci/menu.d/luci-app-netwiz.json | 23 + .../usr/share/rpcd/acl.d/luci-app-netwiz.json | 17 + luci-app-netwiz/probe.py | 234 + luci-app-netwiz/worker.js | 217 + luci-app-wwand/Makefile | 8 +- luci-proto-wwand/Makefile | 6 +- wwand-ipa/Makefile | 4 +- wwand-ipad/Makefile | 4 +- wwand-rsim/Makefile | 10 +- wwand/Makefile | 6 +- 32 files changed, 22765 insertions(+), 18 deletions(-) create mode 100644 luci-app-netwiz/LICENSE create mode 100644 luci-app-netwiz/README.md create mode 100644 luci-app-netwiz/install.sh create mode 100644 luci-app-netwiz/luci-app-netwiz/LICENSE create mode 100644 luci-app-netwiz/luci-app-netwiz/Makefile create mode 100644 luci-app-netwiz/luci-app-netwiz/README.md create mode 100644 luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.css create mode 100644 luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.js create mode 100644 luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_dev.js create mode 100644 luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_tmpl.js create mode 100644 luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/qrcode.min.js create mode 100644 luci-app-netwiz/luci-app-netwiz/po/zh_Hans/netwiz.po create mode 100644 luci-app-netwiz/luci-app-netwiz/po/zh_Hant/netwiz.po create mode 100644 luci-app-netwiz/luci-app-netwiz/root/etc/config/netwiz create mode 100644 luci-app-netwiz/luci-app-netwiz/root/etc/hotplug.d/dhcp/99-netwiz-guard create mode 100644 luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-monitor create mode 100644 luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-recovery create mode 100644 luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-watchdog create mode 100644 luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-autodetect.sh create mode 100644 luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-monitor-loop.sh create mode 100644 luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz create mode 100644 luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz_dev create mode 100644 luci-app-netwiz/luci-app-netwiz/root/usr/share/luci/menu.d/luci-app-netwiz.json create mode 100644 luci-app-netwiz/luci-app-netwiz/root/usr/share/rpcd/acl.d/luci-app-netwiz.json create mode 100644 luci-app-netwiz/probe.py create mode 100644 luci-app-netwiz/worker.js diff --git a/luci-app-netwiz/LICENSE b/luci-app-netwiz/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-app-netwiz/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/luci-app-netwiz/README.md b/luci-app-netwiz/README.md new file mode 100644 index 000000000..9a832107c --- /dev/null +++ b/luci-app-netwiz/README.md @@ -0,0 +1,357 @@ +# NetWiz (luci-app-netwiz) 🚀 + +[English](#english) | [简体中文](#简体中文) + +**NetWiz** —— 您的专属 OpenWrt 极简网络设置向导。开箱即用,化繁为简。 + +凭借严谨缜密的底层逻辑,NetWiz 不仅为您提供直观的交互与大小屏极致丝滑的 UI 体验,更带来全方位的企业级网络守护: + +🛡️ 智能配置与防呆守护:搭载企业级 LAN 口防失联机制与全网段冲突排雷(智能解决 IP、网关、WAN/LAN 冲突)。支持 WAN 口上网方式盲插智能切换(断开 10 秒后重新接入自动侦测)、智能防错校验、一键开启 IPv6,以及智能 Wi-Fi 满血释放(无物理硬件时自动隐藏)。 + +📊 企业级网络管控:深入系统内核,提供灾备级全自动配置管理。涵盖终端流量透视、IP 智能分组、一键批量绑定静态 IP 策略组以及一键防火墙控制,让复杂网络管理得心应手。 + +📦 首创 SmartGhost 智能恢复:NetWiz 不仅仅是设置向导,更打造了独家首创智能备份与恢复的完美闭环。搭载「SmartGhost 全量离线系统恢复」技术,无需全程联网,无惧跨版本刷机!能在彻底断网的新固件上,一键完美恢复您的插件与复杂网络配置,并提供商用级的防内存溢出(OOM)保护与跨网段无缝跃迁的丝滑恢复体验。 + +**NetWiz (Network Wizard)** — Your exclusive, minimalist OpenWrt network setup wizard. Ready out of the box, turning complexity into simplicity. + +Built upon rigorous and well-structured underlying logic, NetWiz delivers not only a seamlessly smooth and responsive UI across all screen sizes but also comprehensive, enterprise-grade network protection: + +🛡️ Intelligent Setup & Failsafe Protection: Features an enterprise-grade LAN fail-safe mechanism and proactive conflict resolution across IP, Gateway, WAN, and LAN subnets. It supports smart WAN mode blind-switching (auto-detects when the cable is reinserted after a 10-second disconnection), smart error-prevention validation, one-click IPv6 activation, and full-performance Wi-Fi configuration (automatically hides when no physical hardware is detected). + +📊 Enterprise-Grade Network Management: Diving deep into the system kernel, NetWiz empowers you with disaster-recovery-level automated management. It offers terminal traffic visibility, smart IP grouping, one-click batch binding for static IP policy groups, and one-click firewall control. + +📦 SmartGhost Offline Full Recovery: Much more than just a setup wizard, NetWiz pioneers a perfect closed-loop backup and restore ecosystem. Powered by the original "SmartGhost Full Offline System Recovery" technology, it requires no continuous internet connection and eliminates the fear of cross-version firmware flashing. It can perfectly clone and restore all your third-party plugins and complex network configs onto newly flashed, completely offline firmware—all while delivering commercial-grade Out-of-Memory (OOM) protection and a seamless cross-subnet recovery experience. + +![Build](https://img.shields.io/badge/Build-Passing-brightgreen.svg) ![OpenWrt](https://img.shields.io/badge/OpenWrt-23.05_|_25.x+-blue.svg) ![ImmortalWrt](https://img.shields.io/badge/ImmortalWrt-Supported-orange.svg) ![Downloads](https://img.shields.io/github/downloads/huchd0/luci-app-netwiz/total.svg?logo=github) ![Views](https://komarev.com/ghpvc/?username=huchd0-luci-app-netwiz&label=Views&color=blue&style=flat) ![License](https://img.shields.io/badge/License-GPL--3.0-green.svg) + +--- + +## English + +### Description + +**NetWiz (Network Setup Wizard)** is a minimalist, secure, and "zero-destructive" network configuration interface exclusively designed for OpenWrt and ImmortalWrt. + +Tailored perfectly for novice users, it enables secure, one-click configuration of secondary routers (Dynamic/Static IP) and Bypass/AP (Wired Repeater) environments. For users who have just flashed a new system, setting up the network is essential for internet access, yet native settings are often deeply hidden and error-prone. Blindly and forcefully resetting network configuration files can **wipe out your configured device lists**, causing carefully crafted network bridges, VLANs, and physical NIC bindings to **crash instantly or trigger inexplicable issues**. **NetWiz is born to solve this exact pain point.** + +### Installation Methods + +[Use the one-click command for quick installation.](#install) + +```bash +if wget -qO /tmp/nw_inst.sh https://raw.githubusercontent.com/huchd0/luci-app-netwiz/master/install.sh; then sh /tmp/nw_inst.sh; else sh /etc/netwiz/custom_pkgs/install.sh; fi +``` + +## 🤝 Compatibility Guide + +* **Firmware Support:** OpenWrt 23.05+ and ImmortalWrt 23.05+ are recommended (Flawlessly supports the new 25.x+ branches and is fully compatible with the `nftables` environment). +* **Architecture Support:** Universal compatibility across all architectures (x86_64, aarch64, ramips/mt7621, etc.). + +**💡 Ultimate System Compatibility:** +NetWiz achieves true "zero external dependencies," utilizing no C/C++/Go compiled binaries or third-party modules. Backed by a highly rigorous underlying safeguard and fallback architecture, NetWiz delivers outstanding compatibility across various branches and heavily modified (custom-built) firmwares. Whether on legacy systems or the new generation `fw4`/DSA architecture post-23.05, it adapts perfectly and runs with rock-solid stability. + +## ✨ Core Features & Advantages + +### 💻 Modern Architecture +* **Lightning-Fast & Stutter-Free:** Built on a modern responsive and elastic architecture, saying goodbye to clunky and laggy interfaces. It perfectly adapts to both desktop and mobile screens for a silky-smooth experience. +* **Strict Privilege Isolation:** Complete separation of UI operations and underlying system privileges. The frontend has no direct permission to modify core configs; all changes are safely executed by a trusted sandbox proxy. +* **Native Multi-Language Support:** Automatically switches between Simplified Chinese, Traditional Chinese, and English based on your system environment. +* **Extensive Hardware Compatibility:** Intelligently adapts to multi-chip and multi-band hardware, maximizing hardware potential and unleashing full Wi-Fi signal strength. + +### 🛡️ Comprehensive Safeguards +* **Failsafe Rollback Mechanism:** Afraid that a configuration error will lock you out of the backend? Don't worry! Applying new configurations triggers a 120-second safety countdown. If no real new IP connections access the LAN port after the change, the system automatically reverts to the previous configuration **(gracefully recovers even upon unexpected power loss or reboot)**. +* **Smart Error Prevention:** Strictly and accurately intercepts high-risk operations such as "IP conflicts or incorrectly filled information," preventing router endless loops right from the source. +* **Bypass/AP Mode Auto-Safeguard:** Switch to Bypass or AP (Wired Repeater) mode safely with one click. The system automatically disables conflicting services, provides smart setup guidance, and helps you fill in the correct information without disrupting your existing home network topology. Beginner-friendly. +* **Comprehensive Device Dashboard:** Master the real-time dynamics of all online, offline, and static devices. +* **Traffic Analysis Probe:** Hover to view the P2P and Web connection distribution for any specific device. +* **Full-Link Disaster Recovery:** Roll back or import external configuration files at any time. + +### ⚡ Ultimate Seamless Experience +* **Smart WAN Mode Switching:** Detects the physical plug/unplug status of the WAN cable and intelligently adapts the network mode. It perfectly filters out "pseudo-disconnections" caused by dial-up failures or software reboots **(unplugging the network cable and reconnecting after 10 seconds triggers the smart mode-switching mechanism)**. +* **Auto-Detection & Instant Redirection:** After modifying the IP, the frontend silently probes the new address. Once the network connects, it instantly redirects, ensuring a buttery-smooth experience. +* **Non-Disruptive Network Settings:** Applying partial network settings keeps Wi-Fi stable and connected without interfering with other devices' internet access. +* **Seamless Bi-Directional Sync:** When switching between "Band Steering (Merged)" and "Split Band" modes, Wi-Fi passwords and SSIDs are intelligently synchronized, eliminating the hassle of repetitive typing. Built-in historical config memory. +* **One-Click IPv6:** Intelligently issues IPv6 addresses with one click, saying goodbye to obscure DHCPv6, SLAAC, and relay settings. With a simple tap, the system automatically assigns IPv6 addresses to your devices. +* **One-Click Seamless Wi-Fi Roaming:** Enable seamless roaming without requiring professional technical skills. Just meet two conditions for automatic, silky-smooth Wi-Fi transitions between multiple routers: identical Wi-Fi names/passwords and devices on the same LAN. *(Note: Ensure no multiple DHCP servers exist in the environment; if routers are close to each other, stagger channels to avoid co-channel signal interference.)* +* **Quick Wireless Repeater (WISP):** Enter the upstream Wi-Fi credentials to receive the signal and broadcast your own exclusive Wi-Fi network. + +### 📶 Powerful Smart Wi-Fi Control +* **Universal Hardware Adaptation:** Automatically takes over all physical wireless NICs in the system, seamlessly adapting whether it's a single-band, dual-band, tri-band, or even multi-band router. +* **Hidden Fault Repair:** Targets incomplete, conflicting, or corrupted underlying configurations. Automatically cleans up legacy error Wi-Fi configs in the system and corrects stuck network states, ensuring highly stable wireless performance. +* **Full Bandwidth Unleashed:** Say goodbye to crippled network speeds! When saving configurations, it intelligently identifies hardware limits and silently optimizes Wi-Fi bandwidth for maximum throughput. + +### 🧭 Quick Navigation +👉 **Find the 👉 Network Wizard directly at the end of the LuCI primary menu (right before the "Logout" button).** + +--- + +### 🚀 Four Core Supported Modules: + +1. 🔌 **Broadband Dial-up (PPPoE)** + * **Scenario:** The optical modem is in pure bridge mode; this router handles the dial-up directly, acting as the whole-house network hub. + * **Behavior:** Accurately writes broadband accounts and passwords, strips residual legacy gateway settings, and safely restarts underlying dial-up processes. + +2. 📶 **Wi-Fi Settings (Auto-hides if no hardware is detected)** + * **Scenario:** Initializing the wireless network for the first time, or modifying the whole-house Wi-Fi name, password, and security encryption protocol. + * **Behavior:** Intelligently detects the number of underlying physical chips and dynamically renders the UI. Supports one-click "Band Steering (Merged)" for a whole-house smart roaming network, or splitting into independent 2.4G/5G/5G_Game networks. Perfectly compatible with legacy 802.11b IoT devices. + +3. 🌐 **Secondary Router Mode (DHCP / Static IP)** + * **Scenario:** The modem handles dial-up, or there is an existing main router upstream. This device acts as a secondary router or subnet router. + * **Behavior:** Freely choose "Dynamic IP (DHCP)" or "Static IP". Automatically takes over WAN port configurations and intelligently validates subnets to prevent routing loops. + +4. 🏠 **LAN Settings (Main Router / Bypass/AP Mode Toggle)** + * **Scenario:** Only modifying the device's internal management IP; or a main router already exists, and this device serves purely as an auxiliary gateway / Bypass router (AP Wired Repeater). + * **Behavior:** One-click toggle for "Bypass/AP Mode". The system automatically disables the local DHCP service and enforces main router gateway input. If acting as the main router, it smartly prompts to leave the gateway blank, guaranteeing zero destruction to the existing LAN topology. + +--- + +## ✨ Device Management Expert: Core Features + +### ⚡ Connection Radar & Kernel-Level Traffic Insight +* **Real-Time Connection Tracking:** Deeply parses the core modules of network connection states, capturing the underlying network connections of terminal devices in real-time. +* **Smart Traffic Categorization:** Automatically and accurately categorizes traffic into Web/HTTPS, DNS/NTP, UDP Media, and P2P High-Port downloads, displayed via an **intuitive dynamic pie chart**. +* **Abnormal Terminal Alert:** Automatically identifies cross-subnet devices and suspected spoofed MAC devices. + +### 🏷️ Smart IP Grouping & Batch Static Assignment +* **Fully Automated Categorization:** Accurately identifies Mobile (Phones/Tablets), PC (Workstations), and IoT (Smart Home devices). +* **Multi-Strategy Batch Deployment:** Select multiple devices to execute a one-click **sequential assignment**, **smart exclusive subnet assignment by device type**, or assign to **custom departments/rooms**. +* **Secure Anti-Conflict:** Built-in smart anti-conflict algorithm. Automatically postpones IP allocation when conflicts occur, eliminating network paralysis. + +### 🌐 Pure-Blood IPv6 Deep Support +* **Seamless Parsing:** Automatically filters out link-local addresses (`fe80`) to accurately extract public IPv6 addresses. +* **PC Keep-Alive Mechanism:** An exclusive `v6pc_` active timestamp mechanism prevents PC IPv6 addresses from frequently shifting, which leads to lost connections. + +### 🛡️ One-Click Firewall & Access Control +* **⛔ Block Internet:** Cut off external network access with one click while preserving LAN communication. Takes effect in seconds. +* **🛡️ LAN Isolation:** One-click ban on accessing other devices within the internal network (prevents bypass router snooping and smart home privacy leaks). +* **🚀 DMZ Host:** Expose all ports of a specific device to the public network with one click (with automatic mutual-exclusion validation to prevent conflicts). +* **🔌 Wake on LAN (WOL):** Deeply integrated; the wake button automatically appears when a device goes offline. + +### 📦 Disaster-Recovery Level Config Management +* **Lossless Auto-Backup:** When the network topology changes (e.g., modifying DHCP or firewall), the backend automatically calculates the MD5 of core configuration files and generates a `.tar.gz` backup archive upon changes. +* **One-Click Import/Export:** Pack and download all current static IPs, blacklists, and isolation rules to your PC. Or, easily restore them from the cloud/local storage after flashing the firmware, automatically waking up all advanced proxy and DNS plugins post-recovery. + +--- + +## 简体中文 + +### 📖简介 | [English](#english) + +**NetWiz (网络设置向导)** 是一款专为 OpenWrt / ImmortalWrt 设计的极简、安全且“零破坏”的网络配置界面。 + +NetWiz极其适合新手用户,能够一键安全地配置二级路由(动态 / 静态 IP)以及旁路由 / AP(有线中继)环境。对于刚刷完系统的玩家,正确配置网络是上网的第一步,但系统自带的设置隐藏较深且容易配错。此时若盲目、暴力地重置整个网络配置文件,会**清空路由器的已配置清单**,导致精心设置的网桥(Bridge)、VLAN 和物理网卡绑定**崩溃或引发莫名其妙的故障**。**NetWiz 专为解决此痛点而生。** + +## 🤝 兼容性说明 + +* **固件支持**:建议 OpenWrt 23.05 及以上版本、ImmortalWrt 23.05 及以上版本(完美支持 25.x+ 新分支,全面兼容 nftables 环境)。 +* **架构支持**:全架构通用 (x86_64, aarch64, ramips/mt7621 等)。 + +**💡 极致的系统兼容性:** +NetWiz 的“零外部依赖”,无任何 C/C++/Go 编译件、第三方模块 和 扎实严谨的底层防护与兜底架构,使 NetWiz 在不同分支和各类魔改固件中表现出众的兼容能力。无论是老旧系统,还是 23.05 之后的新一代 fw4/DSA 架构,皆能适配,稳定运行。 + +## ✨ 核心特性与优势 + +### 💻 现代化架构 +* **极速加载不卡顿:** 采用最新响应式、弹性架构,告别传统界面的简陋与卡顿,完美适配电脑和手机屏幕,操作如丝般顺滑。 +* **严格的权限隔离:** 界面操作与底层系统权限彻底分离。前端不具备直接修改底层配置的权限,所有变更均由受信任的沙箱代理安全执行。 +* **优雅的 UI 细节:** 告别烦人的弹窗打扰,版本更新采用简洁的右上角小红点提示,状态一目了然。(暂未启用) +* **原生多语言支持:** 根据您的系统环境,自动切换简体中文、繁体中文或英文界面。 +* **兼容众多品牌硬件:** 智能适配多芯片频段硬件,最大化挖掘硬件性能,满血释放 Wi-Fi 信号。 + +### 🛡️ 全方位的守护 +* **安全回退机制:** 配置错误导致无法连接后台?不用怕!应用新配置后会启动 120 秒安全倒计时。LAN 口配置后若无真实的新 IP 访问连接,系统将自动恢复到修改前的配置 **(意外断电重启依然优雅恢复)**。 +* **智能防错校验:** 严格并精准拦截“ IP 冲突、信息填错”等高危操作,从源头防止路由死循环。 +* **旁路由 / AP 模式自动排雷:** 一键安全切换旁路由或 AP(有线中继)模式。系统会自动帮您关掉冲突服务,智能指引您正确填写信息,绝不破坏现有的家庭网络拓扑,新手也能轻松设置。 +* **设备综合大盘:** 掌握所有在线、离线及静态设备的实时动态。 +* **流量分析探针:** 悬停即可查看该设备的 P2P 与 Web 连接数分布。 +* **全链路灾备:** 随时回滚或导入外部配置文件。 + +### ⚡ 极致的无感体验 +* **WAN 口上网方式智能切换:** 自动识别 WAN 口网线的物理插拔状态并智能适配,完美过滤由拨号失败、软件重启引起的“假性掉线” **(拔插 WAN 口网线间隔十秒,即可触发智能切换机制)**。 +* **自动探活,秒级跳转:** 修改 IP 后,前端会静默探测新地址,网络一旦连通立刻瞬间跳转,体验丝滑流畅。 +* **改设置不断网:** 在应用部分网络设置时,Wi-Fi 保持稳定不断开,不干扰其他设备的正常上网。 +* **双向数据无缝同步:** 在“多频合一”与“分频独立”模式间切换时,Wi-Fi 密码与 SSID 智能双向传递,彻底告别重复输入的烦恼。自带历史配置记忆功能。 +* **一键开启 IPv6:** 智能下发 IPv6 地址,告别晦涩难懂的 DHCPv6、SLAAC 和中继设置。轻轻一点,系统即可为设备自动下发公网 IPv6。 +* **一键 Wi-Fi 无缝漫游:** 用户无需专业技术,仅需满足两项条件即可拥有多路由器间 Wi-Fi 的自动“丝滑”跳转:Wi-Fi 名称、密码一致,且设备处于同一局域网。**(注意:环境中请勿存在多个 DHCP 服务;若路由器摆放距离较近,建议错开信道,避免同频干扰。)** +* **快速启用无线中继 (WISP):** 输入上级 Wi-Fi 帐号密码接收信号,并广播您自己的专属 Wi-Fi 网络。 + +### 📶 强大的无线智控 +* **自动适配所有硬件:** 自动接管系统内所有物理无线网卡,无论是单频、双频还是三频(甚至多频)路由器,均能完美适配。 +* **修复隐藏故障:** 针对各种残缺、冲突或被带偏的底层配置,自动清理系统中遗留的错误 Wi-Fi 配置,纠正被卡死的网络状态,让无线表现更稳定。 +* **智能频宽满血释放:** 告别残血网络!在保存配置时,智能识别硬件极限,静默优化并提升 Wi-Fi 频宽,轻松跑满带宽。 + +### 🧭 快捷导航 +👉 **在 LuCI 一级菜单的最后面(“退出”按钮的前一个),即可直接找到 👉 网络向导**。 + +--- + +## 🚀 核心支持的四大模块 + +1. 🔌 **宽带拨号 (PPPoE)** + * **适用场景**:光猫为纯桥接模式,由本路由器直接进行宽带拨号上网,承担全屋网络枢纽。 + * **行为**:精准写入宽带账号与密码,剥离多余网关遗留,安全重启底层拨号进程。 + +2. 📶 **Wi-Fi 设置(无硬件时自动隐藏)** + * **适用场景**:首次配置无线网络,或 修改全屋 Wi-Fi 名称、密码 及 是否启用无线中继 (WISP)。 + * **行为**:智能接管底层物理芯片并动态渲染界面。支持一键“多频合一”组建全屋智能漫游网,或拆分为独立的 2.4G / 5G / 5G_Game 频段;可以启用兼容旧版古董智能物联设备 **(网速会被拖累、稳定性变差)**;默认开启不同路由器间5G频段的Wi-Fi信号顺滑切换功能。 + +3. 🌐 **二级路由模式 (DHCP / 静态 IP)** + * **适用场景**:光猫已经负责拨号,或者上级有主路由,本设备作为二级路由或子网段路由接入。 + * **行为**:可自由选择“动态获取”或“静态绑定”,自动接管 WAN 口配置,智能校验网段避免死循环。 + +4. 🏠 **局域网设置 (主路由 / 旁路由切换)** + * **适用场景**:仅需修改设备内网管理 IP;或者网络内已有主路由,本设备仅作为辅助网关或旁路由(AP 有线中继)。 + * **行为**:一键开启“旁路由模式”,系统将自动关闭本机 DHCP 服务,并要求必须填写主路由网关;若为主路由,则提示网关留空,不会破坏现有局域网拓扑。 + +--- + +## ✨ 设备管理专家核心特性 + +### ⚡ 终端雷达 & 内核级流量透视 (Connection Radar) +* **实时连接追踪**:深入解析网络连接状态的核心模块,实时捕获终端设备的底层网络连接。 +* **智能流量分类**:自动将流量精准分类为 Web/HTTPS、DNS/NTP、UDP 媒体、P2P 高端口下载,并以**直观的动态圆饼图**展示。 +* **异常终端预警**:自动识别跨网段设备与疑似伪装 MAC 设备。 + +### 🏷️ 智能 IP 分组与批量绑定固定 IP (Smart IP Assignment) +* **全自动分类**:精准识别 Mobile(手机 / 平板)、PC(电脑 / 工作站)、IoT(智能家居)。 +* **多策略批量下发**:支持选择多个设备,一键执行**顺序分配**、**按设备类型智能分配专属网段**,或分配至**自定义部门 / 房间**。 +* **安全防冲突**:内置智能防冲突算法,IP 冲突时自动顺延,杜绝网络瘫痪。 + +### 🌐 纯血 IPv6 深度支持 +* **无感解析**:自动过滤本地链路地址(`fe80`),精准提取公网 IPv6。 +* **PC 存活保活机制**:独创的 `v6pc_` 活跃时间戳机制,防止 PC 设备的 IPv6 地址频繁变动导致失联。 + +### 🛡️ 一键防火墙与访问控制 (Firewall Control) +* **⛔ 断网 (Block)**:一键阻断设备访问外网,保留局域网通信,秒级生效。 +* **🛡️ 局域网隔离 (Isolate)**:一键禁止设备访问内网其他设备(防旁路由嗅探、防智能家居隐私泄露)。 +* **🚀 DMZ 主机**:一键将特定设备的全部端口暴露至公网(自动互斥校验,防止冲突)。 +* **🔌 局域网唤醒 (WOL)**:深度集成,设备离线时自动显示唤醒按钮。 + +### 📦 灾备级配置管理 (Auto-Backup & Disaster Recovery) +* **无损自动备份**:当网络拓扑发生改变时(如修改 DHCP、防火墙),后台自动计算核心配置文件的 MD5,并在变动时自动生成 `.tar.gz` 备份存档。 +* **一键导入 / 导出**:支持将当前所有的静态 IP、黑名单、隔离规则打包下载到电脑,或在刷机后一键从云端 / 本地恢复,恢复后自动唤醒所有高级代理与 DNS 插件。 + +--- + + +## 一、🚀 极速安装 (推荐) + +我们提供了一键智能安装脚本。无论你是老系统还是新系统,只需在 SSH 终端中直接复制并执行以下单行命令,即可自动完成 **【判断系统架构 -> 下载对应格式 -> 安装 -> 修复权限与清理缓存】** 的全流程: + +```bash +if wget -qO /tmp/nw_inst.sh https://raw.githubusercontent.com/huchd0/luci-app-netwiz/master/install.sh; then sh /tmp/nw_inst.sh; else sh /etc/netwiz/custom_pkgs/install.sh; fi +``` + +💡 **提示**:如果你的网络无法直接访问 GitHub Raw,可以在链接前加上代理加速,例如: +```bash +if wget -qO /tmp/nw_inst.sh https://ghproxy.net/https://raw.githubusercontent.com/huchd0/luci-app-netwiz/master/install.sh; then sh /tmp/nw_inst.sh; else sh /etc/netwiz/custom_pkgs/install.sh; fi +``` + +--- + +## 二、🛠️ 手动安装 (离线包) + +如果你习惯手动操作,请前往 [Releases 页面] 下载最新的安装包,并使用 WinSCP 将其上传至路由器的 `/tmp/` 目录。 + +**对于 OpenWrt 25.x 及最新快照版 (`.apk` 格式):** +上传至 `/tmp/` 目录后,运行以下命令(注意:新版 25.x 由于签名限制,官方界面直接上传安装会报 `Error 99`,优先使用上方的命令行安装)。 + +**手动上传:** +```bash +apk add --allow-untrusted /tmp/*netwiz*.apk +rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart +``` +**官方界面上传(不要点安装):** +```bash +apk add --allow-untrusted /tmp/upload.apk +rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart +``` +**对于 OpenWrt 23.05 及更早系统 (`.ipk` 格式):** +以下两种方式任选其一: +1. 网页端:在系统后台使用 “系统 -> 软件包 -> 上传软件包” 正常安装。 +2. 命令行:上传至 `/tmp/` 目录后,运行以下命令: +```bash +opkg install /tmp/*netwiz*.ipk +rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart +``` + +## 💡 常见问题 (FAQ) + +**Q:安装后点击菜单提示 `HTTP 404` 或 `Object not found (-32000)`?** +A:这是 OpenWrt 系统的权限通行证 (ACL) 和前端缓存机制未更新所致。请在 SSH 中执行彻底清理命令: +```bash +rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart +``` +**做完后,请务必点击网页右上角“退出登录”,重新输入密码进入后台即可解决。** + +**Q:在局域网设置里修改了 IP 后,为什么一直在转圈,打不开网页了?** +A:如果您修改了本机的局域网 IP,点击“确认应用”后底层网络会立刻重置。系统会在几秒后尝试为您自动跳转,如果由于浏览器安全策略未跳转成功,原有的旧 IP 网页将失效。请直接在浏览器地址栏手动输入您新设置的 IP 地址即可恢复访问。 + +**Q:切换上网方式后无法打开网页,打不开网页了?** +A:通常是因为操作系统的 DNS 缓存没清,请尝试断开电脑/手机的 Wi-Fi 重新连接,或在电脑执行:ipconfig /flushdns。 + +**Q:🆘 救援指南:进不去后台怎么办** + +检查网络连接与手动指定 IP(如果电脑无法自动获取 IP 地址),导致浏览器打不开后台: +* 请确保网线插在路由器的 **LAN 口** 上。 +* 将电脑网卡的 IP 获取方式改为“手动/静态 IP”。 +* IP 地址设置为:`192.168.100.2`(与路由器同网段的其他地址)。 +* 子网掩码设置为:`255.255.255.0`。 +* 设置完成后,再次尝试访问路由器后台 `192.168.100.1`。 + +## Q:🆘 救援指南:构建固件时内置Netwiz,刷后进不去后台怎么办? + +如果在刷入固件后遇到无法连接、向导界面异常或无法进入管理后台的情况,请不要紧张,按照以下步骤进行排查与救援: + +### 1. 触发隐藏的紧急救援通道 (Netwiz 防卡死机) +如果您已经连接上路由器的网络,但遇到了前端白屏、向导无限循环或界面卡死的问题,可以利用系统内置的防劫持救援通道: +* 请在浏览器中直接输入并访问:`http://192.168.100.1/cgi-bin/luci/rescue`(请将 IP 替换为您的实际路由器默认 IP)。 +* 此操作会触发底层的紧急释放指令,强制解除向导的网关劫持,并将您安全地送回官方原生的 LuCI 登录页面。 + +### 2. SSH 后台命令修复 +如果您依然可以通过 SSH 连接路由器(默认端口 `22`),您可以通过命令行手动解除向导锁定并重启网络: +```bash +# 解除向导锁定并恢复原生 LuCI 入口 +uci set netwiz.global.configured='1' +uci commit netwiz +[ -f /www/cgi-bin/luci.bak ] && mv -f /www/cgi-bin/luci.bak /www/cgi-bin/luci + +# 重启网页服务器与网络服务 +/etc/init.d/uhttpd restart +/etc/init.d/network restart +``` +--- + +## 📸 界面预览 +Image +Image +Image +Image +Image +Image +Image +Image +Image +Image +Image +Image + +--- + +## 📄 协议与鸣谢 + +本项目基于 GPL-3.0 license 开源。 + +特别感谢 **OpenWrt 开源社区**、**LuCI 框架** 以及 **ImmortalWrt 团队** 提供坚实的基础架构支撑。 + +--- + +## ☕ 赞助与支持 + +如果您觉得这个插件让您的网络配置体验变得更加优雅、省心,欢迎请作者喝杯咖啡,这将是持续维护与更新的最大动力!❤️ + +Donate + +--- + +[返回顶部](#top) diff --git a/luci-app-netwiz/install.sh b/luci-app-netwiz/install.sh new file mode 100644 index 000000000..81b950dec --- /dev/null +++ b/luci-app-netwiz/install.sh @@ -0,0 +1,230 @@ +#!/bin/sh + +echo "🚀 开始极速安装/升级/修复 NetWiz" + +# ========================================== +# 0. 环境初始化与版本识别 +# ========================================== +PKG_TYPE="ipk" +if command -v apk >/dev/null 2>&1; then + PKG_TYPE="apk" +elif command -v opkg >/dev/null 2>&1; then + PKG_TYPE="ipk" +else + echo "❌ 找不到支持的软件包管理器 (未找到 apk 或 opkg)!" + exit 1 +fi + +# 函数:获取当前系统中已安装的版本 +get_installed_version() { + if [ "$PKG_TYPE" = "apk" ]; then + apk info -v 2>/dev/null | grep -E "^luci-app-netwiz-[0-9]" | head -n 1 | sed 's/^luci-app-netwiz-//' + else + opkg status luci-app-netwiz 2>/dev/null | grep -i "^Version:" | awk '{print $2}' + fi +} + +# 函数:透视获取本地保险箱中安装包的版本 +get_local_version() { + local file="$1" + local ver="" + + if [ "$PKG_TYPE" = "apk" ]; then + # 1. 尝试直接跨流提取 .PKGINFO + ver=$(tar -Ozxf "$file" .PKGINFO 2>/dev/null | grep "^pkgver =" | cut -d'=' -f2 | tr -d ' ') + # 2. 核心修复:如果由于多段 gzip 拼接导致 tar 失败,利用 zcat 暴力读取压缩流明文 + [ -z "$ver" ] && ver=$(zcat "$file" 2>/dev/null | grep -a -m 1 "^pkgver =" | cut -d'=' -f2 | tr -d ' ') + # 3. 终极穿透:利用 strings 强制提取二进制文件中的可见版本字符 + [ -z "$ver" ] && ver=$(strings "$file" 2>/dev/null | grep -m 1 "^pkgver =" | cut -d'=' -f2 | tr -d ' ') + else + # ipk 架构的提取非常稳定 + ver=$(tar -Ozxf "$file" ./control.tar.gz 2>/dev/null | tar -Ozxf - ./control 2>/dev/null | grep -i "^Version:" | awk '{print $2}') + fi + + # 4. 从文件名兜底提取 + [ -z "$ver" ] && ver=$(basename "$file" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?(-r[0-9]+)?' | head -n 1) + + echo "$ver" +} + +# 函数:版本对比算法 +version_ge() { + awk -v v1="$1" -v v2="$2" ' + function split_ver(v, a) { + gsub(/^[vV]/, "", v); gsub(/-.*/, "", v); + return split(v, a, "."); + } + BEGIN { + if (v1 == "" && v2 == "") exit 0; + if (v1 == "") exit 1; + if (v2 == "") exit 0; + n1 = split_ver(v1, a1); n2 = split_ver(v2, a2); + max = (n1 > n2) ? n1 : n2; + for (i=1; i<=max; i++) { + if (a1[i]+0 > a2[i]+0) exit 0; + if (a1[i]+0 < a2[i]+0) exit 1; + } + exit 0; + }' +} + +INSTALLED_VER=$(get_installed_version) +echo "🔍 当前系统架构: $PKG_TYPE" +echo "📦 当前运行版本: ${INSTALLED_VER:-未安装 (或已被精简)}" + +# ========================================== +# 1. 备份脚本自身,确保离线可用 +# ========================================== +mkdir -p /etc/netwiz/custom_pkgs/ +cp -f "$0" /etc/netwiz/custom_pkgs/install.sh 2>/dev/null + +# ========================================== +# 2. 尝试从云端拉取最新版 (高透明度探测) +# ========================================== +echo "⬇️ 正在从云端尝试获取最新版本..." +FILES="luci-app-netwiz luci-i18n-netwiz-zh-cn luci-i18n-netwiz-zh-tw" +DOWNLOAD_SUCCESS=0 + +for FILE in $FILES; do + TARGET_FILE="${PKG_TYPE}_${FILE}.${PKG_TYPE}" + URL_DIRECT="https://github.com/huchd0/luci-app-netwiz/releases/latest/download/${TARGET_FILE}" + + PROXY_1="https://mirror.ghproxy.com/${URL_DIRECT}" + PROXY_2="https://ghfast.top/${URL_DIRECT}" + PROXY_3="https://ghp.ci/${URL_DIRECT}" + + echo -n "正在拉取: ${TARGET_FILE} ... " + + # 尝试 1:官方直连 (仅等待 5 秒) + wget -qO "/tmp/${TARGET_FILE}" --no-check-certificate -T 5 "$URL_DIRECT" + if [ "$?" -eq 0 ] && [ -s "/tmp/${TARGET_FILE}" ]; then + DOWNLOAD_SUCCESS=$((DOWNLOAD_SUCCESS + 1)) + echo "👉 👉 👉 [官方直连] 成功!✅✅✅" + continue + fi + + # 尝试 2:代理节点 1 + wget -qO "/tmp/${TARGET_FILE}" --no-check-certificate -T 5 "$PROXY_1" + if [ "$?" -eq 0 ] && [ -s "/tmp/${TARGET_FILE}" ]; then + DOWNLOAD_SUCCESS=$((DOWNLOAD_SUCCESS + 1)) + echo "👉 👉 👉 [加速节点1] 成功!✅✅✅" + continue + fi + + # 尝试 3:代理节点 2 + wget -qO "/tmp/${TARGET_FILE}" --no-check-certificate -T 5 "$PROXY_2" + if [ "$?" -eq 0 ] && [ -s "/tmp/${TARGET_FILE}" ]; then + DOWNLOAD_SUCCESS=$((DOWNLOAD_SUCCESS + 1)) + echo "👉 👉 👉 [加速节点2] 成功!✅✅✅" + continue + fi + + # 尝试 4:代理节点 3 + wget -qO "/tmp/${TARGET_FILE}" --no-check-certificate -T 5 "$PROXY_3" + if [ "$?" -eq 0 ] && [ -s "/tmp/${TARGET_FILE}" ]; then + DOWNLOAD_SUCCESS=$((DOWNLOAD_SUCCESS + 1)) + echo "👉 👉 👉 [加速节点3] 成功!✅✅✅" + continue + fi + + echo "❌ 所有节点均失败!" +done + +# ========================================== +# 3. 云端全新安装 vs 离线智能对比 +# ========================================== +if [ "$DOWNLOAD_SUCCESS" -eq 3 ]; then + echo "🌐 云端下载完整!正在覆盖至本地保险箱..." + for FILE in $FILES; do + TARGET_FILE="${PKG_TYPE}_${FILE}.${PKG_TYPE}" + mv -f "/tmp/${TARGET_FILE}" "/etc/netwiz/custom_pkgs/${TARGET_FILE}" + done + echo "👉 准备执行全新安装/升级!" +else + echo "⚠️ 云端连接失败或文件不完整,转入【离线恢复模式】!" + echo "🔍 正在检查本地保险箱状态..." + + LOCAL_MAIN=$(ls /etc/netwiz/custom_pkgs/*luci-app-netwiz*.${PKG_TYPE} 2>/dev/null | head -n 1) + if [ -n "$LOCAL_MAIN" ] && [ -f "$LOCAL_MAIN" ]; then + LOCAL_VER=$(get_local_version "$LOCAL_MAIN") + echo "📦 发现本地储备版本: ${LOCAL_VER:-未知}" + + # 双向对比机制 + if [ -n "$INSTALLED_VER" ] && [ -n "$LOCAL_VER" ]; then + if version_ge "$LOCAL_VER" "$INSTALLED_VER"; then + echo "✅ 本地储备版 ($LOCAL_VER) >= 当前运行版 ($INSTALLED_VER),允许覆盖恢复!" + else + echo "🚫 智能拦截:本地储备版 ($LOCAL_VER) 低于当前运行版 ($INSTALLED_VER)!" + echo "💡 为保护系统,无需进行降级安装,已自动安全退出。" + exit 0 + fi + elif [ -n "$INSTALLED_VER" ] && [ -z "$LOCAL_VER" ]; then + echo "⚠️ 无法准确识别本地包版本,默认放行执行覆盖安装以尝试修复系统!" + else + echo "✅ 系统核心未挂载,允许执行离线急救部署!" + fi + else + echo "❌ 致命错误:云端下载失败,且本地保险箱无可用急救包!" + exit 1 + fi +fi + +# ========================================== +# 4. 卸载旧版并注入新版 +# ========================================== +echo "🧹 正在清理系统旧版本残留..." +if [ "$PKG_TYPE" = "apk" ]; then + apk del luci-app-netwiz luci-i18n-netwiz-zh-cn luci-i18n-netwiz-zh-tw >/dev/null 2>&1 +else + opkg remove luci-app-netwiz luci-i18n-netwiz-zh-cn luci-i18n-netwiz-zh-tw --force-remove >/dev/null 2>&1 +fi + +echo "⚙️ 正在执行批量注入安装..." +if [ "$PKG_TYPE" = "apk" ]; then + apk add --allow-untrusted --force-overwrite /etc/netwiz/custom_pkgs/*luci-*netwiz*.apk 2>/dev/null +else + opkg install /etc/netwiz/custom_pkgs/*luci-*netwiz*.ipk --force-reinstall --force-overwrite 2>/dev/null +fi + +# ========================================== +# 5. 重建缓存与唤醒服务 +# ========================================== +echo "🔄 正在重建 LuCI 缓存并唤醒服务..." +# 1. 清空所有 LuCI 缓存 +rm -rf /tmp/luci-indexcache* /tmp/luci-modulecache* /var/run/luci-indexcache* /var/run/luci-modulecache* 2>/dev/null +rm -rf /tmp/luci-sessions/* /var/run/luci-sessions/* 2>/dev/null + +# 2. 重启 rpcd,确保后端的 netwiz 脚本被正确加载入内核 +/etc/init.d/rpcd restart 2>/dev/null + +# 3. 智能重启 Web 服务 +if [ -f "/etc/init.d/uhttpd" ]; then + /etc/init.d/uhttpd restart 2>/dev/null +elif [ -f "/etc/init.d/nginx" ]; then + /etc/init.d/nginx restart 2>/dev/null +fi + +echo -e "\n👉 👉 👉 🎉 NetWiz 核心程序部署完成!✅✅✅" +echo -e "💡 登录状态已安全重置,请返回浏览器按下 【F5】 刷新即可看到新菜单!" + +# 6. 匿名安装量统计 +( + if [ -f /etc/openwrt_release ]; then + . /etc/openwrt_release + OW_VER=${DISTRIB_RELEASE:-"unknown"} + OW_ARCH=${DISTRIB_TARGET:-"unknown"} + else + OW_VER="unknown" + OW_ARCH=$(uname -m 2>/dev/null || echo "unknown") + fi + API_URL="https://netwiz-tracker.vercel.app/api/track" + REQ_URL="${API_URL}?app=netwiz&ver=${OW_VER}&arch=${OW_ARCH}" + + if command -v curl >/dev/null 2>&1; then + curl -s -m 3 "$REQ_URL" >/dev/null 2>&1 + else + wget -qO- -T 3 "$REQ_URL" >/dev/null 2>&1 + fi +) & + +exit 0 diff --git a/luci-app-netwiz/luci-app-netwiz/LICENSE b/luci-app-netwiz/luci-app-netwiz/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/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/luci-app-netwiz/luci-app-netwiz/Makefile b/luci-app-netwiz/luci-app-netwiz/Makefile new file mode 100644 index 000000000..02c55ff28 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/Makefile @@ -0,0 +1,17 @@ +include $(TOPDIR)/rules.mk + +# 明确声明包名与版本 +PKG_NAME:=luci-app-netwiz +PKG_VERSION:=1.7.5 +PKG_RELEASE:=1 + +# LuCI 核心定义 +LUCI_TITLE:=LuCI Support for Network Setup Wizard +LUCI_DEPENDS:=+luci-base +iwinfo +rpcd +rpcd-mod-iwinfo +luci-compat +ppp-mod-pppoe +# 声明为纯脚本架构,生成的 IPK/APK 可以在任何 CPU 路由上通用安装 +LUCI_PKGARCH:=all + +# 使用绝对路径引入 luci.mk,保证任何目录下都能成功触发多国语言编译 +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-netwiz/luci-app-netwiz/README.md b/luci-app-netwiz/luci-app-netwiz/README.md new file mode 100644 index 000000000..f178c4419 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/README.md @@ -0,0 +1,139 @@ +# NetWiz (luci-app-netwiz) 🚀 + +![OpenWrt](https://img.shields.io/badge/OpenWrt-23.05_|_25.x+-blue.svg) ![ImmortalWrt](https://img.shields.io/badge/ImmortalWrt-Supported-orange.svg) ![License](https://img.shields.io/badge/License-GPL--3.0-green.svg) ![Build](https://img.shields.io/badge/Build-Passing-brightgreen.svg) + +--- +### Description +**NetWiz(luci-app-netwiz)** is a minimalist, safe, and non-destructive network configuration interface for OpenWrt/ImmortalWrt. + +minimalist and ready out of the box. Built with rigorous, well-structured logic, it features an enterprise-grade LAN fail-safe mechanism, intelligent WAN protocol switching, smart validation, and automatic conflict detection for IPs, gateways, WAN, and LAN interfaces. The front-end delivers a smooth, fluid user experience, eliminating the traditional frustrations of “errors after saving” and frequent configuration conflicts. + +It is designed to be highly user-friendly for novices setting up secondary routers (DHCP/Static IP) or bypass routers. + +### 🌟 Key Features + +**🛡️ Enterprise-Grade Fail-Safes** +* **Persistent Power-Loss Recovery:** Backups are written to non-volatile flash (`/etc/config`). An early-boot `init.d` script guarantees an automatic network restoration even if the router loses power mid-test. +* **Smart Concurrency Radar (LAN Auto-Defuse):** The 120-second rollback bomb uses `netstat` to count concurrent connections, accurately distinguishing true browser access from background IoT probes to prevent false-positive defusals. +* **Smart WAN Auto-Detect (Anti-Jitter):** Features a time-based debounce algorithm that requires 3 continuous down-cycles to confirm a physical cable unplug, safely ignoring temporary software interface bounces. +* **Flash-Friendly Safe Logging:** Includes a persistent, auto-trimming logging system (`/etc/netwiz.log`) that survives reboots but strictly caps file size to protect the router's flash memory lifespan. + +### 📶 Powerful Smart Wi-Fi Engine +* **Infinite Chip Array Detection:** Breaks the rigid limits of traditional single/dual-chip plugins. Automatically scans and takes over all physical wireless adapters in the system. Perfectly adapts to single-band, dual-band, tri-band, or even multi-band routers, ensuring no hardware is left idle. +* **Ultimate Low-Level Override:** Designed for incomplete, conflicting, or misconfigured underlying settings (e.g., a single chip stuck in 5G mode). Features an exclusive "Will Override Rule" that ignores residual hardware history, forcibly cleansing and rewriting the correct physical protocols and channels to guarantee a 100% successful application. +* **Smart Bandwidth Unleashed:** Say goodbye to crippled networks! When applying configurations, it intelligently identifies hardware limits and silently maximizes channel width (e.g., boosting 2.4G to HT40 and 5G to VHT80). It safely preserves existing Wi-Fi 6 (HE) configurations, ensuring effortless gigabit speeds. +* **Seamless Two-Way Data Sync:** When switching between "Smart Connect" and "Separate Bands", Wi-Fi passwords and SSIDs are intelligently synced back and forth, eliminating repetitive typing. Features historical config memory—retrieve your old password instantly even after disabling and re-enabling Wi-Fi. + +**⚙️ Core Architecture & Security** +* **Zero-Zombie `procd` Daemon:** Replaces unreliable `hotplug` tricks with a single, ultra-lightweight native OpenWrt daemon (`netwiz-monitor`) that reads `ubus` states with near 0% CPU footprint. +* **Strict ACL:** The frontend has zero direct write access to UCI. All modifications are safely encapsulated and validated within the backend `rpcd` script. +* **Safe Configuration:** Strictly prevents routing loops and network conflicts (e.g., actively stops users from putting WAN and LAN in the same subnet). + +**🚀 Smooth UX & Frontend** +* **Pure CSR Architecture:** Built with modern Client-Side Rendering for a blazing-fast, app-like experience. +* **Asynchronous UI & Hot-Reload:** Eliminates traditional blind wait times. The UI asynchronously "knocks" on the new IP with a real-time stopwatch. The backend applies changes via `reload` instead of a full `restart` to prevent Wi-Fi dropouts. +* **Bypass Router Mode:** One-click deployment that auto-configures complex DHCP, DNS, and gateway settings flawlessly. +* **Multi-language:** Native built-in i18n support, automatically adapting to English, zh-Hans, and zh-Hant. + +### Breadcrumb Trail +* 👉 **primary menu entry appearing right before “Logout -> Network Wizard**。 + (Placing the Network Setup Wizard immediately before “Logout” is a deliberate UI decision that benefits both beginners and users seeking quick configuration. It offers a highly visible, easily accessible entry point, eliminating the need to navigate complex OpenWrt menus. Moreover, this layout aligns with the polished, user-centric design conventions adopted by established router brands such as ASUS, TP-Link, and Xiaomi.) + +🌐 **Secondary Router Mode (DHCP / Static IP)** + * Use Case: When the upstream modem (ONT) already handles PPPoE dialing, or an existing primary router is present. This device operates as a secondary router or segmented subnet router. + * Behavior: Supports both dynamic IP assignment (DHCP) and static IP configuration. Automatically provisions WAN interface settings and performs intelligent subnet validation to prevent routing loops or address conflicts. + +🔌 **Broadband Dial-Up (PPPoE)** + * Use Case: When the modem is configured in full bridge mode and this router is responsible for establishing the PPPoE connection, acting as the primary network gateway. + * Behavior: Accurately applies PPPoE credentials (username and password), removes residual gateway configurations, and safely restarts the underlying dialer process to ensure a clean connection. + +🏠 **LAN Configuration (Primary / Bypass Router Switching)** + * Use Case: For modifying the device’s LAN management IP, or when a primary router already exists and this device functions as an auxiliary gateway (bypass router). + * Behavior: One-click activation of “Bypass Router Mode” automatically disables the local DHCP service and enforces manual configuration of the upstream gateway. In primary router mode, safeguards ensure the gateway field remains empty, preventing disruption to the existing LAN topology. + + A bypass router lets you add advanced features without touching the main network. The primary router keeps handling DHCP and NAT, while the bypass router processes selected traffic for tasks like policy routing or proxies. This keeps the network stable while giving you more control. + +**A simple, common example:** + + * You have a main router providing Wi-Fi and DHCP for your home. You add a bypass router and set only your laptop’s gateway to it. Now, your laptop’s traffic goes through the bypass router (e.g., for proxy or special routing), while all other devices continue using the main router normally. This way, you get advanced control on one device without affecting the rest of the network. + +📶 **Wi-Fi Settings(Auto-hide when Wi-Fi is unavailable.)** +* **Use Case**: Initial wireless network setup, or modifying the SSID, password, and encryption protocols for your whole-home network. +* **Behavior**: Intelligently detects the number of underlying physical chips to dynamically render the UI. Supports one-click "Smart Connect (All Bands)" to build a seamless roaming network, or splits into independent 2.4G/5G networks. Perfectly backwards compatible with legacy 802.11b IoT devices. + +### 💡 Execution Permissions Requirement (For GitHub Actions / Documentation)** + +**Required Executable Permissions (chmod +x):** + +To ensure the backend daemons and RPC interfaces function correctly, the following files must have executable permissions assigned before building the package: + +```bash +chmod +x luci-app-netwiz/root/usr/libexec/rpcd/netwiz +chmod +x luci-app-netwiz/root/usr/libexec/rpcd/netwiz_dev +chmod +x luci-app-netwiz/root/usr/libexec/netwiz-autodetect.sh +chmod +x luci-app-netwiz/root/usr/libexec/netwiz-monitor-loop.sh +chmod +x luci-app-netwiz/root/etc/init.d/netwiz-monitor +chmod +x luci-app-netwiz/root/etc/init.d/netwiz-recovery +chmod +x luci-app-netwiz/root/etc/init.d/netwiz-watchdog +chmod +x luci-app-netwiz/root/etc/hotplug.d/dhcp/99-netwiz-guard +``` +--- + +```bash +luci-app-netwiz/ +├── README.md # Project Documentation (Features, Installation Guide, Changelog) +├── LICENSE # Open Source License +├── Makefile # OpenWrt Standard Makefile (Package Definition, Dependencies) +│ +├── htdocs/ # Frontend UI Layer (Pure Static Resources) +│ └── luci-static/resources/view/ +│ ├── netwiz.css # Global Common Stylesheet (Modals, Responsive, UI Beautification) +│ ├── netwiz.js # Frontend Core 1: Wizard Engine (incl. Smart Backup, Anti-brick Validation, QR Code Dispatcher) +│ ├── netwiz_dev.js # Frontend Core 2: Manager Engine (Device Radar, Smart Segmentation, UI Interaction) +│ └── qrcode.min.js # Frontend Dependency: Pure Local Micro QR Code Generation Engine (for scan-to-connect Wi-Fi) +│ +├── po/ # Multilingual Internationalization (i18n) +│ ├── zh_Hans/netwiz.po # Simplified Chinese Translation Dictionary +│ └── zh_Hant/netwiz.po # Traditional Chinese Translation Dictionary +│ +└── root/ # Backend System Layer (Mapped to router root directory '/' upon packaging) + ├── etc/ + │ ├── config/ + │ │ └── netwiz # Low-level Configuration Library (Stores wizard status, manager grouping, and device activity timestamps) + │ │ + │ ├── init.d/ # [Core Daemon Services / System Init Scripts] (Manages all background tasks) + │ │ ├── netwiz-monitor # [Wizard Engine] Resident monitor daemon (Handles status during setup & frontend monitoring) + │ │ ├── netwiz-recovery # [Rescue Engine] Auto-recovery on power loss (Anti-disconnect, extremely early boot START=15) + │ │ └── netwiz-watchdog # [Keepalive Engine] IPv6 heartbeat & self-healing daemon (Late boot START=99) + │ │ + │ └── hotplug.d/dhcp/ # [Manager Engine Stealth Guard Module] + │ └── 99-netwiz-guard # MAC Anti-spoofing Trigger (Event-driven microsecond interception, zero background overhead) + │ + └── usr/ + ├── libexec/ + │ ├── netwiz-autodetect.sh # WAN Protocol Auto-detection Engine + │ ├── netwiz-monitor-loop.sh # Wizard Debounce and Fallback Loop Logic + │ │ + │ └── rpcd/ # RPC Interface Layer (Frontend-Backend Data Communication Bridge) + │ ├── netwiz # Backend Interface 1: Dedicated to Wizard/Backup (Handles network protocol modification, kernel-level fingerprint post-mortem) + │ └── netwiz_dev # Backend Interface 2: Dedicated to Manager (ARP/DHCP access, includes crash self-rescue and silent cleanup mechanisms) + │ + └── share/ + ├── luci/menu.d/ + │ └── luci-app-netwiz.json # System Menu Entry Registration + └── rpcd/acl.d/ + └── luci-app-netwiz.json # UBUS Access Control List (Authorizes frontend access to backend interfaces) +``` +--- + +Image +Image +Image +Image +Image +Image +Image +Image +Image + +--- diff --git a/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.css b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.css new file mode 100644 index 000000000..00d55642f --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.css @@ -0,0 +1,715 @@ +/* --------------------------------------------------------- + 1. 全局主容器 +--------------------------------------------------------- */ +body header { + margin: 0 !important; + box-sizing: border-box !important; +} + +body { + overflow-x: clip !important; +} + +/* 所有输入框的占位符样式 */ +/* ------- 开始 ------*/ +body input::placeholder { + color: #94a3b8 !important; + font-weight: 500 !important; + opacity: 1 !important; +} + +body input::-webkit-input-placeholder { + color: #94a3b8 !important; + font-weight: 500 !important; + opacity: 0.99 !important; /* 需要不一样,骗压缩工具 */ +} + +body input::-moz-placeholder { + color: #94a3b8 !important; + font-weight: 500 !important; + opacity: 0.98 !important; /* 需要不一样,骗压缩工具具 */ +} + +body input:-ms-input-placeholder { + color: #94a3b8 !important; + font-weight: 500 !important; + opacity: 0.97 !important; /* 需要不一样,骗压缩工具 */ +} +/* ------- 结束 ------*/ + +#netwiz-container, #netwiz-dev-container { + display: block !important; width: 100% !important; + /* 屏幕高度 + 2px,使滚动条出现 */ + min-height: calc(100vh + 2px) !important; + padding: 0 !important; margin: 0 auto !important; box-sizing: border-box !important; + color: #64748b; font-size: 13.5px; + /* clip 剪掉溢出像素 */ + overflow-x: clip !important; + /* 底部防遮挡缓冲:防止被批量操作栏盖住最后一张卡片 */ + padding-bottom: 30px !important; +} + +.nw-wrapper { + display: flex !important; flex-direction: column !important; + align-items: center !important; + justify-content: flex-start !important; + width: 100% !important; margin: 0 auto !important; + padding: 0px 15px 15px 15px !important; + font-family: -apple-system, BlinkMacSystemFont, sans-serif !important; + box-sizing: border-box !important; + min-height: auto !important; + background: transparent !important; + /* 顶部导航列重新恢复悬浮 */ + max-width: 100% !important; +} + +.nw-header, .nw-step, .nd-control-bar, .nd-list, .nd-list-header, .nd-category-tabs { + width: 100% !important; + max-width: 1080px !important; /* 页面宽度 */ + margin-left: auto !important; + margin-right: auto !important; + box-sizing: border-box !important; +} + +/* --------------------------------------------------------- + 2. 顶部蓝色大横幅 (Header) +--------------------------------------------------------- */ +.nw-header { top: -20px; display: block !important; text-align: center !important; background-color: #5e72e4 !important; padding: 22px 15px 10px 15px !important; border-radius: 0 0 14px 14px !important; position: relative !important; box-shadow: 0 8px 20px rgba(94, 114, 228, 0.15) !important; z-index: 1 !important; margin: 0 auto 20px auto !important; border: none !important; width: 100% !important; max-width: 650px !important; } +.nw-main-title { font-size: 36px !important; letter-spacing: 4px !important; font-weight: 600 !important; margin: 0 0 8px 0 !important; color: #ffffff !important; line-height: 1.2 !important; } +.nw-header p { color: #ffffff !important; font-size: 20px !important; letter-spacing: 2px !important; opacity: 0.9 !important; margin: 8px 0 !important; } + +/* 标题包裹器 */ +.nw-title-wrap { position: relative; display: inline-block; cursor: default; } + +.nw-reopen-btn { margin: 10px 0 5px 0; display: inline-flex; align-items: center; justify-content: center; gap: 5px; cursor: pointer; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); padding: 6px 18px; border-radius: 20px; font-size: 13.5px; font-weight: 500; color: #fff; transition: all 0.25s ease; } +.nw-reopen-btn:hover { background: rgba(255,255,255,0.25); transform: translateY(-1px); } + +/* 版本号标签 (悬浮显示,不可点击) */ +.nw-version-tag { position: absolute; top: 10px; left: 100%; margin-left: 2px; background: rgba(255, 255, 255, 0.15); color: #fff; font-size: 12px; font-weight: bold; padding: 2px 8px; border-radius: 6px; font-family: monospace; z-index: 50; border: 1px solid rgba(255, 255, 255, 0.3); white-space: nowrap; box-shadow: 0 4px 10px rgba(0,0,0,0.08); opacity: 0; pointer-events: none; transition: all 0.3s ease; transform: translateY(0px);letter-spacing: 0; } +.nw-title-wrap:hover .nw-version-tag { opacity: 1; top: 5px; transform: translateY(-5px); } + +/* 当有更新时,JS 会动态加入 .has-update 类 (常亮显示、变绿、允许点击) */ +.nw-version-tag.has-update { pointer-events: auto; cursor: pointer; opacity: 1; top: 5px; transform: translateY(-5px); background: #67A57B; border-color: #67A57B; } +.nw-version-tag.has-update:hover { background: #5a936d; } + +/* 小红点默认隐藏,增加呼吸动画定位在右上角 */ +.nw-version-dot { display: none; position: absolute; top: -3px; right: -3px; width: 8px; height: 8px; background-color: #ef4444; border-radius: 50%; box-shadow: 0 0 0 2px rgba(15, 23, 42, 0.9); animation: pulse-dot 2s infinite; } +@keyframes pulse-dot { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.8); } 70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } + +/* --------------------------------------------------------- + 3. 主向导 4 个彩色大卡片 +--------------------------------------------------------- */ +.nw-step { text-align: center !important; animation: slideUp 0.4s ease-out; } +.nw-form-area, .nw-confirm-board { position: relative; max-width: 460px; margin: -15px auto 25px auto; text-align: left; padding: 30px 35px; border-radius: 16px; background-color: rgba(255, 255, 255, 0.88); box-shadow: 0 10px 30px rgba(0,0,0,0.06); box-sizing: border-box; display: block !important; overflow: hidden; word-break: break-word; } +.nw-step-title { text-align: center; margin-bottom: 25px; color: #111; font-weight: 600; font-size: 18px; } + +.nw-card-group { display: flex !important; gap: 18px !important; justify-content: center !important; flex-wrap: wrap !important; margin-top: -20px !important; width: 100% !important; box-sizing: border-box !important; position: relative !important; z-index: 10 !important; } +.nw-card { flex: 1 !important; min-width: 170px !important; max-width: 220px !important; aspect-ratio: 1 / 1 !important; padding: 28px 18px !important; border-radius: 14px !important; cursor: pointer !important; backdrop-filter: blur(12px) !important; border: 1px solid rgba(255,255,255,0.2) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important; transition: transform 0.25s ease !important; display: flex !important; flex-direction: column !important; align-items: center !important; box-sizing: border-box !important; } +.nw-card:hover { transform: translateY(-5px) !important; } + +.nw-card[data-mode="pppoe"] { background-color: rgba(79, 150, 101, 0.95) !important; } +.nw-card[data-mode="wifi"] { background-color: rgba(245, 54, 92, 0.95) !important; } +.nw-card[data-mode="router"] { background-color: rgba(80, 0, 183, 0.95) !important; } +.nw-card[data-mode="lan"] { background-color: rgba(245, 158, 11, 0.95) !important; } + +.nw-card-title { font-size: 17px !important; margin: 0 0 8px 0 !important; color: #ffffff !important; font-weight: 600 !important; line-height: 1.2 !important; text-align: center !important; } +.nw-card span { font-size: 14.5px !important; color: #ffffff !important; line-height: 1.4 !important; opacity: 0.9 !important; display: block !important; text-align: center !important; } + +.nw-badge { width: 50px !important; height: 50px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; margin-bottom: 15px !important; } +.nw-badge-pppoe { background-color: #fef3c7 !important; color: #059669 !important; } +.nw-badge-wifi { background-color: #f3e8ff !important; color: #0284c7 !important; } +.nw-badge-dhcp { background-color: #e0f2fe !important; color: #9333ea !important; } +.nw-badge-bypass { background-color: #d1fae5 !important; color: #d97706 !important; } +.nw-badge svg { width: 24px !important; height: 24px !important; display: block !important; } + +.nw-current-mode-display { margin-top: 20px !important; background: #5e72e4 !important; padding: 18px 30px !important; border-radius: 12px !important; display: flex !important; flex-direction: column !important; align-items: center !important; box-shadow: 0 8px 20px rgba(94, 114, 228, 0.3) !important; text-align: center !important; box-sizing: border-box !important; word-break: break-all !important; overflow-wrap: anywhere !important; overflow: hidden !important; width: 100% !important; max-width: 820px !important; margin-left: auto !important; margin-right: auto !important; } + +/* --------------------------------------------------------- + 4. 设备管家组件 +--------------------------------------------------------- */ +.nd-control-bar { + position: sticky !important; top: 0px !important; z-index: 750 !important; + display: flex !important; align-items: center !important; justify-content: space-between !important; + background: rgba(94, 114, 228) !important; backdrop-filter: blur(10px) !important; + padding: 12px 20px !important; border-radius: 14px !important; color: #fff !important; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important; margin: -15px auto 5px auto !important; +} +.nd-cb-back { width: 36px; height: 36px; background: rgba(255,255,255,0.2); border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; flex-shrink: 0; } +.nd-cb-back:hover { background: rgba(255,255,255,0.3); transform: translateX(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } +.nd-cb-title-wrap { flex: 1; margin: 0 15px; text-align: left; } +.nd-cb-title { font-size: 17px; font-weight: bold; line-height: 1.2; margin-bottom: 4px; color: #fff; } +.nd-cb-sub { font-size: 12.5px; opacity: 0.85; margin: 0; } +.nd-cb-refresh { padding: 6px 14px; background: transparent; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; font-size: 13.5px; font-weight: bold; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: 0.2s; flex-shrink: 0; } +.nd-cb-refresh:hover { background: rgba(255,255,255,0.15); } + +.nd-list { + display: flex !important; flex-direction: column !important; gap: 12px !important; + padding: 0 !important; margin-bottom: 10px !important; +} +.nd-card { + display: flex !important; align-items: center !important; justify-content: space-between !important; + background: #ffffff !important; padding: 18px 20px !important; border-radius: 14px !important; + box-shadow: 0 2px 8px rgba(0,0,0,0.03) !important; border: 1px dashed #cbd5e1 !important; + transition: all 0.2s ease !important; gap: 12px !important; + width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; margin: 0 !important; +} +.nd-card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.06) !important; transform: translateY(-2px) !important; border-color: #3b82f6 !important; } +.nd-card-left { flex: 2; min-width: 180px; display: flex; flex-direction: column; gap: 6px; } +.nd-card-name { font-size: 15px; font-weight: 600; color: #334155; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; line-height: 1.3; } +.nd-card-mac { font-size: 12.5px; font-family: monospace; color: #64748b; background: #f8fafc; padding: 3px 6px; border-radius: 6px; width: fit-content; border: 1px dashed #cbd5e1; letter-spacing: 0.5px; } +.nd-card-mid { flex: 1.2; min-width: 140px; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; } +.nd-card-ip { font-size: 15.5px; font-weight: bold; color: #3b82f6; font-family: monospace; letter-spacing: 0.5px; } +.nd-lease-info { font-size: 12px; color: #94a3b8; font-family: monospace; display: flex; align-items: center; gap: 4px; } +.nd-card-right { flex: 0 0 auto; display: flex; justify-content: flex-end; gap: 10px; } + +.nd-status-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; margin-left: 2px; } +.nd-status-online { background: #d1fae5; color: #059669; } +.nd-status-offline { background: #f1f5f9; color: #64748b; } +.nd-dot-online { width: 5px; height: 5px; background: #10b981; border-radius: 50%; box-shadow: 0 0 4px #10b981; } +.nd-dot-offline { width: 5px; height: 5px; background: #94a3b8; border-radius: 50%; } +.nd-badge { padding: 2px 6px; border-radius: 4px; font-size: 11px; border: 1px solid transparent; white-space: nowrap; margin-left: 4px; font-weight: bold; } +.nd-badge-static { background: #e0f2fe; color: #0284c7; border-color: #bae6fd; } +.nd-badge-gw { background: #fef08a; color: #854d0e; border-color: #fde047; } +.nd-badge-local { background: #e0e7ff; color: #3730a3; border-color: #c7d2fe; } +.nd-badge-visitor { background: #fae8ff !important; color: #86198f !important; border-color: #f0abfc !important; } +.nd-badge-pending { background: #fffbeb !important; color: #d97706 !important; border-color: #fde68a !important; } + +/* --------------------------------------------------------- + 5. 表单控件与按钮 +--------------------------------------------------------- */ +.nw-setting-row, .nw-setting-row-alt, .nw-switch-row-padded, .nw-adv-setting-row, .nw-roam-row, .nw-roam-row-alt, .nw-split-header-row, .nw-legacy-row, .nw-wisp-header { display: flex; align-items: center; justify-content: space-between; margin: 12px 0; } +.nw-setting-row { padding: 12px 0; border-bottom: 1px solid #f1f5f9; } +.nw-switch-row-padded { padding: 8px 0 12px 0; border-bottom: 1px dashed #e2e8f0; } + +.nw-setting-row-label, .nw-desc-title, .nw-wisp-title, .nw-roam-title { + font-size: 15px !important; font-weight: 700 !important; color: #1e293b !important; flex: 1; line-height: 1.3; margin: 0 10px 0 0 !important; +} + +.nw-adv-setting-row { padding: 4px 0 12px 0; border-bottom: 1px dashed #cbd5e1; animation: fadeIn 1s ease; } +.nw-roam-row { padding: 4px 0 12px 0; border-bottom: 1px dashed #cbd5e1; } +.nw-wisp-header { margin: 8px 0 4px 0; } +.nw-value { border: none !important; padding-bottom: 10px !important; display: flex !important; flex-direction: column !important; width: 100% !important; margin: 0 !important; background: transparent !important; } +.nw-value-title { text-align: left !important; font-weight: 600 !important; color: #334155 !important; font-size: 14.5px !important; margin: 8px 0 8px 4px !important; line-height: 1.2 !important; display: block !important; } +.nw-value-field { width: 100% !important; margin: 0 !important; padding: 0 !important; display: block !important; } + +.nw-form-area input[type="text"], .nw-form-area input[type="password"], .nw-form-area select, .nw-form-area textarea, .nw-wiz-modal-box input[type="text"], .nw-wiz-modal-box textarea, .nd-input { appearance: none !important; width: 100% !important; box-sizing: border-box !important; padding: 12px 14px !important; border: 1px solid #cbd5e1 !important; border-radius: 8px !important; font-size: 14px !important; outline: none !important; background: #f8fafc !important; color: #0f172a !important; min-height: 44px !important; line-height: 1.5 !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.02) !important; transition: all 0.2s ease !important; font-family: inherit; resize: none; word-break: break-all; } +.nw-form-area input:focus, .nw-form-area select:focus, .nw-form-area textarea:focus, .nw-wiz-modal-box input:focus, .nw-wiz-modal-box textarea:focus, .nd-input:focus { border-color: #3b82f6 !important; background: #ffffff !important; box-shadow: 0 0 0 3px rgba(59,130,246,0.15) !important; } + +.nw-switch { position: relative; display: inline-block; width: 42px; height: 22px; margin: 0; } +.nw-switch input { opacity: 0; width: 0; height: 0; } +.nw-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #cbd5e1; transition: background-color 0.3s ease; border-radius: 24px; } +.nw-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: transform 0.3s ease; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } +input:checked + .nw-slider { background-color: #10b981; } +input:checked + .nw-slider:before { transform: translateX(20px); } + +.nw-actions { margin-top: 20px; padding-bottom: 20px; display: flex; justify-content: center; gap: 15px; width: 100%; box-sizing: border-box; } +.nw-u-btn, .nd-btn { appearance: none !important; border: none; border-radius: 8px !important; padding: 10px 15px !important; font-weight: 600 !important; font-size: 14px !important; cursor: pointer !important; transition: all 0.25s ease !important; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; min-width: 110px; letter-spacing: 0.5px; } +.nw-u-btn-green, .nd-btn-green { background: #10b981 !important; color: white !important; } +.nw-u-btn-green:hover, .nd-btn-green:hover { background: #059669 !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.35) !important; } +.nw-u-btn-blue, .nd-btn-blue { background: #3b82f6 !important; color: white !important; } +.nw-u-btn-blue:hover, .nd-btn-blue:hover { background: #2563eb !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35) !important; } +.nw-u-btn-red, .nd-btn-red { background: #ef4444 !important; color: white !important; } +.nw-u-btn-red:hover, .nd-btn-red:hover { background: #dc2626 !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.35) !important; } +.nw-u-btn-gray, .nd-btn-gray { background: #f1f5f9 !important; color: #475569 !important; border: 1px solid #e2e8f0 !important; } +.nw-u-btn-gray:hover, .nd-btn-gray:hover { background: #e2e8f0 !important; color: #0f172a !important; transform: translateY(-2px) !important; } + +/* --------------------------------------------------------- + 6. 弹窗与浮层 (Modals) +--------------------------------------------------------- */ +#nw-global-modal, .nw-wisp-modal, #nd-modal-overlay { + display: none; position: fixed !important; top: 0 !important; left: 0 !important; + width: 100% !important; height: 100% !important; + background: rgba(0,0,0,0.65) !important; z-index: 2000000 !important; + align-items: center !important; justify-content: center !important; + backdrop-filter: blur(4px) !important; + /* 裁切动画瞬間的 20px 溢出,消除出现的滚动条 */ + overflow: hidden !important; +} +#nw-global-modal .nw-modal-box, .nw-wisp-modal-box, .nw-wiz-modal-box, .nd-modal-box { + background: #fff; + width: 98% !important; /* 不超过屏幕 98% */ + max-width: 460px !important; + border-radius: 14px; + overflow: hidden; + display: flex; + flex-direction: column; + animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); + box-sizing: border-box !important; /* 防止内边距把弹窗撑大 */ +} +#nw-global-modal .nw-modal-box, .nd-modal-box { padding: 35px 15px; text-align: center; max-width: 440px; box-sizing: border-box; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.4); } +.nw-wisp-modal-box { max-width: 400px; max-height: 80vh; } +.nw-wiz-modal-box { max-width: 460px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.4); } +.nw-spinner, .nd-spinner { width: 36px; height: 36px; border: 4px solid #f1f5f9; border-top: 4px solid #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; margin: 10px auto 15px; } + +.nd-modal-title { font-size: 18px; color: #0f172a; margin: 0 0 15px 0 !important; font-weight: 600; border: none !important; } +.nd-input-group { text-align: left; margin-bottom: 15px; } +.nd-input-label { display: block; font-size: 14px; color: #64748b; margin-bottom: 5px; font-weight: bold; } +.nd-empty { text-align: center; padding: 50px 20px; color: #64748b; font-size: 14.5px; background: #fff; border-radius: 14px; border: 1px dashed #cbd5e1; width: 100%; box-sizing: border-box; margin-top: 15px; } + +@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + +@keyframes slideUp { + 0% { opacity: 0; transform: scale(0.95); } + 100% { opacity: 1; transform: scale(1); } +} + +/* 7. 顶部返回按钮 */ +.nw-top-back { position: absolute; top: 16px; left: 16px; width: 45px; height: 45px; border-radius: 50%; background: #f1f5f9; color: #64748b; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; z-index: 10; } +.nw-top-back:hover { background: #e2e8f0; color: #0f172a; transform: translateX(-3px); box-shadow: 2px 2px 8px rgba(0,0,0,0.05); } + +/* 8. 单选按钮组 (Radio) */ +.nw-radio-group { display: flex; gap: 12px; align-items: stretch; margin: 0; padding: 0; width: 100%; padding: 1px;} +.nw-radio-btn { cursor: pointer; position: relative; display: block; flex: 1; margin: 0 !important; padding: 0 !important; } +.nw-radio-btn input[type="radio"] { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; } +.nw-radio-btn-text { display: flex; align-items: center; justify-content: center; height: 100%; padding: 15px 8px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 16px; color: #475569; background: #fff; transition: all 0.25s ease; position: relative; overflow: hidden; font-weight: normal; box-sizing: border-box; line-height: 1.3; text-align: center; } +.nw-radio-btn:hover .nw-radio-btn-text { border-color: #3b82f6; color: #3b82f6; background: #f8fafc; } +.nw-radio-btn input[type="radio"]:checked + .nw-radio-btn-text { border-color: #3b82f6; color: #ef4444; font-weight: 600; box-shadow: 0 0 0 1px #3b82f6; background: #eff6ff; } +.nw-radio-btn-text::after { content: ""; position: absolute; right: 0; bottom: 0; width: 0; height: 0; border-bottom: 35px solid #3b82f6; border-left: 30px solid transparent; opacity: 0; transition: opacity 0.2s ease; } +.nw-radio-btn-text::before { content: ""; position: absolute; right: 4px; bottom: 3px; width: 8px; height: 18px; border: solid white; border-width: 0 3px 3px 0; transform: rotate(38deg); opacity: 0; transition: opacity 0.2s ease; z-index: 2; } +.nw-radio-btn input[type="radio"]:checked + .nw-radio-btn-text::after, .nw-radio-btn input[type="radio"]:checked + .nw-radio-btn-text::before { opacity: 1; } + +/* 9. 自定义复选框 (Checkbox) */ +.nw-wiz-cb-wrap { display: inline-flex; align-items: center; cursor: pointer; user-select: none; position: relative; margin: 0; } +.nw-wiz-cb-wrap input { position: absolute !important; opacity: 0 !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important; border: none !important; left: -9999px !important; } +.nw-wiz-checkmark { box-sizing: border-box; height: 18px; width: 18px; background: #fff; border: 2px solid #cbd5e1; border-radius: 4px; margin-right: 8px; transition: all 0.2s ease; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } +.nw-wiz-cb-wrap:hover input ~ .nw-wiz-checkmark { border-color: #3b82f6; } +.nw-wiz-cb-wrap input:checked ~ .nw-wiz-checkmark { background-color: #3b82f6; border-color: #3b82f6; } +.nw-wiz-checkmark:after { content: ""; display: none; width: 5px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); margin-bottom: 2px; } +.nw-wiz-cb-wrap input:checked ~ .nw-wiz-checkmark:after { display: block; } + +/* 10. 面板与提示框 (Advanced, Notes, Alerts) */ +.nw-adv-btn { text-align: center; margin: 12px; cursor: pointer; color: #64748b; font-size: 13.5px; font-weight: bold; user-select: none; transition: color 0.25s ease; } +.nw-adv-btn:hover { color: #3b82f6; } +.nw-adv-panel { background: rgba(241, 245, 249, 0.5); border-radius: 10px; padding: 15px; margin-top: 12px; border: 1px solid #e2e8f0; box-shadow: inset 0 2px 5px rgba(0,0,0,0.02); } +.nw-note-box { background-color: #f8fafc; padding: 12px; font-size: 13px; margin-top: 15px; border: 1px solid #e2e8f0; line-height: 1.6; color: #475569; border-radius: 10px; } +.nw-note-title { font-weight: bold; color: #0f172a; margin-bottom: 6px; font-size: 14px; } +.nw-confirm-mode-text { color: #fff; background: #0055bb; padding: 18px 4px; border-radius: 10px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); margin-top: 12px; } +.nw-roam-desc { font-size: 13.5px; color: #64748b; line-height: 1.5; word-break: break-word; } +.nw-roam-warn { font-size: 13px; color: #ef4444; margin-top: 5px; font-weight: 600; background: #fef2f2; padding: 5px 8px; border-radius: 6px; border: 1px solid #fecaca; line-height: 1.4; } +.nw-warn-bypass, .nw-warn-main { padding: 10px; border-radius: 8px; font-size: 15.5px; margin-bottom: 12px; border: 1px solid; line-height: 1.6; font-weight: bolder; } +.nw-warn-bypass { background: #fef2f2; color: #ef4444; border-color: #fecaca; } +.nw-warn-main { background: #f0fdf4; color: #059669; border-color: #bbf7d0; } + +/* 11. 快速向导弹窗专用类与辅助工具类 */ +.nw-wiz-header-responsive { display: flex; justify-content: space-between; align-items: center; padding: 12px 10px; } +.nw-wiz-step-wrap { flex: 1; display: flex; justify-content: flex-start; order: 1; } +.nw-wiz-title-responsive { flex: 2; margin: 0; padding: 0; text-align: center; font-size: 17px; letter-spacing: 0.5px; white-space: nowrap; color: #fff; order: 2; } +.nw-wiz-close-wrap { flex: 1; display: flex; justify-content: flex-end; order: 3; } +.nw-wisp-modal-title, .nw-wiz-modal-title { margin: 0 15px; font-size: 15px; color: #eee; background: #0f172a; text-align: center; border-radius: 10px; } +.nw-wiz-modal-title { background: transparent; font-size: 25px; font-weight: 600; margin: 0; padding: 0; letter-spacing: 0.5px; } +.nw-wisp-ui-panel { flex-direction: column; align-items: center; background: #f8fafc; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; } +.nw-wisp-target-input { background: #e2e8f0 !important; color: #475569 !important; } +.nw-wifi-tabs { display: flex; gap: 8px; margin-bottom: 12px; } +.nw-tab-btn { padding: 10px 0; flex: 1; border: none; border-radius: 6px; font-size: 17px; font-weight: bold; cursor: pointer; transition: all 0.2s; } +.nw-hl { color: #facc15; font-weight: bold; margin-left: 0; } +.nw-flex-1 { flex: 1; padding-right: 12px; min-width: 0; } +.nw-flex-shrink-0 { flex-shrink: 0; } +.nw-scale-switch { transform: scale(0.9); transform-origin: left; } +.nw-m0 { margin: 0 !important; } +.nw-pointer { cursor: pointer; } +.nw-step-line, .nw-step-line svg { + background: transparent !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; +} + +/* 首页 Wi-Fi 状态面板细节 */ +.nw-info-item { display: inline-block; white-space: normal; word-break: break-all; margin: 0 8px; max-width: 100%; } +.nw-wifi-line { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px 10px; margin-bottom: 6px; width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; } +.nw-wifi-left { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: center; max-width: 100%; min-width: 0; } +.nw-wifi-badge { padding: 6px 8px; background: #ffffff; border-radius: 6px; font-size: 14px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); white-space: nowrap; flex-shrink: 0; } +.nw-wifi-colon { font-size: 14px; opacity: 0.9; font-weight: 600; margin: 0 4px; flex-shrink: 0; } +.nw-wifi-name { font-size: 15px; letter-spacing: 0.5px; display: inline-flex; flex-wrap: wrap; justify-content: center; align-items: center; word-break: break-all; overflow-wrap: anywhere; white-space: normal; text-align: center; max-width: 100%; min-width: 0; } +.nw-wifi-pwd { color: #ffffff; font-size: 14px; font-weight: 600; white-space: normal; word-break: break-all; max-width: 100%; min-width: 0; } + +/* 13. 批量操作悬浮底栏与选择框 */ +.nd-card-checkbox { margin-right: 15px; flex-shrink: 0; } + +.nd-batch-bar { + position: fixed !important; + bottom: -150px !important; + z-index: 99999 !important; + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(12px) !important; + box-shadow: 0 10px 40px rgba(0,0,0,0.2) !important; + border-radius: 16px !important; + padding: 15px 45px !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + + /* 防止 padding 把宽度撐大 */ + box-sizing: border-box !important; + left: 0 !important; + right: 0 !important; + margin: 0 auto !important; + width: calc(100% - 30px) !important; + max-width: 820px !important; + transform: none !important; + + border: 1px solid #cbd5e1 !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1) !important; +} + +.nd-batch-bar.show { + opacity: 1 !important; + visibility: visible !important; + pointer-events: auto !important; + bottom: 25px !important; +} +.nd-batch-info { width: 100%; text-align: center; font-weight: 600; font-size: 20px; color: #000;} +.nd-batch-actions { width: 100%; justify-content: space-between; display: flex; gap: 10px; } +.nd-batch-actions button { flex: 1; } + +/* ======== 批量分配与分段组件 ======== */ +.nd-category-tabs { display: flex; gap: 10px; width: 100%; max-width: 860px; margin: 15px auto 25px auto; flex-wrap: wrap; justify-content: center; padding-bottom: 5px; align-items: center; } +.nd-cat-btn { white-space: nowrap; padding: 8px 14px; margin: 1px; border-radius: 20px; background: #fff; border: 1px solid #cbd5e1; color: #64748b; font-size: 13.5px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; display: inline-flex; align-items: center; gap: 4px; } +.nd-cat-btn:hover { background: #f8fafc; border-color: #94a3b8; transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.05); } +.nd-cat-btn.active { background: #3b82f6 !important; color: #fff !important; border-color: #3b82f6; box-shadow: 0 4px 10px rgba(59,130,246,0.3); } +.nd-list-header { display:flex; width:100%; margin: 0 auto 15px auto; padding: 0 5px; align-items:center; max-width: 860px; } +.nd-strategy-card { padding: 12px 15px; border: 2px solid #e2e8f0; border-radius: 10px; cursor: pointer; font-weight: bold; color: #475569; transition: all 0.2s; background: #f8fafc; line-height: 1.4; display: flex; flex-direction: column; gap: 4px; text-align: left; } +.nd-strategy-card:hover { border-color: #94a3b8; } +.nd-strategy-card.active { border-color: #3b82f6; background: #eff6ff; color: #1e3a8a; box-shadow: 0 4px 12px rgba(59,130,246,0.15); } + +/* 修复后的智能分段布局:全局强制单行并排 */ +.nd-smart-row { flex-direction: column !important; align-items: flex-start !important; padding-bottom: 5px !important; margin: 5px 0 !important; width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } + .nd-smart-label { width: 100% !important; margin-bottom: 3px !important; font-size: 14.5px !important; } + .nd-smart-inputs { align-items: center; gap: 4px; width: 100% !important; max-width: 100% !important; background: #f8fafc !important; padding: 6px !important; box-sizing: border-box !important; display: flex !important; } + .nd-ip-prefix {letter-spacing: 1px; margin-right: auto !important; font-size: 18px !important; flex-shrink: 1 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important;} + .nd-input-sm { flex: 1 !important; min-width: 0 !important; padding: 8px 2px !important; box-sizing: border-box !important; text-align: CENTER; border: 1px solid #cbd5e1 !important;border-radius: 8px !important;font-size: 14px !important;outline: none !important;background: #f8fafc !important;color: #0f172a !important; line-height: 1.5 !important;transition: all 0.2s ease !important;font-family: inherit;resize: none;word-break: break-all;} + .nd-input-sm:focus { border-color: #3b82f6 !important; } +.nd-smart-dash { color: #cbd5e1 !important; font-weight: bold !important; padding: 0 4px !important; } + +/* ======== 入场动画 ======== */ +@keyframes nwPageFadeIn { + 0% { + opacity: 0; + transform: scale(0.96); /* 初始稍微縮小 */ + } + 100% { + opacity: 1; + transform: none; /*悬浮条不失效 */ + } +} + +@keyframes nwPageFadeOut { + 0% { opacity: 1; transform: none; } + 100% { opacity: 0; transform: scale(0.96); /* 出场時也微微缩小消失 */ } +} + +/* ============================================== + 2. 弹窗太长不会让确定按鈕被挤出屏幕 +============================================== */ +.nd-modal-box { + max-height: 95vh !important; /* 占屏幕 95% */ + display: flex !important; + flex-direction: column !important; +} +#nd-m-form { + flex: 1 !important; + overflow-y: auto !important; /* 出现滚动条 */ + overflow-x: hidden !important; + padding-right: 8px !important; + margin-bottom: 10px !important; +} +#nd-m-form::-webkit-scrollbar { width: 6px; } +#nd-m-form::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; } + + +/* ======== 全局弹窗 ======== */ +.nw-modal-box, .nw-wiz-modal-box, .nd-modal-box { + padding: 0 !important; + text-align: center !important; + box-sizing: border-box !important; + overflow: hidden !important; /* 强制裁切顶部直角为圆角 */ +} + +.nw-confirm-board { + padding: 30px 4px !important; + text-align: center !important; + box-sizing: border-box !important; +} + +.nw-confirm-board h3, +.nw-modal-box h3, +.nw-confirm-board .nw-modal-title, +.nd-modal-box .nd-modal-title { + margin: 0 0 10px 0 !important; + line-height: 1.4 !important; + background: #3b82f6 !important; + color: #fff !important; + padding: 15px 0 !important; + width: 100% !important; + font-size: 25px !important; + border-radius: 0 !important; +} + +.nw-confirm-board p, +.nw-modal-box p, +.nw-confirm-board .nw-modal-desc, +.nw-modal-box #nw-global-msg, +.nd-modal-box #nd-m-content, +.nd-modal-box #nd-m-form { + margin: 0 !important; + line-height: 1.6 !important; + color: #475569 !important; + font-size: 16px !important; + padding: 6px !important; + box-sizing: border-box !important; +} + +.nw-modal-box .nw-actions, +.nw-wiz-modal-box .nw-actions, +.nw-confirm-board .nw-actions, +.nd-modal-box .nw-actions, +#nw-global-btn-wrap, +.nd-modal-box > div:last-child { + display: flex !important; + flex-direction: row !important; + justify-content: center !important; + gap: 15px !important; + width: 100% !important; + padding: 0 10px 25px 10px !important; + margin-top: 0 !important; + box-sizing: border-box !important; +} + +.nw-modal-box .nw-u-btn, +.nw-wiz-modal-box .nw-u-btn, +.nw-confirm-board .nw-u-btn, +.nd-modal-box .nd-btn { + margin: 0 !important; + min-width: 120px !important; + flex: 1 !important; + max-width: 220px !important; +} + +.nw-actions [style*="none"], +.nw-actions button:empty, +.nw-actions a:empty { + display: none !important; +} + +#nw-global-modal { + z-index: 9999999 !important; +} + +/* ======== 防主题劫持:强制向导的弹窗按钮横排 ======== */ +.nw-wiz-modal-box div:has(> .nw-u-btn-red):has(> .nw-u-btn-green) { + display: flex !important; + flex-direction: row !important; + justify-content: center !important; + gap: 15px !important; + width: 100% !important; + margin-top: 0 !important; + margin-bottom: 10px !important; +} + +.nw-wiz-modal-box .nw-u-btn-red, +.nw-wiz-modal-box .nw-u-btn-green { + flex: 1 !important; + width: 100% !important; + min-width: 0 !important; + margin: 2px !important; + border-radius: 8px !important; +} + +/* 只要带 none 就不让显示 */ +.nw-wiz-modal-box button[style*="none"], +.nw-wiz-modal-box button.hidden { + display: none !important; +} + +.nd-control-bar label.nw-switch { + width: auto !important; height: auto !important; flex-shrink: 0 !important; + overflow: visible !important; display: flex !important; align-items: center !important; margin: 0 !important; +} +.nd-control-bar label.nw-switch > span { white-space: nowrap !important; } +.nd-control-bar label.nw-switch > div { width: 42px !important; height: 22px !important; flex-shrink: 0 !important; } + +/* 开关开启时变成绿色! */ +.nd-control-bar input:checked + .nw-slider { background-color: #10b981 !important; } +.btn-fw:active { transform: scale(0.95); opacity: 0.8; } + +#nd-modal-overlay .nd-modal-box { max-width: 460px !important; padding: 0 0 !important; } + +.nd-card-mac.btn-fw-mac { + cursor: pointer !important; + transition: all 0.2s ease !important; +} +.nd-card-mac.btn-fw-mac:hover { + background: #ede9fe !important; + color: #7c3aed !important; + border-color: #c4b5fd !important; + transform: translateY(-1px) !important; + box-shadow: 0 2px 6px rgba(124, 58, 237, 0.15) !important; +} +.nd-card-mac.btn-fw-mac:active { + transform: scale(0.95) !important; +} + +.nd-card-warning { border: 2px solid #ff4d4f !important; background-color: #fff1f0 !important; box-shadow: 0 4px 12px rgba(255, 77, 79, 0.2) !important; margin-top: 15px !important; } +@keyframes blink-warning { 0% { opacity: 1; } 50% { opacity: 0.1; } 100% { opacity: 1; } + +} + +/* 呼吸灯闪烁动画 */ +@keyframes blink-warning { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +.nd-conn-badge { position: relative; } +.nd-conn-tooltip { visibility: hidden; opacity: 0; position: absolute; bottom: 135%; left: 50%; transform: translateX(-50%); background: #0f172a; color: #f8fafc; padding: 12px; border-radius: 12px; width: 170px; z-index: 999; box-shadow: 0 10px 25px rgba(0,0,0,0.5); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; text-align: left; } +.nd-conn-tooltip::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -6px; border-width: 6px; border-style: solid; border-color: #0f172a transparent transparent transparent; } +.nd-conn-badge:hover .nd-conn-tooltip { visibility: visible; opacity: 1; bottom: 125%; } +.nd-pie-chart { width: 75px; height: 75px; border-radius: 50%; margin: 0 auto 12px auto; box-shadow: inset 0 0 10px rgba(0,0,0,0.3); } +.nd-legend-item { display: flex; justify-content: space-between; font-size: 11.5px; margin-bottom: 5px; line-height: 1; align-items: center; } +.nd-legend-color { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; } + + +/* 动态弹窗专用样式 (由 JS 呼叫) */ +/* 红色严重警告主文字 */ +.nw-alert-danger { + font-size: 14.5px; + color: #ef4444; + font-weight: bold; + line-height: 1.6; + padding: 10px 0; +} + +/* 黄色依赖警告主文字 */ +.nw-alert-warning { + font-size: 14.5px; + color: #f59e0b; + font-weight: bold; + line-height: 1.6; + padding: 10px 0; +} + +/* 警告弹窗列表(带有关闭/开启图标的那行)*/ +.nw-alert-detail-item { + padding: 6px 0; + color: #0f172a; + font-size: 14px; + margin-top: 4px; + font-weight: bold; +} + + +/* ========================================================= + 12. 手机端响应式适配 +========================================================= */ +@media screen and (max-width: 768px) { + #maincontent, .main, .main-right { padding-bottom: 40px !important; height: auto !important; overflow: visible !important; } + #netwiz-container, #netwiz-dev-container { padding-bottom: 40px !important; } + .nw-current-mode-display { padding-bottom: 10px !important; margin-bottom: 10px !important; height: auto !important; min-height: max-content !important; } + .nw-step, .nw-form-area { height: auto !important; overflow: visible !important; } + .nd-cb-title {display: none; font-size: 14px; margin: 0; } + .nd-cb-title-wrap { margin: 0; } + .nw-reopen-btn { margin-bottom: 0; } + + .main, .container, #maincontent { padding: 0 !important; margin: 0 !important; width: 100% !important; max-width: 100% !important; } + + .nw-wrapper { padding: 0 0 5px 0 !important; width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } + + .nw-header, .nw-step, .nd-control-bar, .nd-list, .nd-list-header, .nd-category-tabs { + width: 100% !important; + max-width: 96% !important; /* 左右留一点空间 */ + margin-left: auto !important; + margin-right: auto !important; + box-sizing: border-box !important; + } + + .nw-form-area, .nw-confirm-board, .nw-current-mode-display { + width: 100% !important; + max-width: 100% !important; + margin-left: 0 !important; + margin-right: 0 !important; + box-sizing: border-box !important; + } + + .nw-header { top: -5px !important; border-radius: 14px !important; padding: 10px 0 10px 0 !important; margin-top: 0 !important; max-width: 100% !important; } + .nw-main-title { font-size: 24px !important; letter-spacing: 0 !important; } + .nw-header p { font-size: 14px !important; letter-spacing: 0 !important; margin-bottom: 0 !important;} + + .nw-card-group { display: block !important; width: 100% !important; padding: 0 !important; margin-top: -10px !important; margin-bottom: 0 !important; position: relative !important; z-index: 10 !important; } + .nw-card { + width: 100% !important; max-width: 100% !important; + margin: 0 0 15px 0 !important; + padding: 24px 15px !important; + height: auto !important; min-height: 140px !important; + aspect-ratio: auto !important; + display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; + } + + .nw-actions button[style*="none"], .nw-actions button.hidden, .nw-actions button:empty { display: none !important; flex: none !important; min-width: 0 !important; width: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; } + .nw-actions { display: flex !important; flex-direction: row !important; justify-content: center !important; gap: 15px !important; margin-top: 15px !important; padding-bottom: 25px !important; } + .nw-actions button, .nw-actions .nw-u-btn { flex: 0 1 auto !important; min-width: 140px !important; padding: 10px 20px !important; margin: 0 !important; font-size: 14px !important; text-align: center !important; justify-content: center !important; } + + /* ======== 设备卡片 ======== */ + .nd-control-bar { position: sticky !important; top: 0 !important; z-index: 750 !important; padding: 8px !important; border-radius: 12px !important; flex-wrap: wrap !important; gap: 3px !important; margin: -5px auto 0 auto !important; max-width: calc(100% - 12px) !important; } + .nd-cb-sub { display: none; } + + .nd-list { gap: 0 !important; } + .nd-card { + width: 100% !important; max-width: 100% !important; + margin: 0 0 5px 0 !important; + flex-direction: column !important; align-items: flex-start !important; + padding: 6px 10px !important; gap: 8px !important; box-sizing: border-box !important; + } + .nd-card-name svg, .nd-card-name i, .nd-card-name img { display: none !important; } + .nd-card-left { display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; align-items: center !important; width: 100% !important; gap: 6px 8px !important; } + .nd-card-left > div:first-child, .nd-card-name { display: contents !important; } + .nd-card-checkbox { margin: 0 !important; } + .nd-card-left > div:first-child > div[style*="33px"] { margin-right: 0 !important; } + .nd-card-name span:first-of-type { font-size: 15px !important; font-weight: 600 !important; color: #334155 !important; word-break: break-all !important; margin: 0 !important; line-height: 1.2 !important; } + .nd-status-badge, .nd-badge, .nd-card-mac { display: inline-flex !important; white-space: nowrap !important; flex-shrink: 0 !important; margin: 0 !important; align-items: center !important; } + .nd-card-mac { font-size: 12px !important; color: #94a3b8 !important; background: transparent !important; border: none !important; padding: 3px 0 0 0 !important; font-family: monospace !important; letter-spacing: 0px !important; } + + .nd-card-mid { display: flex !important; flex-direction: row !important; align-items: center !important; justify-content: space-between !important; width: 100% !important; max-width: 100% !important; background: #f8fafc !important; padding: 4px 6px !important; border-radius: 8px !important; box-sizing: border-box !important; margin: 2px 0 !important; border: none !important; } + .nd-lease-info { order: 1 !important; font-size: 11.5px !important; color: #64748b !important; white-space: nowrap !important; margin: 0 !important; background: transparent !important; padding: 0 !important; } + .nd-card-ip { order: 2 !important; font-size: 16px !important; font-weight: 800 !important; color: #3b82f6 !important; white-space: nowrap !important; font-family: monospace !important; margin: 0 !important; text-align: right !important; white-space: normal !important; word-break: break-word !important; line-height: 1.3 !important;} + + .nd-card-right { width: 100% !important; margin-top: 4px !important; } + .nd-btn { flex: 1 !important; padding: 9px 0 !important; font-size: 14px !important; } + + .nw-top-back { top: 12px; left: 12px; width: 45px; height: 45px; } + .nw-radio-group { flex-wrap: wrap; gap: 10px; } + .nw-wiz-header-responsive { flex-wrap: wrap; padding: 12px 15px !important; } + .nw-wiz-title-responsive { flex: 1; text-align: center; font-size: 16px; order: 1; } + .nw-wiz-close-wrap { flex: 0 0 auto; order: 2; } + .nw-wiz-step-wrap { flex: 0 0 100%; justify-content: center; margin-top: 5px; order: 3; } + .nw-info-item { display: block; margin: 6px 0; text-align: center; max-width: 100%; min-width: 0; } + .nw-wifi-line { flex-direction: column; gap: 8px; margin-bottom: 6px; padding-bottom: 6px; border-bottom: 1px dashed rgba(255,255,255,0.2); max-width: 100%; min-width: 0; } + .nw-wifi-line:last-child { border-bottom: none; margin-bottom: 0; padding: 0 0 5px 0; } + .nw-wifi-left { flex-direction: column; gap: 6px; max-width: 100%; min-width: 0; } + .nw-wifi-colon { display: none; } + .nw-hl { margin-left: 0 !important; max-width: 100%; min-width: 0; display: inline-block; word-break: break-all; white-space: normal; } + .nw-wifi-name { flex-direction: column; gap: 4px; max-width: 100%; min-width: 0; } + .nw-wifi-name span[title] { margin-left: 0 !important; } + + .nd-batch-bar.show { bottom: 25px !important; padding: 12px 15px !important; border-radius: 16px !important; flex-direction: column !important; gap: 12px !important; } + .nd-batch-info { width: 100% !important; text-align: center !important; } + .nd-batch-actions { width: 100% !important; justify-content: space-between !important; display: flex !important; gap: 10px !important; } + .nd-batch-actions button { flex: 1 !important; } + + .nd-smart-row { padding: 10px 4px !important; } + .nd-smart-label { font-size: 13px !important; } + .nd-input-sm { width: 60px !important; flex: 0 0 60px !important; font-size: 13.5px !important; padding: 6px 0 !important; } + + #nd-m-form { width: 100% !important; max-width: 100% !important; overflow-x: hidden !important; box-sizing: border-box !important; } + .nd-modal-box { width: 100% !important; max-width: calc(100% - 20px) !important; box-sizing: border-box !important; margin: 0 auto !important; } + .nw-form-area { padding: 30px 10px !important; } + #nd-modal-overlay .nd-modal-box { max-width: calc(100% - 20px) !important; padding: 0 !important; font-size: 15px} + .nw-switch span{font-size: 12px} + /* .nw-switch span{color: transparent !important; font-size: 0 !important; } */ + .nd-modal-box .nd-modal-title { font-size: 18px !important; } + .nd-cb-refresh {padding: 6px 5px;} + .nw-wiz-modal-title { font-size: 22px; margin: 0; padding: 0 0 0 22px; } +} diff --git a/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.js b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.js new file mode 100644 index 000000000..b83ea2587 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz.js @@ -0,0 +1,6289 @@ +/* + * Copyright (C) 2026 huchd0 + * Licensed under the GNU General Public License v3.0 + */ +'use strict'; +'require view'; +'require dom'; +'require rpc'; +'require uci'; + +// 1. 引入模板 +'require view.netwiz_tmpl as NetwizTmpl'; +// 2. 全局T,让底下所有的bindEvents和弹窗都能存取字典 +var T; +var myTmpl; // 存放实例化的模板对象 + +var callNetSetup = rpc.declare({ object: 'netwiz', method: 'set_network', params: ['mode', 'arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6'], expect: { result: 0 } }); +var callNetDefuse = rpc.declare({ object: 'netwiz', method: 'confirm', expect: { result: 0 } }); +// 调用系统底层 iwinfo 扫描 Wi-Fi +var callIwinfoScan = rpc.declare({ object: 'iwinfo', method: 'scan', params: ['device'], expect: { results: [] } }); +var getWanStatus = rpc.declare({ object: 'network.interface', method: 'dump', expect: { '': {} } }); +var callNetCheckWifi = rpc.declare({ object: 'netwiz', method: 'check_wifi', expect: { '': {} } }); +var callSetPassword = rpc.declare({ object: 'netwiz', method: 'set_password', params: ['password'], expect: { result: 0 } }); +var callSystemBoard = rpc.declare({ object: 'system', method: 'board', expect: { '': {} } }); + +// 单一插件的局部备份与恢复接口 +var callBackupPlugin = rpc.declare({ object: 'netwiz', method: 'backup_plugin_config', params: ['plugin'], expect: { '': {} } }); +var callRestorePlugin = rpc.declare({ object: 'netwiz', method: 'restore_plugin_config', params: ['plugin'], expect: { '': {} } }); + +// 增加智能备份和恢复的接口声明 +var callSmartBackup = rpc.declare({ object: 'netwiz', method: 'smart_backup', params: ['type'], expect: { '': {} } }); +var callCheckBackup = rpc.declare({ object: 'netwiz', method: 'check_backup', expect: { '': {} } }); +var callSmartRestoreExec = rpc.declare({ object: 'netwiz', method: 'smart_restore_exec', params: ['filepath'], expect: { result: 0 } }); +var callCheckStorage = rpc.declare({ object: 'netwiz', method: 'check_storage', expect: { '': {} } }); +var callCheckRestoreStatus = rpc.declare({ object: 'netwiz', method: 'check_restore_status', expect: { '': {} } }); +var callCheckIpConflict = rpc.declare({ object: 'netwiz', method: 'check_ip_conflict', params: ['ip'], expect: { '': {} } }); +var callCheckMissingPkgs = rpc.declare({ object: 'netwiz', method: 'check_missing_pkgs', expect: { '': {} } }); +var callCheckInternet = rpc.declare({ object: 'netwiz', method: 'check_internet', expect: { '': {} } }); +var callGetClientMac = rpc.declare({ object: 'netwiz', method: 'get_client_mac', expect: { '': {} } }); +var callGetAdvSettings = rpc.declare({ object: 'netwiz', method: 'get_adv_settings', expect: { '': {} } }); +var callSetAdvSettings = rpc.declare({ object: 'netwiz', method: 'set_adv_settings', params: ['mac', 'web', 'cron', 'hosts'], expect: { '': {} } }); + +var callGetAdvLayout = rpc.declare({ object: 'netwiz', method: 'get_adv_layout', expect: { '': {} } }); +var callSetAdvLayout = rpc.declare({ object: 'netwiz', method: 'set_adv_layout', params: ['layout'], expect: { result: 0 } }); +var callSysRestore = rpc.declare({ object: 'system', method: 'sysupgrade', params: ['restore', 'filepath'], expect: { result: 0 } }); + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + render: function () { + // 落地自动拆弹!只要页面跳到新IP并加载,立刻解除120秒防砖炸弹 + if (typeof callNetDefuse === 'function') { + callNetDefuse().catch(function(){}); + } + + if (!document.querySelector('meta[name="viewport"]')) { + var meta = document.createElement('meta'); + meta.name = 'viewport'; + meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'; + document.head.appendChild(meta); + } + + var container = dom.create('div', { id: 'netwiz-container' }); + + // ========================================== + // 3. 从模块实例化出对象,并读取字典与HTML + // ========================================== + // 判断LuCI加载的是「类别」还是「已实例化的物件」 + myTmpl = (typeof NetwizTmpl === 'function') ? new NetwizTmpl() : NetwizTmpl; + + T = myTmpl.T; + window.T = myTmpl.T; + + var htmlTemplate = myTmpl.html; // 读取字符串 + + // 动态计算正确的CSS路径与时间戳,替换进模板 + htmlTemplate = htmlTemplate.split('{{CSS_URL}}').join(L.resource('view/netwiz.css') + '?v=' + Date.now()); + + // 字典替换回HTML的循环补回来 + for (var k in T) { + htmlTemplate = htmlTemplate.split('{{' + k + '}}').join(T[k]); + } + // ========================================== + // ========================================== + + container.innerHTML = htmlTemplate; + this.bindEvents(container); + return container; + }, + + bindEvents: function (container) { + + // ========================================== + // 版本更新与右上角小红点逻辑 + // ========================================== + function doUpdateCheck() { + var now = Date.now(), cacheKey = 'nw_last_update_check'; + var cached = JSON.parse(localStorage.getItem(cacheKey) || '{}'); + var currentVer = T['APP_VERSION'] || 'v1.0.0'; + + var checkOTA = rpc.declare({ + object: 'netwiz', + method: 'set_network', + params: ['mode', 'arg1'] + }); + + var showReadyBadge = function(latestVer, rawText) { + console.log('✅ [UI] 准备点亮红点...'); + var cleanText = rawText.split('---')[0].replace(/### ✨ 最新版发布/g, '').trim(); + var verTag = container.querySelector('.nw-version-tag'); + var redDot = container.querySelector('.nw-version-dot'); + + if (!verTag || !redDot) return; + + redDot.style.display = 'block'; + verTag.classList.add('has-update'); + verTag.title = '发现新版本: ' + latestVer + ' (点击更新)'; + + var newVerTag = verTag.cloneNode(true); + verTag.parentNode.replaceChild(newVerTag, verTag); + verTag = newVerTag; + + verTag.addEventListener('click', function() { + openModal({ + title: T['U_NEW_TITLE'] + ' (' + latestVer + ')', + msg: '' + T['U_READY_MSG'] + '

' + cleanText.replace(/\n/g, '
') + '
', + okText: T['U_BTN_NOW'], cancelText: T['U_BTN_LATER'], + onOk: function() { + // 点击升级后,删除防刷锁与更新缓存 + localStorage.removeItem('nw_update_cooldown'); + localStorage.removeItem(cacheKey); + + redDot.style.display = 'none'; + verTag.classList.remove('has-update'); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.display = 'none'; + + openModal({ title: T['U_UPGRADING_TITLE'], msg: T['U_UPGRADING_MSG'], hideCancel: true, hideOk: true, spin: true }); + + // 独立封装倒计时逻辑 + var startCountdown = function() { + var waitSec = 0; + // 10 秒倒计时 + var totalWait = 10; + var pollTimer = setInterval(function() { + waitSec++; + var pMsg = document.querySelector('#nw-global-msg'); + if (pMsg) { + var waitText = (T['U_UPGRADING_WAIT'] || '⏳ Forcing system file overwrite... (Est. {sec}s remaining)').replace('{sec}', (totalWait - waitSec)); + pMsg.innerHTML = '
' + T['U_UPGRADING_MSG'] + '

' + waitText + '
'; + } + + // 10 秒倒计时结束 + if (waitSec >= totalWait) { + clearInterval(pollTimer); + + sessionStorage.clear(); + for (var i = localStorage.length - 1; i >= 0; i--) { + var k = localStorage.key(i); + if (k && k !== 'nw_wizard_never_show' && (k.indexOf('nw_') === 0 || k.indexOf('luci') !== -1)) { + localStorage.removeItem(k); + } + } + + openModal({ + title: T['U_INST_SUCC_TIT'], + msg: '
' + T['U_INST_SUCC_MSG'] + '
' + + '
' + + T['U_INST_SUCC_DESC'] + '
', + okText: T['BTN_OK'], + hideCancel: true, + onOk: function() { + var cleanUrl = window.location.href.split('?')[0]; + window.location.replace(cleanUrl + '?_t=' + Date.now()); + setTimeout(function() { window.location.reload(true); }, 100); + } + }); + } + }, 1000); + }; + + // 断开对 API 的等待,发完指令瞬间启动倒计时 + callNetSetup('do_install').catch(function(){}); // 无论成败,默默执行 + startCountdown(); // 立刻开始倒计时 + } + }); + }); + console.log('🎉 [UI 成功] 红点已成功显示在界面上!'); + }; + + var triggerDownload = function(latestVer, rawText) { + var compareVer = function(v1, v2) { + var a = (v1 || '').replace(/[^0-9.]/g, '').split('.'); + var b = (v2 || '').replace(/[^0-9.]/g, '').split('.'); + for (var i = 0; i < Math.max(a.length, b.length); i++) { + var numA = parseInt(a[i], 10) || 0; + var numB = parseInt(b[i], 10) || 0; + if (numA > numB) return 1; + if (numA < numB) return -1; + } + return 0; + }; + + if (latestVer) { + if (compareVer(latestVer, currentVer) > 0) { + console.log('🔍 [1] 触发版本比对: 线上=' + latestVer + ' | 本地=' + currentVer); + console.log('🚀 [2] 发现新版本!正在后台检查状态...'); + checkOTA('check_update', latestVer).then(function(res) { + var rCode = (res !== null && typeof res === 'object' && res.result !== undefined) ? res.result : res; + if (String(rCode) === '1') { + console.log('✅ [3] 后台已有 .ready,直接显示红点!'); + showReadyBadge(latestVer, rawText); + } else { + console.log('⏳ [3] 后台未就绪,下发 prepare_update 开始下载...'); + callNetSetup('prepare_update', latestVer); + + var pollCount = 0, pollStatus = setInterval(function() { + pollCount++; + if (pollCount > 15) { + console.log('❌ [4] 轮询后端超时!可能遭遇网络问题或下载了问题文件而被阻断。'); + clearInterval(pollStatus); return; + } + checkOTA('check_update', latestVer).then(function(r) { + var rC = (r !== null && typeof r === 'object' && r.result !== undefined) ? r.result : r; + if (String(rC) === '1') { + console.log('🎉 [4] 监测到下载完成!准备点亮红点!'); + clearInterval(pollStatus); + showReadyBadge(latestVer, rawText); + } + }).catch(function(){}); + }, 4000); + } + }).catch(function(e) { console.error('❌ [3] RPC 呼叫 checkOTA 失败:', e); }); + } else { + console.log('✅ [1] 比对完毕:当前固件已是最新版本,或处于防刷保护中。'); + } + } + }; + + // ========== 缓存读取日志 ========== + if (cached.time && (now - cached.time < 5 * 60 * 1000) && cached.version) { + var remainSec = Math.ceil((5 * 60 * 1000 - (now - cached.time)) / 1000); + if (cached.isFallback) { + console.warn('🛡️ [0] API 曾受限,当前处于【静默冷却期】 (剩 ' + remainSec + ' 秒)。'); + } else { + console.log('🌐 [0] 命中缓存 (距下次联网获取还剩 ' + remainSec + ' 秒)。'); + } + triggerDownload(cached.version, cached.body || ''); + return; + } + + console.log('🌐 [0] 缓存已过期或无缓存,发起真实网络请求...'); + fetch('https://api.github.com/repos/huchd0/luci-app-netwiz/releases?t=' + now, { cache: 'no-store' }).then(function(res) { + return res.json(); + }).then(function(data) { + if (data && data.length > 0 && data[0].tag_name) { + console.log('🌐 [0] 请求成功!获取到线上版本:', data[0].tag_name); + localStorage.setItem(cacheKey, JSON.stringify({ time: now, version: data[0].tag_name, body: data[0].body || '' })); + triggerDownload(data[0].tag_name, data[0].body || ''); + } else { + console.warn('⚠️ [0] API 受限或无有效数据!生成【伪缓存】静默 5 分钟!'); + localStorage.setItem(cacheKey, JSON.stringify({ time: now, version: currentVer, body: '', isFallback: true })); + } + }).catch(function(e) { + console.error('❌ [0] API 网络阻断:', e); + console.warn('🛡️ [0] 生成【伪缓存】静默 5 分钟,防止请求风暴...'); + localStorage.setItem(cacheKey, JSON.stringify({ time: now, version: currentVer, body: '', isFallback: true })); + }); + } + setTimeout(doUpdateCheck, 1500); + + // ================= + + // =================高级设置弹窗与逻辑================= + function showAdvModal(title, html, onOk) { + var bg = document.createElement('div'); + bg.id = 'nw-adv-modal'; + bg.style.cssText = 'position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.4); z-index:9999; display:flex; align-items:center; justify-content:center; backdrop-filter: blur(4px);'; + var box = document.createElement('div'); + box.style.cssText = 'background:#fff; width:420px; max-width:95%; border-radius:12px; padding:20px 12px 24px 12px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); font-family:sans-serif;'; + box.innerHTML = '
' + title + '
' + + '
' + html + '
' + + '
' + + '' + + '' + + '
'; + bg.appendChild(box); document.body.appendChild(bg); + box.querySelector('#mdl-cancel').onclick = function() { document.body.removeChild(bg); }; + box.querySelector('#mdl-ok').onclick = function() { if(onOk(box) !== false) document.body.removeChild(bg); }; + } + + // ==================================================================== + // 拖拽排序、显隐引擎与离线上传 + // ==================================================================== + var defaultAdvLayout = [ + { id: 'link-cron-reboot', icon: '⏱️', name: (T['LBL_CRON_REBOOT_LINK'] || '⏱️ Scheduled Reboot').replace('⏱️ ', ''), show: true }, + { id: 'link-modify-hosts', icon: '✏️', name: (T['LBL_HOSTS_LINK'] || '✏️ Custom Hosts').replace('✏️ ', ''), show: true }, + { id: 'link-mac-clone', icon: '🔗', name: (T['LBL_MAC_CLONE_LINK'] || '🔗 MAC Clone').replace('🔗 ', ''), show: false }, + { id: 'link-repair-plugin', icon: '🚑', name: (T['LBL_REPAIR_BTN'] || '🚑 Plugin Repair').replace('🚑 ', ''), show: false }, + { id: 'link-offline-safe', icon: '📦', name: (T['ADV_SAFE_BTN'] || '📦 Plugin Installation').replace('📦 ', ''), show: false }, + { id: 'link-ipv6-watchdog', icon: '📡', name: (T['LBL_WATCHDOG_LINK'] || '📡 IPv6 Watchdog').replace('📡 ', ''), show: false } + ]; + + // 动态合并语言包与用户设置 + function mergeLayout(savedArr) { + var merged = []; + // 1. 根据存储的ID顺序与显示状态,加载当前系统语言的名称与图标。 + savedArr.forEach(function(savedItem) { + for (var i = 0; i < defaultAdvLayout.length; i++) { + if (defaultAdvLayout[i].id === savedItem.id) { + merged.push({ + id: savedItem.id, + icon: defaultAdvLayout[i].icon, + name: defaultAdvLayout[i].name, // 从字典读取当前系统语言 + show: savedItem.show + }); + break; + } + } + }); + // 2. 向上兼容防呆:如果以后固件升级加入了新模块,自动补齐到最后面 + defaultAdvLayout.forEach(function(defItem) { + var found = false; + for (var i = 0; i < merged.length; i++) { + if (merged[i].id === defItem.id) { found = true; break; } + } + if (!found) merged.push(defItem); + }); + return merged; + } + + // 1. 初始化时向路由器获取最新排版 + callGetAdvLayout().then(function(res) { + if (res && res.layout) { + try { + var parsed = JSON.parse(res.layout); + window._nwAdvLayout = Array.isArray(parsed) ? mergeLayout(parsed) : defaultAdvLayout; + } catch(e) { window._nwAdvLayout = defaultAdvLayout; } + } else { + window._nwAdvLayout = defaultAdvLayout; + } + applyAdvancedLayout(window._nwAdvLayout); + }).catch(function() { applyAdvancedLayout(defaultAdvLayout); }); + + // 2. 排版函数 + function applyAdvancedLayout(layoutArr) { + var modContainer = container.querySelector('#nw-adv-modules-container'); + if (!modContainer || !Array.isArray(layoutArr)) return; + layoutArr.forEach(function(item) { + var modEl = container.querySelector('#' + item.id); + if (modEl) { + modEl.style.display = item.show ? 'inline-block' : 'none'; + modContainer.appendChild(modEl); // 自动按数组顺序追加,实现无缝重排 + } + }); + } + + // 3. 绑定齿轮点击事件 (打开拖拽管理面板) + var btnLayoutMgr = container.querySelector('#btn-layout-mgr'); + if (btnLayoutMgr) { + btnLayoutMgr.addEventListener('click', function() { + var currentLayout = window._nwAdvLayout || defaultAdvLayout; + + // 1. 先用循环把所有清单项目的 HTML 组装起来 + var listHtml = ''; + currentLayout.forEach(function(item) { + listHtml += ` +
  • + ☰ + ${item.icon} + ${item.name} + +
  • + `; + }); + + // 2. 读取 tmpl 骨架,并直接把提示文字和「整个清单」都替换进去 + var modalHtml = myTmpl.layoutModalHtml + .replace('{{ADV_LAYOUT_TIP}}', T['ADV_LAYOUT_TIP'] || 'Drag ☰ to reorder, check to show/hide:') + .replace('{{LAYOUT_LIST_ITEMS}}', listHtml); // 占位符把清单塞进去 + + // 3. 呼叫弹窗 + showAdvModal((T['LBL_MOD_LAYOUT'] || 'Advanced Panel Management'), modalHtml, function(box) { + + // 拖拽绑定逻辑 + var list = box.querySelector('#nw-layout-list'); + if (!list) return; + var newList = []; + var saveList = []; // 给后端闪存准备的纯净数组 + + var items = box.querySelectorAll('.nw-layout-item'); + items.forEach(function(el) { + var _id = el.getAttribute('data-id'); + var _show = el.querySelector('.nw-layout-chk').checked; + var _icon = el.querySelector('.mod-icon').innerText; + var _name = el.querySelector('.mod-name').innerText; + + // 前端实时渲染用的数组(包含文字图标) + newList.push({ id: _id, icon: _icon, name: _name, show: _show }); + // 给路由器底层的数组 + saveList.push({ id: _id, show: _show }); + }); + + var jsonStr = JSON.stringify(newList); + var oldJsonStr = JSON.stringify(window._nwAdvLayout || defaultAdvLayout); + + // 防呆拦截:没有任何改动,直接关闭视窗 + if (jsonStr === oldJsonStr) return; + + // 应用到网页,无需刷新 + window._nwAdvLayout = newList; + applyAdvancedLayout(newList); + + // 点击确定后,才写入数据 (发送纯净版 saveList 给后端) + var backendStr = JSON.stringify(saveList); + openModal({ title: T['LBL_ADV_UTILS_TITLE'] || '⚙️', msg: T['MSG_WRITING'] || 'Saving...', spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + callSetAdvLayout(backendStr).then(function(){ + setTimeout(function(){ document.getElementById('nw-global-modal').style.display='none'; }, 500); + }).catch(function(e){ console.error((T['ADV_ERR_SAVE_LAYOUT'] || 'Save layout failed'), e); document.getElementById('nw-global-modal').style.display='none'; }); + }); + + // HTML5 原生拖拽逻辑 + setTimeout(function() { + var list = document.getElementById('nw-layout-list'); + var draggingEl = null; + if(!list) return; + list.addEventListener('dragstart', function(e) { + draggingEl = e.target.closest('.nw-layout-item'); + setTimeout(function() { draggingEl.style.opacity = '0.4'; draggingEl.style.background = '#e2e8f0'; }, 0); + }); + list.addEventListener('dragend', function(e) { + if(draggingEl) { draggingEl.style.opacity = '1'; draggingEl.style.background = '#f8fafc'; draggingEl = null; } + }); + list.addEventListener('dragover', function(e) { + e.preventDefault(); + var target = e.target.closest('.nw-layout-item'); + if (target && target !== draggingEl) { + var rect = target.getBoundingClientRect(); + var next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5; + list.insertBefore(draggingEl, next ? target.nextSibling : target); + } + }); + }, 300); + }); + } + + // 4. 离线保险箱 (多文件批量上传与队列处理) 事件绑定 + var btnOfflineSafe = container.querySelector('#link-offline-safe'); + if (btnOfflineSafe) { + btnOfflineSafe.addEventListener('click', function() { + + // 👇 核心优化 1:从 tmpl 读取骨架,并替换所有多语言字典变量 + var dropZoneHtml = myTmpl.offlineSafeModalHtml + .replace('{{ADV_SAFE_TITLE}}', T['ADV_SAFE_TITLE'] || 'Offline Plugin Installation') + .replace('{{ADV_SAFE_DESC}}', T['ADV_SAFE_DESC'] || 'Drop or click to select multiple .apk / .ipk files') + .replace('{{ADV_SAFE_BACKUP}}', T['ADV_SAFE_BACKUP'] || 'Keep a permanent backup in /etc/netwiz/custom_pkgs') + .replace('{{ADV_BTN_START}}', T['ADV_BTN_START'] || '🚀 Start Upload & Secure'); + + openModal({ title: (T['ADV_SAFE_BTN'] || 'Offline Safe'), msg: dropZoneHtml, okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + + setTimeout(function() { + var dropZone = document.getElementById('nw-drop-zone'); + var fileInput = document.getElementById('nw-file-input'); + var fileListEl = document.getElementById('nw-file-list'); + var btnUpload = document.getElementById('nw-btn-upload'); + var prog = document.getElementById('nw-upload-progress'); + + var selectedFiles = []; // 暂存队列数组 + + if(!dropZone) return; + + // 绑定UI事件 + dropZone.addEventListener('click', function() { fileInput.click(); }); + dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.style.backgroundColor = '#dbeafe'; dropZone.style.borderColor = '#2563eb'; }); + dropZone.addEventListener('dragleave', function() { dropZone.style.backgroundColor = '#eff6ff'; dropZone.style.borderColor = '#3b82f6'; }); + dropZone.addEventListener('drop', function(e) { + e.preventDefault(); dropZone.style.backgroundColor = '#eff6ff'; dropZone.style.borderColor = '#3b82f6'; + if (e.dataTransfer.files.length) addFilesToStaging(e.dataTransfer.files); + }); + fileInput.addEventListener('change', function() { + if (fileInput.files.length) addFilesToStaging(fileInput.files); + fileInput.value = ''; // 清空value,允许重复提取相同档案 + }); + + // 档案加入暂存区逻辑 + function addFilesToStaging(files) { + prog.innerHTML = ''; + for(var i=0; i + 📄 ${f.name} + + ${sizeKB} + ✕ + + + `; + }); + btnUpload.style.display = 'inline-block'; + btnUpload.innerText = (T['ADV_BTN_CONFIRM'] || '🚀 Confirm Upload ({num} files)').replace('{num}', selectedFiles.length); + } + + // 点击上传按钮->启动队列引擎 + btnUpload.addEventListener('click', function() { + if(selectedFiles.length === 0) return; + btnUpload.disabled = true; + btnUpload.style.background = '#94a3b8'; + btnUpload.innerText = T['ADV_BTN_PROC'] || 'Processing, do not close the window...'; + processUploadQueue(0); + }); + + // 循序处理 + function processUploadQueue(idx) { + if (idx >= selectedFiles.length) { + prog.style.color = '#3b82f6'; + prog.innerHTML = T['ADV_MSG_BATCH_INST'] || '⚙️ All files secured, executing batch install...'; + + // 调用新接口,一次性把刚刚传进去的所有包全部安装 + rpc.declare({ object: 'netwiz', method: 'batch_install_pkgs' })().then(function() { + prog.style.color = '#10b981'; + prog.innerHTML = T['ADV_MSG_INST_OK'] || '✅ Installation successful! Refreshing...'; + setTimeout(function() { window.location.reload(); }, 1500); + }).catch(function(err) { + var errStr = String(err).toLowerCase(); + // 拦截 会暴力切断连接导致 aborted 或 timeout 的情况 + if (errStr.indexOf('timed out') !== -1 || errStr.indexOf('timeout') !== -1 || errStr.indexOf('aborted') !== -1) { + prog.style.color = '#f59e0b'; + prog.innerHTML = (T['ADV_MSG_HW_WAIT'] || '⚙️ Hardware configuring, please wait {sec}s...').replace('{sec}', 15); + var wSec = 15; + var cTimer = setInterval(function() { + wSec--; + var mEl = document.getElementById('nw-batch-cnt'); + if (mEl) mEl.innerText = wSec; + if (wSec <= 0) { + clearInterval(cTimer); + window.location.reload(); + } + }, 1000); + } else { + handleError((T['ADV_ERR_BATCH'] || '❌ Batch install error: ') + err); + } + }); + return; + } + + var file = selectedFiles[idx]; + prog.style.color = '#3b82f6'; + prog.innerHTML = (T['ADV_MSG_PROC_FILE'] || '🚀 Processing ({idx}/{total}): {name} - {pct}%') + .replace('{idx}', idx+1).replace('{total}', selectedFiles.length).replace('{name}', file.name).replace('{pct}', 0); + + var fd = new FormData(); + fd.append('sessionid', (typeof L!=='undefined'&&L.env&&L.env.sessionid)?L.env.sessionid:""); + fd.append('filename', '/tmp/' + file.name); + fd.append('filedata', file); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/cgi-bin/cgi-upload', true); + xhr.upload.onprogress = function(e) { + if(e.lengthComputable) { + var pct = Math.floor((e.loaded / e.total) * 100); + prog.innerHTML = (T['ADV_MSG_PROC_FILE'] || '🚀 Processing ({idx}/{total}): {name} - {pct}%') + .replace('{idx}', idx+1).replace('{total}', selectedFiles.length).replace('{name}', file.name).replace('{pct}', pct); + } + }; + xhr.onload = function() { + if (xhr.status === 200) { + prog.innerHTML = (T['ADV_MSG_BG_SECURE'] || '⚙️ Securing in background: {name}').replace('{name}', file.name); + + // 获取复选框的状态 ('1' 为勾选备份,'0' 为仅临时运行) + var chkEl = document.getElementById('nw-chk-backup'); + var isBackup = (chkEl && chkEl.checked) ? '1' : '0'; + + // 修改 RPC 声明,追加 backup 参数传给后端 + rpc.declare({ object: 'netwiz', method: 'store_offline_pkg', params: ['filename', 'backup'] })(file.name, isBackup).then(function() { + // 成功,递归呼叫处理下一个档案 + processUploadQueue(idx + 1); + }).catch(function() { + handleError((T['ADV_ERR_MV'] || '❌ Backend transfer failed: {name}').replace('{name}', file.name)); + }); + } else { + handleError((T['ADV_ERR_UP'] || '❌ Upload failed ({code}): {name}').replace('{code}', xhr.status).replace('{name}', file.name)); + } + }; + xhr.onerror = function() { handleError((T['ADV_ERR_NET'] || '❌ Network disconnected: {name}').replace('{name}', file.name)); }; + xhr.send(fd); + + function handleError(errMsg) { + prog.style.color = '#ef4444'; prog.innerHTML = errMsg; + btnUpload.disabled = false; btnUpload.style.background = '#10b981'; + btnUpload.innerText = T['ADV_BTN_RETRY'] || 'Continue to retry remaining files'; + } + } + }, 100); + }); + } + // ==================== 高级管理器与上传核心引擎结束 ==================== + + // ================== IPv6 保活探针弹窗逻辑 ================== + document.addEventListener('click', function(e) { + var btn = e.target.closest('#link-ipv6-watchdog'); + if (!btn) return; + + uci.load('netwiz').then(function() { + var isEn = safeUciGet('netwiz', 'main', 'watchdog_enable', '0'); + var url = safeUciGet('netwiz', 'main', 'watchdog_url', ''); + + // 渲染极客级设置面板 + var html = '
    ' + + '
    ' + + '' + (T['WOG_ENABLE'] || 'Enable Advanced IPv6 Link Monitoring & Auto Recovery') + '' + + '' + + '
    ' + + '
    ' + + '
    ' + (T['WOG_URL_LBL'] || 'Probe Target URL') + '
    ' + + '' + + '
    ' + + '
    ' + + (T['WOG_HELP'] || 'Help Text') + + '
    ' + + '
    '; + + // 调出保存弹窗 + showAdvModal((T['WOG_TITLE'] || 'IPv6 Heartbeat Probe'), html, function(box) { + var nEn = box.querySelector('#nw-wog-en').checked ? '1' : '0'; + var nUrl = box.querySelector('#nw-wog-url').value.trim(); + + // 完全没有修改,直接退出 + if (nEn === isEn && nUrl === url) { + return; + } + + if (nEn === '1' && nUrl === '') { + alert('❌ ' + T['ERR_EMPTY_URL']); + return false; + } + + openModal({ title: '⚙️ ' + (T['WOG_TITLE'] || 'IPv6 Heartbeat Probe'), msg: T['MSG_WRITING'] || 'Saving...', spin: true }); + + uci.set('netwiz', 'main', 'watchdog_enable', nEn); + uci.set('netwiz', 'main', 'watchdog_url', nUrl); + + var p = Promise.resolve(); + + if (nEn === '1') { + var ipv6Checkbox = document.getElementById('lan-ipv6-toggle'); + if (ipv6Checkbox && ipv6Checkbox.checked) { + uci.set('dhcp', 'lan', 'dhcpv6', 'server'); + uci.set('dhcp', 'lan', 'ra', 'server'); + uci.set('dhcp', 'lan', 'ndp', 'server'); + uci.set('dhcp', 'lan', 'ra_flags', ['managed-config', 'other-config']); + } + } + + p.then(function() { + return uci.save(); // 第二步:将 UCI 数据存入暂存 + }).then(function() { + return uci.apply(); // 第三步:系统级应用与重启服务 + }).then(function() { + // 仅静默隐藏转圈弹窗,让用户可以继续他原本的设定流 + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + }).catch(function(err) { + // 只有发生严重异常时才兜底重载页面 + window.location.reload(); + }); + }); + + // 弹窗内的开关联动动画与 UI 提示 + setTimeout(function() { + var mChk = document.getElementById('nw-wog-en'); + var mBox = document.getElementById('nw-wog-url-box'); + + var ipv6Checkbox = document.getElementById('lan-ipv6-toggle'); + + if(mChk && mBox) { + mChk.addEventListener('change', function() { + mBox.style.display = this.checked ? 'block' : 'none'; + + if (this.checked) { + var needsPrompt = false; + var promptHtml = '
    ' + + '
    ' + + (T['MSG_WOG_LINKAGE'] || "To ensure the probe works correctly, the following dependent features have been automatically enabled:") + '
    '; + + if (ipv6Checkbox && !ipv6Checkbox.checked) { + ipv6Checkbox.checked = true; + needsPrompt = true; + promptHtml += '
    ✅ ' + (T['MSG_WOG_LINK_V6'] || "IPv6 Master Switch") + '
    '; + } + + promptHtml += '
    '; + + if (needsPrompt) { + openModal({ + title: '💡 ' + (T['WOG_TITLE'] || 'IPv6 Watchdog'), + msg: promptHtml, + okText: T['BTN_OK'] || 'OK' + }); + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.zIndex = '100000'; + } + } + }); + } + }, 50); + }); + }); + // ================== 结束 ================== + + // ================= 高级与实验室:动态包裹与折叠逻辑 ================= + setTimeout(function() { + var cloneLink = container.querySelector('#link-mac-clone'); + if (cloneLink) { + var advBlock = cloneLink.closest('div[style*="margin-top"]'); + if (advBlock && advBlock.parentNode && !document.getElementById('nw-hidden-features')) { + var wrapper = document.createElement('div'); + wrapper.id = 'nw-hidden-features'; + // 加上 overflow: hidden,高度折叠时内部元素才不会溢出 + wrapper.style.overflow = 'hidden'; + + // 使用 sessionStorage,关闭浏览器即恢复默认折叠 + var isExp = (sessionStorage.getItem('nw_adv_expanded') === '1'); + wrapper.style.display = isExp ? 'block' : 'none'; + wrapper.style.opacity = isExp ? '1' : '0'; + wrapper.style.height = isExp ? 'auto' : '0px'; + + advBlock.parentNode.insertBefore(wrapper, advBlock); + while (wrapper.nextElementSibling) { + wrapper.appendChild(wrapper.nextElementSibling); + } + } + } + }, 150); + + // 2. 监听按钮点击,出场动画 + container.addEventListener('click', function(e) { + if (e.target && e.target.id === 'nw-toggle-adv') { + var wrapper = document.getElementById('nw-hidden-features'); + if (wrapper && !wrapper.isAnimating) { + wrapper.isAnimating = true; + var isHidden = wrapper.style.display === 'none'; + + if (isHidden) { + // 动画 + sessionStorage.setItem('nw_adv_expanded', '1'); // 会话级记忆展开 + wrapper.style.display = 'block'; + var targetHeight = wrapper.scrollHeight; + + wrapper.offsetHeight; // 强制浏览器重绘 + + wrapper.style.transition = 'height 1s cubic-bezier(0.4, 0, 0.2, 1), opacity 1s cubic-bezier(0.4, 0, 0.2, 1)'; + wrapper.style.height = targetHeight + 'px'; + wrapper.style.opacity = '1'; + + e.target.innerHTML = (T['BTN_ADV_HIDE'] || 'Advanced Settings ▲'); + e.target.style.background = 'rgba(0,0,0,0.3)'; + + // 动画 + setTimeout(function() { + wrapper.style.height = 'auto'; // 解除高度锁定 + wrapper.isAnimating = false; + + wrapper.scrollIntoView({ behavior: 'smooth', block: 'end' }); + }, 500); + } else { + // 收合 + sessionStorage.setItem('nw_adv_expanded', '0'); // 会话级记忆收合 + var currentHeight = wrapper.scrollHeight; + wrapper.style.height = currentHeight + 'px'; + + wrapper.offsetHeight; // 强制重绘 + + wrapper.style.transition = 'height 0.45s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.45s cubic-bezier(0.4, 0, 0.2, 1)'; + wrapper.style.height = '0px'; + wrapper.style.opacity = '0'; + + e.target.innerHTML = (T['BTN_ADV_SHOW'] || 'Advanced Settings ▼'); + e.target.style.background = 'rgba(0,0,0,0.15)'; + + // 收合动画滚动回页面最上方 + setTimeout(function() { + // 让整个插件的主容器顶部对齐屏幕顶部,兼容所有主题 + if (container) { + container.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, 50); + + // 计时器同步:等 450 毫秒动画彻底播完后,再彻底隐藏元素 + setTimeout(function() { + wrapper.style.display = 'none'; + wrapper.isAnimating = false; + }, 450); + } + } + } + }); + // ================================================================= + + // MAC 克隆 + if(container.querySelector('#link-mac-clone')) { + container.querySelector('#link-mac-clone').addEventListener('click', function() { + callGetAdvSettings().then(function(res) { + var currentMac = (res && res.mac !== 'none') ? res.mac : ''; + var html ='
    ' + + (T['MSG_MAC_CLONE_TIP'] || '💡 Tip: If dial-up fails, enter the cloned MAC here.') + + '
    ' + + '' + + ''; + showAdvModal((T['LBL_MAC_CLONE'] || 'MAC Clone'), html, function(box) { + var m = box.querySelector('#mdl-mac-val').value.trim(); + + // 防呆 1:完全没有修改,直接退出 (避免无意义的断网重启) + if (m === currentMac) { + return; + } + + // 防呆 2:如果有输入内容,进行严格的格式校验 + if (m !== '' && !/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/i.test(m)) { + openModal({ title: T['M_FMT_TIT'] || 'Format Error', msg: '
    ' + (T['M_FMT_MAC'] || 'Invalid MAC address format!') + '
    ', okText: T['BTN_OK'] || 'OK' }); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.zIndex = '100000'; + return false; + } + + openModal({ title: (T['LBL_MAC_CLONE'] || 'MAC Clone'), msg: (T['MSG_WRITING'] || 'Saving...'), spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + callSetAdvSettings(m || 'none', '', '').then(function() { setTimeout(function(){ window.location.reload(); }, 2500); }); + }); + document.getElementById('mdl-get-mac').onclick = function() { + callGetClientMac().then(function(r) { + if(r && r.mac) { + document.getElementById('mdl-mac-val').value = r.mac.toUpperCase(); + } else { + openModal({ title: T['M_SYS_ERR'] || 'Notice', msg: '
    ' + (T['MSG_MAC_NOT_FOUND'] || 'No active device connection detected.') + '
    ', okText: T['BTN_OK'] || 'OK' }); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.zIndex = '100000'; + } + }); + }; + document.getElementById('mdl-clear-mac').onclick = function() { document.getElementById('mdl-mac-val').value = ''; }; + }); + }); + } + + // 定时重启 (多选增强版) + if(container.querySelector('#link-cron-reboot')) { + container.querySelector('#link-cron-reboot').addEventListener('click', function() { + callGetAdvSettings().then(function(res) { + var c = res.cron || 'off'; + var isOff = (c === 'off'); + var m = "00", h = "04", d = "*"; + + // 解析后端传来的 Cron 表达式 + if (!isOff && c.split(' ').length >= 5) { + var pts = c.split(' '); + m = pts[0].padStart(2, '0'); + h = pts[1].padStart(2, '0'); + d = pts[4]; + } + + // 将星期字符串转换为数组,方便匹配 + var dArr = (d === '*') ? ['0','1','2','3','4','5','6'] : d.split(','); + // 辅助函数:如果在数组中,则直接在 HTML 层面打上勾 + var chk = function(val) { return dArr.indexOf(val) !== -1 ? 'checked' : ''; }; + + // 复选框抵消 LuCI 全局主题的 top: .4rem 偏移 + var cbBoxStyle = 'margin:0; position:relative; top:0; transform:none; cursor:pointer; background-color: var(--primary) !important;'; + + var html = '' + + '
    ' + + '
    ' + (T['LBL_CRON_TIME'] || 'Time:') + '
    ' + + '' + + '
    ' + (T['LBL_CRON_DAYS'] || 'Days:') + '
    ' + + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    '; + + showAdvModal((T['LBL_CRON_REBOOT'] || 'Scheduled Reboot'), html, function(box) { + var en = box.querySelector('#mdl-cron-en').checked; + var cronStr = 'off'; + if(en) { + var sel = []; + var chks = box.querySelectorAll('.c-day'); + for(var i=0; i' + (T['MSG_CRON_NO_DAY'] || 'Please select at least one day!') + '', okText: T['BTN_OK'] || 'OK' }); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.zIndex = '100000'; + return false; + } + + // 生成 Linux 标准 Cron 表达式 (7天全选则直接用星号) + var dStr = (sel.length === 7) ? '*' : sel.join(','); + var tParts = (box.querySelector('#mdl-cron-time').value || '04:00').split(':'); + cronStr = parseInt(tParts[1]) + " " + parseInt(tParts[0]) + " * * " + dStr; + } + + // 防呆 1:完全没有修改,直接退出 + // c 是打开弹窗前从底层读取的初始定时状态 (off 或 0 4 * * * 等) + if (cronStr === c) { + return; + } + + openModal({ title: (T['LBL_CRON_REBOOT'] || 'Scheduled Reboot'), msg: (T['MSG_WRITING'] || 'Saving...'), spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + callSetAdvSettings('', '', cronStr).then(function() { setTimeout(function(){ window.location.reload(); }, 1500); }); + }); + + // 弹窗出现后,初始化复选框与详情区域的联动显示逻辑 + document.getElementById('mdl-cron-en').onchange = function() { + document.getElementById('mdl-cron-box').style.display = this.checked ? 'block' : 'none'; + }; + }); + }); + } + + // 外网访问开关与自定义端口 + if(container.querySelector('#adv-web-toggle')) { + var webTog = container.querySelector('#adv-web-toggle'); + var webPort = container.querySelector('#adv-web-port'); + var lastValidPort = ''; + var isSaving = false; // 防重复提交锁 + + // 校验与保存通道 + function validateAndSavePort(portStr) { + if (isSaving) return; + + var pText = portStr.trim(); + if (pText !== '') { + var pNum = parseInt(pText); + + // 拦截非法数字范围 + if (isNaN(pNum) || pNum < 1 || pNum > 65535) { + openModal({ title: T['M_REP_NOTICE_TIT'] || 'Notice', msg: T['M_PORT_RANGE'] || '⚠️ Port number must be between 1 and 65535', okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + webPort.value = lastValidPort; + // 根据历史状态来决定开关是开是关 + if (lastValidPort === '') webTog.checked = false; else webTog.checked = true; + return; + } + + // 拦截高危系统端口 + var dangerPorts = [21, 22, 23, 53, 67, 68, 445]; + if (dangerPorts.indexOf(pNum) !== -1) { + var e1 = T['M_PORT_ERR1'] || '⚠️ For system security, do not use'; + var e2 = T['M_PORT_ERR2'] || 'as the external port. It is a reserved high-risk port.'; + var sg = T['M_PORT_SUGG'] || 'It is recommended to use 8080 or a port above 10000.'; + openModal({ title: T['M_REP_NOTICE_TIT'] || 'Notice', msg: e1 + ' ' + pNum + ' ' + e2 + '

    ' + sg, okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + webPort.value = lastValidPort; + // 根据历史状态来决定开关是开是关 + if (lastValidPort === '') webTog.checked = false; else webTog.checked = true; + return; + } + } + + // 通过所有安检,开始执行保存 + isSaving = true; + var val = pText ? pText : '1'; + var oldPort = lastValidPort; // 旧端口备份 + + webTog.checked = true; // 确保开关 UI 处于打开状态 + + openModal({ title: T['LBL_ADV_UTILS_TITLE'] || '⚙️ Advanced Utilities', msg: T['MSG_WRITING'] || 'Please wait...', spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + + callSetAdvSettings('', val, '', '').then(function(res) { + // 此时 res 已经是完整的 JSON,可以成功读取 status + if (res && res.status === 'error') { + isSaving = false; // 遭遇拦截时,解除防抖锁 + + // 完美回滚数值和开关状态 + lastValidPort = oldPort; + webPort.value = oldPort; + if (oldPort === '') webTog.checked = false; + + // 弹出红色警告窗口 + var p1 = T['M_PORT_CONFLICT_P1'] || 'Port'; + var p2 = T['M_PORT_CONFLICT_P2'] || 'is already in use by another application!'; + var errMsg = '⚠️ ' + p1 + ' ' + val + ' ' + p2; + + openModal({ + title: '⚠️ ' + (T['M_REP_NOTICE_TIT'] || 'Notice'), + msg: '
    ' + errMsg + '
    ', + okText: T['M_CLOSE'] || 'Close', + hideCancel: true + }); + return; // 中止后续的 reload,画面静止 + } + + // 后端没有报错,才正式把新端口覆盖进内存 + lastValidPort = val; + setTimeout(function(){ window.location.reload(); }, 3500); + }).catch(function() { + isSaving = false; + lastValidPort = oldPort; + webPort.value = oldPort; + openModal({ title: '⚠️ Error', msg: '
    Network Error or System Busy!
    ', okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + }); + } + + // 1. 页面初始化加载历史状态 + callGetAdvSettings().then(function(res) { + if (res) { + if (res.last_port && res.last_port !== '80' && res.last_port !== '1' && res.last_port !== '0') { + webPort.value = res.last_port; + lastValidPort = res.last_port; + } + if (res.web && res.web !== '0') { + webTog.checked = true; + if (res.web !== '1') { + webPort.value = res.web; + lastValidPort = res.web; + } + + // === IPv6 直达链接 (支持探针代理拼接) === + if (res.wan6_ip) { + if (!container.querySelector('#nw-v6-link')) { + uci.load('netwiz').then(function() { + var wogUrl = safeUciGet('netwiz', 'main', 'watchdog_url', '').trim(); + var wogEn = safeUciGet('netwiz', 'main', 'watchdog_enable', '0'); + var curPort = (res.web !== '1') ? res.web : '80'; + var v6Link = 'http://[' + res.wan6_ip + ']:' + curPort; + var finalLink = ''; + + // 如果用户填了探测源目标网址,并且开启了探针 + if (wogEn === '1' && wogUrl !== '') { + // 确保带有 http/https 前缀 + if (wogUrl.indexOf('http') !== 0) wogUrl = 'https://' + wogUrl; + // 去掉末尾的斜杠防止双斜杠 + wogUrl = wogUrl.replace(/\/+$/, ''); + + // 拼接格式:代理网址/[IPv6]:端口 + finalLink = wogUrl + '/[' + res.wan6_ip + ']:' + curPort; + } else { + // 没有探针,走原本的裸连 IPv6 + finalLink = 'http://[' + res.wan6_ip + ']:' + curPort; + } + + var linkDiv = document.createElement('div'); + linkDiv.id = 'nw-v6-link'; + + linkDiv.style.cssText = 'width: 100%; text-align: left; margin-top: 4px; font-size: 13.5px; opacity: 0.9; display: block;'; + linkDiv.innerHTML = '' + v6Link + ''; + + var rowContainer = webPort.parentNode.parentNode; + rowContainer.insertAdjacentElement('afterend', linkDiv); + }); + } + } + // ================================================== + } + } + }); + + // 2. 点击开关事件 + webTog.addEventListener('change', function() { + if (this.checked) { + // ============================================== + // 【1】:开启时,联动检测 IPv6 总开关与保活开关,弹出确认窗 + // ============================================== + var needSyncV6 = (window._trueIpv6State !== '1'); + var wogEn = safeUciGet('netwiz', 'main', 'watchdog_enable', '0'); + var wogUrl = safeUciGet('netwiz', 'main', 'watchdog_url', '').trim(); // 读取探测源网址 + + // 只有在没开启探针,且【存在探测网址】时,才自动联动开启 + var needSyncWog = (wogEn !== '1' && wogUrl !== ''); + + // 没开启探针,且【没有探测网址】,则给出建议提示,不强制联动 + var suggestWog = (wogEn !== '1' && wogUrl === ''); + + // 准备弹窗文案 + var promptHtml = '
    ' + + (T['MSG_ABOUT_TO_ENABLE'] || 'You are about to enable the [%s].').replace('%s', T['MSG_WOG_LINK_WAN'] || 'WAN Access Web UI Channel'); + + // 如果有需要自动联动的开关,追加提示 + if (needSyncV6 || needSyncWog) { + promptHtml += '

    ' + + (T['MSG_WOG_LINKAGE'] || "To ensure the works correctly, the following dependent features have been automatically enabled:") + ''; + if (needSyncV6) { + promptHtml += '
    ✅ ' + + (T['MSG_WOG_LINK_V6'] || "IPv6 Master Switch") + '
    '; + } + if (needSyncWog) { + promptHtml += '
    ✅ ' + + (T['LBL_WATCHDOG_LINK'] || "IPv6 Watchdog") + '
    '; + } + } + + // 没有探测网址无法联动开启保活,给出黄色的建议提示 + if (suggestWog) { + promptHtml += '

    💡 ' + + (T['MSG_SUGGESTION'] || "Suggestion:") + '' + + '
    ' + + // 用 %s 占位 + (T['MSG_WOG_SUGGEST_MSG'] || "No target URL is configured for the Watchdog. For long-term stability, it is recommended to manually configure and enable it in %s later.").replace('%s', '' + (T['LBL_WATCHDOG_LINK'] || 'IPv6 Watchdog') + '') + + '
    '; + } + + promptHtml += '
    '; + + // 确认后的执行流程 + var proceedTurnOn = function() { + if (needSyncV6 || needSyncWog) { + openModal({ title: T['LBL_ADV_UTILS_TITLE'] || '⚙️ Advanced Utilities', msg: T['MSG_WRITING'] || 'Please wait...', spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + + // 开启 OpenWrt 的 IPv6 配置 + uci.load('dhcp').then(function() { + if (needSyncV6) { + uci.set('dhcp', 'lan', 'dhcpv6', 'server'); + uci.set('dhcp', 'lan', 'ra', 'server'); + uci.set('dhcp', 'lan', 'ndp', 'server'); + uci.set('dhcp', 'lan', 'ra_flags', ['managed-config', 'other-config']); + } + if (needSyncWog) { + uci.set('netwiz', 'main', 'watchdog_enable', '1'); + } + return uci.save(); + }).then(function() { + return uci.apply(); + }).then(function() { + // 同步界面与前端变量 + window._trueIpv6State = '1'; + var v6Toggle = document.getElementById('lan-ipv6-toggle'); + if (v6Toggle) v6Toggle.checked = true; + + // 执行保存外网端口和通道逻辑 + validateAndSavePort(webPort.value); + }); + } else { + // 没有联动项(或只是单纯提醒了 suggestWog),直接保存外网设定 + validateAndSavePort(webPort.value); + } + }; + + // 统一拦截:只要开启就弹窗 + openModal({ + title: '💡 ' + (T['MSG_SEC_NOTICE'] || 'Notice'), + msg: promptHtml, + okText: T['BTN_OK'] || 'OK', + cancelText: T['BTN_CANCEL'] || 'Cancel', + onOk: function() { + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + // 延迟执行避免 DOM 卡顿 + setTimeout(function() { proceedTurnOn(); }, 50); + }, + onCancel: function() { + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + // 用户点击取消,把开关回退到关闭状态 + if (webTog) webTog.checked = false; + } + }); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.zIndex = '100000'; + + } else { + // ============================================== + // 【2】:关闭时,弹窗,不关闭保活开关 + // ============================================== + if (isSaving) return; + + var isWogEn = safeUciGet('netwiz', 'main', 'watchdog_enable', '0'); + + // 将原有的保存、动画和兜底逻辑封装起来 + var proceedSave = function() { + isSaving = true; + openModal({ title: T['LBL_ADV_UTILS_TITLE'] || '⚙️ Advanced Utilities', msg: T['MSG_WRITING'] || 'Please wait...', spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + + // 调用和异常兜底刷逻辑 + callSetAdvSettings('', '0', '', '').then(function() { + setTimeout(function(){ window.location.reload(); }, 3500); + }).catch(function() { + isSaving = false; + window.location.reload(); // 异常兜底直接刷新 + }); + }; + + // 弹窗确认逻辑 + if (isWogEn === '1') { + openModal({ + title: '⚠️ ' + (T['MSG_SEC_NOTICE'] || 'Security Notice'), + msg: ` +
    + ${T['MSG_WOG_OFF_WAN']} +
    +
    + ❌ ${T['MSG_WOG_LINK_WAN']} +
    + `, + okText: T['BTN_OK'] || 'OK', + + // 关闭按钮 + cancelText: T['BTN_CANCEL'] || 'CANCEL', + + onOk: function() { + // 隐藏当前警告弹窗 + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + + // 稍微延迟 50 毫秒,等 DOM 销毁后再呼出正在保存的动画 + setTimeout(function() { proceedSave(); }, 50); + }, + + // 点击关闭时的回退逻辑 + onCancel: function() { + // 1. 先隐藏警告弹窗 + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + + // 2. 用户放弃关闭外网访问,将开关重新拨回“开启”状态 + if (webTog) { + webTog.checked = true; + } + } + }); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.zIndex = '100000'; + } else { + // 探针没开,直接走原有的二次弹窗或者直接保存 + openModal({ + title: '💡 ' + (T['MSG_SEC_NOTICE'] || 'Notice'), + msg: '
    即将关闭 ' + (T['MSG_WOG_LINK_WAN'] || "WAN Access Web UI Channel") + ' 。
    ', + okText: T['BTN_OK'] || 'OK', + cancelText: T['BTN_CANCEL'] || 'Cancel', + onOk: function() { + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + setTimeout(function() { proceedSave(); }, 50); + }, + onCancel: function() { + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + if (webTog) webTog.checked = true; + } + }); + } + } + }); + + // 3. 回车键主动触发 + webPort.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + this.blur(); + } + }); + + // 4. 输入框内容改变且失去焦点事件 + webPort.addEventListener('change', function() { + var self = this; + setTimeout(function() { + // 如果此时开关处于关闭状态,说明用户不想保存,直接退出 + if (!webTog.checked) return; + validateAndSavePort(self.value); + }, 200); + }); + } + // ==================================================== + + // 插件修复弹窗与逻辑 + if(container.querySelector('#link-repair-plugin')) { + container.querySelector('#link-repair-plugin').addEventListener('click', function() { + openModal({ title: T['M_REP_SCAN_TIT'] || 'Please wait', msg: T['M_REP_SCAN_MSG'] || 'Scanning for repairable plugins...', hideCancel: true, hideOk: true }); + + rpc.declare({ object: 'netwiz', method: 'get_repairable_configs', expect: { '': {} } })().then(function(res) { + if (res && res.configs && res.configs.length > 0) { + + // 1. 下拉菜单的选项 + var optText = ' (' + (T['M_REP_OPT'] || 'Factory Default') + ')'; + var opts = ''; + res.configs.forEach(function(pluginName) { + opts += ``; + }); + + // 2. 判断包管理器指令 + var curPkg = window.nwCurrentPkg || 'ipk'; + var cmdCode = (curPkg === 'apk') + ? (T['M_GUIDE_CMD_APK'] || 'apk add --allow-untrusted --force-reinstall --force-overwrite /tmp/XXXXXXX_your_name.apk') + : (T['M_GUIDE_CMD_OPKG'] || 'opkg install --force-reinstall --force-overwrite /tmp/XXXXXXX_your_name.ipk'); + + // 3. 读取tmpl骨架,将变数替换进去 + var modalHtml = myTmpl.repairModalHtml + .replace('{{M_REP_DESC}}', T['M_REP_DESC'] || '') + .replace('{{OPTIONS_HTML}}', opts) + .replace('{{M_GUIDE_TITLE}}', T['M_GUIDE_TITLE'] || '💡 Offline Reinstall Guide') + .replace('{{M_GUIDE_DESC}}', T['M_GUIDE_DESC'] || '') + .replace('{{CMD_CODE}}', cmdCode); + + // 4. 标题 + var titleText = T['M_REP_TIT'] || '🚑 Plugin Repair Toolkit'; + var titleWithX = `
    ${titleText}×
    `; + + openModal({ + title: titleWithX, + msg: modalHtml, // 传入模板 + okText: T['M_REP_OK'] || 'Repair Now', + cancelText: T['M_CLOSE'] || 'Close', + isDanger: true, + onOk: function() { + var selectedPlugin = document.getElementById('nw-repair-select'); + if (!selectedPlugin || !selectedPlugin.value) return; + var pName = selectedPlugin.value; + + var pTit = T['M_REP_PROC_TIT'] || 'Processing'; + var pMsg1 = T['M_REP_PROC_MSG1'] || 'Repairing and restarting'; + var pMsg2 = T['M_REP_SCAN_TIT'] || 'please wait'; + openModal({ title: pTit, msg: pMsg1 + ' ' + pName + ' ' + pMsg2, spin: true, hideCancel: true, hideOk: true }); + + rpc.declare({ object: 'netwiz', method: 'repair_config', params: ['plugin'] })(pName).then(function(r) { + var resCode = '-1'; + if (r !== null && typeof r === 'object' && r.result !== undefined) { + resCode = String(r.result); + } else if (r !== null && (typeof r === 'number' || typeof r === 'string')) { + resCode = String(r); + } + + if (resCode === '0') { + // ✅ 深度重裝成功 + var sTit = T['M_REP_SUCC_TIT'] || 'Repair Successful'; + var sMsg = T['M_REP_SUCC_MSG'] || 'has been successfully restored'; + openModal({ + title: sTit, + msg: pName + ' ' + sMsg, + okText: T['M_CLOSE'] || 'Close', + hideCancel: true, + // 点击关闭后,自动强制刷新网页 + onOk: function() { + // 弹出一个带有转圈动画的不可关闭弹窗,完美过渡这3秒钟 + openModal({ + title: T['M_REP_PROC_TIT'] || 'Processing', + msg: T['MSG_WAIT'] || 'Please wait...', + spin: true, + hideCancel: true, + hideOk: true + }); + // 3秒后强制重整网页 + setTimeout(function() { window.location.reload(); }, 3000); + } + }); + } else if (resCode === '1' || resCode === '2') { + // 拦截孤件 + var ghostTit = T['M_REP_GHOST_TIT'] || '💀 Ghost Plugin Deletion Warning'; + var ghostMsgStr = T['M_REP_GHOST_MSG'] || 'Executing this will uninstall the software and delete its config files. However, no installation package was found, so you will need to manually reinstall it later.'; + var ghostMsg = '
    ' + ghostMsgStr + '
    '; + + openModal({ + title: ghostTit, + msg: ghostMsg, + okText: T['BTN_ERADICATE'] || '🗑️ Eradicate Completely', + cancelText: T['BTN_CANCEL'] || 'Cancel', + isDanger: true, + onOk: function() { + openModal({ title: T['M_REP_PROC_TIT'] || 'Processing', msg: T['M_ERADICATE_PROC'] || 'Eradicating completely...', spin: true, hideCancel: true, hideOk: true }); + rpc.declare({ object: 'netwiz', method: 'eradicate_config', params: ['plugin'] })(pName).then(function(res2) { + var res2Code = '-1'; + if (res2 !== null && typeof res2 === 'object' && res2.result !== undefined) { + res2Code = String(res2.result); + } else if (res2 !== null && (typeof res2 === 'number' || typeof res2 === 'string')) { + res2Code = String(res2); + } + + if (res2Code === '0') { + openModal({ + title: '✅ ' + (T['M_REP_SUCC_TIT'] || 'Success'), + msg: pName + ' ' + (T['M_ERADICATE_SUCC'] || 'has been completely uninstalled and cleaned.'), + okText: T['M_CLOSE'] || 'Close', + hideCancel: true, + // 点击关闭后,自动强制刷新网页 + onOk: function() { + // 弹出一个带有转圈动画的不可关闭弹窗,完美过渡这3秒钟 + openModal({ + title: T['M_REP_PROC_TIT'] || 'Processing', + msg: T['MSG_WAIT'] || 'Please wait...', + spin: true, + hideCancel: true, + hideOk: true + }); + // 3秒后强制重整网页 + setTimeout(function() { window.location.reload(); }, 3000); + } + }); + } else { + openModal({ title: '❌ ' + (T['M_REP_FAIL_TIT'] || 'Failed'), msg: T['M_REP_FAIL_MSG'] || 'Unable to repair this plugin', okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }).catch(function(err) { + var errStr = String(err).toLowerCase(); + // 智能拦截超时报错:后台还在运行。 + if (errStr.indexOf('timed out') !== -1 || errStr.indexOf('timeout') !== -1) { + openModal({ + title: T['M_REP_PROC_TIT'] || 'Processing', + msg: (T['MSG_WAIT'] || 'Please wait...') + '

    ' + (T['M_HW_WAIT'] || 'Hardware execution takes longer, running in background...') + '', + spin: true, hideCancel: true, hideOk: true + }); + // 给底层硬件充足的 12 秒时间继续擦写数据,然后刷新 + setTimeout(function() { window.location.reload(); }, 12000); + } else { + openModal({ title: '❌ ' + (T['M_REP_ERR_TIT'] || 'System Error'), msg: (T['M_REP_ERR_MSG'] || 'Request timeout or error') + '

    ' + err, okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }); + } + }); + } else { + // ❌ 其他未知失敗 + var fTit = T['M_REP_FAIL_TIT'] || 'Repair Failed'; + var fMsg = T['M_REP_FAIL_MSG'] || 'Unable to repair this plugin'; + var dbgInfo = r !== null ? (typeof r === 'object' ? JSON.stringify(r) : String(r)) : 'No Response'; + openModal({ title: fTit, msg: fMsg + '

    Debug: ' + dbgInfo + '', okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }).catch(function(e) { + var errStr = String(e).toLowerCase(); + if (errStr.indexOf('timed out') !== -1 || errStr.indexOf('timeout') !== -1) { + openModal({ + title: T['M_REP_PROC_TIT'] || 'Processing', + msg: (T['MSG_WAIT'] || 'Please wait...') + '

    ' + (T['M_HW_WAIT'] || 'Hardware execution takes longer, running in background...') + '', + spin: true, hideCancel: true, hideOk: true + }); + setTimeout(function() { window.location.reload(); }, 12000); + } else { + var eTit = T['M_REP_ERR_TIT'] || 'System Error'; + var eMsg = T['M_REP_ERR_MSG'] || 'Request timeout or error'; + openModal({ title: eTit, msg: eMsg + '

    ' + e + '', okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }); + } + }); + } else { + var nTit = T['M_REP_NOTICE_TIT'] || 'Notice'; + var nMsg = T['M_REP_EMPTY_MSG'] || 'No repairable plugins found'; + openModal({ title: nTit, msg: nMsg, okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }).catch(function() { + var errTit = T['M_ERR'] || 'Error'; + var errMsg = T['M_REP_GET_ERR'] || 'Failed to get plugin list'; + openModal({ title: errTit, msg: errMsg, okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + }); + }); + } + + // 修改 Hosts + if(container.querySelector('#link-modify-hosts')) { + container.querySelector('#link-modify-hosts').addEventListener('click', function() { + callGetAdvSettings().then(function(res) { + var hostsArr = []; + try { hostsArr = (typeof res.hosts === 'string') ? JSON.parse(res.hosts) : (res.hosts || []); if (!Array.isArray(hostsArr)) hostsArr = []; } catch(e) { hostsArr = []; } + var esc = function(s) { return (s || '').toString().replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }; + + // 记录打开时的初始数据快照,用于“未修改防呆拦截” + var initialData = hostsArr.filter(function(item) { return item.ip.trim() !== '' && item.dom.trim() !== ''; }); + var initialJsonStr = JSON.stringify(initialData); + + var html = '
    ' + + '
    ' + (T['LBL_HOSTS_DESC'] || '💡 This feature forces specific domains to resolve to designated IPs. Commonly used for ad blocking or local NAS redirection.') + '
    ' + + '
    ' + + '
    ' + (T['LBL_HOSTS_VISUAL'] || '💡 Quick Add:') + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + ''; + + var isRawMode = false; + + showAdvModal((T['LBL_HOSTS_TITLE']), html, function(box) { + // 记忆触发保存时的UI状态 + var wasRaw = isRawMode; + + if (isRawMode) { var tBtn = document.getElementById('nw-hosts-raw-toggle'); if(tBtn) tBtn.click(); } + + // 纯文本转换失败,此时 isRawMode 会被恢复为 true。阻止弹窗关闭和保存 + if (isRawMode) return false; + + var finalData = hostsArr.filter(function(item) { return item.ip.trim() !== '' && item.dom.trim() !== ''; }); + var jsonStr = JSON.stringify(finalData); + + // 比对数据与初始数据快照 + if (jsonStr === initialJsonStr) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['MSG_NO_CHANGE'] || 'No changes have been made.', okText: T['M_CLOSE'] || 'Close' }); + + // 原本是在纯文字模式下触发的,瞬间将UI恢复回纯文字模式 + if (wasRaw) { + isRawMode = true; + document.getElementById('nw-hosts-visual-ui').style.display = 'none'; + document.getElementById('nw-hosts-raw-ui').style.display = 'block'; + var rawToggleBtn = document.getElementById('nw-hosts-raw-toggle'); + if (rawToggleBtn) rawToggleBtn.style.color = '#2563eb'; + } + + return false; // 终止后续的写入与重启 + } + + openModal({ title: (T['MSG_WRITING'] || 'Saving...'), msg: (T['MSG_WAIT'] || 'Please wait...'), spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + callSetAdvSettings('', '', '', jsonStr).then(function() { setTimeout(function(){ window.location.reload(); }, 1500); }); + }); + + var modalBox = document.getElementById('nw-adv-modal'); + if (modalBox) { + var innerBox = modalBox.querySelector('div'); + if (innerBox) { innerBox.style.width = '460px'; innerBox.style.maxWidth = '95%'; } + } + + var toggleBtn = document.getElementById('nw-hosts-raw-toggle'); + if (toggleBtn) { + toggleBtn.addEventListener('click', function(e) { + // 1:判断切换,是手动点,还是点击底部保存时程序自动触发 + var isIntentSave = (e && e.isTrusted === false); + + isRawMode = !isRawMode; + var visualUi = document.getElementById('nw-hosts-visual-ui'); + var rawUi = document.getElementById('nw-hosts-raw-ui'); + var rawText = document.getElementById('nw-hosts-raw-text'); + + if (isRawMode) { + // 1. UI 切 纯文本 + var textContent = ''; + hostsArr.forEach(function(item) { + var prefix = item.en ? '' : '# '; + var cmt = item.cmt ? ' \t# ' + item.cmt : ''; + textContent += prefix + item.ip + '\t' + item.dom + cmt + '\n'; + }); + rawText.value = textContent; + visualUi.style.display = 'none'; rawUi.style.display = 'block'; + } else { + // 2. 纯文本 切 UI (查重与格式拦截) + var lines = rawText.value.split('\n'); + var newArr = []; + var errorCount = 0; + + lines.forEach(function(line) { + line = line.trim(); if (!line) return; + var en = true; + if (line.charAt(0) === '#') { + var uncommented = line.substring(1).trim(); + if (/^[a-fA-F0-9\.:]+\s+[^\s#]+/.test(uncommented)) { en = false; line = uncommented; } else { return; } + } + var match = line.match(/^([a-fA-F0-9\.:]+)\s+([^\s<>"'\*#]+)(?:\s+#\s*(.*))?$/); + if (match) { + var parsedIp = match[1]; + var isIpv4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(parsedIp); + var isIpv6 = /^[a-fA-F0-9:]+:[a-fA-F0-9:]+$/.test(parsedIp); + if (isIpv4 || isIpv6) { + var isDup = newArr.some(function(x) { return x.ip === parsedIp && x.dom === match[2]; }); + if (isDup) { errorCount++; } else { newArr.push({ ip: parsedIp, dom: match[2], cmt: match[3] || '', en: en }); } + } else { errorCount++; } + } else { errorCount++; } + }); + + var processValidData = function() { + hostsArr = newArr; + renderHosts(); + rawUi.style.display = 'none'; + visualUi.style.display = 'block'; + var tBtn = document.getElementById('nw-hosts-raw-toggle'); + if (tBtn) tBtn.style.color = 'red'; + }; + + if (errorCount > 0) { + isRawMode = true; + var confirmMsg = (T['MSG_RAW_ERR_1'] || 'Found invalid or duplicate records:') + ' ' + + '' + errorCount + '

    ' + + (T['MSG_RAW_ERR_2'] || 'Click [OK] to automatically discard them and continue, or [Cancel] to manually fix them.'); + + openModal({ + title: T['M_INC_TIT'] || 'Notice', + msg: '
    ' + confirmMsg + '
    ', + okText: T['BTN_OK'] || 'OK', + cancelText: T['BTN_CANCEL'] || 'Cancel', + isDanger: true, + onCancel: function() { + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.display = 'none'; + }, + onOk: function() { + var cleanText = ''; + newArr.forEach(function(item) { + var prefix = item.en ? '' : '# '; + var cmt = item.cmt ? ' \t# ' + item.cmt : ''; + cleanText += prefix + item.ip + '\t' + item.dom + cmt + '\n'; + }); + rawText.value = cleanText; + isRawMode = false; + processValidData(); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.display = 'none'; + + // 在清洗完成后,自动保存 + if (isIntentSave) { + var mdlOk = document.getElementById('mdl-ok'); + if (mdlOk) { + // 100ms 后再保存 + setTimeout(function(){ mdlOk.click(); }, 100); + } + } + } + }); + return; + } + + processValidData(); + } + }); + } + + var renderHosts = function() { + var listHtml = ''; + var totalItems = hostsArr.length; + hostsArr.forEach(function(item, idx) { + var opacity = item.en ? '1' : '0.5'; + // 倒序排列公式:总长度减去当前索引 + var displayNum = totalItems - idx; + + listHtml += '
    '; + listHtml += '
    '; + listHtml += '
    ' + displayNum + '
    '; + listHtml += ''; + listHtml += ''; + listHtml += '
    '; + listHtml += '
    '; + listHtml += ''; + listHtml += ''; + listHtml += ''; + listHtml += '
    '; + }); + + if(hostsArr.length === 0) { listHtml = '
    ' + (T['TXT_HOSTS_EMPTY'] || 'No custom Hosts') + '
    '; } + var listContainer = document.getElementById('nw-hosts-list'); + listContainer.innerHTML = listHtml; + + listContainer.querySelectorAll('.h-en, .h-ip, .h-dom, .h-cmt').forEach(function(el) { + el.addEventListener('change', function() { + var idx = parseInt(this.getAttribute('data-idx'), 10); + if(this.classList.contains('h-en')) { hostsArr[idx].en = this.checked; document.getElementById('h-row-'+idx).style.opacity = this.checked ? '1' : '0.5'; } + + if(this.classList.contains('h-ip')) { + var ipVal = this.value.trim(); + var isIpv4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(ipVal); + var isIpv6 = /^[a-fA-F0-9:]+:[a-fA-F0-9:]+$/.test(ipVal); + if (ipVal !== '' && !isIpv4 && !isIpv6) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['M_FMT_IP'] || 'Invalid IP format!', okText: T['M_CLOSE'] || 'Close' }); + this.value = hostsArr[idx].ip; + return; + } + // 查重防呆:不得与其他行的IP+域名组合完全相同 + var isDupIp = hostsArr.some(function(x, i) { return i !== idx && x.ip === ipVal && x.dom === hostsArr[idx].dom; }); + if (isDupIp) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['MSG_HOSTS_DUP'] || 'Already exists!', okText: T['M_CLOSE'] || 'Close' }); + this.value = hostsArr[idx].ip; + return; + } + hostsArr[idx].ip = ipVal; + } + + if(this.classList.contains('h-dom')) { + var domVal = this.value.trim(); + if (/[\s<>"'\*]/.test(domVal)) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['M_FMT_DOMAIN'] || 'Invalid domain format!', okText: T['M_CLOSE'] || 'Close' }); + this.value = hostsArr[idx].dom; + return; + } + // 查重防呆:不得与其他行的IP+域名组合完全相同 + var isDupDom = hostsArr.some(function(x, i) { return i !== idx && x.ip === hostsArr[idx].ip && x.dom === domVal; }); + if (isDupDom) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['MSG_HOSTS_DUP'] || 'Already exists!', okText: T['M_CLOSE'] || 'Close' }); + this.value = hostsArr[idx].dom; + return; + } + hostsArr[idx].dom = domVal; + } + + if(this.classList.contains('h-cmt')) { + hostsArr[idx].cmt = this.value.trim().replace(/[<>"']/g, ''); + this.value = hostsArr[idx].cmt; + } + }); + }); + + listContainer.querySelectorAll('.h-del').forEach(function(btn) { + btn.addEventListener('click', function() { hostsArr.splice(parseInt(this.getAttribute('data-idx'), 10), 1); renderHosts(); }); + }); + }; + + renderHosts(); + + var addBtn = document.getElementById('nw-quick-add-btn'); + if (addBtn) { + addBtn.onclick = function() { + var ipInput = document.getElementById('nw-quick-ip'); + var domInput = document.getElementById('nw-quick-dom'); + var cmtInput = document.getElementById('nw-quick-cmt'); + var ipVal = ipInput.value.trim(); + var domVal = domInput.value.trim(); + + if (!ipVal || !domVal) { + // 替换 alert()弹窗 + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['MSG_HOSTS_REQ'] || 'IP and Domain cannot be empty!', okText: T['M_CLOSE'] || 'Close' }); + return; + } + var isIpv4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(ipVal); + var isIpv6 = /^[a-fA-F0-9:]+:[a-fA-F0-9:]+$/.test(ipVal); + if (!isIpv4 && !isIpv6) { + // 替换 alert()弹窗 + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['M_FMT_IP'] || 'Invalid IP format!', okText: T['M_CLOSE'] || 'Close' }); + return; + } + if (/[\s<>"'\*]/.test(domVal)) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['M_FMT_DOMAIN'] || 'Invalid domain format!', okText: T['M_CLOSE'] || 'Close' }); + return; + } + + var isSmartAdd = document.getElementById('nw-smart-add-cb').checked; + var cmtVal = cmtInput.value.trim().replace(/[<>"']/g, ''); + var addList = []; + + if (isSmartAdd) { + // 智能解析:去除域名前面的 www.,提取主域名 + var rootDom = domVal.replace(/^www\./i, ''); + var doms = [rootDom, 'www.' + rootDom]; // 组合 1:无 www,组合 2:有 www + var ips = [ipVal]; + + // 智能判断:如果用户填的是屏蔽/回环 IP,自动裂变出 IPv4 和 IPv6 两个版本 + if (ipVal === '127.0.0.1' || ipVal === '::1' || ipVal === '0.0.0.0' || ipVal === '::') { + ips = ['127.0.0.1', '::1']; + } + + // 交叉组合所有 IP 和域名 + ips.forEach(function(i) { + doms.forEach(function(d) { + addList.push({ ip: i, dom: d, cmt: cmtVal, en: true }); + }); + }); + } else { + // 未开启智能补全,只添加当前输入的一条 + addList.push({ ip: ipVal, dom: domVal, cmt: cmtVal, en: true }); + } + + var addedCount = 0; + // 倒序遍历插入,保证生成的 4 条规则在列表里看起来排版舒适且符合逻辑 + for (var i = addList.length - 1; i >= 0; i--) { + var item = addList[i]; + // 查重防呆:如果 4 条规则中有一部分已经存在了,自动跳过重复的,只加上缺失的 + var isDup = hostsArr.some(function(x) { return x.ip === item.ip && x.dom === item.dom; }); + if (!isDup) { + hostsArr.unshift(item); + addedCount++; + } + } + + // 如果计算完发现一条新规则都没加进去(全重复了) + if (addedCount === 0) { + openModal({ title: T['M_INC_TIT'] || 'Notice', msg: T['MSG_HOSTS_DUP'] || 'Already exists!', okText: T['M_CLOSE'] || 'Close' }); + return; + } + + renderHosts(); + ipInput.value = '127.0.0.1'; domInput.value = ''; cmtInput.value = ''; + document.getElementById('nw-hosts-list').scrollTop = 0; + }; + } + }); + }); + } + // ============================================================== + + // 防空指针,重定向 DOM 查找 + var oriQuery = container.querySelector.bind(container); + var oriQueryAll = container.querySelectorAll.bind(container); + container.querySelector = function(sel) { return oriQuery(sel) || document.querySelector(sel); }; + container.querySelectorAll = function(sel) { var r = oriQueryAll(sel); return (r && r.length > 0) ? r : document.querySelectorAll(sel); }; + // ============================================================== + + // ================== 加载库与二维码生成逻辑 ================== + if (typeof window.QRCode === 'undefined' && !window._qrCodeScriptAdded) { + window._qrCodeScriptAdded = true; + var script = document.createElement('script'); + script.src = L.resource('view/qrcode.min.js'); + script.onload = function() { if (typeof window._updateLiveQR === 'function') window._updateLiveQR(); }; + document.head.appendChild(script); + } + + function renderWiFiQR(containerId, ssid, pwd, enc) { + var box = document.getElementById(containerId); + if (!box || typeof window.QRCode === 'undefined') return; + box.innerHTML = ''; + var type = (!enc || enc === 'none') ? 'nopass' : (enc.indexOf('wep') !== -1 ? 'WEP' : 'WPA'); + var esc = function(s) { return String(s).replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/:/g, '\\:'); }; + var qrStr = 'WIFI:T:' + type + ';S:' + esc(ssid) + ';' + (type !== 'nopass' ? 'P:' + esc(pwd) + ';' : '') + ';'; + new QRCode(box, { text: qrStr, width: 140, height: 140, colorDark : "#0f172a", colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.L }); + } + + window._updateLiveQR = function() { + var qrBox = container.querySelector('#nw-live-qr-box'); + if (typeof window.QRCode === 'undefined') { if (qrBox) qrBox.style.display = 'none'; return; } + var smartOn = container.querySelector('#wifi-smart-toggle').checked && !window._isSingleChip; + var ssid = '', pwd = '', enc = '', bandName = '', isEn = false; + + if (smartOn) { + isEn = container.querySelector('#wifi-smart-en').checked; + ssid = container.querySelector('#wifi-smart-ssid').value; pwd = container.querySelector('#wifi-smart-key').value; + enc = container.querySelector('#wifi-smart-enc').value; bandName = T['LBL_SMART_CONN']; + } else { + var t2 = container.querySelector('#tab-2g').style.background; var t5 = container.querySelector('#tab-5g').style.background; + if (t2.indexOf('rgb(59, 130, 246)') !== -1 || t2 === '#3b82f6') { + isEn = container.querySelector('#wifi-2g-en').checked; ssid = container.querySelector('#wifi-2g-ssid').value; + pwd = container.querySelector('#wifi-2g-key').value; enc = container.querySelector('#wifi-2g-enc').value; bandName = T['TAB_2G'] || '2.4G'; + } else if (t5.indexOf('rgb(59, 130, 246)') !== -1 || t5 === '#3b82f6') { + isEn = container.querySelector('#wifi-5g-en').checked; ssid = container.querySelector('#wifi-5g-ssid').value; + pwd = container.querySelector('#wifi-5g-key').value; enc = container.querySelector('#wifi-5g-enc').value; bandName = T['TAB_5G'] || '5G'; + } else { + var e5g2 = container.querySelector('#wifi-5g2-en'); isEn = e5g2 ? e5g2.checked : false; + ssid = container.querySelector('#wifi-5g2-ssid') ? container.querySelector('#wifi-5g2-ssid').value : ''; + pwd = container.querySelector('#wifi-5g2-key') ? container.querySelector('#wifi-5g2-key').value : ''; + enc = container.querySelector('#wifi-5g2-enc') ? container.querySelector('#wifi-5g2-enc').value : 'psk2+ccmp'; bandName = '5G_Game'; + } + } + if (!isEn || !ssid) { qrBox.style.display = 'none'; return; } + qrBox.style.display = 'block'; + var bandEl = container.querySelector('#nw-live-qr-band'); if (bandEl) bandEl.innerText = '(' + bandName + ')'; + renderWiFiQR('nw-live-qr-code', ssid, pwd, enc); + }; + + // 鼠标跟随,无视底层 CSS 干扰 + var updateQRPos = function(e) { + var hoverBox = container.querySelector('#nw-hover-qr-box') || document.getElementById('nw-hover-qr-box'); + if (hoverBox && hoverBox.style.display === 'block') { + hoverBox.style.setProperty('position', 'fixed', 'important'); + hoverBox.style.setProperty('z-index', '999999', 'important'); + hoverBox.style.setProperty('transform', 'translate(-50%, -100%)', 'important'); + hoverBox.style.left = e.clientX + 'px'; + hoverBox.style.top = (e.clientY - 12) + 'px'; // 强制在鼠标正上方 12 像素处弹出 + } + }; + + container.addEventListener('mouseover', function(e) { + var target = e.target.closest('.nw-qr-hover'); + if (target && typeof window.QRCode !== 'undefined') { + renderWiFiQR('nw-hover-qr-code', target.getAttribute('data-ssid'), target.getAttribute('data-pwd'), target.getAttribute('data-enc')); + var hoverBox = container.querySelector('#nw-hover-qr-box') || document.getElementById('nw-hover-qr-box'); + hoverBox.style.display = 'block'; + updateQRPos(e); + } + }); + + container.addEventListener('mousemove', function(e) { + if (e.target.closest('.nw-qr-hover')) updateQRPos(e); + }); + + // 鼠标移出时隐藏 + container.addEventListener('mouseout', function(e) { + if (e.target.closest('.nw-qr-hover')) { + var hoverBox = container.querySelector('#nw-hover-qr-box') || document.getElementById('nw-hover-qr-box'); + if(hoverBox) hoverBox.style.display = 'none'; + } + }); + + // 移动端防粘滞逻辑,滑动屏幕或点击空白处时自动隐藏 + window.addEventListener('scroll', function() { + var hoverBox = document.getElementById('nw-hover-qr-box'); + if (hoverBox && hoverBox.style.display === 'block') { + hoverBox.style.display = 'none'; + } + }, { passive: true }); + + + // 事件委托 + container.addEventListener('input', function(e) { + if (e.target && e.target.id && e.target.id.indexOf('wifi-') !== -1) window._updateLiveQR(); + + // 记录 PPPoE 帐号密码 + if (e.target && (e.target.id === 'pppoe-user' || e.target.id === 'wiz-pppoe-user')) sessionStorage.setItem('nw_pppoe_user', e.target.value); + if (e.target && (e.target.id === 'pppoe-pass' || e.target.id === 'wiz-pppoe-pass')) sessionStorage.setItem('nw_pppoe_pass', e.target.value); + }); + container.addEventListener('change', function(e) { + if (e.target && e.target.id && e.target.id.indexOf('wifi-') !== -1) window._updateLiveQR(); + }); + container.querySelector('#tab-2g').addEventListener('click', function() { setTimeout(window._updateLiveQR, 50); }); + container.querySelector('#tab-5g').addEventListener('click', function() { setTimeout(window._updateLiveQR, 50); }); + if (container.querySelector('#tab-5g2')) container.querySelector('#tab-5g2').addEventListener('click', function() { setTimeout(window._updateLiveQR, 50); }); + container.querySelector('#wifi-smart-toggle').addEventListener('change', function() { setTimeout(window._updateLiveQR, 50); }); + // ================== 结束:二维码生成逻辑 ================== + + container.addEventListener('click', function(e) { + var btnGotoDev = e.target.closest('#btn-goto-dev'); + if (btnGotoDev) { + e.preventDefault(); + var wrap = document.querySelector('.nw-wrapper'); + if (wrap) wrap.classList.add('page-leaving'); + setTimeout(function() { window.location.href = btnGotoDev.href; }, 350); + } + }); + + // ================== 安全XSS 字符转义 ================== + var safeGetLocal = function(k) { try { return window.localStorage.getItem(k); } catch(e) { return null; } }; + var safeSetLocal = function(k, v) { try { window.localStorage.setItem(k, v); } catch(e) {} }; + var safeRemoveLocal = function(k) { try { window.localStorage.removeItem(k); } catch(e) {} }; + // ============================================================== + var escapeHTML = function(str) { + if (!str) return ''; + return String(str).replace(/[&<>'"]/g, function(tag) { + var charsToReplace = { '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }; + return charsToReplace[tag] || tag; + }); + }; + // ============================================================== + + // ================== 向导状态后端静默保存 ================== + var silentSaveWizardState = function(state) { + if (window._currentWizState === String(state)) return Promise.resolve(); + window._currentWizState = String(state); + return callNetSetup('set_wizard', String(state), '', '', '', '', '').catch(function(e){}); + }; + // ============================================================== + + // 所有弹窗移出被主题污染的容器,挂载到最外层 body + var globalModal = container.querySelector('#nw-global-modal'); + if (globalModal) document.body.appendChild(globalModal); + + var wizModalInner = container.querySelector('#nw-wizard-modal'); + if (wizModalInner) document.body.appendChild(wizModalInner); + + var wispModal = container.querySelector('#wisp-scan-modal'); + if (wispModal) document.body.appendChild(wispModal); + + var step1 = container.querySelector('#step-1'), step2 = container.querySelector('#step-2'), step3 = container.querySelector('#step-3'); + var confirmText = container.querySelector('#confirm-mode-text'), modeTextEl = container.querySelector('#current-mode-text'); + var selectedMode = ''; + window._isSingleChip = false; + + // ===== 快速开机向导流 ===== + var wizModal = wizModalInner || document.getElementById('nw-wizard-modal'); + + var wizUserInp = container.querySelector('#wiz-pppoe-user'); + var wizUserMir = container.querySelector('#wiz-user-mirror'); + if (wizUserInp && wizUserMir) { + var syncMir = function() { + if (wizUserInp.value.length > 18) { + wizUserMir.style.display = 'block'; + wizUserMir.textContent = wizUserInp.value; + } else { + wizUserMir.style.display = 'none'; + wizUserMir.textContent = wizUserInp.value; + } + }; + wizUserInp.addEventListener('input', syncMir); + wizUserInp.addEventListener('change', syncMir); + + setInterval(function(){ + if (wizUserInp.value !== wizUserMir.textContent) syncMir(); + }, 800); + } + var wArea1 = container.querySelector('#wiz-step-1-area'), wArea2 = container.querySelector('#wiz-step-2-area'), wArea3 = container.querySelector('#wiz-step-3-area'), wArea4 = container.querySelector('#wiz-step-4-area'); + var wBtnPrev = container.querySelector('#wiz-btn-prev'), wBtnNext = container.querySelector('#wiz-btn-next'), wBtnApply = container.querySelector('#wiz-btn-apply'); + var wizHideCb = container.querySelector('#wiz-hide-checkbox'); + var currentWizStep = 1; + + // 向导高亮逻辑保持不变 + var updateWizSteps = function(step) { + var dots = container.querySelectorAll('.nw-step-dot'); + dots.forEach(function(d, i) { + if (i + 1 === step) { + d.style.background = '#ffffff'; d.style.color = '#5e72e4'; d.style.border = 'none'; d.style.transform = 'scale(1.2)'; d.style.boxShadow = '0 0 8px rgba(255,255,255,0.6)'; d.style.opacity = '1'; + } else if (i + 1 < step) { + d.style.background = 'rgba(255,255,255,0.25)'; d.style.color = '#ffffff'; d.style.border = 'none'; d.style.transform = 'scale(1)'; d.style.boxShadow = 'none'; d.style.opacity = '1'; + } else { + d.style.background = 'transparent'; d.style.color = 'rgba(255,255,255,0.6)'; d.style.border = '1px solid rgba(255,255,255,0.4)'; d.style.transform = 'scale(1)'; d.style.boxShadow = 'none'; d.style.opacity = '0.8'; + } + }); + var lines = container.querySelectorAll('.nw-step-line'); + lines.forEach(function(l, i) { + l.style.setProperty('background', 'transparent', 'important'); + l.style.color = (i + 1 < step) ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.3)'; + }); + }; + updateWizSteps(currentWizStep); + + // ================== 跳过与关闭逻辑 ================== + var handleWizardExit = function(action) { + var hideCb = container.querySelector('#wiz-hide-checkbox'); + + if (action === 'close') { + if (hideCb && hideCb.checked) { + safeSetLocal('nw_wizard_never_show', '1'); + } else { + safeRemoveLocal('nw_wizard_never_show'); + } + } else if (action === 'skip') { + // 跳过清除“不再提示”的缓存 + safeRemoveLocal('nw_wizard_never_show'); + } + + var isFirstRun = (window._realIsConfigured !== '1'); + + silentSaveWizardState('1').then(function() { + executeExitNav(isFirstRun, action); + }).catch(function() { + executeExitNav(isFirstRun, action); + }); + }; + + var executeExitNav = function(isFirstRun, action) { + var wizModal = container.querySelector('#nw-wizard-modal'); + + if (isFirstRun) { + + if (wizModal) wizModal.style.display = 'none'; + openModal({ + title: T['WIZ_SKIP_TITLE'] || 'Unlock', + msg: '
    ' + (T['WIZ_SKIP_MSG'] || 'Entering official dashboard...') + '
    ', + spin: true + }); + setTimeout(function() { + window.location.replace('http://' + window.location.hostname + '/cgi-bin/luci/'); + }, 500); + } else { + + if (wizModal) wizModal.style.display = 'none'; + } + }; + + // 分別綁定不同的 action + container.querySelector('#wiz-modal-close').addEventListener('click', function() { handleWizardExit('close'); }); + container.querySelector('#wiz-btn-skip').addEventListener('click', function() { handleWizardExit('skip'); }); + // ========================================================== + + var btnReopenWiz = container.querySelector('#btn-reopen-wizard'); + if (btnReopenWiz) { + btnReopenWiz.addEventListener('click', function() { + silentSaveWizardState('1'); + currentWizStep = 1; + updateWizSteps(1); + wArea1.style.display = 'block'; + wArea2.style.display = 'none'; + wArea3.style.display = 'none'; + wArea4.style.display = 'none'; + wBtnPrev.style.display = 'none'; + wBtnNext.style.display = 'block'; + wBtnApply.style.display = 'none'; + + if (wizHideCb) wizHideCb.checked = true; + var skipWifiCb = container.querySelector('#wiz-skip-wifi-checkbox'); + if (skipWifiCb) { + skipWifiCb.checked = (window._hasRealWifi === false) ? true : false; + skipWifiCb.dispatchEvent(new Event('change')); + } + wizModal.style.display = 'flex'; + }); + } + + container.querySelectorAll('input[name="wiz_wan_type"]').forEach(function(r) { + r.addEventListener('change', function() { + container.querySelector('#wiz-pppoe-fields').style.display = (this.value === 'pppoe') ? 'block' : 'none'; + + // 向导中切换回PPPoE时,强制恢复 + if (this.value === 'pppoe') { + var savedU = sessionStorage.getItem('nw_pppoe_user'); + var savedP = sessionStorage.getItem('nw_pppoe_pass'); + if (savedU !== null && container.querySelector('#wiz-pppoe-user')) container.querySelector('#wiz-pppoe-user').value = savedU; + if (savedP !== null && container.querySelector('#wiz-pppoe-pass')) container.querySelector('#wiz-pppoe-pass').value = savedP; + } + }); + }); + + var skipWifiCb = container.querySelector('#wiz-skip-wifi-checkbox'); + var wifiInputArea = container.querySelector('#wiz-wifi-input-area'); + if (skipWifiCb && wifiInputArea) { + skipWifiCb.addEventListener('change', function() { + wifiInputArea.style.opacity = this.checked ? '0.3' : '1'; + wifiInputArea.style.pointerEvents = this.checked ? 'none' : 'auto'; + }); + } + + // 密码跳过联动已移除(不再需要打勾,直接留空即可跳过) + // ================== 4 步向导 ================== + wBtnNext.addEventListener('click', function() { + if (currentWizStep === 1) { + // 第一步:密码留空则跳过,填写则必须一致 + var p1 = container.querySelector('#nw-admin-pwd').value; + var p2 = container.querySelector('#nw-admin-pwd-confirm').value; + + if (p1 !== p2) { + openModal({ + title: T['M_INC_TIT'] || 'Notice', + msg: T['M_PWD_MISMATCH'] || 'Passwords do not match, please try again!', + okText: T['M_CLOSE'] || 'Close' + }); + return; + } + + // 只要密码一致(包含两者都为空),就直接进入下一步 + wArea1.style.display = 'none'; wArea2.style.display = 'block'; + wBtnPrev.style.display = 'block'; currentWizStep = 2; updateWizSteps(2); + } else if (currentWizStep === 2) { + // 第二步 + var pppoeBtn = container.querySelector('#wiz-pppoe-submit'); + if (pppoeBtn) pppoeBtn.click(); + + var wType = container.querySelector('input[name="wiz_wan_type"]:checked').value; + if (wType === 'pppoe' && (!container.querySelector('#wiz-pppoe-user').value.replace(/[\r\n\s]+/g, '') || !container.querySelector('#wiz-pppoe-pass').value.trim())) { + openModal({ title: T['M_INC_TIT'], msg: T['M_INC_PPPOE'], okText: T['M_CLOSE'] }); + return; + } + + if (wType === 'dhcp') { + var sysWanIp = window._liveWanIp || ''; + var currentLanIp = safeUciGet('network', 'lan', 'ipaddr', window.location.hostname).split('/')[0]; + if (sysWanIp && currentLanIp && isSameSubnet(sysWanIp, currentLanIp)) { + var newSafeIp = getSafeRouterIp(sysWanIp); + openModal({ + title: T['M_CFLT_INTERCEPT_TIT'], + msg: T['M_CFLT_WIZ_MSG'].replace('{wan_ip}', sysWanIp).replace('{lan_ip}', currentLanIp).replace('{safe_ip}', newSafeIp), + okText: T['BTN_AUTO_EVADE'], + cancelText: T['BTN_EDIT_MYSELF'], // 手动修改(取消)按钮 + isDanger: true, + onOk: function() { + container.querySelector('#lan-ip').value = newSafeIp; + selectedMode = 'lan'; + container.querySelector('#nw-global-modal').style.display = 'none'; + container.querySelector('#nw-wizard-modal').style.display = 'none'; + container.querySelector('#btn-next-2').click(); + } + }); + return; + } + } + + wArea2.style.display = 'none'; wArea3.style.display = 'block'; + currentWizStep = 3; updateWizSteps(3); + } else if (currentWizStep === 3) { + // 第三步 + var isSkipWifi = skipWifiCb ? skipWifiCb.checked : false; + var ssid = container.querySelector('#wiz-wifi-ssid').value.trim(); + var key = container.querySelector('#wiz-wifi-key').value; + + var proceedToStep4 = function() { + var wType2 = container.querySelector('input[name="wiz_wan_type"]:checked').value; + var htmlConfirm = "
    "; + + if (wType2 === 'pppoe') { + var pppoeUser = container.querySelector('#wiz-pppoe-user').value.replace(/[\r\n\s]+/g, ''); + var pppoePass = container.querySelector('#wiz-pppoe-pass').value; + htmlConfirm += "
    WAN: " + T['MODE_PPPOE_TITLE'] + "
    "; + htmlConfirm += "
    "+ T['M_ACCT'] +": " + escapeHTML(pppoeUser) + "
    "; + htmlConfirm += "
    "+ T['M_PWD'] +": " + escapeHTML(pppoePass) + "
    "; + } else { + htmlConfirm += "
    WAN: " + T['OPT_DHCP'] + "
    "; + } + + if (isSkipWifi) { + htmlConfirm += "
    Wi-Fi: " + T['TXT_NOT_CONFIGURED'] + "
    "; + } else { + htmlConfirm += "
    Wi-Fi: " + (ssid ? escapeHTML(ssid) : "" + T['TXT_UNSET'] + "") + "
    "; + htmlConfirm += "
    "+ T['M_PWD'] +": " + (key ? escapeHTML(key) : "" + T['TXT_NO_PWD_OPEN'] + "") + "
    "; + } + + htmlConfirm += "
    "; + container.querySelector('#wiz-confirm-text').innerHTML = htmlConfirm; + + wArea3.style.display = 'none'; wArea4.style.display = 'block'; + wBtnNext.style.display = 'none'; wBtnApply.style.display = 'block'; + currentWizStep = 4; updateWizSteps(4); + }; + + if (!isSkipWifi) { + if (!ssid) { openModal({ title: T['M_INC_TIT'], msg: T['M_INC_WIFI'], okText: T['M_CLOSE'] }); return; } + if (key && key.length < 8) { openModal({ title: T['M_FMT_TIT'], msg: T['M_PWD_SHORT'], okText: T['M_CLOSE'] }); return; } + if (key.length === 0) { + openModal({ + title: T['M_OPEN_WARN_TIT'] || '⚠️ No Password', + msg: T['M_OPEN_WARN_MSG'] || 'Setting up an open Wi-Fi without a password. Continue?', + cancelText: T['M_CLOSE'], + okText: T['BTN_NEXT'] || 'Continue', + isDanger: true, + onCancel: function() { container.querySelector('#nw-global-modal').style.display = 'none'; }, + onOk: function() { + container.querySelector('#nw-global-modal').style.display = 'none'; + proceedToStep4(); + } + }); + return; + } + } + proceedToStep4(); + } + }); + + wBtnPrev.addEventListener('click', function() { + if (currentWizStep === 2) { + wArea2.style.display = 'none'; wArea1.style.display = 'block'; wBtnPrev.style.display = 'none'; currentWizStep = 1; updateWizSteps(1); + } else if (currentWizStep === 3) { + wArea3.style.display = 'none'; wArea2.style.display = 'block'; currentWizStep = 2; updateWizSteps(2); + } else if (currentWizStep === 4) { + wArea4.style.display = 'none'; wArea3.style.display = 'block'; wBtnApply.style.display = 'none'; wBtnNext.style.display = 'block'; currentWizStep = 3; updateWizSteps(3); + } + }); + + // 6. 一键合并提交 + wBtnApply.addEventListener('click', function() { + var adminPwdEl = document.getElementById('nw-admin-pwd'); + var adminPwd = adminPwdEl ? adminPwdEl.value.trim() : ""; + + var wType = container.querySelector('input[name="wiz_wan_type"]:checked').value; + var isSkipWifi = skipWifiCb ? skipWifiCb.checked : false; + + if (typeof window._trueIpv6State === 'undefined' || window._trueIpv6State === null) { + openModal({ title: T['M_SYS_ERR'] || 'System Error', msg: T['MSG_SYS_NOT_READY'], okText: T['M_CLOSE'] }); + return; + } + var keepIpv6 = window._trueIpv6State; + + wizModal.style.display = 'none'; + openModal({ title: T['WIZ_TITLE'] || 'Configuring', msg: '
    ' + T['MSG_WRITING'] + '
    ', spin: true }); + + // 网络配置的核心提取为一个函数,分流调用 + var doNetSetupConfig = function() { + // 1. 处理“不再提示”状态 + var wizHideCb = container.querySelector('#wiz-hide-checkbox'); + if (wizHideCb && wizHideCb.checked) { + safeSetLocal('nw_wizard_never_show', '1'); + } else { + safeRemoveLocal('nw_wizard_never_show'); + } + + // 2. 提交配置,底层必须强制写入 '1' 永久解锁 CGI 拦截 + silentSaveWizardState('0').then(function() { + var applyPromise; + if (isSkipWifi) { + if (wType === 'pppoe') { + var u = container.querySelector('#wiz-pppoe-user').value.replace(/[\r\n\s]+/g, ''); + var p = container.querySelector('#wiz-pppoe-pass').value; + applyPromise = callNetSetup('pppoe', u, p, '', '', '1', keepIpv6); + } else { + applyPromise = callNetSetup('wan_dhcp', '', '', '', '', '1', keepIpv6); + } + } else { + var arg1Obj = { wan_type: wType }; + if (wType === 'pppoe') { + arg1Obj.user = container.querySelector('#wiz-pppoe-user').value.replace(/[\r\n\s]+/g, ''); + arg1Obj.pass = container.querySelector('#wiz-pppoe-pass').value; + } + + var ssid = container.querySelector('#wiz-wifi-ssid').value.trim(); + var key = container.querySelector('#wiz-wifi-key').value; + var enc = (key.length === 0) ? 'none' : 'psk2+ccmp'; + + var arg2Obj = {}; + if (window._isSingleChip) { + arg2Obj = { smart: "true", merged: { enabled: "1", ssid: ssid, key: key, encryption: enc, hidden: "0", roaming: "0" } }; + } else { + arg2Obj = { + smart: "false", + radio_2g: { enabled: "1", ssid: ssid, key: key, encryption: enc, hidden: "0", roaming: "0", mode: "auto", channel: "auto", bandwidth: "auto" }, + radio_5g: { enabled: "1", ssid: ssid, key: key, encryption: enc, hidden: "0", roaming: "1", mode: "auto", channel: "auto", bandwidth: "auto" } + }; + } + var arg2Str = JSON.stringify(arg2Obj); + applyPromise = callNetSetup('wizard', JSON.stringify(arg1Obj), arg2Str, '', '', '1', keepIpv6); + } + + applyPromise.then(function() { + var sec = 0, h = window.location.hostname; + var checkSameTimer = setInterval(function() { + sec += 3; + document.getElementById('nw-global-msg').innerHTML = '
    ' + T['MSG_WAIT_NET'].replace('{sec}', sec) + '
    '; + + if (sec <= 9) return; + + fetchProbe('http://' + h + '/cgi-bin/luci/?v=' + Date.now(), 2000).then(function() { + clearInterval(checkSameTimer); + + // ================== 自动登录官方后台 ================== + document.getElementById('nw-global-msg').innerHTML = '
    ' + T['MSG_SETUP_DONE'] + '
    '; + + if (adminPwd) { + // 创建隐藏表单,登录官方页面 + var form = document.createElement('form'); + form.method = 'POST'; + // 直接 POST 到目标地址,LuCI 鉴权后会停留在 NetWiz 面板 + form.action = 'http://' + h + '/cgi-bin/luci/admin/netwiz'; + form.style.display = 'none'; + + var u = document.createElement('input'); + u.type = 'hidden'; u.name = 'luci_username'; u.value = 'root'; // 默认账号 root + form.appendChild(u); + + var p = document.createElement('input'); + p.type = 'hidden'; p.name = 'luci_password'; p.value = adminPwd; // 向导里的密码 + form.appendChild(p); + + document.body.appendChild(form); + form.submit(); + } else { + // 没改密码,提取当前安全的 stok 令牌并跳转 + var match = window.location.pathname.match(/;stok=[a-zA-Z0-9]+/); + var stok = match ? match[0] + '/' : ''; + window.location.replace('http://' + h + '/cgi-bin/luci/' + stok + 'admin/netwiz'); + } + // ============================================================== + + }).catch(function() {}); + }, 3000); + }).catch(function(err) { + openModal({ title: T['M_SYS_ERR'], msg: (T['M_ERR_WIZ_FAILED'] || 'Wizard execution failed') + ': ' + err, okText: T['M_CLOSE'] }); + }); + }); + }; + + // ================== 判断密码是否需要修改 ================== + if (adminPwd) { + // 填新密码,修改密码,再配网 + callSetPassword(adminPwd).then(function() { + doNetSetupConfig(); + }).catch(function(err) { + alert(T['MSG_PWD_FAIL'] + err); + window.location.reload(); + }); + } else { + // 留空,直接跳过修改密码,执行网络配置 + doNetSetupConfig(); + } + // ============================================================== + }); + + // ===== 动画滚动 ===== + var smoothScrollToTop = function(duration) { + var scroller = document.querySelector('#maincontent') || document.querySelector('.main-right') || document.scrollingElement || document.documentElement; + var start = scroller.scrollTop; + if (start === 0 && window.pageYOffset > 0) { scroller = window; start = window.pageYOffset; } + if (start <= 0) return; // 已经在顶部就不滚了 + + var startTime = null; + // 先加速后减速 (Ease-In-Out) + var easeInOutQuad = function (t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t + b; + t--; + return -c/2 * (t*(t-2) - 1) + b; + }; + + var animateScroll = function(timestamp) { + if (!startTime) startTime = timestamp; + var progress = timestamp - startTime; + var nextStep = easeInOutQuad(progress, start, -start, duration); + + if (scroller === window) window.scrollTo(0, nextStep); + else scroller.scrollTop = nextStep; + + if (progress < duration) window.requestAnimationFrame(animateScroll); + else { + if (scroller === window) window.scrollTo(0, 0); + else scroller.scrollTop = 0; + } + }; + window.requestAnimationFrame(animateScroll); + }; + // ======================================================== + + // ===== 提取当前状态快照 ===== + function getWifiSnapshot() { + var sT = container.querySelector('#wifi-smart-toggle').checked; + var snap = { + sT: sT, + lB: container.querySelector('#legacy-b-toggle').checked, + wt: container.querySelector('#wisp-toggle') ? container.querySelector('#wisp-toggle').checked : false, + ws: container.querySelector('#wisp-target-ssid') ? container.querySelector('#wisp-target-ssid').value : '', + wk: container.querySelector('#wisp-target-key') ? container.querySelector('#wisp-target-key').value : '', + we: container.querySelector('#wisp-target-enc') ? container.querySelector('#wisp-target-enc').value : '', + wd: container.querySelector('#wisp-target-device') ? container.querySelector('#wisp-target-device').value : '', + wb: container.querySelector('#wisp-target-bssid') ? container.querySelector('#wisp-target-bssid').value : '' + }; + if (sT) { + // 如果是多频合一,只记录合一面板的数据 + snap.es = container.querySelector('#wifi-smart-en').checked; + snap.ss = container.querySelector('#wifi-smart-ssid').value; + snap.ks = container.querySelector('#wifi-smart-key').value; + snap.ecs = container.querySelector('#wifi-smart-enc').value; + snap.hs = container.querySelector('#wifi-smart-hidden').checked; + snap.rs = container.querySelector('#wifi-smart-roaming') ? container.querySelector('#wifi-smart-roaming').checked : false; + } else { + // 如果是分开模式,记录独立的面板数据 + snap.e2 = container.querySelector('#wifi-2g-en').checked; + snap.s2 = container.querySelector('#wifi-2g-ssid').value; + snap.k2 = container.querySelector('#wifi-2g-key').value; + snap.ec2 = container.querySelector('#wifi-2g-enc').value; + snap.h2 = container.querySelector('#wifi-2g-hidden').checked; + snap.m2 = container.querySelector('#wifi-2g-mode').value; + snap.c2 = container.querySelector('#wifi-2g-chan').value; + snap.b2 = container.querySelector('#wifi-2g-bw').value; + snap.r2 = container.querySelector('#wifi-2g-roaming') ? container.querySelector('#wifi-2g-roaming').checked : false; + + snap.e5 = container.querySelector('#wifi-5g-en').checked; + snap.s5 = container.querySelector('#wifi-5g-ssid').value; + snap.k5 = container.querySelector('#wifi-5g-key').value; + snap.ec5 = container.querySelector('#wifi-5g-enc').value; + snap.h5 = container.querySelector('#wifi-5g-hidden').checked; + snap.m5 = container.querySelector('#wifi-5g-mode').value; + snap.c5 = container.querySelector('#wifi-5g-chan').value; + snap.b5 = container.querySelector('#wifi-5g-bw').value; + snap.r5 = container.querySelector('#wifi-5g-roaming') ? container.querySelector('#wifi-5g-roaming').checked : false; + var en5g2El = container.querySelector('#wifi-5g2-en'); + if (en5g2El) { + snap.e5g2 = en5g2El.checked; + snap.s5g2 = container.querySelector('#wifi-5g2-ssid') ? container.querySelector('#wifi-5g2-ssid').value : ''; + snap.k5g2 = container.querySelector('#wifi-5g2-key') ? container.querySelector('#wifi-5g2-key').value : ''; + } + } + return JSON.stringify(snap); + } + // ======================================================== + + function safePromise(p, f) { return new Promise(function(r) { var t = setTimeout(function() { r(f); }, 8000); if (!p || !p.then) { clearTimeout(t); return r(f); } p.then(function(res) { clearTimeout(t); r(res); }).catch(function() { clearTimeout(t); r(f); }); }); } + function safeUciGet(c, s, o, d) { try { var v = uci.get(c, s, o); return (v === null || v === undefined) ? d : String(v).trim(); } catch(e) { return d; } } + + // SSID 后缀转换 + // 去除 _2.4G, -5G, 2.4G, 5G, 甚至 2.4, 5 等冗余后缀 + function cleanSsidSuffix(ssid) { + if (!ssid) return ''; + // 兼容 5G_Game 后缀剥离 + return ssid.replace(/[_\-\s]?(2\.4|5)[gG]?(_Game)?$/i, ''); + } + + // 生成纯净的目标名称 + function smartConvertSsid(ssid, targetBand) { + var base = cleanSsidSuffix(ssid); + if (!base) base = 'OpenWrt'; + if (targetBand === '2g') return base + '_2.4G'; + if (targetBand === '5g2') return base + '_5G_Game'; // 第三个芯片的专属后缀 + return base + '_5G'; + } + + Promise.all([ + safePromise(callNetCheckWifi(), { has_wifi: false }), + safePromise(callSystemBoard(), {}), + safePromise(uci.load('netwiz'), null), // 恢复加载 netwiz 配置 + safePromise(uci.load('wireless'), null) // 加载底层 wireless 配置 + ]).then(function(results) { + var wifiRes = results[0]; + var boardRes = results[1] || {}; + var modelName = (boardRes.model || '').toLowerCase(); + + // 加载缓存架构与包管理器,用于后续的文件名秒级校验 + if (wifiRes) { + window.nwCurrentPkg = wifiRes.pkg_type || 'ipk'; + window.nwCurrentArch = wifiRes.sys_arch || ''; + } + + var uciWifiDevs = []; + try { uciWifiDevs = uci.sections('wireless', 'wifi-device') || []; } catch(e) {} + + var hasWifi = (wifiRes === true || (typeof wifiRes === 'object' && wifiRes && wifiRes.has_wifi === true) || uciWifiDevs.length > 0); + window._hasRealWifi = hasWifi; // 真实的硬件状态 + + // 处理主界面的 Wi-Fi 卡片显示与隐藏 + if (hasWifi) { + var wifiCard = container.querySelector('#card-wifi'); + if (wifiCard) wifiCard.style.display = 'flex'; + } else { + console.warn("[Netwiz] 警告: 未检测到 Wi-Fi 硬件,已彻底隐藏 Wi-Fi 配置卡片。"); + var wifiCard = container.querySelector('#card-wifi'); + if (wifiCard) wifiCard.style.setProperty('display', 'none', 'important'); + + // 没有 Wi-Fi 时,向导第二步自动锁死“跳过”并隐藏密码框 + var skipWifiCb = container.querySelector('#wiz-skip-wifi-checkbox'); + var wifiInputArea = container.querySelector('#wiz-wifi-input-area'); + if (skipWifiCb) { + skipWifiCb.checked = true; // 强行勾选跳过 + var cbWrap = skipWifiCb.closest('.nw-wiz-cb-wrap'); + if (cbWrap) { + cbWrap.style.setProperty('display', 'none', 'important'); // 隐藏复选框本身 + if (cbWrap.parentElement) { + cbWrap.parentElement.style.setProperty('display', 'none', 'important'); + } + } + } + if (wifiInputArea) { + wifiInputArea.style.setProperty('display', 'none', 'important'); // 隐藏密码框 + var wArea3 = container.querySelector('#wiz-step-3-area'); + if (wArea3 && !container.querySelector('#nw-no-wifi-tip')) { + var tip = document.createElement('div'); + tip.id = 'nw-no-wifi-tip'; + tip.innerHTML = '
    ' + T['MSG_NO_WIFI_TIP'] + '
    '; + wArea3.insertBefore(tip, wifiInputArea); + } + } + } + + // ================== 弹出向导 ================== + if (typeof uci !== 'undefined') { + // network 一起加载,用于探测底层真实拨号状态 + Promise.all([uci.load('netwiz'), uci.load('network')]).then(function() { + var isConfigured = safeUciGet('netwiz', 'global', 'configured', '0'); + var isWizEnabled = safeUciGet('netwiz', 'main', 'wizard_enable', '1'); + var neverShow = safeGetLocal('nw_wizard_never_show'); + + // pppoe 或 static 拨号 + var wProto = safeUciGet('network', 'wan', 'proto', '').toLowerCase(); + if (isConfigured !== '1' && (wProto === 'pppoe' || wProto === 'static')) { + isConfigured = '1'; + isWizEnabled = '0'; // 在內存中同步关闭向导,防止下方else if误判弹出 + // 静默向后端发送完成指令,永久解除CGI劫持 + silentSaveWizardState('1'); + } + + window._realIsConfigured = String(isConfigured); + + // 核心判断逻辑: + if (window._realIsConfigured !== '1') { + // 1. 第一次开机 (且无历史拨号),强制显示向导 + if (wizModal) wizModal.style.display = 'flex'; + } else if (String(isWizEnabled) === '1' && neverShow !== '1') { + // 2.平常,没有勾选“不再提示” + if (wizModal) wizModal.style.display = 'flex'; + } else { + // 3. 平常勾选“不再提示” 或 静默解除劫持成功 + if (wizModal) wizModal.style.display = 'none'; + } + }).catch(function() { + window._realIsConfigured = '0'; + if (wizModal) wizModal.style.display = 'flex'; + }); + } else { + window._realIsConfigured = '0'; + if (wizModal) wizModal.style.display = 'flex'; + } + // ========================================================== + + if (btnReopenWiz) { + btnReopenWiz.style.display = ''; + } + }).catch(function(err) {}); + + function updateStatusDisplay(isSilent) { + if (window._isUpdatingStatus) return; // 防并发锁,彻底解决请求拥堵 + window._isUpdatingStatus = true; + try { + // --- 互联网状态全域快取与防闪烁逻辑 --- + if (typeof window.nwInetStatus === 'undefined') window.nwInetStatus = 'wait'; + if (typeof window.nwInetLast === 'undefined') window.nwInetLast = 0; + + var now = Date.now(); + if (now - window.nwInetLast > 8000) { // 每 8 秒在背景默默探测一次 + window.nwInetLast = now; + callCheckInternet().then(function(res) { + window.nwInetStatus = (res.status === 'ok') ? 'ok' : 'fail'; + // 探测完成后,更新图标 + var badgeEl = document.getElementById('nw-inet-badge'); + if (badgeEl) { + badgeEl.innerHTML = window.nwInetStatus === 'ok' ? '🌐' : '❌'; + badgeEl.title = window.nwInetStatus === 'ok' ? (T['TXT_NET_OK'] || 'Internet Connected') : (T['TXT_NET_FAIL'] || 'Internet Disconnected'); + } + // 接收信号:惰性垃圾回收成功 + if (res.upgrade_success === 1) { + // 隐藏小红点 + var redDot = document.querySelector('.nw-version-dot'); + if (redDot) redDot.style.display = 'none'; + + var verTag = document.querySelector('.nw-version-tag'); + if (verTag) verTag.classList.remove('has-update'); + } + }); + } + + if (modeTextEl && !isSilent) modeTextEl.innerHTML = "
    " + T['LOADING_CONFIG'] + "
    "; + try { uci.unload('network'); uci.unload('dhcp'); uci.unload('wireless'); } catch(e) {} + + // 底层 Wi-Fi 硬件扫描,只在首次进入时请求一次,极大降低路由器 CPU 负担 + var checkWifiPromise = window._cachedWifiRes ? Promise.resolve(window._cachedWifiRes) : callNetCheckWifi().then(function(r) { window._cachedWifiRes = r; return r; }); + + Promise.all([ safePromise(uci.load('network'), null), safePromise(uci.load('dhcp'), null), safePromise(uci.load('wireless'), null), safePromise(getWanStatus(), {}), safePromise(checkWifiPromise, {}) ]).then(function(results) { + window._isUpdatingStatus = false; // 请求完成,释放锁 + var wifiRes = results[4] || {}; + var rawIfaces = results[3] || {}; + var currentIfaces = Array.isArray(rawIfaces.interface) ? rawIfaces.interface : (Array.isArray(rawIfaces) ? rawIfaces : []); + + // ========================================== + // 1. 拦截浏览器切后台导致的空数据 + // ========================================== + if (currentIfaces.length > 0) { + // 只要当前抓到了网卡数据,就深拷贝存入永久快照 + window._antiSleepIfaces = JSON.parse(JSON.stringify(currentIfaces)); + } + // 只要当前是空数据,用快照顶替整个数组 + var ifaces = (currentIfaces.length === 0 && window._antiSleepIfaces) ? window._antiSleepIfaces : currentIfaces; + if (!Array.isArray(ifaces)) ifaces = []; + + // ========================================== + // 2. 从 UCI 配置抓取真实状态 + // ========================================== + var realWanName = 'wan'; + var wProto = safeUciGet('network', 'wan', 'proto', '').toLowerCase(); + + if (!wProto) { + var netSecs = (typeof uci !== 'undefined' && typeof uci.sections === 'function') ? uci.sections('network', 'interface') : []; + for (var i = 0; i < netSecs.length; i++) { + var s = netSecs[i]; + if (s['.name'] !== 'lan' && s['.name'] !== 'loopback' && s['.name'] !== 'wwan' && s.proto && s.proto !== 'none') { + wProto = (s.proto || '').toLowerCase(); + realWanName = s['.name']; + break; + } + } + } + + // 1:缓存协议 + if (wProto) { + window._nwCachedWProto = wProto; + window._nwCachedWanName = realWanName; + } else if (window._nwCachedWProto) { + // 埋点:当发生底层数据丢失,触发我们的缓存保护时,打印黄色警告 + console.warn('[Netwiz Debug] ⚠️ UCI 协议读取为空!已触发缓存保护,强行恢复为: ' + window._nwCachedWProto); + wProto = window._nwCachedWProto; + realWanName = window._nwCachedWanName; + } + + var isWanMode = (wProto === 'dhcp' || wProto === 'pppoe' || wProto === 'static'); + + // ========================================== + // 3. 捏造完美网卡并强行塞入全局数组 + // ========================================== + var activeWan = ifaces.find(function(i) { return i && i.interface === realWanName; }); + + if (!activeWan) { + activeWan = { + interface: realWanName, + 'ipv4-address': [], + up: false, + available: true, + route: [] + }; + // 外层 UI 遍历不到而报警 + ifaces.push(activeWan); + } + + // ========================================== + // 4. 防跳锁 + 彻底封杀所有网线未连接警告 + // ========================================== + if (isWanMode) { + activeWan.up = true; + activeWan.available = true; + + // 检查物理网卡 (eth0, eth1) 状态 + ifaces.forEach(function(i) { + if (i && i.interface !== 'lan' && i.interface !== 'loopback') { + i.up = true; + i.available = true; + } + }); + } + + var phyWan = activeWan; + var virWwan = ifaces.find(function(i) { return i && i.interface === 'wwan'; }) || {}; + + var isWispActive = false; + var hasWispConfigured = false; + if (typeof uci !== 'undefined' && typeof uci.sections === 'function') { + hasWispConfigured = !!uci.sections('wireless', 'wifi-iface').find(function(i) { return i.network === 'wwan' && i.mode === 'sta'; }); + } + + var activeWanHasIp = activeWan['ipv4-address'] && activeWan['ipv4-address'].length > 0; + var virWwanHasIp = virWwan.up && virWwan['ipv4-address'] && virWwan['ipv4-address'].length > 0; + + if (!activeWanHasIp && virWwanHasIp) { + activeWan = virWwan; + isWispActive = true; + wProto = safeUciGet('network', 'wwan', 'proto', '').toLowerCase(); + } + + // ========================================== + // 5. 强制识别协议名称 + // ========================================== + var protoName = ''; + if (wProto === 'pppoe') protoName = 'PPPoE 拨号'; + else if (wProto === 'dhcp') protoName = '自动获取 (DHCP)'; + else if (wProto === 'static') protoName = '静态 IP'; + else protoName = '自动获取 (DHCP)'; + + // ========================================== + // 6. 提取主力 IP + // ========================================== + var liveWanIp = ''; + if (activeWan['ipv4-address'] && activeWan['ipv4-address'].length > 0) { + var foundMainIp = null; + var foundModemIp = null; + + activeWan['ipv4-address'].forEach(function(ipObj) { + var ip = ipObj.address || ''; + if (ip.indexOf('192.168.') === 0) foundModemIp = ip; + else foundMainIp = ip; + }); + + liveWanIp = foundMainIp ? foundMainIp : (foundModemIp ? foundModemIp : activeWan['ipv4-address'][0].address); + } + + // ========================================== + // 7. 纯净渲染 + // ========================================== + if (liveWanIp) { + window._liveWanIp = liveWanIp + ' (' + protoName + ')'; + } else if (isWanMode || isWispActive) { + window._liveWanIp = '网络连接中/等待分配... (' + protoName + ')'; + } else { + window._liveWanIp = ''; + } + // ========================================== + + var liveGw = activeWan.nexthop || ''; + if (!liveGw && Array.isArray(activeWan.route)) { var defaultRoute = activeWan.route.find(function(r) { return r.target === '0.0.0.0'; }); if (defaultRoute) liveGw = defaultRoute.nexthop; } + if (!liveGw && activeWan['ipv4-address'] && activeWan['ipv4-address'][0]) liveGw = activeWan['ipv4-address'][0].ptpaddress || ''; + window._realUpstreamGw = liveGw; + liveGw = liveGw || T['TXT_GETTING']; + + // 提取并格式化连接时间 + var wanUptime = activeWan.uptime || 0; + var formatUptime = function(secs) { + if (!secs) return ''; + var d = Math.floor(secs / 86400), h = Math.floor((secs % 86400) / 3600), m = Math.floor((secs % 3600) / 60), s = secs % 60; + var res = ''; + if (d > 0) res += d + 'D '; + if (h > 0) res += h + 'H '; + if (m > 0) res += m + 'm '; + res += s + 's'; + return res; + }; + var uptimeStr = formatUptime(wanUptime); + var upBadgeHtml = uptimeStr ? "🕒" + uptimeStr + "" : ""; + + // 提取主备 DNS + var dnsServers = activeWan['dns-server'] || []; + var dns1 = dnsServers[0] || ''; + + // 优先使用 底层物理载波状态判断网线通断 + var isWanDown = false; + if (isWispActive) { + isWanDown = false; // 如果中继生效,不弹网线未插警告 + } else if (wProto === 'pppoe') { + // PPPoE 模式下,虚拟接口在拨号成功前 l1up 永远是 false + isWanDown = false; + } else { + if (typeof activeWan.l1up !== 'undefined') { + isWanDown = (activeWan.l1up === false); + } else { + isWanDown = (activeWan.up === false && (!liveWanIp || liveWanIp === T['TXT_GETTING'] || liveWanIp === T['TXT_NOT_GOT'])); + } + } + + // 网卡驱动 Bug 和软路由虚拟网卡 + // 只要探针检测到网络畅通 (ok),或者底层网卡明确 UP 且已经拿到了合法 IP + // 无论底层驱动报不报物理断开 (l1up: false),都判定网线已连接 + if (window.nwInetStatus === 'ok' || (activeWan.up === true && liveWanIp && liveWanIp !== T['TXT_GETTING'] && liveWanIp !== T['TXT_NOT_GOT'])) { + isWanDown = false; + } + + // 初始化防抖计数器 + if (typeof window._wanDropCount === 'undefined') window._wanDropCount = 0; + + // 初始向导是否在显示 + var wizModalEl = container.querySelector('#nw-wizard-modal'); + var isWizOpen = (wizModalEl && wizModalEl.style.display !== 'none'); + + if (isWanDown && !isBypass && (!selectedMode || selectedMode === 'router')) { + + // 系统正在保存/重启,或者向导没关闭,保持静默 + if (window._isSystemBusy || isWizOpen) { + window._wanDropCount = 0; + } else { + // 系统空闲,且向导关闭,开启防抖 + window._wanDropCount++; + // 埋点:每次侦测到断线,打印一次计数 + console.log('[Netwiz Debug] 🔌 探测到底层 l1up:false 或未获取到IP。当前防抖计次: ' + window._wanDropCount); + + // 2:将防抖阈值从 1 改为 3 + // 连续 3 次(约 9~15 秒)探测到彻底断线,才允许弹出警告,过滤底层握手时的假死 + if (window._wanDropCount >= 3) { + // 埋点:触发弹窗,打印红色错误! + console.error('[Netwiz Debug] 🚨 连续 3 次确认断线,正式触发全屏红窗拦截!'); + if (!window._hasAlertedWanDown && !safeGetLocal('ignoreWanAlert')) { + window._hasAlertedWanDown = true; + + // 专属悬浮层,让探针运行 + var overlay = document.createElement('div'); + overlay.id = 'nw-custom-wan-alert'; + overlay.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:99999; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(4px);'; + + var box = document.createElement('div'); + box.style.cssText = 'background:#fff; width:95%; max-width:420px; border-radius:12px; padding:24px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); text-align:center; font-family:sans-serif;'; + + box.innerHTML = '
    🔌
    ' + + '

    ' + T['M_WAN_DOWN_TIT'] + '

    ' + + '
    ' + T['M_WAN_DOWN_MSG'] + '
    ' + + '
    ' + T['M_WAN_DOWN_WAIT'] + '
    ' + + ''; + + overlay.appendChild(box); + document.body.appendChild(overlay); + + // 点击不处理,手动删掉悬浮层 + document.getElementById('btn-ignore-wan').onclick = function() { + window._hasAlertedWanDown = true; + safeSetLocal('ignoreWanAlert', 'true'); + var el = document.getElementById('nw-custom-wan-alert'); + if (el) el.remove(); + }; + } + } + } + } else if (!isWanDown) { + // 埋点:如果之前有断线计数,现在突然恢复了,打印一条日志 + if (window._wanDropCount > 0) { + console.log('[Netwiz Debug] 🟩 侦测到物理网卡恢复 UP,防抖计数器清零。'); + } + + // 侦测到网线恢复,立刻将计数器清零 + window._wanDropCount = 0; + + // 发现插网线 + var customAlert = document.getElementById('nw-custom-wan-alert'); + if (customAlert) { + customAlert.remove(); + } + window._hasAlertedWanDown = false; + } + + var dns2 = dnsServers[1] || ''; + + var wIp = safeUciGet('network', 'wan', 'ipaddr', T['TXT_NOT_GOT']).split('/')[0], wGw = safeUciGet('network', 'wan', 'gateway', T['TXT_NOT_SET']); + var lIp = safeUciGet('network', 'lan', 'ipaddr', '').split('/')[0], lGw = safeUciGet('network', 'lan', 'gateway', T['TXT_NOT_SET']), lIgnore = safeUciGet('dhcp', 'lan', 'ignore', ''), isBypass = (lIgnore === '1' || lIgnore === 'true' || lIgnore === 'on' || lIgnore === 'yes'); + + // 全局拦截,只要发现网段死环,立刻弹窗 + var sysWanIp = window._liveWanIp || ''; + + // 🛡️ 优先从输入框获取真实 IP,如果输入框没有,用 uci 取到的 lIp + var lanIpInput = container.querySelector('#lan-ip'); + var currentLanIp = (lanIpInput && lanIpInput.value) ? lanIpInput.value : lIp; + + // 防呆锁,如果 currentLanIp 为空,或者根本不是合法的 IP 格式,直接将其置空,跳过后续检测 + if (!currentLanIp || currentLanIp.indexOf('.') === -1) { + currentLanIp = ''; + } + + // 只要拿到 WAN IP,且发现和 LAN 在同一网段,且真实 LAN IP 已就绪,立刻触发警报 + if (sysWanIp && currentLanIp && isSameSubnet(sysWanIp, currentLanIp)) { + if (!window._hasAlertedConflict && container.querySelector('#nw-global-modal').style.display === 'none') { + window._hasAlertedConflict = true; + var newSafeIp = getSafeRouterIp(sysWanIp); // 如果 WAN 是 100 段,自动 192.168.1.1 + + openModal({ + title: T['M_CFLT_GLOBAL_TIT'], + msg: T['M_CFLT_GLOBAL_MSG'].replace('{wan_ip}', sysWanIp).replace('{lan_ip}', currentLanIp) + "
    ", + okText: T['BTN_FIX_CONFLICT'], + isDanger: true, + onOk: function() { + var customIp = document.getElementById('conflict-new-ip').value.trim(); + var cleanIp = customIp.replace(/\s+/g, ''); + if (!isValidIP(cleanIp)) { + alert(T['M_INVALID_IP_FMT']); + document.getElementById('conflict-new-ip').value = newSafeIp; + return; + } + if (isSameSubnet(sysWanIp, cleanIp)) { + alert(T['M_STILL_CONFLICT']); + document.getElementById('conflict-new-ip').value = newSafeIp; + return; + } + if (selectedMode !== 'lan' && selectedMode !== '') { + var userConfirm = confirm(T['M_WARN_UNSAVED']); + if (!userConfirm) return; + } + + // 2. 合法的 IP 塞进表单 + container.querySelector('#lan-ip').value = cleanIp; + selectedMode = 'lan'; // 明确告诉底层:这次只修 LAN + + // 3. 开启 120 秒回滚保护 + var safeToggle = container.querySelector('#lan-safe-toggle'); + if (safeToggle) safeToggle.checked = true; + + // 4. 清理所有弹窗并触发底层保存 + container.querySelector('#nw-global-modal').style.display = 'none'; + var wizModal = document.getElementById('nw-wizard-modal'); + if (wizModal) wizModal.style.display = 'none'; + + container.querySelector('#btn-apply').click(); + } + }); + } + } + + // ========================================== + // 1. IPv6状态获取(必须在外面) + // ========================================== + var v6Dhcp = safeUciGet('dhcp', 'lan', 'dhcpv6', 'disabled'); + var v6Ra = safeUciGet('dhcp', 'lan', 'ra', 'disabled'); + var v6Ndp = safeUciGet('dhcp', 'lan', 'ndp', 'disabled'); + var v6Flags = safeUciGet('dhcp', 'lan', 'ra_flags', ''); + if (Array.isArray(v6Flags)) v6Flags = v6Flags.join(' '); + var isV6Standard = (v6Dhcp === 'server' && v6Ra === 'server' && v6Ndp === 'server' && v6Flags.indexOf('managed-config') !== -1 && v6Flags.indexOf('other-config') !== -1); + var isV6Inconsistent = (!isV6Standard && (v6Dhcp === 'server' || v6Dhcp === 'relay' || v6Ra === 'server' || v6Ra === 'relay')); + window._trueIpv6State = (isV6Standard || isV6Inconsistent) ? '1' : '0'; + + // ========================================== + // 2. 缓存整合+阻挡后台5秒轮询覆盖 + // ========================================== + if (!isSilent) { + // 1. 记录系统底层真实的上网模式和对应文字 + window._nwRealWanMode = (wProto === 'pppoe') ? 'pppoe' : 'router'; + + var protoName = ''; + if (wProto === 'pppoe') protoName = T['STAT_MAIN_PPPOE'] || 'Main Router (PPPoE)'; + else if (wProto === 'static') protoName = T['STAT_SEC_STATIC'] || 'Secondary Router (Static IP)'; + else protoName = T['STAT_SEC_DHCP'] || 'Secondary Router (DHCP)'; + window._nwCurrentProtoText = (T['CURRENT_MODE'] || 'Current:') + ' ' + protoName; + + // 辅助函数:根据状态智能切换红色警告/绿色正常 + var setupStatusBox = function(box, isWarning, warnText) { + if (!box) return; + if (isWarning) { + box.style.cssText = 'color: #dc2626; background: #fef2f2; padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 15px; margin-top: 5px; border-left: 4px solid #ef4444; font-weight: bold; display: block; line-height: 1.6;'; + box.innerHTML = warnText; + } else { + box.style.cssText = 'color: #059669; background: #ecfdf5; padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 15px; margin-top: 5px; border-left: 4px solid #10b981; font-weight: bold; display: block; line-height: 1.6;'; + box.innerHTML = '✅ ' + window._nwCurrentProtoText; + } + }; + + // 2. 动态生成 PPPoE 模式下的状态指示牌 + var pppoeBox = container.querySelector('#fields-pppoe'); + if (pppoeBox && !container.querySelector('#status-pppoe-info')) { + var statP = document.createElement('div'); + statP.id = 'status-pppoe-info'; + var titleP = pppoeBox.querySelector('h3, h4, .title, legend') || pppoeBox.firstElementChild; + if (titleP) titleP.insertAdjacentElement('afterend', statP); + else pppoeBox.appendChild(statP); + } + setupStatusBox(container.querySelector('#status-pppoe-info'), window._nwRealWanMode === 'router', T['WARN_PPPOE_INVALID']); + + // 3. 动态生成二级路由模式下的状态指示牌 + var routerBox = container.querySelector('#fields-router'); + if (routerBox && !container.querySelector('#status-router-info')) { + var statR = document.createElement('div'); + statR.id = 'status-router-info'; + var titleR = routerBox.querySelector('h3, h4, .title, legend') || routerBox.firstElementChild; + if (titleR) titleR.insertAdjacentElement('afterend', statR); + else routerBox.appendChild(statR); + } + setupStatusBox(container.querySelector('#status-router-info'), window._nwRealWanMode === 'pppoe', T['WARN_ROUTER_INVALID']); + + // --- 多线智能探测与渲染 --- + var pppoeIfaces = []; + var netSections = uci.sections('network', 'interface') || []; + netSections.forEach(function(s) { + if (s.proto === 'pppoe') { + pppoeIfaces.push({ name: s['.name'], user: s.username || '', pass: s.password || '' }); + } + }); + + window._nwMultiPppoe=(pppoeIfaces.length>1)?pppoeIfaces:null; + + var wizPppoeForm=container.querySelector('#wiz-pppoe-fields'); + if(wizPppoeForm){ + var existHint=container.querySelector('#wiz-multi-wan-hint'); + if(window._nwMultiPppoe){ + if(!existHint){ + var hint=document.createElement('div'); + hint.id='wiz-multi-wan-hint'; + hint.style.cssText='color:#b45309; font-size:12.5px; font-weight:bold; margin-bottom:12px; padding:8px 10px; background:#fef3c7; border-radius:6px; line-height:1.4; text-align:left;'; + hint.innerHTML=T['MSG_WIZ_MULTI_WAN']||'💡 Multi-WAN detected. Only the primary WAN will be configured here, other lines remain unchanged.'; + wizPppoeForm.insertBefore(hint,wizPppoeForm.firstChild); + } + } else { + if(existHint)existHint.remove(); + } + } + var pppoeForm=container.querySelector('#main-pppoe-fields'); + if (pppoeForm) { + if (window._nwMultiPppoe) { + var oldUser = container.querySelector('#pppoe-user'); + if (oldUser) oldUser.remove(); + var oldPass = container.querySelector('#pppoe-pass'); + if (oldPass) oldPass.remove(); + + var mHtml = '
    ' + (T['MSG_MULTI_WAN'] || '💡 Multi-WAN detected...') + '
    '; + window._nwMultiPppoe.forEach(function(iface) { + mHtml += '
    '; + mHtml += '
    ' + (T['LBL_IFACE'] || 'Interface:') + ' ' + iface.name.toUpperCase() + '
    '; + + // 账号输入框(带上方独立 Label,强制白底深色字,附带聚焦变蓝效果) + mHtml += '
    '; + mHtml += '
    ' + (T['LBL_USER'] || 'User') + '
    '; + mHtml += ''; + mHtml += '
    '; + + // 密码输入框 + mHtml += '
    '; + mHtml += '
    ' + (T['LBL_PASS'] || 'Pass') + '
    '; + mHtml += ''; + mHtml += '
    '; + + mHtml += '
    '; + }); + mHtml += ''; + pppoeForm.innerHTML = mHtml; + } else { + // 还原单线模式的 DOM + if (!container.querySelector('#pppoe-user')) { + pppoeForm.innerHTML = '
    ' + + '
    ' + + ''; + } + var uciUser = safeUciGet('network', 'wan', 'username', ''); + var uciPass = safeUciGet('network', 'wan', 'password', ''); + if (sessionStorage.getItem('nw_pppoe_user') === null) sessionStorage.setItem('nw_pppoe_user', uciUser); + if (sessionStorage.getItem('nw_pppoe_pass') === null) sessionStorage.setItem('nw_pppoe_pass', uciPass); + if (container.querySelector('#pppoe-user')) container.querySelector('#pppoe-user').value = sessionStorage.getItem('nw_pppoe_user'); + if (container.querySelector('#pppoe-pass')) container.querySelector('#pppoe-pass').value = sessionStorage.getItem('nw_pppoe_pass'); + } + } + + if (container.querySelector('#router-ip')) container.querySelector('#router-ip').value = (wIp !== T['TXT_NOT_GOT']) ? wIp : ''; + if (container.querySelector('#router-gw')) container.querySelector('#router-gw').value = (wGw !== T['TXT_NOT_SET']) ? wGw : ''; + if (container.querySelector('#lan-ip')) container.querySelector('#lan-ip').value = lIp; + if (container.querySelector('#lan-gw')) container.querySelector('#lan-gw').value = (lGw !== T['TXT_NOT_SET']) ? lGw : ''; + + if (wProto === 'static') { + var staticRadio = container.querySelector('input[name="router_type"][value="static"]'); + if (staticRadio) staticRadio.checked = true; + var staticFields = container.querySelector('#router-static-fields'); + if (staticFields) staticFields.style.display = 'block'; + } else { + var dhcpRadio = container.querySelector('input[name="router_type"][value="dhcp"]'); + if (dhcpRadio) dhcpRadio.checked = true; + var staticFields = container.querySelector('#router-static-fields'); + if (staticFields) staticFields.style.display = 'none'; + } + + var bypassToggle = container.querySelector('#lan-bypass-toggle'); + if (bypassToggle) { bypassToggle.checked = isBypass; container.querySelector('#lan-bypass-warning').style.display = isBypass ? 'block' : 'none'; container.querySelector('#lan-main-warning').style.display = isBypass ? 'none' : 'block'; } + + var wispToggleEl = container.querySelector('#wisp-toggle'); + if (wispToggleEl) { + var wispIface = uci.sections('wireless', 'wifi-iface').find(function(i) { return i.network === 'wwan' && i.mode === 'sta'; }); + wispToggleEl.checked = !!wispIface; + var wispUi = container.querySelector('#wisp-ui-panel'); + if (wispUi) { + wispUi.style.display = 'none'; + if (wispIface) { + var ssidInput = container.querySelector('#wisp-target-ssid'); if (ssidInput) ssidInput.value = wispIface.ssid || ''; + var keyInput = container.querySelector('#wisp-target-key'); if (keyInput) keyInput.value = wispIface.key || ''; + var encInput = container.querySelector('#wisp-target-enc'); if (encInput) encInput.value = wispIface.encryption || 'psk2+ccmp'; + var devInput = container.querySelector('#wisp-target-device'); if (devInput) devInput.value = wispIface.device || 'radio0'; + var bssidInput = container.querySelector('#wisp-target-bssid'); if (bssidInput) bssidInput.value = wispIface.bssid || ''; + } + } + } + + var ipv6Toggle = container.querySelector('#lan-ipv6-toggle'); + var v6Warn = container.querySelector('#tip-ipv6-warn'); + if (ipv6Toggle) ipv6Toggle.checked = (window._trueIpv6State === '1'); + if (v6Warn) v6Warn.style.display = isV6Inconsistent ? 'block' : 'none'; + } + + // --- IPv6 NAT 冲突防呆逻辑 --- + // 1. 缓存冲突状态,防止 LuCI 每 5 秒的自动轮询 (Polling) 导致的数据丢失 + if (wifiRes && wifiRes.lan_nat_conflict !== undefined) { + window._nw_v6_nat_conflict = (wifiRes.lan_nat_conflict === true || String(wifiRes.lan_nat_conflict) === 'true'); + } + + // 2. 将插件的主容器设置为相对定位,作为居中计算的基准坐标 + if (container) { + container.style.position = 'relative'; + } + + // 3. 使用全局缓存状态进行判断 + if (window._nw_v6_nat_conflict) { + if (ipv6Toggle) { + var v6Label = ipv6Toggle.closest('.nw-switch-row-padded'); + if (v6Label) { + // 查找现有的全局警告框 + var warningDiv = document.getElementById('nw-global-v6-nat-warning'); + if (!warningDiv) { + warningDiv = document.createElement('div'); + warningDiv.id = 'nw-global-v6-nat-warning'; + + // position: absolute,基于 container 居中 + warningDiv.style.cssText = 'position: absolute; top: 12%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 600px; z-index: 9999; font-size: 13.5px; line-height: 1.5; border-radius: 8px; padding: 20px; text-align: left; box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); pointer-events: none;'; + + // 挂载到 container (插件内部),而不是 document.body (整个网页) + if (container) { + container.appendChild(warningDiv); + } else { + document.body.appendChild(warningDiv); // 防报错 + } + } + + if (window._trueIpv6State === '1') { + // IPv6 已开启,且存在冲突,允许关闭,发出红色严重警告 + ipv6Toggle.disabled = false; + v6Label.style.opacity = '1'; + v6Label.style.cursor = 'pointer'; + v6Label.style.filter = 'none'; + v6Label.style.pointerEvents = 'auto'; + + warningDiv.style.color = '#7f1d1d'; + warningDiv.style.background = '#fef2f2'; + warningDiv.style.border = '2px solid #ef4444'; + warningDiv.innerHTML = '' + (T['V6_NAT_ERR_TIT1'] || '🚨 Severe Conflict!') + '
    ' + (T['V6_NAT_ERR_MSG1'] || 'Conflict detected.') + ''; + + } else { + // IPv6 未开启,且存在冲突,反灰死锁,禁止开启 + ipv6Toggle.disabled = true; + v6Label.style.opacity = '0.35'; + v6Label.style.cursor = 'not-allowed'; + v6Label.style.filter = 'grayscale(100%)'; + v6Label.style.pointerEvents = 'none'; + + warningDiv.style.color = '#dc2626'; + warningDiv.style.background = '#fff5f5'; + warningDiv.style.border = '2px solid #dc2626'; + warningDiv.innerHTML = '' + (T['V6_NAT_ERR_TIT2'] || '⚠️ Blocked') + '
    ' + (T['V6_NAT_ERR_MSG2'] || 'NAT is enabled.') + ''; + } + } + } + } else { + // 只有在明确没有冲突时,才清理警告 + var globalWarning = document.getElementById('nw-global-v6-nat-warning'); + if (globalWarning) globalWarning.remove(); + + // 恢复开关正常状态 + if (ipv6Toggle) { + ipv6Toggle.disabled = false; + var v6Label = ipv6Toggle.closest('.nw-switch-row-padded'); + if (v6Label) { + v6Label.style.opacity = '1'; + v6Label.style.cursor = 'pointer'; + v6Label.style.filter = 'none'; + v6Label.style.pointerEvents = 'auto'; + } + } + } + // ------------------------------------- + + if (!window._wifiLoaded) { + try { + var wDevs = uci.sections('wireless', 'wifi-device') || []; + var wIfaces = uci.sections('wireless', 'wifi-iface') || []; + + // ===== Netwiz硬件嗅探日志 ===== + console.log("============== [Netwiz 硬件嗅探] =============="); + console.log("检测到物理射频芯片数量:", wDevs.length); + if (wDevs.length === 1) { + console.log("架构判断: 【单芯片处理中心】 (Single-Chip)"); + console.log("目标核心:", wDevs[0]['.name']); + } else if (wDevs.length > 1) { + console.log("架构判断: 【多芯片独立阵列】 (Multi-Chip)"); + var dNames = []; + for(var _i=0; _i= 3) { + // if (true) { + var hdr5g2 = container.querySelector('#hdr-5g2'); + var tab5g2 = container.querySelector('#tab-5g2'); + if (hdr5g2) hdr5g2.style.display = 'flex'; + if (tab5g2) tab5g2.style.display = 'block'; + } + + var smartToggle = container.querySelector('#wifi-smart-toggle'); + var legacyToggle = container.querySelector('#legacy-b-toggle'); + + if (wDevs.length > 0) { + function findMainIfaceForDev(devName) { + var validIfaces = wIfaces.filter(function(i) { return i.device === devName; }); + if (validIfaces.length === 0) return {}; + var best = validIfaces.find(function(i) { return i.disabled !== '1' && i.ssid && i.mode === 'ap'; }); + if (!best) best = validIfaces.find(function(i) { return i.ssid && i.mode === 'ap'; }); + if (!best) best = validIfaces.find(function(i) { return i.disabled !== '1'; }); + if (!best) best = validIfaces[0]; + return best; + } + + if (wDevs.length === 1) { + window._isSingleChip = true; + var theDev = wDevs[0]; + + smartToggle.closest('#wifi-smart-row').style.display = 'none'; + container.querySelector('#wifi-smart-ui').style.display = 'none'; + container.querySelector('#wifi-split-ui').style.display = 'block'; + + var is5G = false; + var ch = parseInt(theDev.channel); + var bd = (theDev.band || '').toLowerCase(); + var ht = (theDev.htmode || '').toLowerCase(); + var hm = (theDev.hwmode || '').toLowerCase(); + + if (bd === '5g' || bd === '6g') { is5G = true; } + else if (bd === '2g') { is5G = false; } + else if (!isNaN(ch) && ch >= 36) { is5G = true; } + else if (!isNaN(ch) && ch > 0 && ch <= 14) { is5G = false; } + else if (ht.indexOf('80') !== -1 || ht.indexOf('160') !== -1 || ht.indexOf('320') !== -1) { is5G = true; } + else if (hm === '11a' || hm === '11ac') { is5G = true; } + else if (hm === '11g' || hm === '11b') { is5G = false; } + else { is5G = (theDev['.name'] === 'radio0'); } + + var isLegacy = (hm === '11b'); + + var allIfaces = wIfaces.filter(function(i) { return i.device === theDev['.name']; }); + var activeIface = allIfaces.find(function(i) { return i.disabled !== '1' && i.mode === 'ap'; }) || allIfaces[0]; + var inactiveIface = activeIface ? allIfaces.find(function(i) { return i['.name'] !== activeIface['.name'] && i.mode === 'ap'; }) : null; + + var actSsid = activeIface ? (activeIface.ssid || '') : ''; + var actKey = activeIface ? (activeIface.key || '') : ''; + var actEnc = activeIface ? (activeIface.encryption || 'psk2+ccmp') : 'psk2+ccmp'; + if (actEnc === 'psk2') actEnc = 'psk2+ccmp'; + var actHidden = activeIface ? (activeIface.hidden === '1') : false; + var actDisabled = activeIface ? (activeIface.disabled === '1' || theDev.disabled === '1') : true; + + var inactSsid = inactiveIface ? (inactiveIface.ssid || '') : ''; + var inactKey = inactiveIface ? (inactiveIface.key || '') : ''; + var inactEnc = inactiveIface ? (inactiveIface.encryption || 'psk2+ccmp') : 'psk2+ccmp'; + if (inactEnc === 'psk2') inactEnc = 'psk2+ccmp'; + var inactHidden = inactiveIface ? (inactiveIface.hidden === '1') : false; + + var chan = theDev.channel || 'auto'; + var bwMatch = ht.match(/\d+/); + var bw = bwMatch ? bwMatch[0] : 'auto'; + + var pMode = 'auto'; + if (ht.indexOf('eht') !== -1) pMode = '11be'; + else if (ht.indexOf('he') !== -1) pMode = '11ax'; + else if (ht.indexOf('vht') !== -1) pMode = '11ac'; + else if (ht.indexOf('ht') !== -1) pMode = (hm.indexOf('a') !== -1 || is5G) ? '11a' : '11g'; + else if (hm === '11b') pMode = '11b'; + + if (is5G) { + container.querySelector('#wifi-5g-en').checked = !actDisabled; + container.querySelector('#wifi-5g-ssid').value = actSsid; + container.querySelector('#wifi-5g-key').value = actKey; + var enc5gEl = container.querySelector('#wifi-5g-enc'); if(enc5gEl.querySelector('option[value="'+actEnc+'"]')) enc5gEl.value = actEnc; + container.querySelector('#wifi-5g-hidden').checked = actHidden; + var chanEl = container.querySelector('#wifi-5g-chan'); if(chanEl.querySelector('option[value="'+chan+'"]')) chanEl.value = chan; + var bwEl = container.querySelector('#wifi-5g-bw'); if(bwEl.querySelector('option[value="'+bw+'"]')) bwEl.value = bw; + var mEl = container.querySelector('#wifi-5g-mode'); if(mEl.querySelector('option[value="'+pMode+'"]')) mEl.value = pMode; + legacyToggle.checked = false; + + container.querySelector('#wifi-2g-en').checked = false; + if (inactiveIface && inactSsid) { + container.querySelector('#wifi-2g-ssid').value = (inactSsid === actSsid) ? smartConvertSsid(actSsid, '2g') : inactSsid; + container.querySelector('#wifi-2g-key').value = inactKey; + var enc2gEl = container.querySelector('#wifi-2g-enc'); if(enc2gEl.querySelector('option[value="'+inactEnc+'"]')) enc2gEl.value = inactEnc; + container.querySelector('#wifi-2g-hidden').checked = inactHidden; + } else { + container.querySelector('#wifi-2g-ssid').value = smartConvertSsid(actSsid, '2g'); + container.querySelector('#wifi-2g-key').value = actKey; + var enc2gEl = container.querySelector('#wifi-2g-enc'); if(enc2gEl.querySelector('option[value="'+actEnc+'"]')) enc2gEl.value = actEnc; + container.querySelector('#wifi-2g-hidden').checked = false; + } + setTimeout(function(){ container.querySelector('#tab-5g').click(); }, 50); + } else { + container.querySelector('#wifi-2g-en').checked = !actDisabled; + container.querySelector('#wifi-2g-ssid').value = actSsid; + container.querySelector('#wifi-2g-key').value = actKey; + var enc2gEl = container.querySelector('#wifi-2g-enc'); if(enc2gEl.querySelector('option[value="'+actEnc+'"]')) enc2gEl.value = actEnc; + container.querySelector('#wifi-2g-hidden').checked = actHidden; + var chanEl = container.querySelector('#wifi-2g-chan'); if(chanEl.querySelector('option[value="'+chan+'"]')) chanEl.value = chan; + var bwEl = container.querySelector('#wifi-2g-bw'); if(bwEl.querySelector('option[value="'+bw+'"]')) bwEl.value = bw; + var mEl = container.querySelector('#wifi-2g-mode'); if(mEl.querySelector('option[value="'+pMode+'"]')) mEl.value = pMode; + legacyToggle.checked = isLegacy; + + container.querySelector('#wifi-5g-en').checked = false; + if (inactiveIface && inactSsid) { + container.querySelector('#wifi-5g-ssid').value = (inactSsid === actSsid) ? smartConvertSsid(actSsid, '5g') : inactSsid; + container.querySelector('#wifi-5g-key').value = inactKey; + var enc5gEl = container.querySelector('#wifi-5g-enc'); if(enc5gEl.querySelector('option[value="'+inactEnc+'"]')) enc5gEl.value = inactEnc; + container.querySelector('#wifi-5g-hidden').checked = inactHidden; + } else { + container.querySelector('#wifi-5g-ssid').value = smartConvertSsid(actSsid, '5g'); + container.querySelector('#wifi-5g-key').value = actKey; + var enc5gEl = container.querySelector('#wifi-5g-enc'); if(enc5gEl.querySelector('option[value="'+actEnc+'"]')) enc5gEl.value = actEnc; + container.querySelector('#wifi-5g-hidden').checked = false; + } + setTimeout(function(){ container.querySelector('#tab-2g').click(); }, 50); + } + } else { + window._isSingleChip = false; + var dev2g = null, dev5g = null, dev5g2 = null; + + wDevs.forEach(function(d) { + var bd = (d.band || '').toLowerCase(); + var ht = (d.htmode || '').toLowerCase(); + var hm = (d.hwmode || '').toLowerCase(); + var path = (d.path || '').toLowerCase(); + var ch = parseInt(d.channel); + var is_5g_chip = false; + + if (bd === '5g' || bd === '6g') { is_5g_chip = true; } + else if (bd === '2g') { is_5g_chip = false; } + else if (hm === '11a' || hm === '11ac') { is_5g_chip = true; } + else if (hm === '11g' || hm === '11b') { is_5g_chip = false; } + else if (!isNaN(ch) && ch >= 36) { is_5g_chip = true; } + else if (!isNaN(ch) && ch > 0 && ch <= 14) { is_5g_chip = false; } + else if (ht.indexOf('80') !== -1 || ht.indexOf('160') !== -1 || ht.indexOf('320') !== -1) { is_5g_chip = true; } + else { + // 正常radio0 是 2.4G,其余是 5G + if (d['.name'] !== 'radio0') is_5g_chip = true; + else is_5g_chip = false; + } + + if (is_5g_chip) { + if (!dev5g) dev5g = d; + else if (!dev5g2) dev5g2 = d; // 抓取第3个芯片 + } + else { if (!dev2g) dev2g = d; } + }); + + if(!dev2g && wDevs.length > 0) dev2g = wDevs[0]; + if(!dev5g && wDevs.length > 1) dev5g = wDevs.find(d => d['.name'] !== dev2g['.name']); + if(dev2g && dev5g && dev2g['.name'] === dev5g['.name']) { + dev5g = wDevs.find(d => d['.name'] !== dev2g['.name']); + } + + var i2g = findMainIfaceForDev(dev2g ? dev2g['.name'] : 'none'); + var i5g = findMainIfaceForDev(dev5g ? dev5g['.name'] : 'none'); + + // 存在第三个芯片时,才去查找接口,否则直接给空值 + var i5g2 = dev5g2 ? findMainIfaceForDev(dev5g2['.name']) : null; + + var isLegacy = dev2g && dev2g.hwmode === '11b'; + + var s2 = i2g.ssid || '', k2 = i2g.key || ''; + var e2 = i2g.encryption || 'psk2+ccmp'; + if (e2 === 'psk2') e2 = 'psk2+ccmp'; + var h2 = i2g.hidden === '1'; + var d2 = (i2g.disabled === '1' || (dev2g && dev2g.disabled === '1')); + + var s5 = i5g.ssid || '', k5 = i5g.key || ''; + var e5 = i5g.encryption || 'psk2+ccmp'; + if (e5 === 'psk2') e5 = 'psk2+ccmp'; + var h5 = i5g.hidden === '1'; + var d5 = (i5g.disabled === '1' || (dev5g && dev5g.disabled === '1')); + + // 解析 5G_Game 状态 + var s5g2 = '', k5g2 = '', d5g2 = true; + if (dev5g2 && i5g2) { + s5g2 = i5g2.ssid || ''; + k5g2 = i5g2.key || ''; + d5g2 = (i5g2.disabled === '1' || dev5g2.disabled === '1'); + } + + var isSmart = (!isLegacy && s2 && s5 && s2 === s5 && k2 === k5 && e2 === e5); + if (!s2 && !s5 && !d2 && !d5) isSmart = true; + + legacyToggle.checked = isLegacy; + smartToggle.checked = isSmart; + + if (isSmart) { + container.querySelector('#wifi-smart-en').checked = (!d2 && !d5); + container.querySelector('#wifi-smart-ssid').value = s2; + container.querySelector('#wifi-smart-key').value = k2; + container.querySelector('#wifi-smart-enc').value = e2; + container.querySelector('#wifi-smart-hidden').checked = h2; + } else { + container.querySelector('#wifi-2g-en').checked = !d2; + container.querySelector('#wifi-2g-ssid').value = s2; + container.querySelector('#wifi-2g-key').value = k2; + container.querySelector('#wifi-2g-enc').value = e2; + container.querySelector('#wifi-2g-hidden').checked = h2; + if (dev2g) { + var chanEl2 = container.querySelector('#wifi-2g-chan'); if(chanEl2.querySelector('option[value="'+(dev2g.channel||'auto')+'"]')) chanEl2.value = (dev2g.channel||'auto'); + var bwM2 = (dev2g.htmode||'').match(/\d+/); var bw2 = bwM2 ? bwM2[0] : 'auto'; + var bwEl2 = container.querySelector('#wifi-2g-bw'); if(bwEl2.querySelector('option[value="'+bw2+'"]')) bwEl2.value = bw2; + + var ht2 = (dev2g.htmode||'').toLowerCase(), hm2 = (dev2g.hwmode||'').toLowerCase(), md2 = 'auto'; + if(ht2.indexOf('eht') !== -1) md2 = '11be'; else if(ht2.indexOf('he') !== -1) md2 = '11ax'; else if(ht2.indexOf('vht') !== -1) md2 = '11ac'; else if(ht2.indexOf('ht') !== -1) md2 = '11g'; else if(hm2 === '11b') md2 = '11b'; + var mEl2 = container.querySelector('#wifi-2g-mode'); if(mEl2.querySelector('option[value="'+md2+'"]')) mEl2.value = md2; + } + + container.querySelector('#wifi-5g-en').checked = !d5; + container.querySelector('#wifi-5g-ssid').value = s5; + container.querySelector('#wifi-5g-key').value = k5; + container.querySelector('#wifi-5g-enc').value = e5; + container.querySelector('#wifi-5g-hidden').checked = h5; + if (dev5g) { + var chanEl5 = container.querySelector('#wifi-5g-chan'); if(chanEl5.querySelector('option[value="'+(dev5g.channel||'auto')+'"]')) chanEl5.value = (dev5g.channel||'auto'); + var bwM5 = (dev5g.htmode||'').match(/\d+/); var bw5 = bwM5 ? bwM5[0] : 'auto'; + var bwEl5 = container.querySelector('#wifi-5g-bw'); if(bwEl5.querySelector('option[value="'+bw5+'"]')) bwEl5.value = bw5; + + var ht5 = (dev5g.htmode||'').toLowerCase(), hm5 = (dev5g.hwmode||'').toLowerCase(), md5 = 'auto'; + if(ht5.indexOf('eht') !== -1) md5 = '11be'; else if(ht5.indexOf('he') !== -1) md5 = '11ax'; else if(ht5.indexOf('vht') !== -1) md5 = '11ac'; else if(ht5.indexOf('ht') !== -1) md5 = '11a'; + var mEl5 = container.querySelector('#wifi-5g-mode'); if(mEl5.querySelector('option[value="'+md5+'"]')) mEl5.value = md5; + } + var en5g2El = container.querySelector('#wifi-5g2-en'); + if (en5g2El) { + en5g2El.checked = !d5g2; // 根据底层真实情况显示开关 + if (s5g2) container.querySelector('#wifi-5g2-ssid').value = s5g2; + if (k5g2) container.querySelector('#wifi-5g2-key').value = k5g2; + } + } + smartToggle.dispatchEvent(new Event('change')); + } + } + + function syncRoamUI(ifaceList, devName, targetBand, encId, togId, warnId) { + var iface = null; + if (window._isSingleChip) { + iface = ifaceList.find(function(i) { return i.device === devName && i.mode === 'ap' && (i['.name'].indexOf(targetBand) !== -1); }); + if (!iface) { + var apIfaces = ifaceList.filter(function(i) { return i.device === devName && i.mode === 'ap'; }); + iface = (targetBand === '2g') ? apIfaces[0] : (apIfaces[1] || apIfaces[0]); + } + } else { + iface = ifaceList.find(function(i) { return i.device === devName && i.mode === 'ap' && i.disabled !== '1'; }); + if (!iface) iface = ifaceList.find(function(i) { return i.device === devName && i.mode === 'ap'; }); + } + + if (!iface) return; + var tog = container.querySelector(togId); + var warn = container.querySelector(warnId); + var encEl = container.querySelector(encId); + if (!tog) return; + + var rOn = (iface.ieee80211r === '1'); + tog.checked = rOn; + + var encVal = encEl ? encEl.value : (iface.encryption || 'psk2+ccmp'); + var isDirty = rOn && (iface.mobility_domain !== 'e4d1' || encVal === 'none'); + + if (isDirty) { + tog.classList.add('is-dirty'); + if (warn) warn.style.display = 'block'; + } else { + tog.classList.remove('is-dirty'); + if (warn) warn.style.display = 'none'; + } + + var keyId = togId.replace('-roaming', '-key'); + var pwdInput = container.querySelector(keyId); + var pwdRow = pwdInput ? pwdInput.closest('.nw-value') : null; + + if (pwdRow) { + var statRow = pwdRow.nextElementSibling; + var isExist = statRow && statRow.classList.contains('nw-roam-status-row'); + + if (!isExist) { + statRow = document.createElement('div'); + statRow.className = 'nw-roam-status-row'; + statRow.style.cssText = 'margin-top: 5px; margin-bottom: 15px; '; + pwdRow.parentNode.insertBefore(statRow, pwdRow.nextSibling); + } + + if (rOn) { + if (isDirty) { + statRow.innerHTML = "" + T['TXT_ROAMING_ON'] + "! " + T['TXT_CLICK_FIX'] + ""; + } else { + statRow.innerHTML = "" + T['TXT_ROAMING_ON'] + ""; + } + + // 悬浮动画增强 + var badgeSpan = statRow.querySelector('span'); + badgeSpan.onmouseover = function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 12px rgba(16,185,129,0.25)'; }; + badgeSpan.onmouseout = function() { this.style.transform = 'none'; this.style.boxShadow = 'none'; }; + + badgeSpan.onclick = function(e) { + e.stopPropagation(); + if (isDirty) alert(T['DESC_ROAM_DIRTY']); + var advPanel = tog.closest('.nw-adv-panel'); + var advBtn = advPanel ? advPanel.previousElementSibling : null; + if (advPanel && advPanel.style.display === 'none' && advBtn) { + advBtn.click(); + } + setTimeout(function() { + tog.scrollIntoView({ behavior: 'smooth', block: 'center' }); + var targetRow = tog.closest('div'); + if (targetRow) { + var oldBg = targetRow.style.backgroundColor || 'transparent'; + targetRow.style.transition = 'background-color 0.4s ease'; + targetRow.style.backgroundColor = 'rgba(16, 185, 129, 0.25)'; + targetRow.style.borderRadius = '8px'; + setTimeout(function() { + targetRow.style.backgroundColor = oldBg; + setTimeout(function() { targetRow.style.transition = ''; }, 400); + }, 800); + } + }, 80); + }; + } else { + statRow.innerHTML = ""; + } + } + } + + if (window._isSingleChip && wDevs[0]) { + syncRoamUI(wIfaces, wDevs[0]['.name'], '2g', '#wifi-2g-enc', '#wifi-2g-roaming', '#roam-warn-2g'); + syncRoamUI(wIfaces, wDevs[0]['.name'], '5g', '#wifi-5g-enc', '#wifi-5g-roaming', '#roam-warn-5g'); + } else { + if (dev2g) syncRoamUI(wIfaces, dev2g['.name'], '2g', '#wifi-2g-enc', '#wifi-2g-roaming', '#roam-warn-2g'); + if (dev5g) { + syncRoamUI(wIfaces, dev5g['.name'], '5g', '#wifi-5g-enc', '#wifi-5g-roaming', '#roam-warn-5g'); + syncRoamUI(wIfaces, dev5g['.name'], 'smart', '#wifi-smart-enc', '#wifi-smart-roaming', '#roam-warn-smart'); + } + } + + window._origWifiState = getWifiSnapshot(); + + window._wifiLoaded = true; + } catch(ex) { } + } + + // ---- 开始 ---- + var mkB = function(bg, txt) { return "" + txt + ""; }; + var mkD = function(l1, v1, l2, v2) { return "" + l1 + "" + v1 + "" + l2 + "" + v2 + ""; }; + + // 这样时间在内部怎么跳动,都绝对不会再挤压外部的排版! + var fixedUpBadge = upBadgeHtml ? "" + upBadgeHtml + "" : ""; + + var sTitle = "", sDetails = "", statusBadge = ""; + + // 底层物理网口/PPPoE已断开,或还未获取到IP,强制立刻显示断开的X,不等待8秒的ping缓存 + if (!activeWan.up || !liveWanIp) { window.nwInetStatus = 'fail'; } + + var iconStr = window.nwInetStatus === 'ok' ? '🌐' : (window.nwInetStatus === 'fail' ? '❌' : ''); + var titleStr = window.nwInetStatus === 'ok' ? (T['TXT_NET_OK'] || 'Internet Connected') : (window.nwInetStatus === 'fail' ? (T['TXT_NET_FAIL'] || 'Internet Disconnected') : ''); + + var inetBadgeHtml = "" + iconStr + ""; + + if (isBypass) { sTitle = T['STAT_BYPASS']; sDetails = mkD(T['TXT_DEV_IP'], lIp, T['TXT_UP_GW'], lGw); } + else if (isWispActive) { sTitle = T['TXT_WISP_ON'] || 'WISP Enabled'; statusBadge = mkB('#10b981', T['BDG_GOT'] || 'IP Acquired') + inetBadgeHtml + fixedUpBadge; sDetails = mkD(T['TXT_WAN_IP'], liveWanIp, T['TXT_UP_GW'], liveGw); } + else if (hasWispConfigured && !isWispActive) { sTitle = T['TXT_WISP_ON'] || 'WISP Enabled'; statusBadge = mkB('#f59e0b', T['TXT_WISP_WAITING'] || 'Connecting...') + inetBadgeHtml + fixedUpBadge; sDetails = "
    " + (T['MSG_WISP_STUCK'] || '⚠️ Connecting to upstream...') + "
    "; } + else if (wProto === 'pppoe') { sTitle = T['STAT_MAIN_PPPOE']; if (activeWan.up && liveWanIp) { statusBadge = mkB('#10b981', T['BDG_SUCC']) + inetBadgeHtml + fixedUpBadge; sDetails = mkD(T['TXT_PUB_IP'], liveWanIp, T['TXT_REM_GW'], liveGw); } else { statusBadge = mkB('#ef4444', T['BDG_DIAL']) + inetBadgeHtml + fixedUpBadge; sDetails = mkD(T['TXT_LAN_IP'], lIp, T['TXT_STATUS'], T['TXT_WAIT_REM']); } } + else if (wProto === 'dhcp') { sTitle = T['STAT_SEC_DHCP']; if (activeWan.up && liveWanIp) { statusBadge = mkB('#10b981', T['BDG_GOT']) + inetBadgeHtml + fixedUpBadge; sDetails = mkD(T['TXT_WAN_IP'], liveWanIp, T['TXT_UP_GW'], liveGw); } else { statusBadge = mkB('#f59e0b', T['BDG_WAIT']) + inetBadgeHtml + fixedUpBadge; sDetails = mkD(T['TXT_LAN_IP'], lIp, T['TXT_STATUS'], T['TXT_GET_IP']); } } + else if (wProto === 'static') { sTitle = T['STAT_SEC_STATIC']; statusBadge = (activeWan.up ? mkB('#10b981', T['BDG_CONN']) : mkB('#ef4444', T['BDG_UNPLUG'])) + inetBadgeHtml + fixedUpBadge; sDetails = mkD(T['TXT_WAN_IP'], wIp, T['TXT_UP_GW'], wGw); } + else { sTitle = T['STAT_LAN']; sDetails = mkD(T['TXT_LAN_IP'], lIp, T['TXT_DHCP_SRV'], T['TXT_ON']); } + // ---- 结束 ---- + var sDnsHtml = ""; + if (!isBypass && activeWan.up && dns1) { + sDnsHtml = "
    " + T['TXT_DNS1'] + " " + dns1 + "" + (dns2 ? "" + T['TXT_DNS2'] + " " + dns2 + "" : "") + "
    "; + } + + window._gotoRoam = function(band, isDirty) { + var box = document.getElementById('netwiz-container'); + if (!box) return; + if (isDirty && typeof T !== 'undefined') alert(T['DESC_ROAM_DIRTY']); + + selectedMode = 'wifi'; + var s1 = box.querySelector('#step-1'), s2 = box.querySelector('#step-2'), s3 = box.querySelector('#step-3'); + if (s1) s1.style.display = 'none'; + if (s3) s3.style.display = 'none'; + if (s2) s2.style.display = 'block'; + + var fRouter = box.querySelector('#fields-router'); if(fRouter) fRouter.style.display = 'none'; + var fPppoe = box.querySelector('#fields-pppoe'); if(fPppoe) fPppoe.style.display = 'none'; + var fLan = box.querySelector('#fields-lan'); if(fLan) fLan.style.display = 'none'; + var fWifi = box.querySelector('#fields-wifi'); if(fWifi) fWifi.style.display = 'block'; + + setTimeout(function() { + var isSmart = box.querySelector('#wifi-smart-toggle').checked && !window._isSingleChip; + var targetTogId = isSmart ? '#wifi-smart-roaming' : '#wifi-' + band + '-roaming'; + + if (!isSmart) { + var tabBtn = box.querySelector('#tab-' + band); + if (tabBtn) tabBtn.click(); + } + + setTimeout(function() { + var tog = box.querySelector(targetTogId); + if (!tog) return; + + var advPanel = tog.closest('.nw-adv-panel'); + var advBtn = advPanel ? advPanel.previousElementSibling : null; + if (advPanel && advPanel.style.display === 'none' && advBtn) { + advBtn.click(); + } + + setTimeout(function() { + tog.scrollIntoView({ behavior: 'smooth', block: 'center' }); + var targetRow = tog.closest('div'); + if (targetRow) { + var oldBg = targetRow.style.backgroundColor || 'transparent'; + targetRow.style.transition = 'background-color 0.4s ease'; + targetRow.style.backgroundColor = 'rgba(16, 185, 129, 0.3)'; + targetRow.style.borderRadius = '8px'; + + setTimeout(function() { + targetRow.style.backgroundColor = oldBg; + setTimeout(function() { targetRow.style.transition = ''; }, 400); + }, 1000); + } + }, 150); + }, 100); + }, 100); + }; + + var ipv6Label = (window._trueIpv6State === '1') ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + ''; + + var wDevsList = uci.sections('wireless', 'wifi-device') || []; + var wIfacesList = uci.sections('wireless', 'wifi-iface') || []; + var activeIfaces = wIfacesList.filter(function(i) { + var parentDev = wDevsList.find(function(d) { return d['.name'] === i.device; }); + var isDevDisabled = parentDev ? (parentDev.disabled == 1 || parentDev.disabled === 'true' || parentDev.disabled === true) : false; + var isIfaceDisabled = (i.disabled == 1 || i.disabled === 'true' || i.disabled === true); + return !isIfaceDisabled && !isDevDisabled && (i.mode === 'ap' || i.mode === 'sta') && i.ssid; + }); + var wifiLines = []; + // === 首页 Wi-Fi 卡片 === + var animLayer = container.querySelector('.nw-wifi-anim-layer'); + var cardTags = container.querySelector('#nw-wifi-tags'); + if (cardTags) { + if (activeIfaces.length > 0) { + if (animLayer) { + animLayer.style.display = 'block'; + animLayer.style.animation = 'wifi-wave 2s infinite'; + } + + var tagsHtml = ''; + var hasAp = activeIfaces.some(function(i){ return i.mode === 'ap'; }); + var hasSta = activeIfaces.some(function(i){ return i.mode === 'sta'; }); + + if (hasAp) { + var apList = activeIfaces.filter(function(i){ return i.mode === 'ap'; }); + var isSmartGrp = apList.length > 1 && apList.every(function(i){ return i.ssid === apList[0].ssid && i.key === apList[0].key; }); + if (isSmartGrp) { + tagsHtml += '' + (T['TAG_SMART'] || 'Smart Connect') + ''; + } else { + tagsHtml += '' + (T['TAG_SPLIT'] || 'Independent Bands') + ''; + } + } + if (hasSta) { + tagsHtml += '' + (T['TAG_WISP'] || 'WISP Repeater') + ''; + } + cardTags.innerHTML = tagsHtml; + } else { + if (animLayer) { + animLayer.style.display = 'none'; + animLayer.style.animation = 'none'; + } + cardTags.innerHTML = '' + (T['TAG_DISABLED'] || 'Disabled') + ''; + } + } + // ================================== + + if (!window._hasRealWifi) { + // 没有真实物理 Wi-Fi,隐藏状态栏的 Wi-Fi 信息 + } else if (activeIfaces.length === 0) { + wifiLines.push("
    " + T['TXT_WIFI_STATUS'] + ": " + T['TXT_OFF'] + "
    "); + } else { + var apIfaces = activeIfaces.filter(function(i) { return i.mode === 'ap'; }); + var staIfaces = activeIfaces.filter(function(i) { return i.mode === 'sta'; }); + + // 1. 中继网络 (STA) 照常独立显示 + staIfaces.forEach(function(i) { + var sName = escapeHTML(i.ssid); + var tLbl = "" + T['TXT_WISP_ON'] + ""; + var qrAttr = " class='nw-hl nw-wifi-name nw-qr-hover' data-ssid=\"" + escapeHTML(i.ssid||'') + "\" data-pwd=\"" + escapeHTML(i.key||'') + "\" data-enc=\"" + escapeHTML(i.encryption||'none') + "\" style='cursor:pointer; position:relative; z-index:10; border-bottom:1px dashed rgba(255,255,255,0.4); padding-bottom:1px;' "; + wifiLines.push("
    " + tLbl + ":" + sName + "
    "); + }); + + // 2. 判断 AP 是否应该合并为“多频合一”显示 + var isSmartGrouped = false; + if (apIfaces.length > 1) { + var first = apIfaces[0]; + isSmartGrouped = apIfaces.every(function(i) { + return i.ssid === first.ssid && i.key === first.key; + }); + } + if (isSmartGrouped) { + // 渲染合并后的单行 UI + var i = apIfaces[0]; + var sName = escapeHTML(i.ssid); + var kTxt = i.key ? escapeHTML(i.key) : "" + T['TXT_NO_PASS'] + ""; + var bandStr = 'smart'; + + var rOn = apIfaces.some(function(x) { return x.ieee80211r === '1'; }); + var isDirty = apIfaces.some(function(x) { + var enc = (x.encryption || '').toLowerCase(); + var md = (x.mobility_domain || '').toLowerCase(); + return x.ieee80211r === '1' && (md !== 'e4d1' || enc === 'none'); + }); + + var roamBadge = ""; + if (rOn) { + var clickFn = "if(window._gotoRoam){ window._gotoRoam('" + bandStr + "', " + isDirty + "); }"; + var hoverStyle = "onmouseover=\"this.style.transform='translateY(-2px)'; this.style.boxShadow='0 3px 6px rgba(16,185,129,0.3)'\" onmouseout=\"this.style.transform='none'; this.style.boxShadow='none'\""; + if (isDirty) { + roamBadge = "" + T['TXT_ROAMING'] + "!"; + } else { + roamBadge = "" + T['TXT_ROAMING'] + ""; + } + } + + // 多频合一白色标识牌,pointer-events 接收鼠标事件 + var qrData = " data-ssid=\"" + escapeHTML(i.ssid||'') + "\" data-pwd=\"" + escapeHTML(i.key||'') + "\" data-enc=\"" + escapeHTML(i.encryption||'none') + "\" "; + var hoverStr = "WIFI:T:" + ((!i.encryption || i.encryption === 'none') ? "nopass" : "WPA") + ";S:" + (i.ssid||'') + ";" + (i.key ? "P:" + i.key + ";" : "") + ";"; + var tLbl = "" + T['LBL_SMART_CONN'] + ""; + + wifiLines.push("
    " + tLbl + ":" + sName + "" + roamBadge + "(" + T['M_PWD'] + ": " + kTxt + ")
    "); + } else { + // 3. 渲染独立频段 UI + apIfaces.forEach(function(i) { + var sName = escapeHTML(i.ssid); + var kTxt = i.key ? escapeHTML(i.key) : "" + T['TXT_NO_PASS'] + ""; + var bandStr = '2g'; + var dObj = wDevsList.find(function(x) { return x['.name'] === i.device; }); + if (dObj) { + var hw = (dObj.hwmode||'').toLowerCase(); + var bd = (dObj.band||'').toLowerCase(); + var path = (dObj.path||'').toLowerCase(); + if (hw.indexOf('a') !== -1 || bd === '5g' || path.indexOf('pcie1') !== -1 || path.indexOf('pcie2') !== -1) { + bandStr = '5g'; + } else { + bandStr = '2g'; + } + } + + var rOn = (i.ieee80211r === '1'); + var enc = (i.encryption || '').toLowerCase(); + var isDirty = rOn && (i.mobility_domain !== 'e4d1' || enc === 'none'); + + var roamBadge = ""; + if (rOn) { + var clickFn = "if(window._gotoRoam){ window._gotoRoam('" + bandStr + "', " + isDirty + "); }"; + var hoverStyle = "onmouseover=\"this.style.transform='translateY(-2px)'; this.style.boxShadow='0 3px 6px rgba(16,185,129,0.3)'\" onmouseout=\"this.style.transform='none'; this.style.boxShadow='none'\""; + + if (isDirty) { + roamBadge = "" + T['TXT_ROAMING'] + "!"; + } else { + roamBadge = "" + T['TXT_ROAMING'] + ""; + } + } + + // 独立频段白色标识牌,pointer-events 强制接收鼠标事件 + var qrData = " data-ssid=\"" + escapeHTML(i.ssid||'') + "\" data-pwd=\"" + escapeHTML(i.key||'') + "\" data-enc=\"" + escapeHTML(i.encryption||'none') + "\" "; + var hoverStr = "WIFI:T:" + ((!i.encryption || i.encryption === 'none') ? "nopass" : "WPA") + ";S:" + (i.ssid||'') + ";" + (i.key ? "P:" + i.key + ";" : "") + ";"; + var tLblNew = "" + (bandStr === '5g' ? T['TXT_5G_ACCT'] : T['TXT_2G_ACCT']) + ""; + wifiLines.push("
    " + tLblNew + ":" + sName + "" + roamBadge + "(" + T['M_PWD'] + ": " + kTxt + ")
    "); + }); + } + } + + var ipv6Html = "
    IPv6 (DHCPv6): " + ipv6Label + "
    "; + + // 全局出场跳转函数 + window._leavePage = function(url) { + var wrap = document.querySelector('.nw-wrapper'); + if (wrap) wrap.classList.add('page-leaving'); + // 400ms 等动画完了再跳转 + setTimeout(function() { window.location.href = url; }, 400); + }; + + // 跳转到新网络管家按钮 + var devMgrBtn = ""; + if (!isBypass) { + var devUrl = window.location.pathname.replace('/netwiz', '/netwiz_dev'); + // 生成按鈕的 HTML + devMgrBtn = ""; + } + + var extraInfo = "
    "; + extraInfo += wifiLines.join(''); + extraInfo += "
    "; + + // 读取会话记忆,保持按钮文字与智能折叠状态一致 + var isExpanded = (sessionStorage.getItem('nw_adv_expanded') === '1'); + var btnText = isExpanded ? (T['BTN_ADV_HIDE'] || 'Advanced Settings ▲') : (T['BTN_ADV_SHOW'] || 'Advanced Settings ▼'); + var btnBg = isExpanded ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.15)'; + + // 蓝色底板高级设置触发按钮 + var advToggleHtml = "
    " + btnText + "
    "; + + if (modeTextEl) { + modeTextEl.innerHTML = "
    " + sTitle + "" + statusBadge + "
    " + "
    " + sDetails + "
    " + sDnsHtml + ipv6Html + devMgrBtn + extraInfo + advToggleHtml; + + // 异步检测网络连通性 + if (statusBadge.indexOf('nw-inet-badge') !== -1) { + callCheckInternet().then(function(res) { + var el = document.getElementById('nw-inet-badge'); + if (el) { + if (res.status === 'ok') { + el.innerHTML = "🌐"; + el.title = T['TXT_NET_OK']; + } else { + el.innerHTML = "❌"; + el.title = T['TXT_NET_FAIL']; + } + } + + // 接收信号:惰性垃圾回收成功 + if (res.upgrade_success === 1) { + // 隐藏小红点 + var redDot = document.querySelector('.nw-version-dot'); + if (redDot) redDot.style.display = 'none'; + + var verTag = document.querySelector('.nw-version-tag'); + if (verTag) verTag.classList.remove('has-update'); + } + }); + } + } + + }).catch(function() { + window._isUpdatingStatus = false; // 异常释放锁 + }); + } catch(e) { + window._isUpdatingStatus = false; // 异常释放锁 + } + } + + // ========================================== + // 原生备份与恢复配置逻辑绑定 + // ========================================== + + // 独立抽离的上传与恢复执行函数 + var handleRestoreUpload = function(e) { + var file = e.target.files[0]; + if (!file) return; + + openModal({ + title: '⚠️ ' + (T['M_RST_CONF_TIT'] || 'Restore Configuration Confirm'), + msg: '
    ' + + (T['M_RST_CONF_WARN'] || 'Warning: Restoring the configuration will overwrite all current settings and automatically restart the router upon completion!') + '
    ' + + '
    ' + (T['M_SEL_FILE'] || 'Selected file: ') + escapeHTML(file.name) + '
    ', + okText: '🚀 ' + (T['BTN_RST_START'] || 'Confirm Restore'), + cancelText: T['BTN_CANCEL'] || 'Cancel', + isDanger: true, + onOk: function() { + openModal({ + title: T['M_RST_CONF_TIT_ING'] || 'Restoring Configuration', + msg: '
    ' + + (T['MSG_RESTORE_UPLOADING'] || 'Uploading backup file, please do not disconnect power...') + + '
    0%
    ', + spin: true + }); + + var fd = new FormData(); + var sid = (typeof L !== 'undefined' && L.env && L.env.sessionid) ? L.env.sessionid : ""; + if (!sid) { + var match = document.cookie.match(/sysauth_http=([^;]+)/) || document.cookie.match(/sysauth=([^;]+)/); + if (match) sid = match[1]; + } + fd.append("sessionid", sid); + fd.append("filename", "/tmp/backup_restore.tar.gz"); + fd.append("file", file); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/cgi-bin/cgi-upload', true); + + xhr.upload.onprogress = function(evt) { + if (evt.lengthComputable) { + var percent = Math.floor((evt.loaded / evt.total) * 100); + var pEl = document.getElementById('nw-upload-progress'); + if (pEl) pEl.innerText = percent + '%'; + } + }; + + xhr.onload = function() { + if (xhr.status === 200) { + var pEl = document.getElementById('nw-upload-progress'); + if (pEl) pEl.innerHTML = '' + (T['M_RST_DELIVERED'] || 'File delivered, starting restore...') + ''; + + // 使用文件顶部预先声明好的 callSysRestore 方法正确传参 + callSysRestore(true, '/tmp/backup_restore.tar.gz').then(function() { + var rebootSec = 0; + setInterval(function() { + rebootSec++; + if (pEl) pEl.innerHTML = '🔄 ' + (T['MSG_REBOOTING'] || 'System is restarting...') + ' (' + rebootSec + 's)'; + if (rebootSec > 60) window.location.reload(); + }, 1000); + }).catch(function(err) { + if (pEl) pEl.innerHTML = '⏳ ' + (T['M_RST_CMD_SENT'] || 'Restore command issued, device may be restarting...') + ''; + setTimeout(function(){ window.location.reload(); }, 60000); + }); + + } else { + document.getElementById('file-restore-config').value = ''; + openModal({ title: T['M_SYS_ERR'] || 'Error', msg: (T['M_UPL_FAIL_STAT'] || 'Upload failed: %s').replace('%s', xhr.status), hideCancel: true, okText: T['M_CLOSE'] || 'Close' }); + } + }; + + xhr.onerror = function() { + document.getElementById('file-restore-config').value = ''; + openModal({ title: T['M_RST_NET_ERR'] || 'Network Error', msg: T['M_RST_NET_INTR'] || 'Network connection unexpectedly interrupted!', hideCancel: true, okText: T['M_CLOSE'] || 'Close' }); + }; + + xhr.send(fd); + } + }); + }; + + // 将点击事件挂载到最高层级 document,防止被拦截或 container 未就绪 + document.addEventListener('click', function(e) { + + // 1. 拦截备份:前端接收 Base64 并触发浏览器下载 + var btnBackup = e.target.closest('#nw-btn-backup-plugin'); + if (btnBackup) { + e.preventDefault(); + + var selPlugin = document.getElementById('nw-repair-select'); + if (!selPlugin || !selPlugin.value) return; + var pName = selPlugin.value; + + openModal({ + title: '📦 ' + (T['M_BAK_ING_TIT'] || 'Backing up'), + msg: '
    ⏳ ' + + (T['M_BAK_ING_MSG'] || 'Precisely packing %s config and generating download, please wait...').replace('%s', '' + pName + '') + + '
    ', + spin: true, hideCancel: true, hideOk: true + }); + + callBackupPlugin(pName).then(function(r) { + if (r && String(r.result) === '0' && r.url) { + + var a = document.createElement('a'); + a.href = r.url; + a.download = r.url.split('/').pop(); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + openModal({ + title: T['M_BAK_SUCC_TIT'] || 'Backup Successful', + msg: '
    ' + (T['M_BAK_SUCC_MSG'] || 'The file contains core assets and is downloading in the background. Please check your browser!') + '
    ', + okText: T['M_CLOSE'] || 'Close', + hideCancel: true + }); + } else { + openModal({ title: '❌ ' + (T['M_BAK_FAIL_TIT'] || 'Backup Failed'), msg: (T['M_BAK_FAIL_MSG'] || 'Config not found or packaging error. Please check if the plugin is installed.'), okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }).catch(function(err) { + openModal({ title: '❌ ' + (T['M_REQ_FAIL_TIT'] || 'Request Failed'), msg: (T['M_REQ_FAIL_MSG'] || 'API request failed: %s').replace('%s', err), okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + }); + } + + // 2. 拦截恢复:呼出文件选择框上传,然后触发解压 + var btnRestore = e.target.closest('#nw-btn-restore-plugin'); + if (btnRestore) { + e.preventDefault(); + + var selPlugin = document.getElementById('nw-repair-select'); + if (!selPlugin || !selPlugin.value) return; + var pName = selPlugin.value; + + var fileInput = document.getElementById('file-restore-plugin-local'); + if (!fileInput) { + fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = 'file-restore-plugin-local'; + fileInput.style.display = 'none'; + document.body.appendChild(fileInput); + + fileInput.addEventListener('change', function(evt) { + var file = evt.target.files[0]; + if (!file) return; + + // 动态获取下拉框最新选择的值 + var selPluginNow = document.getElementById('nw-repair-select'); + var currentPName = selPluginNow ? selPluginNow.value : ''; + if (!currentPName) return; + + var fileName = file.name.toLowerCase(); + + // 防呆 1:检查文件格式 + if (!fileName.endsWith('.tar.gz') && !fileName.endsWith('.tar')) { + openModal({ + title: '❌ ' + (T['M_FMT_ERR_TIT'] || 'Format Error'), + msg: T['M_FMT_ERR_MSG'] || 'Please upload a valid .tar or .tar.gz backup file!', + okText: T['M_CLOSE'] || 'Close', + hideCancel: true + }); + fileInput.value = ''; + return; + } + + // 防呆 2:检查文件名是否包含该插件的名称 (允许稍微模糊的匹配) + if (fileName.indexOf(currentPName.toLowerCase()) === -1) { + openModal({ + title: '⚠️ ' + (T['M_WARN_TIT'] || 'Warning'), + msg: '
    ' + + (T['M_WARN_MSG'] || 'The selected file does not seem to belong to %s.
    File name: %f
    Are you sure you want to continue?') + .replace('%s', '' + currentPName + '') + .replace('%f', file.name) + + '
    ', + okText: T['BTN_FORCE_RST'] || 'Force Restore', + cancelText: T['BTN_CANCEL'] || 'Cancel', + isDanger: true, + onOk: function() { startUploadRestore(file, currentPName); } // 强制,则放行 + }); + fileInput.value = ''; + return; + } + + // 验证通过,开始上传 + startUploadRestore(file, currentPName); + }); + + // 上传逻辑抽离成一个独立函数,方便调用 + function startUploadRestore(file, pName) { + + openModal({ + title: '⚡ ' + (T['M_RST_ING_TIT'] || 'Restoring'), + msg: '
    ⏳ ' + + (T['M_RST_ING_MSG'] || 'Uploading and restoring %s config...').replace('%s', '' + pName + '') + + '
    ', + spin: true, hideCancel: true, hideOk: true + }); + + var fd = new FormData(); + var sid = (typeof L !== 'undefined' && L.env && L.env.sessionid) ? L.env.sessionid : ""; + if (!sid) { + var match = document.cookie.match(/sysauth_http=([^;]+)/) || document.cookie.match(/sysauth=([^;]+)/); + if (match) sid = match[1]; + } + fd.append("sessionid", sid); + fd.append("filename", "/tmp/plugin_restore_tmp.tar.gz"); + fd.append("file", file); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/cgi-bin/cgi-upload', true); + xhr.onload = function() { + if (xhr.status === 200) { + callRestorePlugin(pName).then(function(r) { + if (r && String(r.result) === '0') { + openModal({ + title: '✅ ' + (T['M_RST_SUCC_TIT'] || 'Restore Successful'), + // 倒计时自动刷新 + msg: '
    ' + + (T['M_RST_SUCC_MSG'] || '%s config restored! Service is restarting in the background...').replace('%s', '' + pName + '') + + '
    ', + okText: T['M_RELOAD'] || 'Reload Page', + hideCancel: true, + onOk: function() { window.location.reload(); } + }); + + // 倒计时 5 秒后自动刷新 + var sec = 5; + var cdEl = document.getElementById('nw-reload-countdown'); + var timer = setInterval(function() { + sec--; + if (cdEl) cdEl.innerText = (T['M_AUTO_RELOAD'] || 'Auto reloading in %s seconds...').replace('%s', sec); + if (sec <= 0) { + clearInterval(timer); + window.location.reload(); + } + }, 1000); + + } else { + var errMsg = T['M_RST_FAIL_MSG'] || 'Failed to extract or restart service.'; + if (r && String(r.result) === '3') { + errMsg = T['M_ERR_FILE_CORRUPT'] || 'The uploaded file is corrupted or not a valid archive!'; + } else if (r && String(r.result) === '4') { + errMsg = (T['M_ERR_FILE_MISMATCH'] || 'The archive content does not match %s!').replace('%s', '' + pName + ''); + } else if (r && String(r.result) === '5') { + // 捕获解压失败错误 + errMsg = T['M_ERR_EXTRACT'] || 'Failed to extract files to the system! (Storage full or permission denied)'; + } + + openModal({ + title: '❌ ' + (T['M_RST_FAIL_TIT'] || 'Restore Failed'), + msg: errMsg, + okText: T['M_CLOSE'] || 'Close', + hideCancel: true + }); + } + }).catch(function(err) { + openModal({ title: '❌ ' + (T['M_REQ_FAIL_TIT'] || 'Request Failed'), msg: (T['M_REQ_FAIL_MSG'] || 'API request failed: %s').replace('%s', err), okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + }); + } else { + openModal({ title: '❌ ' + (T['M_UPL_FAIL_TIT'] || 'Upload Failed'), msg: (T['M_UPL_FAIL_MSG'] || 'File upload rejected, status code: %s').replace('%s', xhr.status), okText: T['M_CLOSE'] || 'Close', hideCancel: true }); + } + }; + xhr.send(fd); + } + } + // 每次点击都清空旧文件并唤起系统选择框 + fileInput.value = ''; + fileInput.click(); + } + }); + // ========================================== + + // 智能备份与恢复事件绑定 + var btnSmartBackup = container.querySelector('#btn-smart-backup'); + var btnSmartRestore = container.querySelector('#btn-smart-restore'); + var fileSmartRestore = container.querySelector('#file-smart-restore'); + + if (btnSmartBackup) { + btnSmartBackup.addEventListener('click', function(e) { + e.preventDefault(); + window._selectedBackupType = 'light'; + + openModal({ + title: '
    ×' + T['M_BAK_SEL_TIT'] + '
    ', + msg: '
    ' + + '
    ' + + '
    ' + T['M_BAK_LIGHT_TIT'] + '
    ' + T['M_BAK_LIGHT_SUB'] + '
    ' + + '
    ' + T['M_BAK_FULL_TIT'] + '
    ' + T['M_BAK_FULL_SUB'] + '
    ' + + '
    ' + + '
    ' + T['M_BAK_LIGHT_DESC'] + '
    ' + + '
    ', + okText: T['BTN_START_BAK'], + onOk: function() { + var bType = window._selectedBackupType; + + // 建立一个确保能执行真实备份 + var performBackup = function() { + var hintText = bType === 'full' ? T['M_BAK_HINT_FULL'] : T['M_BAK_HINT_LIGHT']; + openModal({ + title: T['M_BAK_GEN_TIT'], + msg: '
    ' + T['M_BAK_GEN_MSG'] + '

    ' + hintText + '
    ', + spin: true + }); + + callSmartBackup(bType).then(function(res) { + if (res && res.url) { + var isDone = false; + var checkTimer = setInterval(function() { + if (isDone) { clearInterval(checkTimer); return; } + callCheckBackup().then(function(cRes) { + // 判断running状态并动态生成终端机黑框 + if (cRes && cRes.status === 'running' && cRes.log) { + // 前端显示“Downloading”的行 + var cleanLog = cRes.log.split('\n').filter(function(line) { + return line.indexOf('Downloading') !== -1; + }).join('\n'); + + // 显示Downloading的日志 + if (cleanLog) { + var logEl = document.getElementById('nw-backup-log'); + if (!logEl) { + var modalMsg = document.querySelector('#nw-global-msg'); + if (modalMsg) { + logEl = document.createElement('div'); + logEl.id = 'nw-backup-log'; + logEl.style.cssText = 'margin-top:18px; padding:12px; background:#0f172a; color:#10b981; font-family:monospace; font-size:12px; text-align:left; border-radius:8px; white-space:pre-wrap; word-break:break-all; max-height:110px; overflow-y:hidden; box-shadow:inset 0 2px 4px rgba(0,0,0,0.5); line-height:1.5; border:1px solid #1e293b;'; + modalMsg.appendChild(logEl); + } + } + // 更新文字并自动滚动到底部 + if (logEl && logEl.innerText !== cleanLog) { + logEl.innerText = cleanLog; + logEl.scrollTop = logEl.scrollHeight; + } + } + } + else if (cRes && cRes.status === 'done' && !isDone) { + isDone = true; + clearInterval(checkTimer); + var a = document.createElement("a"); + a.href = res.url; + a.download = res.filename || "NetWiz_SmartGhost.tar.gz"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + openModal({ title: T['M_BAK_SUCC_TIT'], msg: T['M_BAK_SUCC_MSG'], hideCancel: true, okText: T['M_CLOSE'] }); + } + // 截后端的异常退出状态 (OOM 或其他致命错误) + else if (cRes && (cRes.status === 'error' || cRes.status === 'oom') && !isDone) { + isDone = true; + clearInterval(checkTimer); + openModal({ + title: T['M_OOM_TITLE'] || '⚠️ Backup Interrupted Warning', + msg: '
    ' + + (T['M_OOM_HEAD'] || 'Out of Memory (OOM Protection)!') + '
    ' + + '
    ' + + (T['M_OOM_DESC'] || 'The backup task has been canceled to prevent device crash.') + '
    ', + okText: T['M_I_KNOW'] || 'I Got It' + }); + } + }).catch(function() {}); + }, 2000); + } else { + openModal({ title: T['M_BAK_FAIL_TIT'], msg: T['M_BAK_FAIL_MSG'], hideCancel: true, okText: T['M_CLOSE'] }); + } + }).catch(function(err) { + openModal({ title: T['M_SYS_ERR'], msg: err, hideCancel: true, okText: T['M_CLOSE'] }); + }); + }; + + // 1. 显示前置扫描中动画 + openModal({ + title: T['M_BAK_GEN_TIT'], + msg: '
    ⏳ ' + (T['MSG_SCAN_PKGS'] || 'Scanning installed plugins...') + '
    ', + spin: true + }); + + // 2. 呼叫后端扫描 + // 成功渲染逻辑抽离为独立函数,方便首次尝试与后续自动轮询复用 + var handleMissingRes = function(res) { + var backupModal = document.getElementById('nw-global-modal'); + if (backupModal) backupModal.style.display = 'none'; + + var missing = res.missing || []; + var provided = res.provided || []; + var official = res.official || []; + + // --- 三色列表HTML渲染保持不变 --- + var missingHtml = ''; + if (missing.length > 0) { + missingHtml += '
    ❌ ' + (T['TXT_MISSING_PKGS'] || 'Missing packages:') + '
    '; + missingHtml += '
      '; + for (var i = 0; i < missing.length; i++) { missingHtml += '
    • ' + missing[i] + '
    • '; } + missingHtml += '
    '; + } + + var providedHtml = ''; + if (provided.length > 0) { + providedHtml += '
    ✅ ' + (T['TXT_PROVIDED_PKGS'] || 'Manually placed in custom_pkgs:') + '
    '; + providedHtml += '
      '; + for (var j = 0; j < provided.length; j++) { providedHtml += '
    • ' + provided[j] + '
    • '; } + providedHtml += '
    '; + } + + var officialHtml = ''; + if (official.length > 0) { + officialHtml += '
    ☁️ ' + (T['TXT_OFFICIAL_PKGS'] || 'Auto-backup plugins:') + '
    '; + officialHtml += '
      '; + for (var k = 0; k < official.length; k++) { officialHtml += '
    • ' + official[k] + '
    • '; } + officialHtml += '
    '; + } + + var totalCount = missing.length + provided.length + official.length; + var scrollWrapperStart = '
    '; + var scrollWrapperEnd = '
    '; + + if (missing.length > 0) { + var pkgListHtml = scrollWrapperStart + missingHtml + providedHtml + officialHtml + scrollWrapperEnd; + openModal({ + title: '⚠️ ' + (T['TIT_PKG_CHECK'] || 'Plugin Backup Status'), + msg: '
    ' + + (T['MSG_CUSTOM_PKG_DESC'] || 'Please verify backup packages:') + + pkgListHtml + + '' + + (T['MSG_CUSTOM_PKG_ACT'] || 'If you proceed, missing plugins WILL NOT be restored automatically!') + + '

    ' + + (T['MSG_CUSTOM_PKG_TIP'] || 'Tip: Manually place missing packages .ipk/.apk into the /etc/netwiz/custom_pkgs/ directory via SSH to ensure they are automatically reinstalled during future restorations.') + + '

    ', + okText: '🚀 ' + (T['BTN_IGNORE_BAK'] || 'Ignore & Backup'), + cancelText: T['BTN_CANCEL_RST'] || 'Cancel', + isDanger: true, + onOk: function() { performBackup(); } + }); + } else if (totalCount > 0) { + var pkgListHtml = scrollWrapperStart + providedHtml + officialHtml + scrollWrapperEnd; + var mTitle = provided.length > 0 ? (T['TIT_CUSTOM_PKG_READY'] || 'Custom Plugins Ready') : (T['TIT_OFFICIAL_PKG_READY'] || 'Plugin Scan Complete'); + var mDesc = provided.length > 0 ? (T['MSG_CUSTOM_PKG_READY_DESC'] || 'Great! Your custom plugins are safely stored and will be included in the backup capsule.') : (T['MSG_OFFICIAL_PKG_READY_DESC'] || 'All installed plugins are from the official repository and will be safely recorded.'); + + openModal({ + title: '✅ ' + mTitle, + msg: '
    ' + mDesc + pkgListHtml + '
    ', + okText: '📦 ' + (T['BTN_CONFIRM_BACKUP'] || 'Confirm Backup'), + cancelText: T['BTN_CANCEL_RST'] || 'Cancel', + onOk: function() { performBackup(); } + }); + } else { + performBackup(); + } + }; + + // 建立主动轮询流 + var isPolling = false; // 轮询开关 + + var doCheck = function() { + callCheckMissingPkgs().then(handleMissingRes).catch(function(err) { + var backupModal = document.getElementById('nw-global-modal'); + if (backupModal) backupModal.style.display = 'none'; + + isPolling = true; // 开启轮询状态 + openModal({ + title: T['M_FIRST_SYNC_TITLE'], + msg: '
    ' + T['M_FIRST_SYNC_SUB'] + '
    ' + + '
    ' + T['M_FIRST_SYNC_DESC'] + + '

    ' + (T['TXT_GETTING'] || 'Getting...') + ' .
    ', + okText: T['M_SYNC_OK'], + onOk: function() { + isPolling = false; // 点击停止,彻底关闭后续的联机请求 + } + }); + + var dots = 1; + + // 上一个请求彻底结束(超时或被后端秒拒),等待3秒发送下一个 + var pollNext = function() { + if (!isPolling) return; // 如果被停止,立刻终止 + + var dEl = document.getElementById('nw-sync-dots'); + if (dEl) { + dots = (dots % 3) + 1; + var dotStr = ''; + for(var i=0; i×' + T['M_RST_CONFIRM_TIT'] + '', + msg: '
    ' + (T['M_RST_CONFIRM_MSG'] || '') + + ' ' + + '
    ', + okText: T['BTN_CONFIRM_SEL'], + onOk: function() { + var chk = document.getElementById('chk-regret-pill'); + window._wantRegretPill = chk ? chk.checked : true; + fileSmartRestore.click(); + } + }); + + var btnRestoreClose = document.getElementById('btn-restore-close'); + if (btnRestoreClose) { + btnRestoreClose.addEventListener('click', function() { + document.getElementById('nw-global-modal').style.display = 'none'; + }); + } + }); + + fileSmartRestore.addEventListener('change', function(e) { + var file = e.target.files[0]; + if (!file) return; + + var startProcess = function() { + var execRestore = function() { + openModal({ + title: T['M_RST_NATIVE_TIT'], + msg: '
    ' + T['MSG_RESTORE_UPLOADING'] + '
    0%
    ', + spin: true + }); + + var executeRebootProbe = function(newIp) { + var rebootSec = 0; + var h = window.location.hostname; + var pEl = document.getElementById('nw-upload-progress'); + var isRedirecting = false; + + var isOffline = false; + var offlineCount = 0; + + var checkIp = function(targetIp, checkMode) { + var controller = new AbortController(); + var tid = setTimeout(function() { controller.abort(); }, 1500); + + fetch('http://' + targetIp + '/cgi-bin/luci/?_t=' + Date.now(), { + method: 'GET', signal: controller.signal, mode: 'no-cors' + }) + .then(function() { + clearTimeout(tid); + if (checkMode === 'wait_offline') { + offlineCount = 0; + } else if (checkMode === 'wait_online' && !isRedirecting) { + isRedirecting = true; + var doJump = function() { window.location.href = 'http://' + targetIp + '/cgi-bin/luci/'; }; + // 发送跨重启拆弹信号,成功或失败后跳转 + callNetDefuse().then(doJump).catch(doJump); + } + }).catch(function() { + clearTimeout(tid); + if (checkMode === 'wait_offline') { + offlineCount++; + if (offlineCount >= 2) isOffline = true; + } + }); + }; + + var rebootTimer = setInterval(function() { + if (isRedirecting) { clearInterval(rebootTimer); return; } + rebootSec += 2; + + if (!isOffline) { + if (pEl) pEl.innerHTML = '⏳ ' + (T['MSG_WAIT_OFFLINE'] || 'Waiting for device to disconnect...') + ' (' + rebootSec + 's)'; + checkIp(h, 'wait_offline'); + if (rebootSec > 60) isOffline = true; + } else { + if (pEl) pEl.innerHTML = '🔄 ' + (T['MSG_REBOOTING'] || 'System is rebooting...') + ' (' + rebootSec + 's / 300s)'; + checkIp(h, 'wait_online'); + if (newIp && newIp !== h && newIp !== 'undefined' && newIp !== '') checkIp(newIp, 'wait_online'); + + if (rebootSec > 300) { + isRedirecting = true; + clearInterval(rebootTimer); + if (pEl) pEl.innerHTML = '⚠️ ' + (T['MSG_MANUAL_VISIT'] || 'If IP changed, please update PC IP and visit manually.') + ''; + } + } + }, 2000); + }; + + var fd = new FormData(); + var sid = (typeof L !== 'undefined' && L.env && L.env.sessionid) ? L.env.sessionid : ""; + if (!sid) { + var match = document.cookie.match(/sysauth_http=([^;]+)/) || document.cookie.match(/sysauth=([^;]+)/); + if (match) sid = match[1]; + } + + fd.append("sessionid", sid); + fd.append("filename", "/tmp/NetWiz_SmartGhost.tar.gz"); + fd.append("file", file); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/cgi-bin/cgi-upload', true); + + xhr.upload.onprogress = function(evt) { + if (evt.lengthComputable) { + var percent = Math.floor((evt.loaded / evt.total) * 100); + var pEl = document.getElementById('nw-upload-progress'); + if (pEl) pEl.innerText = percent + '%'; + } + }; + + xhr.onload = function() { + if (xhr.status === 200) { + var realPath = "/tmp/NetWiz_SmartGhost.tar.gz"; + try { + var res = JSON.parse(xhr.responseText); + if (res.filepath || res.file || res.path) realPath = res.filepath || res.file || res.path; + } catch(e) {} + + var pEl = document.getElementById('nw-upload-progress'); + if (pEl) pEl.innerHTML = '' + T['M_RST_DELIVERED'] + ''; + + callSmartRestoreExec(realPath).then(function() { + var errCount = 0; + var futureIp = ''; + var checkTimer = setInterval(function() { + callCheckRestoreStatus().then(function(res) { + errCount = 0; + + // 安全解包。处理 LuCI RPC 底层可能返回数组 [0, {data}] 的情况 + var data = (Array.isArray(res) && res.length > 1) ? res[1] : (res || {}); + + var s = data.status; + var code = data.code; + var arg1 = data.arg1 || ''; + var arg2 = data.arg2 || ''; + + // 拦截空数据,防止后端刚启动没来得及写文件时,前端渲染出 undefined + if (!s || !code) return; + + var m = T[code] || code; + + // 占位符替换 + if (code === 'MSG_RST_OOM_INTERCEPT') { + m = m.replace('{u}', arg1).replace('{a}', arg2); + } else if (code === 'MSG_RST_DONE') { + if (arg1) m = m.replace('{arg1}', arg1); + } + + if (!m || m === 'undefined') m = T['MSG_RST_WAIT']; + + if (s === 'running') { + if (pEl) pEl.innerHTML = '🔄 ' + m + ''; + } else if (s === 'error') { + clearInterval(checkTimer); + fileSmartRestore.value = ''; + openModal({ + title: T['M_RST_BLOCKED_TIT'], + msg: '
    ' + m + '
    ' + T['M_RST_BLOCKED_MSG'] + '
    ', + hideCancel: true, + okText: T['M_CLOSE'] + }); + } else if (s === 'done') { + clearInterval(checkTimer); + if (pEl) pEl.innerHTML = '🎉 ' + m + ''; + futureIp = arg1; + executeRebootProbe(futureIp); + } + }).catch(function() { + errCount++; + if (errCount >= 3) { + clearInterval(checkTimer); + if (pEl) pEl.innerHTML = '🎉 ' + (T['MSG_RST_DONE'] || 'Restore thoroughly complete! Router will auto-reboot!') + ''; + executeRebootProbe(futureIp); + } + }); + }, 2500); + }).catch(function() { + executeRebootProbe(''); + }); + } else { + fileSmartRestore.value = ''; + openModal({ title: T['M_RST_TRANS_FAIL'], msg: T['M_RST_TRANS_REJECT'] + xhr.status, hideCancel: true, okText: T['M_CLOSE'] }); + } + }; + + xhr.onerror = function() { + fileSmartRestore.value = ''; + openModal({ title: T['M_RST_NET_ERR'], msg: T['M_RST_NET_INTR'], hideCancel: true, okText: T['M_CLOSE'] }); + }; + + xhr.send(fd); + }; + + if (window._wantRegretPill) { + openModal({ + title: T['M_RST_PILL_TIT'], + msg: '
    ' + T['M_RST_PILL_MSG'] + '
    ', + spin: true + }); + + callSmartBackup('light').then(function(res) { + if (res && res.url) { + var isRegretDone = false; + var checkTimer = setInterval(function() { + if (isRegretDone) { clearInterval(checkTimer); return; } + callCheckBackup().then(function(cRes) { + // 判断running状态并动态生成终端机黑框 + if (cRes && cRes.status === 'running' && cRes.log) { + // 前端显示“Downloading”的行 + var cleanLog = cRes.log.split('\n').filter(function(line) { + return line.indexOf('Downloading') !== -1; + }).join('\n'); + + // 显示Downloading的日志 + if (cleanLog) { + var logEl = document.getElementById('nw-backup-log'); + if (!logEl) { + var modalMsg = document.querySelector('#nw-global-msg'); + if (modalMsg) { + logEl = document.createElement('div'); + logEl.id = 'nw-backup-log'; + logEl.style.cssText = 'margin-top:18px; padding:12px; background:#0f172a; color:#10b981; font-family:monospace; font-size:12px; text-align:left; border-radius:8px; white-space:pre-wrap; word-break:break-all; max-height:110px; overflow-y:hidden; box-shadow:inset 0 2px 4px rgba(0,0,0,0.5); line-height:1.5; border:1px solid #1e293b;'; + modalMsg.appendChild(logEl); + } + } + // 更新文字并自动滚动到底部 + if (logEl && logEl.innerText !== cleanLog) { + logEl.innerText = cleanLog; + logEl.scrollTop = logEl.scrollHeight; + } + } + } + else if (cRes && cRes.status === 'done' && !isDone) { + isDone = true; + clearInterval(checkTimer); + var a = document.createElement("a"); + a.href = res.url; + a.download = res.filename || "NetWiz_SmartGhost.tar.gz"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + openModal({ title: T['M_BAK_SUCC_TIT'], msg: T['M_BAK_SUCC_MSG'], hideCancel: true, okText: T['M_CLOSE'] }); + } + else if (cRes && (cRes.status === 'error' || cRes.status === 'oom') && !isDone) { + isDone = true; + clearInterval(checkTimer); + openModal({ + title: T['M_OOM_TITLE'] || '⚠️ Backup Interrupted Warning', + msg: '
    ' + (T['M_OOM_HEAD'] || 'Out of Memory!') + '
    ', + okText: T['M_I_KNOW'] || 'I Got It' + }); + } + }).catch(function() {}); + }, 2000); + } else { + execRestore(); + } + }).catch(function() { + execRestore(); + }); + } else { + execRestore(); + } + }; + + // ========================================== + // “智能指纹校验 + 容量探测”逻辑 + // ========================================== + + var fileName = file.name; + var lowerFileName = fileName.toLowerCase(); // 统一转为小写 + var curPkg = window.nwCurrentPkg || 'ipk'; + var curArch = window.nwCurrentArch || ''; + + var isApkBackup = lowerFileName.indexOf('_apk_') !== -1; + var isIpkBackup = lowerFileName.indexOf('_ipk_') !== -1; + + // 核心推进函数 + var doNext = function() { + var sizeMB = (file.size / (1024 * 1024)).toFixed(1); + openModal({ + title: T['M_RST_PROBE_TIT'], + msg: '
    ' + T['M_RST_PROBE_MSG'] + '
    ', + spin: true + }); + callCheckStorage().then(function(res) { + var availMB = res.tmp_avail_mb || 60; + if (sizeMB > (availMB * 0.85)) { + openModal({ + title: T['M_RST_OOM_TIT'], + msg: '
    ' + T['M_RST_OOM_MSG'].replace('{size}', sizeMB).replace('{avail}', availMB) + '
    ', + hideCancel: true, okText: T['BTN_CANCEL_RST'], isDanger: true, + onOk: function() { + fileSmartRestore.value = ''; + document.getElementById('nw-global-modal').style.display = 'none'; + } + }); + } else { + startProcess(); + } + }).catch(function() { startProcess(); }); + }; + + // 前端 Fail-Fast 拦截防线 + if (lowerFileName.indexOf('netwiz_') !== -1) { // 改用小写匹配 + + // 1. 检查包管理器是否冲突 (apk vs ipk) + if ((curPkg === 'apk' && isIpkBackup) || (curPkg === 'ipk' && isApkBackup)) { + var errMsg = (curPkg === 'apk') ? (T['MSG_PKG_ERR_APK'] || '') : (T['MSG_PKG_ERR_OPKG'] || ''); + openModal({ + title: '🚨 ' + (T['TIT_PKG_CONFLICT'] || 'Package Manager Conflict'), + msg: '
    ' + errMsg + '
    ' + + '
    ' + + (T['MSG_RESTORE_ARCH_TIP'] || 'Safety Lock: Cross-package manager restoration is strictly blocked to prevent network crash and bricking.') + '
    ', + okText: T['M_I_KNOW'] || 'I Got It', + hideCancel: true, // 隐藏取消按钮 + isDanger: true, + onOk: function() { + fileSmartRestore.value = ''; // 清空文件选择 + document.getElementById('nw-global-modal').style.display = 'none'; // 直接关闭弹窗,不执行 doNext() + } + }); + return; // 彻底死锁拦截 + } + + // 2. 检查 CPU 架构是否冲突 + if (curArch && lowerFileName.indexOf(curArch.toLowerCase()) === -1) { // 架构检查转为小写对比 + var warnMsg1 = T['MSG_ARCH_WARN_1'] ? T['MSG_ARCH_WARN_1'].replace('{arch}', curArch) : 'Architecture mismatch!'; + var warnMsg2 = T['MSG_ARCH_WARN_2'] || 'If you manually renamed this file, ignore this.'; + openModal({ + title: '⚠️ ' + (T['TIT_ARCH_WARN'] || 'Architecture Warning'), + msg: '
    ' + warnMsg1 + '

    ' + warnMsg2 + '
    ', + okText: T['BTN_WARN_CONTINUE'] || 'Continue', + cancelText: T['BTN_CANCEL_RST'] || 'Cancel', + isDanger: true, + onOk: function() { + doNext(); // 继续 + } + }); + return; // 拦截,等待用户选择 + } + } + + // ✅ 架构完全一致,或者无法识别文件名,静默执行下一步 + doNext(); + }); + } + + updateStatusDisplay(false); + setInterval(function() { if (step1.style.display !== 'none' && container.querySelector('#nw-global-modal').style.display === 'none') updateStatusDisplay(true); }, 5000); + + function calculateNetmask(ip) { if (!ip) return '255.255.255.0'; var b = parseInt(ip.split('.')[0], 10); if (b >= 1 && b <= 126) return '255.0.0.0'; if (b >= 128 && b <= 191) return '255.255.0.0'; return '255.255.255.0'; } + function isValidIP(ip) { if (!ip) return false; var p = ip.split('.'); if (p.length !== 4) return false; for (var i = 0; i < 4; i++) { var n = parseInt(p[i], 10); if (isNaN(n) || n < 0 || n > 255 || String(n) !== p[i]) return false; } if (p[0] === '0' || p[0] === '127') return false; var l = parseInt(p[3], 10); return (l !== 0 && l !== 255); } + // 主路由模式 - 向上递增避让算法 (1.1 -> 2.1 -> 3.1) + function getSafeRouterIp(wanIp) { + if (!wanIp) return '192.168.2.1'; + var wanSubnet = parseInt(wanIp.split('.')[2], 10); + var safeSubnet = 1; // 默认从 192.168.1.1 开始试探 + while (safeSubnet === wanSubnet || safeSubnet === 0) { + safeSubnet++; + if (safeSubnet > 250) break; // 极值保护 + } + return '192.168.' + safeSubnet + '.1'; + } + + // 旁路由/AP模式 - 向下递减避让算法 (.254 -> .253) + function getSafeApIp(gatewayIp) { + if (!gatewayIp) return ''; + var parts = gatewayIp.split('.'); + var prefix = parts[0] + '.' + parts[1] + '.' + parts[2]; + var gwTail = parseInt(parts[3], 10); + + var knownTails = [gwTail]; + if (window._liveWanIp && window._liveWanIp.indexOf(prefix) === 0) { + knownTails.push(parseInt(window._liveWanIp.split('.')[3], 10)); + } + + if (window._nwConflictBlacklist) { + knownTails = knownTails.concat(window._nwConflictBlacklist); + } + + var safeTail = 254; + while (knownTails.indexOf(safeTail) !== -1 && safeTail > 200) { + safeTail--; + } + return prefix + '.' + safeTail; + } + function isSameSubnet(ip1, ip2) { if (!ip1 || !ip2) return false; var p1 = ip1.split('.'), p2 = ip2.split('.'); return (p1.length === 4 && p2.length === 4 && p1[0] === p2[0] && p1[1] === p2[1] && p1[2] === p2[2]); } + + function openModal(o) { + var m = container.querySelector('#nw-global-modal'); + container.querySelector('#nw-global-title').innerHTML = o.title || ''; + container.querySelector('#nw-global-msg').innerHTML = o.msg || ''; + container.querySelector('#nw-global-spinner').style.display = o.spin ? 'block' : 'none'; + var w = container.querySelector('#nw-global-btn-wrap'), ok = container.querySelector('#nw-global-btn-ok'), can = container.querySelector('#nw-global-btn-cancel'); + w.style.display = (o.okText || o.cancelText) ? 'flex' : 'none'; + if (o.okText) { ok.style.display = 'block'; ok.innerText = o.okText; ok.className = 'nw-u-btn ' + (o.isDanger ? 'nw-u-btn-red' : 'nw-u-btn-blue'); ok.onclick = function() { if (o.onOk) o.onOk(); else m.style.display = 'none'; }; } else ok.style.display = 'none'; + if (o.cancelText) { can.style.display = 'block'; can.innerText = o.cancelText; can.onclick = function() { if (o.onCancel) o.onCancel(); else m.style.display = 'none'; }; } else can.style.display = 'none'; + m.style.display = 'flex'; + } + + function returnToStep1() { container.querySelector('#nw-global-modal').style.display = 'none'; step3.style.display = 'none'; step2.style.display = 'none'; step1.style.display = 'block'; } + + container.querySelectorAll('.nw-adv-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + var panel = this.nextElementSibling; + if (panel.style.display === 'none') { + panel.style.display = 'block'; + this.innerHTML = '▲ ' + T['LBL_ADVANCED_CLOSE']; + } else { + panel.style.display = 'none'; + this.innerHTML = '▼ ' + T['LBL_ADVANCED']; + } + }); + }); + + var modeWarnHandler = function(e) { + var el = e.target; + if (el.value !== 'auto') { + openModal({ + title: T['M_MODE_WARN_TIT'], + msg: T['M_MODE_WARN_MSG'], + cancelText: T['M_CLOSE'], + okText: T['M_WARN_BTN'], + isDanger: true, + onCancel: function() { el.value = 'auto'; container.querySelector('#nw-global-modal').style.display = 'none'; }, + onOk: function() { container.querySelector('#nw-global-modal').style.display = 'none'; } + }); + } + }; + container.querySelector('#wifi-2g-mode').addEventListener('change', modeWarnHandler); + container.querySelector('#wifi-5g-mode').addEventListener('change', modeWarnHandler); + + container.querySelectorAll('input[name="router_type"]').forEach(function(r) { r.addEventListener('change', function() { container.querySelector('#router-static-fields').style.display = (this.value === 'static') ? 'block' : 'none'; }); }); + + var bypassToggle = container.querySelector('#lan-bypass-toggle'); + bypassToggle.addEventListener('change', function() { container.querySelector('#lan-bypass-warning').style.display = this.checked ? 'block' : 'none'; container.querySelector('#lan-main-warning').style.display = this.checked ? 'none' : 'block'; }); + // ===== 一键探测并分配 IP 逻辑 ===== + var btnAutoIp = container.querySelector('#btn-auto-ip'); + if (btnAutoIp) { + btnAutoIp.addEventListener('click', function() { + // 1. 拦截一:检查是否开启了旁路由模式 + var bypassTog = document.getElementById('lan-bypass-toggle'); + if (bypassTog && !bypassTog.checked) { + openModal({ + title: '⚠️ ' + (T['M_DETECT_OP_INV_TIT'] || 'Invalid Operation'), + msg: T['M_DETECT_OP_INV_MSG'] || 'Gateway must be blank in Main Router mode.', + hideCancel: true, + okText: T['M_CLOSE'] || 'Close' + }); + return; + } + + var wProto = 'dhcp'; + try { wProto = uci.get('network', 'wan', 'proto'); } catch(e) {} + var currentUrlIp = window.location.hostname; + var gw = window._realUpstreamGw; + + // 2. 拦截二:检查是否为 PPPoE 拨号(防止桥接公网基站) + if (wProto === 'pppoe') { + var pppoeMsg = (T['M_DETECT_PPPOE_MSG'] || '').replace('{gw}', gw || ''); + openModal({ + title: '⚠️ ' + (T['M_DETECT_PPPOE_TIT'] || 'Critical Intercept'), + msg: pppoeMsg, + hideCancel: true, + okText: T['M_CLOSE'] || 'Close' + }); + return; + } + + // 3. 正常探测逻辑 + if (!gw || gw === T['TXT_GETTING'] || gw === T['TXT_NOT_SET']) { + // 确保 URL 是数字 IP 格式,绝对不能用域名来切分网关! + if (/^(\d{1,3}\.){3}\d{1,3}$/.test(currentUrlIp)) { + gw = currentUrlIp.substring(0, currentUrlIp.lastIndexOf('.') + 1) + '1'; + } else { + gw = ''; // 域名登录,直接留空 + } + } + + if (gw && gw.indexOf('.') > -1) { + var suggestedIp = getSafeApIp(gw); + + var inputGw = document.getElementById('lan-gw'); + var inputIp = document.getElementById('lan-ip'); + if (inputGw) inputGw.value = gw; + if (inputIp) inputIp.value = suggestedIp; + + openModal({ + title: '✅ ' + (T['BTN_AUTO_DETECT'] || 'Detection Success'), + msg: '
    ' + (T['MSG_DETECT_SUCC'] || 'Upstream subnet detected') + '
    ' + + '
    ' + + (T['LBL_LAN_IP'] || 'IP') + ': ' + suggestedIp + '
    ' + + (T['LBL_LAN_GW'] || 'Gateway') + ': ' + gw + '
    ', + hideCancel: true, + okText: T['M_CLOSE'] || 'Close' + }); + + var checkIpAsync = function(targetIp, targetGw) { + callCheckIpConflict(targetIp).then(function(res) { + if (res.status === 'conflict') { + if (!window._nwConflictBlacklist) window._nwConflictBlacklist = []; + window._nwConflictBlacklist.push(parseInt(targetIp.split('.')[3], 10)); + + var newSafeIp = getSafeApIp(targetGw); + + openModal({ + title: '⚠️ ' + (T['TIT_IP_CONFLICT'] || 'IP Conflict Warning'), + msg: '
    ' + targetIp + ' ' + (T['MSG_IP_IN_USE'] || 'is already used by another device!') + '
    ' + + '
    ' + (T['MSG_SUGGEST_FIX'] || 'We strongly recommend changing it to avoid network crashes.') + '
    ', + okText: '🚀 ' + (T['BTN_FIX_IP'] || 'Fix to') + ' ' + newSafeIp, + cancelText: T['M_CANCEL'] || 'Ignore', + isDanger: true, + onOk: function() { + if (inputIp) inputIp.value = newSafeIp; + checkIpAsync(newSafeIp, targetGw); + } + }); + } + }); + }; + + // 发射背景探针 + checkIpAsync(suggestedIp, gw); + } else { + openModal({ + title: '❌ ' + (T['M_SYS_ERR'] || 'failed'), + msg: T['MSG_DETECT_FAIL'] || 'Detection failed', + hideCancel: true, + okText: T['M_CLOSE'] || 'Close' + }); + } + }); + } + // ================================== + + var smartToggle = container.querySelector('#wifi-smart-toggle'); + var legacyToggle = container.querySelector('#legacy-b-toggle'); + var en2g = container.querySelector('#wifi-2g-en'); + var en5g = container.querySelector('#wifi-5g-en'); + + // ===== 动态刷新漫游状态徽章 ===== + var updateRoamBadge = function(togId) { + var tog = container.querySelector(togId); + if (!tog) return; + var keyId = togId.replace('-roaming', '-key'); + var pwdInput = container.querySelector(keyId); + var pwdRow = pwdInput ? pwdInput.closest('.nw-value') : null; + + if (pwdRow) { + var statRow = pwdRow.nextElementSibling; + // 如果没有框,创建一个 + if (!statRow || !statRow.classList.contains('nw-roam-status-row')) { + statRow = document.createElement('div'); + statRow.className = 'nw-roam-status-row'; + statRow.style.cssText = 'margin-top: 5px; margin-bottom: 15px; '; + pwdRow.parentNode.insertBefore(statRow, pwdRow.nextSibling); + } + + if (tog.checked) { + var isDirty = tog.classList.contains('is-dirty'); + if (isDirty) { + statRow.innerHTML = "" + (T['TXT_ROAMING_ON']||'Roaming Enabled') + "! " + (T['TXT_CLICK_FIX']||'Click to Fix') + ""; + } else { + statRow.innerHTML = "" + (T['TXT_ROAMING_ON']||'Roaming Enabled') + ""; + } + + var badgeSpan = statRow.querySelector('span'); + badgeSpan.onmouseover = function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 12px rgba(16,185,129,0.25)'; }; + badgeSpan.onmouseout = function() { this.style.transform = 'none'; this.style.boxShadow = 'none'; }; + + badgeSpan.onclick = function(e) { + e.stopPropagation(); + if (isDirty) alert(T['DESC_ROAM_DIRTY']); + var advPanel = tog.closest('.nw-adv-panel'); + var advBtn = advPanel ? advPanel.previousElementSibling : null; + if (advPanel && advPanel.style.display === 'none' && advBtn) advBtn.click(); + setTimeout(function() { + tog.scrollIntoView({ behavior: 'smooth', block: 'center' }); + var targetRow = tog.closest('div'); + if (targetRow) { + var oldBg = targetRow.style.backgroundColor || 'transparent'; + targetRow.style.transition = 'background-color 0.4s ease'; + targetRow.style.backgroundColor = 'rgba(16, 185, 129, 0.25)'; + targetRow.style.borderRadius = '8px'; + setTimeout(function() { + targetRow.style.backgroundColor = oldBg; + setTimeout(function() { targetRow.style.transition = ''; }, 400); + }, 800); + } + }, 80); + }; + statRow.style.display = 'block'; // 显示徽章 + } else { + statRow.innerHTML = ""; + statRow.style.display = 'none'; // 彻底隐藏徽章 + } + } + }; + // ========================================== + + // ===== 密码与加密方式双向联动 ===== + var syncEncryption = function(keyInputId, encSelectId) { + var keyEl = container.querySelector(keyInputId); + var encEl = container.querySelector(encSelectId); + if (keyEl && encEl) { + // 1. 密码框输入 -> 影响下拉框 + keyEl.addEventListener('input', function() { + if (this.value.length > 0 && encEl.value === 'none') { + encEl.value = 'psk2+ccmp'; + } else if (this.value.length === 0 && encEl.value !== 'none') { + encEl.value = 'none'; + } + }); + // 2. 下拉框选择 -> 影响密码框 + encEl.addEventListener('change', function() { + if (this.value === 'none') { + keyEl.value = ''; // 手动选了无密码,清空密码框,防止数据残留 + } + }); + } + }; + + // 绑定三个面板的密码输入框和加密下拉框 + syncEncryption('#wifi-smart-key', '#wifi-smart-enc'); + syncEncryption('#wifi-2g-key', '#wifi-2g-enc'); + syncEncryption('#wifi-5g-key', '#wifi-5g-enc'); + // ===== 结束 ===== + + // 联动与自动切换标签页 + en2g.addEventListener('change', function() { + // 2.4G 任何时候开启或关闭,都强制关闭漫游(保障老旧智能家居兼容性) + var r2 = container.querySelector('#wifi-2g-roaming'); + if (r2) { r2.checked = false; r2.dispatchEvent(new Event('change')); } + + // Tab 跳转:开启时留在本页,关闭时自动跳到其他开启的频段 + if (this.checked) { + container.querySelector('#tab-2g').click(); + } else { + var en5g2El = container.querySelector('#wifi-5g2-en'); + if (en5g.checked) container.querySelector('#tab-5g').click(); + else if (en5g2El && en5g2El.checked) { var t = container.querySelector('#tab-5g2'); if (t) t.click(); } + else container.querySelector('#tab-2g').click(); + } + + if (this.checked && window._isSingleChip) { + en5g.checked = false; + var s2El = container.querySelector('#wifi-2g-ssid'); + var s5 = container.querySelector('#wifi-5g-ssid').value; + // 不仅为空时推断,名字一样,加后缀拆分 + if ((!s2El.value || s2El.value === s5) && s5) { + s2El.value = smartConvertSsid(s5, '2g'); + if (!container.querySelector('#wifi-2g-key').value) container.querySelector('#wifi-2g-key').value = container.querySelector('#wifi-5g-key').value; + container.querySelector('#wifi-2g-enc').value = container.querySelector('#wifi-5g-enc').value; + } + } + }); + + en5g.addEventListener('change', function() { + // 5G 开启时自动开启漫游,关闭时联动关闭漫游 + var r5 = container.querySelector('#wifi-5g-roaming'); + if (r5) { r5.checked = this.checked; r5.dispatchEvent(new Event('change')); } + + // Tab 跳转 + if (this.checked) { + container.querySelector('#tab-5g').click(); + } else { + var en5g2El = container.querySelector('#wifi-5g2-en'); + if (en2g.checked) container.querySelector('#tab-2g').click(); + else if (en5g2El && en5g2El.checked) { var t = container.querySelector('#tab-5g2'); if (t) t.click(); } + else container.querySelector('#tab-5g').click(); + } + + if (this.checked && window._isSingleChip) { + en2g.checked = false; + var s5El = container.querySelector('#wifi-5g-ssid'); + var s2 = container.querySelector('#wifi-2g-ssid').value; + if ((!s5El.value || cleanSsidSuffix(s5El.value) === cleanSsidSuffix(s2)) && s2) { + s5El.value = smartConvertSsid(s2, '5g'); + if (!container.querySelector('#wifi-5g-key').value) container.querySelector('#wifi-5g-key').value = container.querySelector('#wifi-2g-key').value; + container.querySelector('#wifi-5g-enc').value = container.querySelector('#wifi-2g-enc').value; + } + } + + // 开启 5G 时,赋上wifi-5g2-ssid 的值 + if (this.checked && !window._isSingleChip) { + var s5g2El = container.querySelector('#wifi-5g2-ssid'); + var s5 = container.querySelector('#wifi-5g-ssid').value; + if (s5g2El && s5 && !s5g2El.value) { + s5g2El.value = smartConvertSsid(s5, '5g2'); + if (!container.querySelector('#wifi-5g2-key').value) container.querySelector('#wifi-5g2-key').value = container.querySelector('#wifi-5g-key').value; + } + } + }); + + // 5G_Game 开关被点击时,跳转到第三个标签页 + var en5g2 = container.querySelector('#wifi-5g2-en'); + if (en5g2) { + en5g2.addEventListener('change', function() { + // Tab 跳转 + if (this.checked) { + var t = container.querySelector('#tab-5g2'); + if (t) t.click(); + } else { + if (en2g.checked) container.querySelector('#tab-2g').click(); + else if (en5g.checked) container.querySelector('#tab-5g').click(); + else { var t = container.querySelector('#tab-5g2'); if (t) t.click(); } + } + + // 联动:开启时以 5G (或 2.4G) 为基准自动补齐 + if (this.checked) { + var s5g2El = container.querySelector('#wifi-5g2-ssid'); + var s5 = container.querySelector('#wifi-5g-ssid').value; + var s2 = container.querySelector('#wifi-2g-ssid').value; + var refSsid = s5 || s2; // 优先学 5G,没有 5G 就学 2.4G + var refKey = container.querySelector('#wifi-5g-key').value || container.querySelector('#wifi-2g-key').value; + + if (s5g2El && refSsid && (!s5g2El.value || cleanSsidSuffix(s5g2El.value) === cleanSsidSuffix(refSsid))) { + s5g2El.value = smartConvertSsid(refSsid, '5g2'); + if (!container.querySelector('#wifi-5g2-key').value) container.querySelector('#wifi-5g2-key').value = refKey; + } + } + }); + } + + smartToggle.addEventListener('change', function(e) { + var isSmart = this.checked; + var smartUi = container.querySelector('#wifi-smart-ui'); + var splitUi = container.querySelector('#wifi-split-ui'); + + if (isSmart) { + // 切换为多频合一 + smartUi.style.display = 'block'; + splitUi.style.display = 'none'; + + if (!e.isTrusted) return; // 防止页面加载时覆盖底层数据 + + // 获取已开启频段的信息(优先提取 5G 的名字) + var en2 = container.querySelector('#wifi-2g-en').checked; + var en5 = container.querySelector('#wifi-5g-en').checked; + var pickBand = (en5 || !en2) ? '5g' : '2g'; + + var pickSsid = container.querySelector('#wifi-' + pickBand + '-ssid').value; + var pickKey = container.querySelector('#wifi-' + pickBand + '-key').value; + var pickEnc = container.querySelector('#wifi-' + pickBand + '-enc').value; + + // 智能联动 1:合一模式下,强制自动“剥离” 2.4G/5G 等后缀 + var smartSsidEl = container.querySelector('#wifi-smart-ssid'); + if (pickSsid) { + smartSsidEl.value = cleanSsidSuffix(pickSsid); + container.querySelector('#wifi-smart-key').value = pickKey; + container.querySelector('#wifi-smart-enc').value = pickEnc; + } + + // 切换为合一模式时,自动开启无缝漫游 + var rSmartEl = container.querySelector('#wifi-smart-roaming'); + if (rSmartEl) { rSmartEl.checked = true; rSmartEl.dispatchEvent(new Event('change')); } + + } else { + // 切换为独立频段 + smartUi.style.display = 'none'; + splitUi.style.display = 'block'; + + // 页面加载或手动切换时,自动判定并跳转到“已开启”的频段标签页 + var jumpToActiveTab = function() { + var e2 = container.querySelector('#wifi-2g-en').checked; + var e5 = container.querySelector('#wifi-5g-en').checked; + var e5g2El = container.querySelector('#wifi-5g2-en'); + var e5g2 = e5g2El ? e5g2El.checked : false; + + if (e2) container.querySelector('#tab-2g').click(); + else if (e5) container.querySelector('#tab-5g').click(); + else if (e5g2) { var t = container.querySelector('#tab-5g2'); if (t) t.click(); } + else container.querySelector('#tab-2g').click(); // 兜底 + }; + + if (!e.isTrusted) { + jumpToActiveTab(); + return; + } + + // 联动 2:分开模式下,强制提取合一名称,并自动“追加”各自的后缀 + var baseSsid = container.querySelector('#wifi-smart-ssid').value; + var baseKey = container.querySelector('#wifi-smart-key').value; + var baseEnc = container.querySelector('#wifi-smart-enc').value; + + container.querySelector('#wifi-2g-ssid').value = smartConvertSsid(baseSsid, '2g'); + container.querySelector('#wifi-2g-key').value = baseKey; + container.querySelector('#wifi-2g-enc').value = baseEnc; + + container.querySelector('#wifi-5g-ssid').value = smartConvertSsid(baseSsid, '5g'); + container.querySelector('#wifi-5g-key').value = baseKey; + container.querySelector('#wifi-5g-enc').value = baseEnc; + + var s5g2El = container.querySelector('#wifi-5g2-ssid'); + if (s5g2El) { + s5g2El.value = smartConvertSsid(baseSsid, '5g2'); + container.querySelector('#wifi-5g2-key').value = baseKey; + container.querySelector('#wifi-5g2-enc').value = baseEnc; + } + + // 多频合一切回分开恢复漫游策略 + var r2gEl = container.querySelector('#wifi-2g-roaming'); + if (r2gEl) { r2gEl.checked = false; r2gEl.dispatchEvent(new Event('change')); } + var r5gEl = container.querySelector('#wifi-5g-roaming'); + if (r5gEl) { r5gEl.checked = true; r5gEl.dispatchEvent(new Event('change')); } + + // 联动完成后,执行跳转 + jumpToActiveTab(); + } + }); + + legacyToggle.addEventListener('change', function() { + if (this.checked && smartToggle.checked) { + smartToggle.checked = false; + smartToggle.dispatchEvent(new Event('change')); + } + }); + + // ================= 2.4G 标签页点击事件 ================= + container.querySelector('#tab-2g').addEventListener('click', function() { + // 1. 切换 Tab 颜色 + this.style.background = '#3b82f6'; this.style.color = '#fff'; + container.querySelector('#tab-5g').style.background = '#f1f5f9'; container.querySelector('#tab-5g').style.color = '#475569'; + var t5g2 = container.querySelector('#tab-5g2'); + if (t5g2) { t5g2.style.background = '#f1f5f9'; t5g2.style.color = '#475569'; } + + // 2. 切换表单显示 + container.querySelector('#wifi-2g-form').style.display = 'block'; + container.querySelector('#wifi-5g-form').style.display = 'none'; + var f5g2 = container.querySelector('#wifi-5g2-form'); + if (f5g2) f5g2.style.display = 'none'; + + // 3. 为空或者同名时触发 2.4G 后缀保护与密码同步 + var s2El = container.querySelector('#wifi-2g-ssid'); + var s5 = container.querySelector('#wifi-5g-ssid').value; + if ((!s2El.value || cleanSsidSuffix(s2El.value) === cleanSsidSuffix(s5)) && s5) { + s2El.value = smartConvertSsid(s5, '2g'); + if(!container.querySelector('#wifi-2g-key').value) { + container.querySelector('#wifi-2g-key').value = container.querySelector('#wifi-5g-key').value; + } + } + }); + // ================================================================ + + container.querySelector('#tab-5g').addEventListener('click', function(){ + this.style.background='#3b82f6'; this.style.color='#fff'; + container.querySelector('#tab-2g').style.background='#f1f5f9'; container.querySelector('#tab-2g').style.color='#475569'; + var t5g2 = container.querySelector('#tab-5g2'); if(t5g2) { t5g2.style.background='#f1f5f9'; t5g2.style.color='#475569'; } + container.querySelector('#wifi-5g-form').style.display='block'; + container.querySelector('#wifi-2g-form').style.display='none'; + var f5g2 = container.querySelector('#wifi-5g2-form'); if(f5g2) f5g2.style.display='none'; + + // 联动:为空或者同主体名时触发 5G 后缀保护与密码同步 + var s5El = container.querySelector('#wifi-5g-ssid'); + var s2 = container.querySelector('#wifi-2g-ssid').value; + if ((!s5El.value || cleanSsidSuffix(s5El.value) === cleanSsidSuffix(s2)) && s2) { + s5El.value = smartConvertSsid(s2, '5g'); + if (!container.querySelector('#wifi-5g-key').value) { + container.querySelector('#wifi-5g-key').value = container.querySelector('#wifi-2g-key').value; + } + } + }); + + // 第三个 Tab 点击事件 + var tab5g2El = container.querySelector('#tab-5g2'); + if(tab5g2El) { + tab5g2El.addEventListener('click', function(){ + this.style.background='#3b82f6'; this.style.color='#fff'; + container.querySelector('#tab-2g').style.background='#f1f5f9'; container.querySelector('#tab-2g').style.color='#475569'; + container.querySelector('#tab-5g').style.background='#f1f5f9'; container.querySelector('#tab-5g').style.color='#475569'; + var f5g2 = container.querySelector('#wifi-5g2-form'); if(f5g2) f5g2.style.display='block'; + container.querySelector('#wifi-2g-form').style.display='none'; + container.querySelector('#wifi-5g-form').style.display='none'; + }); + } + + // ===== 漫游与加密方式联动 ===== + // 1. 多频合一面板联动 + var smartRoamingToggle = container.querySelector('#wifi-smart-roaming'); + if (smartRoamingToggle) { + smartRoamingToggle.addEventListener('change', function(e) { + // 移除 isTrusted 限制,允许程序自动切换时触发联动修复 + if (this.classList.contains('is-dirty')) { + this.classList.remove('is-dirty'); + window._forceWifiSubmit = true; + } + var warn = container.querySelector('#roam-warn-smart'); + if (warn) warn.style.display = 'none'; + + if (this.checked) { + var encSelect = container.querySelector('#wifi-smart-enc'); + if (encSelect && encSelect.value === 'none') encSelect.value = 'psk2+ccmp'; + } + updateRoamBadge('#wifi-smart-roaming'); + }); + } + + // 2. 2.4G 独立面板联动 + var r2gToggle = container.querySelector('#wifi-2g-roaming'); + if (r2gToggle) { + r2gToggle.addEventListener('change', function(e) { + if (this.classList.contains('is-dirty')) { + this.classList.remove('is-dirty'); + window._forceWifiSubmit = true; + } + var warn = container.querySelector('#roam-warn-2g'); + if (warn) warn.style.display = 'none'; + + if (this.checked) { + var encSelect = container.querySelector('#wifi-2g-enc'); + if (encSelect && encSelect.value === 'none') encSelect.value = 'psk2+ccmp'; + } + updateRoamBadge('#wifi-2g-roaming'); + }); + } + + // 3. 5G 独立面板联动 + var r5gToggle = container.querySelector('#wifi-5g-roaming'); + if (r5gToggle) { + r5gToggle.addEventListener('change', function(e) { + if (this.classList.contains('is-dirty')) { + this.classList.remove('is-dirty'); + window._forceWifiSubmit = true; + } + var warn = container.querySelector('#roam-warn-5g'); + if (warn) warn.style.display = 'none'; + + if (this.checked) { + var encSelect = container.querySelector('#wifi-5g-enc'); + if (encSelect && encSelect.value === 'none') encSelect.value = 'psk2+ccmp'; + } + updateRoamBadge('#wifi-5g-roaming'); + }); + } + // ================================== + + // WISP 交互与扫描逻辑 + var wispToggle = container.querySelector('#wisp-toggle'); + var wispUiPanel = container.querySelector('#wisp-ui-panel'); + var scanBtn = container.querySelector('#btn-wisp-scan'); + var wispModal = container.querySelector('#wisp-scan-modal'); + + if (wispToggle) { + wispToggle.addEventListener('change', function(e) { + wispUiPanel.style.display = this.checked ? 'flex' : 'none'; + + // 鼠标真实点击,且打开开关时才触发重置 + if (e && e.isTrusted && this.checked) { + + // ==== 恢复扫描按钮 ==== + var btnScanLive = container.querySelector('#btn-wisp-scan'); + if (btnScanLive) { + btnScanLive.style.display = 'block'; + } + + // 隐藏密码输入框 + var selectedInfo = container.querySelector('#wisp-selected-info'); + if (selectedInfo) selectedInfo.style.display = 'none'; + } + }); + container.querySelector('#wisp-modal-close').addEventListener('click', function() { wispModal.style.display = 'none'; }); + + scanBtn.addEventListener('click', function(e) { + e.preventDefault(); + scanBtn.innerText = T['TXT_SCANNING']; + scanBtn.disabled = true; + + // 单芯片用 radio0,多芯片若 5G 开优先用 radio1 异频中继) + var scanDevice = 'radio0'; + if (!window._isSingleChip && container.querySelector('#wifi-5g-en').checked) scanDevice = 'radio1'; + + callIwinfoScan(scanDevice).then(function(res) { + scanBtn.innerText = T['BTN_SCAN']; + scanBtn.disabled = false; + + var ul = container.querySelector('#wisp-scan-list'); + ul.innerHTML = ''; + var list = res || []; + // 排序信号 + list.sort(function(a, b) { return (b.signal || -100) - (a.signal || -100); }); + + if (list.length === 0) { + ul.innerHTML = '
  • ' + T['TXT_NO_NETWORKS'] + '
  • '; + } else { + var uniqueSsids = {}; + list.forEach(function(net) { + if (!net.ssid || uniqueSsids[net.ssid]) return; + uniqueSsids[net.ssid] = true; + + var li = document.createElement('li'); + li.style.cssText = 'padding:15px 20px; border-bottom:1px solid #f1f5f9; cursor:pointer; display:flex; justify-content:space-between; align-items:center; transition:background 0.2s;'; + li.innerHTML = '' + net.ssid + '' + net.signal + ' dBm'; + + li.onmouseover = function() { this.style.background = '#f8fafc'; }; + li.onmouseout = function() { this.style.background = 'transparent'; }; + + li.onclick = function(e) { + if (e) { e.preventDefault(); e.stopPropagation(); } + + try { + // 1. 填入 SSID + container.querySelector('#wisp-target-ssid').value = net.ssid || ''; + + var encVal = 'none'; // 默认无密码 + if (net.encryption) { + // 容错处理:获取完整的描述字符串 + var desc = typeof net.encryption === 'string' ? net.encryption : (net.encryption.description || JSON.stringify(net.encryption)); + desc = desc.toLowerCase(); + + // 强制判定为有密码! + var isExplicitNone = (net.encryption.enabled === false || desc === 'none' || desc === '{"enabled":false}'); + + if (!isExplicitNone) { + if (desc.indexOf('wpa3') !== -1 || desc.indexOf('sae') !== -1) { + encVal = 'sae-mixed'; + } else { + encVal = 'psk2+ccmp'; + } + } + } + container.querySelector('#wisp-target-enc').value = encVal; + container.querySelector('#wisp-target-device').value = scanDevice; + container.querySelector('#wisp-target-bssid').value = net.bssid || ''; + + // 3. 界面切换:显示填写面板,隐藏弹窗 + container.querySelector('#wisp-selected-info').style.display = 'block'; + wispModal.style.display = 'none'; + + var pwdInput = container.querySelector('#wisp-target-key'); + var pwdRow = pwdInput ? pwdInput.closest('.nw-value') : null; + + if (encVal === 'none') { + // 如果是开放网络,直接隐藏密码行,并清空历史密码 + if (pwdRow) pwdRow.style.display = 'none'; + if (pwdInput) pwdInput.value = ''; + } else { + // 如果有加密,显示密码行并自动对焦 + if (pwdRow) pwdRow.style.display = 'flex'; + if (pwdInput) { + setTimeout(function() { + pwdInput.focus(); + pwdInput.select(); + }, 150); + } + } + } catch(err) { + console.error("选取 Wi-Fi 时发生错误:", err); + } + }; + + ul.appendChild(li); + }); + } + wispModal.style.display = 'flex'; + }).catch(function(err) { + scanBtn.innerText = T['BTN_SCAN']; + scanBtn.disabled = false; + alert(T['TXT_SCAN_FAILED']); + }); + }); + } + // 結束 + + // 页面切换+置顶函数 + var switchStep = function(hideEl, showEl) { + hideEl.style.display = 'none'; + showEl.style.display = 'block'; + setTimeout(function() { + smoothScrollToTop(650); // 毫秒自定义动画 + }, 20); // 给浏览器留20ms的重绘时间 + }; + + container.querySelectorAll('.nw-card').forEach(function (card) { + card.addEventListener('click', function () { + // 在切换离开前,把目前框里的字存进缓存 + var currentU = container.querySelector('#pppoe-user'); + var currentP = container.querySelector('#pppoe-pass'); + if (currentU && currentU.value) sessionStorage.setItem('nw_pppoe_user', currentU.value); + if (currentP && currentP.value) sessionStorage.setItem('nw_pppoe_pass', currentP.value); + + selectedMode = card.getAttribute('data-mode'); + + container.querySelector('#fields-router').style.display = (selectedMode === 'router') ? 'block' : 'none'; + container.querySelector('#fields-pppoe').style.display = (selectedMode === 'pppoe') ? 'block' : 'none'; + container.querySelector('#fields-lan').style.display = (selectedMode === 'lan') ? 'block' : 'none'; + container.querySelector('#fields-wifi').style.display = (selectedMode === 'wifi') ? 'block' : 'none'; + + // 如果是切换回PPPoE,再从缓存拿出来恢复 + if (selectedMode === 'pppoe') { + var savedU = sessionStorage.getItem('nw_pppoe_user'); + var savedP = sessionStorage.getItem('nw_pppoe_pass'); + if (savedU !== null && container.querySelector('#pppoe-user')) container.querySelector('#pppoe-user').value = savedU; + if (savedP !== null && container.querySelector('#pppoe-pass')) container.querySelector('#pppoe-pass').value = savedP; + + } + + switchStep(step1, step2); + }); + }); + container.querySelector('#btn-back-1').addEventListener('click', function () { switchStep(step2, step1); }); + container.querySelector('#top-back-1').addEventListener('click', function () { switchStep(step2, step1); }); + container.querySelector('#btn-back-2').addEventListener('click', function () { switchStep(step3, step2); }); + container.querySelector('#top-back-2').addEventListener('click', function () { switchStep(step3, step2); }); + + container.querySelector('#btn-next-2').addEventListener('click', function () { + try { + var mainBtn = container.querySelector('#main-pppoe-submit'); + if (mainBtn) mainBtn.click(); + + var rTypeEl = container.querySelector('input[name="router_type"]:checked'); + var rType = rTypeEl ? rTypeEl.value : 'dhcp'; + var targetIp = '', targetGw = '', isBypass = false; + + // 获取系统当前的 WAN IP,用于冲突比对 + var sysWanIp = window._liveWanIp || ''; + + if (selectedMode === 'lan') { + + // ---- 将“守门员拦截器”插在 LAN 逻辑的最前面 ---- + var ipv6Toggle = document.getElementById('lan-ipv6-toggle'); + + // 1. 安检开始:判断用户是否关闭了 IPv6 开关 + if (ipv6Toggle && !ipv6Toggle.checked) { + var isWogEn = safeUciGet('netwiz', 'main', 'watchdog_enable', '0'); + var webTog = document.getElementById('adv-web-toggle'); + var isWebEn = (webTog && webTog.checked); + + // 2. 探针或外网还没关!触发拦截 + if (isWogEn === '1' || isWebEn) { + openModal({ + title: '⚠️ ' + (T['MSG_DEP_NOTICE'] || 'Dependency Notice'), + msg: '
    ' + + (T['MSG_WOG_OFF_V6_ALL'] || 'You have disabled IPv6. To ensure security, the dependent IPv6 Watchdog and WAN Access will be automatically disabled.') + + '
    ' + + + '
    ❌ ' + + (T['LBL_WATCHDOG_LINK'] || "IPv6 Watchdog") + '
    ' + + '
    ❌ ' + + (T['MSG_WOG_LINK_WAN'] || "WAN Access Web UI Channel") + '
    ', + okText: T['BTN_OK'] || 'OK', + onOk: function() { + var gm = document.getElementById('nw-global-modal'); + if (gm) gm.style.display = 'none'; + + setTimeout(function() { + openModal({ title: '⚙️ ' + (T['LBL_ADV_UTILS_TITLE'] || 'Processing...'), msg: T['MSG_WRITING'] || 'Please wait...', spin: true }); + var gm2 = document.getElementById('nw-global-modal'); if (gm2) gm2.style.zIndex = '100000'; + + var p = Promise.resolve(); + // 【卸载步骤 1】关闭 IPv6 深度保活并写入底层 + if (isWogEn === '1') { + uci.set('netwiz', 'main', 'watchdog_enable', '0'); + p = uci.save().then(function() { return uci.apply(); }); + } + + // 【卸载步骤 2】关闭外网直联后台 + p.then(function() { + if (isWebEn) { + if (webTog) webTog.checked = false; // 视觉上去掉勾选 + return callSetAdvSettings('', '0', '', ''); + } + return Promise.resolve(); + }).then(function() { + // 3:隐患清理完毕后,自动模拟用户再次点击“下一步”! + // 这样系统就会无缝顺滑地继续执行后续正常的保存和翻页跳转 + var gm3 = document.getElementById('nw-global-modal'); + if (gm3) gm3.style.display = 'none'; // 销毁当前的转圈动画,防止遮挡 + + // 精准触发「下一步」按钮 + var nextBtn = document.getElementById('btn-next-2'); + if (nextBtn) { + nextBtn.click(); + } + }).catch(function() { window.location.reload(); }); + }, 50); + } + }); + var gm = document.getElementById('nw-global-modal'); if (gm) gm.style.zIndex = '100000'; + + return; // 中止本次运行,把控制权交给弹窗,等处理干净了上面的 click() 会再次触发进来 + } + } + // ---- 守门员逻辑结束 ---- + + targetIp = container.querySelector('#lan-ip').value.trim(); + targetGw = container.querySelector('#lan-gw').value.trim(); + isBypass = bypassToggle.checked; + + if (!targetIp) { openModal({title: T['M_INC_TIT'], msg: T['M_INC_IP'], okText:T['BTN_EDIT']}); return; } + if (!isValidIP(targetIp)) { openModal({title:T['M_FMT_TIT'], msg:T['M_FMT_IP'], okText:T['BTN_EDIT']}); return; } + + if (isBypass) { + if (!targetGw) { openModal({title: T['M_LOGIC_TIT'], msg: T['M_LOGIC_BYP'], okText:T['BTN_EDIT']}); return; } + if (!isValidIP(targetGw)) { openModal({title: T['M_FMT_TIT'], msg: T['M_FMT_GW'], okText:T['BTN_EDIT']}); return; } + if (!isSameSubnet(targetIp, targetGw)) { openModal({title: T['M_SUB_ERR_TIT'], msg: T['M_SUB_ERR_BYP'], okText:T['BTN_EDIT']}); return; } + + if (targetIp === targetGw) { + openModal({ + title: T['M_LOGIC_TIT'], + msg: T['M_SAME_BYP'] || 'In AP/Relay mode, Device IP MUST NOT be the same as Gateway!', + okText: T['BTN_EDIT'] + }); + return; + } + if (window._realUpstreamGw && window._realUpstreamGw !== T['TXT_GETTING']) { + // 物理网关抢占拦截 (旁路由模式) + if (targetIp === window._realUpstreamGw) { + // 递減算法 + var suggestBypIp = getSafeApIp(window._realUpstreamGw); + + openModal({ + title: T['M_CFLT_PHYSICAL_TIT'], + msg: T['M_CFLT_PHYSICAL_BYP_MSG'].replace('{ip}', targetIp).replace('{suggest_ip}', suggestBypIp), + okText: T['BTN_EDIT'], + isDanger: true + }); + return; + } + } + } else { + // 在 LAN 模式(未开启 AP)时,如果填写的 LAN IP 与上级 WAN IP 网段冲突 + if (sysWanIp && isSameSubnet(targetIp, sysWanIp)) { + var safeRouterIp = getSafeRouterIp(sysWanIp); + // 跨网段拦截 + openModal({ + title: T['M_CFLT_CROSS_TIT'], + msg: T['M_CFLT_CROSS_MSG'].replace('{ip}', targetIp).replace('{wan_ip}', sysWanIp).replace('{safe_ip}', safeRouterIp), + okText: T['BTN_FIX_APPLY'], + cancelText: T['BTN_EDIT_MYSELF'], + onOk: function() { + container.querySelector('#lan-ip').value = safeRouterIp; + container.querySelector('#nw-global-modal').style.display = 'none'; + container.querySelector('#btn-next-2').click(); + } + }); + return; + } + } + } else if (selectedMode === 'router') { + // 主路由模式下的避让,包含 DHCP 和 Static + // DHCP 还是配置 Static,路由器的 LAN IP 和现在的 WAN 冲突,修改 LAN IP + var currentLanIp = container.querySelector('#lan-ip').value || safeUciGet('network', 'lan', 'ipaddr', '192.168.1.1').split('/')[0]; + + if (rType === 'static') { + targetIp = container.querySelector('#router-ip').value.trim(); + targetGw = container.querySelector('#router-gw').value.trim(); + if (!targetIp || !targetGw) { openModal({title:T['M_INC_TIT'], msg:T['M_INC_WAN'], okText:T['BTN_EDIT']}); return; } + if (!isValidIP(targetIp) || !isValidIP(targetGw)) { openModal({title:T['M_FMT_TIT'], msg:(!isValidIP(targetIp)?T['M_FMT_WAN']:T['M_FMT_GW']), okText:T['BTN_EDIT']}); return; } + if (targetIp === targetGw) { + openModal({ + title: T['M_LOGIC_TIT'], + msg: T['M_SAME_GW'], + okText: T['BTN_EDIT'] + }); + return; + } + // 主路由/WAN 靜态 IP 模式的物理网关拦截 + if (window._realUpstreamGw && window._realUpstreamGw !== T['TXT_GETTING']) { + if (targetIp === window._realUpstreamGw) { + // WAN 口动态生成一個不等于 targetIp 尾数的建义值 (.88 或 .66) + var gwTailWan = parseInt(targetIp.split('.')[3], 10); + var suggestTailWan = (gwTailWan === 88) ? 66 : 88; + var suggestWanIp = targetIp.substring(0, targetIp.lastIndexOf('.')) + "." + suggestTailWan; + + openModal({ + title: T['M_CFLT_PHYSICAL_TIT'], + msg: T['M_CFLT_PHYSICAL_WAN_MSG'].replace('{ip}', targetIp).replace('{suggest_ip}', suggestWanIp), + okText: T['BTN_EDIT'], + isDanger: true + }); + return; + } + } + if (!isSameSubnet(targetIp, targetGw)) { openModal({title: T['M_SUB_ERR_TIT'], msg: T['M_SUB_ERR_WAN1'] + '
    ' + T['M_SUB_ERR_WAN2'].replace('{gw}', targetGw).replace('{ip}', targetGw.substring(0, targetGw.lastIndexOf('.'))), okText:T['BTN_EDIT']}); return; } + sysWanIp = targetIp; // 静态下,将用户填写的 WAN IP 作为校验基准 + } + + // WAN 和 LAN 在主路由模式冲突 + if (sysWanIp && currentLanIp && isSameSubnet(sysWanIp, currentLanIp)) { + var newSafeIp = getSafeRouterIp(sysWanIp); + // 主路由模式跨网段避让 + openModal({ + title: T['M_CFLT_INTERCEPT_TIT'], + msg: T['M_CFLT_ROUTER_MSG'].replace('{wan_ip}', sysWanIp).replace('{lan_ip}', currentLanIp).replace('{safe_ip}', newSafeIp), + okText: T['BTN_AUTO_EVADE'], + cancelText: T['BTN_EDIT_MYSELF'], // 手动修改(取消)按钮 + isDanger: true, + onOk: function() { + // 强制重定向到修改 LAN IP,并附带 120 秒回滚保护! + container.querySelector('#lan-ip').value = newSafeIp; + selectedMode = 'lan'; // 篡改提交模式 + container.querySelector('#nw-global-modal').style.display = 'none'; + container.querySelector('#btn-next-2').click(); + } + }); + return; // 拦截本次提交 + } + } else if (selectedMode === 'pppoe') { + if (window._nwMultiPppoe) { + var mBoxes = container.querySelectorAll('.nw-multi-pppoe-box'); + var mEmpty = false; + mBoxes.forEach(function(b) { + if (!b.querySelector('.m-user').value.trim() || !b.querySelector('.m-pass').value.trim()) mEmpty = true; + }); + if (mEmpty) { openModal({title: T['M_INC_TIT'], msg: T['M_INC_PPPOE'], okText: T['BTN_EDIT']}); return; } + } else { + if (!container.querySelector('#pppoe-user').value.trim() || !container.querySelector('#pppoe-pass').value.trim()) { openModal({title: T['M_INC_TIT'], msg: T['M_INC_PPPOE'], okText: T['BTN_EDIT']}); return; } + } + } else if (selectedMode === 'wifi') { + var isSmart = container.querySelector('#wifi-smart-toggle').checked && !window._isSingleChip; + if (isSmart) { + if (container.querySelector('#wifi-smart-en').checked) { + if (!container.querySelector('#wifi-smart-ssid').value.trim()) { openModal({title: T['M_INC_TIT'], msg: T['M_INC_WIFI'], okText: T['BTN_EDIT']}); return; } + var k = container.querySelector('#wifi-smart-key').value; + var e = container.querySelector('#wifi-smart-enc').value; + if (e !== 'none' && (!k || k.length < 8)) { openModal({title: T['M_FMT_TIT'], msg: T['M_PWD_SHORT'], okText: T['BTN_EDIT']}); return; } + } + } else { + if (container.querySelector('#wifi-2g-en').checked) { + if (!container.querySelector('#wifi-2g-ssid').value.trim()) { openModal({title: T['M_INC_TIT'], msg: T['M_INC_WIFI'], okText: T['BTN_EDIT']}); return; } + var k2 = container.querySelector('#wifi-2g-key').value; + var e2 = container.querySelector('#wifi-2g-enc').value; + if (e2 !== 'none' && (!k2 || k2.length < 8)) { openModal({title: T['M_FMT_TIT'], msg: T['M_PWD_SHORT'], okText: T['BTN_EDIT']}); return; } + } + if (container.querySelector('#wifi-5g-en').checked) { + if (!container.querySelector('#wifi-5g-ssid').value.trim()) { openModal({title: T['M_INC_TIT'], msg: T['M_INC_WIFI'], okText: T['BTN_EDIT']}); return; } + var k5 = container.querySelector('#wifi-5g-key').value; + var e5 = container.querySelector('#wifi-5g-enc').value; + if (e5 !== 'none' && (!k5 || k5.length < 8)) { openModal({title: T['M_FMT_TIT'], msg: T['M_PWD_SHORT'], okText: T['BTN_EDIT']}); return; } + } + } + // WISP 密码校验 + var wispTog = container.querySelector('#wisp-toggle'); + if (wispTog && wispTog.checked) { + var wispSsid = container.querySelector('#wisp-target-ssid').value.trim(); + if (!wispSsid) { + openModal({title: T['M_INC_TIT'], msg: T['MODAL_WISP_TITLE'], okText: T['BTN_EDIT']}); + return; + } + var wispEnc = container.querySelector('#wisp-target-enc').value; + var wispKey = container.querySelector('#wisp-target-key').value; + if (wispEnc !== 'none' && (!wispKey || wispKey.length < 8)) { + openModal({title: T['M_FMT_TIT'], msg: T['M_PWD_SHORT'], okText: T['BTN_EDIT']}); + return; + } + } + } + + uci.load('network').then(function() { + try { + var currentLanIp = safeUciGet('network', 'lan', 'ipaddr', '192.168.1.1').split('/')[0]; + var currentLanGw = safeUciGet('network', 'lan', 'gateway', ''); + var currentWanProto = safeUciGet('network', 'wan', 'proto', '').toLowerCase(); + var currentWanIp = (currentWanProto === 'static') ? safeUciGet('network', 'wan', 'ipaddr', '').split('/')[0] : (window._liveWanIp || ''); + var currentWanGw = safeUciGet('network', 'wan', 'gateway', ''); + + var currentBypass = (safeUciGet('dhcp', 'lan', 'ignore', '') === '1' ? '1' : '0'); + var newBypass = bypassToggle.checked ? '1' : '0'; + + var currentDhcpv6 = safeUciGet('dhcp', 'lan', 'dhcpv6', ''); + var currentIpv6 = (currentDhcpv6 === 'server' || currentDhcpv6 === 'relay') ? '1' : '0'; + var ipv6El = container.querySelector('#lan-ipv6-toggle'); + var newIpv6 = (ipv6El && ipv6El.checked) ? '1' : '0'; + + // 检查黄色的 IPv6 警告灯是否亮起 + var v6WarnEl = container.querySelector('#tip-ipv6-warn'); + var needsV6Fix = (v6WarnEl && v6WarnEl.style.display !== 'none'); + + var currentWifiState = getWifiSnapshot(); + + var checkWanIp = (selectedMode === 'router' && rType === 'static') ? targetIp : currentWanIp; + var checkLanIp = (selectedMode === 'lan') ? targetIp : currentLanIp; + + if (checkWanIp && checkLanIp && isSameSubnet(checkWanIp, checkLanIp)) { + var cfltMsg = T['M_CFLT_SUB1'].replace('{ip}', checkLanIp) + '
    ' + T['M_CFLT_SUB2']; + if (selectedMode === 'router' && rType === 'dhcp') { + cfltMsg += '

    ' + T['M_CFLT_SUGGEST'] + ''; + } + openModal({title: T['M_CFLT_TIT'], msg: cfltMsg, okText: T['BTN_EDIT']}); + return; + } + + var isNoMod = false; + if (selectedMode === 'lan' && targetIp === currentLanIp && targetGw === currentLanGw && newBypass === currentBypass && newIpv6 === currentIpv6 && !needsV6Fix) isNoMod = true; + + if (selectedMode === 'router' && rType === 'static' && targetIp === currentWanIp && targetGw === currentWanGw) isNoMod = true; + if (selectedMode === 'router' && rType === 'dhcp' && currentWanProto === 'dhcp') isNoMod = true; + if (selectedMode === 'wifi') { + if (window._forceWifiSubmit) { + isNoMod = false; + } else if (window._origWifiState && currentWifiState === window._origWifiState) { + isNoMod = true; + } + } + + if (isNoMod) { openModal({title: T['M_NO_MOD_TIT'], msg: T['M_NO_MOD_MSG'], okText: T['M_EXIT'], onOk: returnToStep1 }); return; } + + var b = function(t, p) { var h = "
    " + t + "
    "; for (var i=0; i < p.length; i++) h += "
    " + p[i][0] + "" + p[i][1] + "
    "; return h + "
    "; }; + + // === Diff 高亮渲染带新旧对比助手函数 === + var mkDiff = function(label, newVal, oldVal) { + // 1. HTML 转义函数 + var escapeHtml = function(str) { + if (str == null) return ""; + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + }; + + var sNew = String(newVal).trim(); + var sOld = (oldVal !== undefined && oldVal !== null) ? String(oldVal).trim() : ''; + + // 提取纯文本进行判断 + var rawOld = sOld.replace(/<[^>]+>/g, '').trim(); + var isActuallyNew = (rawOld === '' || rawOld === 'undefined' || rawOld === 'null'); + var isChanged = (sNew !== sOld) && !isActuallyNew; + + // 2. 将新旧值转义,DOM 渲染 + var safeNew = escapeHtml(sNew); + var safeOld = escapeHtml(sOld); + + // div 独立成行 + var highlightBadge = function(txt) { + return "
    " + txt + "
    "; + }; + + if (isActuallyNew) { + // 文字在上,徽章在下 + var newHtml = "
    " + + "
    " + safeNew + "
    " + + highlightBadge(T['TXT_NEW_MOD'] || 'NEW') + + "
    "; + return [label, newHtml]; + } else if (isChanged) { + // 旧值 -> 新值 -> 徽章独立在一行 (使用 safeOld 和 safeNew) + var diffHtml = "
    " + + "
    " + safeOld + "
    " + + "
    " + + "↳" + + "
    " + safeNew + "
    " + + "
    " + + highlightBadge(T['TXT_MODIFIED'] || 'OK') + + "
    "; + return [label, diffHtml]; + } else { + // 样式未变更时的渲染 (使用 safeNew) + var dimStyle = "opacity: 0.7; color: rgba(255, 255, 255, 0.85);"; + return ["" + label + "", "" + safeNew + ""]; + } + }; + // ============================= + + if (selectedMode === 'lan') { + confirmText.innerHTML = b(isBypass ? T['MODE_LAN_TITLE']+" - "+T['STAT_BYPASS'] : T['MODE_LAN_TITLE']+" - "+T['STAT_LAN'], [ + mkDiff(T['TXT_DEV_IP'].replace(':',''), targetIp, currentLanIp), + mkDiff(T['LBL_GW'], targetGw || T['TXT_NOT_SET'], currentLanGw || T['TXT_NOT_SET']), + mkDiff("DHCP", isBypass ? T['TXT_OFF'] : T['TXT_ON'], currentBypass === '1' ? T['TXT_OFF'] : T['TXT_ON']), + mkDiff("IPv6 (DHCPv6)", newIpv6 === '1' ? T['TXT_ON'] : T['TXT_OFF'], currentIpv6 === '1' ? T['TXT_ON'] : T['TXT_OFF']) + ]); + } else if (selectedMode === 'router') { + if (rType === 'static') { + confirmText.innerHTML = b(T['STAT_SEC_STATIC'], [ + mkDiff(T['TXT_WAN_IP'].replace(':',''), targetIp, currentWanIp), + mkDiff(T['TXT_UP_GW'].replace(':',''), targetGw, currentWanGw) + ]); + } else { + confirmText.innerHTML = b(T['STAT_SEC_DHCP'], [ + mkDiff(T['LBL_CONN_TYPE'], T['OPT_DHCP'], currentWanProto === 'dhcp' ? T['OPT_DHCP'] : ''), + mkDiff(T['M_IP_GW'], T['M_AUTO_UP'], currentWanProto === 'dhcp' ? T['M_AUTO_UP'] : '') + ]); + } + } else if (selectedMode === 'wifi') { + var confirmList = []; + var sTog = container.querySelector('#wifi-smart-toggle').checked && !window._isSingleChip; + var getSelTxt = function(id) { + var e = container.querySelector(id); + return (e && e.options && e.selectedIndex >= 0) ? e.options[e.selectedIndex].text : (e ? e.value : ''); + }; + + // 解析旧状态对象以便对比 + var oldS = window._origWifiState ? JSON.parse(window._origWifiState) : {}; + + // 兜底,防止多频合一和分开模式互相切换时产生 undefined 污染 + oldS.s2 = oldS.s2 || ''; oldS.ec2 = oldS.ec2 || ''; + oldS.s5 = oldS.s5 || ''; oldS.ec5 = oldS.ec5 || ''; + oldS.s5g2 = oldS.s5g2 || ''; + oldS.ss = oldS.ss || ''; oldS.ecs = oldS.ecs || ''; + oldS.ws = oldS.ws || ''; + + if (sTog) { + var isEn = container.querySelector('#wifi-smart-en').checked; + confirmList.push(mkDiff(T['LBL_SMART_CONN'], isEn ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', oldS.es ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + if (isEn) { + confirmList.push(mkDiff('SSID', '' + container.querySelector('#wifi-smart-ssid').value + '', '' + oldS.ss + '')); + confirmList.push(mkDiff('' + T['LBL_WIFI_ENC'] + '', '' + getSelTxt('#wifi-smart-enc') + '', '' + (container.querySelector('#wifi-smart-enc').querySelector('option[value="'+oldS.ecs+'"]') ? container.querySelector('#wifi-smart-enc').querySelector('option[value="'+oldS.ecs+'"]').text : oldS.ecs) + '')); + + var roamNew = container.querySelector('#wifi-smart-roaming').checked; + var roamOld = oldS.rs; + if (roamNew || roamOld) { + confirmList.push(mkDiff('' + T['LBL_ROAMING'] + '', roamNew ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', roamOld ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + } + + var hidNew = container.querySelector('#wifi-smart-hidden').checked; + var hidOld = oldS.hs; + if (hidNew || hidOld) { + confirmList.push(mkDiff('' + T['LBL_HIDE_SSID'] + '', hidNew ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', hidOld ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + } + } + } else { + var en2g = container.querySelector('#wifi-2g-en').checked; + confirmList.push(mkDiff('' + T['TAB_2G'] + '', en2g ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', oldS.e2 ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + if (en2g) { + confirmList.push(mkDiff('└ SSID', '' + container.querySelector('#wifi-2g-ssid').value + '', '' + oldS.s2 + '')); + confirmList.push(mkDiff('└ ' + T['LBL_WIFI_ENC'] + '', '' + getSelTxt('#wifi-2g-enc') + '', '' + (container.querySelector('#wifi-2g-enc').querySelector('option[value="'+oldS.ec2+'"]') ? container.querySelector('#wifi-2g-enc').querySelector('option[value="'+oldS.ec2+'"]').text : oldS.ec2) + '')); + + var r2New = container.querySelector('#wifi-2g-roaming').checked; + var r2Old = oldS.r2; + if (r2New || r2Old) { + confirmList.push(mkDiff('└ ' + T['LBL_ROAMING'] + '', r2New ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', r2Old ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + } + } + + var en5g = container.querySelector('#wifi-5g-en').checked; + confirmList.push(mkDiff('' + T['TAB_5G'] + '', en5g ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', oldS.e5 ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + if (en5g) { + confirmList.push(mkDiff('└ SSID', '' + container.querySelector('#wifi-5g-ssid').value + '', '' + oldS.s5 + '')); + confirmList.push(mkDiff('└ ' + T['LBL_WIFI_ENC'] + '', '' + getSelTxt('#wifi-5g-enc') + '', '' + (container.querySelector('#wifi-5g-enc').querySelector('option[value="'+oldS.ec5+'"]') ? container.querySelector('#wifi-5g-enc').querySelector('option[value="'+oldS.ec5+'"]').text : oldS.ec5) + '')); + + var r5New = container.querySelector('#wifi-5g-roaming').checked; + var r5Old = oldS.r5; + if (r5New || r5Old) { + confirmList.push(mkDiff('└ ' + T['LBL_ROAMING'] + '', r5New ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', r5Old ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + } + } + + // 5G_Game 确认信息 + var en5g2El = container.querySelector('#wifi-5g2-en'); + if (en5g2El && (en5g2El.checked || oldS.e5g2)) { + var en5g2 = en5g2El.checked; + confirmList.push(mkDiff('5G_Game', en5g2 ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', oldS.e5g2 ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + if (en5g2) { + confirmList.push(mkDiff('└ SSID', '' + container.querySelector('#wifi-5g2-ssid').value + '', '' + oldS.s5g2 + '')); + confirmList.push(mkDiff('└ ' + T['LBL_WIFI_ENC'] + '', 'WPA2/WPA3 Mixed', 'WPA2/WPA3 Mixed')); + } + } + } + + // 中继 (WISP) 的确认信息展示 + var wTogConfirm = container.querySelector('#wisp-toggle'); + if (wTogConfirm) { + var wNew = wTogConfirm.checked; + var wOld = oldS.wt; + if (wNew || wOld) { + confirmList.push(mkDiff('🌐 ' + T['LBL_WISP_EN'] + '', wNew ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '', wOld ? '' + T['TXT_ON'] + '' : '' + T['TXT_OFF'] + '')); + if (wNew) { + confirmList.push(mkDiff('└ ' + T['TXT_TARGET_SSID'] + '', '' + container.querySelector('#wisp-target-ssid').value + '', '' + oldS.ws + '')); + } + } + } + + confirmText.innerHTML = b(T['MODE_WIFI_TITLE'], confirmList); + } else { + if (window._nwMultiPppoe) { + var mDiffs = []; + var mBoxes = container.querySelectorAll('.nw-multi-pppoe-box'); + mBoxes.forEach(function(b) { + var ifc = b.getAttribute('data-iface'); + var u = b.querySelector('.m-user').value.trim(); + var p = b.querySelector('.m-pass').value.trim(); + var oldIf = window._nwMultiPppoe.find(function(x){ return x.name === ifc; }); + mDiffs.push(mkDiff(ifc.toUpperCase() + ' ' + (T['M_ACCT']||'Account'), u, oldIf ? oldIf.user : '')); + mDiffs.push(mkDiff(ifc.toUpperCase() + ' ' + (T['M_PWD']||'Password'), p, oldIf ? oldIf.pass : '')); + }); + confirmText.innerHTML = b(T['MODE_PPPOE_TITLE'], mDiffs); + } else { + var oldPppoeUser = safeUciGet('network', 'wan', 'username', ''); + var oldPppoePass = safeUciGet('network', 'wan', 'password', ''); + confirmText.innerHTML = b(T['MODE_PPPOE_TITLE'], [ + mkDiff(T['M_ACCT'], container.querySelector('#pppoe-user').value, oldPppoeUser), + mkDiff(T['M_PWD'], container.querySelector('#pppoe-pass').value, oldPppoePass) + ]); + } + } + + if (selectedMode === 'lan' && !isBypass && targetGw !== '') { openModal({ title: T['M_WARN_TIT'], msg: T['M_WARN_MSG'], cancelText: T['BTN_EDIT'], okText: T['M_WARN_BTN'], isDanger: true, onOk: function() { container.querySelector('#nw-global-modal').style.display = 'none'; step2.style.display = 'none'; step3.style.display = 'block'; setTimeout(function(){ smoothScrollToTop(650); }, 20); } }); return; } + + // ===== Wi-Fi 无密码拦截 ===== + var hasOpenWifi = false; + if (selectedMode === 'wifi') { + var checkSmart = container.querySelector('#wifi-smart-toggle').checked && !window._isSingleChip; + if (checkSmart) { + if (container.querySelector('#wifi-smart-en').checked && container.querySelector('#wifi-smart-enc').value === 'none') hasOpenWifi = true; + } else { + if (container.querySelector('#wifi-2g-en').checked && container.querySelector('#wifi-2g-enc').value === 'none') hasOpenWifi = true; + if (container.querySelector('#wifi-5g-en').checked && container.querySelector('#wifi-5g-enc').value === 'none') hasOpenWifi = true; + } + } + + if (hasOpenWifi) { + openModal({ + title: T['M_OPEN_WARN_TIT'] || '⚠️ No Password', + msg: T['M_OPEN_WARN_MSG'] || 'Setting up an open Wi-Fi without a password. Continue?', + cancelText: T['BTN_EDIT'], + okText: T['M_WARN_BTN'], + isDanger: true, + onOk: function() { + container.querySelector('#nw-global-modal').style.display = 'none'; + step2.style.display = 'none'; + step3.style.display = 'block'; + setTimeout(function(){ smoothScrollToTop(650); }, 20); + } + }); + return; + } + // ====================================== + + step2.style.display = 'none'; step3.style.display = 'block'; + setTimeout(function(){ smoothScrollToTop(650); }, 20); + } catch (err) { + openModal({ title: T['M_SYS_ERR'], msg: (T['M_ERR_DATA_PROC'] || 'Configuration data processing failed') + ': ' + err, okText: T['M_CLOSE'] }); + } + }).catch(function(e) { + openModal({ title: T['M_SYS_ERR'], msg: T['M_SYS_MSG'], okText: T['M_CLOSE'] }); + }); + } catch (err) { + openModal({ title: T['M_SYS_ERR'], msg: (T['M_ERR_VALIDATE'] || 'Input validation failed') + ': ' + err, okText: T['M_CLOSE'] }); + } + }); + + var fetchProbe = function(url, ms) { + return Promise.race([ + fetch(url, { mode: 'no-cors', cache: 'no-store' }), + new Promise(function(resolve, reject) { setTimeout(function(){ reject(new Error('timeout')); }, ms); }) + ]); + }; + + container.querySelector('#btn-apply').addEventListener('click', function () { + // 开启“系统忙碌锁” + window._isSystemBusy = true; + // 设定 15 秒后自动解锁 + setTimeout(function() { window._isSystemBusy = false; }, 15000); + + try { + var rTypeEl = container.querySelector('input[name="router_type"]:checked'); + var rType = rTypeEl ? rTypeEl.value : 'dhcp'; + var mode = selectedMode, a1 = '', a2 = '', a3 = '', a4 = '', a5 = '1', a6 = '0'; + var actionDetail = ""; + var mTitle = ""; + + if (selectedMode === 'lan') { + a1 = container.querySelector('#lan-ip').value.trim(); + a2 = container.querySelector('#lan-gw').value.trim(); + a3 = calculateNetmask(a1); + a4 = bypassToggle.checked ? '1' : '0'; + var safeEl = container.querySelector('#lan-safe-toggle'); + a5 = (safeEl && safeEl.checked) ? '1' : '0'; + var ipv6El = container.querySelector('#lan-ipv6-toggle'); + a6 = (ipv6El && ipv6El.checked) ? '1' : '0'; + actionDetail = '' + a1 + ''; + mTitle = bypassToggle.checked ? T['ACT_BYPASS'] : (a1 !== window.location.hostname ? T['ACT_LAN'] : T['M_RST_TIT']); + } else if (selectedMode === 'router') { + mode = (rType === 'dhcp') ? 'wan_dhcp' : 'wan_static'; + if(rType === 'static') { a1 = container.querySelector('#router-ip').value.trim(); a2 = container.querySelector('#router-gw').value.trim(); a3 = calculateNetmask(a1); actionDetail = '' + a1 + ''; } + else { actionDetail = '' + T['OPT_DHCP'] + ''; } + mTitle = rType === 'dhcp' ? T['ACT_WAN_DHCP'] : T['ACT_WAN_STATIC']; + } else if (selectedMode === 'pppoe') { + if (window._nwMultiPppoe) { + mode = 'multi_pppoe'; + actionDetail = 'Multi-WAN (' + window._nwMultiPppoe.length + ')'; + mTitle = T['ACT_PPPOE']; + } else { + a1 = container.querySelector('#pppoe-user').value.replace(/[\r\n\s]+/g, ''); + a2 = container.querySelector('#pppoe-pass').value; + actionDetail = '' + a1 + ''; + mTitle = T['ACT_PPPOE']; + } + } else if (selectedMode === 'wifi') { + var isSmart = container.querySelector('#wifi-smart-toggle').checked && !window._isSingleChip; + var legacyB = container.querySelector('#legacy-b-toggle').checked ? '1' : '0'; + var payload = { smart: isSmart ? "true" : "false" }; + + if (isSmart) { + payload.merged = { + enabled: container.querySelector('#wifi-smart-en').checked ? "1" : "0", + ssid: container.querySelector('#wifi-smart-ssid').value.trim(), + key: container.querySelector('#wifi-smart-key').value, + encryption: container.querySelector('#wifi-smart-enc').value, + hidden: container.querySelector('#wifi-smart-hidden').checked ? "1" : "0", + roaming: container.querySelector('#wifi-smart-roaming').checked ? "1" : "0" + }; + } else { + payload.radio_2g = { + enabled: container.querySelector('#wifi-2g-en').checked ? "1" : "0", + ssid: container.querySelector('#wifi-2g-ssid').value.trim(), + key: container.querySelector('#wifi-2g-key').value, + encryption: container.querySelector('#wifi-2g-enc').value, + hidden: container.querySelector('#wifi-2g-hidden').checked ? "1" : "0", + mode: container.querySelector('#wifi-2g-mode').value, + channel: container.querySelector('#wifi-2g-chan').value, + bandwidth: container.querySelector('#wifi-2g-bw').value, + roaming: container.querySelector('#wifi-2g-roaming').checked ? "1" : "0" + }; + payload.radio_5g = { + enabled: container.querySelector('#wifi-5g-en').checked ? "1" : "0", + ssid: container.querySelector('#wifi-5g-ssid').value.trim(), + key: container.querySelector('#wifi-5g-key').value, + encryption: container.querySelector('#wifi-5g-enc').value, + hidden: container.querySelector('#wifi-5g-hidden').checked ? "1" : "0", + mode: container.querySelector('#wifi-5g-mode').value, + channel: container.querySelector('#wifi-5g-chan').value, + bandwidth: container.querySelector('#wifi-5g-bw').value, + roaming: container.querySelector('#wifi-5g-roaming').checked ? "1" : "0" + }; + + // 5G_Game 逻辑 + if (container.querySelector('#tab-5g2').style.display !== 'none') { + payload.radio_5g2 = { + enabled: container.querySelector('#wifi-5g2-en').checked ? "1" : "0", + ssid: container.querySelector('#wifi-5g2-ssid').value.trim(), + key: container.querySelector('#wifi-5g2-key').value, + encryption: "psk2+ccmp", + hidden: "0", mode: "auto", channel: "auto", bandwidth: "auto", roaming: "0" + }; + } + } + // WISP 参数打包 + var wispTog = container.querySelector('#wisp-toggle'); + if (wispTog) { + var targetEnc = container.querySelector('#wisp-target-enc').value; + payload.wisp = { + enabled: wispTog.checked ? "1" : "0", + ssid: container.querySelector('#wisp-target-ssid').value, + // none,强制密码传空,防止残留脏数据导致连接失败 + key: (targetEnc === 'none') ? '' : container.querySelector('#wisp-target-key').value, + encryption: targetEnc, + device: container.querySelector('#wisp-target-device').value, + bssid: container.querySelector('#wisp-target-bssid').value + }; + } + + a1 = JSON.stringify(payload); + a4 = legacyB; + actionDetail = '' + T['MODE_WIFI_TITLE'] + ''; + mTitle = T['ACT_WIFI']; + } + + openModal({ title: mTitle, msg: '
    ' + T['LBL_TARGET'] + ' ' + actionDetail + '
    ' + T['MSG_WRITING'] + '
    ', spin: true }); + + var succ = function() { + var h = window.location.hostname, sec = 0; + if (selectedMode === 'lan' && a1 && a1 !== h) { + if (a5 === '1') { + var countdownTimer = setInterval(function() { + sec += 3; + if (sec <= 120) { + document.getElementById('nw-global-msg').innerHTML = '
    ' + T['LBL_TARGET'] + ' ' + a1 + '
    ' + T['MSG_TIMER'].replace('{sec}', sec).replace('{total}', 120) + '
    '; + if (sec >= 8) { + fetchProbe('http://' + a1 + '/luci-static/resources/view/netwiz.js?v=' + Date.now(), 2000) + .then(function() { + clearInterval(countdownTimer); + var jumpUrl = 'http://' + a1 + '/cgi-bin/luci/admin/netwiz'; + var doJump = function() { window.location.href = jumpUrl; }; + callNetDefuse().then(doJump).catch(doJump); + setTimeout(doJump, 1000); + }).catch(function() {}); + } + } else { + clearInterval(countdownTimer); + var rollbackSec = 0; + var checkOldIpTimer = setInterval(function() { + rollbackSec += 3; + document.getElementById('nw-global-msg').innerHTML = '
    ' + T['MSG_WAIT_OLD'].replace('{sec}', rollbackSec) + '
    ' + T['MSG_ABANDONING'] + '
    '; + fetchProbe('http://' + h + '/cgi-bin/luci/?v=' + Date.now(), 2000) + .then(function() { + clearInterval(checkOldIpTimer); + + // 网络恢复后,向后端发送确认信号,解除看门狗回滚警报 + var confirmRpc = rpc.declare({ object: 'netwiz', method: 'confirm' }); + confirmRpc().then(function() { + // 确认成功后,再刷新页面进入系统 + var match = window.location.pathname.match(/;stok=[a-zA-Z0-9]+/); + var stok = match ? match[0] + '/' : ''; + window.location.href = window.location.protocol + '//' + h + '/cgi-bin/luci/' + stok + 'admin/netwiz'; + }).catch(function() { + // 确认失败,刷新避免卡死 + window.location.reload(); + }); + }).catch(function() {}); + }, 3000); + } + }, 3000); + } else { + var probeNewTimer = setInterval(function() { + sec += 3; + document.getElementById('nw-global-msg').innerHTML = '
    ' + T['LBL_TARGET'] + ' ' + actionDetail + '
    ' + T['MSG_WAIT_NET'].replace('{sec}', sec) + '
    '; + + // 前 9 秒倒数 + if (sec <= 9) return; + + fetchProbe('http://' + a1 + '/cgi-bin/luci/?v=' + Date.now(), 2000) + .then(function() { + clearInterval(probeNewTimer); + var doJump = function() { window.location.href = 'http://' + a1 + '/cgi-bin/luci/admin/netwiz'; }; + callNetDefuse().then(doJump).catch(doJump); + }).catch(function() {}); + }, 3000); + } + } else { + var checkSameTimer = setInterval(function() { + sec += 3; + document.getElementById('nw-global-msg').innerHTML = '
    ' + T['LBL_TARGET'] + ' ' + actionDetail + '
    ' + T['MSG_WAIT_NET'].replace('{sec}', sec) + '
    '; + + if (sec <= 9) return; + + fetchProbe('http://' + h + '/cgi-bin/luci/?v=' + Date.now(), 2000) + .then(function() { + clearInterval(checkSameTimer); + var match = window.location.pathname.match(/;stok=[a-zA-Z0-9]+/); + var stok = match ? match[0] + '/' : ''; + var jumpUrl = window.location.protocol + '//' + h + '/cgi-bin/luci/' + stok + 'admin/netwiz'; + var doJump = function() { window.location.href = jumpUrl; }; + callNetDefuse().then(doJump).catch(doJump); + }).catch(function() {}); + }, 3000); + } + }; + + if (mode === 'multi_pppoe') { + var mBoxes = container.querySelectorAll('.nw-multi-pppoe-box'); + mBoxes.forEach(function(b) { + var ifc = b.getAttribute('data-iface'); + var u = b.querySelector('.m-user').value.replace(/[\r\n\s]+/g, ''); + var p = b.querySelector('.m-pass').value; + uci.set('network', ifc, 'username', u); + uci.set('network', ifc, 'password', p); + }); + uci.save().then(function() { return uci.apply(); }).then(function() { succ(); }).catch(function() { succ(); }); + } else if (selectedMode === 'wifi') { + // 锁定 12 秒等待无线硬件就绪 + callNetSetup(mode, a1, a2, a3, a4, a5, a6).then(function() { + openModal({ + title: '🔄 ' + (T['M_REP_PROC_TIT'] || 'Processing'), + msg: '
    ' + + (T['MSG_WAIT'] || 'Please wait...') + '
    ' + + '
    ' + + '正在应用配置并重启无线驱动,大约需要 12 秒...
    ', + spin: true, + hideCancel: true, + hideOk: true // 不传按钮 + }); + + setTimeout(function() { + window.location.reload(); + }, 12000); // 强行挂起 12 秒后刷新 + }).catch(function(err) { + openModal({ title: T['M_SYS_ERR'] || 'Error', msg: 'Application failed: ' + err, okText: T['M_CLOSE'] || 'Close' }); + }); + } else { + // 非 WiFi 模式 + callNetSetup(mode, a1, a2, a3, a4, a5, a6).then(function() { succ(); }).catch(function() { succ(); }); + } + } catch (err) { + openModal({ title: T['M_SYS_ERR'], msg: 'Application failed: ' + err, okText: T['M_CLOSE'] }); + } + }); + } +}); diff --git a/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_dev.js b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_dev.js new file mode 100644 index 000000000..b41356c89 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_dev.js @@ -0,0 +1,2707 @@ +/* + * Copyright (C) 2026 huchd0 + * Licensed under the GNU General Public License v3.0 + */ +'use strict'; +'require view'; +'require dom'; +'require rpc'; + +var T = { + 'NW_TITLE': _('Netwiz NETWORK SETUP'), + 'NW_SUBTITLE': _('Pure · Secure · Non-destructive Minimalist Config'), + 'BTN_HOME': _('Back to Home'), + 'DEV_TITLE': _('Device Network Manager'), + 'DEV_SUBTITLE': _('Terminal Device Monitoring & Static IP Management'), + 'BTN_RESCAN': _('Rescan Network'), + 'BTN_REFRESH': _('Refresh'), + 'TXT_LOADING_RADAR': _('Syncing underlying network data...'), + 'TAB_ALL': _('All'), + 'TAB_MOBILE': _('Mobile/Tablet'), + 'TAB_PC': _('PC/Work'), + 'TAB_IOT': _('Smart Home'), + 'TAB_OTHER': _('Others'), + 'LBL_ROW_TYPE_SMART': _('Device Type (By IP)'), + 'LBL_ROW_TYPE_NAME': _('Device Type (Built-in)'), + 'LBL_ROW_CUSTOM': _('Custom Groups'), + 'TAB_DEPT_OTHER': _('Uncategorized'), + 'LBL_SMART_FILTER': _('Filter by IP Subnet'), + 'TIP_SMART_FILTER': _('Checked: Classify device types by IP subnet') + '\n' + _('Unchecked: Classify device types by built-in names'), + 'PH_DEPT_NAME': _('Alias'), + 'TXT_GROUP_PREFIX': _('Group '), + 'ERR_SAVE_RPC': '❌ ' + _('Save Failed!') + '\n' + _('Reason: RPC interface unresponsive ({err})') + '\n' + _('Please run this command in router SSH to restart the service: /etc/init.d/rpcd restart'), + 'LBL_SELECT_ALL': _('Select all available devices'), + 'TXT_SELECTED': _('Selected'), + 'TXT_ITEMS': _('items'), + 'BTN_BATCH_UNBIND': _('Batch Unbind'), + 'BTN_BATCH_BIND': _('Batch Assign'), + 'LBL_DEV_ALIAS': _('Device Alias'), + 'PH_DEV_ALIAS': _('e.g., John\'s PC'), + 'LBL_QUICK_STRAT': _('Quick Assign Strategy (Click to auto-fill)'), + 'STRAT_KEEP': _('Keep Original IP'), + 'STRAT_SMART': _('Smart Categorization'), + 'STRAT_SEQ': _('Sequential Memory'), + 'LBL_ASSIGN_IP': _('Assign Static IP (Auto anti-conflict)'), + 'PH_IP': _('e.g., 192.168.1.50'), + 'LBL_SEL_STRAT': _('Select Assign Strategy'), + 'STRAT_KEEP_TITLE': _('Keep Current IP (Recommended)'), + 'STRAT_KEEP_DESC': _('Convert the device\'s current random IP to a permanent static IP'), + 'STRAT_SMART_TITLE': _('Smart Subnet (By Device Type)'), + 'STRAT_SMART_DESC': _('Auto-identify mobile/PC, assign to exclusive subnets'), + 'STRAT_SEQ_TITLE': _('Specify Starting IP (Sequential)'), + 'STRAT_SEQ_DESC': _('Enter starting suffix, sequentially assign to all selected devices'), + 'LBL_START_IP': _('Starting IP'), + 'LBL_OTHERS': _('Others:'), + 'LBL_MOBILE': _('Mobile/Tablet:'), + 'LBL_PC': _('PC/Work:'), + 'LBL_IOT': _('Smart Home:'), + 'BTN_CANCEL': _('Cancel'), + 'BTN_OK': _('OK'), + 'MSG_EMPTY_CAT': _('No device records in this category'), + 'BDG_ONLINE': _('Online'), + 'BDG_OFFLINE': _('Offline'), + 'BDG_LONG_OFFLINE': _('Long-term Offline'), + 'BDG_STATIC': _('Static'), + 'BDG_GW': _('Upstream Gateway'), + 'BDG_LOCAL': _('Local System'), + 'BDG_VISITOR': _('Visiting'), + 'TXT_SYS_ROUTE': _('System Core Route'), + 'TXT_SYS_RESERVED': _('System Reserved Device'), + 'BTN_EDIT': _('Edit'), + 'BTN_UNBIND': _('Unbind'), + 'BTN_QUICK_BIND': _('Quick Bind'), + 'TXT_UNKNOWN_DEV': _('Unknown Device'), + 'TXT_UNKNOWN_IP': _('Unknown IP'), + 'BDG_ADDR_IP': _('Terminal Addressed IP'), + 'BDG_PENDING': _('Pending'), + 'TIT_EDIT_DEV': _('Edit Device Info'), + 'TIT_QUICK_BIND': _('Quick Bind IP'), + 'TXT_CONFIG_MAC': _('Configuring MAC:'), + 'BTN_SAVE': _('Save Changes'), + 'BTN_BIND_DEV': _('Bind Device'), + 'MSG_WRITING': _('Safely writing and restarting services...'), + 'TIT_UNBIND': _('Unbind IP'), + 'MSG_UNBIND_CONFIRM': _('Are you sure you want to unbind the static IP for device ({mac})?'), + 'BTN_CONFIRM_UNBIND': _('Confirm Unbind'), + 'MSG_RELEASING': _('Releasing static binding...'), + 'TIT_BATCH_UNBIND': _('Batch Unbind Devices'), + 'MSG_BATCH_UNBIND_CONFIRM': _('Among the {total} selected devices, {valid} have fixed IPs.

    Are you sure you want to unbind them?'), + 'MSG_BATCH_RELEASING': _('Batch releasing IPs, please wait...'), + 'TIT_BATCH_BIND': _('Batch Assign Settings'), + 'TXT_SEL_STRAT_COUNT': _('Select Assign Strategy (Selected {count} devices)'), + 'BTN_START_ASSIGN': _('Start Assign'), + 'MSG_RADAR_AVOID': _('Avoiding conflicts and silently writing rules...'), + 'MSG_NO_DEVS': _('No device records found in current LAN'), + 'MSG_SCAN_FAIL': '❌ ' + _('Scan failed: Cannot retrieve underlying data'), + 'ERR_POOL_FULL': '❌ ' + _('The IP pool for this type is full. Please expand the range or choose another strategy!'), + 'ERR_IP_EMPTY': _('IP address cannot be empty!'), + 'ERR_IP_CONFLICT': '❌ ' + _('IP Conflict Blocked!') + '\n\n' + _('The IP [{ip}] is occupied by device [{name}].') + '\n' + _('Please modify the strategy or enter a free IP!'), + 'TIP_NO_CHANGE': '💡 ' + _('Tip: The device is already fixed at this IP. No changes needed!'), + 'TIP_ALL_UNBOUND': '💡 ' + _('Tip: The {count} selected devices are all [Unbound]. No unbind action needed!'), + 'ERR_SUF_RANGE': _('Starting suffix must be between 2 and 254!'), + 'ERR_POOL_INSUFF': '❌ ' + _('IP pool insufficient!') + '\n' + _('Only {avail} IPs left from .{suf}, but {count} devices selected.'), + 'ERR_CAT_FAIL': '❌ ' + _('[{name}] assignment failed!') + '\n' + _('Selected {req} devices, but only {avail} IPs available in the preset range.') + '\n' + _('Please expand the IP range for this category!'), + 'TIP_BATCH_NO_CHANGE': '💡 ' + _('Smart block: According to your strategy, the {count} selected devices are already fixed with unchanged IPs. No need to rewrite rules!'), + 'TXT_INFINITE': _('Infinite'), + 'TXT_EXPIRED': _('Expired'), + 'STRAT_DEPT': _('Group IP Pool'), + 'STRAT_DEPT_TITLE': _('Group Pool (Auto Assign)'), + 'STRAT_DEPT_DESC': _('Assign free IPs automatically from the selected Target Group\'s specific IP range'), + 'TIT_MGR_DEPTS': _('Department Network Segments'), + 'BTN_ADD_DEPT': _('+ Add New Department'), + 'ERR_DEPT_OVERLAP': '❌ ' + _('Subnet Conflict: IP ranges between groups cannot overlap!') + '\n' + _('Conflicting groups: {groups}'), + 'ERR_DEPT_NAME_DUP': '❌ ' + _('Save Failed: Group names cannot be duplicated!') + '\n' + _('Duplicate name: {name}'), + 'ERR_DEPT_INVALID': '❌ ' + _('Save Failed: IPs must be between 2-254 and format must be valid!'), + 'ERR_DEPT_POOL_FULL': '❌ ' + _('IP pool reached the end (254). Cannot auto-append. Please arrange subnets manually!'), + 'ERR_DEPT_FULL': '❌ ' + _('The IP pool for the selected department is full! Please expand the range.'), + 'LBL_TARGET_GROUP': _('Assign to Group (Optional)'), + 'OPT_NO_GROUP': _('-- Uncategorized (None) --'), + 'ERR_DEPT_NOT_SEL': '❌ ' + _('Strategy Error: Please select a Target Group first to allocate IPs from its pool!'), + 'TIT_FW_CONTROL': _('Firewall & Access Control'), + 'BDG_FW_BLK': _('Net Blocked'), + 'BDG_FW_ISO': _('Isolated'), + 'BDG_FW_DMZ': _('DMZ Host'), + 'LBL_SHOW_CONNS': _('Live Connections'), + 'TIP_SHOW_CONNS': _('Monitor real-time connections (Auto turn off in 3 mins)'), + 'LBL_CONN_COUNT': _('Conns'), + 'TXT_FW_PANEL_TITLE': _('Manage device network access anytime, anywhere'), + 'LBL_FW_BLK_TITLE': '⛔ ' + _('Block Internet (WAN)'), + 'LBL_FW_BLK_DESC': _('Instantly cut off internet access. LAN communication remains normal.'), + 'LBL_FW_ISO_TITLE': '🛡️ ' + _('LAN Isolation (Anti-snooping)'), + 'LBL_FW_ISO_DESC': _('Block internal network access for this device to prevent scanning and privacy leaks.'), + 'LBL_FW_DMZ_TITLE': '🚀 ' + _('DMZ Host (Expose to WAN)'), + 'LBL_FW_DMZ_DESC': _('Forward all WAN ports to this device. Only one device allowed.'), + 'TIP_MAC_CTRL': _('Click to control access'), + 'ERR_DMZ_NO_IP': '❌ ' + _('Cannot enable DMZ: Device has no valid IP or is offline!'), + 'ERR_DMZ_OCCUPIED_1': '❌ ' + _('Denied! Only one DMZ host allowed.') + '\n\n' + _('Device ['), + 'ERR_DMZ_OCCUPIED_2': _('] is currently the DMZ.') + '\n' + _('Please disable its DMZ first!'), + 'ERR_FW_SAVE_FAIL': '❌ ' + _('Save failed!') + '\n\n' + _('Reason: RPC Error ({err}).') + '\n' + _('Please run `/etc/init.d/rpcd restart` in SSH and try again!'), + 'ERR_SAVE_FAIL_SHORT': '❌ ' + _('Save failed!') + '\n' + _('Reason: {err}') + '\n' + _('Please run `/etc/init.d/rpcd restart` in SSH'), + 'ERR_IP_FORMAT': '❌ ' + _('Invalid IP format! Please enter a valid IPv4 address (e.g., 192.168.1.50)'), + 'TIP_V6_COPY': _('Public IPv6 (Click to copy):'), + 'MSG_V6_COPIED': _('First IPv6 address copied successfully:'), + 'BTN_EXPORT_DEPTS': _('Export Groups'), + 'BTN_IMPORT_DEPTS': _('Import Groups'), + 'MSG_IMPORT_SUCCESS': '✅ ' + _('Import successful!') + '\n' + _('Please verify and click [Save] below to apply.'), + 'ERR_IMPORT_FAIL': '❌ ' + _('Import failed!') + '\n' + _('Invalid or corrupted file format. Please select a valid JSON backup file.'), + 'BDG_NEW_UNKNOWN': _('Suspected Spoofed Device'), + 'BTN_RESET_ALL': _('Factory Reset'), + 'TIT_RESET_ALL': '⚠️ ' + _('Danger: Restore Default Network'), + 'MSG_RESET_CONFIRM': _('Are you sure you want to clear all static IPs and firewall rules set by Netwiz?

    This operation will delete all groups, blacklists, isolations, and DMZ settings, and restart network services.'), + 'MSG_RESETTING': _('Cleaning up and restarting network services...'), + 'TIT_CONN_RADAR': '⚡ ' + _('Connection Radar Analysis'), + 'MSG_DIVE_KERNEL': _('Diving into system kernel to capture connection records...'), + 'LBL_CONN_WEB': _('Web/HTTPS'), + 'LBL_CONN_DNS': _('DNS/NTP'), + 'LBL_CONN_UDP': _('UDP Stream/Game'), + 'LBL_CONN_P2P': _('P2P/High Ports'), + 'LBL_CONN_OTH': _('Others/Unknown'), + 'MSG_CONN_TOTAL': _('# Successfully parsed {total} source connection records...'), + 'MSG_CONN_CMD': _('root@OpenWrt:~# cat /proc/net/nf_conntrack | awk \'...\' # (Filtering src={ip})'), + 'MSG_CONN_EMPTY': _('[ No active connection records ]'), + 'MSG_CONN_TRUNC': _('... (First 50 records shown for performance) ...'), + 'MSG_CONN_ANALYZE_FAIL': _('Analysis failed: {err}'), + 'BTN_EXPORT_NET': _('Export Config'), + 'BTN_IMPORT_NET': _('Import Config'), + 'TIT_EXPORT_ING': '📦 ' + _('Packing Network Config'), + 'MSG_EXPORT_ING': _('
    Generating exclusive backup file

    (Includes: WAN, Wi-Fi, IPv6, Bypass, Static IPs & Firewall rules)
    '), + 'BTN_PLEASE_WAIT': _('Please wait...'), + 'TIT_EXPORT_OK': '✅ ' + _('Export Successful'), + 'MSG_EXPORT_OK': _('Core network configuration file has been successfully downloaded!'), + 'BTN_CLOSE': _('Close'), + 'TIT_EXPORT_FAIL': '❌ ' + _('Export Failed'), + 'MSG_EXPORT_FAIL_NODATA': _('Backend returned no valid data'), + 'MSG_EXPORT_FAIL_ERR': _('Error occurred: '), + 'TIT_IMPORT_CONFIRM': '⚠️ ' + _('Confirm Overwrite?'), + 'MSG_IMPORT_CONFIRM': _('About to overwrite the router\'s core network settings!

    ') + '💡 ' + _('Safety Mechanism: The system will automatically backup the current state to the /root/netwiz_bak/ directory before importing.

    Network services will automatically restart after import, which may cause brief Wi-Fi/LAN disconnection. Please confirm.'), + 'BTN_CONFIRM_IMPORT': _('Confirm & Apply'), + 'MSG_IMPORT_RESTARTING': _('Writing configuration and restarting network services,
    Please wait... Network may disconnect briefly, taking about 10-25 seconds...'), + 'TIT_RESTORE_NET': '⬆️ ' + _('Restore Network Config'), + 'MSG_READING_BAK': _('
    Reading router backup records...
    '), + 'OPT_NO_BAK': _('[ No history backups found ]'), + 'LBL_RESTORE_ROUTER': '📂 ' + _('Restore from Router History'), + 'BTN_RESTORE_SEL': '⬇️ ' + _('Restore Selected Backup'), + 'TXT_OR': _('— OR —'), + 'LBL_RESTORE_PC': '💻 ' + _('Upload from Local PC'), + 'BTN_BROWSE_PC': '📁 ' + _('Browse PC Files...'), + 'TIT_READ_FAIL': '❌ ' + _('Read Failed'), + 'MSG_READ_FAIL': _('Unable to fetch router backup list.'), + 'TXT_BAK_AUTO': _('Auto Backup'), + 'TXT_BAK_IMPORT': _('Before Import'), + 'TXT_BAK_RESET': _('Before Reset'), + 'OPT_NO_CHANGE': _('-- Keep Unchanged --'), + 'BDG_V6_RESERVED': _('Reserved'), + 'TIP_DEPT_BIND_RULE': _('Custom group names are not strictly bound to IP subnets. The ranges are only used for automatic IP assignment when a group is selected.'), + 'TXT_UNOPERABLE': _('Unoperable'), + 'TXT_NOTE': _('Note:'), + 'BTN_WOL': _('Wake on LAN (WOL)'), + 'MSG_WOL_SENT_1': _('Wake packet sent to {mac}!'), + 'MSG_WOL_SENT_2': _('(Device booting may take a minute)'), + 'TIT_FILE_LARGE': _('File Too Large'), + 'MSG_FILE_LARGE': _('
    Backup files are usually under 1MB!

    The selected file is too large and has been blocked. Please ensure you didn\'t select the wrong file (e.g., firmware or video).'), + 'MSG_RESTORE_NET_CONFIRM': _('This action will overwrite the existing network configuration and restart the network.

    ') + '⚠️ ' + _('Security Warning: Do not upload backups from unknown sources to prevent router hijacking.

    Confirm restore?'), + 'TIT_RESTORING': _('Restoring'), + 'TIT_RESTORE_FAIL': _('Restore Failed'), + 'TXT_ERROR': _('Error: '), + 'TIT_JSON_LARGE': _('File Too Large'), + 'MSG_JSON_LARGE': _('
    Config files are usually under 100KB!

    The selected file is too large and has been blocked.'), + 'TIT_JSON_INVALID': _('Invalid Format'), + 'MSG_JSON_INVALID': _('The uploaded file is not a valid JSON format and cannot be parsed!'), + 'TIT_RESTORE_JSON': _('Import Configuration'), + 'MSG_RESTORE_JSON': _('Are you sure you want to import this configuration? Existing data will be overwritten.'), + 'V6_NAT_ERR_TIT1': '🚨 ' + _('Severe Network Topology Conflict!'), + 'V6_NAT_ERR_MSG1': _('System detected that IPv6 and LAN "Masquerading (NAT)" are BOTH enabled!This will paralyze IPv6 allocation and cause routing loops.
    ') + '👉 ' + _('Fix: Please go to Network -> Firewall to disable LAN Masquerading, or Disable IPv6 in the LAN settings on the Netwiz homepage.'), + 'TIT_RESTORE_DATA': _('Restore Device Data'), + 'MSG_RESTORE_CONFIRM': _('This action will overwrite existing device groups and static IP bindings, and restart the network.

    ') + '⚠️ ' + _('Security Warning: Do not upload backups from unknown sources to prevent DNS hijacking.

    Confirm to restore?'), + 'TXT_SUCCESS': _('Success'), + 'TXT_WARNING': _('Warning'), + 'TXT_WAKING_UP': _('Waking up...'), + 'TIP_LOCAL_V6': _('Displaying self-assigned local IPv6 as IPv4 is unavailable'), + 'BDG_LOCAL_V6': _('Local IPv6'), + 'LBL_FW_EXPOSE_TITLE': '🌍 ' + _('Direct WAN Access (Whitelist)'), + 'LBL_FW_EXPOSE_DESC': _('Open firewall to allow direct external access to this device.'), + 'OPT_PROTO_V6': _('IPv6 Only'), + 'OPT_PROTO_V4': _('IPv4 Only'), + 'OPT_PROTO_ALL': _('IPv4 + IPv6'), +}; + +var callDeviceList = rpc.declare({ object: 'netwiz_dev', method: 'get_list', params: ['show_conns', 'do_rescan'], expect: { '': {} } }); +var callWakeDevice = rpc.declare({ object: 'netwiz_dev', method: 'wake_device', params: ['mac'], expect: { result: 0 } }); +var callFwSet = rpc.declare({ object: 'netwiz_dev', method: 'fw_set', params: ['mac', 'ip', 'blk_en', 'iso_en', 'dmz_en', 'expose_en', 'expose_proto'], expect: { result: 0 } }); +var callDeviceBind = rpc.declare({ object: 'netwiz_dev', method: 'bind', params: ['mac', 'ip', 'name', 'dept', 'no_reload'], expect: { result: 0 } }); +var callDeviceUnbind = rpc.declare({ object: 'netwiz_dev', method: 'unbind', params: ['mac', 'no_reload'], expect: { result: 0 } }); +var callApplyDhcp = rpc.declare({ object: 'netwiz_dev', method: 'apply_dhcp', expect: { result: 0 } }); +var callGetDepts = rpc.declare({ object: 'netwiz_dev', method: 'get_depts', expect: { depts: [] } }); +var callSaveDepts = rpc.declare({ object: 'netwiz_dev', method: 'save_depts', params: ['data'], expect: { result: 0 } }); +var callV6KeepAlive = rpc.declare({ object: 'netwiz_dev', method: 'v6_keep_alive', params: ['mac', 'name'], expect: { result: 0 } }); + +var callGetSmartRanges = rpc.declare({ object: 'netwiz_dev', method: 'get_smart_ranges', expect: { ranges: {} } }); +var callSaveSmartRanges = rpc.declare({ object: 'netwiz_dev', method: 'save_smart_ranges', params: ['data'], expect: { result: 0 } }); +var callResetAll = rpc.declare({ object: 'netwiz_dev', method: 'reset_all', expect: { result: 0 } }); +var callAnalyzeConns = rpc.declare({ object: 'netwiz_dev', method: 'analyze_conns', params: ['ip'], expect: { '': {} } }); +var callExportConfig = rpc.declare({ object: 'netwiz_dev', method: 'export_config', expect: { '': {} } }); +var callImportConfig = rpc.declare({ object: 'netwiz_dev', method: 'import_config', params: ['data', 'file'], expect: { result: 0 } }); +var callListBackups = rpc.declare({ object: 'netwiz_dev', method: 'list_backups', expect: { backups: [] } }); +var callAutoBackup = rpc.declare({ object: 'netwiz_dev', method: 'auto_backup', expect: { result: 0 } }); + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + render: function () { + if (!document.querySelector('meta[name="viewport"]')) { + var meta = document.createElement('meta'); + meta.name = 'viewport'; + meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'; + document.head.appendChild(meta); + } + + var container = dom.create('div', { id: 'netwiz-dev-container' }); + + var htmlTemplate = [ + '', + '', + '
    ', + '
    ', + '
    ', + '
    {{NW_TITLE}}
    ', + '
    {{APP_VERSION}}
    ', + '
    ', + '

    {{NW_SUBTITLE}}

    ', + '
    ', + + '
    ', + '
    ', + ' ', + '
    ', + '
    ', + '
    {{DEV_TITLE}}
    ', + '

    {{DEV_SUBTITLE}}

    ', + '
    ', + '
    ', + ' ', + '
    ', + ' {{BTN_REFRESH}}', + '
    ', + '
    ', + '
    ', + + '
    ', + '
    ', + ' {{TXT_LOADING_RADAR}}', + '
    ', + + ' ', + + ' ', + + ' ', + + '
    ', + '
    ×
    ', + '
    {{TXT_SELECTED}} 0 {{TXT_ITEMS}}
    ', + '
    ', + ' ', + ' ', + '
    ', + '
    ', + '
    ', + + '
    ', + '
    ', + '
    ', + '
    ', + + ' ', + + ' ', + + ' ', + '
    ', + ' ', + ' ', + '
    ', + '
    ', + '
    ' + ].join('\n'); + + for (var k in T) { htmlTemplate = htmlTemplate.split('{{' + k + '}}').join(T[k]); } + container.innerHTML = htmlTemplate; + this.bindEvents(container); + return container; + }, + + bindEvents: function(container) { + + // ========================================== + // 自定义 Alert 弹窗 + // ========================================== + var showCustomAlert = function(msg, title) { + // 智能识别文本中的 emoji,自动分配标题 + var defaultTitle = '💡 ' + (T['TXT_NOTE'] || '提示'); + if (msg.indexOf('❌') !== -1) defaultTitle = '❌ ' + (T['TXT_ERROR'] || '错误'); + if (msg.indexOf('✅') !== -1) defaultTitle = '✅ ' + (T['TXT_SUCCESS'] || '成功'); + if (msg.indexOf('⚠️') !== -1) defaultTitle = '⚠️ ' + (T['TXT_WARNING'] || '警告'); + + // 创建独立遮罩层 + var overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(15,23,42,0.65); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); z-index:2147483647 !important; display:flex; justify-content:center; align-items:center; opacity:0; transition:opacity 0.2s;'; + + // 自动将 \n 转换为
    + var formattedMsg = String(msg).replace(/\n/g, '
    '); + + // 完全复用原有的 .nd-modal-box 样式类 + var box = document.createElement('div'); + box.className = 'nd-modal-box'; + box.style.cssText = 'transform:scale(0.95); transition:transform 0.2s; max-width: 380px; width: 90%; padding: 22px;'; + box.innerHTML = + '
    ' + (title || defaultTitle) + '
    ' + + '
    ' + formattedMsg + '
    ' + + '
    ' + + '' + + '
    '; + + overlay.appendChild(box); + document.body.appendChild(overlay); + + // 触发平滑弹出的动画 + requestAnimationFrame(function() { + overlay.style.opacity = '1'; + box.style.transform = 'scale(1)'; + }); + + var closeModal = function() { + overlay.style.opacity = '0'; + box.style.transform = 'scale(0.95)'; + setTimeout(function() { + if (document.body.contains(overlay)) document.body.removeChild(overlay); + }, 200); + }; + + // 绑定关闭按钮和点击背景空白处关闭 + box.querySelector('button').onclick = closeModal; + overlay.onclick = function(e) { if (e.target === overlay) closeModal(); }; + }; + // ========================================== + + container.querySelector('#dev-back').addEventListener('click', function(e) { + e.preventDefault(); + var wrap = document.querySelector('.nw-wrapper'); + if (wrap) wrap.classList.add('page-leaving'); + + setTimeout(function() { + window.location.href = window.location.pathname.replace('/netwiz_dev', '/netwiz'); + }, 350); + }); + + var modalOverlay = container.querySelector('#nd-modal-overlay'); + var mTitle = container.querySelector('#nd-m-title'); + var mContent = container.querySelector('#nd-m-content'); + var mForm = container.querySelector('#nd-m-form'); + var mDeptMgr = container.querySelector('#nd-m-dept-mgr'); + var mNormalFields = container.querySelector('#nd-m-normal-fields'); + var mBatchFields = container.querySelector('#nd-m-batch-fields'); + var mInpName = container.querySelector('#nd-inp-name'); + var mInpIp = container.querySelector('#nd-inp-ip'); + + var mSingleStrategyGroup = container.querySelector('#nd-single-strategy-group'); + var mSingleTagSelect = container.querySelector('#nd-single-tag-select'); + var singleRadios = container.querySelectorAll('input[name="single_strategy"]'); + var currentSingleDev = null; + var currentOriginalIp = ""; + + var strategyCards = container.querySelectorAll('.nd-strategy-card'); + var batchIpGroup = container.querySelector('#nd-batch-ip-group'); + var batchSmartDesc = container.querySelector('#nd-batch-smart-desc'); + var batchTagSelect = container.querySelector('#nd-batch-tag-select'); + var batchSuffixInput = container.querySelector('#nd-batch-suffix'); + + var mBtnOk = container.querySelector('#nd-m-ok'); + var mBtnCancel = container.querySelector('#nd-m-cancel'); + + if (modalOverlay) { document.body.appendChild(modalOverlay); } + + var controlBar = container.querySelector('.nd-control-bar'); + if (controlBar) { + var lastHeight = -1; + var adjustStickyTop = function() { + var validBottoms = [0]; + var headers = document.querySelectorAll('header, .navbar, .main-top, #header, .topbar'); + for (var i = 0; i < headers.length; i++) { + var style = window.getComputedStyle(headers[i]); + if (style.position === 'fixed' || style.position === 'sticky') { + var rect = headers[i].getBoundingClientRect(); + + if (rect.top <= 5 && rect.height >= 35 && rect.height <= 90) { + validBottoms.push(rect.bottom); + } + } + } + + var topOffset = 0; + var realBottoms = validBottoms.filter(function(b) { return b > 0; }); + if (realBottoms.length > 0) topOffset = Math.min.apply(null, realBottoms); + if (topOffset > 0) topOffset -= 3; + + if (topOffset !== lastHeight) { + controlBar.style.setProperty('top', topOffset + 'px', 'important'); + lastHeight = topOffset; + } + }; + + var startTime = Date.now(); + var fastCheck = function() { + adjustStickyTop(); + if (Date.now() - startTime < 2000) { + window.requestAnimationFrame(fastCheck); + } + }; + window.requestAnimationFrame(fastCheck); + + window.addEventListener('scroll', function() { + if (lastHeight <= 0) adjustStickyTop(); + }, { passive: true }); + window.addEventListener('resize', adjustStickyTop); + } + + var savedStrategy = localStorage.getItem('nw_batch_strategy') || 'keep'; + var defaultSmartRanges = {ms:30, me:69, ps:70, pe:109, is:110, ie:149, os:150, oe:199}; + var cachedRangesStr = localStorage.getItem('nw_smart_ranges'); + var savedRanges = cachedRangesStr ? JSON.parse(cachedRangesStr) : JSON.parse(JSON.stringify(defaultSmartRanges)); + var needFetchSmart = !cachedRangesStr; + var basePrefix = '192.168.1.'; + + var smartFilterByIp = localStorage.getItem('nw_smart_filter') !== 'false'; + + var cbSmartFilter = container.querySelector('#cb-smart-filter'); + if (cbSmartFilter) { + cbSmartFilter.checked = smartFilterByIp; + cbSmartFilter.addEventListener('change', function() { + smartFilterByIp = this.checked; + localStorage.setItem('nw_smart_filter', smartFilterByIp); + selectedDevices = []; + updateTabsAndFilter(); + }); + } + + var rangeInputs = [ + {s: '#sm-mob-s', e: '#sm-mob-e'}, + {s: '#sm-pc-s', e: '#sm-pc-e'}, + {s: '#sm-iot-s', e: '#sm-iot-e'}, + {s: '#sm-oth-s', e: '#sm-oth-e'} + ]; + function autoCascadeRanges() { + var prevEnd = 1; + for (var i = 0; i < rangeInputs.length; i++) { + var elS = modalOverlay.querySelector(rangeInputs[i].s); + var elE = modalOverlay.querySelector(rangeInputs[i].e); + var valS = parseInt(elS.value) || 2; + var valE = parseInt(elE.value) || 2; + + if (valS <= prevEnd) valS = prevEnd + 1; + if (valE < valS) valE = valS + 10; + if (valE > 254) valE = 254; + if (valS > 254) valS = 253; + + elS.value = valS; + elE.value = valE; + prevEnd = valE; + } + } + rangeInputs.forEach(function(ri) { + modalOverlay.querySelector(ri.s).addEventListener('change', autoCascadeRanges); + modalOverlay.querySelector(ri.e).addEventListener('change', autoCascadeRanges); + }); + + var globalDepartments = []; + + function getDeviceDept(dev) { + if (dev.dept && dev.dept !== 'none' && dev.dept !== '') { + return globalDepartments.find(function(d){ return d.id === dev.dept; }) || null; + } + return null; + } + + function populateTagSelects() { + var baseHtml = ''; + globalDepartments.forEach(function(d) { + baseHtml += ''; + }); + + if(mSingleTagSelect) { + mSingleTagSelect.innerHTML = '' + baseHtml; + } + if(batchTagSelect) { + // “保持不变”选项 + batchTagSelect.innerHTML = '' + baseHtml; + } + } + + function renderDeptManager(overrideDepts) { + var c = modalOverlay.querySelector('#dept-list-container'); + c.innerHTML = ''; + var tempDepts = overrideDepts ? overrideDepts : JSON.parse(JSON.stringify(globalDepartments)); + + function renderRow(d, idx) { + var el = document.createElement('div'); + el.className = 'dept-row'; + el.style.cssText = 'background:#f8fafc; padding:10px; border-radius:8px; border:1px solid #e2e8f0; margin-bottom:10px;'; + el.innerHTML = + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + ''+basePrefix.split('.')[0]+'.'+basePrefix.split('.')[1]+'.'+basePrefix.split('.')[2]+'.' + + '' + + '-' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + + el.querySelector('.d-del').onclick = function() { el.remove(); }; + + var inpS = el.querySelector('.d-start'); + var inpE = el.querySelector('.d-end'); + var enforceLimits = function() { + var s = parseInt(inpS.value); + var e = parseInt(inpE.value); + if (isNaN(s)) s = 2; + if (isNaN(e)) e = 2; + if (s < 2) s = 2; if (s > 254) s = 254; + if (e < 2) e = 2; if (e > 254) e = 254; + if (s > e) e = s; + inpS.value = s; + inpE.value = e; + }; + inpS.addEventListener('blur', enforceLimits); + inpE.addEventListener('blur', enforceLimits); + + return el; + } + + tempDepts.forEach(function(d, i) { c.appendChild(renderRow(d, i)); }); + + if (tempDepts.length === 0) { + var newId = 'dept_' + Math.random().toString(36).substring(2,8); + c.appendChild(renderRow({id: newId, icon: '🏷️', name: '', start: 50, end: 80, color: '#3b82f6'}, 0)); + } + + modalOverlay.querySelector('#btn-add-dept').onclick = function() { + var currentRows = c.querySelectorAll('.dept-row'); + var maxEnd = 49; + currentRows.forEach(function(r) { + var eVal = parseInt(r.querySelector('.d-end').value); + if (!isNaN(eVal) && eVal > maxEnd) maxEnd = eVal; + }); + + var newStart = maxEnd + 1; + if (newStart > 254) { + showCustomAlert(T['ERR_DEPT_POOL_FULL']); + return; + } + + var newEnd = newStart + 30; + if (newEnd > 254) newEnd = 254; + + var newId = 'dept_' + Math.random().toString(36).substring(2,8); + var newRow = renderRow({id: newId, icon: '🏷️', name: '', start: newStart, end: newEnd, color: '#64748b'}, currentRows.length); + if (c.firstChild) { + c.insertBefore(newRow, c.firstChild); + } else { + c.appendChild(newRow); + } + c.scrollTop = 0; + }; + + // ================= 导入导出 ================= + modalOverlay.querySelector('#btn-export-depts').onclick = function() { + var currentData = saveDepartmentsFromDOM(); + if (currentData === false) return; // 如果有错误(重叠或留空),退出 + + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(currentData, null, 2)); + var dlNode = document.createElement('a'); + dlNode.setAttribute("href", dataStr); + var dateStr = new Date().toISOString().slice(0,10).replace(/-/g,""); + dlNode.setAttribute("download", "netwiz_groups_" + dateStr + ".json"); + + document.body.appendChild(dlNode); + dlNode.click(); + document.body.removeChild(dlNode); + }; + + modalOverlay.querySelector('#btn-import-depts').onclick = function() { + var fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.json'; + fileInput.style.display = 'none'; // 保持隐藏 + + fileInput.onchange = function(e) { + var file = e.target.files[0]; + if (!file) return; + + // 不超 100KB + if (file.size > 100 * 1024) { + showCustomAlert( + T['MSG_JSON_LARGE'] || '
    配置文件通常小于 100KB!

    您选择的文件过大,已被安全拦截。', + '⚠️ ' + (T['TIT_JSON_LARGE'] || '文件过大') + ); + return; + } + + var reader = new FileReader(); + reader.onload = function(evt) { + try { + // 是否合法 JSON 格式 + var importedData = JSON.parse(evt.target.result); + if (!Array.isArray(importedData)) throw new Error('Not an array'); + + // 成功,渲染到介面 + renderDeptManager(importedData); + showCustomAlert(T['MSG_IMPORT_SUCCESS']); + } catch (err) { + // 解析失败 + showCustomAlert( + T['MSG_JSON_INVALID'] || '您上传的文件不是有效的 JSON 格式,无法解析!', + '❌ ' + (T['TIT_JSON_INVALID'] || '格式无效') + ); + } + }; + // 注意:這裡使用 readAsText 讀取純文字,不需要 Base64 轉換,效能極高 + reader.readAsText(file); + }; + + document.body.appendChild(fileInput); + fileInput.click(); + document.body.removeChild(fileInput); + }; + // ================================================================ + + } + + function saveDepartmentsFromDOM() { + var rows = modalOverlay.querySelectorAll('.dept-row'); + var newDepts = []; + var nameSet = {}; + + for(var idx=0; idx 254 || d.start > d.end) { + showCustomAlert(T['ERR_DEPT_INVALID']); + return false; + } + + if (nameSet[d.name]) { + showCustomAlert(T['ERR_DEPT_NAME_DUP'].replace('{name}', '[' + d.name + ']')); + return false; + } + nameSet[d.name] = true; + + newDepts.push(d); + } + + for(var i=0; i>> 0; } + function longToIp(long) { return [ (long >>> 24), (long >> 16 & 255), (long >> 8 & 255), (long & 255) ].join('.'); } + + function getAvailableIpInRange(bPrefix, startSuffix, endSuffix, usedIps) { + for (var i = startSuffix; i <= endSuffix; i++) { + var testIp = bPrefix + i; + if (usedIps.indexOf(testIp) === -1) return testIp; + } + return null; + } + + function getNextAvailableIp(startIp, usedIps) { + var num = ipToLong(startIp); + while(usedIps.indexOf(longToIp(num)) !== -1) { num++; } + return longToIp(num); + } + + function getDeviceType(dev) { + var n = (dev.name || '').toLowerCase(); + if (n.match(/iphone|ipad|ios|android|galaxy|huawei|xiaomi|redmi|vivo|oppo|realme|oneplus|phone|pad|tablet|honor|meizu/)) return 'mobile'; + if (n.match(/win|windows|desktop|laptop|pc|macbook|imac|macmini|thinkpad|dell|hp|lenovo|asus|acer|ubuntu|linux|debian|ds|nas/)) return 'pc'; + if (n.match(/router|ap|wifi|mesh|tplink|tenda|asus_router|netgear|phicomm|zte|ruijie|mercury|xiaomi_router|camera|plug|socket|switch|light|lamp|bulb|printer|tv|soundbox|iot|smart|miap|lumi|viomi|aqara|cam|speaker|audio|watch|jdc/)) return 'iot'; + return 'type_other'; + } + + var updateSingleIpByStrategy = function() { + if(!currentSingleDev) return; + var activeRadio = modalOverlay.querySelector('input[name="single_strategy"]:checked'); + if(!activeRadio) return; + var val = activeRadio.value; + + var usedIps = []; + globalDevices.forEach(function(d) { + if (d.mac === currentSingleDev.mac) return; + if (d.ip && d.ip !== 'Unknown IP') usedIps.push(d.ip); + if (d.bound_ip && d.bound_ip !== 'Unknown IP') usedIps.push(d.bound_ip); + }); + + if (val === 'keep') { + mInpIp.value = currentOriginalIp; + } else if (val === 'smart') { + var devType = getDeviceType(currentSingleDev); + var sStart = savedRanges.os, sEnd = savedRanges.oe; + if (devType === 'mobile') { sStart = savedRanges.ms; sEnd = savedRanges.me; } + else if (devType === 'pc') { sStart = savedRanges.ps; sEnd = savedRanges.pe; } + else if (devType === 'iot') { sStart = savedRanges.is; sEnd = savedRanges.ie; } + var smartIp = getAvailableIpInRange(basePrefix, sStart, sEnd, usedIps); + if(smartIp) { + mInpIp.value = smartIp; + } else { + mInpIp.value = ''; + showCustomAlert(T['ERR_POOL_FULL']); + } + } else if (val === 'dept') { + var dId = mSingleTagSelect.value; + if (dId === 'none') { + mInpIp.value = ''; + } else { + var tgt = globalDepartments.find(function(d){ return d.id === dId; }); + if(tgt) { + var dip = getAvailableIpInRange(basePrefix, tgt.start, tgt.end, usedIps); + mInpIp.value = dip || ''; + if(!dip) showCustomAlert(T['ERR_DEPT_FULL']); + } + } + } else if (val === 'seq') { + var lastIp = localStorage.getItem('nw_last_ip'); + if (!lastIp || lastIp.substring(0, lastIp.lastIndexOf('.') + 1) !== basePrefix) { + lastIp = basePrefix + "50"; + } + mInpIp.value = getNextAvailableIp(lastIp, usedIps); + } + }; + + singleRadios.forEach(function(radio) { + radio.addEventListener('change', updateSingleIpByStrategy); + }); + + mSingleTagSelect.addEventListener('change', function() { + var activeRadio = modalOverlay.querySelector('input[name="single_strategy"]:checked'); + if (activeRadio && activeRadio.value === 'dept') { + updateSingleIpByStrategy(); + } + }); + + function applyStrategyUI(val) { + strategyCards.forEach(function(c) { c.classList.remove('active'); }); + var targetCard = modalOverlay.querySelector('.nd-strategy-card[data-val="'+val+'"]'); + if (targetCard) targetCard.classList.add('active'); + + if (val === 'seq') { + batchIpGroup.style.display = 'block'; + batchSmartDesc.style.display = 'none'; + setTimeout(function(){ batchSuffixInput.focus(); }, 100); + } else if (val === 'smart') { + batchIpGroup.style.display = 'none'; + batchSmartDesc.style.display = 'block'; + modalOverlay.querySelectorAll('.nd-ip-prefix').forEach(function(el) { el.innerText = basePrefix; }); + modalOverlay.querySelector('#sm-oth-s').value = savedRanges.os; + modalOverlay.querySelector('#sm-oth-e').value = savedRanges.oe; + modalOverlay.querySelector('#sm-mob-s').value = savedRanges.ms; + modalOverlay.querySelector('#sm-mob-e').value = savedRanges.me; + modalOverlay.querySelector('#sm-pc-s').value = savedRanges.ps; + modalOverlay.querySelector('#sm-pc-e').value = savedRanges.pe; + modalOverlay.querySelector('#sm-iot-s').value = savedRanges.is; + modalOverlay.querySelector('#sm-iot-e').value = savedRanges.ie; + autoCascadeRanges(); + } else { + batchIpGroup.style.display = 'none'; + batchSmartDesc.style.display = 'none'; + } + } + + strategyCards.forEach(function(card) { + card.addEventListener('click', function() { + var val = this.getAttribute('data-val'); + localStorage.setItem('nw_batch_strategy', val); + applyStrategyUI(val); + }); + }); + + batchSuffixInput.addEventListener('focus', function() { this.parentElement.style.borderColor = '#3b82f6'; this.parentElement.style.boxShadow = '0 0 0 3px rgba(59,130,246,0.15)'; }); + batchSuffixInput.addEventListener('blur', function() { this.parentElement.style.borderColor = '#cbd5e1'; this.parentElement.style.boxShadow = 'none'; }); + + var openModal = function(options) { + var floatBar = document.querySelector('#nd-batch-bar'); + if (floatBar) floatBar.style.setProperty('display', 'none', 'important'); + + mTitle.innerText = options.title || ''; + if (options.content) { mContent.innerHTML = options.content; mContent.style.display = 'block'; } else { mContent.style.display = 'none'; } + + var mFwPanel = modalOverlay.querySelector('#nd-m-fw-panel'); + var fwCurrentMac = options.targetMac || ""; + var fwCurrentIp = options.targetIp || ""; + + mForm.style.display = 'none'; + mDeptMgr.style.display = 'none'; + if (mFwPanel) mFwPanel.style.display = 'none'; + + if (options.isDeptMgr) { + mDeptMgr.style.display = 'block'; + renderDeptManager(); + } else if (options.isFwPanel) { + if (mFwPanel) mFwPanel.style.display = 'block'; + var d = options.targetDev; + var chkBlk = modalOverlay.querySelector('#fw-blk-en'); + var chkIso = modalOverlay.querySelector('#fw-iso-en'); + var chkDmz = modalOverlay.querySelector('#fw-dmz-en'); + var chkExpose = modalOverlay.querySelector('#fw-expose-en'); + var selExposeProto = modalOverlay.querySelector('#fw-expose-proto'); + + if (chkBlk) chkBlk.checked = (d.fw_block === 'true' || d.fw_block === true); + if (chkIso) chkIso.checked = (d.fw_isolate === 'true' || d.fw_isolate === true); + if (chkDmz) { + chkDmz.checked = (d.fw_dmz === 'true' || d.fw_dmz === true); + chkDmz.dataset.orig = chkDmz.checked; // 记住初始状态 + } + if (chkExpose) chkExpose.checked = (d.fw_expose === 'true' || d.fw_expose === true); + if (selExposeProto) selExposeProto.value = d.fw_expose_proto || 'ipv6'; + } else if (options.showForm) { + mForm.style.display = 'block'; + populateTagSelects(); + + if (options.isBatchBind) { + mNormalFields.style.display = 'none'; + mBatchFields.style.display = 'block'; + applyStrategyUI(savedStrategy); + // 默认选中“保持不变” + batchTagSelect.value = 'keep'; + } else { + mBatchFields.style.display = 'none'; + mNormalFields.style.display = 'block'; + mInpName.value = options.defName || ''; + + if (options.showSingleStrategy) { + mSingleStrategyGroup.style.display = 'block'; + // 单机绑定/编辑打开时,如果它原来有分组,回显;否则设为未分组 + if (currentSingleDev && currentSingleDev.dept && currentSingleDev.dept !== 'none' && currentSingleDev.dept !== '') { + mSingleTagSelect.value = currentSingleDev.dept; + } else { + mSingleTagSelect.value = 'none'; + } + } else { + mSingleStrategyGroup.style.display = 'none'; + mInpIp.value = options.defIp || ''; + } + } + } + + mBtnOk.className = 'nd-btn ' + (options.danger ? 'nd-btn-red' : 'nd-btn-blue'); + mBtnOk.innerText = options.okText || T['BTN_OK']; + mBtnCancel.style.display = options.hideCancel ? 'none' : ''; + + mBtnOk.onclick = function() { + if (options.isDeptMgr) { + var finalDepts = saveDepartmentsFromDOM(); + if(finalDepts !== false) { + if (options.onOk) options.onOk(finalDepts); + modalOverlay.style.display = 'none'; + } + } else if (options.isFwPanel) { + var blkEn = modalOverlay.querySelector('#fw-blk-en') ? modalOverlay.querySelector('#fw-blk-en').checked : false; + var isoEn = modalOverlay.querySelector('#fw-iso-en') ? modalOverlay.querySelector('#fw-iso-en').checked : false; + var dmzEl = modalOverlay.querySelector('#fw-dmz-en'); + var dmzEn = dmzEl ? dmzEl.checked : false; + var exposeEn = modalOverlay.querySelector('#fw-expose-en') ? modalOverlay.querySelector('#fw-expose-en').checked : false; + var exposeProto = modalOverlay.querySelector('#fw-expose-proto') ? modalOverlay.querySelector('#fw-expose-proto').value : 'ipv6'; + + // DMZ校验 + if (dmzEn && dmzEl.dataset.orig !== 'true') { + if (!fwCurrentIp || fwCurrentIp === 'Unknown IP') { + showCustomAlert(T['ERR_DMZ_NO_IP']); return; + } + var otherDmz = globalDevices.find(function(d){ return (d.fw_dmz === 'true' || d.fw_dmz === true) && d.mac !== fwCurrentMac; }); + if (otherDmz) { + showCustomAlert(T['ERR_DMZ_OCCUPIED_1'] + (otherDmz.name||otherDmz.mac) + T['ERR_DMZ_OCCUPIED_2']); + return; + } + } + + if (options.onOk) options.onOk({ mac: fwCurrentMac, ip: fwCurrentIp, blk_en: blkEn, iso_en: isoEn, dmz_en: dmzEn, expose_en: exposeEn, expose_proto: exposeProto }); + modalOverlay.style.display = 'none'; + if (typeof floatBar !== 'undefined' && floatBar) floatBar.style.removeProperty('display'); + } else if (options.showForm) { + if (options.isBatchBind) { + var activeStrategy = modalOverlay.querySelector('.nd-strategy-card.active').getAttribute('data-val'); + var batchDeptId = batchTagSelect.value; + + // 策略选了“按部门网段分配”,那目标部门就绝对不能是“保持不变”或“未分组”,必须指定一个具体部门 + if (activeStrategy === 'dept' && (batchDeptId === 'none' || batchDeptId === 'keep')) { + showCustomAlert(T['ERR_DEPT_NOT_SEL']); + return; + } + + if (activeStrategy === 'smart') { + autoCascadeRanges(); + var nr = { + ms: parseInt(modalOverlay.querySelector('#sm-mob-s').value), me: parseInt(modalOverlay.querySelector('#sm-mob-e').value), + ps: parseInt(modalOverlay.querySelector('#sm-pc-s').value), pe: parseInt(modalOverlay.querySelector('#sm-pc-e').value), + is: parseInt(modalOverlay.querySelector('#sm-iot-s').value), ie: parseInt(modalOverlay.querySelector('#sm-iot-e').value), + os: parseInt(modalOverlay.querySelector('#sm-oth-s').value), oe: parseInt(modalOverlay.querySelector('#sm-oth-e').value) + }; + + if (JSON.stringify(nr) !== JSON.stringify(savedRanges)) { + savedRanges = nr; + localStorage.setItem('nw_smart_ranges', JSON.stringify(nr)); + needFetchSmart = false; + + var dataToSend = (JSON.stringify(nr) === JSON.stringify(defaultSmartRanges)) ? "{}" : JSON.stringify(nr); + callSaveSmartRanges(dataToSend).catch(function(e){ console.error('Save smart ranges fail', e); }); + } + } + var resBatch = { strategy: activeStrategy, startSuffix: batchSuffixInput.value.trim(), ranges: savedRanges, dept: batchDeptId }; + if (options.onOk) options.onOk(resBatch); + modalOverlay.style.display = 'none'; + if (floatBar) floatBar.style.removeProperty('display'); + } else { + var submitIp = mInpIp.value.trim(); + var singleDeptId = mSingleTagSelect.value; + var activeSingleRadio = modalOverlay.querySelector('input[name="single_strategy"]:checked'); + + if (activeSingleRadio && activeSingleRadio.value === 'dept' && singleDeptId === 'none') { + showCustomAlert(T['ERR_DEPT_NOT_SEL']); + return; + } + + if (!submitIp) { showCustomAlert(T['ERR_IP_EMPTY']); return; } + + var ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + if (!ipRegex.test(submitIp)) { + showCustomAlert(T['ERR_IP_FORMAT']); + return; + } + + var conflictDev = globalDevices.find(function(d) { + return (d.bound_ip === submitIp || d.ip === submitIp) && d.mac !== options.targetMac; + }); + + if (conflictDev) { + var cName = conflictDev.name === 'Unknown' ? conflictDev.mac.toUpperCase() : conflictDev.name; + showCustomAlert(T['ERR_IP_CONFLICT'].replace('{ip}', submitIp).replace('{name}', cName)); + return; + } + + var newName = mInpName.value.trim(); + var isCurrentlyStatic = currentSingleDev.is_static === true || currentSingleDev.is_static === 'true'; + var oldDeptId = currentSingleDev.dept || 'none'; + + if (isCurrentlyStatic && submitIp === currentOriginalIp && newName === options.defName && singleDeptId === oldDeptId) { + showCustomAlert(T['TIP_NO_CHANGE']); + return; + } + + var resSingle = { name: newName, ip: submitIp, dept: singleDeptId }; + if(resSingle.ip) localStorage.setItem('nw_last_ip', resSingle.ip); + + if (options.onOk) options.onOk(resSingle); + modalOverlay.style.display = 'none'; + if (floatBar) floatBar.style.removeProperty('display'); + } + } else { + if (options.onOk) options.onOk(true); + modalOverlay.style.display = 'none'; + if (floatBar) floatBar.style.removeProperty('display'); + } + }; + mBtnCancel.onclick = function() { + modalOverlay.style.display = 'none'; + if (floatBar) floatBar.style.removeProperty('display'); + }; + modalOverlay.style.display = 'flex'; + }; + + var loadingEl = container.querySelector('#nd-loading'); + var loadingText = container.querySelector('#nd-loading-text'); + var listHeader = container.querySelector('#nd-list-header'); + var listEl = container.querySelector('#nd-list-container'); + var catTabs = container.querySelector('#nd-category-tabs'); + var refreshBtn = container.querySelector('#dev-refresh'); + + var selectAllCb = container.querySelector('#cb-select-all'); + var batchBar = container.querySelector('#nd-batch-bar'); + + if (batchBar) { document.body.appendChild(batchBar); } + + var batchCount = batchBar.querySelector('#nd-batch-count'); + var btnBatchBind = batchBar.querySelector('#btn-batch-bind'); + var btnBatchUnbind = batchBar.querySelector('#btn-batch-unbind'); + var btnBatchClose = batchBar.querySelector('#nd-batch-close'); + if (btnBatchClose) { + btnBatchClose.addEventListener('click', function() { + batchBar.classList.remove('show'); + }); + } + + var globalDevices = []; + var filteredDevices = []; + var selectedDevices = []; + var currentFilter = 'all'; + + // 全局并发控制 + window.nwKeepAliveQueue = window.nwKeepAliveQueue || []; + window.nwActiveCount = window.nwActiveCount || 0; // 当前正在执行的任务数 + var MAX_CONCURRENT = 10; // 并发数:10 + + function processKeepAliveQueue() { + // 如果队列空了,或者并发数已经达到上限,就退出等待 + if (window.nwKeepAliveQueue.length === 0 || window.nwActiveCount >= MAX_CONCURRENT) return; + + // 没到并发上限,且队列里还有任务,执行 + while (window.nwKeepAliveQueue.length > 0 && window.nwActiveCount < MAX_CONCURRENT) { + window.nwActiveCount++; + // 取出对象的方式 + var item = window.nwKeepAliveQueue.shift(); + + // 传入的参数,将对象解析为 mac 和 name + (function(curr) { + callV6KeepAlive(curr.mac, curr.name).then(function() { + // 成功,名字指纹已经在触发时记录过了,什么都不用做 + }).catch(function() { + // 如果请求因为网络等原因失败了,删掉它的防刷缓存,允许它下一次重试 + sessionStorage.removeItem('nw_v6_hb_' + curr.mac); + }).finally(function() { + setTimeout(function() { + window.nwActiveCount--; + processKeepAliveQueue(); + }, 200); + }); + })(item); + } + } + + function isSelectable(dev) { + var isSys = dev.is_gw === 'true' || dev.is_gw === true || dev.is_local === 'true' || dev.is_local === true; + var isVisitor = dev.is_visitor === 'true' || dev.is_visitor === true; + + var isCrossSubnet = (dev.ip && dev.ip !== 'Unknown IP' && dev.ip.substring(0, dev.ip.lastIndexOf('.') + 1) !== basePrefix); + var isStatic = (dev.is_static === true || dev.is_static === 'true'); + + if (isSys || isVisitor) return false; + + if (isCrossSubnet && !isStatic) return false; + + return true; + } + + function updateBatchBar() { + batchCount.innerText = selectedDevices.length; + if (selectedDevices.length >= 2) { + batchBar.classList.add('show'); + } else { + batchBar.classList.remove('show'); + } + var selectableInView = filteredDevices.filter(isSelectable); + if (selectableInView.length > 0 && selectedDevices.length === selectableInView.length) { + selectAllCb.checked = true; + } else { + selectAllCb.checked = false; + } + } + + function toggleSelection(dev, isChecked) { + var idx = selectedDevices.findIndex(function(d) { return d.mac === dev.mac; }); + if (isChecked) { + if (idx === -1 && isSelectable(dev)) selectedDevices.push(dev); + } else { + if (idx !== -1) selectedDevices.splice(idx, 1); + } + updateBatchBar(); + } + + selectAllCb.addEventListener('change', function() { + var checked = this.checked; + selectedDevices = []; + if(checked) { + filteredDevices.forEach(function(dev) { + if (isSelectable(dev)) selectedDevices.push(dev); + }); + } + renderListHTML(); + updateBatchBar(); + }); + + function updateTabsAndFilter() { + var cAll = globalDevices.length; + + var cMob = 0, cPc = 0, cIot = 0, cTypeOth = 0; + + var deptCounts = {}; + var cDeptOth = 0; + globalDepartments.forEach(function(d) { deptCounts[d.id] = 0; }); + + globalDevices.forEach(function(d) { + var dept = getDeviceDept(d); + if (dept) { + deptCounts[dept.id]++; + } else { + cDeptOth++; + } + + var smartCat = 'type_other'; + if (smartFilterByIp) { + var ip = d.bound_ip || d.ip; + if (ip && ip !== 'Unknown IP' && ip.substring(0, basePrefix.length) === basePrefix) { + var suffix = parseInt(ip.split('.').pop()); + if (!isNaN(suffix)) { + if (suffix >= savedRanges.ms && suffix <= savedRanges.me) smartCat = 'mobile'; + else if (suffix >= savedRanges.ps && suffix <= savedRanges.pe) smartCat = 'pc'; + else if (suffix >= savedRanges.is && suffix <= savedRanges.ie) smartCat = 'iot'; + } + } + } else { + var rawType = getDeviceType(d); + if (['mobile', 'pc', 'iot'].includes(rawType)) smartCat = rawType; + } + + if (smartCat === 'mobile') cMob++; + else if (smartCat === 'pc') cPc++; + else if (smartCat === 'iot') cIot++; + else cTypeOth++; + }); + + if (currentFilter === 'mobile' && cMob === 0) currentFilter = 'all'; + else if (currentFilter === 'pc' && cPc === 0) currentFilter = 'all'; + else if (currentFilter === 'iot' && cIot === 0) currentFilter = 'all'; + else if (currentFilter === 'type_other' && cTypeOth === 0) currentFilter = 'all'; + else if (currentFilter === 'dept_other' && cDeptOth === 0) currentFilter = 'all'; + else if (currentFilter.startsWith('d_') && deptCounts[currentFilter] === 0) currentFilter = 'all'; + + var btnAll = ''; + var btnMob = ''; + var btnPc = ''; + var btnIot = ''; + var btnTypeOth = ''; + + var deptBtns = ''; + globalDepartments.forEach(function(d) { + var count = deptCounts[d.id]; + var activeStyle = currentFilter === d.id ? 'background:'+d.color+'15;' : ''; + var displayStyle = count === 0 ? 'display:none;' : ''; + deptBtns += ''; + }); + var btnDeptOth = ''; + var btnMgr = ''; + + var tabsHtml = ''; + + var row1Title = smartFilterByIp ? T['LBL_ROW_TYPE_SMART'] : T['LBL_ROW_TYPE_NAME']; + tabsHtml += '
    '; + tabsHtml += '
    ' + row1Title + '
    '; + tabsHtml += btnAll + btnMob + btnPc + btnIot + btnTypeOth; + tabsHtml += '
    '; + + tabsHtml += '
    '; + + tabsHtml += deptBtns + btnDeptOth + btnMgr; + tabsHtml += '
    '; + + catTabs.innerHTML = tabsHtml; + + catTabs.querySelectorAll('.nd-cat-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + if (this.id === 'btn-manage-depts') { + openModal({ + title: T['TIT_MGR_DEPTS'], + isDeptMgr: true, + okText: T['BTN_SAVE'], + onOk: function(newDepts) { + loadingEl.style.display = 'flex'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + callSaveDepts(JSON.stringify(newDepts)).then(function() { setTimeout(loadDevices, 800); }) + .catch(function(e) { + showCustomAlert(T['ERR_FW_SAVE_FAIL'].replace('{err}', e)); + setTimeout(loadDevices, 2500); + }); + } + }); + return; + } + catTabs.querySelectorAll('.nd-cat-btn').forEach(function(b){ b.classList.remove('active'); b.style.background = ''; }); + this.classList.add('active'); + if (this.hasAttribute('style') && this.style.color) { + this.style.background = this.style.color.replace('rgb', 'rgba').replace(')', ', 0.15)'); + } + currentFilter = this.getAttribute('data-cat'); + selectedDevices = []; + filterAndRender(); + }); + }); + + filterAndRender(); + } + + function filterAndRender() { + if (currentFilter === 'all') { + filteredDevices = globalDevices; + } else if (['mobile', 'pc', 'iot', 'type_other'].includes(currentFilter)) { + filteredDevices = globalDevices.filter(function(d) { + var smartCat = 'type_other'; + if (smartFilterByIp) { + var ip = d.bound_ip || d.ip; + if (ip && ip !== 'Unknown IP' && ip.substring(0, basePrefix.length) === basePrefix) { + var suffix = parseInt(ip.split('.').pop()); + if (!isNaN(suffix)) { + if (suffix >= savedRanges.ms && suffix <= savedRanges.me) smartCat = 'mobile'; + else if (suffix >= savedRanges.ps && suffix <= savedRanges.pe) smartCat = 'pc'; + else if (suffix >= savedRanges.is && suffix <= savedRanges.ie) smartCat = 'iot'; + } + } + } else { + var rawType = getDeviceType(d); + if (['mobile', 'pc', 'iot'].includes(rawType)) smartCat = rawType; + } + return smartCat === currentFilter; + }); + } else if (currentFilter === 'dept_other') { + filteredDevices = globalDevices.filter(function(d) { + return getDeviceDept(d) === null; + }); + } else { + filteredDevices = globalDevices.filter(function(d) { + var dept = getDeviceDept(d); + return dept && dept.id === currentFilter; + }); + } + selectAllCb.checked = false; + renderListHTML(); + updateBatchBar(); + } + + function renderListHTML() { + listEl.style.display = 'flex'; + + if (filteredDevices.length === 0) { + listEl.innerHTML = '
    ' + T['MSG_EMPTY_CAT'] + '
    '; + return; + } + + var html = ""; + filteredDevices.forEach(function(dev) { + var isOnline = (dev.online === true || dev.online === 'true'); + var isStatic = (dev.is_static === true || dev.is_static === 'true'); + var isGw = (dev.is_gw === true || dev.is_gw === 'true'); + var isLocal = (dev.is_local === true || dev.is_local === 'true'); + var isVisitor = (dev.is_visitor === true || dev.is_visitor === 'true'); + var isPending = (isStatic && dev.bound_ip && dev.ip && dev.ip !== 'Unknown IP' && dev.ip !== dev.bound_ip); + + var isBlk = (dev.fw_block === 'true' || dev.fw_block === true); + var isIso = (dev.fw_isolate === 'true' || dev.fw_isolate === true); + var isDmz = (dev.fw_dmz === 'true' || dev.fw_dmz === true); + + // 解析后端的 in_config 状态 + var isInConfig = (dev.in_config === 'true' || dev.in_config === true); + + var statusBadgesHtml = ''; + if (isOnline) { + // 1. 在线 + statusBadgesHtml = '' + T['BDG_ONLINE'] + ''; + } else if (!isInConfig) { + // 2. 长期离线,离线并后端配置文件里无此MAC + statusBadgesHtml = '' + (T['BDG_LONG_OFFLINE'] || '长期离线') + ''; + } else { + // 3. 普通离线,配置文件里有,暂时不在线) + statusBadgesHtml = '' + T['BDG_OFFLINE'] + ''; + } + + if (isBlk) statusBadgesHtml += '⛔ ' + T['BDG_FW_BLK'] + ''; + if (isIso) statusBadgesHtml += '🛡️ ' + T['BDG_FW_ISO'] + ''; + if (isDmz) statusBadgesHtml += '🚀 ' + T['BDG_FW_DMZ'] + ''; + + if (isPending) { + statusBadgesHtml += '⏳ ' + (T['BDG_PENDING'] || 'Pending') + ''; + } else if (isStatic) { + statusBadgesHtml += '🔒 ' + T['BDG_STATIC'] + ''; + } + + var dept = getDeviceDept(dev); + if (dept) { + statusBadgesHtml += '' + dept.icon + ' ' + dept.name + ''; + } + + if (isGw) statusBadgesHtml += '🌐 ' + T['BDG_GW'] + ''; + if (isLocal) statusBadgesHtml += '💻 ' + T['BDG_LOCAL'] + ''; + if (isVisitor) statusBadgesHtml += '👤 ' + (T['BDG_VISITOR'] || 'Visiting') + ''; + + var leaseText = dev.lease || '-'; + if (leaseText === 'Static' || leaseText === 'Infinite' || (isStatic && leaseText === '-')) { + leaseText = T['TXT_INFINITE']; + } else if (leaseText === 'Expired') { + leaseText = T['TXT_EXPIRED']; + } else if (leaseText === 'Device Addressed IP') { + leaseText = T['BDG_ADDR_IP']; + } + + if (isGw || isLocal) leaseText = T['TXT_SYS_ROUTE']; + + var showConnsCbEl = container.querySelector('#cb-show-conns'); + var showConns = showConnsCbEl ? showConnsCbEl.checked : false; + var connHtml = ''; + if (showConns && dev.conn_count !== undefined) { + var connColor = dev.conn_count > 500 ? '#ef4444' : '#3b82f6'; + var safeName = (dev.name || '').replace(/(<([^>]+)>)/gi, "").replace(/"/g, '"'); + + var t = dev.conn_count; + var pWeb = t ? (dev.conn_web / t * 100) : 0; + var pDns = t ? (dev.conn_dns / t * 100) : 0; + var pUdp = t ? (dev.conn_udp / t * 100) : 0; + var pP2p = t ? (dev.conn_p2p / t * 100) : 0; + + // 圆饼图 + var deg1 = pWeb, deg2 = deg1 + pDns, deg3 = deg2 + pUdp, deg4 = deg3 + pP2p; + var gradient = t ? 'conic-gradient(#3b82f6 0% '+deg1+'%, #10b981 '+deg1+'% '+deg2+'%, #8b5cf6 '+deg2+'% '+deg3+'%, #ef4444 '+deg3+'% '+deg4+'%, #64748b '+deg4+'% 100%)' : 'conic-gradient(#64748b 0% 100%)'; + + var tooltipHtml = '
    ' + + '
    '; + + if (dev.conn_web > 0) { + tooltipHtml += '
    ' + T['LBL_CONN_WEB'] + ' ' + dev.conn_web + '
    '; + } + if (dev.conn_dns > 0) { + tooltipHtml += '
    ' + T['LBL_CONN_DNS'] + ' ' + dev.conn_dns + '
    '; + } + if (dev.conn_udp > 0) { + tooltipHtml += '
    ' + T['LBL_CONN_UDP'] + ' ' + dev.conn_udp + '
    '; + } + if (dev.conn_p2p > 0) { + // P2P > 50 時显示数字 + var p2pValStyle = dev.conn_p2p > 50 ? 'color:#ef4444;' : '#f8fafc'; + tooltipHtml += '
    ' + T['LBL_CONN_P2P'] + ' ' + dev.conn_p2p + '
    '; + } + if (dev.conn_oth > 0) { + tooltipHtml += '
    ' + T['LBL_CONN_OTH'] + ' ' + dev.conn_oth + '
    '; + } + + if (t === 0) { + tooltipHtml += '
    ' + T['MSG_CONN_EMPTY'] + '
    '; + } + + tooltipHtml += '
    '; + + connHtml = '
    ⚡' + dev.conn_count + ' ' + T['LBL_CONN_COUNT'] + tooltipHtml + '
    '; + } + + // 只保留 2 或 3 开头的公网 IP,屏蔽 fd/fe 等内网 IP + var ipv6Html = ''; + if (dev.ipv6 && dev.ipv6.trim() !== '') { + var v6List = dev.ipv6.trim().split(' '); + var publicV6List = v6List.filter(function(v) { + var firstChar = v.charAt(0).toLowerCase(); + return firstChar === '2' || firstChar === '3'; + }); + + // 显示徽章 + if (publicV6List.length > 0) { + // 这三行极其重要,不能删 + var memKey = 'nw_v6_mem_' + dev.mac; + var hbKey = 'nw_v6_hb_' + dev.mac; + var currentPrefix = publicV6List[0].split(':').slice(0, 4).join(':'); + + // 去重(防止重複的火星文和 IP) + var uniqueV6 = []; + publicV6List.forEach(function(v) { if(uniqueV6.indexOf(v) === -1) uniqueV6.push(v); }); + publicV6List = uniqueV6; + + var radarShortV6 = publicV6List.find(function(v) { return v.indexOf('::') !== -1 && v.length < 25; }); + var localShortV6 = localStorage.getItem(memKey); + + // 清理緩存 + if (localShortV6 && (localShortV6.indexOf('::') === -1 || localShortV6.length >= 25)) { + localShortV6 = null; + localStorage.removeItem(memKey); + } + + var triggerKeepAlive = function() { + var lastSentName = sessionStorage.getItem(hbKey); + var inQueue = window.nwKeepAliveQueue.find(function(q){ return q.mac === dev.mac; }); + + // 没有发过,或者用户名跟上次不一样,更改 + if (lastSentName !== dev.name && !inQueue) { + sessionStorage.setItem(hbKey, dev.name); // 将现在的用户名作为指纹存入防刷缓存 + window.nwKeepAliveQueue.push({mac: dev.mac, name: dev.name}); + processKeepAliveQueue(); + } + }; + + // 保活 + if (radarShortV6) { + localStorage.setItem(memKey, radarShortV6); + triggerKeepAlive(); + } else if (localShortV6 && localShortV6.indexOf(currentPrefix) === 0) { + if (publicV6List.indexOf(localShortV6) === -1) publicV6List.unshift(localShortV6); + triggerKeepAlive(); + } + + // 5. 短 IP 置顶 + var showV6 = publicV6List.find(function(v) { return v.indexOf('::') !== -1 && v.length < 25; }) || publicV6List[0]; + var mainIdx = publicV6List.indexOf(showV6); + if (mainIdx > 0) { + publicV6List.splice(mainIdx, 1); + publicV6List.unshift(showV6); // 置顶 + } + var moreV6 = publicV6List.length >= 2 ? (' +' + (publicV6List.length - 1)) : ''; + + // 6. 预留状态 + var isV6Unseen = (dev.ipv6 || '').indexOf(showV6) === -1; + + var pureV6Str = publicV6List.join('\n'); + + var v6ReservedText = T['BDG_V6_RESERVED'] || '预留'; + var displayV6Str = publicV6List.map(function(v) { + return (isV6Unseen && v === showV6) ? (v + ' (' + v6ReservedText + ')') : v; + }).join('\n'); + + var titleStr = (T['TIP_V6_COPY'] || 'Public IPv6 (Click to copy):') + '\n' + displayV6Str; + + // 渲染 + ipv6Html = '
    IPv6 ' + showV6 + '' + moreV6 + '
    '; + } + } + + var actions = ""; + if (isGw || isLocal) { + actions = '' + T['TXT_SYS_RESERVED'] + ''; + } else if (isStatic) { + actions = '' + + ''; + } else if (!isSelectable(dev) && !isVisitor) { + + actions = '' + T['TXT_UNOPERABLE'] + ''; + } else { + actions = ''; + } + + var displayName = dev.name === 'Unknown' ? '' + T['TXT_UNKNOWN_DEV'] + '' : dev.name; + var isChecked = selectedDevices.findIndex(function(d){ return d.mac === dev.mac; }) !== -1; + + var isSys = isGw || isLocal; + var noCheckbox = !isSelectable(dev); + var crossSubnetWarn = ""; + + var isValidIp = (dev.ip && dev.ip !== 'Unknown IP' && dev.ip.substring(0, dev.ip.lastIndexOf('.') + 1) === basePrefix); + if (!isSys && !isValidIp) { + crossSubnetWarn = '' + T['BDG_ADDR_IP'] + ''; + } + + var ipText = dev.ip === 'Unknown IP' ? T['TXT_UNKNOWN_IP'] : dev.ip; + + // --- IPv4 为未知时,提取设备自签的 IPv6 (fe80:: / fd::) --- + if (dev.ip === 'Unknown IP' && dev.ipv6 && dev.ipv6.trim() !== '') { + var allV6 = dev.ipv6.trim().split(' '); + // 优先寻找 fe80 或 fd 开头的自签/本地 IPv6 + var localV6 = allV6.find(function(v) { return v.indexOf('fe80:') === 0 || v.indexOf('fd') === 0; }) || allV6[0]; + if (localV6) { + ipText = '' + localV6 + ' ' + (T['BDG_LOCAL_V6'] || 'Local IPv6') + ''; + } + } + // ------------------------------------------------------------- + + if (isPending) { + ipText = '' + dev.ip + '➜ ' + dev.bound_ip + ''; + } + + // 时间是否过期 + var isNewUnknown = (dev.is_new_unknown === 'true' || dev.is_new_unknown === true); + var cardClass = isNewUnknown ? "nd-card nd-card-warning" : "nd-card"; + + var warningBadge = ''; + var handStyle = 'font-size:1.5em; color:#94a3b8; margin-left:4px; vertical-align:middle; animation:blink-warning 0.6s infinite;'; + + if (isNewUnknown) { + warningBadge = '
    ⚠️ ' + (T['BDG_NEW_UNKNOWN'] || '疑似伪装新设备') + '
    '; + + // 小手 + handStyle = 'font-size:1.5em; color:#ff4d4f; margin-left:4px; vertical-align:middle; animation:blink-warning 0.6s infinite;'; + } + + var handHtml = isSys ? '' : '👈'; + var cursorStyle = isSys ? 'cursor:default;' : 'cursor:pointer;'; + var macTitle = isSys ? '' : ('title="' + T['TIP_MAC_CTRL'] + '"'); + var macClass = isSys ? 'nd-card-mac' : 'nd-card-mac btn-fw-mac'; + + html += '
    ' + warningBadge + '
    '; + + if (noCheckbox) { + html += '
    '; + } else { + html += ''; + } + + html += [ + '
    ', + ' ' + displayName + crossSubnetWarn + '', + ' ' + statusBadgesHtml, + '
    ', + '
    ', + '
    ' + (dev.mac).toUpperCase() + ' ' + handHtml + '
    ', + '
    ', + '
    ', + '
    ', + '
    ' + ipText + '
    ', + ' ' + ipv6Html, + '
    ', + '
    ', + '
    ' + leaseText + '
    ', + ' ' + connHtml, + '
    ', + '
    ', + '
    ' + actions + '
    ', + '
    ' + ].join('\n'); + }); + + listEl.innerHTML = html; + + // 绑定分析面板点击事件 + container.querySelectorAll('.nd-conn-badge').forEach(function(badge) { + badge.addEventListener('click', function(e) { + e.stopPropagation(); + var ip = this.getAttribute('data-ip'); + var name = this.getAttribute('data-name'); + if (!ip || ip === 'Unknown IP') return; + + // 1. 弹出 Loading 画面 + openModal({ + title: T['TIT_CONN_RADAR'] + ' - NetWiz', + content: '
    ' + T['MSG_DIVE_KERNEL'] + '
    ', + okText: T['BTN_OK'], // 通常使用统一的“确定”或“关闭” + hideCancel: true + }); + + // 2. 呼叫后端执行抓包 + callAnalyzeConns(ip).then(function(res) { + var s = res.stats || {}; + var pctWeb = s.total ? (s.web / s.total * 100).toFixed(1) : 0; + var pctDns = s.total ? (s.dns / s.total * 100).toFixed(1) : 0; + var pctUdp = s.total ? (s.udp_media / s.total * 100).toFixed(1) : 0; + var pctP2p = s.total ? (s.p2p / s.total * 100).toFixed(1) : 0; + var pctOth = s.total ? (s.other / s.total * 100).toFixed(1) : 0; + + var p2pStyle = s.p2p > 50 ? 'color:#ef4444; font-weight:bold; background:#fef2f2; padding:2px 4px; border-radius:4px;' : ''; + + // 3. 拼装统计面板 + var html = '
    ' + + '
    ' + + '🌐 ' + T['LBL_CONN_WEB'] + ': '+s.web+' ('+pctWeb+'%)' + + '🔍 ' + T['LBL_CONN_DNS'] + ': '+s.dns+' ('+pctDns+'%)' + + '
    ' + + '
    ' + + '🎮 ' + T['LBL_CONN_UDP'] + ': '+s.udp_media+' ('+pctUdp+'%)' + + '🚀 ' + T['LBL_CONN_P2P'] + ': '+s.p2p+' ('+pctP2p+'%)' + + '
    ' + + '
    ' + + '🏷️ ' + T['LBL_CONN_OTH'] + ': '+s.other+' ('+pctOth+'%)' + + '
    ' + + '
    '; + + // 4. 终端机黑框 + html += '
    '; + + // 替换总数佔位符 {total} + var msgTotal = T['MSG_CONN_TOTAL'].replace('{total}', s.total || 0); + html += '' + msgTotal + '\n'; + + // 替换命令行中的 IP 佔位符 {ip} + var msgCmd = T['MSG_CONN_CMD'].replace('{ip}', ip); + html += '' + msgCmd + '\n\n'; + + if (res.raw && res.raw.length > 0) { + html += res.raw.join('\n'); + if (s.total > 50) { + html += '\n\n' + T['MSG_CONN_TRUNC'] + ''; + } + } else { + html += '' + T['MSG_CONN_EMPTY'] + ''; + } + html += '
    '; + + // 替换弹窗 + var contentEl = document.querySelector('#nd-m-content'); + if (contentEl) contentEl.innerHTML = html; + }).catch(function(e) { + var contentEl = document.querySelector('#nd-m-content'); + // 替换错误信息佔位符 {err} + var msgFail = T['MSG_CONN_ANALYZE_FAIL'].replace('{err}', e); + if (contentEl) contentEl.innerHTML = '
    ' + msgFail + '
    '; + }); + }); +}); + + + container.querySelectorAll('.nd-ipv6-badge').forEach(function(badge) { + badge.addEventListener('click', function(e) { + e.stopPropagation(); // 阻止点击事件穿透 + + var rawV6Text = this.getAttribute('data-v6') || ''; + + // 1. 保留完整的 IPv6 列表(用于弹窗展示) + var fullV6Text = decodeURIComponent(rawV6Text); + + // 2. 单独提取第一条 IPv6(用于复制到剪贴板) + var firstV6Ip = fullV6Text.split('\n')[0].split(',')[0].split('
    ')[0].trim(); + + // 3. 自定义弹窗的提示文字 + var successMsg = '✅ ' + (T['MSG_V6_COPIED'] || 'First IPv6 address copied successfully:') + '\n\n' + fullV6Text + '\n\n' ; + + if (navigator.clipboard && window.isSecureContext) { + // 只复制第一条 (firstV6Ip) + navigator.clipboard.writeText(firstV6Ip).then(function() { + // 弹窗显示完整列表 (successMsg) + showCustomAlert(successMsg); + }); + } else { + var textArea = document.createElement("textarea"); + // 只把第一条写入隐藏的输入框 + textArea.value = firstV6Ip; + textArea.style.position = "fixed"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + // 弹窗显示完整列表 (successMsg) + showCustomAlert(successMsg); + } catch (err) {} + document.body.removeChild(textArea); + } + }); + }); + + container.querySelectorAll('.nd-card-checkbox input').forEach(function(cb) { + cb.addEventListener('change', function() { + var mac = this.getAttribute('data-mac'); + var dev = globalDevices.find(function(d){ return d.mac === mac; }); + toggleSelection(dev, this.checked); + }); + }); + + container.querySelectorAll('.btn-fw-mac').forEach(function(btn) { + btn.addEventListener('click', function() { + var mac = this.getAttribute('data-mac'); + var ip = this.getAttribute('data-ip'); + var dev = globalDevices.find(function(d){ return d.mac === mac; }); + if (dev.is_gw === 'true' || dev.is_gw === true || dev.is_local === 'true' || dev.is_local === true) return; // 系统设备不可点 + + var dName = dev.name === 'Unknown' ? mac.toUpperCase() : dev.name; + + // 1. 打开弹窗面板 + openModal({ + title: T['TIT_FW_CONTROL'] + ' - ' + dName, + isFwPanel: true, targetMac: mac, targetIp: ip, targetDev: dev, okText: T['BTN_SAVE'], + onOk: function(data) { + loadingEl.style.display = 'flex'; + listEl.style.display = 'none'; catTabs.style.display = 'none'; + callFwSet(data.mac, data.ip, data.blk_en, data.iso_en, data.dmz_en, data.expose_en, data.expose_proto) + .then(function() { setTimeout(loadDevices, 2500); }) + .catch(function(e) { + showCustomAlert(T['ERR_SAVE_FAIL_SHORT'].replace('{err}', e)); + setTimeout(loadDevices, 2500); + }); + } + }); + + // 2. 控制 WOL 区块显示按鈕绑定事件 + var wolZone = modalOverlay.querySelector('#fw-panel-wol-zone'); + var wolBtn = modalOverlay.querySelector('#btn-fw-panel-wol'); + var isOnline = (dev.online === true || dev.online === 'true'); + + if (wolZone && wolBtn) { + // 过滤字典闪电符号 + var pureWolText = (T['BTN_WOL'] || '局域网唤醒').replace(/⚡/g, '').trim(); + + if (!isOnline) { + // 设备离线,显示按鈕绑定事件 + wolZone.style.display = 'flex'; + wolBtn.disabled = false; + wolBtn.innerText = '⚡ ' + pureWolText + ' ⚡'; + wolBtn.style.opacity = '1'; + + wolBtn.onclick = function(e) { + e.preventDefault(); + wolBtn.innerText = '⚡ ' + (T['TXT_WAKING_UP'] || 'Waking up...') + ' ⚡'; + wolBtn.disabled = true; + wolBtn.style.opacity = '0.6'; + + callWakeDevice(mac).then(function() { + showCustomAlert(T['MSG_WOL_SENT_1'].replace('{mac}', mac.toUpperCase()) + '\n' + T['MSG_WOL_SENT_2']); + wolBtn.innerText = '⚡ ' + pureWolText + ' ⚡'; + wolBtn.disabled = false; + wolBtn.style.opacity = '1'; + }).catch(function() { + wolBtn.innerText = '⚡ ' + pureWolText + ' ⚡'; + wolBtn.disabled = false; + wolBtn.style.opacity = '1'; + }); + }; + } else { + // 设备在线,隐藏 WOL + wolZone.style.display = 'none'; + wolBtn.onclick = null; + } + } + }); + }); + + container.querySelectorAll('.btn-bind, .btn-edit').forEach(function(btn) { + btn.addEventListener('click', function() { + var mac = this.getAttribute('data-mac'); + var ip = this.getAttribute('data-ip'); + var rawName = this.getAttribute('data-name'); + var isEdit = this.classList.contains('btn-edit'); + + currentSingleDev = globalDevices.find(function(d){ return d.mac === mac; }); + currentOriginalIp = ip; + var isValidIp = (ip && ip !== 'Unknown IP' && ip.substring(0, ip.lastIndexOf('.') + 1) === basePrefix); + + var initStrategy = isValidIp ? 'keep' : 'smart'; + + modalOverlay.querySelector('input[name="single_strategy"][value="'+initStrategy+'"]').checked = true; + + var keepRadio = modalOverlay.querySelector('input[name="single_strategy"][value="keep"]'); + if (!isValidIp) { + keepRadio.parentElement.style.opacity = '0.4'; + keepRadio.disabled = true; + } else { + keepRadio.parentElement.style.opacity = '1'; + keepRadio.disabled = false; + } + + openModal({ + title: isEdit ? T['TIT_EDIT_DEV'] : T['TIT_QUICK_BIND'], + content: T['TXT_CONFIG_MAC'] + ' ' + mac.toUpperCase() + '', + showForm: true, + showSingleStrategy: true, + targetMac: mac, + defName: rawName === 'Unknown' ? '' : rawName, + defIp: '', + okText: isEdit ? T['BTN_SAVE'] : T['BTN_BIND_DEV'], + onOk: function(data) { + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + if (batchBar) batchBar.classList.remove('show'); + loadingEl.style.display = 'flex'; + loadingText.innerText = T['MSG_WRITING']; + + callDeviceBind(mac, data.ip, data.name, data.dept, false).then(function() { + setTimeout(loadDevices, 2500); + }).catch(function() { setTimeout(loadDevices, 2500); }); + } + }); + + modalOverlay.querySelector('input[name="single_strategy"][value="'+initStrategy+'"]').dispatchEvent(new Event('change')); + }); + }); + + container.querySelectorAll('.btn-unbind').forEach(function(btn) { + btn.addEventListener('click', function() { + var mac = this.getAttribute('data-mac'); + + openModal({ + title: T['TIT_UNBIND'], + content: T['MSG_UNBIND_CONFIRM'].replace('{mac}', mac.toUpperCase()), + danger: true, + okText: T['BTN_CONFIRM_UNBIND'], + onOk: function() { + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + if (batchBar) batchBar.classList.remove('show'); + loadingEl.style.display = 'flex'; + loadingText.innerText = T['MSG_RELEASING']; + callDeviceUnbind(mac, false).then(function() { + setTimeout(loadDevices, 2500); + }).catch(function() { setTimeout(loadDevices, 2500); }); + } + }); + }); + }); + } + + // 并发任务池 + function runWithConcurrency(tasks, limit) { + return new Promise(function(resolve, reject) { + if (tasks.length === 0) return resolve(); + var index = 0; + var activeCount = 0; + + function next() { + // 所有任务发完,且没执行的任务,结束 + if (index >= tasks.length && activeCount === 0) { + resolve(); + return; + } + // 有任务,继续 + while (activeCount < limit && index < tasks.length) { + activeCount++; + var task = tasks[index++]; + task().then(function() { + activeCount--; + next(); // 完成一個,上一個 + }).catch(function(err) { + activeCount--; + next(); // 即使失败下一個 + }); + } + } + next(); + }); + } + + btnBatchUnbind.addEventListener('click', function() { + var validUnbinds = selectedDevices.filter(function(d) { + return d.is_static === true || d.is_static === 'true'; + }); + + if (validUnbinds.length === 0) { + showCustomAlert(T['TIP_ALL_UNBOUND'].replace('{count}', selectedDevices.length)); + return; + } + + openModal({ + title: T['TIT_BATCH_UNBIND'], + content: T['MSG_BATCH_UNBIND_CONFIRM'].replace('{total}', selectedDevices.length).replace('{valid}', validUnbinds.length), + danger: true, + okText: T['BTN_CONFIRM_UNBIND'], + onOk: function() { + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + if (batchBar) batchBar.classList.remove('show'); + loadingEl.style.display = 'flex'; + loadingText.innerText = T['MSG_BATCH_RELEASING']; + + var tasks = validUnbinds.map(function(dev) { + return function() { return callDeviceUnbind(dev.mac, true); }; + }); + + runWithConcurrency(tasks, 10).then(function() { // 批量释放并发数:10 + return callApplyDhcp(); + }).then(function() { + setTimeout(loadDevices, 2500); + }).catch(function() { setTimeout(loadDevices, 2500); }); + } + }); + }); + + btnBatchBind.addEventListener('click', function() { + var strategyLabel = modalOverlay.querySelector('#nd-m-batch-fields .nd-input-label'); + if(strategyLabel) { + strategyLabel.innerHTML = T['TXT_SEL_STRAT_COUNT'].replace('{count}', selectedDevices.length); + } + + openModal({ + title: T['TIT_BATCH_BIND'], + content: '', + showForm: true, + isBatchBind: true, + okText: T['BTN_START_ASSIGN'], + onOk: function(data) { + var strategy = data.strategy; + var dept_id = data.dept; + + var usedIps = []; + var selectedMacs = selectedDevices.map(function(d) { return d.mac; }); + globalDevices.forEach(function(d) { + if (selectedMacs.indexOf(d.mac) === -1) { + if (d.ip && d.ip !== 'Unknown IP') usedIps.push(d.ip); + if (d.bound_ip && d.bound_ip !== 'Unknown IP') usedIps.push(d.bound_ip); + } + }); + + if (strategy === 'seq') { + var suf = parseInt(data.startSuffix, 10); + if (isNaN(suf) || suf < 2 || suf > 254) { showCustomAlert(T['ERR_SUF_RANGE']); return; } + + var availSeq = 0; + for (var i = suf; i <= 254; i++) { if (usedIps.indexOf(basePrefix + i) === -1) availSeq++; } + if (selectedDevices.length > availSeq) { + showCustomAlert(T['ERR_POOL_INSUFF'].replace('{suf}', suf).replace('{avail}', availSeq).replace('{count}', selectedDevices.length)); + return; + } + } else if (strategy === 'smart') { + var reqCounts = { mobile: 0, pc: 0, iot: 0, type_other: 0 }; + selectedDevices.forEach(function(d) { + var t = getDeviceType(d); + if (['mobile','pc','iot'].includes(t)) reqCounts[t]++; + else reqCounts['type_other']++; + }); + var rg = data.ranges; + var zones = { + mobile: { s: rg.ms, e: rg.me, name: T['TAB_MOBILE'] }, + pc: { s: rg.ps, e: rg.pe, name: T['TAB_PC'] }, + iot: { s: rg.is, e: rg.ie, name: T['TAB_IOT'] }, + type_other: { s: rg.os, e: rg.oe, name: T['TAB_OTHER'] } + }; + for (var k in reqCounts) { + if (reqCounts[k] > 0) { + var availSmart = 0; + for (var i = zones[k].s; i <= zones[k].e; i++) { if (usedIps.indexOf(basePrefix + i) === -1) availSmart++; } + if (reqCounts[k] > availSmart) { + showCustomAlert(T['ERR_CAT_FAIL'].replace('{name}', zones[k].name).replace('{req}', reqCounts[k]).replace('{avail}', availSmart)); + return; + } + } + } + } else if (strategy === 'dept') { + var tgt = globalDepartments.find(function(d){ return d.id === dept_id; }); + if(tgt) { + var availDept = 0; + for (var j = tgt.start; j <= tgt.end; j++) { if (usedIps.indexOf(basePrefix + j) === -1) availDept++; } + if (selectedDevices.length > availDept) { + showCustomAlert(T['ERR_POOL_INSUFF'].replace('{suf}', tgt.start).replace('{avail}', availDept).replace('{count}', selectedDevices.length)); + return; + } + } + } + + var tasks = []; + var currentIp = strategy === 'seq' ? (basePrefix + parseInt(data.startSuffix, 10)) : null; + + // 顺延“预先锁”机制 + var seqReservedIps = {}; // 记录专属座位 + if (strategy === 'seq') { + var suf = parseInt(data.startSuffix, 10); + selectedDevices.forEach(function(d) { + var eIp = d.bound_ip || d.ip; + if (eIp && eIp !== 'Unknown IP' && eIp.indexOf(basePrefix) === 0) { + var eSuf = parseInt(eIp.split('.').pop(), 10); + // 如果它的原 IP 在我们要顺延的范围内,且没被别人占领 + if (eSuf >= suf && usedIps.indexOf(eIp) === -1) { + usedIps.push(eIp); // 提前把这个 IP 占了 + seqReservedIps[d.mac] = eIp; // 记录这是它专属ip + } + } + }); + } + // ------------------------------------ + + var lastAssignedGlobal = null; + var skippedCount = 0; + + selectedDevices.forEach(function(dev) { + var assignIp = dev.bound_ip || dev.ip; + var existingIp = dev.bound_ip || dev.ip; + var exSuf = -1; + if (existingIp && existingIp !== 'Unknown IP' && existingIp.indexOf(basePrefix) === 0) { + exSuf = parseInt(existingIp.split('.').pop(), 10); + } + + if (strategy === 'keep') { + var devPrefix = assignIp.substring(0, assignIp.lastIndexOf('.') + 1); + if (devPrefix !== basePrefix || usedIps.indexOf(assignIp) !== -1) { + assignIp = getAvailableIpInRange(basePrefix, 50, 250, usedIps); + } + } else if (strategy === 'seq') { + // 检查设备有没有成功锁ip + if (seqReservedIps[dev.mac]) { + assignIp = seqReservedIps[dev.mac]; // 使用原ip + } else { + // 往下排队拿空闲 IP + assignIp = getNextAvailableIp(currentIp, usedIps); + currentIp = getNextAvailableIp(assignIp, usedIps); + } + } else if (strategy === 'smart') { + var devType = getDeviceType(dev); + var sStart = data.ranges.os, sEnd = data.ranges.oe; + if (devType === 'mobile') { sStart = data.ranges.ms; sEnd = data.ranges.me; } + else if (devType === 'pc') { sStart = data.ranges.ps; sEnd = data.ranges.pe; } + else if (devType === 'iot') { sStart = data.ranges.is; sEnd = data.ranges.ie; } + + // 原 IP 已经处于该分类的目标网段,且未被抢占,保留 + if (exSuf >= sStart && exSuf <= sEnd && usedIps.indexOf(existingIp) === -1) { + assignIp = existingIp; + } else { + var smartIp = getAvailableIpInRange(basePrefix, sStart, sEnd, usedIps); + if (smartIp) assignIp = smartIp; + } + } else if (strategy === 'dept') { + var tgt2 = globalDepartments.find(function(d){ return d.id === dept_id; }); + if (tgt2) { + // 原 IP 若在自定义组网段内,保留 + if (exSuf >= tgt2.start && exSuf <= tgt2.end && usedIps.indexOf(existingIp) === -1) { + assignIp = existingIp; + } else { + var dip = getAvailableIpInRange(basePrefix, tgt2.start, tgt2.end, usedIps); + if (dip) assignIp = dip; + } + } + } + + if (assignIp && usedIps.indexOf(assignIp) === -1) { + usedIps.push(assignIp); + } + + var isCurrentlyStatic = dev.is_static === true || dev.is_static === 'true'; + var oldDeptId = dev.dept || 'none'; + + // 选了“保持不变”,oldDeptId + var finalDept = (dept_id === 'keep') ? oldDeptId : dept_id; + + if (isCurrentlyStatic && assignIp === (dev.bound_ip || dev.ip) && finalDept === oldDeptId) { + skippedCount++; + return; + } + + lastAssignedGlobal = assignIp; + var safeName = dev.name === "Unknown" ? "" : dev.name; + tasks.push(function() { + // 写入时使用 finalDept + return callDeviceBind(dev.mac, assignIp, safeName, finalDept, true); + }); + }); + + if (tasks.length === 0) { + showCustomAlert(T['TIP_BATCH_NO_CHANGE'].replace('{count}', selectedDevices.length)); + return; + } + + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + if (batchBar) batchBar.classList.remove('show'); + loadingEl.style.display = 'flex'; + loadingText.innerText = T['MSG_RADAR_AVOID']; + + if (lastAssignedGlobal) localStorage.setItem('nw_last_ip', lastAssignedGlobal); + + runWithConcurrency(tasks, 10).then(function() { // 批量绑定并发数:10 + return callApplyDhcp(); + }).then(function() { + setTimeout(loadDevices, 2500); + }).catch(function() { setTimeout(loadDevices, 2500); }); + } + }); + }); + + var loadDevices = function(isRescan) { + var doRescanStr = (isRescan === true) ? 'true' : 'false'; + loadingEl.style.display = 'flex'; + loadingText.innerText = T['TXT_LOADING_RADAR']; + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + selectedDevices = []; + selectAllCb.checked = false; + updateBatchBar(); + + var showConnsCbEl = container.querySelector('#cb-show-conns'); + var isShowConns = showConnsCbEl ? showConnsCbEl.checked : false; + + + // 本地有缓存,直接返回 + var pSmart = needFetchSmart ? callGetSmartRanges() : Promise.resolve(null); + + Promise.all([callDeviceList(isShowConns, doRescanStr), callGetDepts(), pSmart]).then(function(results) { + loadingEl.style.display = 'none'; + + var resList = results[0]; + window._lanNatConflict = (resList && (resList.lan_nat_conflict === true || String(resList.lan_nat_conflict) === 'true')); + window._ipv6Enabled = (resList && (resList.ipv6_enabled === true || String(resList.ipv6_enabled) === 'true')); + + // --- 全局 NAT 冲突警告 --- + var globalWarnContainer = container.querySelector('#global-nat-v6-warn'); + + if (!globalWarnContainer) { + globalWarnContainer = document.createElement('div'); + globalWarnContainer.id = 'global-nat-v6-warn'; + globalWarnContainer.style.cssText = 'box-sizing: border-box; margin: 10px 0 0 0; width: 100% !important; max-width: 1080px !important;'; + + var ctrlBar = container.querySelector('.nd-control-bar'); + if (ctrlBar) { + ctrlBar.parentNode.insertBefore(globalWarnContainer, ctrlBar.nextSibling); + } + } + + if (window._lanNatConflict === true && window._ipv6Enabled === true) { + globalWarnContainer.style.display = 'block'; + // 内部样式微调:去掉这里多余的 margin,依靠外层的 margin: 15px 0 即可 + globalWarnContainer.innerHTML = '
    ' + + '🚨 ' + (T['V6_NAT_ERR_TIT1'] || '严重网络冲突') + '
    ' + + '
    ' + (T['V6_NAT_ERR_MSG1'] || '系统检测到 IPv6 与“动态伪装(NAT)”同时开启,会导致寻址瘫痪。请前往官方网络设置中关闭 NAT,或关闭 IPv6。') + '
    ' + + '
    '; + } else { + globalWarnContainer.style.display = 'none'; + globalWarnContainer.innerHTML = ''; + } + // -------------------------------- + + var resDepts = results[1]; + var resSmart = results[2]; + + // 发起请求且拿到了数据,写入本地缓存 + if (resSmart && resSmart.ranges) { + var parsedR = typeof resSmart.ranges === 'string' ? JSON.parse(resSmart.ranges || '{}') : resSmart.ranges; + if (parsedR && Object.keys(parsedR).length > 0) { + savedRanges = parsedR; + } else { + // 为空,使用默认值 + savedRanges = JSON.parse(JSON.stringify(defaultSmartRanges)); + } + localStorage.setItem('nw_smart_ranges', JSON.stringify(savedRanges)); + needFetchSmart = false; + } + + globalDepartments = []; + if (Array.isArray(resDepts)) { + globalDepartments = resDepts; + } else if (resDepts && resDepts.depts) { + var dData = typeof resDepts.depts === 'string' ? JSON.parse(resDepts.depts || '[]') : resDepts.depts; + globalDepartments = Array.isArray(dData) ? dData : []; + } else if (typeof resDepts === 'string') { + try { globalDepartments = JSON.parse(resDepts || '[]'); } catch(e) {} + } + + var devices = []; + if (resList && Array.isArray(resList.devices)) { + devices = resList.devices; + } else if (resList && typeof resList[''] === 'string') { + try { devices = JSON.parse(resList['']).devices || []; } catch(e){} + } else if (resList && typeof resList === 'string') { + try { devices = JSON.parse(resList).devices || []; } catch(e){} + } + + var currentHostIp = window.location.hostname; + var nowTs = Date.now(); + devices.forEach(function(d) { + if (d.ip === currentHostIp) d.is_local = true; + + var cacheKey = 'nw_unk_ts_' + d.mac; + + // 设备绑定静态ip,才清理它的計時器快取 + if (d.is_static === true || d.is_static === 'true') { + localStorage.removeItem(cacheKey); + } + // 如果它是未綁定的在线新设备 + else if (d.is_new_unknown === 'true' || d.is_new_unknown === true) { + var ts = localStorage.getItem(cacheKey); + + if (!ts) { + ts = nowTs; + localStorage.setItem(cacheKey, ts); + } + + // 檢查是否已安分守己超過 24 小時 + if (nowTs - parseInt(ts, 10) > 86400000) { + d.is_new_unknown = false; // 超時 + } else { + d.is_new_unknown = true; // 24小時內,亮紅燈 + } + } + }); + + globalDevices = devices; + + var gwDev = globalDevices.find(function(d) { return d.is_gw === 'true' || d.is_local === 'true'; }); + var baseIp = gwDev ? gwDev.ip : (globalDevices.length > 0 ? globalDevices[0].ip : '192.168.1.1'); + basePrefix = baseIp.substring(0, baseIp.lastIndexOf('.') + 1); + var preEl = container.querySelector('#nd-batch-prefix'); + if (preEl) preEl.innerText = basePrefix; + + globalDevices.sort(function(a, b) { + var getWeight = function(d) { + + if (d.is_new_unknown === 'true' || d.is_new_unknown === true) return 200; + + if (d.is_gw === true || d.is_gw === 'true') return 100; + if (d.is_local === true || d.is_local === 'true') return 90; + if (d.is_visitor === true || d.is_visitor === 'true') return 80; + if (d.is_static === true || d.is_static === 'true') return 70; + + var isOnline = (d.online === true || d.online === 'true'); + var isUnknown = (d.name === 'Unknown' || d.ip === 'Unknown IP'); + + if (isOnline && !isUnknown) return 60; + if (isOnline && isUnknown) return 50; + if (!isOnline && isUnknown) return 40; + return 30; + }; + + var weightA = getWeight(a); + var weightB = getWeight(b); + if (weightA !== weightB) { return weightB - weightA; } + + return a.ip.localeCompare(b.ip, undefined, {numeric: true, sensitivity: 'base'}); + }); + + if (globalDevices.length === 0) { + listEl.innerHTML = '
    ' + T['MSG_NO_DEVS'] + '
    '; + listEl.style.display = 'flex'; + return; + } + + catTabs.style.display = 'block'; + listHeader.style.display = 'flex'; + + updateTabsAndFilter(); + + }).catch(function(e) { + loadingEl.style.display = 'none'; + listHeader.style.display = 'none'; + catTabs.style.display = 'none'; + listEl.style.display = 'block'; + listEl.innerHTML = '
    ' + T['MSG_SCAN_FAIL'] + ' ('+e+')
    '; + }); + }; + + var showConnsCbEl = container.querySelector('#cb-show-conns'); + if (showConnsCbEl) { + // 页面加载读取绝对时间戳 + var expTime = localStorage.getItem('nw_conns_exp'); + if (expTime && Date.now() < parseInt(expTime)) { + showConnsCbEl.checked = true; + } else { + localStorage.removeItem('nw_conns_exp'); + showConnsCbEl.checked = false; + } + + // 每 3 秒检查一次 + setInterval(function() { + var exp = localStorage.getItem('nw_conns_exp'); + if (exp && Date.now() > parseInt(exp)) { + localStorage.removeItem('nw_conns_exp'); + var cb = container.querySelector('#cb-show-conns'); + if (cb && cb.checked) { + cb.checked = false; + loadDevices(); + } + } + }, 3000); + + showConnsCbEl.addEventListener('change', function() { + var icon = refreshBtn.querySelector('.nd-refresh-icon'); + if(icon) { icon.style.transform = 'rotate(360deg)'; setTimeout(function(){ icon.style.transform = 'none'; }, 800); } + + if (this.checked) { + // 本地浏览器记录 3 分钟过期时间戳 + localStorage.setItem('nw_conns_exp', Date.now() + 3 * 60 * 1000); + } else { + localStorage.removeItem('nw_conns_exp'); + } + loadDevices(); + }); + } + + refreshBtn.addEventListener('click', function() { + var icon = this.querySelector('.nd-refresh-icon'); + if(icon) { icon.style.transform = 'rotate(360deg)'; setTimeout(function(){ icon.style.transform = 'none'; }, 800); } + + // 手动点击刷新,清空所有心跳缓存! + Object.keys(sessionStorage).forEach(function(key) { + if (key.indexOf('nw_v6_hb_') === 0) { + sessionStorage.removeItem(key); + } + }); + + loadDevices(true); // 主动Ping + }); + + var fwResetGear = modalOverlay.querySelector('#fw-reset-gear'); + if (fwResetGear) { + fwResetGear.addEventListener('click', function() { + openModal({ + title: T['TIT_RESET_ALL'], + content: T['MSG_RESET_CONFIRM'], + danger: true, + okText: T['BTN_RESET_ALL'], + onOk: function() { + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + if (batchBar) batchBar.classList.remove('show'); + loadingEl.style.display = 'flex'; + loadingText.innerText = T['MSG_RESETTING']; + + callResetAll().then(function() { + setTimeout(loadDevices, 2000); + }).catch(function(e) { + showCustomAlert(T['ERR_SAVE_FAIL_SHORT'].replace('{err}', e)); + setTimeout(loadDevices, 2000); + }); + } + }); + }); + } + + // 导入和导出的 事件 + var btnExportNet = modalOverlay.querySelector('#nd-btn-export-net'); + var btnImportNet = modalOverlay.querySelector('#nd-btn-import-net'); + var fileImportNet = modalOverlay.querySelector('#nd-file-import-net'); + + if (btnExportNet) { + btnExportNet.addEventListener('click', function() { + openModal({ + title: T['TIT_EXPORT_ING'], + content: T['MSG_EXPORT_ING'], + hideCancel: true, + okText: T['BTN_PLEASE_WAIT'] + }); + callExportConfig().then(function(res) { + + if(res && res.url) { + var a = document.createElement("a"); + a.href = res.url; + a.download = res.filename || "NetWiz_NetConf.tar.gz"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + openModal({ title: T['TIT_EXPORT_OK'], content: T['MSG_EXPORT_OK'], hideCancel: true, okText: T['BTN_CLOSE'] }); + } else { + openModal({ title: T['TIT_EXPORT_FAIL'], content: T['MSG_EXPORT_FAIL_NODATA'], hideCancel: true, okText: T['BTN_CLOSE'] }); + } + }).catch(function(err) { + openModal({ title: T['TIT_EXPORT_FAIL'], content: T['MSG_EXPORT_FAIL_ERR'] + err, hideCancel: true, okText: T['BTN_CLOSE'] }); + }); + }); + } + + function confirmAndRestore(payload) { + openModal({ + title: '⚡ ' + (T['TIT_RESTORE_DATA'] || 'Restore Device Data'), + content: '
    ' + (T['MSG_RESTORE_CONFIRM'] || 'Confirm to restore?') + '
    ', + okText: T['BTN_CONFIRM'] || 'Confirm', + danger: true, + onOk: function() { + listHeader.style.display = 'none'; + listEl.style.display = 'none'; + catTabs.style.display = 'none'; + loadingEl.style.display = 'flex'; + loadingText.innerHTML = T['MSG_IMPORT_RESTARTING']; + callImportConfig(payload.data || "", payload.file || "").then(function() { + setTimeout(function() { window.location.reload(); }, 25000); + }).catch(function() { + setTimeout(function() { window.location.reload(); }, 25000); + }); + } + }); + } + + if (btnImportNet && fileImportNet) { + btnImportNet.addEventListener('click', function() { + openModal({ + title: T['TIT_RESTORE_NET'], + content: T['MSG_READING_BAK'], + hideCancel: true, + okText: T['BTN_CLOSE'] + }); + + callListBackups().then(function(res) { + var bks = res || []; + var optsHtml = ''; + if (bks.length === 0) { + optsHtml = ''; + } else { + bks.forEach(function(f) { + var display = f.replace('.tar.gz', '').replace('netwiz_', ''); + + // 解析前缀和时间 + var typeStr = ''; + if (f.indexOf('auto_') !== -1) typeStr = '🤖 ' + (T['TXT_BAK_AUTO'] || 'Auto Backup'); + else if (f.indexOf('pre_import_') !== -1) typeStr = '⬆️ ' + (T['TXT_BAK_IMPORT'] || 'Before Import'); + else if (f.indexOf('pre_reset_') !== -1) typeStr = '⚠️ ' + (T['TXT_BAK_RESET'] || 'Before Reset'); + + // 提取时间字符串后缀 20XXXXXX_170748 + var match = f.match(/(\d{8})_(\d{6})/); + if (match && typeStr) { + var d = match[1], t = match[2]; + var timeStr = d.substring(0,4) + '/' + d.substring(4,6) + '/' + d.substring(6,8) + ' ' + + t.substring(0,2) + ':' + t.substring(2,4) + ':' + t.substring(4,6); + display = typeStr + ' (' + timeStr + ')'; + } + + optsHtml += ''; + }); + } + + var html = '
    ' + + '
    ' + + '
    ' + T['LBL_RESTORE_ROUTER'] + '
    ' + + '' + + '' + + '
    ' + + '
    ' + T['TXT_OR'] + '
    ' + + '
    ' + + '
    ' + T['LBL_RESTORE_PC'] + '
    ' + + '' + + '
    ' + + '
    '; + + openModal({ title: T['TIT_RESTORE_NET'], content: html, hideCancel: true, okText: T['BTN_CLOSE'] }); + + // 绑定弹窗里的两个按钮事件 + var btnRestoreRouter = document.getElementById('nd-btn-restore-router'); + var btnUploadPc = document.getElementById('nd-btn-upload-pc'); + var selRouterBak = document.getElementById('nd-sel-router-bak'); + + if (btnRestoreRouter) { + btnRestoreRouter.onclick = function() { + var file = selRouterBak.value; + if (!file) return; + confirmAndRestore({ file: file }); + }; + } + + if (btnUploadPc) { + btnUploadPc.onclick = function() { + fileImportNet.click(); // 触发隐藏的文件选择器 + }; + } + }).catch(function() { + openModal({ title: T['TIT_READ_FAIL'], content: T['MSG_READ_FAIL'], hideCancel: true, okText: T['BTN_CLOSE'] }); + }); + }); + + // 电脑上传文件选择后的处理 + fileImportNet.addEventListener('change', function(e) { + var file = e.target.files[0]; + if (!file) return; + + // [防呆拦截] 限制文件大小不超过 2MB + if (file.size > 2 * 1024 * 1024) { + openModal({ + title: '⚠️ ' + (T['TIT_FILE_LARGE'] || 'File Too Large'), + content: T['MSG_FILE_LARGE'], + hideCancel: true, + okText: T['BTN_CLOSE'] || 'Close' + }); + fileImportNet.value = ''; + return; + } + + var reader = new FileReader(); + reader.onload = function(evt) { + var b64data = evt.target.result.split(',')[1]; + + openModal({ + title: '⚡ ' + (T['TIT_RESTORE_NET'] || 'Restore Network Config'), + content: '
    ' + T['MSG_RESTORE_NET_CONFIRM'] + '
    ', + okText: T['BTN_CONFIRM'] || 'Confirm', + onOk: function() { + openModal({ title: T['TIT_RESTORING'] || 'Restoring', content: T['MSG_WAIT'] || 'Writing config...', spin: true }); + rpc.declare({ object: 'netwiz_dev', method: 'import_config', params: ['data'], expect: { result: 0 } })({ data: b64data }).then(function(res) { + if (res && res.error) { + openModal({ title: '❌ ' + (T['TIT_RESTORE_FAIL'] || 'Restore Failed'), content: (T['TXT_ERROR'] || 'Error: ') + res.error, hideCancel: true, okText: T['BTN_CLOSE'] || 'Close' }); + } else { + setTimeout(function() { window.location.reload(); }, 6000); + } + }).catch(function() { + setTimeout(function() { window.location.reload(); }, 6000); + }); + } + }); + }; + reader.readAsDataURL(file); + fileImportNet.value = ''; + }); + } + + + loadDevices(); + } +}); diff --git a/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_tmpl.js b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_tmpl.js new file mode 100644 index 000000000..c60e0096b --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/netwiz_tmpl.js @@ -0,0 +1,1115 @@ +'use strict'; +'require view'; // 引入 LuCI 的 view 官方核心 + +return view.extend({ // 继承view + // 1. 多语言字典 + T: { + 'Network_Wizard': _('Network Wizard'), + 'TITLE': _('Netwiz NETWORK SETUP'), + 'SUBTITLE': _('Pure · Secure · Non-destructive Minimalist Config'), + 'APP_VERSION': 'v0.1.0', + 'MODE_ROUTER_TITLE': _('DHCP / Static IP (WAN)'), + 'MODE_ROUTER_DESC': _('Automatically obtain IP from the upstream network, or manually set a static IP.'), + 'MODE_PPPOE_TITLE': _('PPPoE Dial-up'), + 'MODE_PPPOE_DESC': _('Dial up directly using account and password on this device.'), + 'MODE_LAN_TITLE': _('LAN Settings'), + 'MODE_LAN_DESC': _('Change device LAN IP, switch to AP Wired Relay mode, or one-click IPv6 distribution.'), + 'MODE_WIFI_TITLE': _('Wi-Fi Settings'), + 'MODE_WIFI_DESC': _('Set Wi-Fi name, password, enable Wireless Relay (WISP) and Seamless Roaming.'), + 'TITLE_WIFI': _('Configure Wi-Fi'), + 'LBL_SMART_CONN': _('Smart Connect (All Bands)'), + 'LBL_WIFI_SWITCH': _('Enable Wi-Fi'), + 'LBL_WIFI_2G_EN': _('Enable 2.4G Wi-Fi'), + 'LBL_WIFI_5G_EN': _('Enable 5G Wi-Fi'), + 'PH_WIFI_SSID': _('e.g., My_WiFi'), + 'LBL_SSID': _('Network Name (SSID)'), + 'LBL_WIFI_PASS': _('Wi-Fi Password'), + 'LBL_WIFI_ENC': _('Encryption'), + 'LBL_5G2_SSID': _('Network Name (5G_Game)'), + 'LBL_5G2_PWD': _('Wi-Fi Password (5G_Game)'), + 'LBL_ADVANCED': _('Advanced Settings'), + 'LBL_ADVANCED_CLOSE': _('Hide Advanced'), + 'LBL_HIDE_SSID': _('Hide Wi-Fi Name (SSID)'), + 'LBL_CHANNEL': _('Channel'), + 'LBL_BANDWIDTH': _('Channel Width'), + 'LBL_MODE': _('Wireless Mode'), + 'OPT_AUTO': _('Auto'), + 'LBL_LEGACY_B': _('Enable 802.11b (Legacy Mode)'), + 'DESC_LEGACY_B': _('Only enable if very old IoT devices cannot connect.'), + 'OPT_NONE': _('No Password (Open)'), + 'OPT_PSK2': _('WPA2-PSK (Stable)'), + 'OPT_SAE': _('WPA3-SAE (Secure)'), + 'OPT_PSK2SAE': _('WPA2/WPA3 Mixed (Recommended)'), + 'TAB_2G': _('2.4G Wi-Fi'), + 'TAB_5G': _('5G Wi-Fi'), + 'M_INC_WIFI': _('SSID cannot be empty.'), + 'M_PWD_SHORT': _('Wi-Fi password must be at least 8 characters.'), + 'ACT_WIFI': _('Applying Wi-Fi Settings'), + 'M_MODE_WARN_TIT': '⚠️ ' + _('Severe Warning'), + 'M_MODE_WARN_MSG': _('Forcibly modifying the wireless physical mode may cause the hardware driver to crash or the Wi-Fi to disappear permanently if the chip does not support it! It is highly recommended to keep it on [Auto].

    Are you absolutely sure you want to change this?'), + 'LOADING_CONFIG': _('Reading underlying config...'), + 'BTN_HOME': _('Back to Home'), + 'TITLE_WAN': _('Configure WAN'), + 'LBL_CONN_TYPE': _('Connection Type'), + 'OPT_DHCP': _('DHCP (Auto)'), + 'OPT_STATIC': _('Static IP'), + 'LBL_IP': _('Static IP'), + 'LBL_GW': _('Gateway'), + 'PH_IP': _('e.g., 192.168.1.2'), + 'PH_GW': _('e.g., 192.168.1.1'), + 'TITLE_PPPOE': _('PPPoE Credentials'), + 'LBL_USER': _('PPPoE Username'), + 'PH_USER': _('Enter PPPoE username'), + 'LBL_PASS': _('PPPoE Password'), + 'PH_PASS': _('Enter PPPoE password'), + 'TITLE_LAN': _('Configure LAN'), + 'LBL_IPV6': _('Enable IPv6 (DHCPv6)'), + 'TIP_IPV6_DESC': _('When enabled, [Terminal Device & IP Binding] can assign short IPv6 addresses to PCs.'), + 'LBL_FORCE_APPLY': _('Safe Mode (Recommended ON)'), + 'DESC_FORCE_APPLY': _('If enabled, the system will auto-revert if you lose connection within 120s.'), + 'MSG_SAFE_OFF': _('Safe mode disabled. Applying immediately without rollback protection...'), + 'LBL_BYPASS': _('Enable AP Wired Relay'), + 'WARN_BYPASS': _('AP Wired Relay Enabled:
    1. DHCP will be disabled. Devices must use static IPs or get IPs from upstream.
    2. If LAN IP changes, ensure your client is in the same subnet to avoid losing access.'), + 'WARN_MAIN': _('Main Router Mode Enabled:
    1. DHCP will be enabled. This device assigns IPs.
    2. If LAN IP changes, ensure your client is in the same subnet to avoid losing access.'), + 'LBL_LAN_IP': _('Device LAN IP'), + 'LBL_LAN_GW': _('LAN Gateway'), + // ===== 一键探测与拦截防呆新增 ===== + 'BTN_AUTO_DETECT': _('Auto Detect'), + 'MSG_DETECT_SUCC': _('Upstream subnet detected, recommended IP assigned'), + 'MSG_DETECT_FAIL': _('Detection failed, cannot get upstream gateway'), + 'M_DETECT_OP_INV_TIT': _('Invalid Operation'), + 'M_DETECT_OP_INV_MSG': _('In Main Router mode, LAN Gateway MUST be empty.

    "Auto Detect" is only for AP/Relay mode!'), + 'M_DETECT_PPPOE_TIT': _('Critical Intercept'), + 'M_DETECT_PPPOE_MSG': _('System detected WAN is PPPoE Dial-up.

    The acquired {gw} is the ISP BRAS gateway!
    NEVER set this as LAN gateway, or your network will crash!'), + + 'PH_LAN_GW': _('Blank for Main, required for AP Wired Relay'), + 'BTN_BACK': _('Back'), + 'BTN_NEXT': _('Next Step'), + 'BTN_EDIT': _('Back to Edit'), + 'TITLE_CONFIRM': _('Confirm Configuration'), + 'DESC_CONFIRM': _('The following settings will be applied, please verify:'), + 'NOTE_TITLE': _('Application Notes:'), + 'NOTE_1': _('After confirmation, the network will restart and apply new settings.'), + 'NOTE_2': _('The system will auto-refresh or redirect in 15 seconds.'), + 'BTN_APPLY': _('Apply Settings'), + 'STAT_BYPASS': _('AP Wired Relay'), + 'CURRENT_MODE': _('Current:'), + 'STAT_MAIN_PPPOE': _('PPPoE Dial-up'), + 'STAT_SEC_DHCP': _('DHCP Client (Auto IP)'), + 'STAT_SEC_STATIC': _('Static IP'), + 'STAT_LAN': _('LAN Mode'), + 'TXT_DEV_IP': _('Device IP:'), + 'TXT_UP_GW': _('Upstream GW:'), + 'TXT_PUB_IP': _('Public IP:'), + 'TXT_REM_GW': _('Remote GW:'), + 'TXT_LAN_IP': _('LAN IP:'), + 'TXT_STATUS': _('Status:'), + 'TXT_WAIT_REM': _('Waiting for remote response'), + 'TXT_WAN_IP': _('WAN IP:'), + 'TXT_GET_IP': _('Getting IP...'), + 'TXT_DHCP_SRV': _('DHCP Service:'), + 'TXT_ON': _('Enabled'), + 'TXT_OFF': _('Disabled'), + 'BDG_SUCC': _('Dial Success'), + 'BDG_DIAL': _('Dialing / Disconnected'), + 'BDG_GOT': _('IP Acquired'), + 'BDG_WAIT': _('Waiting for IP...'), + 'BDG_CONN': _('Interface Connected'), + 'BDG_UNPLUG': _('Cable Unplugged'), + 'TXT_GETTING': _('Getting...'), + 'TXT_NOT_GOT': _('Not acquired'), + 'TXT_NOT_SET': _('Not set'), + 'M_INC_TIT': _('Incomplete info'), + 'M_INC_IP': _('Device IP cannot be empty.'), + 'M_INC_WAN': _('Static IP and Gateway cannot be empty.'), + 'M_INC_PPPOE': _('PPPoE username and password cannot be empty.'), + 'M_FMT_TIT': _('Format Error'), + 'M_FMT_IP': _('The device IP is invalid, please check!'), + 'M_FMT_WAN': _('WAN IP is invalid, please check!'), + 'M_FMT_GW': _('Gateway IP is invalid, please check!'), + 'M_LOGIC_TIT': _('Logic Error'), + 'M_LOGIC_BYP': _('AP Wired Relay requires an upstream gateway IP.'), + 'M_SAME_GW': _('WAN Static IP MUST NOT be the same as the gateway!'), + 'M_SAME_BYP': _('In AP/Relay mode, the device IP MUST NOT be the exact same as the Gateway! (') + '💡 ' + _('Tip: The Gateway should be the LAN IP of your upstream main router, and this device needs its own unique IP)'), + 'M_NO_MOD_TIT': _('No Changes Needed'), + 'M_NO_MOD_MSG': _('Your settings match the current router config exactly.'), + 'M_EXIT': _('Exit to Home'), + 'M_CFLT_TIT': _('Conflict Blocked'), + 'M_CFLT_IP': _('The WAN IP cannot be the same as the current LAN IP ({ip})!'), + 'M_CFLT_SUB1': _('The WAN port cannot be in the same subnet as the LAN ({ip})!'), + 'M_CFLT_SUB2': _('This causes a routing loop.'), + 'M_CFLT_SUGGEST': _('Suggestion: Your upstream network uses the same IP subnet. Please go to [LAN Settings] first and change your Device LAN IP (e.g. to 192.168.10.1) to prevent network crash.'), + 'M_SUB_ERR_TIT': _('Subnet Error'), + 'M_SUB_ERR_WAN1': _('The WAN Static IP must be in the same subnet as the Gateway!'), + 'M_SUB_ERR_WAN2': _('e.g., if gateway is {gw}, the IP must be {ip}.x'), + 'M_SUB_ERR_BYP': _('In AP/Relay mode, the device IP must be in the same subnet as the Gateway! (') + '💡 ' + _('Tip: The Gateway should be the LAN IP of your upstream main router)'), + 'M_WARN_TIT': _('Config Warning'), + 'M_WARN_MSG': _('You selected [Main Router Mode] but filled in the [Gateway].

    For a standard main router, the gateway must be blank. Entering a gateway may cause the device to fail at distributing network, leading to a total outage!

    Are you sure you want to do this?'), + 'M_WARN_BTN': _('Force Apply'), + 'M_SYS_ERR': _('System Exception'), + 'M_SYS_MSG': _('Cannot read underlying config for validation, please refresh.'), + 'M_APP_MSG': _('Writing request, please wait...'), + 'M_RST_TIT': _('Applying Configuration'), + 'M_CLOSE': _('Close'), + 'M_ACCT': _('Account'), + 'M_PWD': _('Password'), + 'M_IP_GW': _('IP & Gateway'), + 'M_AUTO_UP': _('Auto-assigned by upstream router'), + 'LBL_TARGET': _('Target:'), + 'ACT_LAN': _('Modifying LAN IP'), + 'ACT_BYPASS': _('Switching to AP Wired Relay'), + 'ACT_WAN_DHCP': _('Switching WAN to DHCP'), + 'ACT_WAN_STATIC': _('Switching WAN to Static IP'), + 'ACT_PPPOE': _('Applying PPPoE Dial-up'), + 'MSG_WRITING': _('Writing configuration to system, please do not close the page...'), + 'MSG_KNOCKING': _('Connecting to new IP... Config will auto-rollback upon timeout.'), + 'MSG_WAIT_NET': _('Waiting for network service to restart... Elapsed: {sec}s'), + 'MSG_WAIT_OLD': _('Waiting for router to safely restore... Elapsed: {sec}s'), + 'MSG_TIMER': _('Rollback countdown: {sec} / {total} s'), + 'MSG_MANUAL_VISIT': _('If IP changed, please update PC IP. Auto-redirecting when connected...'), + 'MSG_ABANDONING': _('Waiting for router to abort changes and restore network...'), + 'TXT_WIFI_STATUS': _('Wi-Fi Status'), + 'TXT_5G_ACCT': _('5G Wi-Fi Account'), + 'TXT_2G_ACCT': _('2.4G Wi-Fi Account'), + 'TXT_NO_PASS': _('No Password'), + // 中继功能词条 + 'LBL_WISP_EN': _('Enable Wireless Relay (WISP)'), + 'DESC_WISP': _('Receive upstream Wi-Fi and broadcast your own network.'), + 'BTN_SCAN': '🔄 ' + _('Scan Nearby Wi-Fi'), + 'MODAL_WISP_TITLE': _('Select Upstream Network'), + 'WISP_PWD_PROMPT': _('Password for upstream:'), + 'TXT_WISP_ON': _('WISP Enabled'), + // 扫描与错误提示词条 + 'TXT_SCANNING': '⏳ ' + _('Scanning...'), + 'TXT_NO_NETWORKS': _('No networks found.'), + 'TXT_SCAN_FAILED': _('Scan failed. Driver might be busy.'), + 'LBL_ROAMING': _('802.11k/v/r Fast Roaming'), + 'DESC_ROAMING': _('Enable seamless roaming between routers with one click (Prerequisite: Same SSID, password, and LAN). Note: May cause connection issues with older smart home (IoT) devices.'), + 'TXT_TARGET_SSID': _('Target Wi-Fi'), + 'PH_WISP_PWD': _('Upstream Wi-Fi Password'), + 'TXT_ROAM_DIRTY': '⚠️ ' + _('Manual Configuration Warning'), + 'DESC_ROAM_DIRTY': _('Underlying parameter mismatch detected, which may cause roaming failures. Please toggle this switch off and on again, then save to apply the standard seamless roaming profile.'), + 'TXT_ROAMING': _('Roaming'), + 'TXT_ROAMING_ON': _('Roaming Enabled'), + 'TXT_CLICK_FIX': _('Click to Fix'), + 'TXT_CLICK_GOTO': _('Click to Settings'), + 'MSG_WAN_AUTODETECT': _('WAN Blind-Switch: Unplug the WAN cable for 10 seconds and reconnect to auto-detect and switch the connection type (takes about 2 mins).'), + 'TXT_NEW_MOD': _('New Config'), + 'TXT_MODIFIED': _('Modified'), + 'M_OPEN_WARN_TIT': _('Security Warning'), + 'M_OPEN_WARN_MSG': _('You are setting up an Open Wi-Fi network without a password. Anyone nearby will be able to connect and access your network.

    Are you sure you want to continue?'), + // ===== 向导词条 ===== + 'WIZ_TITLE': _('Quick Setup Wizard'), + 'WIZ_PWD': _('Step 1: Admin Password'), + 'WIZ_WAN': _('Step 2: Internet Setup'), + 'WIZ_WIFI': _('Step 3: Wi-Fi Setup'), + 'WIZ_CONFIRM': _('Step 4: Confirm & Apply'), + 'LBL_CONFIRM_PWD': _('Confirm Password'), + 'PH_CONFIRM_PWD': _('Enter password again'), + 'M_PWD_MISMATCH': _('Passwords do not match, please try again!'), + 'WIZ_SKIP': _('Skip this time'), + 'TXT_NOT_CONFIGURED': _('Not configured (Keep current)'), + 'WIZ_WIFI_DESC': _('Set your wireless network name and password.'), + 'WIZ_HIDE': _("Don't show this again"), + 'WIZ_REOPEN': '✨ ' + _('Reopen Wizard'), + 'WIZ_SKIP_WIFI': _('Skip Wi-Fi Setup (Keep current)'), + 'TXT_UNSET': _('Not set'), + 'TXT_NO_PWD_OPEN': _('No Password (Open)'), + 'BTN_DEV_BIND': _('Terminal Device & IP Binding'), + 'TXT_DNS1': _('Primary DNS:'), + 'TXT_DNS2': _('Secondary DNS:'), + 'TIP_IPV6_WARN':_('Non-standard parameters (Default configuration recommended)'), + 'PH_PWD_TIP': '💡 ' + _('This password will be used for logging into the router web interface and SSH. Setting it now is highly recommended.'), + // ===== 新增防呆与冲突拦截词条 ===== + 'M_WAN_DOWN_TIT': _('Cable Unplugged or Wrong Port'), + 'M_WAN_DOWN_MSG': _('System detected NO SIGNAL on the WAN port!

    ') + '💡 ' + _('Troubleshooting:
    1. Did you plug the upstream cable into the LAN port?
    2. Are both ends plugged in tightly? Is the modem powered on?
    '), + 'M_WAN_DOWN_WAIT': _('Detecting in background... This will close automatically once connected.'), + 'BTN_IGNORE_WAN': _('I am an AP / Ignore for now'), + 'M_CFLT_GLOBAL_TIT': _('Severe Warning: Routing Loop'), + 'M_CFLT_GLOBAL_MSG': _('System detected that the upstream network ({wan_ip}) and the LAN ({lan_ip}) are in the same subnet!

    This will crash the router. Please confirm or modify the new LAN IP below and click auto-evade:

    '), + 'BTN_FIX_CONFLICT': _('Modify and Restart Network'), + 'M_INVALID_IP_FMT': '❌ ' + _('Invalid IP format! System restored safe default, please confirm.'), + 'M_STILL_CONFLICT': '❌ ' + _('Still conflicting! The modified IP is still in the same subnet as upstream.') + '\n' + _('System restored safe default, please change the 3rd number!'), + 'M_WARN_UNSAVED': _('IMPORTANT:') + '\n\n' + + _('To resolve the fatal network loop, the system must change LAN IP and restart immediately.') + '\n\n' + + _('Changes you made on this page will NOT be saved.') + '\n' + + _('Please reconfigure after the network restarts and redirects to the new IP.') + '\n\n' + + _('Continue to fix conflict?'), + 'M_CFLT_INTERCEPT_TIT': _('Network Conflict Blocked'), + 'M_CFLT_WIZ_MSG': _('Upstream network ({wan_ip}) conflicts with local LAN ({lan_ip})!

    Please click below to safely evade to: {safe_ip} to finish setup.'), + 'BTN_AUTO_EVADE': _('Auto Evade'), + 'M_CFLT_CROSS_TIT': _('Cross-Subnet Loop Blocked'), + 'M_CFLT_CROSS_MSG': _('The LAN IP ({ip}) you set is in the same subnet as your upstream network ({wan_ip}), which will crash the router!

    Suggested evasion to: {safe_ip}'), + 'BTN_FIX_APPLY': _('Fix and Apply'), + 'BTN_EDIT_MYSELF': _('Edit Manually'), + 'M_CFLT_ROUTER_MSG': _('Upstream network ({wan_ip}) conflicts with local LAN ({lan_ip})!

    You MUST change device LAN IP. Suggested evasion to: {safe_ip}'), + 'M_CFLT_PHYSICAL_TIT': _('Severe Physical Conflict'), + 'M_CFLT_PHYSICAL_WAN_MSG': _('The WAN Static IP ({ip}) you set is exactly the same as the upstream Gateway!

    This causes a severe physical loop. Please change your Static IP (e.g., {suggest_ip}).'), + 'M_CFLT_PHYSICAL_BYP_MSG': _('The AP IP ({ip}) you set is exactly the same as the upstream Gateway!

    This will paralyze the network. Please change to another free IP (e.g., {suggest_ip}).'), + 'LBL_NEW_PWD': _('Router Admin Password (Optional)'), + 'PH_NEW_PWD': _('Keep empty to retain current password'), + 'WIZ_SKIP_TITLE': _('Skip Wizard'), + 'WIZ_SKIP_MSG': _('Releasing wizard lock, entering official dashboard...'), + 'M_PWD_REQ': _('Please enter a new password or check "Skip Password Setup"!'), + 'TAG_SMART': _('Smart Connect'), + 'TAG_SPLIT': _('Independent Bands'), + 'TAG_WISP': _('WISP Repeater'), + 'TAG_DISABLED': _('Disabled'), + 'MSG_SYS_NOT_READY': _('The underlying network status is not fully loaded. Please wait before submitting to prevent configuration loss.'), + 'MSG_SETUP_DONE': _('Configuration complete! Automatically logging you in...'), + 'MSG_PWD_FAIL': _('Password setup failed: '), + 'MSG_NO_WIFI_TIP': _('No Wi-Fi hardware detected, this step will be skipped automatically.
    Please click [Next Step] directly.'), + + // 高级与实验室功能词条 + 'LBL_LAB_TITLE': _('Advanced & Lab Features'), + 'LBL_LAB_BETA': _('Beta'), + 'TXT_FULL_BACKUP_TIT': _('Full Software Backup & Restore'), + 'TXT_FULL_BACKUP_DESC': _('Resolves traditional backup soft-brick risks. Smart backup is cross-version compatible. After flashing new firmware, upload backup to rapidly reinstall all software and losslessly restore configs (Requires Internet). Does not delete current software during restore. For absolute purity, factory reset the router first.
    Tip: If the backup shows Missing packages, manually place the installation packages in the /etc/netwiz/custom_pkgs/ directory.
    If Netwiz is missing after a reset or needs an upgrade, connect to the internet and run this in SSH:'), + 'BTN_SMART_BACKUP': '📦 ' + _('Generate Backup'), + 'BTN_SMART_RESTORE': '⚡ ' + _('Restore System'), + 'TXT_COPY_TIP': '📋 ' + _('Click to Copy'), + 'TXT_COPIED': '✅ ' + _('Copied'), + 'TXT_COPY_FAIL': '❌ ' + _('Copy Failed'), + + // 备份提示 + 'M_BAK_SEL_TIT': '📦 ' + _('Select Backup Mode'), + 'M_BAK_LIGHT_TIT': _('Lightweight Backup'), + 'M_BAK_LIGHT_SUB': _('Recommended'), + 'M_BAK_FULL_TIT': _('Full Backup'), + 'M_BAK_FULL_SUB': _('Includes Dependencies'), + 'M_BAK_LIGHT_DESC': _('Lightweight Backup: Only packs core configs (Passwords/Wi-Fi/IP) and main plugin packages. Fast and requires no extra RAM. Requires internet or built-in dependencies when restoring.'), + 'M_BAK_FULL_DESC': _('Full Backup: Packs core configs and recursively downloads ALL underlying dependencies for third-party plugins. Large size, slower generation. NO internet required during restore. Recommended for devices with >256MB RAM.'), + 'BTN_START_BAK': _('Start Backup'), + 'M_BAK_GEN_TIT': '📦 ' + _('Generating Backup'), + 'M_BAK_GEN_MSG': _('Extracting configs and packing files in background...'), + 'M_BAK_HINT_FULL': _('Note: Full mode selected, downloading all dependencies. Takes 1-3 mins, please wait.'), + 'M_BAK_HINT_LIGHT': _('Note: Lightweight mode selected, packing is very fast (~30s).'), + 'M_BAK_SUCC_TIT': '✅ ' + _('Backup Successful'), + 'M_BAK_SUCC_MSG': _('Backup capsule downloaded. Upload this file after flashing new firmware to restore.'), + 'M_BAK_FAIL_TIT': '❌ ' + _('Backup Failed'), + 'M_BAK_FAIL_MSG': _('Unable to start background backup task.'), + + // 恢复提示 + 'M_RST_CONFIRM_TIT': '⚡ ' + _('Confirm System Restore'), + 'M_RST_CONFIRM_MSG': _('WARNING: This will overwrite current configs and reinstall plugins!

    Router will auto-reboot upon completion. Ensure file is correct.

    ') + '🛡️ ' + _('Safe Mode: Auto-rollback if connection is lost for 300s.

    '), + 'M_RST_REGRET_PILL': _('Auto-download current state backup before restore (Regret Pill)'), + 'BTN_CONFIRM_SEL': _('Confirm & Select File'), + 'M_RST_NATIVE_TIT': '⚡ ' + _('Native Fast Restore Mode'), + 'MSG_RESTORE_UPLOADING': _('Transferring capsule via high-speed system channel...

    DO NOT power off. Router will auto-reboot upon completion.

    ') + '🛡️ ' + _('Safe Mode: Auto-rollback if connection is lost for 300s.'), + 'M_ERR_WIZ_FAILED': _('Wizard execution failed'), + 'M_ERR_DATA_PROC': _('Configuration data processing failed'), + 'M_ERR_VALIDATE': _('Input validation failed'), + 'M_RST_DELIVERED': '✅ ' + _('Capsule delivered, preparing offline task...'), + 'M_RST_BLOCKED_TIT': '❌ ' + _('Restore Blocked by Security'), + 'M_RST_BLOCKED_MSG': _('Self-healing mechanism triggered. Garbage cleared, router is safe and unharmed.'), + 'M_RST_TRANS_FAIL': '❌ ' + _('Transfer Failed'), + 'M_RST_TRANS_REJECT': _('Interface rejected reception, status code: '), + 'M_RST_NET_ERR': '❌ ' + _('Network Error'), + 'M_RST_NET_INTR': _('File transfer interrupted unexpectedly, check network!'), + 'M_RST_PILL_TIT': '💊 ' + _('Preparing Regret Pill'), + 'M_RST_PILL_MSG': _('Backing up current state to prevent regrets...

    Tip: Restore will begin automatically after download'), + 'M_RST_PROBE_TIT': '🔍 ' + _('Probing Environment'), + 'M_RST_PROBE_MSG': _('Calculating system available RAM and storage...'), + 'M_BAK_OOM_TIT': '⚠️ ' + _('Critical RAM Shortage'), + 'M_RST_OOM_MSG': _('Your full capsule size is {size} MB.

    But /tmp (RAM) only has {avail} MB free!

    Force uploading will cause OOM, crash, and network drop.

    Solution: Please reboot the router once to clear RAM fragments, then retry.'), + 'BTN_CANCEL_RST': _('Cancel Restore'), + + // 后端执行状态映射代码 + 'MSG_RST_WAIT': _('Waiting for background task to start...'), + 'MSG_RST_INIT': _('Capsule delivered, initializing restore environment...'), + 'MSG_RST_SCAN': _('Scanning capsule structure to estimate uncompressed size (5-10s)...'), + 'MSG_RST_OOM_INTERCEPT': _('Uncompressed size reaches {u}MB, but RAM only has {a}MB left! Intercepted to prevent router crash.'), + 'MSG_RST_SAFE': _('Size safe! Extracting backup capsule...'), + 'MSG_RST_FAIL': _('Extraction failed! Capsule may be corrupted, initiating self-healing...'), + 'MSG_RST_CONF': _('Extraction successful, applying core configs...'), + 'MSG_RST_PKGS': _('Force offline installing plugins (this takes a while, please wait)...'), + 'MSG_RST_DONE': _('Restore thoroughly complete! Router will auto-reboot!'), + 'MSG_RST_INVALID': _('Invalid capsule file! NetWiz signature missing, intercepted for security.'), + 'V6_NAT_ERR_TIT1': '🚨 ' + _('Severe Network Topology Conflict!'), + 'V6_NAT_ERR_MSG1': _('System detected that IPv6 and LAN "Masquerading (NAT)" are BOTH enabled!This will paralyze IPv6 allocation and cause routing loops.
    %sFix: Please go to Network -> Firewall to disable LAN Masquerading, or Disable IPv6 in the LAN settings on the Netwiz homepage.').replace('%s', '👉 '), + 'V6_NAT_ERR_TIT2': '⚠️ ' + _('IPv6 Configuration Blocked'), + 'V6_NAT_ERR_MSG2': _('Detected that LAN "IP Masquerading (NAT)" is enabled. Forcing IPv6 on under a double-NAT topology will cause network disconnection.
    %sFix: Please go to Network -> Firewall -> Zones to disable LAN Masquerading first.').replace('%s', '👉 '), + 'MSG_REBOOTING': _('System is rebooting, please wait...'), + 'MSG_WAIT_OFFLINE': _('Waiting for device to disconnect...'), + 'TIT_IP_CONFLICT': _('IP Conflict Warning'), + 'MSG_IP_IN_USE': _('is already used by another device!'), + 'MSG_SUGGEST_FIX': _('We strongly recommend changing it to avoid network crashes.'), + 'BTN_FIX_IP': _('Fix to'), + 'MSG_SCAN_PKGS': _('Scanning installed plugins...'), + 'TIT_PKG_CHECK': _('Plugin Backup Status'), + 'MSG_CUSTOM_PKG_DESC': _('Please verify backup packages:'), + 'MSG_CUSTOM_PKG_ACT': _('If you proceed, missing plugins WILL NOT be restored automatically!'), + 'MSG_CUSTOM_PKG_TIP': _('Tip: Manually place missing packages .ipk/.apk into the /etc/netwiz/custom_pkgs/ directory via SSH to ensure they are automatically reinstalled during future restorations.'), + 'BTN_IGNORE_BAK': _('Ignore & Backup'), + 'TIT_ARCH_CONFLICT': _('Architecture Conflict Warning'), + 'MSG_RESTORE_ARCH_TIP': _('Architecture Safety Lock: Cross-package manager restoration (e.g., IPK to APK) is strictly prohibited. The underlying network and firewall configurations differ significantly across these system versions. Forcing a restore will cause severe network failure and brick your router!'), + 'BTN_FORCE_RESTORE': _('Force Restore Config'), + 'TXT_MISSING_PKGS': _('Missing packages:'), + 'TXT_PROVIDED_PKGS': _('Manually placed in custom_pkgs:'), + 'TIT_CUSTOM_PKG_READY': _('Custom Plugins Ready'), + 'MSG_CUSTOM_PKG_READY_DESC': _('Great! Your custom plugins are safely stored in the local directory and will be included in the backup capsule.'), + 'BTN_CONFIRM_BACKUP': _('Confirm Backup'), + 'TXT_OFFICIAL_PKGS': _('Auto-backup plugins:'), + 'TIT_OFFICIAL_PKG_READY': _('Plugin Scan Complete'), + 'MSG_OFFICIAL_PKG_READY_DESC': _('System scan complete! All your installed plugins are from the official repository and will be safely recorded for automatic restoration.'), + 'M_OOM_TITLE': '⚠️ ' + _('Backup Interrupted Warning'), + 'M_OOM_HEAD': _('Out of Memory (OOM Protection)!'), + 'M_OOM_DESC': _('The files you are trying to pack are too large and exceed the available memory.

    To prevent device crash, the backup task has been safely canceled. Please clear unnecessary core files and try again.'), + 'M_I_KNOW': _('I Got It'), + 'M_SCAN_TIMEOUT_TITLE': '⚠️ ' + _('Scan Timeout Warning'), + 'M_SCAN_TIMEOUT_HEAD': _('Communication Timeout or Network Error'), + 'M_SCAN_TIMEOUT_DESC': _('Communication with the router timed out (possibly due to slow network source retrieval or network fluctuations).

    To ensure backup integrity, the task has been safely canceled. Please try again later, or manually run opkg update via SSH.'), + 'TXT_WISP_WAITING': _('Connecting...'), + 'MSG_WISP_STUCK': '⚠️ ' + _('Connecting to upstream... (Check password or signal strength if stuck)'), + 'M_FIRST_SYNC_TITLE': '🔄 ' + _('First Time Syncing'), + 'M_FIRST_SYNC_SUB': _('Syncing lists in the background...'), + 'M_FIRST_SYNC_DESC': _('Verifying the list of plugins for automatic backup...

    Depending on the network, this usually takes 15 to 20 seconds.
    The system is verifying, please wait...'), + 'M_SYNC_OK': _('OK, I will try again later'), + 'MSG_RST_PKG_ERR': '🚨 ' + _('RESTORE FAILED: Package manager mismatch (apk vs ipk). Please use firmware with the same underlying system.'), + 'MSG_RST_ARCH_ERR': '🚨 ' + _('RESTORE FAILED: CPU Architecture mismatch. Forcing this restore will brick your router! Process aborted.'), + 'TIT_PKG_CONFLICT': _('Package Manager Conflict'), + 'TIT_ARCH_CONFLICT': _('CPU Architecture Conflict'), + 'MSG_PKG_ERR_APK': '🚨 ' + _('Your current system uses the apk architecture, but you are trying to flash an ipk backup!'), + 'MSG_PKG_ERR_OPKG': '🚨 ' + _('Your current system uses the opkg architecture, but you are trying to flash an apk backup!'), + 'MSG_ARCH_ERR_UI': _('Architecture mismatch! (Current router: {arch})'), + 'MSG_ARCH_ERR_DESC': _('The backup package you selected belongs to a different hardware architecture. Forcing this restore will brick your router!'), + 'MSG_FAST_BLOCK': _('The frontend security system has instantly blocked this dangerous operation.'), + 'TIT_ARCH_WARN': _('Architecture Warning'), + 'MSG_ARCH_WARN_1': _('No architecture identifier ({arch}) detected in the selected backup filename.'), + 'MSG_ARCH_WARN_2': _('If you have manually renamed this file, please ignore this warning.

    If it is the wrong package, the system\'s underlying security mechanism will forcibly intercept the restoration later!'), + 'BTN_WARN_CONTINUE': _('I understand, continue'), + 'TXT_SCAN_TO_CONN': _('Scan to Connect'), + 'WARN_PPPOE_INVALID': '⚠️ ' + _('Current WAN is in DHCP/Static mode. PPPoE settings will not take effect until applied.'), + 'WARN_ROUTER_INVALID': '⚠️ ' + _('Current WAN is in PPPoE mode. DHCP/Static settings will not take effect until applied.'), + // ===== 高级设置 ===== + 'LBL_ADV_UTILS_TITLE': '⚙️ ' + _('Advanced Utilities'), + 'LBL_MAC_CLONE_LINK': '🔗 ' + _('MAC Address Clone'), + 'LBL_CRON_REBOOT_LINK': '⏱️ ' + _('Scheduled Reboot'), + 'LBL_WEB_ACCESS_TOGGLE': '🌐 ' + _('IPv6 Web Access, Port:'), + + 'MSG_MAC_CLONE_TIP': '💡 ' + _('Tip: Some ISPs or campus networks bind to a specific device MAC. If dial-up fails, enter the cloned MAC here.'), + 'BTN_GET_MAC': '⚡ ' + _('Auto-fill MAC'), + 'MSG_MAC_NOT_FOUND': _('No active device connection detected, please enter MAC address manually.'), + + 'LBL_CRON_ENABLE': _('Enable Scheduled Reboot'), + 'LBL_CRON_TIME': _('Reboot Time:'), + 'LBL_CRON_DAYS': _('Repeat Days (Multi-select):'), + 'MSG_CRON_NO_DAY': _('Please select at least one day!'), + + 'LBL_DAY_1': _('Mon'), + 'LBL_DAY_2': _('Tue'), + 'LBL_DAY_3': _('Wed'), + 'LBL_DAY_4': _('Thu'), + 'LBL_DAY_5': _('Fri'), + 'LBL_DAY_6': _('Sat'), + 'LBL_DAY_0': _('Sun'), + + 'BTN_ADV_HIDE': _('Advanced Settings') + ' ▲', + 'BTN_ADV_SHOW': _('Advanced Settings') + ' ▼', + + 'LBL_CRON_REBOOT': _('Scheduled Reboot'), + 'LBL_MAC_CLONE': _('MAC Clone'), + 'BTN_CLEAR': '🗑️ ' + _('Clear (Restore Default)'), + 'TXT_NET_OK': _('Internet Connected'), + 'TXT_NET_FAIL': _('Internet Disconnected'), + 'BTN_CANCEL': _('Cancel'), + 'BTN_OK': _('OK'), + 'M_CANCEL': _('Ignore'), + 'M_FMT_MAC':_('Invalid MAC address format!'), + 'PH_MAC': _('e.g., AA:BB:CC:DD:EE:FF'), + 'MSG_MULTI_WAN':'💡 ' + _('Multi-WAN detected. You can modify the account and password for each line separately (other settings remain unchanged).'), + 'MSG_WIZ_MULTI_WAN':'💡 ' + _('Multi-WAN detected. Only the primary WAN will be configured here, other lines remain unchanged.'), + 'LBL_IFACE':_('Interface:'), + 'LBL_HOSTS_LINK': '✏️ ' + _('Custom Hosts'), + 'LBL_HOSTS_TITLE': _('Custom Hosts') + '👈 ' + _('(Domain Hijack)'), + 'LBL_HOSTS_VISUAL': '💡 ' + _('Quick Add:'), + 'BTN_HOSTS_ADD': '➕ ' + _('Add'), + 'TXT_HOSTS_EMPTY': _('No custom Hosts, please add below.'), + 'PH_HOSTS_IP': _('IP (e.g., 127.0.0.1 or ::1)'), + 'PH_HOSTS_DOMAIN': _('Domain (e.g., abc.com)'), + 'PH_HOSTS_CMT': _('Comment (Optional)'), + 'LBL_HOSTS_RAW_TIP': '💡 ' + _('Pure Text Mode: Paste to import, Copy to export. Format: IP Domain #Comment'), + 'MSG_HOSTS_REQ': _('IP and Domain cannot be empty!'), + 'M_FMT_IP': _('Invalid IP address format!'), + 'M_FMT_DOMAIN': _('Invalid domain! Spaces, wildcards (*), and special characters are not allowed.'), + 'MSG_NO_CHANGE': _('No changes have been made.'), + 'M_INC_TIT': _('Notice'), + 'MSG_WAIT': _('Please wait...'), + 'MSG_HOSTS_DUP': _('This IP and Domain combination already exists!'), + 'MSG_HOSTS_DUP_RAW': _('Duplicate records found in Hosts! Please remove them before continuing.'), + 'LBL_SMART_ADD': _('Smart Auto-fill'), + 'TIP_SMART_ADD': _('Auto-fill IPv4/v6 & www domain combinations'), + 'LBL_HOSTS_DESC': '💡 ' + _('This feature forces specific domains to resolve to designated IPs. Commonly used for blocking domain access or local device redirection.'), + 'MSG_RAW_ERR_1': _('Found invalid or duplicate records:'), + 'MSG_RAW_ERR_2': _('Click [OK] to automatically discard them and continue, or [Cancel] to manually fix them.'), + // --- 插件修复急救箱 --- + 'LBL_REPAIR_BTN': '🚑 ' + _('Plugin Repair'), + 'M_REP_SCAN_TIT': _('Please wait'), + 'M_REP_SCAN_MSG': _('Scanning for repairable plugins...'), + 'M_REP_DESC': _('Standard uninstallation does not remove plugin configuration files. If a plugin malfunctions after reinstallation, select it below to reset it to its initial state.'), + 'M_REP_OPT': _('Factory Default'), + 'M_REP_TIT': '🚑 ' + _('Plugin Repair Toolkit'), + 'M_REP_OK': _('Repair Now'), + 'M_REP_PROC_TIT': _('Processing'), + 'M_REP_PROC_MSG1': _('Repairing and restarting'), + 'M_REP_SUCC_TIT': _('Repair Successful'), + 'M_REP_SUCC_MSG': _('has been successfully restored'), + 'M_REP_FAIL_TIT': _('Repair Failed'), + 'M_REP_FAIL_MSG': _('Unable to repair this plugin'), + 'M_REP_ERR_TIT': _('System Error'), + 'M_REP_ERR_MSG': _('Request timeout or error'), + 'M_HW_WAIT': _('Hardware execution takes longer, running in background...'), + 'M_REP_NOTICE_TIT': _('Notice'), + 'M_REP_EMPTY_MSG': _('No repairable plugins found'), + 'M_REP_GET_ERR': _('Failed to get plugin list'), + // --- 孤件拔除警告词条 --- + 'M_REP_GHOST_TIT': '💀 ' + _('Ghost Plugin Deletion Warning'), + 'M_REP_GHOST_MSG': _('Executing this will uninstall the software and delete its config files. However, no installation package was found, so you will need to manually reinstall it later.'), + 'BTN_ERADICATE': '🗑️ ' + _('Eradicate Completely'), + 'M_ERADICATE_PROC': _('Eradicating completely...'), + 'M_ERADICATE_SUCC': _('has been completely uninstalled and cleaned.'), + // --- 重裝指南 --- + 'M_GUIDE_TITLE': '💡 ' + _('Offline Reinstall Guide'), + 'M_GUIDE_DESC': _('To reinstall the plugin, it is highly recommended to use the [%s] feature in the Advanced Utilities panel for quick drag-and-drop setup.

    If conflicts occur or the menu is missing, you can upload the package via SSH to /etc/netwiz/custom_pkgs/ and use the Force Install command:').replace('%s', '📦 ' + _('Plugin Installation')), + 'M_GUIDE_CMD_APK': 'apk add --allow-untrusted --force-reinstall --force-overwrite /etc/netwiz/custom_pkgs/XXXXXXX_your_name.apk', + 'M_GUIDE_CMD_OPKG': 'opkg install --force-reinstall --force-overwrite /etc/netwiz/custom_pkgs/XXXXXXX_your_name.ipk', + // --------------------------- + 'M_PORT_RANGE': '⚠️ ' + _('Port number must be between 1 and 65535'), + 'M_PORT_ERR1': '⚠️ ' + _('For system security, do not use'), + 'M_PORT_ERR2': _('as the external port. It is a reserved high-risk port.'), + 'M_PORT_SUGG': _('It is recommended to use 8080 or a port above 10000.'), + 'LBL_WEB_PORT_TITLE': _('Enter custom external port number'), + 'M_PORT_CONFLICT_P1': _('Port'), + 'M_PORT_CONFLICT_P2': _('is already in use by another application!'), + // --------------------------- + 'LBL_MOD_LAYOUT': '⚙️ ' + _('Advanced Panel Management'), + 'ADV_LAYOUT_TIP': _('Drag ☰ to reorder, check to show/hide:'), + 'ADV_LAYOUT_SAVE': _('Save and Apply'), + 'ADV_SAFE_BTN': '📦 ' + _('Plugin Installation'), + 'ADV_SAFE_TITLE': _('Offline Plugin Installation'), + 'ADV_SAFE_DESC': _('Drop or click to select multiple .apk / .ipk files'), + 'ADV_ONLY_PKG': _('Security Block: Only .apk and .ipk allowed!'), + 'ADV_BTN_START': '🚀 ' + _('Start Upload & Secure'), + 'ADV_BTN_CONFIRM': '🚀 ' + _('Confirm Upload ({num} files)'), + 'ADV_BTN_PROC': _('Processing, do not close the window...'), + 'ADV_ERR_FORMAT': _('Format blocked!'), + 'ADV_MSG_ALL_DONE': '✅ ' + _('All files secured! Refreshing...'), + 'ADV_MSG_BATCH_INST': '⚙️ ' + _('All files secured, executing batch install...'), + 'ADV_MSG_INST_OK': '✅ ' + _('Installation successful! Refreshing...'), + 'ADV_MSG_HW_WAIT': '⚙️ ' + _('Hardware configuring, please wait {sec}s...'), + 'ADV_ERR_BATCH': '❌ ' + _('Batch install error: '), + 'ADV_MSG_PROC_FILE': '🚀 ' + _('Processing ({idx}/{total}): {name} - {pct}%'), + 'ADV_MSG_BG_SECURE': '⚙️ ' + _('Securing in background: {name}'), + 'ADV_ERR_MV': '❌ ' + _('Backend transfer failed: {name}'), + 'ADV_ERR_UP': '❌ ' + _('Upload failed ({code}): {name}'), + 'ADV_ERR_NET': '❌ ' + _('Network disconnected: {name}'), + 'ADV_BTN_RETRY': _('Continue to retry remaining files'), + 'ADV_SAFE_BACKUP': _('Keep a permanent backup in /etc/netwiz/custom_pkgs'), + 'ADV_BTN_REMOVE': _('Remove'), + 'ADV_ERR_SAVE_LAYOUT': _('Save layout failed'), + 'ADV_WARN_NO_WIFI': _('Warning: No Wi-Fi hardware detected, Wi-Fi configuration card is hidden.'), + 'LBL_WATCHDOG_LINK': '📡 ' + _('IPv6 Watchdog'), + 'WOG_TITLE': _('IPv6 Heartbeat Probe'), + 'WOG_ENABLE': _('Enable Advanced IPv6 Link Monitoring & Auto Recovery'), + 'WOG_URL_LBL': _('Probe Target URL'), + 'WOG_URL_PH': _('e.g., XXXXX-XXXXX.workers.dev'), + 'WOG_HELP': '
    ' + + '💡 ' + _('Note:') + '
    ' + + _('The server must parse this address and perform a live test:') + ' ' + _('Your_URL/[IPv6_Address]:Port') + '. ' + + _('If the network is accessible, it must return the plain text') + ' OK.

    ' + + '✍️ ' + _('Examples:') + '
    ' + + 'netwiz-probe.xxxx.workers.dev ' + '
    ' + + 'http://your-vps-ip:18080 ' + _('' + _('(VPS_IPv6)') + '') + '
    ' + + '🔗 ' + _('Click to view Cloudflare tutorial & source code') + '' + + '
    ', + 'MSG_WOG_LINKAGE': _('To ensure the works correctly, the following dependent features have been automatically enabled:'), + 'MSG_WOG_LINK_V6': _('IPv6 Master Switch'), + 'MSG_WOG_LINK_WAN': _('WAN Access Web UI Channel'), + 'MSG_SEC_NOTICE': _('Security Notice'), + 'MSG_WOG_OFF_WAN': _('WAN access is about to be closed. To ensure complete firewall protection, the system will simultaneously disable the IPv6 Watchdog mechanism.'), + 'MSG_DEP_NOTICE': _('Dependency Notice'), + 'MSG_WOG_OFF_V6_ALL': _('You have disabled IPv6. To ensure security, the dependent IPv6 Watchdog and WAN Access will be automatically disabled.'), + 'ERR_EMPTY_URL': _('Probe URL cannot be empty, please fill it in before saving!'), + 'U_NEW_TITLE': '✨ ' + _('New Version Found'), + 'U_READY_MSG': _('Ready to install the update?'), + 'U_BTN_NOW': _('Update Now'), + 'U_BTN_LATER': _('Later'), + 'U_UPGRADING_TITLE': '🔄 ' + _('System Upgrading'), + 'U_UPGRADING_MSG': _('Downloading and replacing core files, please do not power off...'), + 'U_UPGRADING_WAIT': '⏳ ' + _('Forcing system file overwrite... (Est. {sec}s remaining)'), + 'U_INST_SUCC_TIT': '🎉 ' + _('Upgrade Successful!'), + 'U_INST_SUCC_MSG': _('The new version core has been deployed in the background!'), + 'U_INST_SUCC_DESC': '⚠️ ' + _('Notice: Aggressive browser caching detected.
    To ensure new menus and features display correctly, please press Ctrl + F5 or Shift + F5 to force refresh this page!
    (Mobile users: please manually clear browser cache and refresh)'), + 'MSG_ABOUT_TO_ENABLE': _('You are about to enable the [%s].'), + 'MSG_ABOUT_TO_DISABLE': _('You are about to disable the [%s].'), + 'MSG_SUGGESTION': _('Suggestion:'), + 'MSG_WOG_SUGGEST_MSG': _('No target URL is configured for the Watchdog. For long-term stability, it is recommended to manually configure and enable it in %s later.'), + + 'BTN_GENERATE_BACKUP': _('Backup Config'), + 'BTN_RESTORE_SYSTEM': _('Restore Config'), + // ========================================== + // 备份相关 + // ========================================== + 'M_BAK_ING_TIT': _('Backing up'), + 'M_BAK_ING_MSG': _('Precisely packing %s config and generating download, please wait...'), + 'M_BAK_SUCC_MSG': _('The file contains core assets and is downloading in the background. Please check your browser!'), + 'M_BAK_FAIL_TIT': _('Backup Failed'), + 'M_BAK_FAIL_MSG': _('Config not found or packaging error. Please check if the plugin is installed.'), + + // ========================================== + // 恢复相关 + // ========================================== + 'M_RST_CONF_TIT': _('Restore Configuration Confirm'), + 'M_RST_CONF_WARN': _('Warning: Restoring the configuration will overwrite all current settings and automatically restart the router upon completion!'), + 'M_SEL_FILE': _('Selected file: %s'), + 'BTN_RST_START': _('Confirm Restore'), + 'M_RST_CONF_TIT_ING': _('Restoring Configuration'), + 'MSG_RESTORE_UPLOADING': _('Uploading backup file, please do not disconnect power...'), + 'M_RST_DELIVERED': _('File delivered, starting restore...'), + 'MSG_REBOOTING': _('System is restarting...'), + 'M_RST_CMD_SENT': _('Restore command issued, device may be restarting...'), + 'M_RST_ING_TIT': _('Restoring'), + 'M_RST_ING_MSG': _('Uploading and restoring %s config...'), + 'M_RST_SUCC_TIT': _('Restore Successful'), + 'M_RST_SUCC_MSG': _('%s config file has been restored and reloaded!'), + 'M_RST_FAIL_TIT': _('Restore Failed'), + 'M_RST_FAIL_MSG': _('Failed to extract or restart service.'), + 'M_RELOAD': _('Reload Page'), + + // ========================================== + // 网络与报错相关 + // ========================================== + 'M_REQ_FAIL_TIT': _('Request Failed'), + 'M_REQ_FAIL_MSG': _('API request failed: %s'), + 'M_UPL_FAIL_TIT': _('Upload Failed'), + 'M_UPL_FAIL_MSG': _('File upload rejected, status code: %s'), + 'M_UPL_FAIL_STAT': _('Upload failed: %s'), + 'M_SYS_ERR': _('Error'), + 'M_RST_NET_ERR': _('Network Error'), + 'M_RST_NET_INTR': _('Network connection unexpectedly interrupted!'), + + 'M_FMT_ERR_TIT': _('Format Error'), + 'M_FMT_ERR_MSG': _('Please upload a valid .tar.gz backup file!'), + 'M_WARN_TIT': _('Warning'), + 'M_WARN_MSG': _('The selected file does not seem to belong to %s.
    File name: %f
    Are you sure you want to continue?'), + 'BTN_FORCE_RST': _('Force Restore'), + 'M_ERR_FILE_CORRUPT': _('The uploaded file is corrupted or not a valid archive!'), + 'M_ERR_FILE_MISMATCH': _('The archive content does not match %s!'), + 'M_RST_SUCC_MSG': _('%s config restored! Service is restarting in the background...'), + 'M_AUTO_RELOAD': _('Auto reloading in %s seconds...'), + 'M_ERR_EXTRACT': _('Failed to extract files to the system! (Storage full or permission denied)'), + }, + + // 2. HTML 结构 + html: ` + + + +
    + +
    +
    +
    {{TITLE}}{{APP_VERSION}}
    +
    +

    {{SUBTITLE}}

    +
    {{WIZ_REOPEN}}
    +
    + + + +
    +
    +
    +
    {{MODE_PPPOE_TITLE}}
    {{MODE_PPPOE_DESC}}
    + +
    +
    {{MODE_ROUTER_TITLE}}
    {{MODE_ROUTER_DESC}}
    +
    +
    {{MODE_LAN_TITLE}}
    {{MODE_LAN_DESC}}
    +
    +
    +
    {{LOADING_CONFIG}}
    +
    +
    +
    +
    + {{LBL_ADV_UTILS_TITLE}} +
    ⚙️
    +
    + +
    +
    +
    {{LBL_WEB_ACCESS_TOGGLE}}
    + +
    + +
    +
    + +
    +
    + 🧪 + {{LBL_LAB_TITLE}} +
    + {{LBL_LAB_BETA}} +
    +
    +
    +
    {{TXT_FULL_BACKUP_TIT}}
    +
    + {{TXT_FULL_BACKUP_DESC}} +
    + if wget -qO /tmp/nw_inst.sh https://raw.githubusercontent.com/huchd0/luci-app-netwiz/master/install.sh; then sh /tmp/nw_inst.sh; else sh /etc/netwiz/custom_pkgs/install.sh; fi + {{TXT_COPY_TIP}} +
    +
    +
    +
    + + + +
    +
    +
    +
    + + + + `, // 这里有逗号 + + // 3. 外挂修复弹窗的HTML骨架 (下拉选单版本) + repairModalHtml: ` +
    +

    + {{M_REP_DESC}} +

    + + + +
    + + +
    + + +
    + {{M_GUIDE_TITLE}}
    + {{M_GUIDE_DESC}}
    +
    + {{CMD_CODE}} +
    +
    +
    + `, // 这里有逗号,为了连接后面的 layoutModalHtml + + // 面板排版管理器的 HTML 骨架 + layoutModalHtml: ` +
    +
    + {{ADV_LAYOUT_TIP}} +
    +
      + {{LAYOUT_LIST_ITEMS}}
    +
    + `,// 这里有逗号 + + // 离线保险箱 (拖拽上传) 的 HTML 骨架 + offlineSafeModalHtml: ` +
    +
    📦
    +
    {{ADV_SAFE_TITLE}}
    +
    {{ADV_SAFE_DESC}}
    + +
    +
    +
    + +
    +
    + +
    +
    + ` +}); diff --git a/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/qrcode.min.js b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/htdocs/luci-static/resources/view/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
    "),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/luci-app-netwiz/luci-app-netwiz/po/zh_Hans/netwiz.po b/luci-app-netwiz/luci-app-netwiz/po/zh_Hans/netwiz.po new file mode 100644 index 000000000..938d25d99 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/po/zh_Hans/netwiz.po @@ -0,0 +1,2353 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "Network Wizard" +msgstr "网络向导" + +msgid "Netwiz NETWORK SETUP" +msgstr "Netwiz 网络设置向导" + +msgid "Pure · Secure · Non-destructive Minimalist Config" +msgstr "纯粹 · 安全 · 无损的极简配置" + +msgid "DHCP / Static IP (WAN)" +msgstr "动态 / 静态 IP (WAN)" + +msgid "Automatically obtain IP from the upstream network, or manually set a static IP." +msgstr "自动从上级网络(或光猫)获取 IP,或手动设置固定 IP。" + +msgid "PPPoE Dial-up" +msgstr "PPPoE 拨号 (WAN)" + +msgid "Dial up directly using account and password on this device." +msgstr "在本设备上直接使用宽带账号和密码进行拨号。" + +msgid "LAN Settings" +msgstr "局域网设置 (LAN)" + +msgid "Change device LAN IP, switch to AP Wired Relay mode, or one-click IPv6 distribution." +msgstr "修改设备局域网 IP,或切换至旁路由(AP有线中继)模式,一键下发IPv6。" + +msgid "Reading underlying config..." +msgstr "正在读取底层配置..." + +msgid "Back to Home" +msgstr "返回首页" + +msgid "Configure WAN" +msgstr "设置外网 (WAN)" + +msgid "Connection Type" +msgstr "连接方式" + +msgid "DHCP (Auto)" +msgstr "自动获取 IP (DHCP)" + +msgid "Static IP" +msgstr "静态 IP" + +msgid "Gateway" +msgstr "网关" + +msgid "e.g., 192.168.1.2" +msgstr "例如:192.168.1.2" + +msgid "e.g., 192.168.1.1" +msgstr "例如:192.168.1.1" + +msgid "PPPoE Credentials" +msgstr "PPPoE 拨号信息 (WAN)" + +msgid "PPPoE Username" +msgstr "宽带账号" + +msgid "Enter PPPoE username" +msgstr "请输入宽带账号" + +msgid "PPPoE Password" +msgstr "宽带密码" + +msgid "Enter PPPoE password" +msgstr "请输入宽带密码" + +msgid "Configure LAN" +msgstr "设置局域网 (LAN)" + +msgid "Safe Mode (Recommended ON)" +msgstr "安全模式 (建议开启,防失联)" + +msgid "Enable AP Wired Relay" +msgstr "启用旁路由(AP有线中继)模式" + +msgid "AP Wired Relay Enabled:
    1. DHCP will be disabled. Devices must use static IPs or get IPs from upstream.
    2. If LAN IP changes, ensure your client is in the same subnet to avoid losing access." +msgstr "AP 有线中继模式已开启:
    1. DHCP 将被禁用。设备必须使用静态 IP 或由上级路由分配 IP。
    2. 若修改了局域网 IP,请确保设备与新 IP 处于同一网段,以免防失联回滚触发。" + +msgid "Main Router Mode Enabled:
    1. DHCP will be enabled. This device assigns IPs.
    2. If LAN IP changes, ensure your client is in the same subnet to avoid losing access." +msgstr "主路由模式已开启:
    1. DHCP 服务将开启,由本设备负责分配局域网 IP。
    2. 若修改了局域网 IP,请确保设备与新 IP 处于同一网段,以免防失联回滚触发。" + +msgid "Device LAN IP" +msgstr "设备局域网 IP" + +msgid "LAN Gateway" +msgstr "局域网网关" + +msgid "Auto Detect" +msgstr "一键探测" + +msgid "Upstream subnet detected, recommended IP assigned" +msgstr "已获取上级网段,并为您分配推荐 IP" + +msgid "Detection failed, cannot get upstream gateway" +msgstr "探测失败,无法获取到上级网关信息" + +msgid "Invalid Operation" +msgstr "操作无效" + +msgid "In Main Router mode, LAN Gateway MUST be empty.

    \"Auto Detect\" is only for AP/Relay mode!" +msgstr "当前为主路由模式,局域网网关必须留空。

    “一键探测”仅用于配置“旁路由/AP有线中继”模式!" + +msgid "Critical Intercept" +msgstr "极度危险拦截" + +msgid "System detected WAN is PPPoE Dial-up.

    The acquired {gw} is the ISP BRAS gateway!
    NEVER set this as LAN gateway, or your network will crash!" +msgstr "系统检测到当前外网为 PPPoE 拨号。

    获取到的 {gw} 是运营商机房的远程基站网关!
    绝不能将其填入局域网中,否则将导致网络瘫痪!" + +msgid "Blank for Main, required for AP Wired Relay" +msgstr "主路由留空,旁路由(有线中继)必填" + +msgid "Back" +msgstr "上一步" + +msgid "Next Step" +msgstr "下一步" + +msgid "Back to Edit" +msgstr "返回修改" + +msgid "Confirm Configuration" +msgstr "确认配置信息" + +msgid "The following settings will be applied, please verify:" +msgstr "即将应用以下网络配置,请仔细核对:" + +msgid "Application Notes:" +msgstr "应用须知:" + +msgid "After confirmation, the network will restart and apply new settings." +msgstr "确认后,系统将重启网络服务以应用新配置。" + +msgid "The system will auto-refresh or redirect in 15 seconds." +msgstr "大约 15 秒后,页面将自动刷新或跳转。" + +msgid "Apply Settings" +msgstr "确认并应用" + +msgid "AP Wired Relay" +msgstr "旁路由(AP有线中继)模式" + +msgid "Current:" +msgstr "当前:" + +msgid "PPPoE Dial-up" +msgstr "宽带拨号 (PPPoE)" + +msgid "DHCP Client (Auto IP)" +msgstr "自动获取 (DHCP)" + +msgid "Static IP" +msgstr "静态固定 IP (Static)" + +msgid "LAN Mode" +msgstr "局域网模式" + +msgid "Device IP:" +msgstr "设备 IP:" + +msgid "Upstream GW:" +msgstr "上级网关:" + +msgid "Public IP:" +msgstr "公网 IP:" + +msgid "Remote GW:" +msgstr "远程网关:" + +msgid "LAN IP:" +msgstr "局域网 IP:" + +msgid "Status:" +msgstr "状态:" + +msgid "Waiting for remote response" +msgstr "等待远程响应..." + +msgid "WAN IP:" +msgstr "外网 IP:" + +msgid "Getting IP..." +msgstr "正在获取 IP..." + +msgid "DHCP Service:" +msgstr "DHCP 服务:" + +msgid "Enabled" +msgstr "已开启" + +msgid "Disabled" +msgstr "已停用" + +msgid "Dial Success" +msgstr "拨号成功" + +msgid "Dialing / Disconnected" +msgstr "拨号中 / 未连接" + +msgid "IP Acquired" +msgstr "已获取 IP" + +msgid "Waiting for IP..." +msgstr "等待分配 IP..." + +msgid "Interface Connected" +msgstr "接口已连接" + +msgid "Cable Unplugged" +msgstr "网线未插入" + +msgid "Getting..." +msgstr "获取中..." + +msgid "Not acquired" +msgstr "未获取" + +msgid "Not set" +msgstr "未设置" + +msgid "Incomplete info" +msgstr "信息不完整" + +msgid "Device IP cannot be empty." +msgstr "设备 IP 不能为空。" + +msgid "Static IP and Gateway cannot be empty." +msgstr "静态 IP 和网关不能为空。" + +msgid "PPPoE username and password cannot be empty." +msgstr "宽带账号和密码不能为空。" + +msgid "Format Error" +msgstr "格式错误" + +msgid "The device IP is invalid, please check!" +msgstr "设备 IP 格式不正确,请检查!" + +msgid "WAN IP is invalid, please check!" +msgstr "外网 IP 格式不正确,请检查!" + +msgid "Gateway IP is invalid, please check!" +msgstr "网关 IP 格式不正确,请检查!" + +msgid "Logic Error" +msgstr "逻辑错误" + +msgid "AP Wired Relay requires an upstream gateway IP." +msgstr "旁路由(AP有线中继)模式下,网关为必填项。" + +msgid "WAN Static IP MUST NOT be the same as the gateway!" +msgstr "WAN 静态 IP 绝对不能和网关一模一样!" + +msgid "In AP/Relay mode, the device IP MUST NOT be the exact same as the Gateway! (" +msgstr "【旁路由 / 有线中继 (AP)】模式下,本设备 IP 绝对不能与网关完全一样但网段需要相同!(" + +msgid "Tip: The Gateway should be the LAN IP of your upstream main router, and this device needs its own unique IP)" +msgstr "提示:网关请填写上级主路由的 LAN 口 IP 地址)" + +msgid "No Changes Needed" +msgstr "未发生更改" + +msgid "Your settings match the current router config exactly." +msgstr "您的设置与底层配置完全一致,无需重复下发。" + +msgid "Exit to Home" +msgstr "返回首页" + +msgid "Conflict Blocked" +msgstr "配置冲突拦截" + +msgid "The WAN IP cannot be the same as the current LAN IP ({ip})!" +msgstr "WAN 口 IP 不能与当前局域网 IP ({ip}) 相同!" + +msgid "The WAN port cannot be in the same subnet as the LAN ({ip})!" +msgstr "WAN 口不能与局域网 ({ip}) 处于同一网段!" + +msgid "This causes a routing loop." +msgstr "这会导致严重的路由死循环。" + +msgid "Suggestion: Your upstream network uses the same IP subnet. Please go to [LAN Settings] first and change your Device LAN IP (e.g. to 192.168.10.1) to prevent network crash." +msgstr "建议:您的上级网络使用了相同的 IP 网段。请先前往【局域网设置】,修改本设备的局域网 IP(例如改为 192.168.10.1)以防止网络瘫痪。" + +msgid "Subnet Error" +msgstr "网段错误" + +msgid "The WAN Static IP must be in the same subnet as the Gateway!" +msgstr "WAN 静态 IP 必须与网关处于同一网段!" + +msgid "e.g., if gateway is {gw}, the IP must be {ip}.x" +msgstr "例如:如果网关是 {gw},则 IP 必须是 {ip}.x" + +msgid "In AP/Relay mode, the device IP must be in the same subnet as the Gateway! (" +msgstr "【旁路由 / 有线中继 (AP)】模式下,本设备 IP 必须与网关处于同一网段!(" + +msgid "Tip: The Gateway should be the LAN IP of your upstream main router)" +msgstr "提示:网关请填写上级主路由的 LAN 口 IP 地址)" + +msgid "Config Warning" +msgstr "高危操作警告" + +msgid "You selected [Main Router Mode] but filled in the [Gateway].

    For a standard main router, the gateway must be blank. Entering a gateway may cause the device to fail at distributing network, leading to a total outage!

    Are you sure you want to do this?" +msgstr "您选择了【主路由模式】,但填写了【网关】。

    作为主路由,网关必须留空。强行指定网关极易导致底层路由表错乱,全屋断网!

    您确定要继续强行保存吗?" + +msgid "Force Apply" +msgstr "确认强行保存" + +msgid "System Exception" +msgstr "系统异常" + +msgid "Cannot read underlying config for validation, please refresh." +msgstr "无法读取底层配置进行防呆校验,请刷新页面重试。" + +msgid "Applying Configuration" +msgstr "正在应用配置" + +msgid "Close" +msgstr "关闭" + +msgid "Account" +msgstr "账号" + +msgid "Password" +msgstr "密码" + +msgid "IP & Gateway" +msgstr "IP 与网关" + +msgid "Auto-assigned by upstream router" +msgstr "由上级路由自动分配" + +msgid "Target:" +msgstr "目标:" + +msgid "Modifying LAN IP" +msgstr "正在修改局域网 IP" + +msgid "Switching to AP Wired Relay" +msgstr "正在切换至旁路由(AP有线中继)模式" + +msgid "Switching WAN to DHCP" +msgstr "正在切换 WAN 口为 DHCP (自动获取)" + +msgid "Switching WAN to Static IP" +msgstr "正在切换 WAN 口为静态 IP" + +msgid "Applying PPPoE Dial-up" +msgstr "正在应用 PPPoE 拨号" + +msgid "Writing configuration to system, please do not close the page..." +msgstr "正在将配置写入系统,请勿关闭页面..." + +msgid "Connecting to new IP... Config will auto-rollback upon timeout." +msgstr "正在尝试连接新 IP... 失联超时将会自动回滚配置" + +msgid "Waiting for network service to restart... Elapsed: {sec}s" +msgstr "正在等待网络服务重启... 已耗时:{sec}秒" + +msgid "Waiting for router to safely restore... Elapsed: {sec}s" +msgstr "正在等待旧 IP 恢复... 已耗时:{sec}秒" + +msgid "Rollback countdown: {sec} / {total} s" +msgstr "防失联倒计时:{sec} / {total} 秒" + +msgid "If enabled, the system will auto-revert if you lose connection within 120s." +msgstr "开启后,若修改 IP 导致失联,系统将在 120 秒后自动回退恢复。" + +msgid "Safe mode disabled. Applying immediately without rollback protection..." +msgstr "已关闭防失联保护,配置立刻生效。" + +msgid "If IP changed, please update PC IP. Auto-redirecting when connected..." +msgstr "若 IP 修改引发失联,将设备 IP 改为同网段即可正常访问路由器,网络连通后将自动跳转…" + +msgid "Waiting for router to abort changes and restore network..." +msgstr "正在等待路由器放弃修改并恢复网络..." + +msgid "Wi-Fi Settings" +msgstr "Wi-Fi 设置" + +msgid "Set Wi-Fi name, password, enable Wireless Relay (WISP) and Seamless Roaming." +msgstr "设置WiFi帐号、密码,开启无线中继 (WISP),开启路由间无缝漫游。" + +msgid "Configure Wi-Fi" +msgstr "设置 Wi-Fi" + +msgid "Smart Connect (All Bands)" +msgstr "多频合一 (智能漫游)" + +msgid "Enable Wi-Fi" +msgstr "启用 Wi-Fi" + +msgid "Enable 2.4G Wi-Fi" +msgstr "启用 2.4G Wi-Fi" + +msgid "Enable 5G Wi-Fi" +msgstr "启用 5G Wi-Fi" + +msgid "e.g., My_WiFi" +msgstr "例如:My_WiFi" + +msgid "Network Name (SSID)" +msgstr "无线网络名称 (SSID)" + +msgid "Wi-Fi Password" +msgstr "Wi-Fi 密码" + +msgid "Encryption" +msgstr "加密方式" + +msgid "Enable 802.11b (Legacy Mode)" +msgstr "启用 802.11b (旧版兼容模式)" + +msgid "Only enable if very old IoT devices cannot connect." +msgstr "仅在古董智能家电/物联网设备无法连接时才开启(正常不建议开启)。" + +msgid "No Password (Open)" +msgstr "无密码 (开放)" + +msgid "WPA2-PSK (Stable)" +msgstr "WPA2-PSK (稳定兼容)" + +msgid "WPA3-SAE (Secure)" +msgstr "WPA3-SAE (高安全性)" + +msgid "WPA2/WPA3 Mixed (Recommended)" +msgstr "WPA2/WPA3 混合 (推荐)" + +msgid "2.4G Wi-Fi" +msgstr "2.4G Wi-Fi" + +msgid "5G Wi-Fi" +msgstr "5G Wi-Fi" + +msgid "SSID cannot be empty." +msgstr "网络名称 (SSID) 不能为空。" + +msgid "Wi-Fi password must be at least 8 characters." +msgstr "Wi-Fi 密码长度必须至少为 8 个字符。" + +msgid "Applying Wi-Fi Settings" +msgstr "正在应用 Wi-Fi 设置" + +msgid "Advanced Settings" +msgstr "高级设置" + +msgid "Hide Advanced" +msgstr "收起高级设置" + +msgid "Hide Wi-Fi Name (SSID)" +msgstr "隐藏 Wi-Fi 名称" + +msgid "Channel" +msgstr "信道 (Channel)" + +msgid "Channel Width" +msgstr "频宽 (Channel Width)" + +msgid "Auto" +msgstr "自动" + +msgid "Wireless Mode" +msgstr "无线模式 (Mode)" + +msgid "Severe Warning" +msgstr "严重警告" + +msgid "Forcibly modifying the wireless physical mode may cause the hardware driver to crash or the Wi-Fi to disappear permanently if the chip does not support it! It is highly recommended to keep it on [Auto].

    Are you absolutely sure you want to change this?" +msgstr "如果强行指定的物理模式与网卡芯片实际支持的能力不匹配,将导致底层驱动直接崩溃、Wi-Fi 永久消失!强烈建议保持为【自动 (推荐)】。

    您确定要强行修改模式吗?" + +msgid "Enable IPv6 (DHCPv6)" +msgstr "启用 IPv6 (DHCPv6)" + +msgid "Wi-Fi Status" +msgstr "Wi-Fi 状态" + +msgid "5G Wi-Fi Account" +msgstr "5G Wi-Fi 账号" + +msgid "2.4G Wi-Fi Account" +msgstr "2.4G Wi-Fi 账号" + +msgid "No Password" +msgstr "无密码" + +msgid "Enable Wireless Relay (WISP)" +msgstr "启用无线中继 (WISP)" + +msgid "Receive upstream Wi-Fi and broadcast your own network." +msgstr "接收上级 Wi-Fi 信号,并广播您自己的专属网络。" + +msgid "WISP Enabled" +msgstr "已开启无线中继" + +msgid "Scan Nearby Wi-Fi" +msgstr "扫描附近 Wi-Fi" + +msgid "Select Upstream Network" +msgstr "选择上级网络" + +msgid "Password for upstream:" +msgstr "上级 Wi-Fi 密码:" + +msgid "Scanning..." +msgstr "正在扫描..." + +msgid "No networks found." +msgstr "未找到任何网络。" + +msgid "Scan failed. Driver might be busy." +msgstr "扫描失败。无线驱动可能繁忙,请重试。" + +msgid "Target Wi-Fi" +msgstr "目标 Wi-Fi 名称" + +msgid "Upstream Wi-Fi Password" +msgstr "请输入上级 Wi-Fi 的密码" + +msgid "802.11k/v/r Fast Roaming" +msgstr "802.11k/v/r 无缝漫游" + +msgid "Enable seamless roaming between routers with one click (Prerequisite: Same SSID, password, and LAN). Note: May cause connection issues with older smart home (IoT) devices." +msgstr "顺滑切换不同路由器间WiFi信号(需同Wi-Fi名和密码 + 同局域网、唯一DHCP)。注意:可能老旧智能家居(IoT)无法连接。" + +msgid "Manual Configuration Warning" +msgstr "手动配置警告" + +msgid "Underlying parameter mismatch detected, which may cause roaming failures. Please toggle this switch off and on again, then save to apply the standard seamless roaming profile." +msgstr "检测到底层参数与预设不一致,可能会导致漫游失败。建议将开关关闭后重新开启,保存后即可加入多设备全空间「丝滑」跳转的“朋友圈”。" + +msgid "Roaming" +msgstr "漫游" + +msgid "Roaming Enabled" +msgstr "已开通漫游" + +msgid "Click to Fix" +msgstr "点击修复" + +msgid "Click to Settings" +msgstr "点击直达设置" + +msgid "WAN Blind-Switch: Unplug the WAN cable for 10 seconds and reconnect to auto-detect and switch the connection type (takes about 2 mins)." +msgstr "盲切WAN口上网方式:断开 WAN 网线 10 秒再插回,系统会自动检测并切换上网方式(过程约2分钟)" + +msgid "New Config" +msgstr "新建" + +msgid "Modified" +msgstr "新修改" + +msgid "Security Warning" +msgstr "无密码安全警告" + +msgid "You are setting up an Open Wi-Fi network without a password. Anyone nearby will be able to connect and access your network.

    Are you sure you want to continue?" +msgstr "您正在设置无密码的开放 Wi-Fi,附近任何人都可以随意连接并访问您的局域网设备。

    您确定要继续吗?" + +msgid "Set your wireless network name and password." +msgstr "设置您的无线网络名称和密码。" + +msgid "Skip this time" +msgstr "跳过本次引导" + +msgid "Don't show this again" +msgstr "不再提示" + +msgid "Reopen Wizard" +msgstr "初始向导" + +msgid "Skip Wi-Fi Setup (Keep current)" +msgstr "暂不配置 Wi-Fi (保持现状)" + +msgid "Not configured (Keep current)" +msgstr "暂不配置 (保持当前状态)" + +msgid "Not set" +msgstr "未设置" + +msgid "No Password (Open)" +msgstr "无密码 (Open)" + +msgid "Wizard execution failed" +msgstr "向导执行失败" + +msgid "Configuration data processing failed" +msgstr "配置数据处理失败" + +msgid "Input validation failed" +msgstr "输入校验失败" + +msgid "Device Network Manager" +msgstr "设备网络管家" + +msgid "Terminal Device Monitoring & Static IP Management" +msgstr "终端设备状态监控与固定 IP 管理" + +msgid "Rescan Network" +msgstr "重新扫描网络" + +msgid "Refresh" +msgstr "刷新" + +msgid "Syncing underlying network data..." +msgstr "正在同步底层网络数据..." + +msgid "All" +msgstr "全部" + +msgid "Mobile/Tablet" +msgstr "手机平板" + +msgid "PC/Work" +msgstr "电脑办公" + +msgid "Smart Home" +msgstr "智能家居" + +msgid "Others" +msgstr "其它" + +msgid "Select all available devices" +msgstr "全选可用设备" + +msgid "Selected" +msgstr "已选择" + +msgid "items" +msgstr "项" + +msgid "Batch Unbind" +msgstr "批量解绑" + +msgid "Batch Assign" +msgstr "批量分配" + +msgid "Device Alias" +msgstr "设备备注名" + +msgid "e.g., John's PC" +msgstr "例如: 小明的电脑" + +msgid "Quick Assign Strategy (Click to auto-fill)" +msgstr "快速分配策略 (点击自动计算填入)" + +msgid "Keep Original IP" +msgstr "保持原 IP" + +msgid "Smart Categorization" +msgstr "智能分类" + +msgid "Sequential Memory" +msgstr "记忆顺延" + +msgid "Assign Static IP (Auto anti-conflict)" +msgstr "分配固定 IP (自动防冲突)" + +msgid "e.g., 192.168.1.50" +msgstr "例如: 192.168.1.50" + +msgid "Select Assign Strategy" +msgstr "选择分配策略" + +msgid "Keep Current IP (Recommended)" +msgstr "保持当前 IP (安全推荐)" + +msgid "Convert the device's current random IP to a permanent static IP" +msgstr "直接将设备现在的随机 IP 转为永久固定" + +msgid "Smart Subnet (By Device Type)" +msgstr "智能分段 (按设备类型)" + +msgid "Auto-identify mobile/PC, assign to exclusive subnets" +msgstr "自动识别手机/电脑等,分别放入专属网段" + +msgid "Specify Starting IP (Sequential)" +msgstr "指定起始 IP (顺延分配)" + +msgid "Enter starting suffix, sequentially assign to all selected devices" +msgstr "填写起始后缀,所有选中设备依次顺延分配" + +msgid "Starting IP" +msgstr "分配起始 IP" + +msgid "Others:" +msgstr "其它设备:" + +msgid "Mobile/Tablet:" +msgstr "手机平板:" + +msgid "PC/Work:" +msgstr "电脑办公:" + +msgid "Smart Home:" +msgstr "智能家居:" + +msgid "Cancel" +msgstr "取消" + +msgid "OK" +msgstr "确定" + +msgid "No device records in this category" +msgstr "该分类下没有设备记录" + +msgid "Online" +msgstr "在线" + +msgid "Offline" +msgstr "离线" + +msgid "Unoperable" +msgstr "不可操作" + +msgid "Long-term Offline" +msgstr "长期离线" + +msgid "Static" +msgstr "静态" + +msgid "Upstream Gateway" +msgstr "上级网关" + +msgid "Local System" +msgstr "本机系统" + +msgid "System Core Route" +msgstr "系统核心路由" + +msgid "System Reserved Device" +msgstr "系统保留设备" + +msgid "Edit" +msgstr "修改" + +msgid "Unbind" +msgstr "解绑" + +msgid "Quick Bind" +msgstr "一键固定" + +msgid "Unknown Device" +msgstr "未知设备" + +msgid "Unknown IP" +msgstr "未知IP(无外网)" + +msgid "Terminal Addressed IP" +msgstr "终端自签IP" + +msgid "Edit Device Info" +msgstr "修改设备信息" + +msgid "Quick Bind IP" +msgstr "一键固定 IP" + +msgid "Configuring MAC:" +msgstr "正在为 MAC: " + +msgid "Save Changes" +msgstr "保存修改" + +msgid "Bind Device" +msgstr "绑定此设备" + +msgid "Safely writing and restarting services..." +msgstr "正在安全写入并无感重启服务..." + +msgid "Unbind IP" +msgstr "解除 IP 绑定" + +msgid "Are you sure you want to unbind the static IP for device ({mac})?" +msgstr "确定要解除对该设备 ({mac}) 的静态 IP 绑定吗?" + +msgid "Confirm Unbind" +msgstr "确认解绑" + +msgid "Releasing static binding..." +msgstr "正在释放静态绑定..." + +msgid "Batch Unbind Devices" +msgstr "批量解绑设备" + +msgid "Among the {total} selected devices, {valid} have fixed IPs.

    Are you sure you want to unbind them?" +msgstr "选中的 {total} 台设备中,有 {valid} 台当前已固定 IP。

    确定要解除它们的绑定吗?" + +msgid "Batch releasing IPs, please wait..." +msgstr "正在批量释放 IP,请稍候..." + +msgid "Batch Assign Settings" +msgstr "批量分配设置" + +msgid "Select Assign Strategy (Selected {count} devices)" +msgstr "选择分配策略 (已选中 {count} 台设备)" + +msgid "Start Assign" +msgstr "开始分配" + +msgid "Avoiding conflicts and silently writing rules..." +msgstr "正在雷达避冲突并静默写入规则..." + +msgid "No device records found in current LAN" +msgstr "当前局域网内未发现任何设备记录" + +msgid "Scan failed: Cannot retrieve underlying data" +msgstr "扫描失败:无法获取底层数据" + +msgid "The IP pool for this type is full. Please expand the range or choose another strategy!" +msgstr "该类型的 IP 分段池已全部被占满,请扩大范围或选择其它策略!" + +msgid "IP address cannot be empty!" +msgstr "IP 地址不能为空!" + +msgid "IP Conflict Blocked!" +msgstr "IP 冲突拦截!" + +msgid "The IP [{ip}] is occupied by device [{name}]." +msgstr "您输入的 IP [{ip}] 已被局域网中的设备【{name}】占用。" + +msgid "Please modify the strategy or enter a free IP!" +msgstr "请修改策略或输入一个未被占用的空闲 IP!" + +msgid "Tip: The device is already fixed at this IP. No changes needed!" +msgstr "提示:该设备已经是当前 IP 且已固定,配置未发生任何改变,无需重复提交!" + +msgid "Tip: The {count} selected devices are all [Unbound]. No unbind action needed!" +msgstr "提示:您选中的 {count} 台设备目前全是【未绑定】状态,无需执行解绑操作!" + +msgid "Starting suffix must be between 2 and 254!" +msgstr "起始后缀必须在 2 到 254 之间!" + +msgid "IP pool insufficient!" +msgstr "IP 资源池不足!" + +msgid "Only {avail} IPs left from .{suf}, but {count} devices selected." +msgstr "从 .{suf} 开始仅剩 {avail} 个可用 IP,但您选了 {count} 台设备。" + +msgid "[{name}] assignment failed!" +msgstr "[{name}] 分配失败!" + +msgid "Selected {req} devices, but only {avail} IPs available in the preset range." +msgstr "选中了 {req} 台设备,但预设网段内仅剩 {avail} 个可用 IP。" + +msgid "Please expand the IP range for this category!" +msgstr "请扩大该分类的 IP 范围!" + +msgid "Smart block: According to your strategy, the {count} selected devices are already fixed with unchanged IPs. No need to rewrite rules!" +msgstr "智能拦截:根据您的策略计算,选中的 {count} 台设备【均已固定且 IP 未改变】,无需重复下发规则!" + +msgid "Infinite" +msgstr "永久" + +msgid "Expired" +msgstr "已过期" + +msgid "Terminal Device & IP Binding" +msgstr "终端设备与 IP 绑定" + +msgid "Pending" +msgstr "待生效" + +msgid "Device Type (By IP)" +msgstr "类型(按IP)" + +msgid "Device Type (Built-in)" +msgstr "类型(内置)" + +msgid "Custom Groups" +msgstr "自定义组" + +msgid "Filter by IP Subnet" +msgstr "按 IP 网段归类" + +msgid "Checked: Classify device types by IP subnet" +msgstr "勾选:依据 IP 网段来对设备分类" + +msgid "Unchecked: Classify device types by built-in names" +msgstr "取消:依据内置名称来对设备分类" + +msgid "Alias" +msgstr "分组名称" + +msgid "Group " +msgstr "分组 " + +msgid "Save Failed!" +msgstr "保存失败!" + +msgid "Reason: RPC interface unresponsive ({err})" +msgstr "原因:RPC 接口无响应 ({err})" + +msgid "Please run this command in router SSH to restart the service: /etc/init.d/rpcd restart" +msgstr "请在路由器 SSH 运行此命令重启服务:/etc/init.d/rpcd restart" + +msgid "Subnet Conflict: IP ranges between groups cannot overlap!" +msgstr "网段冲突:不同分组间的 IP 范围不能有重叠!" + +msgid "Conflicting groups: {groups}" +msgstr "冲突组:{groups}" + +msgid "Save Failed: Group names cannot be duplicated!" +msgstr "保存失败:分组名称不能重复!" + +msgid "Duplicate name: {name}" +msgstr "重复名称:{name}" + +msgid "Save Failed: IPs must be between 2-254 and format must be valid!" +msgstr "保存失败:IP 必须在 2-254 之间,且存在格式错误!" + +msgid "IP pool reached the end (254). Cannot auto-append. Please arrange subnets manually!" +msgstr "IP 段已分配到末尾 (达到254),无法继续自动追加,请手动整理网段!" + +msgid "Visiting" +msgstr "正在访问" + +msgid "Department Network Segments" +msgstr "自定义组IP选号池" + +msgid "+ Add New Department" +msgstr "+ 新建分组" + +msgid "Group Pool (Auto Assign)" +msgstr "自定义分组网段 (自动发号)" + +msgid "Assign free IPs automatically from the selected Target Group's specific IP range" +msgstr "自动从下方选择的“目标分组”所设定的专属 IP 段中提取空闲 IP 进行分配" + +msgid "Target Department" +msgstr "目标分组" + +msgid "Group IP Pool" +msgstr "自定义分组专属网段" + +msgid "The IP pool for the selected department is full! Please expand the range." +msgstr "该分组的 IP 池已全部分配完毕!请前往“管理分组”扩大网段范围。" + +msgid "Uncategorized" +msgstr "未分组" + +msgid "Assign to Group (Optional)" +msgstr "归入自定义组 (可选)" + +msgid "-- Uncategorized (None) --" +msgstr "-- 不加入任何组 (未分组) --" + +msgid "Strategy Error: Please select a Target Group first to allocate IPs from its pool!" +msgstr "策略错误:请先选择一个“目标组”,以便从其专属网段中分配 IP!" + +msgid "Manage device network access anytime, anywhere" +msgstr "随时随地,极简管控该设备的网络权限" + +msgid "Block Internet (WAN)" +msgstr "一键断网 (禁止外网)" + +msgid "Instantly cut off internet access. LAN communication remains normal." +msgstr "立即断开该设备访问互联网的权限。局域网内互通正常。" + +msgid "LAN Isolation (Anti-snooping)" +msgstr "局域网隔离 (接下级交换机的设备失效)" + +msgid "Block internal network access for this device to prevent scanning and privacy leaks." +msgstr "隔离该设备通过本路由器内网访问权限,阻止内网扫描,防护隐私泄露。" + +msgid "DMZ Host (Expose to WAN)" +msgstr "DMZ 主机 (暴露到公网)" + +msgid "Forward all WAN ports to this device. Only one device allowed." +msgstr "将外网端口全转发给该设备。仅能开启一台设备。" + +msgid "Click to control access" +msgstr "点击管控权限" + +msgid "Cannot enable DMZ: Device has no valid IP or is offline!" +msgstr "无法开启 DMZ:该设备尚未获取到有效 IP,或不在线!" + +msgid "Denied! Only one DMZ host allowed." +msgstr "拒绝!仅允许设置一台 DMZ 主机。" + +msgid "Device [" +msgstr "设备 [" + +msgid "] is currently the DMZ." +msgstr "] 目前已是 DMZ。" + +msgid "Please disable its DMZ first!" +msgstr "请先关闭该设备的 DMZ 功能!" + +msgid "Reason: RPC Error ({err})." +msgstr "原因:RPC 错误 ({err})。" + +msgid "Please run `/etc/init.d/rpcd restart` in SSH and try again!" +msgstr "请在 SSH 运行 `/etc/init.d/rpcd restart` 后再试一次!" + +msgid "Reason: {err}" +msgstr "原因:{err}" + +msgid "Please run `/etc/init.d/rpcd restart` in SSH" +msgstr "请在 SSH 运行 `/etc/init.d/rpcd restart`" + +msgid "Firewall & Access Control" +msgstr "防火墙与权限管控" + +msgid "Live Connections" +msgstr "实时连接数" + +msgid "Monitor real-time connections (Auto turn off in 3 mins)" +msgstr "监控设备实时并发连接数 (开启3分钟后自动关闭以节省性能)" + +msgid "Net Blocked" +msgstr "已断网" + +msgid "Isolated" +msgstr "已隔离" + +msgid "DMZ Host" +msgstr "DMZ 主机" + +msgid "Conns" +msgstr "连接" + +msgid "Invalid IP format! Please enter a valid IPv4 address (e.g., 192.168.1.50)" +msgstr "IP 格式错误!请输入合法的 IPv4 地址 (例如: 192.168.1.50)" + +msgid "Public IPv6 (Click to copy):" +msgstr "公网 IPv6 (点击即可复制):" + +msgid "First IPv6 address copied successfully:" +msgstr "首个 IPv6 地址已成功复制:" + +msgid "Export Groups" +msgstr "导出自定义" + +msgid "Import Groups" +msgstr "导入自定义" + +msgid "Import successful!" +msgstr "导入成功!" + +msgid "Please verify and click [Save] below to apply." +msgstr "请检查无误后,点击下方【保存】按钮生效。" + +msgid "Import failed!" +msgstr "导入失败!" + +msgid "Invalid or corrupted file format. Please select a valid JSON backup file." +msgstr "文件格式错误或已损坏,请选择正确的 JSON 备份文件。" + +msgid "Suspected Spoofed Device" +msgstr "疑似伪装或新设备" + +msgid "Factory Reset" +msgstr "恢复出厂" + +msgid "Danger: Restore Default Network" +msgstr "危险操作:恢复初始网络" + +msgid "Are you sure you want to clear all static IPs and firewall rules set by Netwiz?

    This operation will delete all groups, blacklists, isolations, and DMZ settings, and restart network services." +msgstr "确定要清空所有由 Netwiz 网络管家设置的静态 IP 和防火墙规则吗?

    此操作将删除所有分组、黑名单、隔离和 DMZ 设置,并重启网络服务。" + +msgid "Cleaning up and restarting network services..." +msgstr "正在大扫除并重启网络服务..." + +msgid "Connection Radar Analysis" +msgstr "连线雷达分析" + +msgid "Diving into system kernel to capture connection records..." +msgstr "正在潜入系统内核抓取连线记录..." + +msgid "Web/HTTPS" +msgstr "网页/常规" + +msgid "DNS/NTP" +msgstr "DNS解析" + +msgid "UDP Stream/Game" +msgstr "UDP串流/游戏" + +msgid "P2P/High Ports" +msgstr "P2P下载/挂机" + +msgid "Others/Unknown" +msgstr "其他/未知" + +msgid "# Successfully parsed {total} source connection records..." +msgstr "# 成功解析到 {total} 条源连线记录..." + +msgid "root@OpenWrt:~# cat /proc/net/nf_conntrack | awk '...' # (Filtering src={ip})" +msgstr "root@OpenWrt:~# cat /proc/net/nf_conntrack | awk '...' # (过滤源IP={ip})" + +msgid "[ No active connection records ]" +msgstr "[ 暂无活动连线记录 ]" + +msgid "... (First 50 records shown for performance) ..." +msgstr "... (已截断显示前 50 条数据以保障效能) ..." + +msgid "Analysis failed: {err}" +msgstr "抓取失败: {err}" + +msgid "Export Config" +msgstr "导出配置" + +msgid "Import Config" +msgstr "导入配置" + +msgid "Packing Network Config" +msgstr "正在打包网络配置" + +msgid "
    Generating exclusive backup file

    (Includes: WAN, Wi-Fi, IPv6, Bypass, Static IPs & Firewall rules)
    " +msgstr "
    正在生成专属备份文件

    (包含:宽带、Wi-Fi、IPv6、旁路由、静态IP及防火墙规则)
    " + +msgid "Please wait..." +msgstr "请稍候..." + +msgid "Export Successful" +msgstr "导出成功" + +msgid "Core network configuration file has been successfully downloaded!" +msgstr "核心网络配置文件已成功下载!" + +msgid "Export Failed" +msgstr "导出失败" + +msgid "Backend returned no valid data" +msgstr "后端未返回有效数据" + +msgid "Error occurred: " +msgstr "发生错误: " + +msgid "Confirm Overwrite?" +msgstr "确认覆盖网络配置?" + +msgid "About to overwrite the router's core network settings!

    " +msgstr "即将覆盖路由器的核心网络设置!

    " + +msgid "Safety Mechanism: The system will automatically backup the current state to the /root/netwiz_bak/ directory before importing.

    Network services will automatically restart after import, which may cause brief Wi-Fi/LAN disconnection. Please confirm." +msgstr "安全机制:在导入前,系统会自动将当前状态备份到 /root/netwiz_bak/ 目录。


    导入后网络服务将自动重启,可能会导致 Wi-Fi/LAN 短暂断开,请确认。" + +msgid "Confirm & Apply" +msgstr "确认导入并应用" + +msgid "Writing configuration and restarting network services,
    Please wait... Network may disconnect briefly, taking about 10-25 seconds..." +msgstr "正在写入配置并重启网络服务,
    请稍候... 网络可能会短暂断开,大约需要 10-25 秒..." + +msgid "Restore Network Config" +msgstr "恢复网络配置" + +msgid "
    Reading router backup records...
    " +msgstr "
    正在读取路由器备份记录...
    " + +msgid "[ No history backups found ]" +msgstr "[ 暂无历史备份记录 ]" + +msgid "Restore from Router History" +msgstr "从路由器历史备份恢复" + +msgid "Restore Selected Backup" +msgstr "恢复选中的备份" + +msgid "— OR —" +msgstr "— 或者 —" + +msgid "Upload from Local PC" +msgstr "从本地电脑上传恢复" + +msgid "Browse PC Files..." +msgstr "浏览电脑文件..." + +msgid "Read Failed" +msgstr "读取失败" + +msgid "Unable to fetch router backup list." +msgstr "无法获取路由器备份列表。" + +msgid "Auto Backup" +msgstr "自动静默备份" + +msgid "Before Import" +msgstr "导入配置前备份" + +msgid "Before Reset" +msgstr "重置网络前备份" + +msgid "-- Keep Unchanged --" +msgstr "-- 保持不变 --" + +msgid "Reserved" +msgstr "预留" + +msgid "Note:" +msgstr "说明:" + +msgid "Custom group names are not strictly bound to IP subnets. The ranges are only used for automatic IP assignment when a group is selected." +msgstr "自定义组的名称不会强制绑定IP网段,网段也不受名称限制,仅用于选择自定义组时自动发号到对应网段。" + +msgid "When enabled, [Terminal Device & IP Binding] can assign short IPv6 addresses to PCs." +msgstr "联动【终端设备与 IP 绑定】固定 IP,电脑享受短 IPv6" + +msgid "Primary DNS:" +msgstr "主 DNS:" + +msgid "Secondary DNS:" +msgstr "备 DNS:" + +msgid "Non-standard parameters (Default configuration recommended)" +msgstr "与默认参数不一致 (推荐使用默认配置)" + +msgid "Cable Unplugged or Wrong Port" +msgstr "网线未连接 或 插错网口" + +msgid "System detected NO SIGNAL on the WAN port!

    " +msgstr "系统检测到路由器的 WAN 口(外网口) 没有信号!

    " + +msgid "Troubleshooting:
    1. Did you plug the upstream cable into the LAN port?
    2. Are both ends plugged in tightly? Is the modem powered on?
    " +msgstr "新手常见错误排查:

    1. 您是不是把上级网线 插错到了 LAN 口?
    2. 网线两端是否已插紧?光猫/墙壁网口是否已通电?
    " + +msgid "Detecting in background... This will close automatically once connected." +msgstr "正在后台极速侦测... 插好后本窗口将自动消失" + +msgid "I am an AP / Ignore for now" +msgstr "我是旁路由(中继)/ 暂不处理" + +msgid "Severe Warning: Routing Loop" +msgstr "严重警告:网络冲突死环" + +msgid "System detected that the upstream network ({wan_ip}) and the LAN ({lan_ip}) are in the same subnet!

    This will crash the router. Please confirm or modify the new LAN IP below and click auto-evade:

    " +msgstr "系统实时检测到,入户网络 ({wan_ip}) 与本路由器的局域网 ({lan_ip}) 处于同一网段!

    这将导致路由器彻底瘫痪。请确认或修改下方的新局域网 IP,并点击一键避让:

    " + +msgid "Modify and Restart Network" +msgstr "立即修改并重启网络" + +msgid "Invalid IP format! System restored safe default, please confirm." +msgstr "无效的 IP 格式!系统已恢复安全推荐值,请重新确认。" + +msgid "Still conflicting! The modified IP is still in the same subnet as upstream." +msgstr "仍然冲突!修改后的 IP 依然与上级网络处于同一网段。" + +msgid "System restored safe default, please change the 3rd number!" +msgstr "系统已恢复安全默认值,请尝试修改第 3 段数字!" + +msgid "IMPORTANT:" +msgstr "重要提示:" + +msgid "To resolve the fatal network loop, the system must change LAN IP and restart immediately." +msgstr "为了解除致命的网络死环,系统必须立即修改局域网 IP 并重启网络。" + +msgid "Changes you made on this page will NOT be saved." +msgstr "您在此页面上做出的其他修改将不会被保存。" + +msgid "Please reconfigure after the network restarts and redirects to the new IP." +msgstr "请在网络重启并跳转到新 IP 后,重新进行配置。" + +msgid "Continue to fix conflict?" +msgstr "是否继续修复冲突?" + +msgid "Network Conflict Blocked" +msgstr "网络冲突已拦截" + +msgid "Upstream network ({wan_ip}) conflicts with local LAN ({lan_ip})!

    Please click below to safely evade to: {safe_ip} to finish setup." +msgstr "检测到上级网络分配的地址 ({wan_ip}) 与本路由器的内网 ({lan_ip}) 发生严重冲突!

    请先点击下方按钮安全避让至:{safe_ip},随后即可正常完成向导。" + +msgid "Auto Evade" +msgstr "一键自动避让" + +msgid "Cross-Subnet Loop Blocked" +msgstr "跨网段路由死环拦截" + +msgid "The LAN IP ({ip}) you set is in the same subnet as your upstream network ({wan_ip}), which will crash the router!

    Suggested evasion to: {safe_ip}" +msgstr "您设置的 LAN IP ({ip}) 与您的入户网络 ({wan_ip}) 处于同一网段,这会导致路由器瘫痪!

    建议一键避让至:{safe_ip}" + +msgid "Fix and Apply" +msgstr "一键修复并应用" + +msgid "Edit Manually" +msgstr "我自己去改" + +msgid "Upstream network ({wan_ip}) conflicts with local LAN ({lan_ip})!

    You MUST change device LAN IP. Suggested evasion to: {safe_ip}" +msgstr "检测到上级网络分配的地址 ({wan_ip}) 与本路由器的内网 ({lan_ip}) 发生严重冲突!

    必须修改本设备的 LAN IP。系统建议安全避让至:{safe_ip}" + +msgid "Severe Physical Conflict" +msgstr "严重物理冲突" + +msgid "The WAN Static IP ({ip}) you set is exactly the same as the upstream Gateway!

    This causes a severe physical loop. Please change your Static IP (e.g., {suggest_ip})." +msgstr "您设置的 WAN 静态 IP ({ip}) 与上级网络的真实网关完全相同!

    这将导致严重的物理死环。请修改您的静态 IP(例如改为 {suggest_ip})。" + +msgid "The AP IP ({ip}) you set is exactly the same as the upstream Gateway!

    This will paralyze the network. Please change to another free IP (e.g., {suggest_ip})." +msgstr "您设置的旁路由 IP ({ip}) 与上级网络的真实网关完全相同!

    这将导致全屋网络瘫痪。请修改为同网段的其他空闲 IP(例如 {suggest_ip})。" + +msgid "Wake on LAN (WOL)" +msgstr "局域网唤醒 (WOL)" + +msgid "Wake packet sent to {mac}!" +msgstr "已向 {mac} 发送唤醒封包!" + +msgid "(Device booting may take a minute)" +msgstr "(设备开机可能需要一分钟时间)" + +msgid "Quick Setup Wizard" +msgstr "快速设置向导" + +msgid "Step 1: Admin Password" +msgstr "第一步:管理密码" + +msgid "Step 2: Internet Setup" +msgstr "第二步:上网设置" + +msgid "Step 3: Wi-Fi Setup" +msgstr "第三步:Wi-Fi 设置" + +msgid "Step 4: Confirm & Apply" +msgstr "第四步:确认并应用" + +msgid "Confirm Password" +msgstr "确认密码" + +msgid "Enter password again" +msgstr "请再次输入密码" + +msgid "Passwords do not match, please try again!" +msgstr "两次输入的密码不一致,请重新检查!" + +msgid "Router Admin Password (Optional)" +msgstr "路由器管理密码 (可选)" + +msgid "Keep empty to retain current password" +msgstr "留空则保持目前密码不变" + +msgid "Skip Wizard" +msgstr "跳过向导" + +msgid "Releasing wizard lock, entering official dashboard..." +msgstr "正在退出向导,进入后台..." + +msgid "This password will be used for logging into the router web interface and SSH. Setting it now is highly recommended." +msgstr "此密码将用于日后登录路由器后台及 SSH,建议立即设置。" + +msgid "Please enter a new password or check \"Skip Password Setup\"!" +msgstr "请输入新管理员密码,或勾选「暂不配置」!" + +msgid "Smart Connect" +msgstr "多频合一" + +msgid "Independent Bands" +msgstr "分频独立" + +msgid "WISP Repeater" +msgstr "中继 (WISP)" + +msgid "Disabled" +msgstr "未开启" + +msgid "Network Name (5G_Game)" +msgstr "无线网络名称 (5G_Game)" + +msgid "Wi-Fi Password (5G_Game)" +msgstr "Wi-Fi 密码 (5G_Game)" + +msgid "The underlying network status is not fully loaded. Please wait before submitting to prevent configuration loss." +msgstr "底层网络状态尚未加载完毕,请等待页面初始化完成后再提交,以防覆盖丢失配置。" + +msgid "Configuration complete! Automatically logging you in..." +msgstr "配置完成!正在为您自动登录系统..." + +msgid "Password setup failed: " +msgstr "密码设置失败: " + +msgid "No Wi-Fi hardware detected, this step will be skipped automatically.
    Please click [Next Step] directly." +msgstr "未检测到 Wi-Fi 硬件,本步骤自动跳过。
    请直接点击右下角【下一步】。" + +msgid "Advanced & Lab Features" +msgstr "高级与实验室功能" + +msgid "Beta" +msgstr "测试" + +msgid "Full Software Backup & Restore" +msgstr "全量软件的备份与恢复" + +msgid "Resolves traditional backup soft-brick risks. Smart backup is cross-version compatible. After flashing new firmware, upload backup to rapidly reinstall all software and losslessly restore configs (Requires Internet). Does not delete current software during restore. For absolute purity, factory reset the router first.
    Tip: If the backup shows Missing packages, manually place the installation packages in the /etc/netwiz/custom_pkgs/ directory.
    If Netwiz is missing after a reset or needs an upgrade, connect to the internet and run this in SSH:" +msgstr "智能备份(需联网)兼容跨openwrt版本恢复(不能跨架构和安装包格式)。刷入新固件后,上传备份即可极速还原(全量备份后可离线恢复)。恢复时不会删除当前软件。若需达到绝对纯净,请先恢复出厂设置。
    「提示:若备份中显示“缺失安装包(未备妥)”,请手动将安装包放置于 /etc/netwiz/custom_pkgs/ 目录下。」
    恢复出厂后若未安装 Netwiz 或需要升级,可联网后在 SSH 终端执行以下命令:" + +msgid "Generate Backup" +msgstr "生成备份文件" + +msgid "Restore System" +msgstr "恢复系统软件" + +msgid "Click to Copy" +msgstr "点击复制" + +msgid "Copied" +msgstr "已复制" + +msgid "Copy Failed" +msgstr "复制失败" + +msgid "Select Backup Mode" +msgstr "选择备份模式" + +msgid "Lightweight Backup" +msgstr "轻量化备份" + +msgid "Recommended" +msgstr "推荐模式" + +msgid "Full Backup" +msgstr "全量备份" + +msgid "Includes Dependencies" +msgstr "包含深层依赖" + +msgid "Lightweight Backup: Only packs core configs (Passwords/Wi-Fi/IP) and main plugin packages. Fast and requires no extra RAM. Requires internet or built-in dependencies when restoring." +msgstr "轻量化备份说明: 仅打包核心配置(账号密码/无线/IP)和主插件包,不抓取底层深层依赖,速度极快。恢复时新固件需自带对应依赖或处于联网状态。" + +msgid "Full Backup: Packs core configs and recursively downloads ALL underlying dependencies for third-party plugins. Large size, slower generation. NO internet required during restore. Recommended for devices with >256MB RAM." +msgstr "全量备份说明: 下载所有第三方插件的完整底层依赖。体积庞大生成较慢,优点是恢复时完全不需要网络即可重装成功。建议内存 >256M 的设备使用。" + +msgid "Start Backup" +msgstr "确定开始备份" + +msgid "Generating Backup" +msgstr "正在生成备份" + +msgid "Extracting configs and packing files in background..." +msgstr "正在后台提取配置并打包文件..." + +msgid "Note: Full mode selected, downloading all dependencies. Takes 1-3 mins, please wait." +msgstr "提示: 当前为全量模式,需要下载全部底层依赖。大约需 1 到 3 分钟,请耐心等待。" + +msgid "Note: Lightweight mode selected, packing is very fast (~30s)." +msgstr "提示: 当前为轻量模式,打包速度极快。大约需要 30 秒左右。" + +msgid "Backup Successful" +msgstr "打包成功" + +msgid "Backup capsule downloaded. Upload this file after flashing new firmware to restore." +msgstr "备份胶囊已成功下载。刷入新系统后上传此文件即可恢复。" + +msgid "Backup Failed" +msgstr "打包失败" + +msgid "Unable to start background backup task." +msgstr "无法启动后台备份任务" + +msgid "Confirm System Restore" +msgstr "确认恢复系统" + +msgid "WARNING: This will overwrite current configs and reinstall plugins!

    Router will auto-reboot upon completion. Ensure file is correct.

    " +msgstr "警告:此操作将覆盖现有的网络设置并重新安装插件!

    完成后路由器将自动重启,请确保选择的文件无误。

    " + +msgid "Safe Mode: Auto-rollback if connection is lost for 300s.

    " +msgstr "安全机制:若重启后失联 300 秒,系统将自动回退配置以防变砖。


    " + +msgid "Auto-download current state backup before restore (Regret Pill)" +msgstr "恢复前自动下载当前状态备份 (后悔药)" + +msgid "Confirm & Select File" +msgstr "确认并选择文件" + +msgid "Native Fast Restore Mode" +msgstr "原生极速恢复模式" + +msgid "Transferring capsule via high-speed system channel...

    DO NOT power off. Router will auto-reboot upon completion.

    " +msgstr "正在调用系统底层高速通道直传胶囊...

    请勿断开电源,路由器将在完成后自动重启。

    " + +msgid "Safe Mode: Auto-rollback if connection is lost for 300s." +msgstr "安全机制:若重启后失联 300 秒,系统将自动回退配置以防变砖。
    " + +msgid "Capsule delivered, preparing offline task..." +msgstr "胶囊已送达底层,准备执行脱机任务..." + +msgid "Restore Blocked by Security" +msgstr "恢复异常被安全拦截" + +msgid "Self-healing mechanism triggered. Garbage cleared, router is safe and unharmed." +msgstr "系统已启动自愈机制清理了垃圾残留,您的路由器目前处于安全状态,并未受损。" + +msgid "Transfer Failed" +msgstr "传输失败" + +msgid "Interface rejected reception, status code: " +msgstr "底层接口拒绝接收,状态码: " + +msgid "Network Error" +msgstr "网络错误" + +msgid "File transfer interrupted unexpectedly, check network!" +msgstr "文件传输被意外中断,请检查网络连线!" + +msgid "Preparing Regret Pill" +msgstr "正在为您调配后悔药" + +msgid "Backing up current state to prevent regrets...

    Tip: Restore will begin automatically after download" +msgstr "正在备份当前系统状态以防恢复后反悔

    提示: 备份下载完成后将自动开始覆盖并恢复新系统" + +msgid "Probing Environment" +msgstr "正在探测运行环境" + +msgid "Calculating system available RAM and storage..." +msgstr "正在计算系统可用内存与存储空间..." + +msgid "Critical RAM Shortage" +msgstr "物理内存严重不足" + +msgid "Your full capsule size is {size} MB.

    But /tmp (RAM) only has {avail} MB free!

    Force uploading will cause OOM, crash, and network drop.

    Solution: Please reboot the router once to clear RAM fragments, then retry." +msgstr "检测到您的全量胶囊大小为 {size} MB。

    但当前路由器的 /tmp (运行内存) 仅剩余 {avail} MB 可用空间!

    强行上传将导致路由器内存溢出、死机并断网。

    解决方案:请先重启一次路由器来清空内存碎片,重启后再进行恢复操作。" + +msgid "Cancel Restore" +msgstr "取消恢复" + +msgid "Waiting for background task to start..." +msgstr "等待后台任务启动..." + +msgid "Capsule delivered, initializing restore environment..." +msgstr "胶囊已送达,正在初始化恢复环境..." + +msgid "Scanning capsule structure to estimate uncompressed size (5-10s)..." +msgstr "正在扫描胶囊深层结构以预估解压体积 (约需 5-10 秒)..." + +msgid "Uncompressed size reaches {u}MB, but RAM only has {a}MB left! Intercepted to prevent router crash." +msgstr "解压体积达 {u}MB,但内存仅剩 {a}MB!为防止死机,已触发安全拦截。" + +msgid "Size safe! Extracting backup capsule..." +msgstr "体积安全!正在解压备份胶囊..." + +msgid "Extraction failed! Capsule may be corrupted, initiating self-healing..." +msgstr "解压失败!可能是压缩包损坏,已启动自愈机制清理内存。" + +msgid "Extraction successful, applying core configs..." +msgstr "解压成功,正在覆盖系统核心配置..." + +msgid "Force offline installing plugins (this takes a while, please wait)..." +msgstr "正在强制离线覆盖插件 (此过程较长,请耐心等待)..." + +msgid "Restore thoroughly complete! Router will auto-reboot!" +msgstr "恢复彻底完成!路由器即将自动重启!" + +msgid "MSG_RST_INVALID" +msgstr "无效的胶囊文件!系统未检测到 NetWiz 专属特征码,为保护系统安全已自动拦截并销毁。" + +msgid "File Too Large" +msgstr "文件过大" + +msgid "
    Config files are usually under 100KB!

    The selected file is too large and has been blocked." +msgstr "
    配置文件通常小于 100KB!

    您选择的文件过大,已被安全拦截。" + +msgid "Restore Network Config" +msgstr "还原网络配置" + +msgid "This action will overwrite existing device groups and static IP bindings, and restart the network.

    " +msgstr "此操作将覆盖现有的设备分组和静态 IP 绑定,并重启网络。

    " + +msgid "Security Warning: Do not upload backups from unknown sources to prevent DNS hijacking.

    Confirm to restore?" +msgstr "安全警告:请勿上传来源不明的备份,以防 DNS 被劫持。


    确认还原?" + +msgid "Restoring" +msgstr "正在还原" + +msgid "Restore Failed" +msgstr "还原失败" + +msgid "Error: " +msgstr "错误: " + +msgid "Invalid Format" +msgstr "格式无效" + +msgid "The uploaded file is not a valid JSON format and cannot be parsed!" +msgstr "您上传的文件不是有效的 JSON 格式,无法解析!" + +msgid "Import Configuration" +msgstr "导入配置" + +msgid "Are you sure you want to import this configuration? Existing data will be overwritten." +msgstr "确认要导入该配置吗?现有数据将被彻底覆盖。" + +msgid "Severe Network Topology Conflict!" +msgstr "严重网络拓扑冲突!" + +msgid "System detected that IPv6 and LAN \"Masquerading (NAT)\" are BOTH enabled!This will paralyze IPv6 allocation and cause routing loops.
    %sFix: Please go to Network -> Firewall to disable LAN Masquerading, or Disable IPv6 in the LAN settings on the Netwiz homepage." +msgstr "系统检测到 IPv6 与局域网“IP 动态伪装 (NAT)”同时开启!这将导致 IPv6 分配瘫痪及路由环路。
    %s修复方法:请前往 网络 -> 防火墙 关闭局域网的动态伪装,或者在 Netwiz 首页的局域网设置中关闭 IPv6。" + +msgid "IPv6 Configuration Blocked" +msgstr "IPv6 配置已被系统安全拦截" + +msgid "Detected that LAN \"IP Masquerading (NAT)\" is enabled. Forcing IPv6 on under a double-NAT topology will cause network disconnection.
    %sFix: Please go to Network -> Firewall -> Zones to disable LAN Masquerading first." +msgstr "检测到局域网“IP 动态伪装 (NAT)”已开启。在双重 NAT 拓扑下强制开启 IPv6 会导致断网。
    %s修复方法:请先前往 网络 -> 防火墙 -> 区域 关闭局域网动态伪装。" + +msgid "Restore Device Data" +msgstr "还原设备数据" + +msgid "This action will overwrite the existing network configuration and restart the network.

    " +msgstr "此操作将覆盖现有的网络配置并重启网络。

    " + +msgid "Security Warning: Do not upload backups from unknown sources to prevent router hijacking.

    Confirm restore?" +msgstr "安全警告:请勿上传来源不明的备份,以防路由器被劫持。


    确认还原?" + +msgid "Success" +msgstr "成功" + +msgid "Warning" +msgstr "警告" + +msgid "Waking up..." +msgstr "唤醒中..." + +msgid "Displaying self-assigned local IPv6 as IPv4 is unavailable" +msgstr "由于无法获取 IPv4,显示设备底层自签的本地 IPv6" + +msgid "Local IPv6" +msgstr "自签 IPv6" + +msgid "System is rebooting, please wait..." +msgstr "系统重启中,请稍候..." + +msgid "Waiting for device to disconnect..." +msgstr "等待设备断开连接..." + +msgid "IP Conflict Warning" +msgstr "IP 冲突警告" + +msgid "is already used by another device!" +msgstr "已被局域网内其他设备占用!" + +msgid "We strongly recommend changing it to avoid network crashes." +msgstr "强烈建议更改为新 IP,以防止网络瘫痪或断网。" + +msgid "Fix to" +msgstr "一键修复为" + +msgid "Scanning installed plugins..." +msgstr "正在扫描已安装的插件清单..." + +msgid "Plugin Backup Status" +msgstr "插件备份状态检查" + +msgid "Please verify backup packages:" +msgstr "请核对备份安装包:" + +msgid "If you proceed, missing plugins WILL NOT be restored automatically!" +msgstr "如果现在执行备份,缺失安装包在还原时将无法被恢复!" + +msgid "Tip: Manually place missing packages .ipk/.apk into the /etc/netwiz/custom_pkgs/ directory via SSH to ensure they are automatically reinstalled during future restorations." +msgstr "建议:通过 SSH 将缺失的安装包(.ipk/.apk)放入 /etc/netwiz/custom_pkgs/ 目录,还原时会自动恢复当前状态。" + +msgid "Ignore & Backup" +msgstr "忽略并备份" + +msgid "Missing packages:" +msgstr "缺失安装包(未备妥):" + +msgid "Manually placed in custom_pkgs:" +msgstr "手动放入 custom_pkgs 插件:" + +msgid "Custom Plugins Ready" +msgstr "自定义插件已就绪" + +msgid "Great! Your custom plugins are safely stored in the local directory and will be included in the backup capsule." +msgstr "恭喜!已将所有自定义插件离线包存入本地保险箱,后续将统一打包封装进备份胶囊。" + +msgid "Confirm Backup" +msgstr "确定备份" + +msgid "Auto-backup plugins:" +msgstr "可自动备份插件:" + +msgid "Plugin Scan Complete" +msgstr "插件扫描完成" + +msgid "System scan complete! All your installed plugins are from the official repository and will be safely recorded for automatic restoration." +msgstr "系统扫描完毕!您安装的所有第三方可视化插件,均存在于当前软件源中。它们将被安全记录并在未来自动联网恢复。" + +msgid "Backup Interrupted Warning" +msgstr "备份中断警告" + +msgid "Out of Memory (OOM Protection)!" +msgstr "内存不足 (OOM 预防机制)!" + +msgid "The files you are trying to pack are too large and exceed the available memory.

    To prevent device crash, the backup task has been safely canceled. Please clear unnecessary core files and try again." +msgstr "您要打包的文件体积过大,超过了路由器当前的可用内存。

    为防止设备死机,备份任务已自动安全取消。请尝试清理无用的核心文件后重试。" + +msgid "I Got It" +msgstr "我知道了" + +msgid "Scan Timeout Warning" +msgstr "扫描超时警告" + +msgid "Communication Timeout or Network Error" +msgstr "通信超时或网络异常" + +msgid "Communication with the router timed out (possibly due to slow network source retrieval or network fluctuations).

    To ensure backup integrity, the task has been safely canceled. Please try again later, or manually run opkg update via SSH." +msgstr "与路由器的通信超时 (可能是网络源拉取过慢或网络波动)。

    为保证备份完整性,任务已安全取消。请稍后再试,或在 SSH 中手动运行 opkg update。" + +msgid "Connecting..." +msgstr "正在连接..." + +msgid "Connecting to upstream... (Check password or signal strength if stuck)" +msgstr "正在连接上级网络... (若一直连接不上请检查密码是否正确或是否上级信号太弱)" + +msgid "First Time Syncing" +msgstr "首次核实插件清单" + +msgid "Syncing lists in the background..." +msgstr "正在后台拉取最新列表..." + +msgid "Verifying the list of plugins for automatic backup...

    Depending on the network, this usually takes 15 to 20 seconds.
    The system is verifying, please wait..." +msgstr "正在核实可以自动备份的插件清单...

    受限于网络,这通常需要 15 ~ 20秒。
    系统正在核实,请稍候..." + +msgid "OK, I will try again later" +msgstr "停止备份" + +msgid "RESTORE FAILED: Package manager mismatch (apk vs ipk). Please use firmware with the same underlying system." +msgstr "恢复失败:包管理器格式冲突!您的备份包与当前系统的底层不兼容(apk 与 ipk 冲突),强行恢复将导致系统损坏,进程已被系统强制拦截!" + +msgid "RESTORE FAILED: CPU Architecture mismatch. Forcing this restore will brick your router! Process aborted." +msgstr "恢复致命拦截:CPU 架构不匹配!您正在尝试将其他架构的备份强行刷入当前路由器,这会导致路由器彻底变砖!系统已自动终止恢复进程。" + +msgid "Package Manager Conflict" +msgstr "包管理器冲突" + +msgid "CPU Architecture Conflict" +msgstr "CPU 架构冲突" + +msgid "Your current system uses the apk architecture, but you are trying to flash an ipk backup!" +msgstr "您当前的系统使用 apk 架构,但您正尝试刷入一个 ipk 格式的备份包!" + +msgid "Your current system uses the opkg architecture, but you are trying to flash an apk backup!" +msgstr "您当前的系统使用 opkg 架构,但您正尝试刷入一个 apk 格式的备份包!" + +msgid "Architecture Warning" +msgstr "架构标识未匹配" + +msgid "No architecture identifier ({arch}) detected in the selected backup filename." +msgstr "您选中的备份包名称中,未检测到当前路由器的架构标识 ({arch})。" + +msgid "If you have manually renamed this file, please ignore this warning.

    If it is the wrong package, the system's underlying security mechanism will forcibly intercept the restoration later!" +msgstr "如果您手动修改过该备份包的文件名,请忽略此警告。

    如果您选错了包,稍后的强行恢复将会被系统底层安全机制强制拦截!" + +msgid "I understand, continue" +msgstr "我知道了,继续" + +msgid "Scan to Connect" +msgstr "原生相机扫码直连" + +msgid "Architecture Safety Lock: Cross-package manager restoration (e.g., IPK to APK) is strictly prohibited. The underlying network and firewall configurations differ significantly across these system versions. Forcing a restore will cause severe network failure and brick your router!" +msgstr "架构安全锁:严禁跨包管理器(如 IPK 备份至 APK 系统)进行恢复!由于新旧系统的网络与防火墙底层配置格式存在重大差异,强行恢复配置将导致路由器直接断网变砖!为了您的设备安全,系统已强制拦截此次操作。" + +msgid "Force Restore Config" +msgstr "强制恢复配置" + +msgid "Current WAN is in DHCP/Static mode. PPPoE settings will not take effect until applied." +msgstr "当前 WAN 口为动态/静态 IP 模式,本页应用配置前 PPPoE 拨号信息不生效。" + +msgid "Current WAN is in PPPoE mode. DHCP/Static settings will not take effect until applied." +msgstr "当前 WAN 口为 PPPoE 拨号模式,本页应用配置前动态/静态 IP 模式无效。" + +msgid "Advanced Utilities" +msgstr "高级实用设置" + +msgid "MAC Address Clone" +msgstr "MAC 地址克隆" + +msgid "Scheduled Reboot" +msgstr "定时重启设置" + +msgid "IPv6 Web Access, Port:" +msgstr "IPv6直联后台,端口:" + +msgid "Tip: Some ISPs or campus networks bind to a specific device MAC. If dial-up fails, enter the cloned MAC here." +msgstr "提示:部分小区宽带或校园网绑定了特定的设备 MAC(如旧路由器或电脑)。若拨号失败,请在此填入该 MAC 进行伪装克隆。" + +msgid "Auto-fill MAC" +msgstr "一键填写 MAC" + +msgid "No active device connection detected, please enter MAC address manually." +msgstr "未检测到活动的设备连接,请手动输入 MAC 地址。" + +msgid "Enable Scheduled Reboot" +msgstr "启用定时重启路由器功能" + +msgid "Reboot Time:" +msgstr "重启时间:" + +msgid "Repeat Days (Multi-select):" +msgstr "重复星期 (可多选):" + +msgid "Please select at least one day!" +msgstr "请至少选择一天!" + +msgid "Mon" +msgstr "一" + +msgid "Tue" +msgstr "二" + +msgid "Wed" +msgstr "三" + +msgid "Thu" +msgstr "四" + +msgid "Fri" +msgstr "五" + +msgid "Sat" +msgstr "六" + +msgid "Sun" +msgstr "日" + +msgid "Scheduled Reboot" +msgstr "定时重启" + +msgid "MAC Clone" +msgstr "MAC 克隆" + +msgid "Clear (Restore Default)" +msgstr "清空(恢复默认)" + +msgid "Internet Connected" +msgstr "互联网已连通" + +msgid "Internet Disconnected" +msgstr "互联网未连通" + +msgid "Ignore" +msgstr "忽略" + +msgid "Invalid MAC address format!" +msgstr "MAC 地址格式不正确,请检查!" + +msgid "e.g., AA:BB:CC:DD:EE:FF" +msgstr "例如:AA:BB:CC:DD:EE:FF" + +msgid "Multi-WAN detected. You can modify the account and password for each line separately (other settings remain unchanged)." +msgstr "系统检测到多个拨号环境,可分别修改各拨号的账号密码(其他设置保持不变)。" + +msgid "Interface:" +msgstr "线路接口:" + +msgid "Multi-WAN detected. Only the primary WAN will be configured here, other lines remain unchanged." +msgstr "系统检测到多拨环境,向导中仅配置主线路 (WAN),其余将保持原样。" + +msgid "Custom Hosts" +msgstr "自定义 Hosts" + +msgid "Custom Hosts" +msgstr "自定义 Hosts" + +msgid "(Domain Hijack)" +msgstr "(域名劫持)" + +msgid "Quick Add:" +msgstr "快速添加记录:" + +msgid "Add" +msgstr "添加" + +msgid "No custom Hosts, please add below." +msgstr "暂无自定义 Hosts,请点击上方添加记录" + +msgid "IP (e.g., 127.0.0.1 or ::1)" +msgstr "指向 IP (127.0.0.1 或 ::1)" + +msgid "Domain (e.g., abc.com)" +msgstr "劫持域名 (如 abc.com)" + +msgid "Comment (Optional)" +msgstr "备注说明 (选填)" + +msgid "Pure Text Mode: Paste to import, Copy to export. Format: IP Domain #Comment" +msgstr "纯文本模式:粘贴导入,全选复制可导出到纯文本保存。格式:IP 域名 #备注" + +msgid "IP and Domain cannot be empty!" +msgstr "IP 和 域名 不能为空!" + +msgid "Invalid IP address format!" +msgstr "IP 地址格式不正确!" + +msgid "Invalid domain! Spaces, wildcards (*), and special characters are not allowed." +msgstr "域名格式不正确!不支持泛解析(*),且不允许包含空格或特殊危险字符。" + +msgid "No changes have been made." +msgstr "您没有进行任何修改!" + +msgid "Notice" +msgstr "提示信息" + +msgid "This IP and Domain combination already exists!" +msgstr "此 IP 和 域名的组合已经存在,请勿重复添加!" + +msgid "Duplicate records found in Hosts! Please remove them before continuing." +msgstr "Hosts文件中发现重复的 IP+域名 规则!请手动删除重复项后再试。" + +msgid "Smart Auto-fill" +msgstr "智能补全" + +msgid "Auto-fill IPv4/v6 & www domain combinations" +msgstr "自动生成 IPv4/v6 以及带 www 和不带 www 的组合规则" + +msgid "This feature forces specific domains to resolve to designated IPs. Commonly used for blocking domain access or local device redirection." +msgstr "此功能可将特定域名强制解析到指定的 IP 地址。常用于阻止某域名联网 或 内网设备重定向。" + +msgid "Found invalid or duplicate records:" +msgstr "在纯文本中发现了格式错误或重复的记录,共计:" + +msgid "Click [] to automatically discard them and continue, or [Cancel] to manually fix them." +msgstr "点击【确定】将自动丢弃错误项并保存,点击【取消】将返回手动修改。" + +msgid "Plugin Repair" +msgstr "插件修复" + +msgid "Please wait" +msgstr "请稍候" + +msgid "Scanning for repairable plugins..." +msgstr "正在扫描可修复的插件列表..." + +msgid "Standard uninstallation does not remove plugin configuration files. If a plugin malfunctions after reinstallation, select it below to reset it to its initial state." +msgstr "常规卸载不会清除插件配置文件。若插件重装后仍无法正常使用,可在下方选中对应插件,修复为初始状态。也可以手动备份配置,需要时再手动恢复配置。" + +msgid "Factory Default" +msgstr "出厂默认值" + +msgid "Plugin Repair Toolkit" +msgstr "插件修复急救箱" + +msgid "Repair Now" +msgstr "立即修复" + +msgid "Processing" +msgstr "处理中" + +msgid "Repairing and restarting" +msgstr "正在修复并且重启" + +msgid " please wait" +msgstr " 请稍候" + +msgid "Repair Successful" +msgstr "修复成功" + +msgid "has been successfully restored" +msgstr "已经成功恢复" + +msgid "Repair Failed" +msgstr "修复失败" + +msgid "Unable to repair this plugin" +msgstr "无法修复该插件" + +msgid "System Error" +msgstr "系统异常" + +msgid "Request timeout or error" +msgstr "请求超时或者出错" + +msgid "Notice" +msgstr "提示" + +msgid "No repairable plugins found" +msgstr "没有找到可以修复的插件" + +msgid "Failed to get plugin list" +msgstr "获取插件列表失败" + +msgid "Port number must be between 1 and 65535" +msgstr "端口号必须介于 1 到 65535 之间" + +msgid "For system security, do not use" +msgstr "为了系统安全请勿使用" + +msgid "as the external port. It is a reserved high-risk port." +msgstr "作为外网端口此为系统保留高危端口" + +msgid "It is recommended to use 8080 or a port above 10000." +msgstr "建议使用 8080 或 10000 以上的端口" + +msgid "Enter custom external port number" +msgstr "填写自定义外网端口号" + +msgid "Port" +msgstr "端口冲突:端口" + +msgid "is already in use by another application!" +msgstr "已被其他服务占用!" + +msgid "Direct WAN Access (Whitelist)" +msgstr "外网直连 (白名单)" + +msgid "Open firewall to allow direct external access to this device." +msgstr "开放防火墙,允许外网直接访问该设备。" + +msgid "IPv6 Only" +msgstr "仅 IPv6" + +msgid "IPv4 Only" +msgstr "仅 IPv4" + +msgid "IPv4 + IPv6" +msgstr "IPv4 + IPv6" + +msgid "Ghost Plugin Deletion Warning" +msgstr "孤件彻底删除警告" + +msgid "Executing this will uninstall the software and delete its config files. However, no installation package was found, so you will need to manually reinstall it later." +msgstr "执行后会卸载该软件并删除配置文件,但找不到安装包,后续使用需自行安装。" + +msgid "Eradicate Completely" +msgstr "一键彻底清除" + +msgid "Eradicating completely..." +msgstr "正在执行彻底清除..." + +msgid "has been completely uninstalled and cleaned." +msgstr "已被彻底卸载并清理。" + +msgid "Offline Reinstall Guide" +msgstr "孤件离线重装指南" + +# 1. 对应按钮名词的翻译(会被替换到 %s 的位置) +msgid "Plugin Installation" +msgstr "插件安装" + +# 2. 对应整段指南的翻译 +msgid "To reinstall the plugin, it is highly recommended to use the [%s] feature in the Advanced Utilities panel for quick drag-and-drop setup.

    If conflicts occur or the menu is missing, you can upload the package via SSH to /etc/netwiz/custom_pkgs/ and use the Force Install command:" +msgstr "重新安装插件,建议直接使用“高级功能”面板中的 [%s] 功能进行快捷的可视化拖拽安装。

    如果发生冲突或菜单丢失,您仍可通过 SSH 将安装包上传至 /etc/netwiz/custom_pkgs/ 并使用以下强制安装命令:" + +msgid "Hardware execution takes longer, running in background..." +msgstr "底层硬件正在执行中,请耐心等待..." + +msgid "Advanced Panel Management" +msgstr "高级功能面板管理" + +msgid "Scheduled Reboot" +msgstr "定时重启" + +msgid "MAC Address Clone" +msgstr "MAC 地址克隆" + +msgid "MAC Clone" +msgstr "MAC 克隆" + +msgid "Custom Hosts" +msgstr "自定义 Hosts" + +msgid "Plugin Repair" +msgstr "系统急救箱" + +msgid "Plugin Installation" +msgstr "插件安装" + +msgid "Offline Plugin Installation" +msgstr "离线插件安装" + +msgid "Drag ☰ to reorder, check to show/hide:" +msgstr "按住 ☰ 拖拽排序,勾选开关显示:" + +msgid "Save and Apply" +msgstr "保存并应用" + +msgid "Drop or click to select multiple .apk / .ipk files" +msgstr "支持拖拽或点击选择多个 .apk / .ipk" + +msgid "Security Block: Only .apk and .ipk allowed!" +msgstr "安全拦截:仅允许 .apk 和 .ipk!" + +msgid "Start Upload & Secure" +msgstr "开始上传并入库" + +msgid "Confirm Upload ({num} files)" +msgstr "确认上传并入库 ({num} 个文件)" + +msgid "Processing, do not close the window..." +msgstr "处理中,请勿关闭窗口..." + +msgid "Format blocked!" +msgstr "格式被拦截!" + +msgid "All files secured! Refreshing..." +msgstr "所有文件均已成功入库!正在刷新..." + +msgid "All files secured, executing batch install..." +msgstr "所有文件入库完毕,正在执行合并安装..." + +msgid "Installation successful! Refreshing..." +msgstr "安装与重置成功!正在刷新..." + +msgid "Hardware configuring, please wait {sec}s..." +msgstr "底层硬件正在拼命配置中,请耐心等待 {sec}s..." + +msgid "Batch install error: " +msgstr "批量安装异常: " + +msgid "Processing ({idx}/{total}): {name} - {pct}%" +msgstr "正在处理 ({idx}/{total}): {name} - {pct}%" + +msgid "Securing in background: {name}" +msgstr "后台安装中: {name}" + +msgid "Backend transfer failed: {name}" +msgstr "后端转移失败: {name}" + +msgid "Upload failed ({code}): {name}" +msgstr "上传失败 ({code}): {name}" + +msgid "Network disconnected: {name}" +msgstr "网络连接中断: {name}" + +msgid "Continue to retry remaining files" +msgstr "继续重试上传剩余文件" + +msgid "Keep a permanent backup in /etc/netwiz/custom_pkgs" +msgstr "同时保留备份至 /etc/netwiz/custom_pkgs" + +msgid "Eradicating completely..." +msgstr "正在彻底执行连根拔除..." + +msgid "has been completely uninstalled and cleaned." +msgstr "已被彻底卸载并清理。" + +msgid "Remove" +msgstr "移除" + +msgid "Save layout failed" +msgstr "保存排版失败" + +msgid "IPv6 Watchdog" +msgstr "IPv6 深度保活" + +msgid "IPv6 Heartbeat Probe" +msgstr "IPv6 直联进阶保活" + +msgid "Enable Advanced IPv6 Link Monitoring & Auto Recovery" +msgstr "启用 IPv6 链路存活监测与自愈" + +msgid "Probe Target URL" +msgstr "探测源目标网址" + +msgid "e.g., XXXXX-XXXXX.workers.dev" +msgstr "例如:XXXXX-XXXXX.workers.dev" + +msgid "Note:" +msgstr "说明:" + +msgid "The server must parse this address and perform a live test:" +msgstr "服务端需解析该地址并进行测活:" + +msgid "Your_URL/[IPv6_Address]:Port" +msgstr "您的链接/[IPv6地址]:端口" + +msgid "If the network is accessible, it must return the plain text" +msgstr "若网络畅通,请务必返回纯文本" + +msgid "Examples:" +msgstr "填写示例:" + +msgid "Click to view Cloudflare tutorial & source code" +msgstr "点击查看 Cloudflare 示例教程与源码" + +msgid "To ensure the works correctly, the following dependent features have been automatically enabled:" +msgstr "为了确保正常工作,系统已同步为您开启以下依赖功能:" + +msgid "IPv6 Master Switch" +msgstr "IPv6 总开关" + +msgid "WAN Access Web UI Channel" +msgstr "外网访问面板通道" + +msgid "Security Notice" +msgstr "安全提示" + +msgid "WAN access is about to be closed. To ensure complete firewall protection, the system will simultaneously disable the IPv6 Watchdog mechanism." +msgstr "即将关闭外网访问通道。为保障防火墙防护完整封闭,系统将同步停用 IPv6 深度保活机制。" + +msgid "Dependency Notice" +msgstr "依赖提示" + +msgid "You have disabled IPv6. To ensure security, the dependent IPv6 Watchdog and WAN Access will be automatically disabled." +msgstr "您关闭了 IPv6 功能。为确保系统安全,依赖此功能的「IPv6 深度保活」与「外网直联后台」将同步关闭。" + +msgid "Probe URL cannot be empty, please fill it in before saving!" +msgstr "探测网址不能为空,请填写后再保存!" + +msgid "New Version Found" +msgstr "发现新版本" + +msgid "Ready to install the update?" +msgstr "准备好安装更新了吗?" + +msgid "Update Now" +msgstr "立即更新" + +msgid "Later" +msgstr "稍后再说" + +msgid "System Upgrading" +msgstr "系统升级中" + +msgid "Downloading and replacing core files, please do not power off..." +msgstr "正在下载并替换核心文件,请勿断电..." + +msgid "Forcing system file overwrite... (Est. {sec}s remaining)" +msgstr "正在强制覆盖底层文件... (预计还需 {sec} 秒)" + +msgid "Upgrade Successful!" +msgstr "升级安装成功!" + +msgid "The new version core has been deployed in the background!" +msgstr "新版本核心已在底层部署完毕!" + +msgid "Notice: Aggressive browser caching detected.
    To ensure new menus and features display correctly, please press Ctrl + F5 or Shift + F5 to force refresh this page!
    (Mobile users: please manually clear browser cache and refresh)" +msgstr "注意:浏览器存在页面缓存
    为确保新菜单和新功能正常显示,请按下键盘上的 Ctrl + F5 或 Shift + F5 强制刷新本页!
    (手机端用户请手动清理浏览器缓存后刷新)" + +msgid "You are about to enable the [%s]." +msgstr "即将开启 [%s]。" + +msgid "You are about to disable the [%s]." +msgstr "即将关闭 [%s]。" + +msgid "Suggestion:" +msgstr "建议提醒:" + +msgid "No target URL is configured for the Watchdog. For long-term stability, it is recommended to manually configure and enable it in %s later." +msgstr "检测到尚未配置探测源目标网址。为保障外网访问的长期稳定性,建议您稍后前往 %s 手动配置并开启此功能。" + +msgid "Backup Config" +msgstr "备份配置" + +msgid "Restore Config" +msgstr "恢复配置" + +msgid "Restore Configuration Confirm" +msgstr "恢复配置确认" + +msgid "Warning: Restoring the configuration will overwrite all current settings and automatically restart the router upon completion!" +msgstr "警告:恢复配置将覆盖当前所有设置,并在完成后自动重启路由器!" + +msgid "Selected file: " +msgstr "选中文件:" + +msgid "Confirm Restore" +msgstr "确认恢复" + +msgid "Restoring Configuration" +msgstr "正在恢复配置" + +msgid "Uploading backup file, please do not disconnect power..." +msgstr "正在上传备份文件,请勿断开电源..." + +msgid "File delivered, starting restore..." +msgstr "文件已送达,开始执行恢复..." + +msgid "System is restarting..." +msgstr "系统正在重启..." + +msgid "Restore command issued, device may be restarting..." +msgstr "恢复指令已下发,设备可能正在重启中..." + +msgid "Backing up" +msgstr "备份中" + +msgid "Precisely packing %s config and generating download, please wait..." +msgstr "正在精准打包 %s 的配置并生成下载,请稍候..." + +msgid "Backup Successful" +msgstr "备份成功" + +msgid "The file contains core assets and is downloading in the background. Please check your browser!" +msgstr "文件包含了核心资产,已开始在后台为您下载,请留意浏览器提示!" + +msgid "Backup Failed" +msgstr "备份失败" + +msgid "Config not found or packaging error. Please check if the plugin is installed." +msgstr "找不到配置或打包出错,请检查是否安装了该插件。" + +msgid "Request Failed" +msgstr "请求失败" + +msgid "API request failed: %s" +msgstr "请求接口失败:%s" + +msgid "Restoring" +msgstr "恢复中" + +msgid "Uploading and restoring %s config..." +msgstr "正在上传并恢复 %s 的配置..." + +msgid "Restore Successful" +msgstr "恢复成功" + +msgid "%s config file has been restored and reloaded!" +msgstr "%s 配置文件已还原并重新加载!" + +msgid "Restore Failed" +msgstr "恢复失败" + +msgid "Failed to extract or restart service." +msgstr "解压或重启服务失败。" + +msgid "Upload Failed" +msgstr "上传失败" + +msgid "File upload rejected, status code: %s" +msgstr "文件上传被拒绝,状态码:%s" + +msgid "Upload failed: %s" +msgstr "上传失败: %s" + +msgid "Error" +msgstr "错误" + +msgid "Network Error" +msgstr "网络错误" + +msgid "Network connection unexpectedly interrupted!" +msgstr "网络连接意外中断!" + +msgid "Reload Page" +msgstr "刷新页面" + +msgid "Format Error" +msgstr "格式错误" + +msgid "Please upload a valid .tar.gz backup file!" +msgstr "请上传有效的 .tar.gz 备份文件!" + +msgid "Warning" +msgstr "警告" + +msgid "The selected file does not seem to belong to %s.
    File name: %f
    Are you sure you want to continue?" +msgstr "选中的文件似乎不属于 %s。
    文件名:%f
    您确定要继续强制恢复吗?" + +msgid "Force Restore" +msgstr "强制恢复" + +msgid "The uploaded file is corrupted or not a valid archive!" +msgstr "上传的文件已损坏,或不是有效的压缩包!" + +msgid "The archive content does not match %s!" +msgstr "压缩包内部内容与 %s 不匹配!" + +msgid "%s config restored! Service is restarting in the background..." +msgstr "%s 配置还原成功!服务正在后台平滑重启..." + +msgid "Auto reloading in %s seconds..." +msgstr "%s 秒后将自动刷新页面..." + +msgid "Failed to extract files to the system! (Storage full or permission denied)" +msgstr "文件解压写入系统失败!(可能是存储空间已满或权限不足)" diff --git a/luci-app-netwiz/luci-app-netwiz/po/zh_Hant/netwiz.po b/luci-app-netwiz/luci-app-netwiz/po/zh_Hant/netwiz.po new file mode 100644 index 000000000..9bec63f4e --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/po/zh_Hant/netwiz.po @@ -0,0 +1,2353 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "Network Wizard" +msgstr "網路設定" + +msgid "Netwiz NETWORK SETUP" +msgstr "Netwiz 網路設定精靈" + +msgid "Pure · Secure · Non-destructive Minimalist Config" +msgstr "純粹 · 安全 · 無損的極簡設定" + +msgid "Secondary Router Mode" +msgstr "二級路由模式 (LAN)" + +msgid "Upstream network dials up, this device acts as a secondary router." +msgstr "上級網路負責撥號,本設備作為二級路由使用。" + +msgid "PPPoE Dial-up" +msgstr "PPPoE 撥號 (WAN)" + +msgid "Dial up directly using account and password on this device." +msgstr "在本設備上直接使用寬頻帳號和密碼進行撥號。" + +msgid "LAN Settings" +msgstr "區域網路設定 (LAN)" + +msgid "Change device LAN IP, switch to AP Wired Relay mode, or one-click IPv6 distribution." +msgstr "修改設備區域網 IP,或切換至旁路由(AP有線中繼)模式,一鍵下發IPv6。" + +msgid "Reading underlying config..." +msgstr "正在讀取底層設定..." + +msgid "Back to Home" +msgstr "返回首頁" + +msgid "Configure WAN" +msgstr "設定外網 (WAN)" + +msgid "Connection Type" +msgstr "連線方式" + +msgid "DHCP (Auto)" +msgstr "自動取得 IP (DHCP)" + +msgid "Static IP" +msgstr "靜態 IP" + +msgid "Gateway" +msgstr "閘道器" + +msgid "e.g., 192.168.1.2" +msgstr "例如:192.168.1.2" + +msgid "e.g., 192.168.1.1" +msgstr "例如:192.168.1.1" + +msgid "PPPoE Credentials" +msgstr "PPPoE 撥號資訊 (WAN)" + +msgid "PPPoE Username" +msgstr "寬頻帳號" + +msgid "Enter PPPoE username" +msgstr "請輸入寬頻帳號" + +msgid "PPPoE Password" +msgstr "寬頻密碼" + +msgid "Enter PPPoE password" +msgstr "請輸入寬頻密碼" + +msgid "Configure LAN" +msgstr "設定區域網路 (LAN)" + +msgid "Safe Mode (Recommended ON)" +msgstr "安全模式 (建議開啟,防失聯)" + +msgid "Enable AP Wired Relay" +msgstr "啟用旁路由(AP有線中繼)模式" + +msgid "AP Wired Relay Enabled:
    1. DHCP will be disabled. Devices must use static IPs or get IPs from upstream.
    2. If LAN IP changes, ensure your client is in the same subnet to avoid losing access." +msgstr "AP 有線中繼模式已開啟:
    1. DHCP 將被禁用。設備必須使用靜態 IP 或由上級路由分配 IP。
    2. 若修改了區域網 IP,請確保設備與新 IP 處於同一網段,以免防失聯回滾觸發。" + +msgid "Main Router Mode Enabled:
    1. DHCP will be enabled. This device assigns IPs.
    2. If LAN IP changes, ensure your client is in the same subnet to avoid losing access." +msgstr "主路由模式已開啟:
    1. DHCP 服務將開啟,由本設備負責分配區域網 IP。
    2. 若修改了區域網 IP,請確保設備與新 IP 處於同一網段,以免防失聯回滾觸發。" + +msgid "Device LAN IP" +msgstr "設備區域網路 IP" + +msgid "LAN Gateway" +msgstr "區域網路閘道器" + +msgid "Auto Detect" +msgstr "一鍵探測" + +msgid "Upstream subnet detected, recommended IP assigned" +msgstr "已獲取上級網段,並為您分配推薦 IP" + +msgid "Detection failed, cannot get upstream gateway" +msgstr "探測失敗,無法獲取到上級網關資訊" + +msgid "Invalid Operation" +msgstr "操作無效" + +msgid "In Main Router mode, LAN Gateway MUST be empty.

    \"Auto Detect\" is only for AP/Relay mode!" +msgstr "當前為主路由模式,區域網路網關必須留空。

    「一鍵探測」僅用於配置「旁路由/AP有線中繼」模式!" + +msgid "Critical Intercept" +msgstr "極度危險攔截" + +msgid "System detected WAN is PPPoE Dial-up.

    The acquired {gw} is the ISP BRAS gateway!
    NEVER set this as LAN gateway, or your network will crash!" +msgstr "系統檢測到當前外網為 PPPoE 撥號。

    獲取到的 {gw} 是電信機房的遠程基地台網關!
    絕不能將其填入區域網路中,否則將導致網路癱瘓!" + +msgid "Blank for Main, required for AP Wired Relay" +msgstr "主路由留空,旁路由(AP有線中繼)必填" + +msgid "Back" +msgstr "上一步" + +msgid "Next Step" +msgstr "下一步" + +msgid "Back to Edit" +msgstr "返回修改" + +msgid "Confirm Configuration" +msgstr "確認設定資訊" + +msgid "The following settings will be applied, please verify:" +msgstr "即將套用以下網路設定,請仔細核對:" + +msgid "Application Notes:" +msgstr "套用須知:" + +msgid "After confirmation, the network will restart and apply new settings." +msgstr "確認後,系統將重啟網路服務以套用新設定。" + +msgid "The system will auto-refresh or redirect in 15 seconds." +msgstr "大約 15 秒後,頁面將自動重新整理或跳轉。" + +msgid "Apply Settings" +msgstr "確認並套用" + +msgid "AP Wired Relay" +msgstr "旁路由(AP有線中繼)模式" + +msgid "Current:" +msgstr "目前:" + +msgid "PPPoE Dial-up" +msgstr "寬頻撥號 (PPPoE)" + +msgid "DHCP Client (Auto IP)" +msgstr "自動獲取 (DHCP)" + +msgid "Static IP" +msgstr "靜態固定 IP (Static)" + +msgid "LAN Mode" +msgstr "區域網路模式" + +msgid "Device IP:" +msgstr "設備 IP:" + +msgid "Upstream GW:" +msgstr "上級閘道器:" + +msgid "Public IP:" +msgstr "公網 IP:" + +msgid "Remote GW:" +msgstr "遠端閘道器:" + +msgid "LAN IP:" +msgstr "區域網路 IP:" + +msgid "Status:" +msgstr "狀態:" + +msgid "Waiting for remote response" +msgstr "等待遠端回應..." + +msgid "WAN IP:" +msgstr "外網 IP:" + +msgid "Getting IP..." +msgstr "正在取得 IP..." + +msgid "DHCP Service:" +msgstr "DHCP 服務:" + +msgid "Enabled" +msgstr "已開啟" + +msgid "Disabled" +msgstr "已停用" + +msgid "Dial Success" +msgstr "撥號成功" + +msgid "Dialing / Disconnected" +msgstr "撥號中 / 未連線" + +msgid "IP Acquired" +msgstr "已取得 IP" + +msgid "Waiting for IP..." +msgstr "等待分配 IP..." + +msgid "Interface Connected" +msgstr "介面已連線" + +msgid "Cable Unplugged" +msgstr "網路線未插入" + +msgid "Getting..." +msgstr "取得中..." + +msgid "Not acquired" +msgstr "未取得" + +msgid "Not set" +msgstr "未設定" + +msgid "Incomplete info" +msgstr "資訊不完整" + +msgid "Device IP cannot be empty." +msgstr "設備 IP 不能為空。" + +msgid "Static IP and Gateway cannot be empty." +msgstr "靜態 IP 和閘道器不能為空。" + +msgid "PPPoE username and password cannot be empty." +msgstr "寬頻帳號和密碼不能為空。" + +msgid "Format Error" +msgstr "格式錯誤" + +msgid "The device IP is invalid, please check!" +msgstr "設備 IP 格式不正確,請檢查!" + +msgid "WAN IP is invalid, please check!" +msgstr "外網 IP 格式不正確,請檢查!" + +msgid "Gateway IP is invalid, please check!" +msgstr "閘道器 IP 格式不正確,請檢查!" + +msgid "Logic Error" +msgstr "邏輯錯誤" + +msgid "AP Wired Relay requires an upstream gateway IP." +msgstr "旁路由(AP有線中繼)模式下,閘道器為必填項。" + +msgid "WAN Static IP MUST NOT be the same as the gateway!" +msgstr "WAN 靜態 IP 絕對不能和閘道器一模一樣!" + +msgid "In AP/Relay mode, the device IP MUST NOT be the exact same as the Gateway! (" +msgstr "【旁路由 / 有線中繼 (AP)】模式下,本設備 IP 絕對不能與網關完全一樣但網段需要相同!(" + +msgid "Tip: The Gateway should be the LAN IP of your upstream main router, and this device needs its own unique IP)" +msgstr "提示:網關請填寫上級主路由的 LAN 口 IP 位址)" + +msgid "No Changes Needed" +msgstr "未發生更改" + +msgid "Your settings match the current router config exactly." +msgstr "您的設定與底層設定完全一致,無需重複下發。" + +msgid "Exit to Home" +msgstr "返回首頁" + +msgid "Conflict Blocked" +msgstr "設定衝突攔截" + +msgid "The WAN IP cannot be the same as the current LAN IP ({ip})!" +msgstr "WAN 埠 IP 不能與目前區域網路 IP ({ip}) 相同!" + +msgid "The WAN port cannot be in the same subnet as the LAN ({ip})!" +msgstr "WAN 埠不能與區域網路 ({ip}) 處於同一網段!" + +msgid "This causes a routing loop." +msgstr "這會導致嚴重的路由迴圈。" + +msgid "Suggestion: Your upstream network uses the same IP subnet. Please go to [LAN Settings] first and change your Device LAN IP (e.g. to 192.168.10.1) to prevent network crash." +msgstr "建議:您的上級網路使用了相同的 IP 網段。請先前往【區域網路設定】,修改本設備的區域網路 IP(例如改為 192.168.10.1)以防止網路癱瘓。" + +msgid "Subnet Error" +msgstr "網段錯誤" + +msgid "The WAN Static IP must be in the same subnet as the Gateway!" +msgstr "WAN 靜態 IP 必須與閘道器處於同一網段!" + +msgid "e.g., if gateway is {gw}, the IP must be {ip}.x" +msgstr "例如:如果閘道器是 {gw},則 IP 必須是 {ip}.x" + +msgid "In AP/Relay mode, the device IP must be in the same subnet as the Gateway! (" +msgstr "【旁路由 / 有線中繼 (AP)】模式下,本設備 IP 必須與網關處於同一網段!(" + +msgid "Tip: The Gateway should be the LAN IP of your upstream main router)" +msgstr "提示:網關請填寫上級主路由的 LAN 口 IP 位址)" + +msgid "Config Warning" +msgstr "高危操作警告" + +msgid "You selected [Main Router Mode] but filled in the [Gateway].

    For a standard main router, the gateway must be blank. Entering a gateway may cause the device to fail at distributing network, leading to a total outage!

    Are you sure you want to do this?" +msgstr "您選擇了【主路由模式】,但填寫了【閘道器】。

    作為主路由,閘道器必須留空。強行指定閘道器極易導致底層路由表錯亂,全屋斷網!

    您確定要繼續強行保存嗎?" + +msgid "Force Apply" +msgstr "確認強行保存" + +msgid "System Exception" +msgstr "系統異常" + +msgid "Cannot read underlying config for validation, please refresh." +msgstr "無法讀取底層設定進行防呆校驗,請重新整理頁面重試。" + +msgid "Applying Configuration" +msgstr "正在套用設定" + +msgid "Close" +msgstr "關閉" + +msgid "Account" +msgstr "帳號" + +msgid "Password" +msgstr "密碼" + +msgid "IP & Gateway" +msgstr "IP 與閘道器" + +msgid "Auto-assigned by upstream router" +msgstr "由上級路由自動分配" + +msgid "Target:" +msgstr "目標:" + +msgid "Modifying LAN IP" +msgstr "正在修改區域網路 IP" + +msgid "Switching to AP Wired Relay" +msgstr "正在切換至旁路由(AP有線中繼)模式" + +msgid "Switching WAN to DHCP" +msgstr "正在切換 WAN 埠為 DHCP (自動取得)" + +msgid "Switching WAN to Static IP" +msgstr "正在切換 WAN 埠為靜態 IP" + +msgid "Applying PPPoE Dial-up" +msgstr "正在套用 PPPoE 撥號" + +msgid "Writing configuration to system, please do not close the page..." +msgstr "正在將設定寫入系統,請勿關閉頁面..." + +msgid "Connecting to new IP... Config will auto-rollback upon timeout." +msgstr "正在嘗試連接新 IP... 失聯超時將會自動回滾配置" + +msgid "Waiting for network service to restart... Elapsed: {sec}s" +msgstr "正在等待網路服務重啟... 已耗時:{sec}秒" + +msgid "Waiting for router to safely restore... Elapsed: {sec}s" +msgstr "正在等待舊 IP 恢復... 已耗時:{sec}秒" + +msgid "Rollback countdown: {sec} / {total} s" +msgstr "防失聯倒數計時:{sec} / {total} 秒" + +msgid "If enabled, the system will auto-revert if you lose connection within 120s." +msgstr "開啟後,若修改 IP 導致失聯,系統將在 120 秒後自動回退恢復。" + +msgid "Safe mode disabled. Applying immediately without rollback protection..." +msgstr "已關閉防失聯保護,配置立刻生效。" + +msgid "If IP changed, please update PC IP. Auto-redirecting when connected..." +msgstr "若 IP 修改引發失聯,將設備 IP 改為同網段即可正常存取路由器,網路連通後將自動跳轉…" + +msgid "Waiting for router to abort changes and restore network..." +msgstr "正在等待路由器放棄修改並恢復網絡..." + +msgid "Wi-Fi Settings" +msgstr "Wi-Fi 設定" + +msgid "Set Wi-Fi name, password, enable Wireless Relay (WISP) and Seamless Roaming." +msgstr "設置WiFi帳號、密碼,開啟無線中繼 (WISP),開啟路由間無縫漫遊。" + +msgid "Configure Wi-Fi" +msgstr "設定 Wi-Fi" + +msgid "Smart Connect (All Bands)" +msgstr "多頻合一 (智能漫遊)" + +msgid "Enable Wi-Fi" +msgstr "啟用 Wi-Fi" + +msgid "Enable 2.4G Wi-Fi" +msgstr "啟用 2.4G Wi-Fi" + +msgid "Enable 5G Wi-Fi" +msgstr "啟用 5G Wi-Fi" + +msgid "e.g., My_WiFi" +msgstr "例如:My_WiFi" + +msgid "Network Name (SSID)" +msgstr "無線網路名稱 (SSID)" + +msgid "Wi-Fi Password" +msgstr "Wi-Fi 密碼" + +msgid "Encryption" +msgstr "加密方式" + +msgid "Enable 802.11b (Legacy Mode)" +msgstr "啟用 802.11b (舊版相容模式)" + +msgid "Only enable if very old IoT devices cannot connect." +msgstr "僅在極舊的智慧家電/物聯網設備無法連線時才建議開啟。" + +msgid "No Password (Open)" +msgstr "無密碼 (開放)" + +msgid "WPA2-PSK (Stable)" +msgstr "WPA2-PSK (穩定相容)" + +msgid "WPA3-SAE (Secure)" +msgstr "WPA3-SAE (高安全性)" + +msgid "WPA2/WPA3 Mixed (Recommended)" +msgstr "WPA2/WPA3 混合 (建議)" + +msgid "2.4G Wi-Fi" +msgstr "2.4G Wi-Fi" + +msgid "5G Wi-Fi" +msgstr "5G Wi-Fi" + +msgid "SSID cannot be empty." +msgstr "網路名稱 (SSID) 不能為空。" + +msgid "Wi-Fi password must be at least 8 characters." +msgstr "Wi-Fi 密碼長度必須至少為 8 個字元。" + +msgid "Applying Wi-Fi Settings" +msgstr "正在套用 Wi-Fi 設定" + +msgid "Advanced Settings" +msgstr "進階設定" + +msgid "Hide Advanced" +msgstr "收起進階設定" + +msgid "Hide Wi-Fi Name (SSID)" +msgstr "隱藏 Wi-Fi 名稱" + +msgid "Channel" +msgstr "頻道 (Channel)" + +msgid "Channel Width" +msgstr "通道寬度 (頻寬)" + +msgid "Auto" +msgstr "自動" + +msgid "Wireless Mode" +msgstr "無線模式 (Mode)" + +msgid "Severe Warning" +msgstr "嚴重警告" + +msgid "Forcibly modifying the wireless physical mode may cause the hardware driver to crash or the Wi-Fi to disappear permanently if the chip does not support it! It is highly recommended to keep it on [Auto].

    Are you absolutely sure you want to change this?" +msgstr "如果強行指定的物理模式與網卡晶片實際支援的能力不匹配,將導致底層驅動直接崩潰、Wi-Fi 永久消失!強烈建議保持為【自動 (推薦)】。

    您確定要強行修改模式嗎?" + +msgid "Enable IPv6 (DHCPv6)" +msgstr "啟用 IPv6 (DHCPv6)" + +msgid "Wi-Fi Status" +msgstr "Wi-Fi 狀態" + +msgid "5G Wi-Fi Account" +msgstr "5G Wi-Fi 帳號" + +msgid "2.4G Wi-Fi Account" +msgstr "2.4G Wi-Fi 帳號" + +msgid "No Password" +msgstr "無密碼" + +msgid "Enable Wireless Relay (WISP)" +msgstr "啟用無線中繼 (WISP)" + +msgid "Receive upstream Wi-Fi and broadcast your own network." +msgstr "接收上級 Wi-Fi 信號,並廣播您自己的專屬網絡。" + +msgid "WISP Enabled" +msgstr "已開啟無線中繼" + +msgid "Scan Nearby Wi-Fi" +msgstr "掃描附近 Wi-Fi" + +msgid "Select Upstream Network" +msgstr "選擇上級網絡" + +msgid "Password for upstream:" +msgstr "上級 Wi-Fi 密碼:" + +msgid "Scanning..." +msgstr "正在掃描..." + +msgid "No networks found." +msgstr "未找到任何網絡。" + +msgid "Scan failed. Driver might be busy." +msgstr "掃描失敗。無線驅動可能繁忙,請重試。" + +msgid "Target Wi-Fi" +msgstr "目標 Wi-Fi 名稱" + +msgid "Upstream Wi-Fi Password" +msgstr "請輸入上級 Wi-Fi 的密碼" + +msgid "802.11k/v/r Fast Roaming" +msgstr "802.11k/v/r 無縫漫遊" + +msgid "Enable seamless roaming between routers with one click (Prerequisite: Same SSID, password, and LAN). Note: May cause connection issues with older smart home (IoT) devices." +msgstr "一鍵開啟路由器間的無縫切換(基本前提:帳號、密碼一樣 + 同一區域網路)。注意:可能導致老舊的智慧家庭(IoT)設備無法連線。" + +msgid "Manual Configuration Warning" +msgstr "手動設定警告" + +msgid "Underlying parameter mismatch detected, which may cause roaming failures. Please toggle this switch off and on again, then save to apply the standard seamless roaming profile." +msgstr "檢測到底層參數與預設不一致,可能會導致漫遊失敗。建議將開關關閉後重新開啟,儲存後即可加入多設備全空間「絲滑」跳轉的「朋友圈」。" + +msgid "Roaming" +msgstr "漫遊" + +msgid "Roaming Enabled" +msgstr "已開通漫遊" + +msgid "Click to Fix" +msgstr "點擊修復" + +msgid "Click to Settings" +msgstr "點擊直達設定" + +msgid "WAN Blind-Switch: Unplug the WAN cable for 10 seconds and reconnect to auto-detect and switch the connection type (takes about 2 mins)." +msgstr "盲切WAN 口上網方式:斷開 WAN 網線 10 秒再插回,系統會自動偵測並切換上網方式(過程約 2 分鐘)" + +msgid "New Config" +msgstr "新建" + +msgid "Modified" +msgstr "新修改" + +msgid "Security Warning" +msgstr "無密碼安全警告" + +msgid "You are setting up an Open Wi-Fi network without a password. Anyone nearby will be able to connect and access your network.

    Are you sure you want to continue?" +msgstr "您正在設定無密碼的開放 Wi-Fi,附近任何人都可以隨意連線並存取您的區域網路設備。

    您確定要繼續嗎?" + +msgid "Set your wireless network name and password." +msgstr "設定您的無線網路名稱與密碼。" + +msgid "Skip this time" +msgstr "略過本次引導" + +msgid "Don't show this again" +msgstr "不再提示" + +msgid "Reopen Wizard" +msgstr "初始精靈" + +msgid "Skip Wi-Fi Setup (Keep current)" +msgstr "暫不設定 Wi-Fi (保持現狀)" + +msgid "Not configured (Keep current)" +msgstr "暫不設定 (保持當前狀態)" + +msgid "Not set" +msgstr "未設定" + +msgid "No Password (Open)" +msgstr "無密碼 (Open)" + +msgid "Wizard execution failed" +msgstr "向導執行失敗" + +msgid "Configuration data processing failed" +msgstr "配置數據處理失敗" + +msgid "Input validation failed" +msgstr "輸入校驗失敗" + +msgid "Device Network Manager" +msgstr "裝置網路管家" + +msgid "Terminal Device Monitoring & Static IP Management" +msgstr "終端裝置狀態監控與固定 IP 管理" + +msgid "Rescan Network" +msgstr "重新掃描網路" + +msgid "Refresh" +msgstr "重新整理" + +msgid "Syncing underlying network data..." +msgstr "正在同步底層網絡數據..." + +msgid "All" +msgstr "全部" + +msgid "Mobile/Tablet" +msgstr "手機平板" + +msgid "PC/Work" +msgstr "電腦辦公" + +msgid "Smart Home" +msgstr "智慧家電" + +msgid "Others" +msgstr "其它" + +msgid "Select all available devices" +msgstr "全選可用裝置" + +msgid "Selected" +msgstr "已選擇" + +msgid "items" +msgstr "項" + +msgid "Batch Unbind" +msgstr "批次解除綁定" + +msgid "Batch Assign" +msgstr "批次分配" + +msgid "Device Alias" +msgstr "裝置備註名" + +msgid "e.g., John's PC" +msgstr "例如: 小明的電腦" + +msgid "Quick Assign Strategy (Click to auto-fill)" +msgstr "快速分配策略 (點擊自動計算填入)" + +msgid "Keep Original IP" +msgstr "保持原 IP" + +msgid "Smart Categorization" +msgstr "智慧分類" + +msgid "Sequential Memory" +msgstr "記憶順延" + +msgid "Assign Static IP (Auto anti-conflict)" +msgstr "分配固定 IP (自動防衝突)" + +msgid "e.g., 192.168.1.50" +msgstr "例如: 192.168.1.50" + +msgid "Select Assign Strategy" +msgstr "選擇分配策略" + +msgid "Keep Current IP (Recommended)" +msgstr "保持目前 IP (安全推薦)" + +msgid "Convert the device's current random IP to a permanent static IP" +msgstr "直接將裝置目前的隨機 IP 轉為永久固定" + +msgid "Smart Subnet (By Device Type)" +msgstr "智慧分段 (按裝置類型)" + +msgid "Auto-identify mobile/PC, assign to exclusive subnets" +msgstr "自動識別手機/電腦等,分別放入專屬網段" + +msgid "Specify Starting IP (Sequential)" +msgstr "指定起始 IP (順延分配)" + +msgid "Enter starting suffix, sequentially assign to all selected devices" +msgstr "填寫起始後綴,所有選中裝置依序順延分配" + +msgid "Starting IP" +msgstr "分配起始 IP" + +msgid "Others:" +msgstr "其它裝置:" + +msgid "Mobile/Tablet:" +msgstr "手機平板:" + +msgid "PC/Work:" +msgstr "電腦辦公:" + +msgid "Smart Home:" +msgstr "智慧家電:" + +msgid "Cancel" +msgstr "取消" + +msgid "OK" +msgstr "確定" + +msgid "No device records in this category" +msgstr "該分類下沒有裝置紀錄" + +msgid "Online" +msgstr "連線" + +msgid "Offline" +msgstr "離線" + +msgid "Unoperable" +msgstr "不可操作" + +msgid "Long-term Offline" +msgstr "長期離線" + +msgid "Static" +msgstr "靜態" + +msgid "Upstream Gateway" +msgstr "上層閘道" + +msgid "Local System" +msgstr "本機系統" + +msgid "System Core Route" +msgstr "系統核心路由" + +msgid "System Reserved Device" +msgstr "系統保留裝置" + +msgid "Edit" +msgstr "修改" + +msgid "Unbind" +msgstr "解除綁定" + +msgid "Quick Bind" +msgstr "一鍵固定" + +msgid "Unknown Device" +msgstr "未知裝置" + +msgid "Unknown IP" +msgstr "未知IP(無外網)" + +msgid "Terminal Addressed IP" +msgstr "終端自簽IP" + +msgid "Edit Device Info" +msgstr "修改裝置資訊" + +msgid "Quick Bind IP" +msgstr "一鍵固定 IP" + +msgid "Configuring MAC:" +msgstr "正在為 MAC: " + +msgid "Save Changes" +msgstr "儲存修改" + +msgid "Bind Device" +msgstr "綁定此裝置" + +msgid "Safely writing and restarting services..." +msgstr "正在安全寫入並無感重啟服務..." + +msgid "Unbind IP" +msgstr "解除 IP 綁定" + +msgid "Are you sure you want to unbind the static IP for device ({mac})?" +msgstr "確定要解除對該裝置 ({mac}) 的靜態 IP 綁定嗎?" + +msgid "Confirm Unbind" +msgstr "確認解綁" + +msgid "Releasing static binding..." +msgstr "正在釋放靜態綁定..." + +msgid "Batch Unbind Devices" +msgstr "批次解綁裝置" + +msgid "Among the {total} selected devices, {valid} have fixed IPs.

    Are you sure you want to unbind them?" +msgstr "選中的 {total} 台裝置中,有 {valid} 台目前已固定 IP。

    確定要解除它們的綁定嗎?" + +msgid "Batch releasing IPs, please wait..." +msgstr "正在批次釋放 IP,請稍候..." + +msgid "Batch Assign Settings" +msgstr "批次分配設定" + +msgid "Select Assign Strategy (Selected {count} devices)" +msgstr "選擇分配策略 (已選取 {count} 台裝置)" + +msgid "Start Assign" +msgstr "開始分配" + +msgid "Avoiding conflicts and silently writing rules..." +msgstr "正在雷達避衝突並靜默寫入規則..." + +msgid "No device records found in current LAN" +msgstr "目前區域網路內未發現任何裝置紀錄" + +msgid "Scan failed: Cannot retrieve underlying data" +msgstr "掃描失敗:無法獲取底層資料" + +msgid "The IP pool for this type is full. Please expand the range or choose another strategy!" +msgstr "該類型的 IP 分段池已全部被佔滿,請擴大範圍或選擇其它策略!" + +msgid "IP address cannot be empty!" +msgstr "IP 位址不能為空!" + +msgid "IP Conflict Blocked!" +msgstr "IP 衝突攔截!" + +msgid "The IP [{ip}] is occupied by device [{name}]." +msgstr "您輸入的 IP [{ip}] 已被區域網路中的設備【{name}】佔用。" + +msgid "Please modify the strategy or enter a free IP!" +msgstr "請修改策略或輸入一個未被佔用的空閒 IP!" + +msgid "Tip: The device is already fixed at this IP. No changes needed!" +msgstr "提示:該裝置已經是目前 IP 且已固定,設定未發生任何改變,無需重複提交!" + +msgid "Tip: The {count} selected devices are all [Unbound]. No unbind action needed!" +msgstr "提示:您選取的 {count} 台裝置目前全是【未綁定】狀態,無需執行解綁操作!" + +msgid "Starting suffix must be between 2 and 254!" +msgstr "起始後綴必須在 2 到 254 之間!" + +msgid "IP pool insufficient!" +msgstr "IP 資源池不足!" + +msgid "Only {avail} IPs left from .{suf}, but {count} devices selected." +msgstr "從 .{suf} 開始僅剩 {avail} 個可用 IP,但您選了 {count} 台設備。" + +msgid "[{name}] assignment failed!" +msgstr "[{name}] 分配失敗!" + +msgid "Selected {req} devices, but only {avail} IPs available in the preset range." +msgstr "選中了 {req} 台設備,但預設網段內僅剩 {avail} 個可用 IP。" + +msgid "Please expand the IP range for this category!" +msgstr "請擴大該分類的 IP 範圍!" + +msgid "Smart block: According to your strategy, the {count} selected devices are already fixed with unchanged IPs. No need to rewrite rules!" +msgstr "智慧攔截:根據您的策略計算,選取的 {count} 台裝置【均已固定且 IP 未改變】,無需重複下發規則!" + +msgid "Infinite" +msgstr "永久" + +msgid "Expired" +msgstr "已過期" + +msgid "Terminal Device & IP Binding" +msgstr "終端裝置與 IP 綁定" + +msgid "Pending" +msgstr "待生效" + +msgid "Device Type (By IP)" +msgstr "類型(按IP)" + +msgid "Device Type (Built-in)" +msgstr "類型(內置)" + +msgid "Custom Groups" +msgstr "自定義組" + +msgid "Filter by IP Subnet" +msgstr "按 IP 網段歸類" + +msgid "Checked: Classify device types by IP subnet" +msgstr "勾選:依據 IP 網段來對設備分類" + +msgid "Unchecked: Classify device types by built-in names" +msgstr "取消:依據內建名稱來對設備分類" + +msgid "Alias" +msgstr "分組名稱" + +msgid "Group " +msgstr "分組 " + +msgid "Reason: RPC interface unresponsive ({err})" +msgstr "原因:RPC 介面無回應 ({err})" + +msgid "Please run this command in router SSH to restart the service: /etc/init.d/rpcd restart" +msgstr "請在路由器 SSH 執行此指令重啟服務:/etc/init.d/rpcd restart" + +msgid "Subnet Conflict: IP ranges between groups cannot overlap!" +msgstr "網段衝突:不同分組間的 IP 範圍不能有重疊!" + +msgid "Conflicting groups: {groups}" +msgstr "衝突組:{groups}" + +msgid "Save Failed: Group names cannot be duplicated!" +msgstr "儲存失敗:分組名稱不能重複!" + +msgid "Duplicate name: {name}" +msgstr "重複名稱:{name}" + +msgid "Save Failed: IPs must be between 2-254 and format must be valid!" +msgstr "保存失敗:IP 必須在 2-254 之間,且存在格式錯誤!" + +msgid "IP pool reached the end (254). Cannot auto-append. Please arrange subnets manually!" +msgstr "IP 段已分配到末尾 (達到254),無法繼續自動追加,請手動整理網段!" + +msgid "Visiting" +msgstr "正在訪問" + +msgid "Department Network Segments" +msgstr "自定義組IP選號池" + +msgid "+ Add New Department" +msgstr "+ 新建分組" + +msgid "Group Pool (Auto Assign)" +msgstr "自定義分組網段 (自動發號)" + +msgid "Assign free IPs automatically from the selected Target Group's specific IP range" +msgstr "自動從下方選擇的“目標分組”所設定的專屬 IP 段中提取空閒 IP 進行分配" + +msgid "Target Department" +msgstr "目標分組" + +msgid "Group IP Pool" +msgstr "自定義分組專屬網段" + +msgid "The IP pool for the selected department is full! Please expand the range." +msgstr "該分組的 IP 池已全部分配完畢!請前往“管理分組”擴大網段範圍。" + +msgid "Uncategorized" +msgstr "未分組" + +msgid "Assign to Group (Optional)" +msgstr "歸入自定義組 (可選)" + +msgid "-- Uncategorized (None) --" +msgstr "-- 不加入任何組 (未分組) --" + +msgid "Strategy Error: Please select a Target Group first to allocate IPs from its pool!" +msgstr "策略錯誤:請先選擇一個“目標組”,以便從其專屬網段中分配 IP!" + +msgid "Manage device network access anytime, anywhere" +msgstr "隨時隨地,極簡管控該設備的網絡權限" + +msgid "Block Internet (WAN)" +msgstr "一鍵斷網 (禁止外網)" + +msgid "Instantly cut off internet access. LAN communication remains normal." +msgstr "立即斷開該設備訪問互聯網的權限。局域網內互通正常。" + +msgid "LAN Isolation (Anti-snooping)" +msgstr "局域網隔離 (接下級交換機的設備失效)" + +msgid "Block internal network access for this device to prevent scanning and privacy leaks." +msgstr "隔離該設備通過本路由器內網訪問權限,阻止內網掃描,防護隱私洩露。" + +msgid "DMZ Host (Expose to WAN)" +msgstr "DMZ 主機 (暴露到公網)" + +msgid "Forward all WAN ports to this device. Only one device allowed." +msgstr "將外網端口全轉發給該設備。僅能開啟一台設備。" + +msgid "Click to control access" +msgstr "點擊管控權限" + +msgid "Cannot enable DMZ: Device has no valid IP or is offline!" +msgstr "無法開啟 DMZ:該設備尚未獲取到有效 IP,或不在線!" + +msgid "Denied! Only one DMZ host allowed." +msgstr "拒絕!僅允許設定一台 DMZ 主機。" + +msgid "Device [" +msgstr "設備 [" + +msgid "] is currently the DMZ." +msgstr "] 目前已是 DMZ。" + +msgid "Please disable its DMZ first!" +msgstr "請先關閉該設備的 DMZ 功能!" + +msgid "Save failed!" +msgstr "儲存失敗!" + +msgid "Reason: RPC Error ({err})." +msgstr "原因:RPC 錯誤 ({err})。" + +msgid "Please run `/etc/init.d/rpcd restart` in SSH and try again!" +msgstr "請在 SSH 執行 `/etc/init.d/rpcd restart` 後再試一次!" + +msgid "Reason: {err}" +msgstr "原因:{err}" + +msgid "Please run `/etc/init.d/rpcd restart` in SSH" +msgstr "請在 SSH 執行 `/etc/init.d/rpcd restart`" + +msgid "Firewall & Access Control" +msgstr "防火牆與權限管控" + +msgid "Live Connections" +msgstr "實時連接數" + +msgid "Monitor real-time connections (Auto turn off in 3 mins)" +msgstr "監控設備實時並發連接數 (開啟3分鐘後自動關閉以節省性能)" + +msgid "Net Blocked" +msgstr "已斷網" + +msgid "Isolated" +msgstr "已隔離" + +msgid "DMZ Host" +msgstr "DMZ 主機" + +msgid "Conns" +msgstr "連接" + +msgid "Invalid IP format! Please enter a valid IPv4 address (e.g., 192.168.1.50)" +msgstr "IP格式錯誤!請輸入合法的IPv4位址 (例如: 192.168.1.50)" + +msgid "Public IPv6 (Click to copy):" +msgstr "公網 IPv6 (點擊即可複製):" + +msgid "First IPv6 address copied successfully:" +msgstr "首個 IPv6 地址已成功複製:" + +msgid "Export Groups" +msgstr "匯出自定義" + +msgid "Import Groups" +msgstr "匯入自定義" + +msgid "Import successful!" +msgstr "匯入成功!" + +msgid "Please verify and click [Save] below to apply." +msgstr "請檢查無誤後,點擊下方【儲存】按鈕生效。" + +msgid "Import failed!" +msgstr "匯入失敗!" + +msgid "Invalid or corrupted file format. Please select a valid JSON backup file." +msgstr "檔案格式錯誤或已損壞,請選擇正確的 JSON 備份檔。" + +msgid "Suspected Spoofed Device" +msgstr "疑似偽裝或新設備" + +msgid "Factory Reset" +msgstr "恢復出廠" + +msgid "Danger: Restore Default Network" +msgstr "危險操作:恢復初始網路" + +msgid "Are you sure you want to clear all static IPs and firewall rules set by Netwiz?

    This operation will delete all groups, blacklists, isolations, and DMZ settings, and restart network services." +msgstr "確定要清空所有由 Netwiz 網絡管家設定的靜態 IP 和防火牆規則嗎?

    此操作將刪除所有分組、黑名單、隔離和 DMZ 設定,並重啟網路服務。" + +msgid "Cleaning up and restarting network services..." +msgstr "正在大掃除並重啟網路服務..." + +msgid "Connection Radar Analysis" +msgstr "連線雷達分析" + +msgid "Diving into system kernel to capture connection records..." +msgstr "正在潛入系統內核抓取連線記錄..." + +msgid "Web/HTTPS" +msgstr "網頁/常規" + +msgid "DNS/NTP" +msgstr "DNS解析" + +msgid "UDP Stream/Game" +msgstr "UDP串流/遊戲" + +msgid "P2P/High Ports" +msgstr "P2P下載/掛機" + +msgid "Others/Unknown" +msgstr "其他/未知" + +msgid "# Successfully parsed {total} source connection records..." +msgstr "# 成功解析到 {total} 條源連線記錄..." + +msgid "root@OpenWrt:~# cat /proc/net/nf_conntrack | awk '...' # (Filtering src={ip})" +msgstr "root@OpenWrt:~# cat /proc/net/nf_conntrack | awk '...' # (過濾源IP={ip})" + +msgid "[ No active connection records ]" +msgstr "[ 暫無活動連線記錄 ]" + +msgid "... (First 50 records shown for performance) ..." +msgstr "... (已截斷顯示前 50 條數據以保障效能) ..." + +msgid "Analysis failed: {err}" +msgstr "抓取失敗: {err}" + +msgid "Export Config" +msgstr "匯出配置" + +msgid "Import Config" +msgstr "匯入配置" + +msgid "Packing Network Config" +msgstr "正在打包網路配置" + +msgid "
    Generating exclusive backup file

    (Includes: WAN, Wi-Fi, IPv6, Bypass, Static IPs & Firewall rules)
    " +msgstr "
    正在生成專屬備份檔案

    (包含:寬頻、Wi-Fi、IPv6、旁路由、靜態IP及防火牆規則)
    " + +msgid "Please wait..." +msgstr "請稍候..." + +msgid "Export Successful" +msgstr "匯出成功" + +msgid "Core network configuration file has been successfully downloaded!" +msgstr "核心網路配置檔案已成功下載!" + +msgid "Export Failed" +msgstr "匯出失敗" + +msgid "Backend returned no valid data" +msgstr "後端未回傳有效數據" + +msgid "Error occurred: " +msgstr "發生錯誤: " + +msgid "Confirm Overwrite?" +msgstr "確認覆蓋網路配置?" + +msgid "About to overwrite the router's core network settings!

    " +msgstr "即將覆蓋路由器的核心網路設定!

    " + +msgid "Safety Mechanism: The system will automatically backup the current state to the /root/netwiz_bak/ directory before importing.

    Network services will automatically restart after import, which may cause brief Wi-Fi/LAN disconnection. Please confirm." +msgstr "安全機制:在匯入前,系統會自動將當前狀態備份到 /root/netwiz_bak/ 目錄。


    匯入後網路服務將自動重啟,可能會導致 Wi-Fi/LAN 短暫斷開,請確認。" + +msgid "Confirm & Apply" +msgstr "確認匯入並套用" + +msgid "Writing configuration and restarting network services,
    Please wait... Network may disconnect briefly, taking about 10-25 seconds..." +msgstr "正在寫入配置並重啟網路服務,
    請稍候... 網路可能會短暫斷開,大約需要 10-25 秒..." + +msgid "Restore Network Config" +msgstr "恢復網路配置" + +msgid "
    Reading router backup records...
    " +msgstr "
    正在讀取路由器備份記錄...
    " + +msgid "[ No history backups found ]" +msgstr "[ 暫無歷史備份記錄 ]" + +msgid "Restore from Router History" +msgstr "從路由器歷史備份恢復" + +msgid "Restore Selected Backup" +msgstr "恢復選中的備份" + +msgid "— OR —" +msgstr "— 或者 —" + +msgid "Upload from Local PC" +msgstr "從本機電腦上傳恢復" + +msgid "Browse PC Files..." +msgstr "瀏覽電腦檔案..." + +msgid "Read Failed" +msgstr "讀取失敗" + +msgid "Unable to fetch router backup list." +msgstr "無法獲取路由器備份列表。" + +msgid "Auto Backup" +msgstr "自動靜默備份" + +msgid "Before Import" +msgstr "匯入配置前備份" + +msgid "Before Reset" +msgstr "重置網路前備份" + +msgid "-- Keep Unchanged --" +msgstr "-- 保持不變 --" + +msgid "Reserved" +msgstr "預留" + +msgid "Note:" +msgstr "說明:" + +msgid "Custom group names are not strictly bound to IP subnets. The ranges are only used for automatic IP assignment when a group is selected." +msgstr "自訂群組的名稱不會強制綁定 IP 網段,網段也不受名稱限制,僅用於選擇自訂群組時自動發號到對應網段。" + +msgid "When enabled, [Terminal Device & IP Binding] can assign short IPv6 addresses to PCs." +msgstr "聯動【終端設備與 IP 綁定】固定 IP,電腦享受短 IPv6" + +msgid "Primary DNS:" +msgstr "主 DNS:" + +msgid "Secondary DNS:" +msgstr "備 DNS:" + +msgid "Non-standard parameters (Default configuration recommended)" +msgstr "與預設參數不一致 (建議使用預設設定)" + +msgid "Cable Unplugged or Wrong Port" +msgstr "網線未連接 或 插錯網口" + +msgid "System detected NO SIGNAL on the WAN port!

    " +msgstr "系統檢測到路由器的 WAN 口(外網口) 沒有信號!

    " + +msgid "Troubleshooting:
    1. Did you plug the upstream cable into the LAN port?
    2. Are both ends plugged in tightly? Is the modem powered on?
    " +msgstr "新手常見錯誤排查:

    1. 您是不是把上級網線 插錯到了 LAN 口?
    2. 網線兩端是否已插緊?光貓/牆壁網口是否已通電?
    " + +msgid "Detecting in background... This will close automatically once connected." +msgstr "正在後台極速偵測... 插好後本窗口將自動消失" + +msgid "I am an AP / Ignore for now" +msgstr "我是旁路由(中繼)/ 暫不處理" + +msgid "Severe Warning: Routing Loop" +msgstr "嚴重警告:網絡衝突死環" + +msgid "System detected that the upstream network ({wan_ip}) and the LAN ({lan_ip}) are in the same subnet!

    This will crash the router. Please confirm or modify the new LAN IP below and click auto-evade:

    " +msgstr "系統實時檢測到,入戶網絡 ({wan_ip}) 與本路由器的局域網 ({lan_ip}) 處於同一網段!

    這將導致路由器徹底癱瘓。請確認或修改下方的新局域網 IP,並點擊一鍵避讓:

    " + +msgid "Modify and Restart Network" +msgstr "立即修改並重啟網絡" + +msgid "Invalid IP format! System restored safe default, please confirm." +msgstr "無效的 IP 格式!系統已恢復安全推薦值,請重新確認。" + +msgid "Still conflicting! The modified IP is still in the same subnet as upstream." +msgstr "仍然衝突!修改後的 IP 依然與上級網路處於同一網段。" + +msgid "System restored safe default, please change the 3rd number!" +msgstr "系統已恢復安全預設值,請嘗試修改第 3 段數字!" + +msgid "IMPORTANT:" +msgstr "重要提示:" + +msgid "To resolve the fatal network loop, the system must change LAN IP and restart immediately." +msgstr "為了解除致命的網路死環,系統必須立即修改區域網路 IP 並重啟網路。" + +msgid "Changes you made on this page will NOT be saved." +msgstr "您在此頁面上做出的其他修改將不會被保存。" + +msgid "Please reconfigure after the network restarts and redirects to the new IP." +msgstr "請在網路重啟並跳轉到新 IP 後,重新進行設定。" + +msgid "Continue to fix conflict?" +msgstr "是否繼續修復衝突?" + +msgid "Network Conflict Blocked" +msgstr "網絡衝突已攔截" + +msgid "Upstream network ({wan_ip}) conflicts with local LAN ({lan_ip})!

    Please click below to safely evade to: {safe_ip} to finish setup." +msgstr "檢測到上級網絡分配的地址 ({wan_ip}) 與本路由器的內網 ({lan_ip}) 發生嚴重衝突!

    請先點擊下方按鈕安全避讓至:{safe_ip},隨後即可正常完成嚮導。" + +msgid "Auto Evade" +msgstr "一鍵自動避讓" + +msgid "Cross-Subnet Loop Blocked" +msgstr "跨網段路由死環攔截" + +msgid "The LAN IP ({ip}) you set is in the same subnet as your upstream network ({wan_ip}), which will crash the router!

    Suggested evasion to: {safe_ip}" +msgstr "您設置的 LAN IP ({ip}) 與您的入戶網絡 ({wan_ip}) 處於同一網段,這會導致路由器癱瘓!

    建議一鍵避讓至:{safe_ip}" + +msgid "Fix and Apply" +msgstr "一鍵修復並應用" + +msgid "Edit Manually" +msgstr "我自己去改" + +msgid "Upstream network ({wan_ip}) conflicts with local LAN ({lan_ip})!

    You MUST change device LAN IP. Suggested evasion to: {safe_ip}" +msgstr "檢測到上級網絡分配的地址 ({wan_ip}) 與本路由器的內網 ({lan_ip}) 發生嚴重衝突!

    必須修改本設備的 LAN IP。系統建議安全避讓至:{safe_ip}" + +msgid "Severe Physical Conflict" +msgstr "嚴重物理衝突" + +msgid "The WAN Static IP ({ip}) you set is exactly the same as the upstream Gateway!

    This causes a severe physical loop. Please change your Static IP (e.g., {suggest_ip})." +msgstr "您設置的 WAN 靜態 IP ({ip}) 與上級網絡的真實網關完全相同!

    這將導致嚴重的物理死環。請修改您的靜態 IP(例如改為 {suggest_ip})。" + +msgid "The AP IP ({ip}) you set is exactly the same as the upstream Gateway!

    This will paralyze the network. Please change to another free IP (e.g., {suggest_ip})." +msgstr "您設置的旁路由 IP ({ip}) 與上級網絡的真實網關完全相同!

    這將導致全屋網絡癱瘓。請修改為同網段的其他空閒 IP(例如 {suggest_ip})。" + +msgid "Wake on LAN (WOL)" +msgstr "局域網喚醒 (WOL)" + +msgid "Wake packet sent to {mac}!" +msgstr "已向 {mac} 發送喚醒封包!" + +msgid "(Device booting may take a minute)" +msgstr "(設備開機可能需要一分鐘時間)" + +msgid "Quick Setup Wizard" +msgstr "快速設置嚮導" + +msgid "Step 1: Admin Password" +msgstr "第一步:管理員密碼" + +msgid "Step 2: Internet Setup" +msgstr "第二步:上網設置" + +msgid "Step 3: Wi-Fi Setup" +msgstr "第三步:Wi-Fi 設置" + +msgid "Step 4: Confirm & Apply" +msgstr "第四步:確認並套用" + +msgid "Confirm Password" +msgstr "確認密碼" + +msgid "Enter password again" +msgstr "請再次輸入密碼" + +msgid "Passwords do not match, please try again!" +msgstr "兩次輸入的密碼不一致,請重新檢查!" + +msgid "Router Admin Password (Optional)" +msgstr "路由器管理密碼 (可選)" + +msgid "Keep empty to retain current password" +msgstr "留空則保持目前密碼不變" + +msgid "Skip Wizard" +msgstr "跳過嚮導" + +msgid "Releasing wizard lock, entering official dashboard..." +msgstr "正在退出嚮導,進入後台..." + +msgid "This password will be used for logging into the router web interface and SSH. Setting it now is highly recommended." +msgstr "此密碼將用於日後登入路由器後台及 SSH,建議立即設定。" + +msgid "Please enter a new password or check \"Skip Password Setup\"!" +msgstr "請輸入新管理員密碼,或勾選「暫不配置」!" + +msgid "Smart Connect" +msgstr "多頻合一" + +msgid "Independent Bands" +msgstr "分頻獨立" + +msgid "WISP Repeater" +msgstr "中繼 (WISP)" + +msgid "Disabled" +msgstr "未開啟" + +msgid "Network Name (5G_Game)" +msgstr "無線網路名稱 (5G_Game)" + +msgid "Wi-Fi Password (5G_Game)" +msgstr "Wi-Fi 密碼 (5G_Game)" + +msgid "The underlying network status is not fully loaded. Please wait before submitting to prevent configuration loss." +msgstr "底層網路狀態尚未加載完畢,請等待頁面初始化完成後再提交,以防覆蓋丟失配置。" + +msgid "Configuration complete! Automatically logging you in..." +msgstr "配置完成!正在為您自動登入系統..." + +msgid "Password setup failed: " +msgstr "密碼設定失敗: " + +msgid "No Wi-Fi hardware detected, this step will be skipped automatically.
    Please click [Next Step] directly." +msgstr "未檢測到 Wi-Fi 硬體,本步驟自動跳過。
    請直接點擊右下角【下一步】。" + +msgid "Advanced & Lab Features" +msgstr "進階與實驗室功能" + +msgid "Beta" +msgstr "測試" + +msgid "Full Software Backup & Restore" +msgstr "全量軟體的備份與還原" + +msgid "Resolves traditional backup soft-brick risks. Smart backup is cross-version compatible. After flashing new firmware, upload backup to rapidly reinstall all software and losslessly restore configs (Requires Internet). Does not delete current software during restore. For absolute purity, factory reset the router first.
    Tip: If the backup shows Missing packages, manually place the installation packages in the /etc/netwiz/custom_pkgs/ directory.
    If Netwiz is missing after a reset or needs an upgrade, connect to the internet and run this in SSH:" +msgstr "解決傳統備份容易導致當機、軟磚風險,智能備份(需聯網)相容跨版本還原(不能跨硬體)。刷入新韌體後,上傳備份即可極速重裝所有軟體並無損還原設定。還原時不會刪除當前軟體。若需達到絕對純淨,請先還原原廠設定。
    「提示:若備份中顯示“缺失安裝包(未備妥)”,請手動將安裝包放置於 /etc/netwiz/custom_pkgs/ 目錄下。」
    恢復出廠後若未安裝 Netwiz 或需要陞級,可聯網後在 SSH 終端執行以下命令:" + +msgid "Generate Backup" +msgstr "產生備份檔案" + +msgid "Restore System" +msgstr "還原系統軟體" + +msgid "Click to Copy" +msgstr "點擊複製" + +msgid "Copied" +msgstr "已複製" + +msgid "Copy Failed" +msgstr "複製失敗" + +msgid "Select Backup Mode" +msgstr "選擇備份模式" + +msgid "Lightweight Backup" +msgstr "輕量化備份" + +msgid "Recommended" +msgstr "推薦模式" + +msgid "Full Backup" +msgstr "全量備份" + +msgid "Includes Dependencies" +msgstr "包含深層依賴" + +msgid "Lightweight Backup: Only packs core configs (Passwords/Wi-Fi/IP) and main plugin packages. Fast and requires no extra RAM. Requires internet or built-in dependencies when restoring." +msgstr "輕量化備份說明: 僅打包核心設定(帳號密碼/無線/IP)和主外掛包,不抓取底層深層依賴,速度極快。還原時新韌體需自帶對應依賴或處於聯網狀態。" + +msgid "Full Backup: Packs core configs and recursively downloads ALL underlying dependencies for third-party plugins. Large size, slower generation. NO internet required during restore. Recommended for devices with >256MB RAM." +msgstr "全量備份說明: 下載所有第三方外掛的完整底層依賴。體積龐大生成較慢,優點是還原時完全不需要網路即可重裝成功。建議記憶體 >256M 的設備使用。" + +msgid "Start Backup" +msgstr "確定開始備份" + +msgid "Generating Backup" +msgstr "正在產生備份" + +msgid "Extracting configs and packing files in background..." +msgstr "正在後台提取設定並打包檔案..." + +msgid "Note: Full mode selected, downloading all dependencies. Takes 1-3 mins, please wait." +msgstr "提示: 當前為全量模式,需要下載全部底層依賴。大約需 1 到 3 分鐘,請耐心等待。" + +msgid "Note: Lightweight mode selected, packing is very fast (~30s)." +msgstr "提示: 當前為輕量模式,打包速度極快。大約需要 30 秒左右。" + +msgid "Backup Successful" +msgstr "打包成功" + +msgid "Backup capsule downloaded. Upload this file after flashing new firmware to restore." +msgstr "備份膠囊已成功下載。刷入新系統後上傳此檔案即可還原。" + +msgid "Backup Failed" +msgstr "打包失敗" + +msgid "Unable to start background backup task." +msgstr "無法啟動後台備份任務" + +msgid "Confirm System Restore" +msgstr "確認還原系統" + +msgid "WARNING: This will overwrite current configs and reinstall plugins!

    Router will auto-reboot upon completion. Ensure file is correct.

    " +msgstr "警告:此操作將覆蓋現有的網路設定並重新安裝外掛!

    完成後路由器將自動重啟,請確保選擇的檔案無誤。

    " + +msgid "Safe Mode: Auto-rollback if connection is lost for 300s.

    " +msgstr "安全機制:若重啟後失聯 300 秒,系統將自動回退配置以防變磚。


    " + +msgid "Auto-download current state backup before restore (Regret Pill)" +msgstr "恢復前自動下載目前狀態備份 (後悔藥)" + +msgid "Confirm & Select File" +msgstr "確認並選擇檔案" + +msgid "Native Fast Restore Mode" +msgstr "原生極速還原模式" + +msgid "Transferring capsule via high-speed system channel...

    DO NOT power off. Router will auto-reboot upon completion.

    " +msgstr "正在呼叫系統底層高速通道直傳膠囊...

    請勿斷開電源,路由器將在完成後自動重啟。

    " + +msgid "Safe Mode: Auto-rollback if connection is lost for 300s." +msgstr "安全機制:若重啟後失聯 300 秒,系統將自動回退配置以防變磚。
    " + +msgid "Capsule delivered, preparing offline task..." +msgstr "膠囊已送達底層,準備執行脫機任務..." + +msgid "Restore Blocked by Security" +msgstr "還原異常被安全攔截" + +msgid "Self-healing mechanism triggered. Garbage cleared, router is safe and unharmed." +msgstr "系統已啟動自癒機制清理了垃圾殘留,您的路由器目前處於安全狀態,並未受損。" + +msgid "Transfer Failed" +msgstr "傳輸失敗" + +msgid "Interface rejected reception, status code: " +msgstr "底層接口拒絕接收,狀態碼: " + +msgid "Network Error" +msgstr "網路錯誤" + +msgid "File transfer interrupted unexpectedly, check network!" +msgstr "檔案傳輸被意外中斷,請檢查網路連線!" + +msgid "Preparing Regret Pill" +msgstr "正在為您調配後悔藥" + +msgid "Backing up current state to prevent regrets...

    Tip: Restore will begin automatically after download" +msgstr "正在備份當前系統狀態以防還原後反悔

    提示: 備份下載完成後將自動開始覆蓋並還原新系統" + +msgid "Probing Environment" +msgstr "正在探測運行環境" + +msgid "Calculating system available RAM and storage..." +msgstr "正在計算系統可用記憶體與儲存空間..." + +msgid "Critical RAM Shortage" +msgstr "實體記憶體嚴重不足" + +msgid "Your full capsule size is {size} MB.

    But /tmp (RAM) only has {avail} MB free!

    Force uploading will cause OOM, crash, and network drop.

    Solution: Please reboot the router once to clear RAM fragments, then retry." +msgstr "檢測到您的全量膠囊大小為 {size} MB。

    但當前路由器的 /tmp (運行記憶體) 僅剩餘 {avail} MB 可用空間!

    強行上傳將導致路由器記憶體溢出、當機並斷網。

    解決方案:請先重啟一次路由器來清空記憶體碎片,重啟後再進行還原操作。" + +msgid "Cancel Restore" +msgstr "取消還原" + +msgid "Waiting for background task to start..." +msgstr "等待後台任務啟動..." + +msgid "Capsule delivered, initializing restore environment..." +msgstr "膠囊已送達,正在初始化還原環境..." + +msgid "Scanning capsule structure to estimate uncompressed size (5-10s)..." +msgstr "正在掃描膠囊深層結構以預估解壓體積 (約需 5-10 秒)..." + +msgid "Uncompressed size reaches {u}MB, but RAM only has {a}MB left! Intercepted to prevent router crash." +msgstr "解壓體積達 {u}MB,但記憶體僅剩 {a}MB!為防止當機,已觸發安全攔截。" + +msgid "Size safe! Extracting backup capsule..." +msgstr "體積安全!正在解壓備份膠囊..." + +msgid "Extraction failed! Capsule may be corrupted, initiating self-healing..." +msgstr "解壓失敗!可能是壓縮包損壞,已啟動自癒機制清理記憶體。" + +msgid "Extraction successful, applying core configs..." +msgstr "解壓成功,正在覆蓋系統核心設定..." + +msgid "Force offline installing plugins (this takes a while, please wait)..." +msgstr "正在強制離線覆蓋外掛 (此過程較長,請耐心等待)..." + +msgid "Restore thoroughly complete! Router will auto-reboot!" +msgstr "還原徹底完成!路由器即將自動重啟!" + +msgid "MSG_RST_INVALID" +msgstr "無效的膠囊檔案!系統未檢測到 NetWiz 專屬特徵碼,為保護系統安全已自動攔截並銷毀。" + +msgid "File Too Large" +msgstr "檔案過大" + +msgid "
    Config files are usually under 100KB!

    The selected file is too large and has been blocked." +msgstr "
    配置檔案通常小於 100KB!

    您選擇的檔案過大,已被安全攔截。" + +msgid "Restore Network Config" +msgstr "還原網路配置" + +msgid "This action will overwrite the existing network configuration and restart the network.

    " +msgstr "此操作將覆蓋現有的網路設定並重啟網路。

    " + +msgid "Security Warning: Do not upload backups from unknown sources to prevent router hijacking.

    Confirm restore?" +msgstr "安全警告:請勿上傳來源不明的備份,以防路由器被劫持。


    確認還原?" + +msgid "Restoring" +msgstr "正在還原" + +msgid "Restore Failed" +msgstr "還原失敗" + +msgid "Error: " +msgstr "錯誤: " + +msgid "Invalid Format" +msgstr "格式無效" + +msgid "The uploaded file is not a valid JSON format and cannot be parsed!" +msgstr "您上傳的檔案不是有效的 JSON 格式,無法解析!" + +msgid "Import Configuration" +msgstr "匯入配置" + +msgid "Are you sure you want to import this configuration? Existing data will be overwritten." +msgstr "確認要匯入該配置嗎?現有資料將被徹底覆蓋。" + +msgid "Severe Network Topology Conflict!" +msgstr "嚴重網路拓撲衝突!" + +msgid "System detected that IPv6 and LAN \"Masquerading (NAT)\" are BOTH enabled!This will paralyze IPv6 allocation and cause routing loops.
    %sFix: Please go to Network -> Firewall to disable LAN Masquerading, or Disable IPv6 in the LAN settings on the Netwiz homepage." +msgstr "系統偵測到 IPv6 與區域網路「IP 動態偽裝 (NAT)」同時開啟!這將導致 IPv6 分配癱瘓及路由迴圈。
    %s修復方法:請前往 網路 -> 防火牆 關閉區域網路的動態偽裝,或者在 Netwiz 首頁的區域網路設定中關閉 IPv6。" + +msgid "IPv6 Configuration Blocked" +msgstr "IPv6 配置已被系統安全攔截" + +msgid "Detected that LAN \"IP Masquerading (NAT)\" is enabled. Forcing IPv6 on under a double-NAT topology will cause network disconnection.
    %sFix: Please go to Network -> Firewall -> Zones to disable LAN Masquerading first." +msgstr "偵測到區域網路「IP 動態偽裝 (NAT)」已開啟。在雙重 NAT 拓撲下強制開啟 IPv6 會導致斷網。
    %s修復方法:請先前往 網路 -> 防火牆 -> 區域 關閉區域網路動態偽裝。" + +msgid "Restore Device Data" +msgstr "還原設備資料" + +msgid "This action will overwrite existing device groups and static IP bindings, and restart the network.

    " +msgstr "此操作將覆蓋現有的設備分組和靜態 IP 綁定,並重啟網路。

    " + +msgid "Security Warning: Do not upload backups from unknown sources to prevent DNS hijacking.

    Confirm to restore?" +msgstr "安全警告:請勿上傳來源不明的備份,以防 DNS 被劫持。


    確認還原?" + +msgid "Success" +msgstr "成功" + +msgid "Warning" +msgstr "警告" + +msgid "Waking up..." +msgstr "喚醒中..." + +msgid "Displaying self-assigned local IPv6 as IPv4 is unavailable" +msgstr "由於無法獲取 IPv4,顯示設備底層自簽的區域 IPv6" + +msgid "Local IPv6" +msgstr "自簽 IPv6" + +msgid "System is rebooting, please wait..." +msgstr "系統重啟中,請稍候..." + +msgid "Waiting for device to disconnect..." +msgstr "等待設備斷開連接..." + +msgid "IP Conflict Warning" +msgstr "IP 衝突警告" + +msgid "is already used by another device!" +msgstr "已被區域網內其他設備佔用!" + +msgid "We strongly recommend changing it to avoid network crashes." +msgstr "強烈建議更改為新 IP,以防止網路癱瘓或斷網。" + +msgid "Fix to" +msgstr "一鍵修復為" + +msgid "Scanning installed plugins..." +msgstr "正在掃描已安裝的插件清單..." + +msgid "Plugin Backup Status" +msgstr "插件備份狀態檢查" + +msgid "Please verify backup packages:" +msgstr "請核對備份安裝包:" + +msgid "If you proceed, missing plugins WILL NOT be restored automatically!" +msgstr "如果現在執行備份,缺失安裝包在還原時將無法被恢復!" + +msgid "Tip: Manually place missing packages .ipk/.apk into the /etc/netwiz/custom_pkgs/ directory via SSH to ensure they are automatically reinstalled during future restorations." +msgstr "建議:透過 SSH 將缺失的安裝包 .ipk 或 .apk 放入 /etc/netwiz/custom_pkgs/ 目錄,還原時會自動恢復當前狀態。" + +msgid "Ignore & Backup" +msgstr "忽略並備份" + +msgid "Missing packages:" +msgstr "缺失安裝包(未備妥):" + +msgid "Manually placed in custom_pkgs:" +msgstr "手動放入 custom_pkgs 插件:" + +msgid "Custom Plugins Ready" +msgstr "自定義插件已就緒" + +msgid "Great! Your custom plugins are safely stored in the local directory and will be included in the backup capsule." +msgstr "恭喜!已將所有自訂外掛離線包存入本地保險箱,後續將統一包裝封裝進備份膠囊。" + +msgid "Confirm Backup" +msgstr "確定備份" + +msgid "Auto-backup plugins:" +msgstr "可自動備份插件:" + +msgid "Plugin Scan Complete" +msgstr "插件掃描完成" + +msgid "System scan complete! All your installed plugins are from the official repository and will be safely recorded for automatic restoration." +msgstr "系統掃描完畢!您安裝的所有第三方可視化插件,均存在於當前軟體源中。它們將被安全記錄並在未來自動聯網恢復。" + +msgid "Backup Interrupted Warning" +msgstr "備份中斷警告" + +msgid "Out of Memory (OOM Protection)!" +msgstr "記憶體不足 (OOM 預防機制)!" + +msgid "The files you are trying to pack are too large and exceed the available memory.

    To prevent device crash, the backup task has been safely canceled. Please clear unnecessary core files and try again." +msgstr "您要打包的檔案體積過大,超過了路由器當前的可用記憶體。

    為防止設備死機,備份任務已自動安全取消。請嘗試清理無用的核心檔案後重試。" + +msgid "I Got It" +msgstr "我知道了" + +msgid "Scan Timeout Warning" +msgstr "掃描超時警告" + +msgid "Communication Timeout or Network Error" +msgstr "通訊超時或網路異常" + +msgid "Communication with the router timed out (possibly due to slow network source retrieval or network fluctuations).

    To ensure backup integrity, the task has been safely canceled. Please try again later, or manually run opkg update via SSH." +msgstr "與路由器的通訊超時 (可能是網路源拉取過慢或網路波動)。

    為保證備份完整性,任務已安全取消。請稍後再試,或在 SSH 中手動執行 opkg update。" + +msgid "Connecting..." +msgstr "正在連接..." + +msgid "Connecting to upstream... (Check password or signal strength if stuck)" +msgstr "正在連接上級網路... (若一直連接不上請檢查密碼是否正確或是否上級訊號太弱)" + +msgid "First Time Syncing" +msgstr "首次核實插件清單" + +msgid "Syncing lists in the background..." +msgstr "正在後台拉取最新列表..." + +msgid "Verifying the list of plugins for automatic backup...

    Depending on the network, this usually takes 15 to 20 seconds.
    The system is verifying, please wait..." +msgstr "正在核實可以自動備份的插件清單...

    受限於網路,這通常需要 15 ~ 20秒。
    系統正在核實,請稍候..." + +msgid "OK, I will try again later" +msgstr "停止備份" + +msgid "RESTORE FAILED: Package manager mismatch (apk vs ipk). Please use firmware with the same underlying system." +msgstr "恢復失敗:套件管理員格式衝突!您的備份包與當前系統的底層不相容(apk 與 ipk 衝突),強行恢復將導致系統損壞,進程已被系統強制攔截!" + +msgid "RESTORE FAILED: CPU Architecture mismatch. Forcing this restore will brick your router! Process aborted." +msgstr "恢復致命攔截:CPU 架構不匹配!您正在嘗試將其他架構的備份強行刷入當前路由器,這會導致路由器徹底變磚!系統已自動終止恢復進程。" + +msgid "Package Manager Conflict" +msgstr "軟體包管理器衝突" + +msgid "CPU Architecture Conflict" +msgstr "CPU 架構衝突" + +msgid "Your current system uses the apk architecture, but you are trying to flash an ipk backup!" +msgstr "您當前的系統使用 apk 架構,但您正嘗試刷入一個 ipk 格式的備份包!" + +msgid "Your current system uses the opkg architecture, but you are trying to flash an apk backup!" +msgstr "您當前的系統使用 opkg 架構,但您正嘗試刷入一個 apk 格式的備份包!" + +msgid "Architecture Warning" +msgstr "架構標識未匹配" + +msgid "No architecture identifier ({arch}) detected in the selected backup filename." +msgstr "您選中的備份包名稱中,未檢測到當前路由器的架構標識 ({arch})。" + +msgid "If you have manually renamed this file, please ignore this warning.

    If it is the wrong package, the system's underlying security mechanism will forcibly intercept the restoration later!" +msgstr "如果您手動修改過該備份包的檔案名稱,請忽略此警告。

    如果您選錯了包,稍後的強行恢復將會被系統底層安全機制強制攔截!" + +msgid "I understand, continue" +msgstr "我知道了,繼續" + +msgid "Scan to Connect" +msgstr "手機相機掃碼直連" + +msgid "Architecture Safety Lock: Cross-package manager restoration (e.g., IPK to APK) is strictly prohibited. The underlying network and firewall configurations differ significantly across these system versions. Forcing a restore will cause severe network failure and brick your router!" +msgstr "架構安全鎖:嚴禁跨軟體包管理器(如 IPK 備份至 APK 系統)進行恢復!由於新舊系統的網路與防火牆底層設定格式存在重大差異,強行恢復設定將導致路由器直接斷網變磚!為了您的設備安全,系統已強制攔截此次操作。" + +msgid "Force Restore Config" +msgstr "強制恢復設定" + +msgid "Current WAN is in DHCP/Static mode. PPPoE settings will not take effect until applied." +msgstr "當前 WAN 埠為動態/靜態 IP 模式,本頁套用設定前 PPPoE 撥號資訊不生效。" + +msgid "Current WAN is in PPPoE mode. DHCP/Static settings will not take effect until applied." +msgstr "當前 WAN 埠為 PPPoE 撥號模式,本頁套用設定前動態/靜態 IP 模式無效。" + +msgid "Advanced Utilities" +msgstr "高級實用設定" + +msgid "MAC Address Clone" +msgstr "MAC 位址複製" + +msgid "Scheduled Reboot" +msgstr "定時重啟設定" + +msgid "IPv6 Web Access, Port:" +msgstr "IPv6直連後台,埠號:" + +msgid "Tip: Some ISPs or campus networks bind to a specific device MAC. If dial-up fails, enter the cloned MAC here." +msgstr "提示:部分社區寬頻或校園網綁定了特定的設備 MAC(如舊路由器或電腦)。若撥號失敗,請在此填入該 MAC 進行偽裝複製。" + +msgid "Auto-fill MAC" +msgstr "一鍵填寫 MAC" + +msgid "No active device connection detected, please enter MAC address manually." +msgstr "未偵測到活動的設備連接,請手動輸入 MAC 位址。" + +msgid "Enable Scheduled Reboot" +msgstr "啟用定時重啟路由器功能" + +msgid "Reboot Time:" +msgstr "重啟時間:" + +msgid "Repeat Days (Multi-select):" +msgstr "重複星期 (可多選):" + +msgid "Please select at least one day!" +msgstr "請至少選擇一天!" + +msgid "Mon" +msgstr "一" + +msgid "Tue" +msgstr "二" + +msgid "Wed" +msgstr "三" + +msgid "Thu" +msgstr "四" + +msgid "Fri" +msgstr "五" + +msgid "Sat" +msgstr "六" + +msgid "Sun" +msgstr "日" + +msgid "Scheduled Reboot" +msgstr "定時重啟" + +msgid "MAC Clone" +msgstr "MAC 複製" + +msgid "Clear (Restore Default)" +msgstr "清空(恢復預設)" + +msgid "Internet Connected" +msgstr "網際網路已連線" + +msgid "Internet Disconnected" +msgstr "網際網路未連線" + +msgid "Ignore" +msgstr "略過" + +msgid "Invalid MAC address format!" +msgstr "MAC 位址格式不正確,請檢查!" + +msgid "e.g., AA:BB:CC:DD:EE:FF" +msgstr "例如:AA:BB:CC:DD:EE:FF" + +msgid "Multi-WAN detected. You can modify the account and password for each line separately (other settings remain unchanged)." +msgstr "系統偵測到多線撥號環境,可分別修改各線路的帳號密碼(其他設定保持不變)。" + +msgid "Interface:" +msgstr "線路介面:" + +msgid "Multi-WAN detected. Only the primary WAN will be configured here, other lines remain unchanged." +msgstr "系統偵測到多線環境,向導中僅配置主線路 (WAN),其餘線路將保持原樣。" + +msgid "Custom Hosts" +msgstr "自訂 Hosts" + +msgid "Custom Hosts" +msgstr "自訂 Hosts" + +msgid "(Domain Hijack)" +msgstr "(網域劫持)" + +msgid "Quick Add:" +msgstr "快速新增紀錄:" + +msgid "Add" +msgstr "新增" + +msgid "No custom Hosts, please add below." +msgstr "暫無自訂 Hosts,請點擊上方新增紀錄" + +msgid "IP (e.g., 127.0.0.1 or ::1)" +msgstr "指向 IP (127.0.0.1 或 ::1)" + +msgid "Domain (e.g., router.lan)" +msgstr "劫持網域 (如 router.lan)" + +msgid "Comment (Optional)" +msgstr "備註說明 (選填)" + +msgid "Pure Text Mode: Paste to import, Copy to export. Format: IP Domain #Comment" +msgstr "純文字模式:貼上匯入,全選複製可匯出保存到txt。格式:IP 網域 #備註" + +msgid "IP and Domain cannot be empty!" +msgstr "IP 和 網域 不能為空!" + +msgid "Invalid IP address format!" +msgstr "IP 位址格式不正確!" + +msgid "Invalid domain! Spaces, wildcards (*), and special characters are not allowed." +msgstr "網域格式不正確!不支援泛解析(*),且不允許包含空格或特殊危險字元。" + +msgid "No changes have been made." +msgstr "您沒有進行任何修改!" + +msgid "Notice" +msgstr "提示訊息" + +msgid "This IP and Domain combination already exists!" +msgstr "此 IP 和 網域的組合已經存在,請勿重複新增!" + +msgid "Duplicate records found in Hosts! Please remove them before continuing." +msgstr "Hosts文件中發現重複的 IP+網域 規則!請手動刪除重複項後再試。" + +msgid "Smart Auto-fill" +msgstr "智能補全" + +msgid "Auto-fill IPv4/v6 & www domain combinations" +msgstr "自動生成 IPv4/v6 以及帶 www 和不帶 www 的組合規則" + +msgid "This feature forces specific domains to resolve to designated IPs. Commonly used for blocking domain access or local device redirection." +msgstr "此功能可制將特定網域強解析到指定的 IP 位址。常用於阻止某網域連網 或 區域網路設備重定向。" + +msgid "Found invalid or duplicate records:" +msgstr "在純文字中發現了格式錯誤或重複的記錄,共計:" + +msgid "Click [OK] to automatically discard them and continue, or [Cancel] to manually fix them." +msgstr "點擊【確定】將自動捨棄錯誤項並保存,點擊【取消】則返回手動修改。" + +msgid "Plugin Repair" +msgstr "插件修復" + +msgid "Please wait" +msgstr "請稍候" + +msgid "Scanning for repairable plugins..." +msgstr "正在掃描可修復的插件清單..." + +msgid "Standard uninstallation does not remove plugin configuration files. If a plugin malfunctions after reinstallation, select it below to reset it to its initial state." +msgstr "常規卸載不會清除插件設定檔。若插件重裝後仍無法正常使用,可在下方選中對應插件,將其重置為初始狀態。也可以手動備份配置,需要時再手動恢復配置。" + +msgid "Factory Default" +msgstr "出廠預設值" + +msgid "Plugin Repair Toolkit" +msgstr "插件修復急救箱" + +msgid "Repair Now" +msgstr "立即修復" + +msgid "Processing" +msgstr "處理中" + +msgid "Repairing and restarting" +msgstr "正在修復並且重啟" + +msgid " please wait" +msgstr " 請稍候" + +msgid "Repair Successful" +msgstr "修復成功" + +msgid "has been successfully restored" +msgstr "已經成功恢復" + +msgid "Repair Failed" +msgstr "修復失敗" + +msgid "Unable to repair this plugin" +msgstr "無法修復該插件" + +msgid "System Error" +msgstr "系統異常" + +msgid "Request timeout or error" +msgstr "請求超時或者出錯" + +msgid "Notice" +msgstr "提示" + +msgid "No repairable plugins found" +msgstr "沒有找到可以修復的插件" + +msgid "Failed to get plugin list" +msgstr "獲取插件清單失敗" + +msgid "Port number must be between 1 and 65535" +msgstr "埠號必須介於 1 到 65535 之間" + +msgid "For system security, do not use" +msgstr "為了系統安全請勿使用" + +msgid "as the external port. It is a reserved high-risk port." +msgstr "作為外網埠號此為系統保留高危險埠號" + +msgid "It is recommended to use 8080 or a port above 10000." +msgstr "建議使用 8080 或 10000 以上的埠號" + +msgid "Enter custom external port number" +msgstr "填寫自訂外網埠號" + +msgid "Port" +msgstr "通訊埠衝突:通訊埠" + +msgid "is already in use by another application!" +msgstr "已被其他服務佔用!" + +msgid "Direct WAN Access (Whitelist)" +msgstr "外網直連 (白名單)" + +msgid "Open firewall to allow direct external access to this device." +msgstr "開放防火牆,允許外網直接存取該設備。" + +msgid "IPv6 Only" +msgstr "僅 IPv6" + +msgid "IPv4 Only" +msgstr "僅 IPv4" + +msgid "IPv4 + IPv6" +msgstr "IPv4 + IPv6" + +msgid "Ghost Plugin Deletion Warning" +msgstr "孤件徹底刪除警告" + +msgid "Executing this will uninstall the software and delete its config files. However, no installation package was found, so you will need to manually reinstall it later." +msgstr "執行後會解除安裝該軟體並刪除設定檔,但找不到安裝包,後續使用需自行安裝。" + +msgid "Eradicate Completely" +msgstr "一鍵徹底清除" + +msgid "Eradicating completely..." +msgstr "正在執行徹底清除..." + +msgid "has been completely uninstalled and cleaned." +msgstr "已被徹底解除安裝並清理。"wf + +msgid "Offline Reinstall Guide" +msgstr "孤件離線重裝指南" + +# 1. 對應按鈕名詞的翻譯(會被替換到 %s 的位置) +msgid "Plugin Installation" +msgstr "插件安裝" + +# 2. 對應整段指南的翻譯 +msgid "To reinstall the plugin, it is highly recommended to use the [%s] feature in the Advanced Utilities panel for quick drag-and-drop setup.

    If conflicts occur or the menu is missing, you can upload the package via SSH to /etc/netwiz/custom_pkgs/ and use the Force Install command:" +msgstr "重新安裝外掛,建議使用「進階功能」面板中的 [%s] 功能進行快捷的視覺化拖曳安装。

    如果發生衝突或選單遺失,您仍可透過 SSH 將安裝包上傳至 /etc/netwiz/custom_pkgs/ 並使用以下強制安裝指令:" + +msgid "Hardware execution takes longer, running in background..." +msgstr "底層硬體正在執行中,請耐心等待..." + +msgid "Advanced Panel Management" +msgstr "進階功能面板管理" + +msgid "Scheduled Reboot" +msgstr "定時重啟" + +msgid "MAC Address Clone" +msgstr "MAC 位址複製" + +msgid "MAC Clone" +msgstr "MAC 複製" + +msgid "Custom Hosts" +msgstr "自訂 Hosts" + +msgid "Plugin Repair" +msgstr "系統急救箱" + +msgid "Plugin Installation" +msgstr "外掛安裝" + +msgid "Offline Plugin Installation" +msgstr "離線外掛安裝" + +msgid "Drag ☰ to reorder, check to show/hide:" +msgstr "按住 ☰ 拖曳排序,勾選開關顯示:" + +msgid "Save and Apply" +msgstr "儲存並套用" + +msgid "Drop or click to select multiple .apk / .ipk files" +msgstr "支援拖曳或點擊選擇多個 .apk / .ipk" + +msgid "Security Block: Only .apk and .ipk allowed!" +msgstr "安全攔截:僅允許 .apk 和 .ipk!" + +msgid "Start Upload & Secure" +msgstr "開始上傳並入庫" + +msgid "Confirm Upload ({num} files)" +msgstr "確認上傳並入庫 ({num} 個檔案)" + +msgid "Processing, do not close the window..." +msgstr "處理中,請勿關閉視窗..." + +msgid "Format blocked!" +msgstr "格式被攔截!" + +msgid "All files secured! Refreshing..." +msgstr "所有檔案均已成功入庫!正在重新整理..." + +msgid "All files secured, executing batch install..." +msgstr "所有檔案入庫完畢,正在執行合併安裝..." + +msgid "Installation successful! Refreshing..." +msgstr "安裝與重置成功!正在重新整理..." + +msgid "Hardware configuring, please wait {sec}s..." +msgstr "底層硬體正在拼命配置中,請耐心等待 {sec}s..." + +msgid "Batch install error: " +msgstr "批次安裝異常: " + +msgid "Processing ({idx}/{total}): {name} - {pct}%" +msgstr "正在處理 ({idx}/{total}): {name} - {pct}%" + +msgid "Securing in background: {name}" +msgstr "後台安裝中: {name}" + +msgid "Backend transfer failed: {name}" +msgstr "後端轉移失敗: {name}" + +msgid "Upload failed ({code}): {name}" +msgstr "上傳失敗 ({code}): {name}" + +msgid "Network disconnected: {name}" +msgstr "網路連線中斷: {name}" + +msgid "Continue to retry remaining files" +msgstr "繼續重試上傳剩餘檔案" + +msgid "Keep a permanent backup in /etc/netwiz/custom_pkgs" +msgstr "同時保留備份至 /etc/netwiz/custom_pkgs" + +msgid "Eradicating completely..." +msgstr "正在徹底執行連根拔除..." + +msgid "has been completely uninstalled and cleaned." +msgstr "已被徹底解除安裝並清理。" + +msgid "Remove" +msgstr "移除" + +msgid "Save layout failed" +msgstr "儲存排版失敗" + +msgid "IPv6 Watchdog" +msgstr "IPv6 進階保活" + +msgid "IPv6 Heartbeat Probe" +msgstr "IPv6 鏈路進階保活" + +msgid "Enable Advanced IPv6 Link Monitoring & Auto Recovery" +msgstr "啟用 IPv6 鏈路存活監測與自癒" + +msgid "Probe Target URL" +msgstr "探測來源目標網址" + +msgid "e.g., XXXXX-XXXXX.workers.dev" +msgstr "例如:XXXXX-XXXXX.workers.dev" + +msgid "Note:" +msgstr "說明:" + +msgid "The server must parse this address and perform a live test:" +msgstr "伺服器端需解析該位址並進行測活:" + +msgid "Your_URL/[IPv6_Address]:Port" +msgstr "您的連結/[IPv6位址]:通訊埠" + +msgid "If the network is accessible, it must return the plain text" +msgstr "若網路暢通,請務必返回純文字" + +msgid "Examples:" +msgstr "填寫範例:" + +msgid "Click to view Cloudflare tutorial & source code" +msgstr "點擊查看 Cloudflare 範例教學與原始碼" + +msgid "To ensure the works correctly, the following dependent features have been automatically enabled:" +msgstr "為了確保正常運作,系統已同步為您啟用以下相依功能:" + +msgid "IPv6 Master Switch" +msgstr "IPv6 總開關" + +msgid "WAN Access Web UI Channel" +msgstr "外網存取面板通道" + +msgid "Security Notice" +msgstr "安全提示" + +msgid "WAN access is about to be closed. To ensure complete firewall protection, the system will simultaneously disable the IPv6 Watchdog mechanism." +msgstr "即將關閉外網存取通道。為保障防火牆防護完整封閉,系統將同步停用 IPv6 深度保活機制。" + +msgid "Dependency Notice" +msgstr "依賴提示" + +msgid "You have disabled IPv6. To ensure security, the dependent IPv6 Watchdog and WAN Access will be automatically disabled." +msgstr "您關閉了 IPv6 功能。為確保系統安全,依賴此功能的「IPv6 深度保活」與「外網直聯後台」將同步關閉。" + +msgid "Probe URL cannot be empty, please fill it in before saving!" +msgstr "探測網址不能為空,請填寫後再保存!" + +msgid "New Version Found" +msgstr "發現新版本" + +msgid "Ready to install the update?" +msgstr "準備好安裝更新了嗎?" + +msgid "Update Now" +msgstr "立即更新" + +msgid "Later" +msgstr "稍後再說" + +msgid "System Upgrading" +msgstr "系統升級中" + +msgid "Downloading and replacing core files, please do not power off..." +msgstr "正在下載並替換核心文件,請勿斷電..." + +msgid "Forcing system file overwrite... (Est. {sec}s remaining)" +msgstr "正在強制覆蓋底層檔案... (預計還需 {sec} 秒)" + +msgid "Upgrade Successful!" +msgstr "升級安裝成功!" + +msgid "The new version core has been deployed in the background!" +msgstr "新版本核心已在底層部署完畢!" + +msgid "Notice: Aggressive browser caching detected.
    To ensure new menus and features display correctly, please press Ctrl + F5 or Shift + F5 to force refresh this page!
    (Mobile users: please manually clear browser cache and refresh)" +msgstr "注意:瀏覽器存在強力頁面快取
    為確保新選單和新功能正常顯示,請務必按下鍵盤上的 Ctrl + F5 或 Shift + F5 強制重新整理本頁!
    (手機端用戶請手動清理瀏覽器快取後重新整理)" + +msgid "You are about to enable the [%s]." +msgstr "即將開啟 [%s]。" + +msgid "You are about to disable the [%s]." +msgstr "即將關閉 [%s]。" + +msgid "Suggestion:" +msgstr "建議提醒:" + +msgid "No target URL is configured for the Watchdog. For long-term stability, it is recommended to manually configure and enable it in %s later." +msgstr "檢測到尚未配置探測源目標網址。為保障外網存取的長期穩定性,建議您稍後前往 %s 手動配置並開啟此功能。" + +msgid "Backup Config" +msgstr "備份設定" + +msgid "Restore Config" +msgstr "還原設定" + +msgid "Restore Configuration Confirm" +msgstr "恢復配置確認" + +msgid "Warning: Restoring the configuration will overwrite all current settings and automatically restart the router upon completion!" +msgstr "警告:恢復配置將覆蓋目前所有設定,並在完成後自動重啟路由器!" + +msgid "Selected file: " +msgstr "選取檔案:" + +msgid "Confirm Restore" +msgstr "確認恢復" + +msgid "Restoring Configuration" +msgstr "正在恢復配置" + +msgid "Uploading backup file, please do not disconnect power..." +msgstr "正在上傳備份檔案,請勿斷開電源..." + +msgid "File delivered, starting restore..." +msgstr "檔案已送達,開始執行恢復..." + +msgid "System is restarting..." +msgstr "系統正在重啟..." + +msgid "Restore command issued, device may be restarting..." +msgstr "恢復指令已下發,設備可能正在重啟中..." + +msgid "Backing up" +msgstr "備份中" + +msgid "Precisely packing %s config and generating download, please wait..." +msgstr "正在精準打包 %s 的配置並生成下載,請稍候..." + +msgid "Backup Successful" +msgstr "備份成功" + +msgid "The file contains core assets and is downloading in the background. Please check your browser!" +msgstr "檔案包含了核心資產,已開始在後台為您下載,請留意瀏覽器提示!" + +msgid "Backup Failed" +msgstr "備份失敗" + +msgid "Config not found or packaging error. Please check if the plugin is installed." +msgstr "找不到配置或打包出錯,請檢查是否安裝了該外掛程式。" + +msgid "Request Failed" +msgstr "請求失敗" + +msgid "API request failed: %s" +msgstr "請求介面失敗:%s" + +msgid "Restoring" +msgstr "恢復中" + +msgid "Uploading and restoring %s config..." +msgstr "正在上傳並恢復 %s 的配置..." + +msgid "Restore Successful" +msgstr "恢復成功" + +msgid "%s config file has been restored and reloaded!" +msgstr "%s 設定檔已還原並重新載入!" + +msgid "Restore Failed" +msgstr "恢復失敗" + +msgid "Failed to extract or restart service." +msgstr "解壓或重啟服務失敗。" + +msgid "Upload Failed" +msgstr "上傳失敗" + +msgid "File upload rejected, status code: %s" +msgstr "檔案上傳被拒絕,狀態碼:%s" + +msgid "Upload failed: %s" +msgstr "上傳失敗: %s" + +msgid "Error" +msgstr "錯誤" + +msgid "Network Error" +msgstr "網路錯誤" + +msgid "Network connection unexpectedly interrupted!" +msgstr "網路連線意外中斷!" + +msgid "Reload Page" +msgstr "刷新頁面" + +msgid "Format Error" +msgstr "格式錯誤" + +msgid "Please upload a valid .tar.gz backup file!" +msgstr "請上傳有效的 .tar.gz 備份檔案!" + +msgid "Warning" +msgstr "警告" + +msgid "The selected file does not seem to belong to %s.
    File name: %f
    Are you sure you want to continue?" +msgstr "選取的檔案似乎不屬於 %s。
    檔案名稱:%f
    您確定要繼續強制恢復嗎?" + +msgid "Force Restore" +msgstr "強制恢復" + +msgid "The uploaded file is corrupted or not a valid archive!" +msgstr "上傳的檔案已損壞,或不是有效的壓縮包!" + +msgid "The archive content does not match %s!" +msgstr "壓縮包內部內容與 %s 不匹配!" + +msgid "%s config restored! Service is restarting in the background..." +msgstr "%s 配置還原成功!服務正在後台平滑重啟..." + +msgid "Auto reloading in %s seconds..." +msgstr "%s 秒後將自動刷新頁面..." + +msgid "Failed to extract files to the system! (Storage full or permission denied)" +msgstr "檔案解壓寫入系統失敗!(可能是儲存空間已滿或權限不足)" diff --git a/luci-app-netwiz/luci-app-netwiz/root/etc/config/netwiz b/luci-app-netwiz/luci-app-netwiz/root/etc/config/netwiz new file mode 100644 index 000000000..ee6d27dfe --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/etc/config/netwiz @@ -0,0 +1,5 @@ +config global 'global' + option configured '0' + +config main 'main' + option wizard_enable '1' diff --git a/luci-app-netwiz/luci-app-netwiz/root/etc/hotplug.d/dhcp/99-netwiz-guard b/luci-app-netwiz/luci-app-netwiz/root/etc/hotplug.d/dhcp/99-netwiz-guard new file mode 100644 index 000000000..d41a740b9 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/etc/hotplug.d/dhcp/99-netwiz-guard @@ -0,0 +1,46 @@ +#!/bin/sh +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 +# 作用: Netwiz 设备防伪装 (防止修改 MAC 绕过断网功能) + +# 1. 获取 IP 时触发 +[ "$ACTION" = "add" ] || [ "$ACTION" = "update" ] || exit 0 +[ -z "$MACADDR" ] || [ -z "$HOSTNAME" ] || [ "$HOSTNAME" = "*" ] && exit 0 + +MAC_SAFE=$(echo "$MACADDR" | tr -d ':' | tr 'a-z' 'A-Z') + +# 2. 检查新 MAC 是否已被拉黑 +ALREADY_BLOCKED=$(uci -q show firewall | grep -E "\.name=.*nw_blk_${MAC_SAFE}") +[ -n "$ALREADY_BLOCKED" ] && exit 0 + +# 3.DHCP 租约查找所有与该设备“同名”的历史 MAC 地址 +KNOWN_MACS=$(awk -v hn="$HOSTNAME" '$4 == hn {print $2}' /tmp/dhcp.leases 2>/dev/null) + +for k_mac in $KNOWN_MACS; do + K_MAC_SAFE=$(echo "$k_mac" | tr -d ':' | tr 'a-z' 'A-Z') + + # 4. 检查以前用过的 MAC 是不是在断网黑名单里 + IS_BLOCKED=$(uci -q show firewall | grep -E "\.name=.*nw_blk_${K_MAC_SAFE}") + + if [ -n "$IS_BLOCKED" ]; then + logger -t Netwiz "🚨 Anti-Spoofing! Device [$HOSTNAME] changed MAC to $MACADDR. Auto-blocking..." + + # 5. 发现伪装!自动将它的新 MAC 追加到防火墙断网规则中 + uci -q add firewall rule >/dev/null + uci -q set firewall.@rule[-1].name="nw_blk_${MAC_SAFE}" + uci -q set firewall.@rule[-1].src="lan" + uci -q set firewall.@rule[-1].dest="wan" + uci -q set firewall.@rule[-1].src_mac="$MACADDR" + uci -q set firewall.@rule[-1].family="any" + uci -q set firewall.@rule[-1].target="DROP" + uci -q set firewall.@rule[-1].enabled="1" + + uci commit firewall + + # 6. 踢下线并重载防火墙 + if command -v iptables >/dev/null 2>&1; then iptables -I FORWARD 1 -m mac --mac-source $MACADDR -j DROP >/dev/null 2>&1; fi + if command -v nft >/dev/null 2>&1; then nft insert rule inet fw4 forward ether saddr $MACADDR drop >/dev/null 2>&1; fi + /etc/init.d/firewall reload /dev/null 2>&1 & + break + fi +done diff --git a/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-monitor b/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-monitor new file mode 100644 index 000000000..cea4aa92f --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-monitor @@ -0,0 +1,148 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 + +START=99 +USE_PROCD=1 +LOG_FILE="/tmp/netwiz.log" + +log() { + echo "$(date '+%F %T') [Service] $1" >> "$LOG_FILE" + if [ $(wc -l < "$LOG_FILE" 2>/dev/null || echo 0) -gt 600 ]; then + tail -n 500 "$LOG_FILE" > "$LOG_FILE.tmp" + mv "$LOG_FILE.tmp" "$LOG_FILE" + fi +} + +start_service() { + log "Netwiz 后台守护进程已启动" + + # 获取向导配置状态 + local configured=$(uci -q get netwiz.global.configured) + + # 配置为 0,触发拦截 + if [ "$configured" != "1" ]; then + log "触发初次向导配置,生成拦截 CGI 脚本" + + local is_lang_init=$(uci -q get netwiz.global.lang_init) + if [ "$is_lang_init" != "1" ]; then + uci -q set luci.main.lang="auto" + uci commit luci + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null + /etc/init.d/rpcd restart 2>/dev/null + /etc/init.d/uhttpd restart 2>/dev/null + uci -q set netwiz.global.lang_init='1' + uci commit netwiz + fi + + [ -f /www/cgi-bin/luci.bak ] || cp -p /www/cgi-bin/luci /www/cgi-bin/luci.bak + + cat << 'EOF' > /www/cgi-bin/luci +#!/bin/sh +CONF=$(uci -q get netwiz.global.configured) + +if [ "$CONF" != "1" ]; then + case "$PATH_INFO" in + *rescue*) + uci -q set netwiz.global.configured='1' + uci commit netwiz + [ -f /www/cgi-bin/luci.bak ] && mv -f /www/cgi-bin/luci.bak /www/cgi-bin/luci + printf "Status: 302 Found\r\n" + printf "Location: /cgi-bin/luci/\r\n" + printf "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n\r\n" + exit 0 + ;; + + *admin/netwiz*|*admin/translations*|*admin/menu*|*admin/ubus*|*luci-static*|*rpc*) + export HTTP_ACCEPT_LANGUAGE="zh-CN,zh;q=0.9" + exec /www/cgi-bin/luci.bak + exit 0 + ;; + + *) + # 浏览器带登录凭证,弹出向导 + if echo "$HTTP_COOKIE" | grep -q "sysauth="; then + printf "Status: 302 Found\r\n" + printf "Location: /cgi-bin/luci/admin/netwiz\r\n" + printf "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n\r\n" + exit 0 + fi + + # 网址带官方重定向的 url= 弹出向导 + if echo "$QUERY_STRING" | grep -q "url="; then + printf "Status: 302 Found\r\n" + printf "Location: /cgi-bin/luci/admin/netwiz\r\n" + printf "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n\r\n" + exit 0 + fi + + IS_EMPTY_PWD=$(awk -F: '/^root:/ {print $2}' /etc/shadow) + if [ -z "$IS_EMPTY_PWD" ] || [ "$IS_EMPTY_PWD" = "!" ] || [ "$IS_EMPTY_PWD" = "*" ] || [ "$IS_EMPTY_PWD" = "x" ]; then + + printf "Status: 200 OK\r\n" + printf "Content-Type: text/html; charset=utf-8\r\n" + printf "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n\r\n" + printf "" + printf "
    " + printf "" + printf "" + printf "" + printf "
    " + + printf "\n" + exit 0 + + else + # 固件自带默认密码,官方登录 + printf "Status: 302 Found\r\n" + printf "Location: /cgi-bin/luci/?url=/cgi-bin/luci/admin/netwiz\r\n" + printf "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n\r\n" + exit 0 + fi + ;; + esac +fi + +exec /www/cgi-bin/luci.bak +EOF + + # 赋予执行权限 + chmod +x /www/cgi-bin/luci + else + # 已配置过,恢复官方网关 + if [ -f /www/cgi-bin/luci.bak ]; then + log "向导已完成,还原官方网关" + mv /www/cgi-bin/luci.bak /www/cgi-bin/luci + fi + fi + + procd_open_instance + procd_set_param command /usr/libexec/netwiz-monitor-loop.sh + procd_set_param respawn + procd_close_instance +} + +stop_service() { + log "Netwiz 后台守护进程已停止" +} + +service_triggers() { + procd_add_reload_trigger "netwiz" +} + +reload_service() { + restart +} diff --git a/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-recovery b/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-recovery new file mode 100644 index 000000000..f78e80641 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-recovery @@ -0,0 +1,128 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 +# START=15 确保在 network 服务前执行 +START=15 + +RECOVERY_LOG="/etc/netwiz_recovery.log" +AUTODETECT_SCRIPT="/usr/libexec/netwiz-autodetect.sh" + +boot() { + # 处理由于 LAN 口 / 全局设置向导导致的异常断电 + if [ -f /etc/config/network.netwiz_bak ]; then + logger -t Netwiz "Power loss detected during LAN/Global setup! Forcefully recovering..." + + echo "=========================================================" >> "$RECOVERY_LOG" + echo "⚠️ [BOOT-RECOVERY] $(date '+%F %T') 检测到 LAN/全局 配置异常断电,正从闪存强制回退..." >> "$RECOVERY_LOG" + + cat /etc/config/network.netwiz_bak > /etc/config/network + [ -f /etc/config/dhcp.netwiz_bak ] && cat /etc/config/dhcp.netwiz_bak > /etc/config/dhcp + + rm -f /etc/config/network.netwiz_bak /etc/config/dhcp.netwiz_bak + rm -f /tmp/netwiz_rollback_time /tmp/netwiz_target_ip + + echo "✅ [BOOT-RECOVERY] LAN/全局 异常回退完成!" >> "$RECOVERY_LOG" + echo "=========================================================" >> "$RECOVERY_LOG" + fi + + # 处理由于 WAN 口自动探测导致的异常断电 + if [ -f /etc/config/network.netwiz_wan_bak ]; then + logger -t Netwiz "Power loss detected during WAN autodetect! Forcefully recovering..." + + echo "=========================================================" >> "$RECOVERY_LOG" + echo "⚠️ [BOOT-RECOVERY] $(date '+%F %T') 检测到 WAN 探测过程异常断电,强制回退..." >> "$RECOVERY_LOG" + + cat /etc/config/network.netwiz_wan_bak > /etc/config/network + rm -f /etc/config/network.netwiz_wan_bak + + echo "✅ [BOOT-RECOVERY] WAN 探测回退完成!" >> "$RECOVERY_LOG" + + # 只有因为 WAN 探测断电,才需要在后台重新拉起探测探针 + if [ -x "$AUTODETECT_SCRIPT" ]; then + echo "🔄 [BOOT-RECOVERY] 已埋设后台探针,等待网卡就绪后触发检测..." >> "$RECOVERY_LOG" + # 使用子 shell 放入后台执行,完全不阻塞系统的后续启动 + ( + WAN_DEV=$(uci -q get network.wan.device || uci -q get network.wan.device || echo "eth0") + # 轮询等待最多 60 秒 (30 次 * 2 秒) + for i in $(seq 1 30); do + if ubus call network.device status "{\"name\":\"$WAN_DEV\"}" 2>/dev/null | grep -q '"carrier": true'; then + sleep 3 # 给内核几秒钟梳理路由表 + "$AUTODETECT_SCRIPT" + exit 0 + fi + sleep 2 + done + ) & + fi + echo "=========================================================" >> "$RECOVERY_LOG" + fi + + # ====================================================================== + # 备份恢复后重启 (300 秒跨重启看门狗) + # 如果在 300 秒内前端没有发送 confirm 确认存活,则判定为失联变砖并强制回滚 + # ====================================================================== + if [ -f "/etc/netwiz_pending_recovery" ]; then + logger -t Netwiz "[RECOVERY] ⚠️ 检测到未清除的恢复备份防砖标记,启动 300 秒幽灵看门狗..." + + # 剥离子进程进入后台倒数,绝不阻塞路由器的正常开机流程 + ( + sleep 300 + + # 300秒后苏醒,如果标记还在,说明前端没发来 confirm + if [ -f "/etc/netwiz_pending_recovery" ]; then + + # ==================================================== + # 判断是否为“假失联” + # ==================================================== + # 检测 1: LAN 接口是否在底层总线 (ubus) 真正 up + LAN_UBUS=$(ubus call network.interface.lan status 2>/dev/null | grep '"up": true') + + # 检测 2: Web 网页服务器 (uhttpd) 必须存活!如果用户打不开网页,等于半砖 + HTTPD_ALIVE=$(pgrep uhttpd) + + # 检测 3: 核心 DHCP 守护进程必须存活 (除非用户处于 AP 模式,豁免此项) + IS_AP=$(uci -q get dhcp.lan.ignore) + if [ "$IS_AP" = "1" ]; then + DNS_ALIVE="AP_MODE_SKIP" # 旁路由模式,豁免检查 + else + DNS_ALIVE=$(pgrep dnsmasq) + fi + + # 三大核心系统必须全部存活,允许静默拆弹 + if [ -n "$LAN_UBUS" ] && [ -n "$HTTPD_ALIVE" ] && [ -n "$DNS_ALIVE" ]; then + logger -t Netwiz "[RECOVERY] 🛡️ 300秒超时,底层网络、Web与DNS服务完全健康!判定为假阳性。" + logger -t Netwiz "[RECOVERY] 终止回滚动作,仅静默拆除炸弹。" + echo "$(date '+%F %T') [RECOVERY] 🛡️ 核心网络自检通过 (无论有无设备连接),取消回滚,静默拆弹。" >> "$RECOVERY_LOG" + + rm -f /etc/netwiz_pending_recovery + rm -rf /etc/netwiz/failsafe + exit 0 + fi + + # 如果上面三大核心有任何一个挂了 (比如 uhttpd 崩溃进不去网页),才执行硬回滚! + logger -t Netwiz "[RECOVERY] 🚨 300秒超时且核心网络自检未完全通过!判断为恢复后变砖,启动硬回滚!" + + # 备用配置覆盖当前损坏的配置 + cp -p /etc/netwiz/failsafe/network_bak /etc/config/network 2>/dev/null + cp -p /etc/netwiz/failsafe/dhcp_bak /etc/config/dhcp 2>/dev/null + cp -p /etc/netwiz/failsafe/wireless_bak /etc/config/wireless 2>/dev/null + + # 删除,防止无限循环回滚 + rm -f /etc/netwiz_pending_recovery + rm -rf /etc/netwiz/failsafe + + logger -t Netwiz "[RECOVERY] 回滚覆盖完成,正在强制重启系统以自救..." + # 底层架构可能被错误插件破坏,最安全的自救方式是再次整机重启 + sleep 3 + reboot + fi + ) /dev/null 2>&1 & + fi + # ====================================================================== + + # 日志体积控制,保留最后 100 行即可 + if [ $(wc -l < "$RECOVERY_LOG" 2>/dev/null || echo 0) -gt 150 ]; then + tail -n 100 "$RECOVERY_LOG" > "$RECOVERY_LOG.tmp" + mv "$RECOVERY_LOG.tmp" "$RECOVERY_LOG" + fi +} diff --git a/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-watchdog b/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-watchdog new file mode 100644 index 000000000..5014b0e6b --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/etc/init.d/netwiz-watchdog @@ -0,0 +1,29 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 +# 描述: Netwiz IPv6 背景守护进程 + +START=99 +USE_PROCD=1 + +start_service() { + # 1. 检查前端是否开启了开关,没开就不启动进程,0內存损耗 + local enable=$(uci -q get netwiz.main.watchdog_enable) + [ "$enable" != "1" ] && return 0 + + # 2. 注册背景守护进程(交由procd监控) + procd_open_instance + # 指向我们唯一的后端脚本,并传入daemon参数 + procd_set_param command /usr/libexec/rpcd/netwiz daemon + # 开启崩溃自动重启机制(Respawn) + procd_set_param respawn + # 捕获脚本的标准输出到系统日志 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + # 连动:在前端修改netwiz界面并储存时,procd会自动重新启动此进程套用新网址 + procd_add_reload_trigger "netwiz" +} \ No newline at end of file diff --git a/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-autodetect.sh b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-autodetect.sh new file mode 100644 index 000000000..eb0cc2162 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-autodetect.sh @@ -0,0 +1,172 @@ +#!/bin/sh +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 + +LOG_FILE="/tmp/netwiz.log" + +log() { + echo "$(date '+%F %T') [Engine] $1" >> "$LOG_FILE" + if [ $(wc -l < "$LOG_FILE" 2>/dev/null || echo 0) -gt 600 ]; then + tail -n 500 "$LOG_FILE" > "$LOG_FILE.tmp" + mv "$LOG_FILE.tmp" "$LOG_FILE" + fi +} + +# === 智能推算子网掩码 === +calc_netmask() { + local ip="$1" + local b="${ip%%.*}" + case "$b" in ''|*[!0-9]*) echo "255.255.255.0"; return ;; esac + + if [ "$b" -ge 1 ] && [ "$b" -le 126 ]; then + echo "255.0.0.0" + elif [ "$b" -ge 128 ] && [ "$b" -le 191 ]; then + echo "255.255.0.0" + else + echo "255.255.255.0" + fi +} + +LOCK_FILE="/var/run/netwiz_autodetect.lock" +# WAN 备份名,与 LAN 向导的备份隔离 +BAK_FILE="/etc/config/network.netwiz_wan_bak" + +# ================= 退出与异常清理机制 ================= +cleanup() { + rm -f "$LOCK_FILE" + # 如果脚本退出时,备份文件依然存在,说明脚本被意外中止 + if [ -f "$BAK_FILE" ]; then + log "⚠️ 检测到脚本被外部异常中断,触发安全清理,正在恢复原配置..." + cp "$BAK_FILE" /etc/config/network + rm -f "$BAK_FILE" + /etc/init.d/network reload & + fi +} +# 捕获 EXIT(正常或报错退出), INT(Ctrl+C), TERM(系统kill) +trap cleanup EXIT INT TERM +# ========================================================== + +# 防止 kill -9 (强制抹杀,不触发 trap) 导致的残酷遗留 +if [ -f "$BAK_FILE" ] && [ ! -f "$LOCK_FILE" ]; then + log "警告:发现无主遗留备份文件,先恢复配置..." + cp "$BAK_FILE" /etc/config/network + rm -f "$BAK_FILE" +fi + +if [ -f "$LOCK_FILE" ]; then exit 0; fi +touch "$LOCK_FILE" + +WAN_DEV=$(uci -q get network.wan.device) +[ -z "$WAN_DEV" ] && WAN_DEV=$(uci -q get network.wan.device) +[ -z "$WAN_DEV" ] && WAN_DEV="eth0" + +wait_for_internet() { + local max_wait=20 + local i=0 + local offline_strikes=0 + + while [ $i -lt $max_wait ]; do + if ! ubus call network.device status "{\"name\":\"$WAN_DEV\"}" 2>/dev/null | grep -q '"carrier": true'; then + offline_strikes=$((offline_strikes+1)) + else + offline_strikes=0 + fi + + if [ $offline_strikes -ge 3 ]; then + log "检测到网线已物理拔出,终止探测" + return 1 + fi + + # 多目标 Ping,只要一个通就算有网 + if ping -c 1 -W 1 223.5.5.5 >/dev/null 2>&1 || \ + ping -c 1 -W 1 114.114.114.114 >/dev/null 2>&1 || \ + ping -c 1 -W 1 8.8.8.8 >/dev/null 2>&1; then + return 0 + fi + sleep 2 + i=$((i+1)) + done + return 1 +} + +log "检测到 WAN 口网线插入,启动自动探测引擎..." +sleep 5 +if wait_for_internet; then + log "当前配置网络正常,无需切换协议" + exit 0 +fi + +log "当前配置无法连通互联网,准备进行协议切换探测" +cp /etc/config/network "$BAK_FILE" +sync + +ORIG_PROTO=$(uci -q get network.wan.proto) +HAS_PPPOE_USER=$(uci -q get network.wan.username) +SAVED_STATIC_IP=$(uci -q get network.wan.ipaddr) +SAVED_STATIC_GW=$(uci -q get network.wan.gateway) +SAVED_STATIC_MASK=$(uci -q get network.wan.netmask) +success=0 + +# 1、切换DHCP +if [ "$ORIG_PROTO" != "dhcp" ]; then + log "正在尝试通过 DHCP 自动获取 IP 地址..." + uci set network.wan.proto='dhcp' + uci commit network + /etc/init.d/network reload + if wait_for_internet; then success=1; fi +fi + +test_static_ip() { + local try_mask="$1" + log "正在探测静态 IP: $SAVED_STATIC_IP,网关: $SAVED_STATIC_GW,掩码: $try_mask" + cp "$BAK_FILE" /etc/config/network + uci set network.wan.proto='static' + uci set network.wan.ipaddr="$SAVED_STATIC_IP" + uci set network.wan.gateway="$SAVED_STATIC_GW" + uci set network.wan.netmask="$try_mask" + uci commit network + /etc/init.d/network reload + if wait_for_internet; then return 0; else return 1; fi +} + +# 2、切换静态 IP +if [ "$success" -eq 0 ] && [ "$ORIG_PROTO" != "static" ] && [ -n "$SAVED_STATIC_IP" ] && [ -n "$SAVED_STATIC_GW" ]; then + CALC_MASK=$(calc_netmask "$SAVED_STATIC_IP") + + if [ -n "$SAVED_STATIC_MASK" ]; then + if test_static_ip "$SAVED_STATIC_MASK"; then + success=1 + else + if [ "$SAVED_STATIC_MASK" != "$CALC_MASK" ]; then + log "原掩码无响应,尝试切换为推算掩码再试..." + if test_static_ip "$CALC_MASK"; then success=1; fi + fi + fi + else + if test_static_ip "$CALC_MASK"; then success=1; fi + fi +fi + +# 3、切换 PPPoE +if [ "$success" -eq 0 ] && [ "$ORIG_PROTO" != "pppoe" ] && [ -n "$HAS_PPPOE_USER" ]; then + log "前置探测均失败,正在探测 PPPoE 服务器..." + cp "$BAK_FILE" /etc/config/network + uci set network.wan.proto='pppoe' + uci commit network + /etc/init.d/network reload + if wait_for_internet; then success=1; fi +fi + +# === 最终 === +if [ "$success" -eq 1 ]; then + log "新协议探测连通成功,已保存配置" + rm -f "$BAK_FILE" +else + log "未检测到有效的网络协议,正在回退到原始配置" + cp "$BAK_FILE" /etc/config/network + rm -f "$BAK_FILE" + /etc/init.d/network reload +fi + +# 正常退出时,清理变量已消除,Trap 将只执行清理 LOCK_FILE +exit 0 diff --git a/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-monitor-loop.sh b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-monitor-loop.sh new file mode 100644 index 000000000..7ad03f7d8 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/netwiz-monitor-loop.sh @@ -0,0 +1,134 @@ +#!/bin/sh +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 + +# 日志统一存放在 /tmp/ 下,避免损耗路由器闪存 +LOG_FILE="/tmp/netwiz.log" +LOCK_FILE="/var/run/netwiz_autodetect.lock" + +# 定义最大保留500行 +MAX_LINES=500 + +log() { + # 写入新日志 + echo "$(date '+%F %T') [Monitor] $1" >> "$LOG_FILE" + + # 自动删除 + # 日志超过 600 行时,自动删除,只保留最新的 500 行 + if [ $(wc -l < "$LOG_FILE" 2>/dev/null || echo 0) -gt 600 ]; then + tail -n $MAX_LINES "$LOG_FILE" > "$LOG_FILE.tmp" + mv "$LOG_FILE.tmp" "$LOG_FILE" + fi +} + +# 获取当前的 WAN 网卡名称 +WAN_DEV=$(uci -q get network.wan.device) +[ -z "$WAN_DEV" ] && WAN_DEV=$(uci -q get network.wan.device) +[ -z "$WAN_DEV" ] && WAN_DEV="eth0" + +# 检查 WAN 口物理连接状态 +check_wan_link() { + if ubus call network.device status "{\"name\":\"$WAN_DEV\"}" 2>/dev/null | grep -q '"carrier": true'; then + echo "up" + else + echo "down" + fi +} + +LAST_WAN_STATE=$(check_wan_link) +# 记录连续断开的次数,用于防抖 +DOWN_COUNT=0 + +log "服务已启动,正在监控 WAN 插拔和 LAN 回退定时器" + +while true; do + # --- 1. WAN 接口插拔监控 (带防抖逻辑) --- + CURRENT_WAN_STATE=$(check_wan_link) + + # 如果自动探测引擎没有在运行,才执行监控 + if [ ! -f "$LOCK_FILE" ]; then + if [ "$CURRENT_WAN_STATE" = "down" ]; then + # 发现断开,累加次数 + DOWN_COUNT=$((DOWN_COUNT+1)) + else + # 发现连通,检查之前是否真的拔出过 + if [ "$LAST_WAN_STATE" = "down" ]; then + # 只有断开超过 3 个周期 (约 9 秒),才认为是人工插拔 + if [ "$DOWN_COUNT" -ge 3 ]; then + log "确认 WAN 口物理插拔,正在启动探测引擎" + /usr/libexec/netwiz-autodetect.sh >/dev/null 2>&1 /dev/null) + + # 不仅查并发数,还要强制校验“客户端 IP 子网”! + # 只有当您的电脑 IP (如 192.168.10.100) 和目标 IP (192.168.10.1) 在同一个 /24 网段时,才算有效连接! + conns=$(netstat -nt 2>/dev/null | awk -v tip="$TARGET_IP" ' + BEGIN { + # 提取目标 IP 的前三段作为基准前缀 (例如 192.168.10.) + split(tip, p, ".") + prefix = p[1]"."p[2]"."p[3]"." + } + /ESTABLISHED/ { + if ($4 == tip":80" || $4 == tip":443") { + split($5, c, ":") + client_ip = c[1] + # 客户端 IP 的前缀不等于目标 IP 的前缀,直接无视! + if (substr(client_ip, 1, length(prefix)) == prefix) { + count++ + } + } + } + END { print count+0 } + ') + + # 阈值依然是 2,完美过滤前端的单线程探针 + if [ "$conns" -ge 2 ]; then + log "成功:雷达检测到同网段真实浏览器访问新 IP ($TARGET_IP),自动解除定时" + # 彻底清理所有相关的临时文件和备份 + rm -f /tmp/netwiz_rollback_time /tmp/netwiz_target_ip /etc/config/network.netwiz_bak /etc/config/dhcp.netwiz_bak + else + log "等待用户浏览器跳转中... (当前有效目标连接数: $conns)" + + # 读取时间方式:剔除任何可能的空行或隐藏符号 + TARGET_TIME=$(cat /tmp/netwiz_rollback_time 2>/dev/null | tr -cd '0-9') + CURRENT_TIME=$(date +%s) + + # 只有读到了纯数字时间,且当前时间大于目标时间,才执行回退 + if [ -n "$TARGET_TIME" ] && [ "$CURRENT_TIME" -ge "$TARGET_TIME" ]; then + log "时间到!未检测到任何浏览器访问,确认为失联,开始执行强制性回退" + rm -f /tmp/netwiz_rollback_time /tmp/netwiz_target_ip + + # 从 /tmp/ 读取纯净备份,并使用 cat 暴力覆盖以防权限断裂 + if [ -f /etc/config/network.netwiz_bak ]; then + log "正在从闪存中恢复原始配置..." + cat /etc/config/network.netwiz_bak > /etc/config/network + [ -f /etc/config/dhcp.netwiz_bak ] && cat /etc/config/dhcp.netwiz_bak > /etc/config/dhcp + + ( + exec >/dev/null 2>&1 > "$LOG_FILE" + # 恢复完后删除闪存备份 + rm -f /etc/config/network.netwiz_bak /etc/config/dhcp.netwiz_bak + ) & + fi + fi + fi + fi + + # 每 3 秒执行一次检查 + sleep 3 +done diff --git a/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz new file mode 100644 index 000000000..054d81840 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz @@ -0,0 +1,2648 @@ +#!/bin/sh +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 + +LOG_FILE="/tmp/netwiz.log" + +# ======================================================= +# 全量备份:高级资产目录配置白名单 +# 需要备份的插件配置文件夹,直接在此处追加,系统会自动探测打包 +# ======================================================= +EXTRA_BACKUP_DIRS=" + /etc/openclash/core + /etc/openclash/history + /etc/mosdns/rule + /etc/adguardhome + /usr/share/v2ray + /usr/share/xray + /etc/passwall + /etc/smartdns + /etc/homeproxy + /etc/mihomo + /etc/ssrplus + /etc/bypass + /etc/ddns-go +" + +# 用法: log "[标签]" "日志内容" +log() { + local tag="$1" + local msg="$2" + # 如果只传了一个参数,默认标签为 [SYSTEM] + if [ -z "$msg" ]; then + msg="$tag" + tag="[SYSTEM]" + fi + + # 写入系统标准日志 (logread 可见) + logger -t Netwiz "$tag $msg" + # 写入本地统一文件 + echo "$(date '+%F %T') $tag $msg" >> "$LOG_FILE" + + # 日志控制 防止文件过大撑爆内存 (保留最新的 600 行) + if [ $(wc -l < "$LOG_FILE" 2>/dev/null || echo 0) -gt 600 ]; then + tail -n 500 "$LOG_FILE" > "$LOG_FILE.tmp" + mv "$LOG_FILE.tmp" "$LOG_FILE" + fi +} + +# 重启依赖网络的高级应用 +restart_depend_apps() { + log ">>> 正在检测并唤醒高级网络应用..." + + # 定义需要唤醒的插件池 (按业务逻辑分类,方便以后随时添加) + local proxy_apps="openclash passwall shadowsocksr bypass vssr homeproxy mihomo" + local dns_apps="adguardhome smartdns mosdns" + local vpn_apps="tailscale zerotier ipsec" + local route_apps="mwan3 sqm ddns miniupnpd socat" + + # 唤醒: 代理网关、DNS处理、异地组网、路由流控 + for app in $proxy_apps $dns_apps $vpn_apps $route_apps; do + if [ -f "/etc/init.d/$app" ] && /etc/init.d/$app enabled 2>/dev/null; then + # 中继正在运行,强行跳过 mwan3,防止它阻断网络 + if [ "$app" = "mwan3" ] && uci -q get network.wwan >/dev/null; then + log "拦截: 检测到 WISP 开启,阻止唤醒 mwan3 以防流量被阻断" + continue + fi + log "唤醒 $app" + /etc/init.d/$app restart >/dev/null 2>&1 & + fi + done + + # 特殊处理: Docker 容器服务 (网络重启常导致 Docker 虚拟网桥失效) + if [ -f "/etc/init.d/dockerd" ] && /etc/init.d/dockerd enabled 2>/dev/null; then + log "唤醒 Docker 守护进程" + /etc/init.d/dockerd restart >/dev/null 2>&1 & + fi +} +# 结束 + +# ======================================================= +# PPPoE 拨号后 IPv6 虚拟接口未生成的底层自愈 +watchdog_pppoe_ipv6() { + if [ "$(uci -q get network.wan.proto)" = "pppoe" ]; then + log ">>> 启动 PPPoE IPv6 虚拟接口守护 (20秒后检测)..." + ( + sleep 20 + # 确认 WAN 是否已经拨号成功 (IPv4 连通) + if ifstatus wan | grep -q '"up": true'; then + + # 局域网 IPv6 被关闭,直接跳过 + if [ "$(uci -q get dhcp.lan.dhcpv6)" = "disabled" ]; then + log "✅ IPv6 已全局关闭,跳过虚拟接口检测。" + exit 0 + fi + + # 检查接口是否“存在” (grep '"up"' 能同时匹配 true 和 false) + if ! ifstatus wan_6 2>/dev/null | grep -q '"up"' && ! ifstatus wan6 2>/dev/null | grep -q '"up"'; then + log "⚠️ PPPoE 已连接,但 IPv6 虚拟接口完全丢失!正在执行自愈重置..." + ifdown wan + sleep 3 + ifup wan + else + log "✅ PPPoE IPv6 接口已存在 (健康通网 或 光猫不支持),无需干预。" + fi + fi + ) & + fi +} +# ======================================================= +# 智能 IPv6 保活状态评估引擎 (中央决策 V2.0) +evaluate_keepalive() { + local need_keepalive=0 + + # 1. 检查是否开启了全局外网访问后台 + local web_rule=$(uci -q get firewall.netwiz_web.target 2>/dev/null) + if [ "$web_rule" = "ACCEPT" ]; then + need_keepalive=1 + fi + + # 2. 检查是否有任何设备开启了外网直连 (白名单) + if [ "$need_keepalive" -eq 0 ]; then + local expose_count=$(uci -q show firewall 2>/dev/null | grep -c "\.name='nw_expose_") + if [ "$expose_count" -gt 0 ]; then + need_keepalive=1 + fi + fi + + # 3. 检查是否在高级面板中强制开启了高级保活探针 + if [ "$need_keepalive" -eq 0 ]; then + local watchdog_en=$(uci -q get netwiz.main.watchdog_enable) + if [ "$watchdog_en" = "1" ]; then + need_keepalive=1 + fi + fi + + # 4. 动态管理官方 procd 守护进程 (netwiz-watchdog) + if [ "$need_keepalive" -eq 1 ]; then + if /etc/init.d/netwiz-watchdog enabled 2>/dev/null; then + # 已经启用则重启应用新配置 + /etc/init.d/netwiz-watchdog restart >/dev/null 2>&1 + logger -t Netwiz "[保活引擎] 智能联动: 已刷新 IPv6 保活与自愈守护进程" + else + /etc/init.d/netwiz-watchdog enable >/dev/null 2>&1 + /etc/init.d/netwiz-watchdog start >/dev/null 2>&1 + logger -t Netwiz "[保活引擎] 智能联动: 已唤醒 IPv6 保活与自愈守护进程" + fi + else + # 没有任何需求,彻底休眠进程,0 内存损耗 + /etc/init.d/netwiz-watchdog stop >/dev/null 2>&1 + /etc/init.d/netwiz-watchdog disable >/dev/null 2>&1 + logger -t Netwiz "[保活引擎] 智能联动: 无任何外网穿透需求,已彻底休眠保活服务" + fi +} +# ======================================================= + +case "$1" in + list) + echo '{ "set_network": { "mode": "string", "arg1": "string", "arg2": "string", "arg3": "string", "arg4": "string", "arg5": "string", "arg6": "string" }, "set_password": { "password": "string" }, "check_wifi": {}, "confirm": {}, "smart_backup": { "type": "string" }, "smart_restore_exec": { "filepath": "string" }, "check_backup": {}, "check_storage": {}, "check_restore_status": {}, "check_ip_conflict": { "ip": "192.168.1.254" }, "check_missing_pkgs": {}, "get_wizard_status": {}, "set_wizard_status": { "mode": "string" }, "check_internet": {}, "get_client_mac": {}, "get_adv_settings": {}, "set_adv_settings": { "mac": "string", "web": "string", "cron": "string", "hosts": "string" }, "get_repairable_configs": {}, "repair_config": { "plugin": "string" }, "eradicate_config": { "plugin": "string" }, "get_adv_layout": {}, "set_adv_layout": { "layout": "string" }, "store_offline_pkg": { "filename": "string", "backup": "string" }, "batch_install_pkgs": {}, "backup_plugin_config": { "plugin": "string" }, "restore_plugin_config": { "plugin": "string" } }' + exit 0 + ;; + call) + case "$2" in + + backup_plugin_config) + read input + plugin_name=$(echo "$input" | jsonfilter -e '@.plugin') + + if [ -z "$plugin_name" ]; then + echo '{ "result": 1, "error": "plugin_name_empty" }' + exit 0 + fi + + TARGET_FILES="" + + # 辅助函数:格式化路径,抹平斜杠差异 + add_target() { + local p="$1" + p=$(echo "$p" | tr -s '/') # 合并连续的斜杠 (如 // 变 /) + p="${p#/}" # 砍掉开头的斜杠 + p="${p%/}" # 砍掉结尾的斜杠 + if [ -n "$p" ]; then + TARGET_FILES="$TARGET_FILES $p" + fi + } + + # 规则 1:匹配 /etc/config/ 下的同名文件及其衍生文件夹 + for item in /etc/config/${plugin_name}*; do + if [ -e "$item" ] || [ -L "$item" ]; then + add_target "$item" + fi + done + + # 规则 2:匹配同名的大型插件核心目录 + if [ -d "/etc/$plugin_name" ] || [ -L "/etc/$plugin_name" ]; then + add_target "/etc/$plugin_name" + fi + + # 规则 3:遍历全局白名单 + if [ -n "$EXTRA_BACKUP_DIRS" ]; then + for dir in $EXTRA_BACKUP_DIRS; do + if echo "$dir" | grep -q "$plugin_name"; then + if [ -e "$dir" ] || [ -L "$dir" ]; then + add_target "$dir" + fi + fi + done + fi + + # 父子目录冲突剪枝算法 + FINAL_TARGETS="" + # 先按字母顺序排序(确保 /etc/abc 一定排在 /etc/abc/def 前面) + SORTED_TARGETS=$(echo "$TARGET_FILES" | tr ' ' '\n' | sort -u) + + for current in $SORTED_TARGETS; do + is_sub=0 + for parent in $FINAL_TARGETS; do + # 判断当前路径是否以 "父路径/" 开头,如果是,说明它是子目录,抛弃 + case "$current/" in + "$parent/"*) is_sub=1; break ;; + esac + done + # 如果不是任何已存在路径的子目录,才允许加入最终名单 + if [ $is_sub -eq 0 ]; then + FINAL_TARGETS="$FINAL_TARGETS $current" + fi + done + + TARGET_FILES=$(echo "$FINAL_TARGETS" | xargs) + if [ -z "$TARGET_FILES" ]; then + echo '{ "result": 2, "error": "no_target_files_found" }' + exit 0 + fi + + # 获取 Web 根目录 + WEB_ROOT=$(uci get uhttpd.main.home 2>/dev/null || echo "/www") + DL_DIR="$WEB_ROOT/netwiz_dl" + + # 确保目录存在 + mkdir -p "$DL_DIR" >/dev/null 2>&1 + chmod 777 "$DL_DIR" >/dev/null 2>&1 + + # 发生过断电重启等意外导致自毁进程丢失,先用 find 扫一遍过期文件 + find "$DL_DIR" -type f -name "*.tar*" -mmin +60 -exec rm -f {} \; >/dev/null 2>&1 + # 每次生成新备份前,清空当前目录下的所有老备份 + rm -f "$DL_DIR"/*.tar* >/dev/null 2>&1 + + # 如果当前 Web 根目录因为挂载问题不可写,则切换备用目录 + if [ ! -w "$DL_DIR" ]; then + DL_DIR="$WEB_ROOT/luci-static/netwiz_dl" + mkdir -p "$DL_DIR" >/dev/null 2>&1 + chmod 777 "$DL_DIR" >/dev/null 2>&1 + + find "$DL_DIR" -type f -name "*.tar*" -mmin +60 -exec rm -f {} \; >/dev/null 2>&1 + rm -f "$DL_DIR"/*.tar* >/dev/null 2>&1 + fi + + # 生成带时间戳的文件名 + TIME_STAMP=$(date +%Y%m%d_%H%M%S) + FILE_NAME="${plugin_name}_backup_${TIME_STAMP}.tar" + TMP_FILE="$DL_DIR/$FILE_NAME" + + # 执行纯 tar 极速打包 + tar -cf "$TMP_FILE" -C / $TARGET_FILES >/dev/null 2>&1 + + if [ -s "$TMP_FILE" ]; then + chmod 644 "$TMP_FILE" >/dev/null 2>&1 + + # 15分钟后,删除最后产生的一个文件 + ( sleep 900 && rm -f "$TMP_FILE" ) >/dev/null 2>&1 & + + # 返回下载链接 + if echo "$DL_DIR" | grep -q "luci-static"; then + echo "{ \"result\": 0, \"url\": \"/luci-static/netwiz_dl/$FILE_NAME\" }" + else + echo "{ \"result\": 0, \"url\": \"/netwiz_dl/$FILE_NAME\" }" + fi + else + echo '{ "result": 3, "error": "tar_command_failed" }' + fi + ;; + + restore_plugin_config) + read input + plugin_name=$(echo "$input" | jsonfilter -e '@.plugin') + + # 注意:前端固定上传到了这个临时文件名,Linux 不看后缀只看文件头,所以名字带 .gz 没关系 + TMP_FILE="/tmp/plugin_restore_tmp.tar.gz" + + if [ ! -f "$TMP_FILE" ]; then + echo '{ "result": 1 }' + exit 0 + fi + + # 防呆 1:自适应识别文件格式 (是纯 tar 还是 gzip) + # 优先测试纯 tar (-tf) + if tar -tf "$TMP_FILE" >/dev/null 2>&1; then + TAR_TEST="tar -tf" + TAR_EXEC="tar -xf" + # 再测试 gzip (-tzf) + elif tar -tzf "$TMP_FILE" >/dev/null 2>&1; then + TAR_TEST="tar -tzf" + TAR_EXEC="tar -xzf" + else + # 两种都不是,说明传错文件或者损坏了 + rm -f "$TMP_FILE" + echo '{ "result": 3 }' + exit 0 + fi + + # 防呆 2:内容交叉验证(防止张冠李戴) + # 动态使用上面检测出来的命令 + if ! $TAR_TEST "$TMP_FILE" | grep -qi "$plugin_name"; then + rm -f "$TMP_FILE" + echo '{ "result": 4 }' # 状态 4:内容不匹配 + exit 0 + fi + + # 防呆通过!释放系统锁 + if [ -f "/etc/init.d/$plugin_name" ]; then + /etc/init.d/$plugin_name stop >/dev/null 2>&1 + sleep 1 + fi + + # 防呆 3:严苛校验写入系统 + $TAR_EXEC "$TMP_FILE" -C / >/dev/null 2>&1 + if [ $? -ne 0 ]; then + rm -f "$TMP_FILE" + echo '{ "result": 5 }' # 状态 5:解压写入失败 + exit 0 + fi + + rm -f "$TMP_FILE" + + # 保护性后台平滑重启 + if [ -f "/etc/init.d/$plugin_name" ]; then + ( + sleep 1 + /etc/init.d/$plugin_name start + /etc/init.d/$plugin_name reload + ) >/dev/null 2>&1 & + fi + + echo '{ "result": 0 }' + ;; + + get_repairable_configs) + arr="" + # 扫描系统所有的设定档 + for f in /etc/config/*; do + if [ -f "$f" ]; then + name=$(basename "$f") + + # 过滤高危网络核心组件以及后缀异常的临时档案 + if echo "$name" | grep -qE "network|wireless|dhcp|firewall|system|dropbear|rpcd|uhttpd|luci|fstab|mac80211|netwiz|.*\.apk-new|.*\.bak"; then + continue + fi + + # 包管理器反查该配置文件是否有主 + is_ghost=1 + if command -v apk >/dev/null 2>&1; then + if apk info -W "$f" 2>/dev/null | grep -q "owns"; then + is_ghost=0 + fi + elif command -v opkg >/dev/null 2>&1; then + if opkg search "$f" 2>/dev/null | grep -q " - "; then + is_ghost=0 + fi + fi + + # 1:包管理器不认,但系统里有它的同名启动脚本,视为存活 + [ -f "/etc/init.d/$name" ] && is_ghost=0 + + # 2:如果是出厂自带插件 (在 ROM 中有备份),不是死件 + [ -f "/rom/etc/config/$name" ] && is_ghost=0 + + display_name="$name" + # 如果是孤件,加上标签 + if [ "$is_ghost" -eq 1 ]; then + display_name="$name [💀💀💀]" + fi + + [ -n "$arr" ] && arr="$arr," + arr="$arr\"$display_name\"" + fi + done + + # 回传数组给前端 + echo "{\"configs\": [$arr]}" + exit 0 + ;; + + get_adv_layout) + # 1. 获取面板排版信息 + layout=$(uci -q get netwiz.main.adv_layout) + if [ -n "$layout" ]; then + # 安全转义双引号,确保前端 JSON.parse 能完美解析 + safe_layout=$(echo "$layout" | sed 's/"/\\"/g') + printf '{"layout": "%s"}\n' "$safe_layout" + else + echo "{\"layout\": null}" + fi + exit 0 + ;; + + set_adv_layout) + # 2. 只有点击确定时,才写入闪存保存排版 + input=$(cat) + layout_data=$(echo "$input" | jsonfilter -e '@.layout') + if [ -n "$layout_data" ]; then + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=main + uci set netwiz.main.adv_layout="$layout_data" + uci commit netwiz + fi + echo '{"result": 0}' + exit 0 + ;; + + store_offline_pkg) + # 3. 离线保险箱:根据用户勾选,执行临时入队或同时永久备份 + input=$(cat) + fname=$(echo "$input" | jsonfilter -e '@.filename') + fname=$(basename "$fname") + backup=$(echo "$input" | jsonfilter -e '@.backup') + + if [ -n "$fname" ] && [ -f "/tmp/$fname" ]; then + mkdir -p /tmp/nw_staging + + # 轨道一:只有用户在前端勾选了备份 (backup="1"),才写入 + if [ "$backup" = "1" ]; then + mkdir -p /etc/netwiz/custom_pkgs + cp -f "/tmp/$fname" "/etc/netwiz/custom_pkgs/$fname" + chmod 644 "/etc/netwiz/custom_pkgs/$fname" + logger -t Netwiz "[OFFLINE_SAFE] 离线包已永久备份并加入安装队列: $fname" + else + logger -t Netwiz "[OFFLINE_SAFE] 离线包已加入临时安装队列 (不留存备份): $fname" + fi + + # 轨道二:不论是否备份,当前文件都要移动到内存的临时安装队列中,供后续合并安装 + mv -f "/tmp/$fname" "/tmp/nw_staging/$fname" + fi + echo '{"result": 0}' + exit 0 + ;; + + batch_install_pkgs) + # 4. 临时队列安装引擎 + logger -t Netwiz "[OFFLINE_SAFE] 触发队列合并安装" + + ( + exec /dev/null 2>&1 + sleep 1 + + # 只安装本次拖曳进入/tmp/nw_staging/的档案 + # 避开保险箱里以前存放的所有其他旧外挂和netwiz自身 + if command -v apk >/dev/null 2>&1; then + local_pkgs=$(ls /tmp/nw_staging/*.apk 2>/dev/null) + if [ -n "$local_pkgs" ]; then + apk add --no-network --allow-untrusted --force-reinstall --force-overwrite $local_pkgs + fi + elif command -v opkg >/dev/null 2>&1; then + local_pkgs=$(ls /tmp/nw_staging/*.ipk 2>/dev/null) + if [ -n "$local_pkgs" ]; then + opkg install --force-reinstall --force-overwrite --force-depends $local_pkgs + fi + fi + + # 安装完毕,销毁本次的临时安装队列 + rm -rf /tmp/nw_staging + + # 清空LuCI界面缓存 + rm -f /tmp/luci-indexcache* + rm -f /tmp/luci-modulecache* + ) & + + echo '{"result": 0}' + exit 0 + ;; + + repair_config) + input=$(cat) + # 剥离前端传来的标签后缀 (例如 "passwall [💀💀💀]" 还原为 "passwall") + plugin=$(echo "$input" | jsonfilter -e '@.plugin' | awk '{print $1}') + + # --- 向导防呆锁:防止系统状态遗失导致向导意外弹出 --- + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + uci -q get netwiz.global >/dev/null 2>&1 || uci set netwiz.global=global + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=main + uci set netwiz.global.configured='1' + uci set netwiz.main.wizard_enable='0' + uci commit netwiz + # -------------------------------------------------- + + # 确保档案存在 + if [ -n "$plugin" ] && [ -f "/etc/config/$plugin" ]; then + log "[REPAIR]" ">>> 正在启动急救包,修复 $plugin 的设定档..." + + # 强制停止服务防止档案被占用 + [ -f "/etc/init.d/$plugin" ] && /etc/init.d/$plugin stop 2>/dev/null + + # 双引擎判断 + if [ -f "/rom/etc/config/$plugin" ]; then + # 固件内置外挂提取纯净版覆盖 + log "[REPAIR]" "检测为固件内置外挂,从ROM还原官方预设值..." + cp -f "/rom/etc/config/$plugin" "/etc/config/$plugin" + else + # 后装外挂删除配置后重装 + log "[REPAIR]" "检测为后装第三方外挂,正在执行深度重置 (Purge & Clean Install)..." + + # === 1. 更新软件源,防止缓存假死 === + if [ ! -f /tmp/nw_pkg_updated_ok ]; then + log "[REPAIR]" "正在同步最新软件源..." + if command -v apk >/dev/null 2>&1; then apk update >/dev/null 2>&1; fi + if command -v opkg >/dev/null 2>&1; then opkg update >/dev/null 2>&1; fi + touch /tmp/nw_pkg_updated_ok + fi + + # === 1.2 底层数据库查出所属套件名 === + pkg_name="" + if command -v apk >/dev/null 2>&1; then + pkg_name=$(apk info -W "/etc/config/$plugin" 2>/dev/null | awk '{print $1}' | head -n1) + elif command -v opkg >/dev/null 2>&1; then + pkg_name=$(opkg search "/etc/config/$plugin" 2>/dev/null | awk '{print $1}' | awk -F' - ' '{print $1}' | head -n1) + fi + [ -z "$pkg_name" ] && pkg_name="$plugin" + + # === 1.5 沙盒演练 === + INSTALL_POSSIBLE=0 + if command -v apk >/dev/null 2>&1; then + local_pkgs=$(ls /etc/netwiz/custom_pkgs/*${pkg_name}*.apk 2>/dev/null) + [ -z "$local_pkgs" ] && local_pkgs=$(ls /etc/netwiz/custom_pkgs/*${plugin}*.apk 2>/dev/null) + + if [ -n "$local_pkgs" ]; then + if apk add --simulate --allow-untrusted --force-reinstall --force-overwrite $local_pkgs >/dev/null 2>&1; then INSTALL_POSSIBLE=1; fi + else + # --force-reinstall去软件源查询 + if apk add --simulate --force-reinstall "$pkg_name" >/dev/null 2>&1; then INSTALL_POSSIBLE=1; fi + fi + elif command -v opkg >/dev/null 2>&1; then + local_pkgs=$(ls /etc/netwiz/custom_pkgs/*${pkg_name}*.ipk 2>/dev/null) + [ -z "$local_pkgs" ] && local_pkgs=$(ls /etc/netwiz/custom_pkgs/*${plugin}*.ipk 2>/dev/null) + + if [ -n "$local_pkgs" ]; then + if opkg install --noaction --force-reinstall --force-overwrite $local_pkgs >/dev/null 2>&1; then INSTALL_POSSIBLE=1; fi + else + # --force-reinstall去软件源查询 + if opkg install --noaction --force-reinstall --force-maintainer "$pkg_name" >/dev/null 2>&1; then INSTALL_POSSIBLE=1; fi + fi + fi + + if [ "$INSTALL_POSSIBLE" -eq 0 ]; then + log "[REPAIR]" "🚨 沙盒预检失败 (缺少依赖或源失效),触发孤件逻辑!" + + # 重新启动刚才为预检而停止的服务 + [ -f "/etc/init.d/$plugin" ] && /etc/init.d/$plugin restart >/dev/null 2>&1 & + echo '{"result": 2, "error": "Ghost package"}' + exit 0 + fi + + # === 2. 【改名】无损备份 === + # 将配置加后缀,不跨分区,不压缩 + mv "/etc/config/$plugin" "/etc/config/${plugin}.nw_bak_main" 2>/dev/null + if [ -d "/etc/${plugin}" ]; then + mv "/etc/${plugin}" "/etc/${plugin}_nw_bak_dir" 2>/dev/null + fi + + # 删除缓存或分身配置 + for f in /etc/config/${plugin}.*; do + if [ "$f" != "/etc/config/${plugin}.nw_bak_main" ]; then + # 文件或文件夹,全部删除 + rm -rf "$f" 2>/dev/null + fi + done + + # === 4. 执行真实重装逻辑 === + INSTALL_OK=0 + if command -v apk >/dev/null 2>&1; then + if [ -n "$local_pkgs" ]; then + log "[REPAIR]" "💡 在本地发现离线安装包,开始强制重装..." + apk add --no-network --allow-untrusted --force-reinstall --force-overwrite $local_pkgs >/dev/null 2>&1 && INSTALL_OK=1 + else + log "[REPAIR]" "未发现本地包,尝试官方源重装..." + apk add --force-reinstall "$pkg_name" >/dev/null 2>&1 && INSTALL_OK=1 + fi + elif command -v opkg >/dev/null 2>&1; then + if [ -n "$local_pkgs" ]; then + log "[REPAIR]" "💡 在本地发现离线安装包,开始强制重装..." + opkg install --force-reinstall --force-overwrite $local_pkgs >/dev/null 2>&1 && INSTALL_OK=1 + else + log "[REPAIR]" "未发现本地包,尝试官方源重装..." + opkg install --force-reinstall --force-maintainer "$pkg_name" >/dev/null 2>&1 && INSTALL_OK=1 + fi + fi + + # === 5. 真实重装失败,执行改名回滚 === + if [ "$INSTALL_OK" -eq 0 ] || [ ! -f "/etc/config/$plugin" ]; then + log "[REPAIR]" "🚨 真实安装遭遇断网等玄学阻断!触发原地无损回滚..." + + # 粉碎残缺安装留下的垃圾 + rm -rf "/etc/config/$plugin" "/etc/${plugin}" 2>/dev/null + + # 把之前加了后缀的备份,改名复原 + mv "/etc/config/${plugin}.nw_bak_main" "/etc/config/$plugin" 2>/dev/null + if [ -d "/etc/${plugin}_nw_bak_dir" ]; then + mv "/etc/${plugin}_nw_bak_dir" "/etc/${plugin}" 2>/dev/null + fi + + # 配置回滚完,重启服务 + [ -f "/etc/init.d/$plugin" ] && /etc/init.d/$plugin restart >/dev/null 2>&1 & + + echo '{"result": 2, "error": "Ghost package"}' + exit 0 + fi + + # 修复成功,清理掉备份的后缀文件 + rm -f "/etc/config/${plugin}.nw_bak_main" 2>/dev/null + rm -rf "/etc/${plugin}_nw_bak_dir" 2>/dev/null + fi + + # 网络组件特殊重启加载新配置 + if [ "$plugin" = "network" ] || [ "$plugin" = "dhcp" ] || [ "$plugin" = "wireless" ]; then + /etc/init.d/network restart >/dev/null 2>&1 & + elif [ "$plugin" = "firewall" ]; then + /etc/init.d/firewall restart >/dev/null 2>&1 & + else + [ -f "/etc/init.d/$plugin" ] && /etc/init.d/$plugin restart >/dev/null 2>&1 & + fi + + rm -f /tmp/luci-indexcache* >/dev/null 2>&1 + rm -f /tmp/luci-modulecache* >/dev/null 2>&1 + + log "[REPAIR]" "✅ $plugin 深度重置完毕,已满血复活!" + echo '{"result": 0}' + else + log "[REPAIR]" "⚠️ 找不到 $plugin 的配置文件!" + echo '{"result": 1, "error": "Not found"}' + fi + exit 0 + ;; + + eradicate_config) + input=$(cat) + # 剥离前端传来的标签后缀 + plugin=$(echo "$input" | jsonfilter -e '@.plugin' | awk '{print $1}') + + # --- 向导防呆锁:防止系统状态丢失导致向导意外弹出 --- + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + uci -q get netwiz.global >/dev/null 2>&1 || uci set netwiz.global=global + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=main + uci set netwiz.global.configured='1' + uci set netwiz.main.wizard_enable='0' + uci commit netwiz + sync # <--- 刷入物理硬盘,防止向导意外弹出 + # -------------------------------------------------- + + if [ -n "$plugin" ]; then + log "[REPAIR]" "💀 触发终极系统净化,准备彻底拔除孤件: $plugin" + + # 1. 停止服务并取消开机自启 + if [ -x "/etc/init.d/$plugin" ]; then + /etc/init.d/$plugin stop >/dev/null 2>&1 + /etc/init.d/$plugin disable >/dev/null 2>&1 + fi + + # 2. 查找所属套件并暴力卸载 + log "[REPAIR]" "正在扫描并卸载所有关联的依赖包..." + if command -v apk >/dev/null 2>&1; then + # 找出所有包含该名字的包 (核心包、界面包、语言包) + pkgs=$(apk info 2>/dev/null | grep -i "$plugin") + if [ -n "$pkgs" ]; then + log "[REPAIR]" "找到关联包全家桶: $pkgs" + # 绕过依赖死锁拒绝卸载 + apk del --force-broken-world $pkgs >/dev/null 2>&1 + fi + elif command -v opkg >/dev/null 2>&1; then + pkgs=$(opkg list-installed 2>/dev/null | awk '{print $1}' | grep -i "$plugin") + if [ -n "$pkgs" ]; then + log "[REPAIR]" "找到关联包全家桶: $pkgs" + opkg remove --autoremove --force-remove $pkgs >/dev/null 2>&1 + fi + fi + + # 3. 物理删除所有配置残留 + log "[REPAIR]" "删除配置残留: /etc/config/$plugin 及其附加目录" + rm -f "/etc/config/$plugin" 2>/dev/null + rm -rf "/etc/config/${plugin}."* 2>/dev/null + rm -rf "/etc/${plugin}" 2>/dev/null + rm -f "/etc/init.d/$plugin" + + # 4. 清理 LuCI 界面残留与系统状态缓存 + rm -rf "/usr/lib/lua/luci/controller/$plugin.lua" >/dev/null 2>&1 + rm -f /tmp/luci-indexcache* >/dev/null 2>&1 + rm -f /tmp/luci-modulecache* >/dev/null 2>&1 + + log "[REPAIR]" "✅ $plugin 及其残留文件已彻底从系统中抹除!" + echo '{"result": 0}' + else + echo '{"result": 1, "error": "Missing plugin param"}' + fi + exit 0 + ;; + + set_password) + input=$(cat) + pwd=$(echo "$input" | jsonfilter -e '@.password') + if [ -n "$pwd" ]; then + # 強制修改系統 root 密碼 + (echo "$pwd"; echo "$pwd") | passwd root + fi + echo '{"result":0}' + exit 0 + ;; + check_wifi) + has_wifi="false" + retry_cnt=0 + + # 增加最多3秒的等待回圈,解决路由器刚重启时内核Wi-Fi驱动加载慢的问题 + while [ "$retry_cnt" -lt 3 ]; do + # 1. 检测物理网卡底层接口 + if [ -n "$(ls -A /sys/class/net/wlan* 2>/dev/null)" ] || \ + [ -n "$(ls -A /sys/class/net/ra* 2>/dev/null)" ] || \ + [ -n "$(ls -A /sys/class/net/wl* 2>/dev/null)" ] || \ + [ -n "$(ls -A /sys/class/net/ath* 2>/dev/null)" ]; then + has_wifi="true" + break + # 2. 备用检测:iwinfo 射频状态 + elif iwinfo 2>/dev/null | grep -q -E "ESSID|Access Point|Mode"; then + has_wifi="true" + break + # 3. 终极硬件探针:直接嗅探 Linux 内核模块 (比网卡接口生成早得多,瞬间抓取) + elif ls /sys/module/ 2>/dev/null | grep -q -E "mac80211|cfg80211|mt76|ath10|ath11|ath12"; then + has_wifi="true" + break + fi + + sleep 1 + retry_cnt=$((retry_cnt + 1)) + done + + # 只有真正检测到硬件,但配置文件丢失时,才生成配置 + if [ "$has_wifi" = "true" ] && [ ! -f /etc/config/wireless ]; then + # 如果是靠底层模块探测到的,稍微等1秒让网卡接口注册完毕,否则 wifi config 可能会抓空 + sleep 1 + wifi config > /etc/config/wireless 2>/dev/null + fi + + # --- IPv6 与 LAN NAT 冲突检测 --- + lan_zone_idx=$(uci show firewall 2>/dev/null | grep -E "\.name='?lan'?" | cut -d'.' -f2 | head -n 1) + lan_nat_conflict="false" + if [ -n "$lan_zone_idx" ]; then + lan_masq=$(uci -q get firewall.$lan_zone_idx.masq) + if [ "$lan_masq" = "1" ]; then + lan_nat_conflict="true" + fi + fi + # ------------------------------------------ + + # --- 获取架构信息,供前端核对文件名防呆 --- + pkg_type="ipk" + command -v apk >/dev/null 2>&1 && pkg_type="apk" + sys_arch=$(uname -m) + + # 输出,把冲突状态和架构信息一起传给前端 + echo "{\"has_wifi\":$has_wifi, \"lan_nat_conflict\":$lan_nat_conflict, \"pkg_type\":\"$pkg_type\", \"sys_arch\":\"$sys_arch\"}" + exit 0 + ;; + + + # 检测网络连通性 + check_internet) + # ========================================== + # 惰性垃圾回收 (/tmp/ 升级文件) + # ========================================== + if [ -f /tmp/netwiz_update.version ]; then + if ! pidof wget >/dev/null 2>&1 && ! pidof apk >/dev/null 2>&1 && ! pidof opkg >/dev/null 2>&1; then + TARGET_VER=$(cat /tmp/netwiz_update.version | sed 's/[vV]//g' | tr -d '\r\n ') + + LOCAL_VERS="" + for f in $(find /www/luci-static/ -name "netwiz.js" 2>/dev/null); do + # 锁定 "APP_VERSION + 任意符号 + 数字" 的结构,无视逗号、大括号或换行 + RAW_EXTRACT=$(grep -oE "APP_VERSION[^a-zA-Z0-9]+[vV]?[0-9]+\.[0-9]+\.[0-9]+" "$f" 2>/dev/null) + + # 纯净提炼数字 + v=$(echo "$RAW_EXTRACT" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+") + LOCAL_VERS="$LOCAL_VERS $v" + done + + if [ -n "$LOCAL_VERS" ] && [ -n "$TARGET_VER" ] && echo "$LOCAL_VERS" | grep -q "$TARGET_VER"; then + rm -f /tmp/apk_luci-* /tmp/ipk_luci-* /tmp/netwiz_update.* /tmp/nw_dl.sh /tmp/nw_install.sh /tmp/netwiz_install.log 2>/dev/null + # 清场成功,准备发射隐藏小红点信号 + UPGRADE_SUCCESS=1 + fi + fi + fi + # ========================================== + + # 检测网络连通性,原生 ping 逻辑 + if ping -c 1 -W 2 223.5.5.5 >/dev/null 2>&1 || ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then + echo '{"status":"ok"}' + else + echo '{"status":"fail"}' + fi + exit 0 + ;; + get_client_mac) + # 找出当前正在连接路由器网页后台(80/443端口)的设备 IP,匹配 MAC + client_ip=$(netstat -nt 2>/dev/null | awk '$4 ~ /:(80|443)$/ && $6 == "ESTABLISHED" {split($5, a, ":"); print a[1]}' | head -n 1) + + if [ -n "$client_ip" ]; then + mac=$(cat /proc/net/arp 2>/dev/null | awk -v ip="$client_ip" '$1 == ip {print $4}') + fi + + # 兜底:降级抓取第一个活动的有效设备 + if [ -z "$mac" ] || [ "$mac" = "00:00:00:00:00:00" ]; then + mac=$(cat /proc/net/arp 2>/dev/null | grep -E "0x2" | grep -iv "00:00:00:00:00:00" | head -n 1 | awk '{print $4}') + fi + + echo "{\"mac\":\"$mac\"}" + exit 0 + ;; + get_adv_settings) + mac=$(uci -q get network.wan.macaddr) + + web="0" + _src=$(uci -q get firewall.netwiz_web.src_dport 2>/dev/null) + _dest=$(uci -q get firewall.netwiz_web.dest_port 2>/dev/null) + + if [ -n "$_src" ]; then + web="$_src" + elif [ -n "$_dest" ]; then + if [ "$_dest" = "80 443" ]; then + web="1" + else + web="$_dest" + fi + fi + + cron="off" + if grep -q "netwiz_reboot" /etc/crontabs/root 2>/dev/null; then + cron=$(grep "netwiz_reboot" /etc/crontabs/root | head -n 1 | awk '{print $1" "$2" "$3" "$4" "$5}') + fi + + hosts_json="[]" + if [ -s /etc/netwiz/hosts.json ]; then + _tmp=$(cat /etc/netwiz/hosts.json 2>/dev/null) + [ -n "$_tmp" ] && hosts_json="$_tmp" + fi + + # 从UCI配置文件读取曾经保存过的自定义端口 + last_port=$(uci -q get netwiz.main.web_port 2>/dev/null) + [ -z "$last_port" ] && last_port="80" + + # --- 提取全局活跃公网 IPv6 地址 (过滤掉局域网 fd 开头的地址) --- + wan6_ip=$(ip -o -6 addr list scope global 2>/dev/null | grep -v "deprecated" | grep -v " fd" | awk '{print $4}' | cut -d'/' -f1 | head -n 1) + + # 输出 JSON 时加上 last_port 和 wan6_ip + echo "{\"mac\":\"$mac\", \"web\":\"$web\", \"cron\":\"$cron\", \"hosts\":$hosts_json, \"last_port\":\"$last_port\", \"wan6_ip\":\"$wan6_ip\"}" + exit 0 + ;; + set_adv_settings) + input=$(cat) + mac=$(echo "$input" | jsonfilter -e '@.mac') + web=$(echo "$input" | jsonfilter -e '@.web') + cron=$(echo "$input" | jsonfilter -e '@.cron') + + # --- 接收前端传来的 Hosts JSON 字符串 --- + hosts_str=$(echo "$input" | jsonfilter -e '@.hosts') + + # 只有当 hosts_str 不是空字串時才執行更新 + # (前端传 '' 代表不修改,傳 '[]' 代表清空所有規則) + if [ -n "$hosts_str" ]; then + mkdir -p /etc/netwiz + echo "$hosts_str" > /etc/netwiz/hosts.json + + # 写入系统核心 /etc/hosts + # 采用重定向写入,极其安全,不破坏 OpenWrt 底层系统软连接 + cp /etc/hosts /tmp/hosts.bak 2>/dev/null + sed '/# === NETWIZ HOSTS BEGIN ===/,/# === NETWIZ HOSTS END ===/d' /tmp/hosts.bak > /etc/hosts + rm -f /tmp/hosts.bak + + # 写入新数据 + if [ -n "$hosts_str" ] && [ "$hosts_str" != "[]" ]; then + echo "# === NETWIZ HOSTS BEGIN ===" >> /etc/hosts + echo "$hosts_str" | sed 's/},{/\n/g' | grep '"en":true' | sed 's/.*"ip":"\([^"]*\)".*"dom":"\([^"]*\)".*/\1 \2/' >> /etc/hosts + echo "# === NETWIZ HOSTS END ===" >> /etc/hosts + fi + + # 清理上一版可能残留的旧配置 + if uci -q get dhcp.@dnsmasq[0].addnhosts | grep -q '/etc/dnsmasq.hosts.custom'; then + uci del_list dhcp.@dnsmasq[0].addnhosts='/etc/dnsmasq.hosts.custom' + uci commit dhcp + fi + + # 重启底层 dnsmasq,所有插件都会瞬间读取到 /etc/hosts 的新规则 + /etc/init.d/dnsmasq restart & + fi + + if [ -n "$mac" ]; then + if [ "$mac" = "none" ]; then uci delete network.wan.macaddr 2>/dev/null + else uci set network.wan.macaddr="$mac"; fi + uci commit network + /etc/init.d/network restart & + fi + + # ========== 1. 外网访问配置逻辑 (包含 Procd 保活) ========== + if [ -n "$web" ]; then + + # --- 后端核心安全校验 (防绕过与防注入) --- + if [ "$web" != "0" ] && [ "$web" != "1" ] && [ "$web" != "80" ] && [ "$web" != "443" ]; then + if ! expr "$web" + 0 >/dev/null 2>&1; then echo '{"status":"error", "msg":"Security Block: Invalid port format"}'; exit 0; fi + if [ "$web" -lt 1 ] || [ "$web" -gt 65535 ]; then echo '{"status":"error", "msg":"Security Block: Port out of range"}'; exit 0; fi + case "$web" in 21|22|23|53|67|68|445) echo '{"status":"error", "msg":"Security Block: Danger port is not allowed"}'; exit 0; ;; esac + + # 动态检测端口是否已被占用 + if netstat -tuln 2>/dev/null | awk '{print $4}' | grep -E -q ":$web$"; then + is_uhttpd=$(netstat -tulpn 2>/dev/null | grep -E ":$web " | grep "uhttpd") + if [ -z "$is_uhttpd" ]; then + # exit 0 送达前端 + echo '{"status":"error", "msg":"Port Conflict: Port '$web' is already in use by another application!"}' + exit 0 + fi + fi + fi + # ---------------------------------------- + + # --- Web 服务端口联动 --- + # 彻底清空并重建 uhttpd 的 HTTP 监听列表,完美解决 option/list 冲突引发的静默失败 + uci -q delete uhttpd.main.listen_http + uci add_list uhttpd.main.listen_http="0.0.0.0:80" + uci add_list uhttpd.main.listen_http="[::]:80" + + # 如果开启了外网访问且自定义了非标端口,则安全追加监听 + if [ "$web" != "0" ] && [ "$web" != "1" ] && [ "$web" != "80" ] && [ "$web" != "443" ]; then + uci add_list uhttpd.main.listen_http="0.0.0.0:$web" + uci add_list uhttpd.main.listen_http="[::]:$web" + fi + uci commit uhttpd + # ---------------------------------------- + + uci -q delete firewall.netwiz_web + + # 记忆用户输入的非标端口统一保存到底层UCI配置文件 + if [ "$web" != "0" ] && [ "$web" != "1" ] && [ "$web" != "80" ] && [ "$web" != "443" ]; then + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=main + uci set netwiz.main.web_port="$web" + uci commit netwiz + fi + + if [ "$web" != "0" ]; then + if [ "$web" = "1" ] || [ "$web" = "80" ] || [ "$web" = "443" ]; then + uci set firewall.netwiz_web=rule + uci set firewall.netwiz_web.name='Netwiz_Web_Access' + uci set firewall.netwiz_web.src='wan' + if [ "$web" = "1" ]; then + uci set firewall.netwiz_web.dest_port='80 443' + else + uci set firewall.netwiz_web.dest_port="$web" + fi + uci set firewall.netwiz_web.target='ACCEPT' + uci set firewall.netwiz_web.proto='tcp' + else + # --- 兼容 IPv6 的入站规则 (rule) --- + uci set firewall.netwiz_web=rule + uci set firewall.netwiz_web.name='Netwiz_Web_Custom' + uci set firewall.netwiz_web.src='wan' + uci set firewall.netwiz_web.dest_port="$web" + uci set firewall.netwiz_web.target='ACCEPT' + uci set firewall.netwiz_web.proto='tcp' + fi + fi + + uci commit firewall + evaluate_keepalive # 🚀 通知中央引擎重新评估保活状态 + + # 背景执行防火墙重启与高级应用唤醒防止代理规则被冲刷 + ( + # 优先重启 uhttpd 让新端口立刻生效 + /etc/init.d/uhttpd restart >/dev/null 2>&1 + /etc/init.d/firewall restart >/dev/null 2>&1 + # 等待防火墙完全启动后再次唤醒依赖网络的高级应用 + sleep 8 + restart_depend_apps >/dev/null 2>&1 + ) & + fi + # ========== 2. 定时重启逻辑 (保留您原本的,完全不用改) ========== + if [ -n "$cron" ]; then + touch /etc/crontabs/root + grep -v "netwiz_reboot" /etc/crontabs/root > /tmp/cron.tmp 2>/dev/null || true + if [ "$cron" != "off" ]; then echo "$cron sleep 70 && touch /etc/banner && reboot #netwiz_reboot" >> /tmp/cron.tmp; fi + mv /tmp/cron.tmp /etc/crontabs/root + /etc/init.d/cron restart 2>/dev/null + fi + echo '{"result":0}' + exit 0 + ;; + + check_missing_pkgs) + missing_pkgs="" + provided_pkgs="" + official_pkgs="" + + # 1. 取 /etc/netwiz/custom_pkgs/ 里面的文件 + for file in /etc/netwiz/custom_pkgs/*.[ia]pk; do + if [ -f "$file" ]; then + filename=$(basename "$file") + # 保险箱有netwiz,直接跳过,显示在「已备妥」清单 + if echo "$filename" | grep -qi "netwiz"; then + continue + fi + provided_pkgs="$provided_pkgs $filename" + fi + done + + # 2. 比对函数:是否备妥了该插件的安装包,兼容任意前缀、版本号后缀 + check_pkg_missing() { + local p=$1 + if ls /etc/netwiz/custom_pkgs/*${p}*.[ia]pk >/dev/null 2>&1; then + : # 找到了包含该包名的物理文件,不做处理 + else + missing_pkgs="$missing_pkgs $p" + fi + } + + # 3. 扫描系统中已安裝与同步源 + if [ ! -f "/tmp/nw_pkg_updated_ok" ]; then + # 正在更新,直接抛错,掐断前端的重试请求,不堵塞网络 + if [ -f "/tmp/nw_pkg_updating" ]; then + exit 1 + fi + touch "/tmp/nw_pkg_updating" + + if ping -c 1 -W 2 223.5.5.5 >/dev/null 2>&1 || ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then + log "[BACKUP]" "检测到网络畅通,正在同步软件源..." + + current_year=$(date +%Y) + if [ "$current_year" -lt 2024 ]; then + ntpd -n -q -p ntp.aliyun.com >/dev/null 2>&1 || ntpd -n -q -p time.apple.com >/dev/null 2>&1 + fi + + if command -v opkg >/dev/null 2>&1; then + opkg update >/dev/null 2>&1 + elif command -v apk >/dev/null 2>&1; then + apk update >/dev/null 2>&1 + fi + + touch "/tmp/nw_pkg_updated_ok" + log "[BACKUP]" "软件源同步完毕!" + fi + rm -f "/tmp/nw_pkg_updating" + fi + + # 4. 统一获取已安装列表与精准确比对 + installed="" + # 优先使用 opkg!官方的无缝包装器 + if command -v opkg >/dev/null 2>&1; then + installed=$(opkg list-installed 2>/dev/null | awk '{print $1}' | grep -E "^luci-(app|theme)-") + + # 调用包装器生成官方包列表 + avail_pkgs=$(opkg list 2>/dev/null | awk '{print $1}') + + for pkg in $installed; do + # opkg扫描时遇到netwiz相关文件直接跳过,不列入比对名单 + if echo "$pkg" | grep -qi "netwiz"; then + continue + fi + + if echo "$avail_pkgs" | grep -qx "$pkg"; then + official_pkgs="$official_pkgs $pkg" + else + check_pkg_missing "$pkg" + fi + done + + elif command -v apk >/dev/null 2>&1; then + # 仅在极限精简的纯血 apk 固件上才会走到这里 + installed=$(apk info 2>/dev/null | grep -E "^luci-(app|theme)-") + if [ -z "$installed" ]; then + installed=$(apk list -I 2>/dev/null | awk '{print $1}' | grep -E "^luci-(app|theme)-") + fi + + for pkg in $installed; do + # apk扫描时遇到netwiz相关文件直接跳过,不列入比对名单 + if echo "$pkg" | grep -qi "netwiz"; then + continue + fi + + # 精确查询,逐个查验 + if [ -n "$(apk search -q -e "$pkg" 2>/dev/null)" ]; then + official_pkgs="$official_pkgs $pkg" + else + check_pkg_missing "$pkg" + fi + done + fi + + # 4. JSON 阵列格式 + m_arr="" + if [ -n "$missing_pkgs" ]; then m_arr='"'"$(echo "$missing_pkgs" | awk '{$1=$1;print}' | sed 's/ /","/g')"'"'; fi + + p_arr="" + if [ -n "$provided_pkgs" ]; then p_arr='"'"$(echo "$provided_pkgs" | awk '{$1=$1;print}' | sed 's/ /","/g')"'"'; fi + + o_arr="" + if [ -n "$official_pkgs" ]; then o_arr='"'"$(echo "$official_pkgs" | awk '{$1=$1;print}' | sed 's/ /","/g')"'"'; fi + + # 回传:缺失插件、文件插件、官方插件 + pkg_type="ipk" + command -v apk >/dev/null 2>&1 && pkg_type="apk" + sys_arch=$(uname -m) + + echo "{ \"missing\": [$m_arr], \"provided\": [$p_arr], \"official\": [$o_arr], \"pkg_type\": \"$pkg_type\", \"sys_arch\": \"$sys_arch\" }" + ;; + + confirm) + # 收到前端存活信号,连通性确认成功 + # 一键清理所有看门狗标记与健康备份档案 + if [ -f /tmp/netwiz_rollback_time ] || [ -f /etc/netwiz_pending_recovery ]; then + + # 拆除 LAN 120 秒看门狗与异常断电备份 + rm -f /tmp/netwiz_rollback_time \ + /tmp/netwiz_target_ip \ + /etc/config/network.netwiz_bak \ + /etc/config/dhcp.netwiz_bak \ + /etc/config/network.netwiz_wan_bak + + # 拆除跨重启物理看门狗 (/etc) + rm -f /etc/netwiz_pending_recovery + rm -rf /etc/netwiz/failsafe + + log "前端已重新连接:已彻底拆除所有防失联回滚装置!" + fi + echo '{"status": "confirmed", "result": 0}' + exit 0 + ;; + + check_ip_conflict) + read input + # 前端传來的 IP + req_ip=$(echo "$input" | jsonfilter -e '@.ip') + + if ping -c 1 -W 1 "$req_ip" >/dev/null 2>&1; then + # Ping 通了,-> 冲突 + echo '{ "status": "conflict" }' + else + # Ping 不通,-> 安全 + echo '{ "status": "safe" }' + fi + ;; + + # 获取向导状态与智能探针 + get_wizard_status) + configured=$(uci -q get netwiz.global.configured) + [ -z "$configured" ] && configured=0 + + wizard_enable=$(uci -q get netwiz.main.wizard_enable) + [ -z "$wizard_enable" ] && wizard_enable=1 + + is_pppoe=0 + wan_proto=$(uci -q get network.wan.proto) + wan_user=$(uci -q get network.wan.username) + if [ "$wan_proto" = "pppoe" ] && [ -n "$wan_user" ]; then + is_pppoe=1 + fi + + echo "{\"configured\": $configured, \"wizard_enable\": $wizard_enable, \"is_pppoe\": $is_pppoe}" + exit 0 + ;; + + # 状态锁控 + set_wizard_status) + input=$(cat) + mode=$(echo "$input" | jsonfilter -e '@.mode') + + # 防呆 + # 检查并创建节点,防止因为配置文件缺失导致的 uci set 静默失效 + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + uci -q get netwiz.global >/dev/null 2>&1 || uci set netwiz.global=global + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=main + + if [ "$mode" = "skip" ]; then + # 普通跳过 + uci set netwiz.global.configured='1' + elif [ "$mode" = "restore" ]; then + # 手动恢复弹窗 + uci set netwiz.global.configured='1' + uci set netwiz.main.wizard_enable='1' + elif [ "$mode" = "done" ]; then + # 彻底完成/不再提示 + uci set netwiz.global.configured='1' + uci set netwiz.main.wizard_enable='0' + fi + + uci commit netwiz + sync # 刷入物理闪存,防止丢失 + + echo '{"result": 0}' + exit 0 + ;; + + set_network) + input=$(cat) + mode=$(echo "$input" | jsonfilter -e '@.mode') + arg1=$(echo "$input" | jsonfilter -e '@.arg1') + + # ========================================== + # 拦截热更新 (OTA) - 放在最顶层,拦截无意义的磁盘 I/O + # ========================================== + if [ "$mode" = "prepare_update" ]; then + # --- 抗默拦截 (防缓存重复下载) --- + TARGET_VER=$(echo "$arg1" | tr -d '\r\n ' | sed 's/^v//') + CUR_VER="" + if command -v apk >/dev/null 2>&1; then + CUR_VER=$(apk info -e luci-app-netwiz 2>/dev/null | sed 's/luci-app-netwiz-//') + elif command -v opkg >/dev/null 2>&1; then + CUR_VER=$(opkg status luci-app-netwiz 2>/dev/null | grep Version | awk '{print $2}') + fi + if [ -n "$CUR_VER" ] && [ -n "$TARGET_VER" ]; then + if echo "$CUR_VER" | grep -q "$TARGET_VER"; then + rm -f /tmp/netwiz_update.* /tmp/apk_luci-* /tmp/ipk_luci-* /tmp/nw_dl.sh + echo '{"result":0, "status":"ignored_by_cache"}' + exit 0 + fi + fi + # ------------------------------------ + + echo '{"result":0}' + rm -f /tmp/netwiz_update.* /tmp/apk_luci-* /tmp/ipk_luci-* /tmp/nw_dl.sh + echo "$arg1" > /tmp/netwiz_update.version + + cat << 'EOF' > /tmp/nw_dl.sh +#!/bin/sh +TARGET_VER=$(cat /tmp/netwiz_update.version | tr -d '\r\n ') + +if command -v apk >/dev/null 2>&1; then + PREFIX="apk_" + EXT=".apk" +else + PREFIX="ipk_" + EXT=".ipk" +fi + +F_MAIN="${PREFIX}luci-app-netwiz${EXT}" +F_CN="${PREFIX}luci-i18n-netwiz-zh-cn${EXT}" +F_TW="${PREFIX}luci-i18n-netwiz-zh-tw${EXT}" + +dl_file() { + local fname=$1 + local out="/tmp/$fname" + local url1="https://github.com/huchd0/luci-app-netwiz/releases/download/${TARGET_VER}/${fname}" + local url2="https://mirror.ghproxy.com/${url1}" + + for i in 1 2 3; do + rm -f "$out" + if wget -qO "$out" --no-check-certificate --timeout=15 "$url1" || \ + wget -qO "$out" --no-check-certificate --timeout=15 "$url2"; then + + if [ -f "$out" ]; then + local fsize=$(wc -c < "$out" 2>/dev/null | awk '{print $1}') + if [ -n "$fsize" ] && [ "$fsize" -gt 10240 ]; then + if ! grep -qi "/dev/null 2>&1 & + exit 0 + + # --- 检查更新就绪 (带物理实体核验) --- + elif [ "$mode" = "check_update" ]; then + if [ -f /tmp/netwiz_update.ready ]; then + PREFIX="ipk_" + EXT=".ipk" + command -v apk >/dev/null 2>&1 && { PREFIX="apk_"; EXT=".apk"; } + F_MAIN="/tmp/${PREFIX}luci-app-netwiz${EXT}" + F_SIZE=$(wc -c < "$F_MAIN" 2>/dev/null || echo 0) + if [ "$F_SIZE" -gt 10240 ]; then + echo '{"result": 1}' + else + rm -f /tmp/netwiz_update.ready /tmp/netwiz_update.version "$F_MAIN" + echo '{"result": 0}' + fi + else + echo '{"result": 0}' + fi + exit 0 + + # --- 无感热重载 --- + elif [ "$mode" = "do_install" ]; then + echo '{"result":0}' + cat << 'EOF' > /tmp/nw_install.sh +#!/bin/sh +# 稍微等一秒,让前端接口有时间返回 {"result":0},避免网络连接被强制掐断 +sleep 1 + +if command -v apk >/dev/null 2>&1; then + PREFIX="apk_" + EXT=".apk" +else + PREFIX="ipk_" + EXT=".ipk" +fi + +F_MAIN="/tmp/${PREFIX}luci-app-netwiz${EXT}" +F_CN="/tmp/${PREFIX}luci-i18n-netwiz-zh-cn${EXT}" +F_TW="/tmp/${PREFIX}luci-i18n-netwiz-zh-tw${EXT}" + +INSTALL_LIST="$F_MAIN" +[ -f "$F_CN" ] && INSTALL_LIST="$INSTALL_LIST $F_CN" +[ -f "$F_TW" ] && INSTALL_LIST="$INSTALL_LIST $F_TW" + +# 执行安装 +if [ "$EXT" = ".apk" ]; then + apk add --allow-untrusted --force-overwrite --force-reinstall $INSTALL_LIST > /tmp/netwiz_install.log 2>&1 + INSTALL_STATUS=$? +else + opkg install $INSTALL_LIST --force-reinstall --force-overwrite --force-maintainer > /tmp/netwiz_install.log 2>&1 + INSTALL_STATUS=$? +fi + +# 安装成功后,清理 luci 缓存并热重载 +if [ "$INSTALL_STATUS" -eq 0 ]; then + rm -rf /tmp/luci-modulecache/ /tmp/luci-indexcache* /tmp/luci-app-netwiz.* + ubus call luci reload >/dev/null 2>&1 +fi +EOF + chmod +x /tmp/nw_install.sh + /tmp/nw_install.sh /dev/null 2>&1 & + exit 0 + fi + # ========================================== + + # ========================================== + # 以下为您原本的网络配置、向导等逻辑 (安全区域) + # ========================================== + arg2=$(echo "$input" | jsonfilter -e '@.arg2') + arg3=$(echo "$input" | jsonfilter -e '@.arg3') + arg4=$(echo "$input" | jsonfilter -e '@.arg4') + arg5=$(echo "$input" | jsonfilter -e '@.arg5') + arg6=$(echo "$input" | jsonfilter -e '@.arg6') + + set +e + + # 应用网络配置,死锁向导 + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + uci -q get netwiz.global >/dev/null 2>&1 || uci set netwiz.global=global + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=main + + uci set netwiz.global.configured='1' + uci set netwiz.main.wizard_enable='0' + uci commit netwiz + sync # 将RAM缓存刷入物理闪存 + + log "接收到前端下发的新配置,删除所有历史备份记录" + rm -f /etc/config/network.netwiz_wan_bak /etc/config/network.netwiz_bak /etc/config/dhcp.netwiz_bak + # ========================================== + + validate_strict_ip() { + if [ -n "$1" ] && ! echo "$1" | awk -F. 'NF==4 && $1>=0 && $1<=255 && $2>=0 && $2<=255 && $3>=0 && $3<=255 && $4>=0 && $4<=255 {print "ok"}' | grep -q "ok"; then + log "安全拦截 IP格式非法" + echo '{"result":1,"error":"Invalid format"}' + exit 1 + fi + } + + if [ "$mode" = "wan_static" ] || [ "$mode" = "lan" ]; then + validate_strict_ip "$arg1" + validate_strict_ip "$arg2" + validate_strict_ip "$arg3" + fi + + cleanup_lan_for_wan_modes() { + uci delete network.lan.gateway 2>/dev/null || true + uci delete network.lan.dns 2>/dev/null || true + uci set dhcp.lan.ignore='0' + } + + # --- 协议净化 --- + cleanup_wan_for_proto() { + local target="$1" + + # 1. 切换后,统一刪除容易 netifd 死锁的 DHCP 干扰 + uci delete network.wan.reqopts 2>/dev/null || true + uci delete network.wan.clientid 2>/dev/null || true + uci delete network.wan.vendorid 2>/dev/null || true + + # 2. 根据目标协议,删除「不兼容」的参数,保留dns、metric等共享高级设定 + if [ "$target" = "pppoe" ]; then + uci delete network.wan.ipaddr 2>/dev/null || true + uci delete network.wan.netmask 2>/dev/null || true + uci delete network.wan.gateway 2>/dev/null || true + elif [ "$target" = "dhcp" ]; then + uci delete network.wan.ipaddr 2>/dev/null || true + uci delete network.wan.netmask 2>/dev/null || true + uci delete network.wan.gateway 2>/dev/null || true + uci delete network.wan.username 2>/dev/null || true + uci delete network.wan.password 2>/dev/null || true + uci delete network.wan.ipv6 2>/dev/null || true + uci delete network.wan.norelease 2>/dev/null || true + uci delete network.wan.multipath 2>/dev/null || true + elif [ "$target" = "static" ]; then + uci delete network.wan.username 2>/dev/null || true + uci delete network.wan.password 2>/dev/null || true + uci delete network.wan.ipv6 2>/dev/null || true + uci delete network.wan.norelease 2>/dev/null || true + uci delete network.wan.multipath 2>/dev/null || true + fi + } + # ------------------------------------------------------------- + + CURRENT_LAN_IP=$(uci -q get network.lan.ipaddr | cut -d/ -f1) + + # 各模式网络写入 + + # ====== 首次开机向导模式 (Wizard) ====== + # 一次性接收并处理上网方式与 Wi-Fi 设定,最后统一重启网络 + if [ "$mode" = "wizard" ]; then + log ">>> [向导模式] 开始执行双通道配置任务..." + + # 1. 提取向导中的路由/WAN参数 + wiz_wan_type=$(echo "$arg1" | jsonfilter -e '@.wan_type' || echo "dhcp") + + cleanup_lan_for_wan_modes; + if [ "$wiz_wan_type" = "dhcp" ]; then + log "向导: 写入 DHCP 动态获取配置" + cleanup_wan_for_proto "dhcp" + uci set network.wan.proto='dhcp' + elif [ "$wiz_wan_type" = "static" ]; then + log "向导: 写入静态 IP 配置" + cleanup_wan_for_proto "static" + wiz_ip=$(echo "$arg1" | jsonfilter -e '@.ip') + wiz_gw=$(echo "$arg1" | jsonfilter -e '@.gw') + wiz_mask=$(echo "$arg1" | jsonfilter -e '@.mask') + + # 静默格式检查 (非法直接退回 DHCP 兜底,防止挂掉) + if echo "$wiz_ip" | awk -F. 'NF==4 {print "ok"}' | grep -q "ok" && \ + echo "$wiz_gw" | awk -F. 'NF==4 {print "ok"}' | grep -q "ok"; then + uci set network.wan.proto='static' + uci set network.wan.ipaddr="$wiz_ip" + uci set network.wan.gateway="$wiz_gw" + uci set network.wan.netmask="$wiz_mask" + else + log "向导警告: 静态IP格式非法,安全回退至 DHCP" + cleanup_wan_for_proto "dhcp" + uci set network.wan.proto='dhcp' + fi + elif [ "$wiz_wan_type" = "pppoe" ]; then + log "向导: 写入 PPPoE 拨号配置" + wiz_user=$(echo "$arg1" | jsonfilter -e '@.user') + wiz_pass=$(echo "$arg1" | jsonfilter -e '@.pass') + cleanup_wan_for_proto "pppoe" + uci set network.wan.proto='pppoe' + [ -n "$wiz_user" ] && uci set network.wan.username="$wiz_user" + [ -n "$wiz_pass" ] && uci set network.wan.password="$wiz_pass" + + uci set network.wan.ipv6='auto' + uci set network.wan.norelease='1' + uci set network.wan.multipath='off' + # ----------------------- + fi + + uci commit network; uci commit dhcp + log "向导: 上网配置已提交,准备接管并透传 Wi-Fi 配置任务..." + + + # 2. 现成的 Wi-Fi 处理引擎 + mode="wifi" + arg1="$arg2" + fi + # ================================== + + if [ "$mode" = "wan_dhcp" ]; then + cleanup_lan_for_wan_modes; + cleanup_wan_for_proto "dhcp" + uci set network.wan.proto='dhcp' + uci commit network; uci commit dhcp + elif [ "$mode" = "wan_static" ]; then + cleanup_lan_for_wan_modes; + cleanup_wan_for_proto "static" + uci set network.wan.proto='static' + uci set network.wan.ipaddr="$arg1" + uci set network.wan.gateway="$arg2" + uci set network.wan.netmask="$arg3" + uci commit network; uci commit dhcp + elif [ "$mode" = "pppoe" ]; then + cleanup_lan_for_wan_modes; + cleanup_wan_for_proto "pppoe" + uci set network.wan.proto='pppoe' + uci set network.wan.username="$arg1" + uci set network.wan.password="$arg2" + + uci set network.wan.ipv6='auto' + uci set network.wan.norelease='1' + uci set network.wan.multipath='off' + # ---------------------------------------------- + + uci commit network; uci commit dhcp + + # ========================================== + # 拦截并静默处理向导状态的写入 (包含跳过/关闭时的劫持释放) + elif [ "$mode" = "set_wizard" ]; then + log "静默修改向导弹出状态为: $arg1" + + # 确保文件存在,防止 uci 报错 + [ ! -f /etc/config/netwiz ] && touch /etc/config/netwiz + + uci -q get netwiz.main >/dev/null 2>&1 || uci set netwiz.main=global + uci set netwiz.main.wizard_enable="$arg1" + + uci -q get netwiz.global >/dev/null 2>&1 || uci set netwiz.global=netwiz + uci set netwiz.global.configured='1' + + uci commit netwiz + + if [ -f /www/cgi-bin/luci.bak ]; then + log "释放底层 LuCI 强制劫持" + mv /www/cgi-bin/luci.bak /www/cgi-bin/luci + fi + + echo '{"result":0}' + exit 0 + # ========================================== + + elif [ "$mode" = "lan" ]; then + + # LAN 口 120 秒安全模式防失联 (跨重启物理防砖) + if [ "$arg5" = "1" ]; then + log ">>> [LAN设置] 已启用 120 秒安全模式,生成物理防砖备份..." + cp -p /etc/config/network /etc/config/network.netwiz_bak 2>/dev/null + cp -p /etc/config/dhcp /etc/config/dhcp.netwiz_bak 2>/dev/null + touch /tmp/netwiz_rollback_time + + ( + sleep 120 + if [ -f "/tmp/netwiz_rollback_time" ]; then + logger -t Netwiz "[WATCHDOG] 🚨 LAN 120秒超时!前端未重连,执行回滚!" + echo "$(date '+%F %T') [WATCHDOG] 🚨 LAN口 120秒内未重连,触发安全回滚!" >> /etc/netwiz_recovery.log + cp -p /etc/config/network.netwiz_bak /etc/config/network 2>/dev/null + cp -p /etc/config/dhcp.netwiz_bak /etc/config/dhcp 2>/dev/null + /etc/init.d/network restart + /etc/init.d/dnsmasq restart + rm -f /tmp/netwiz_rollback_time + fi + ) /dev/null 2>&1 & + fi + + uci set network.lan.proto='static'; uci set network.lan.ipaddr="$arg1"; uci set network.lan.netmask="$arg3" + if [ -n "$arg2" ]; then uci set network.lan.gateway="$arg2"; else uci delete network.lan.gateway 2>/dev/null || true; fi + if [ "$arg4" = "1" ]; then + uci set dhcp.lan.ignore='1' + [ -n "$arg2" ] && uci set network.lan.dns="$arg2" + else + uci set dhcp.lan.ignore='0'; uci delete network.lan.dns 2>/dev/null || true + fi + + # ========================================== + # IPv6 状态对齐与自愈 + # ========================================== + current_v6=$(uci -q get dhcp.lan.dhcpv6) # 获取路由器当前真实的 IPv6 状态 + + # 如前端明确要求开启 (arg6=1),或者前端没传但底层原本就是开启的 (current_v6=server) + if [ "$arg6" = "1" ] || { [ -z "$arg6" ] && [ "$current_v6" = "server" ]; }; then + log ">>> [IPv6] 强制清洗并应用标准 DHCPv6 五重参数..." + # 1. 净化 ULA 前缀 + uci -q delete network.globals.ula_prefix + + # 2. 确保 LAN 口拥有申请 IPv6 前缀的权限 + uci set network.lan.ip6assign='60' + + # 3. 开启服务与通告 + uci set dhcp.lan.dhcpv6='server' + uci set dhcp.lan.ra='server' + uci set dhcp.lan.ndp='server' + + # 4. 开启 SLAAC (增加安卓兼容性) + uci set dhcp.lan.ra_slaac='1' + + # 5. 注入 M-Flag 和 O-Flag (先删后加防重复) + uci -q del_list dhcp.lan.ra_flags='managed-config' + uci add_list dhcp.lan.ra_flags='managed-config' + uci -q del_list dhcp.lan.ra_flags='other-config' + uci add_list dhcp.lan.ra_flags='other-config' + + uci commit network + + elif [ "$arg6" = "0" ]; then + log ">>> [IPv6] 明确要求关闭,执行无痕清理..." + uci set dhcp.lan.dhcpv6='disabled' + uci set dhcp.lan.ra='disabled' + uci set dhcp.lan.ndp='disabled' + + uci -q delete dhcp.lan.ra_slaac + uci -q del_list dhcp.lan.ra_flags='managed-config' + uci -q del_list dhcp.lan.ra_flags='other-config' + fi + + uci commit dhcp + + uci commit network; uci commit dhcp + elif [ "$mode" = "wifi" ]; then + payload="$arg1" + echo '{"result":0}' + + # 放入后台执行 避免前端卡死 + ( + exec >/tmp/netwiz_debug.log 2>&1 >> [步骤 1/5] 无线配置子进程启动,开始解析前端 Payload" + smart=$(echo "$payload" | jsonfilter -e '@.smart' || echo "false") + log "识别到智能模式状态: $smart" + + list_2g="" + list_5g="" + dev_list=$(uci show wireless 2>/dev/null | grep '=wifi-device' | cut -d'.' -f2 | cut -d'=' -f1 || true) + + if [ -z "$dev_list" ]; then + log "未找到无线网卡 终止操作" + exit 1 + fi + + # 解析物理网卡频段 + for dev in $dev_list; do + hwmode=$(uci -q get wireless.$dev.hwmode | tr 'A-Z' 'a-z' || true) + band=$(uci -q get wireless.$dev.band | tr 'A-Z' 'a-z' || true) + path=$(uci -q get wireless.$dev.path || true) + is_5g=0 + current_channel=$(uci -q get wireless.$dev.channel || echo "0") + current_ht=$(uci -q get wireless.$dev.htmode | tr 'A-Z' 'a-z' || echo "") + + # Wi-Fi 6/7 (11ax/11be) 在 2.4G 频段的误判 Bug + if [ "$band" = "5g" ] || [ "$band" = "6g" ]; then is_5g=1 + elif [ "$band" = "2g" ]; then is_5g=0 + elif [ "$current_channel" != "0" ] && [ "$current_channel" != "auto" ] && [ "$current_channel" -ge 36 ]; then is_5g=1 + elif [ "$current_channel" != "0" ] && [ "$current_channel" != "auto" ] && [ "$current_channel" -le 14 ]; then is_5g=0 + elif echo "$current_ht" | grep -qE "80|160|320"; then is_5g=1 + elif echo "$hwmode" | grep -qE "11a|11ac"; then is_5g=1 + elif echo "$hwmode" | grep -qE "11b|11g"; then is_5g=0 + else + # 没有明确特征,radio0 归 2.4G,其余归 5G + if echo "$dev" | grep -qE "radio0"; then is_5g=0; else is_5g=1; fi + fi + + if [ "$is_5g" = "1" ]; then + list_5g="$list_5g $dev" + else + list_2g="$list_2g $dev" + fi + done + + # 处理防重叠 + if [ $(echo "$list_5g" | wc -w) -ge 2 ] && [ -z "$list_2g" ]; then + list_2g=$(echo $list_5g | awk '{print $1}') + list_5g=$(echo $list_5g | cut -d' ' -f2-) + elif [ $(echo "$list_2g" | wc -w) -ge 2 ] && [ -z "$list_5g" ]; then + list_5g=$(echo $list_2g | awk '{print $2}') + list_2g=$(echo $list_2g | awk '{print $1}') + fi + + log ">>> [步骤 2/5] 网卡归类完毕。2.4G阵列: [$list_2g] | 5G阵列: [$list_5g]" + log "清空所有旧的无线网络接口" + while uci -q delete wireless.@wifi-iface[0]; do :; done + + # 定义写网卡函数 + configure_physical_radio() { + local dev="$1" + local ui_prefix="$2" + local iface_name="$3" + local intended_band="$4" + local ssid_suffix="$5" + + local p_en=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.enabled" || echo "0") + local p_ssid=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.ssid" || echo "") + + # <--- 有后缀,自动追加到 SSID 尾部 + if [ -n "$p_ssid" ] && [ -n "$ssid_suffix" ]; then + p_ssid="${p_ssid}${ssid_suffix}" + fi + local p_key=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.key" || echo "") + local p_enc=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.encryption" || echo "sae-mixed") + local p_hidden=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.hidden" || echo "0") + local p_mode=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.mode" || echo "auto") + local p_chan=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.channel" || echo "auto") + local p_bw=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.bandwidth" || echo "auto") + local p_roam=$(echo "$payload" | jsonfilter -e "@.${ui_prefix}.roaming" || echo "0") + + uci set wireless.$iface_name=wifi-iface + uci set wireless.$iface_name.device="$dev" + uci set wireless.$iface_name.network="lan" + uci set wireless.$iface_name.mode="ap" + uci set wireless.$dev.country='CN' + + if [ "$p_en" = "0" ]; then + uci set wireless.$iface_name.disabled='1' + else + uci delete wireless.$iface_name.disabled 2>/dev/null || true + uci delete wireless.$dev.disabled 2>/dev/null || true + fi + + [ -n "$p_ssid" ] && uci set wireless.$iface_name.ssid="$p_ssid" + + # === 只要有密码,不允许 none === + if [ "$p_enc" = "none" ] && [ -n "$p_key" ]; then + p_enc="sae-mixed" + fi + if [ "$p_enc" = "psk2+sae" ]; then p_enc="sae-mixed"; fi + + [ -n "$p_enc" ] && uci set wireless.$iface_name.encryption="$p_enc" + + # 只有在非 none 的情况下,才允许写入密码 + if [ "$p_enc" != "none" ] && [ -n "$p_key" ]; then + uci set wireless.$iface_name.key="$p_key" + else + uci delete wireless.$iface_name.key 2>/dev/null || true + fi + # ================================================== + + if echo "$p_enc" | grep -q "sae"; then uci set wireless.$iface_name.ieee80211w='1' + else uci delete wireless.$iface_name.ieee80211w 2>/dev/null || true; fi + + if [ "$p_roam" = "1" ] && [ "$p_enc" != "none" ] && [ -n "$p_enc" ]; then + uci set wireless.$iface_name.ieee80211r='1' + uci set wireless.$iface_name.ieee80211k='1' + uci set wireless.$iface_name.ieee80211v='1' + uci delete wireless.$iface_name.ft_psk_generate_local 2>/dev/null || true + uci set wireless.$iface_name.ft_over_ds='0' + uci set wireless.$iface_name.mobility_domain='e4d1' + uci set wireless.$iface_name.ocv='0' + else + uci delete wireless.$iface_name.ieee80211r 2>/dev/null || true + uci delete wireless.$iface_name.ieee80211k 2>/dev/null || true + uci delete wireless.$iface_name.ieee80211v 2>/dev/null || true + uci delete wireless.$iface_name.ft_psk_generate_local 2>/dev/null || true + uci delete wireless.$iface_name.ft_over_ds 2>/dev/null || true + uci delete wireless.$iface_name.mobility_domain 2>/dev/null || true + uci delete wireless.$iface_name.ocv 2>/dev/null || true + fi + + if [ "$p_hidden" = "1" ]; then uci set wireless.$iface_name.hidden='1'; else uci delete wireless.$iface_name.hidden 2>/dev/null || true; fi + + if [ "$p_en" = "1" ] || [ "$smart" = "true" ]; then + if [ "$p_chan" != "auto" ] && [ -n "$p_chan" ]; then uci set wireless.$dev.channel="$p_chan"; else uci set wireless.$dev.channel='auto'; fi + + # ===== 显式写入硬件模式 (hwmode),解决空白及不激活 Bug ===== + uci set wireless.$dev.band="$intended_band" + if [ "$intended_band" = "5g" ]; then + uci set wireless.$dev.hwmode="11a" + else + if [ "$p_mode" = "11b" ]; then + uci set wireless.$dev.hwmode="11b" + else + uci set wireless.$dev.hwmode="11g" + fi + fi + + local current_ht=$(uci -q get wireless.$dev.htmode || echo "") + local final_bw="40" + if [ "$intended_band" = "5g" ]; then final_bw="80"; fi + if [ "$p_bw" != "auto" ] && [ -n "$p_bw" ]; then final_bw="$p_bw"; fi + + # ===== htmode ,防止旧协议参数冲突 ===== + if [ "$p_mode" = "11b" ]; then + # 纯 11b 模式下不能有 htmode 参数,否则底层驱动会报错冲突 + uci delete wireless.$dev.htmode 2>/dev/null || true + else + if [ "$p_mode" != "auto" ] && [ -n "$p_mode" ]; then + if [ "$p_mode" = "11be" ]; then uci set wireless.$dev.htmode="EHT${final_bw}" + elif [ "$p_mode" = "11ax" ]; then uci set wireless.$dev.htmode="HE${final_bw}" + elif [ "$p_mode" = "11ac" ]; then uci set wireless.$dev.htmode="VHT${final_bw}" + elif [ "$p_mode" = "11n" ] || [ "$p_mode" = "11g" ]; then uci set wireless.$dev.htmode="HT${final_bw}" + else uci set wireless.$dev.htmode="HT20"; fi + else + # auto 模式下保留原生协议头 + if echo "$current_ht" | grep -qEi "eht|he|vht|ht"; then + local prefix=$(echo "$current_ht" | grep -oEi "eht|he|vht|ht" | tr 'a-z' 'A-Z') + uci set wireless.$dev.htmode="${prefix}${final_bw}" + else + uci set wireless.$dev.htmode="HE${final_bw}" + fi + fi + fi + fi + } + + log ">>> [步骤 3/5] 开始将新配置写入无线物理网卡..." + chip_count=$(echo "$dev_list" | wc -w) + + if [ "$chip_count" -le 1 ]; then + single_dev=$(echo "$dev_list" | awk '{print $1}') + if [ "$smart" = "true" ]; then + configure_physical_radio "$single_dev" "merged" "wifinet_0" "2g" "" + else + configure_physical_radio "$single_dev" "radio_2g" "wifinet_2g" "2g" "" + configure_physical_radio "$single_dev" "radio_5g" "wifinet_5g" "5g" "" + fi + else + if [ "$smart" = "true" ]; then + log "采用多频合一配置策略" + idx=0 + for dev in $dev_list; do + if echo " $list_5g " | grep -q " $dev "; then + configure_physical_radio "$dev" "merged" "wifinet_$idx" "5g" "" + else + configure_physical_radio "$dev" "merged" "wifinet_$idx" "2g" "" + fi + idx=$((idx + 1)) + done + else + log "采用独立频段配置策略" + idx=0 + for dev in $list_2g; do + configure_physical_radio "$dev" "radio_2g" "wifinet_2g_$idx" "2g" "" + idx=$((idx + 1)) + done + + idx=0 + for dev in $list_5g; do + target_prefix="radio_5g" + custom_suffix="" + + # 第二个 5G 芯片 (idx=1) + if [ "$idx" -eq 1 ]; then + # 尝试从前端提取 radio_5g2 的配置 + has_5g2=$(echo "$payload" | jsonfilter -e "@.radio_5g2.ssid" || echo "") + if [ -n "$has_5g2" ]; then + target_prefix="radio_5g2" + else + custom_suffix="_Game" + fi + # 第三、第四个 5G 芯片的终极兜底 + elif [ "$idx" -ge 2 ]; then + custom_suffix="_Game${idx}" + fi + + configure_physical_radio "$dev" "$target_prefix" "wifinet_5g_$idx" "5g" "$custom_suffix" + idx=$((idx + 1)) + done + fi + fi + + # 中继配置解析与挂载 + log ">>> [步骤 4/5] 检查并处理 WISP 无线中继配置..." + wisp_en=$(echo "$payload" | jsonfilter -e "@.wisp.enabled" || echo "0") + if [ "$wisp_en" = "1" ]; then + wisp_ssid=$(echo "$payload" | jsonfilter -e "@.wisp.ssid" || echo "") + wisp_key=$(echo "$payload" | jsonfilter -e "@.wisp.key" || echo "") + wisp_enc=$(echo "$payload" | jsonfilter -e "@.wisp.encryption" || echo "psk2") + wisp_dev=$(echo "$payload" | jsonfilter -e "@.wisp.device" || echo "radio0") + wisp_bssid=$(echo "$payload" | jsonfilter -e "@.wisp.bssid" || echo "") + log "WISP 开启:正在将网卡 $wisp_dev 桥接至上级网络 [$wisp_ssid]" + + uci delete wireless.$wisp_dev.disabled 2>/dev/null || true + uci set wireless.$wisp_dev.channel='auto' + + uci set network.wwan=interface + uci set network.wwan.proto='dhcp' + uci set network.wwan.metric='20' + + uci set wireless.wisp_sta=wifi-iface + uci set wireless.wisp_sta.device="$wisp_dev" + uci set wireless.wisp_sta.network="wwan" + uci set wireless.wisp_sta.mode="sta" + uci set wireless.wisp_sta.ssid="$wisp_ssid" + uci set wireless.wisp_sta.key="$wisp_key" + + # 适配加密格式 + if [ "$wisp_enc" = "none" ] && [ -n "$wisp_key" ]; then + wisp_enc="psk2" + elif [ "$wisp_enc" = "psk2+sae" ]; then + wisp_enc="sae-mixed" + fi + uci set wireless.wisp_sta.encryption="$wisp_enc" + + # 没有传入 BSSID,强行清空底层历史残留,避免 MAC 绑定死锁 + if [ -n "$wisp_bssid" ]; then + uci set wireless.wisp_sta.bssid="$wisp_bssid" + else + uci delete wireless.wisp_sta.bssid 2>/dev/null || true + fi + + # 官方标准的 add_list 操作防火墙,避免字符串截取错误 + wan_zone=$(uci show firewall | grep "\.name='wan'" | cut -d'.' -f2 | head -n1 || echo "") + if [ -n "$wan_zone" ]; then + uci add_list firewall.$wan_zone.network='wwan' 2>/dev/null || uci set firewall.$wan_zone.network='wan wan6 wwan' + uci commit firewall + fi + + # 强制停止并禁用 mwan3 (负载均衡会100%拦截 wwan 接口的流量导致没网) + if [ -f "/etc/init.d/mwan3" ]; then + /etc/init.d/mwan3 stop 2>/dev/null + /etc/init.d/mwan3 disable 2>/dev/null + fi + + # 中继自愈守护进程(Watchdog)写入系统的计划任务 + # 如果中继接口获取不到 IP (假死),每 2 分钟自动重连 + touch /etc/crontabs/root + grep -v "netwiz_wisp_check" /etc/crontabs/root > /tmp/cron.tmp || true + echo "*/2 * * * * ifstatus wwan | grep -q '\"up\": true' || (ifdown wwan; sleep 2; ifup wwan) #netwiz_wisp_check" >> /tmp/cron.tmp + mv /tmp/cron.tmp /etc/crontabs/root + /etc/init.d/cron restart 2>/dev/null + + else + log "清理无线中继虚拟接口及防火墙残留" + uci delete wireless.wisp_sta 2>/dev/null || true + uci delete network.wwan 2>/dev/null || true + + # 官方标准的 del_list 清理防火墙 + wan_zone=$(uci show firewall | grep "\.name='wan'" | cut -d'.' -f2 | head -n1 || echo "") + if [ -n "$wan_zone" ]; then + uci del_list firewall.$wan_zone.network='wwan' 2>/dev/null + uci commit firewall + fi + + # 关闭中继时,清理掉自愈守护进程 + if [ -f "/etc/crontabs/root" ]; then + grep -v "netwiz_wisp_check" /etc/crontabs/root > /tmp/cron.tmp || true + mv /tmp/cron.tmp /etc/crontabs/root + /etc/init.d/cron restart 2>/dev/null + fi + fi + + log ">>> [步骤 5/5] 所有配置已就绪,正在执行系统 Commit 并重启网络服务..." + uci commit wireless + uci commit network + log "无线数据写入完毕 准备重启网络" + + sleep 3 + /etc/init.d/network restart + + # 唤醒调用1 + restart_depend_apps + watchdog_pppoe_ipv6 + + log "✅ 无线配置流程全部安全执行完毕!" + exit 0 + ) & + + exit 0 + fi + + echo '{"result":0}' + ( + exec >/dev/null 2>&1 /dev/null 2>&1 || uci set netwiz.main=global + uci set netwiz.main.wizard_enable='0' + + uci -q get netwiz.global >/dev/null 2>&1 || uci set netwiz.global=netwiz + uci set netwiz.global.configured='1' + + uci commit netwiz + # ========================================== + + # 喚醒调用2 + restart_depend_apps + watchdog_pppoe_ipv6 + ) & + exit 0 + ;; + + smart_backup) + # 并发防呆锁。防止用户刷新页面多次点击,导致两个进程互相删文件的惨案 + if [ -f /tmp/nw_backup_running ]; then + # 是否锁档案已经存在超过10分钟 + if [ -n "$(find /tmp/nw_backup_running -mmin +10 2>/dev/null)" ]; then + logger -t Netwiz "[BACKUP] 檢測到超時的死鎖,系統已自動強制解除!" + rm -f /tmp/nw_backup_running + else + echo '{"error":"backup is currently running"}' + exit 0 + fi + fi + + LOG_FILE="/etc/netwiz_recovery.log" + touch /tmp/nw_backup_running + + # 解析前端传来的备份类型参数 + input=$(cat) + backup_type=$(echo "$input" | jsonfilter -e '@.type') + [ -z "$backup_type" ] && backup_type="light" + + log "[BACKUP]" "=== 开始执行智能备份 (类型: $backup_type) ===" + + PKG_TYPE="ipk" + command -v apk >/dev/null 2>&1 && PKG_TYPE="apk" + + # 1. 获取固件编译作者/分支名 + FW_TYPE="OpenWrt" + if [ -f /etc/openwrt_release ]; then + . /etc/openwrt_release + [ -n "$DISTRIB_ID" ] && FW_TYPE="$DISTRIB_ID" + fi + + # 2. 获取路由器的主机名 (如未设置则默认为 Router) + HOST_NAME=$(uci -q get system.@system[0].hostname || hostname || echo "Router") + HOST_NAME=$(echo "$HOST_NAME" | sed 's/[^a-zA-Z0-9_-]/_/g') + + # 3. 获取底层真实 CPU 架构 + SYS_ARCH=$(uname -m) + + # 文件名:前缀_类型_固件名_主机名_真实架构_包管理器_时间 + dl_name="NetWiz_Ghost_${backup_type}_${FW_TYPE}_${HOST_NAME}_${SYS_ARCH}_${PKG_TYPE}_$(date +%Y%m%d_%H%M).tar.gz" + + backup_file="/tmp/${dl_name}" + link_file="/www/luci-static/${dl_name}" + + ( + # 关闭“遇错不动”机制 + set +e + + log "[BACKUP]" "[1/4] 开始备份系统设置..." + log "[BACKUP]" "采用原生 tar 极速快照模式,防止底层进程卡死..." + + # ash 脚本的非函数区 + tar_targets="" + # 备份列表 + # 包含设置文件、软件源 rc.local, 定時任務, MAC綁定, 防火牆腳本 + base_targets="/etc/config /etc/passwd /etc/shadow /etc/group /etc/dropbear /etc/profile /etc/sysupgrade.conf /etc/hosts /etc/netwiz/hosts.json /etc/apk/repositories.d/distfeeds.list /etc/opkg/distfeeds.conf /etc/opkg/customfeeds.conf /etc/rc.local /etc/crontabs/root /etc/ethers /etc/firewall.user" + # 绝对安全过滤 + for target in $base_targets; do + if [ -e "$target" ]; then + # 去掉前面的 / 变成相对路径,配合 -C / 使用 + rel=$(echo "$target" | sed 's/^\///') + tar_targets="$tar_targets $rel" + fi + done + + # 探测高级资产目录(直接调用脚本顶部的全局白名单变量) + for dir in $EXTRA_BACKUP_DIRS; do + if [ -d "$dir" ]; then + rel=$(echo "$dir" | sed 's/^\///') + tar_targets="$tar_targets $rel" + fi + done + + # ---------------------------------------------------- + # 防呆 1:打包前生成备份核对元数据文件 + # ---------------------------------------------------- + # 穿透伪装:直接向 Linux 内核索要真实的硬件 CPU 架构 (例如 x86_64, aarch64) + SYS_ARCH=$(uname -m) + + mkdir -p /etc/netwiz + cat > "/etc/netwiz/nw_backup_meta.env" </dev/null | awk '{sum+=$1} END{print sum}') + [ -z "$req_kb" ] && req_kb=0 + avail_kb=$(df -k /tmp | awk 'NR==2 {print $4}') + [ -z "$avail_kb" ] && avail_kb=0 + safe_req_kb=$((req_kb + 5120)) + + if [ "$avail_kb" -lt "$safe_req_kb" ]; then + log "[BACKUP]" "[DANGER] 🚨 触发 OOM 防呆保护:打包目标 ($((req_kb/1024))MB) 超过可用内存 ($((avail_kb/1024))MB)!" + rm -f /tmp/nw_backup_running + exit 1 + fi + + log "[BACKUP]" "内存校验通过,开始极速压缩 (已智能排除日志与庞大数据库)..." + + # 生成排除列表文件配合 -X 参数使用 + cat > /tmp/nw_exclude_list.txt << 'EOF' +*.log +*querylog.json +*stats.db +*stats.db-wal +*stats.db-shm +*cache* +EOF + + # 使用 -X 挂载排除列表 + tar -czf /tmp/nw_sys_config.tar.gz -X /tmp/nw_exclude_list.txt -C / $tar_targets >> "$LOG_FILE" 2>&1 || true + + # 打包完成,清理临时排除列表 + rm -f /tmp/nw_exclude_list.txt + + mkdir -p /tmp/nw_offline_pkgs + cd /tmp/nw_offline_pkgs + + # 保险箱的自定义插件提取到离线包目录,随胶囊一起直观打包在外层 + if ls /etc/netwiz/custom_pkgs/*.[ia]pk >/dev/null 2>&1; then + log "[BACKUP]" "检测到本地保险箱有自定义插件,正在合并至备份胶囊外层..." + cp -p /etc/netwiz/custom_pkgs/*.[ia]pk /tmp/nw_offline_pkgs/ 2>/dev/null + fi + + log "[BACKUP]" "[2/4] 准备下载离线套件..." + if command -v apk >/dev/null 2>&1; then + apk update >> "$LOG_FILE" 2>&1 + pkg_list=$(apk info | grep -e '^luci-app-' -e '^luci-theme-') + + # 全量备份则加入R参数下载全部底层依赖 + if [ "$backup_type" = "full" ]; then + fetch_args="-R" + else + fetch_args="" + fi + + if [ -n "$pkg_list" ]; then + for pkg in $pkg_list; do + apk fetch $fetch_args "$pkg" >> "$LOG_FILE" 2>&1 + done + fi + elif command -v opkg >/dev/null 2>&1; then + opkg update >> "$LOG_FILE" 2>&1 + pkg_list=$(opkg list-installed | awk '{print $1}' | grep -e '^luci-app-' -e '^luci-theme-') + + if [ -n "$pkg_list" ]; then + for pkg in $pkg_list; do + opkg download "$pkg" >> "$LOG_FILE" 2>&1 + done + fi + fi + + log "[BACKUP]" "[3/4] 开始在内存中打包..." + # 指纹配置文件复制一份到外层 + # 打包前生成备份核对元数据文件 + cat > "/tmp/nw_backup_meta.env" <> "$LOG_FILE" 2>&1 + + # 建立真实的备份成功状态锁 + touch /tmp/nw_backup_ok + + log "[BACKUP]" "[4/4] 打包结束,清理零碎文件。" + rm -rf /tmp/nw_sys_config.tar.gz /tmp/nw_offline_pkgs /tmp/nw_backup_running /tmp/nw_backup_meta.env + + chmod 644 "$backup_file" + ln -sf "$backup_file" "$link_file" + + sleep 300 + rm -f "$backup_file" "$link_file" + + ) >/dev/null 2>&1 & + + printf '{"url":"/luci-static/%s", "filename":"%s"}\n' "$dl_name" "$dl_name" + exit 0 + ;; + + check_backup) + if [ -f "/tmp/nw_backup_running" ]; then + # 提取最后4行日志,并进行严格的JSON符号安全转义 + log_safe=$(tail -n 4 /etc/netwiz_recovery.log 2>/dev/null | tr -d '\r' | sed 's/\\/\\\\/g; s/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//') + + echo "{ \"status\": \"running\", \"log\": \"$log_safe\" }" + elif [ -f "/tmp/nw_backup_ok" ]; then + # 依据真实状态锁宣告打包圆满成功,且将 opkg 转换为符合前端识别的 ipk 烙印 + SYS_ARCH=$(uname -m) + PKG_TYPE="ipk" + command -v apk >/dev/null 2>&1 && PKG_TYPE="apk" + DATE_STR=$(date '+%Y%m%d_%H%M') + + FNAME="NetWiz_${SYS_ARCH}_${PKG_TYPE}_${DATE_STR}.tar.gz" + echo "{ \"status\": \"done\", \"url\": \"/luci-static/nw_sys_config.tar.gz\", \"filename\": \"$FNAME\" }" + rm -f "/tmp/nw_backup_ok" + else + echo '{ "status": "error" }' + fi + exit 0 + ;; + + check_storage) + # 动态获取 /tmp 的真实可用空间 (从 KB 转换为 MB) + TMP_AVAIL_KB=$(df -k /tmp | awk 'END{print $(NF-2)}') + TMP_AVAIL_MB=$((TMP_AVAIL_KB / 1024)) + printf '{"tmp_avail_mb": %d}\n' "$TMP_AVAIL_MB" + exit 0 + ;; + + check_restore_status) + # 读取恢复任务的实时状态,供前端进度条轮询 + if [ -f /tmp/nw_restore_status.json ]; then + cat /tmp/nw_restore_status.json + else + printf '{"status":"unknown", "code":"MSG_RST_WAIT"}\n' + fi + exit 0 + ;; + + smart_restore_exec) + input=$(cat) + target_file=$(echo "$input" | jsonfilter -e '@.filepath') + + if [ -z "$target_file" ] || [ ! -s "$target_file" ]; then + target_file=$(ls -t /tmp/luci-upload-* /tmp/NetWiz_SmartGhost.tar.gz 2>/dev/null | head -n 1) + fi + + if [ -n "$target_file" ] && [ -s "$target_file" ]; then + + # 初始化状态文件 (Code 机制) + echo '{"status":"running", "code":"MSG_RST_INIT"}' > /tmp/nw_restore_status.json + + cat << 'EOF' > /tmp/nw_do_restore.sh +#!/bin/sh +# 恢复操作需要重启,将核心日志写入实体存储 +LOG_FILE="/etc/netwiz_recovery.log" +STATUS_FILE="/tmp/nw_restore_status.json" +TARGET="$1" + +# 状态更新函数,使用状态码进行国际化传递,写入日志 +update_status() { + local stat="$1" + local code="$2" + local arg1="$3" + local arg2="$4" + echo "{\"status\":\"$stat\", \"code\":\"$code\", \"arg1\":\"$arg1\", \"arg2\":\"$arg2\"}" > "$STATUS_FILE" + + logger -t Netwiz "[RESTORE] $code $arg1 $arg2" + echo "$(date '+%F %T') [RESTORE] $code $arg1 $arg2" >> "$LOG_FILE" +} + +echo "$(date '+%F %T') [RESTORE] === 开始执行极速恢复 ===" >> "$LOG_FILE" + +update_status "running" "MSG_RST_SCAN" + +UNCOMPRESSED_BYTES=$(zcat "$TARGET" | wc -c 2>/dev/null) +UNCOMPRESSED_MB=$((UNCOMPRESSED_BYTES / 1024 / 1024)) + +TMP_AVAIL_KB=$(df -k /tmp | awk 'END{print $(NF-2)}') +TMP_AVAIL_MB=$((TMP_AVAIL_KB / 1024)) + +echo "$(date '+%F %T') [RESTORE] 预估体积: ${UNCOMPRESSED_MB}MB, 可用空间: ${TMP_AVAIL_MB}MB" >> "$LOG_FILE" + +# 安全拦截:所需空间大于剩余空间时拦截 +if [ $((UNCOMPRESSED_MB + 15)) -gt "$TMP_AVAIL_MB" ]; then + update_status "error" "MSG_RST_OOM_INTERCEPT" "$UNCOMPRESSED_MB" "$TMP_AVAIL_MB" + # 拦截后清理垃圾释放内存 + rm -f "$TARGET" + exit 1 +fi + +update_status "running" "MSG_RST_SAFE" +mkdir -p /tmp/nw_ghost_tmp + +# ---------------------------------------------------- +# 防呆 3:拆包核对!先提取出元数据文件进行比对 +# ---------------------------------------------------- +echo "$(date '+%F %T') [RESTORE] 正在进行安全性预检:核对架构与安装包格式..." >> "$LOG_FILE" + +# 只单独解压这一个配置文件,不触碰系统其他文件 +# 从最外层瞬间抽离出指纹文件 +tar -xzf "$TARGET" -C /tmp/nw_ghost_tmp nw_backup_meta.env >/dev/null 2>&1 + +if [ -f "/tmp/nw_ghost_tmp/nw_backup_meta.env" ]; then + CUR_ARCH=$(uname -m) + CUR_PKG="ipk" + command -v apk >/dev/null 2>&1 && CUR_PKG="apk" + + # 仅读取外层提取的指纹数据,绝对不可访问内层路径。 + . /tmp/nw_ghost_tmp/nw_backup_meta.env + + if [ "$BACKUP_PKG" != "$CUR_PKG" ]; then + echo "$(date '+%F %T') [RESTORE] [DANGER] 🚨 严重拦截:包管理器格式冲突!(当前: ${CUR_PKG} / 备份: ${BACKUP_PKG})" >> "$LOG_FILE" + update_status "error" "MSG_RST_PKG_ERR" + rm -rf /tmp/nw_ghost_tmp "$TARGET" + exit 1 + fi + + if [ "$BACKUP_ARCH" != "$CUR_ARCH" ]; then + echo "$(date '+%F %T') [RESTORE] [DANGER] 🚨 严重拦截:底层架构不匹配!(当前路由器: ${CUR_ARCH} / 备份包来源: ${BACKUP_ARCH})" >> "$LOG_FILE" + update_status "error" "MSG_RST_ARCH_ERR" + rm -rf /tmp/nw_ghost_tmp "$TARGET" + exit 1 + fi + + echo "$(date '+%F %T') [RESTORE] ✅ 安全性预检通过!架构 (${CUR_ARCH}) 与包管理器 (${CUR_PKG}) 完全匹配。" >> "$LOG_FILE" +else + echo "$(date '+%F %T') [RESTORE] ⚠️ 警告:这是一个旧版备份包 (未包含安全预检文件),将跳过严格核对直接放行。" >> "$LOG_FILE" +fi +# ---------------------------------------------------- + +# 安全校验通过,执行真正的完整解压 +tar -xzf "$TARGET" -C /tmp/nw_ghost_tmp >> "$LOG_FILE" 2>&1 + +if [ $? -ne 0 ]; then + update_status "error" "MSG_RST_FAIL" + rm -rf /tmp/nw_ghost_tmp "$TARGET" + exit 1 +fi + +if [ ! -f "/tmp/nw_ghost_tmp/nw_sys_config.tar.gz" ] && [ ! -d "/tmp/nw_ghost_tmp/nw_offline_pkgs" ]; then + update_status "error" "MSG_RST_INVALID" + rm -rf /tmp/nw_ghost_tmp "$TARGET" + exit 1 +fi + +if [ -f "/tmp/nw_ghost_tmp/nw_sys_config.tar.gz" ]; then + update_status "running" "MSG_RST_CONF" + + # ========================================== + # 前置防御: 启动跨重启防砖看门狗 + # ========================================== + echo "$(date '+%F %T') [RESTORE] [安全防御] 正在将当前健康配置写入闪存,准备跨越重启防砖..." >> "$LOG_FILE" + mkdir -p /etc/netwiz/failsafe + cp -p /etc/config/network /etc/netwiz/failsafe/network_bak 2>/dev/null + cp -p /etc/config/dhcp /etc/netwiz/failsafe/dhcp_bak 2>/dev/null + cp -p /etc/config/wireless /etc/netwiz/failsafe/wireless_bak 2>/dev/null + touch /etc/netwiz_pending_recovery + + # 获取当前系统版本号与固件派系 (OpenWrt/ImmortalWrt/LEDE等) + CUR_VER="unknown" + CUR_FW_TYPE="OpenWrt" + if [ -f /etc/openwrt_release ]; then + . /etc/openwrt_release + CUR_VER=${VERSION:-${DISTRIB_RELEASE:-unknown}} + [ -n "$DISTRIB_ID" ] && CUR_FW_TYPE="$DISTRIB_ID" + fi + + # 版本号与固件派系【双重对比】 + if [ "$BACKUP_VER" = "$CUR_VER" ] && [ "$BACKUP_FW_TYPE" = "$CUR_FW_TYPE" ] && [ "$BACKUP_VER" != "unknown" ]; then + echo "$(date '+%F %T') [RESTORE] 检测到同派系同版本恢复 ($CUR_FW_TYPE $CUR_VER),允许无损还原历史软件源..." >> "$LOG_FILE" + tar -xzf /tmp/nw_ghost_tmp/nw_sys_config.tar.gz -C / >> "$LOG_FILE" 2>&1 + else + echo "$(date '+%F %T') [RESTORE] 检测到跨版本或跨派系 ($BACKUP_FW_TYPE $BACKUP_VER -> $CUR_FW_TYPE $CUR_VER),触发软件源智能护盾..." >> "$LOG_FILE" + + # 提取当前新系统的健康软件源 + mkdir -p /tmp/nw_repo_bak + cp -r /etc/apk/repositories* /tmp/nw_repo_bak/ 2>/dev/null + cp -p /etc/opkg/*feeds.conf /tmp/nw_repo_bak/ 2>/dev/null + + # 执行危险解压操作 + tar -xzf /tmp/nw_ghost_tmp/nw_sys_config.tar.gz -C / >> "$LOG_FILE" 2>&1 + + # 用健康的源覆盖,删除旧版遗留的污染 + cp -r /tmp/nw_repo_bak/repositories* /etc/apk/ 2>/dev/null + cp -p /tmp/nw_repo_bak/*feeds.conf /etc/opkg/ 2>/dev/null + rm -rf /tmp/nw_repo_bak + fi +fi + +if [ -d "/tmp/nw_ghost_tmp/nw_offline_pkgs" ]; then + update_status "running" "MSG_RST_PKGS" + + # --- 安装前删除除底层系统核心包,彻底杜绝依赖冲突中断 --- + rm -f /tmp/nw_ghost_tmp/nw_offline_pkgs/firewall-* \ + /tmp/nw_ghost_tmp/nw_offline_pkgs/firewall4-* \ + /tmp/nw_ghost_tmp/nw_offline_pkgs/dnsmasq-* \ + /tmp/nw_ghost_tmp/nw_offline_pkgs/wpad-* \ + /tmp/nw_ghost_tmp/nw_offline_pkgs/base-files-* \ + /tmp/nw_ghost_tmp/nw_offline_pkgs/libc-* 2>/dev/null + # --------------------------------------------------------------- + + # 全局剔除包含 netwiz / NetWiz 的安装包,防止重启 rpcd 导致脚本自杀 + rm -f /tmp/nw_ghost_tmp/nw_offline_pkgs/*[nN]et[wW]iz*.[ia]pk 2>/dev/null + rm -f /etc/netwiz/custom_pkgs/*[nN]et[wW]iz*.[ia]pk 2>/dev/null + + # ========================================== + # 防呆检测:跨架构 (ipk <-> apk) 拦截机制 + # ========================================== + + if command -v apk >/dev/null 2>&1; then + # 当前系统为 apk,检测备份包中是否包含旧版 ipk + if ls /tmp/nw_ghost_tmp/nw_offline_pkgs/*.ipk >/dev/null 2>&1 || ls /etc/netwiz/custom_pkgs/*.ipk >/dev/null 2>&1; then + logger -t Netwiz "[RESTORE] [DANGER] 触发智能防呆:当前为 apk 系统,拦截到 ipk 安装包!" + echo "$(date '+%F %T') [RESTORE] [DANGER] 🚨 触发智能防呆:当前系统使用 apk (Alpine),但备份胶囊中检测到不兼容的 ipk 安装包!" >> "$LOG_FILE" + echo "$(date '+%F %T') [RESTORE] [DANGER] 已自动拦截并跳过所有插件的还原,以防止底层架构损坏!请重新下载对应 apk 版本的插件放入保险箱。" >> "$LOG_FILE" + else + echo "$(date '+%F %T') [RESTORE] >>> 系统架构匹配 (apk),启动动态依赖解析引擎..." >> "$LOG_FILE" + + # 1. 收集所有需要安装的插件包 + ALL_PKGS="" + [ -d "/tmp/nw_ghost_tmp/nw_offline_pkgs" ] && ALL_PKGS="$ALL_PKGS $(ls /tmp/nw_ghost_tmp/nw_offline_pkgs/*.apk 2>/dev/null)" + [ -d "/etc/netwiz/custom_pkgs" ] && ALL_PKGS="$ALL_PKGS $(ls /etc/netwiz/custom_pkgs/*.apk 2>/dev/null)" + + if [ -n "$ALL_PKGS" ]; then + PENDING_PKGS="$ALL_PKGS" + LAST_FAIL_COUNT=-1 + + # 2. 只要还有待装的包,就持续循环 + while [ -n "$PENDING_PKGS" ]; do + CUR_FAIL_COUNT=$(echo "$PENDING_PKGS" | wc -w) + + # 核心防呆:如果失败数量没变,说明剩下的全是 kmod 或死锁坏包,立刻跳出循环! + if [ "$CUR_FAIL_COUNT" -eq "$LAST_FAIL_COUNT" ]; then + echo "$(date '+%F %T') [RESTORE] ⚠️ 发现无法解决的内核冲突或死锁依赖,已彻底抛弃以下坏包: " >> "$LOG_FILE" + for bad_pkg in $PENDING_PKGS; do echo "$(date '+%F %T') [RESTORE] ❌ 放弃: $(basename "$bad_pkg")" >> "$LOG_FILE"; done + break + fi + + LAST_FAIL_COUNT=$CUR_FAIL_COUNT + NEXT_PENDING="" + + # 3. 逐个尝试安装 + for pkg in $PENDING_PKGS; do + echo "$(date '+%F %T') [RESTORE] 尝试安装: $(basename "$pkg")" >> "$LOG_FILE" + if ! apk add --allow-untrusted --force-overwrite "$pkg" >> "$LOG_FILE" 2>&1; then + NEXT_PENDING="$NEXT_PENDING $pkg" + fi + done + + # 将这一轮失败的包,交给下一轮 + PENDING_PKGS="$NEXT_PENDING" + done + fi + fi + + elif command -v opkg >/dev/null 2>&1; then + # 当前系统为 opkg,检测备份包中是否包含新版 apk + if ls /tmp/nw_ghost_tmp/nw_offline_pkgs/*.apk >/dev/null 2>&1 || ls /etc/netwiz/custom_pkgs/*.apk >/dev/null 2>&1; then + logger -t Netwiz "[RESTORE] [DANGER] 触发智能防呆:当前为 opkg 系统,拦截到 apk 安装包!" + echo "$(date '+%F %T') [RESTORE] [DANGER] 🚨 触发智能防呆:当前系统使用 opkg,但备份胶囊中检测到不兼容的 apk 安装包!" >> "$LOG_FILE" + echo "$(date '+%F %T') [RESTORE] [DANGER] 已自动拦截并跳过所有插件的还原,以防止底层架构损坏!请重新下载对应 ipk 版本的插件放入保险箱。" >> "$LOG_FILE" + else + echo "$(date '+%F %T') [RESTORE] >>> 系统架构匹配 (opkg),启动动态依赖解析引擎..." >> "$LOG_FILE" + + # 1. 收集所有需要安装的插件包 + ALL_PKGS="" + [ -d "/tmp/nw_ghost_tmp/nw_offline_pkgs" ] && ALL_PKGS="$ALL_PKGS $(ls /tmp/nw_ghost_tmp/nw_offline_pkgs/*.ipk 2>/dev/null)" + [ -d "/etc/netwiz/custom_pkgs" ] && ALL_PKGS="$ALL_PKGS $(ls /etc/netwiz/custom_pkgs/*.ipk 2>/dev/null)" + + if [ -n "$ALL_PKGS" ]; then + PENDING_PKGS="$ALL_PKGS" + LAST_FAIL_COUNT=-1 + + # 2. 只要还有待装的包,就持续循环 + while [ -n "$PENDING_PKGS" ]; do + CUR_FAIL_COUNT=$(echo "$PENDING_PKGS" | wc -w) + + # 核心防呆:如果失败数量没变,说明剩下的全是 kmod 或死锁坏包,立刻跳出循环! + if [ "$CUR_FAIL_COUNT" -eq "$LAST_FAIL_COUNT" ]; then + echo "$(date '+%F %T') [RESTORE] ⚠️ 发现无法解决的内核冲突或死锁依赖,已彻底抛弃以下坏包: " >> "$LOG_FILE" + for bad_pkg in $PENDING_PKGS; do echo "$(date '+%F %T') [RESTORE] ❌ 放弃: $(basename "$bad_pkg")" >> "$LOG_FILE"; done + break + fi + + LAST_FAIL_COUNT=$CUR_FAIL_COUNT + NEXT_PENDING="" + + # 3. 逐个尝试安装 + for pkg in $PENDING_PKGS; do + echo "$(date '+%F %T') [RESTORE] 尝试安装: $(basename "$pkg")" >> "$LOG_FILE" + if ! opkg install --force-reinstall --force-overwrite "$pkg" >> "$LOG_FILE" 2>&1; then + NEXT_PENDING="$NEXT_PENDING $pkg" + fi + done + + # 将这一轮失败的包,交给下一轮 + PENDING_PKGS="$NEXT_PENDING" + done + fi + fi + fi +fi + +# 提取备份中 LAN IP +NEW_IP=$(uci -q get network.lan.ipaddr | cut -d/ -f1) + +# 把未来的新 IP 作为 arg1 传递给前端进行双向探测 +update_status "done" "MSG_RST_DONE" "$NEW_IP" +rm -rf /tmp/nw_ghost_tmp "$TARGET" + +echo "$(date '+%F %T') [RESTORE] >>> 恢复任务全部完成!系统即将重启。" >> "$LOG_FILE" + +# 等待 8 秒,确保前端 充足的时间抓取新 IP +sleep 8 +reboot +EOF + chmod +x /tmp/nw_do_restore.sh + /tmp/nw_do_restore.sh "$target_file" >/dev/null 2>&1 & + + echo '{"result":0}' + else + echo '{"error":"No data"}' + fi + exit 0 + ;; + esac + ;; + + daemon) + heal_fail_cnt=0 + loop_cnt=0 + PING_TARGET="2400:3200::1" + logger -t Netwiz "[保活守护] 引擎启动成功!进入后台常驻监控模式..." + + while true; do + ENABLE=$(uci -q get netwiz.main.watchdog_enable) + + # --- [ 动作A:每20秒执行(高频轻量保活)] --- + if [ "$ENABLE" = "1" ]; then + ping -6 -c 1 -W 2 "$PING_TARGET" >/dev/null 2>&1 + fi + + # --- [ 动作 B:每 120 秒执行 (深度探测与高级自愈) ] --- + if [ $((loop_cnt % 6)) -eq 0 ]; then + + # --- 1. 防呆:动态接管防火墙 Web 端口 --- + raw_port=$(uci -q get firewall.netwiz_web.dest_port 2>/dev/null) + [ -z "$raw_port" ] && raw_port=80 + web_port=$(echo "$raw_port" | awk '{print $1}') + + # 清理旧版本的冗余规则 + if uci -q get firewall.Netwiz_Probe_Allow >/dev/null 2>&1; then + uci -q delete firewall.Netwiz_Probe_Allow + uci -q commit firewall + /etc/init.d/firewall reload & + fi + + # 守护进程主规则 netwiz_web 进行保活修复 + current_fw_port=$(uci -q get firewall.netwiz_web.dest_port 2>/dev/null) + if [ "$current_fw_port" != "$raw_port" ]; then + uci -q set firewall.netwiz_web=rule + uci -q set firewall.netwiz_web.name='Netwiz_Web_Custom' + uci -q set firewall.netwiz_web.src='wan' + uci -q set firewall.netwiz_web.dest_port="$raw_port" + uci -q set firewall.netwiz_web.proto='tcp' + # 注:去掉 family='ipv6',让它同时兼容 IPv4 和 IPv6,一劳永逸 + uci -q set firewall.netwiz_web.target='ACCEPT' + uci -q commit firewall + /etc/init.d/firewall reload & + fi + + # --- 2. 本地 Web 进程健康度探测 --- + local_ok=0 + if curl -s -m 3 "http://127.0.0.1:${web_port}" >/dev/null 2>&1 || netstat -tuln 2>/dev/null | awk '{print $4}' | grep -E -q ":${web_port}$"; then + local_ok=1 + fi + + # --- 3. 外网出站保活探测 --- + http_ok=1 + HTTP_URL=$(uci -q get netwiz.main.watchdog_url) + + if [ "$ENABLE" = "1" ] && [ -n "$HTTP_URL" ]; then + http_ok=0 + case "$HTTP_URL" in + http://*|https://*) ;; + *) HTTP_URL="http://$HTTP_URL" ;; + esac + + if curl -6 -s -m 5 --noproxy "*" "$HTTP_URL" >/dev/null 2>&1; then + http_ok=1 + elif ping -6 -c 2 -W 3 "2400:3200::1" >/dev/null 2>&1 || ping -6 -c 2 -W 3 "2001:4860:4860::8888" >/dev/null 2>&1; then + http_ok=1 + fi + fi + + # --- 4. 核心深度探测与【精准医疗】自愈逻辑 --- + if [ $http_ok -eq 0 ] || [ $local_ok -eq 0 ]; then + if [ $heal_fail_cnt -lt 3 ]; then + heal_fail_cnt=$((heal_fail_cnt + 1)) + + if [ $local_ok -eq 0 ]; then + logger -t Netwiz "[保活守护] 🚨 本地 Web 进程崩溃(端口假死),执行第 $heal_fail_cnt 次局部自愈..." + [ -x /etc/init.d/uhttpd ] && /etc/init.d/uhttpd restart >/dev/null 2>&1 + [ -x /etc/init.d/nginx ] && /etc/init.d/nginx restart >/dev/null 2>&1 + [ -x /etc/init.d/lighttpd ] && /etc/init.d/lighttpd restart >/dev/null 2>&1 + fi + + if [ $http_ok -eq 0 ]; then + logger -t Netwiz "[保活守护] 🚨 IPv6 出站保活失败(外部断网),执行第 $heal_fail_cnt 次内核网络重置..." + /etc/init.d/firewall restart >/dev/null 2>&1 + [ -x /etc/init.d/ddns ] && /etc/init.d/ddns restart >/dev/null 2>&1 + fi + + logger -t Netwiz "[保活守护] ✅ 自愈指令下发完毕!" + elif [ $heal_fail_cnt -eq 3 ]; then + heal_fail_cnt=4 + logger -t Netwiz "[保活守护] ⚠️ 连续 3 次自愈无效,推测为 ISP 严重故障。已触发熔断,进入完全静默等待模式,下次如果探测成功,熔断器会自动复位。" + fi + else + [ $heal_fail_cnt -gt 0 ] && logger -t Netwiz "[保活守护] 🌐 网络探测成功,自愈熔断器复位。" + heal_fail_cnt=0 + fi + + # --- 5. IPv6 动态白名单追踪引擎(配合 netwiz_dev 外网直连 (白名单) 功能 ) --- + expose_rules=$(uci -q show firewall 2>/dev/null | grep "\.name='nw_expose_" | cut -d'.' -f2) + if [ -n "$expose_rules" ]; then + need_fw_reload=0 + for rule in $expose_rules; do + target_mac=$(uci -q get firewall.$rule.dest_mac) + + if [ -n "$target_mac" ]; then + # 抓取真实 IP,避开 fd 内网地址和 FAILED 失效记录 + live_ipv6=$(ip -6 neigh show dev br-lan 2>/dev/null | grep -i "$target_mac" | grep -vE "^(fd|fe80|FAILED)" | awk '{print $1}' | head -n 1) + saved_ipv6=$(uci -q get firewall.$rule.dest_ip) + + if [ -n "$live_ipv6" ] && [ "$live_ipv6" != "$saved_ipv6" ]; then + logger -t Netwiz "[保活守护] 🔄 动态追踪: 检测到设备 $target_mac 的 IPv6 已变更为 $live_ipv6,正在自动重写防火墙!" + uci set firewall.$rule.dest_ip="$live_ipv6" + need_fw_reload=1 + fi + fi + done + + if [ "$need_fw_reload" -eq 1 ]; then + uci commit firewall + /etc/init.d/firewall reload >/dev/null 2>&1 & + fi + fi + # ----------------------------------------------------------- + fi + + loop_cnt=$((loop_cnt + 1)) + sleep 20 + done + ;; +esac +exit 1 diff --git a/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz_dev b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz_dev new file mode 100644 index 000000000..d44f6526a --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/usr/libexec/rpcd/netwiz_dev @@ -0,0 +1,1320 @@ +#!/bin/sh +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 + +[ ! -f /etc/config/netwiz ] && { + touch /etc/config/netwiz + uci -q batch </dev/null | grep "^firewall\.@${sec_type}\[[0-9]*\]\.name=.*$target_name" | cut -d'.' -f2 | sed 's/[^0-9]//g' | sort -rn) + for idx in $matches; do + uci -q delete firewall.@${sec_type}[$idx] + done +} + +# --- 高级应用唤醒函數 --- +wakeup_apps() { + local proxy_apps="openclash passwall shadowsocksr bypass vssr homeproxy mihomo" + local dns_apps="adguardhome smartdns mosdns" + + # 1. 喚醒 DNS 类 + for app in $dns_apps; do + if [ -x "/etc/init.d/$app" ] && /etc/init.d/$app enabled >/dev/null 2>&1; then + logger -t Netwiz "⚡ Re-linking DNS app: $app" + /etc/init.d/$app restart >/dev/null 2>&1 + fi + done + + # 2. 喚醒 代理/加速 + for app in $proxy_apps; do + if [ -x "/etc/init.d/$app" ] && /etc/init.d/$app enabled >/dev/null 2>&1; then + logger -t Netwiz "🚀 Re-linking Proxy app: $app" + /etc/init.d/$app restart >/dev/null 2>&1 + fi + done +} +# ------------------------------ + +# --- 提取设备 DUID 函數 --- +extract_device_duid() { + local target_mac="$1" + local target_clean=$(echo "$target_mac" | tr -d ':' | tr 'A-Z' 'a-z') + local out_duid="" + + # 1. DHCPv6 底层数据中没有 MAC, 通过 IPv4 租约作为桥梁,用 MAC 换取 Hostname + local host_name=$(cat /tmp/dhcp.leases 2>/dev/null | grep -i "$target_mac" | awk '{print $4}' | grep -v '^\*$' | head -n 1) + local host_name_lower=$(echo "$host_name" | tr 'A-Z' 'a-z') + + local ubus1=$(ubus call dhcp ipv6leases 2>/dev/null) + local ubus2=$(ubus call odhcpd ipv6leases 2>/dev/null) + local raw_ubus="$ubus1 $ubus2" + + if [ -n "$raw_ubus" ]; then + # 优先用 Hostname 桥接寻找 DUID (100% 精准锁定) + if [ -n "$host_name_lower" ]; then + out_duid=$(echo "$raw_ubus" | awk -v RS="}" -v hname="$host_name_lower" ' + tolower($0) ~ "\"" hname "\"" { + idx = index(tolower($0), "\"duid\""); + if (idx > 0) { + s = substr($0, idx); + split(s, a, "\""); + print a[4]; + exit; + } + }' | tr 'A-Z' 'a-z' | head -n 1) + fi + + # 如果 Hostname 没找到,尝试在 DUID 中直接寻找包含 MAC 字符串的情况 + if [ -z "$out_duid" ]; then + out_duid=$(echo "$raw_ubus" | awk -v RS="}" -v mac="$target_clean" ' + tolower($0) ~ mac { + idx = index(tolower($0), "\"duid\""); + if (idx > 0) { + s = substr($0, idx); + split(s, a, "\""); + print a[4]; + exit; + } + }' | tr 'A-Z' 'a-z' | head -n 1) + fi + fi + + # 2. odhcpd 底层状态文件 (同样用 Hostname 桥接) + if [ -z "$out_duid" ] && ls /var/lib/odhcpd/*.state >/dev/null 2>&1; then + if [ -n "$host_name" ]; then + out_duid=$(grep -i "$host_name" /var/lib/odhcpd/*.state 2>/dev/null | grep -oEi 'duid=[0-9a-fA-F]+' | awk -F'=' '{print $2}' | head -n 1 | tr 'A-Z' 'a-z') + fi + [ -z "$out_duid" ] && out_duid=$(grep -i "$target_clean" /var/lib/odhcpd/*.state 2>/dev/null | grep -oEi 'duid=[0-9a-fA-F]+' | awk -F'=' '{print $2}' | head -n 1 | tr 'A-Z' 'a-z') + fi + + echo "$out_duid" +} +# ------------------------------ +# 智能 IPv6 保活状态评估引擎 (中央决策) +evaluate_keepalive() { + local need_keepalive=0 + + # 1. 检查是否开启了全局外网访问后台 + local web_rule=$(uci -q get firewall.netwiz_web.target 2>/dev/null) + if [ "$web_rule" = "ACCEPT" ]; then + need_keepalive=1 + fi + + # 2. 检查是否有任何设备开启了外网直连 (白名单) + if [ "$need_keepalive" -eq 0 ]; then + local expose_count=$(uci -q show firewall 2>/dev/null | grep -c "\.name='nw_expose_") + if [ "$expose_count" -gt 0 ]; then + need_keepalive=1 + fi + fi + + # 3. 动态管理 procd 守护进程 + if [ "$need_keepalive" -eq 1 ]; then + if [ ! -f /etc/init.d/netwiz-keepalive ]; then + cat << 'EOF' > /etc/init.d/netwiz-keepalive +#!/bin/sh /etc/rc.common +START=99 +USE_PROCD=1 + +start_service() { + procd_open_instance + procd_set_param command /bin/sh -c "while true; do ping -6 -c 1 -W 1 2400:3200::1 >/dev/null 2>&1; sleep 20; done" + procd_set_param respawn + procd_close_instance +} +EOF + chmod +x /etc/init.d/netwiz-keepalive + /etc/init.d/netwiz-keepalive enable + /etc/init.d/netwiz-keepalive start + logger -t Netwiz "[保活引擎] 智能联动: 已启动 IPv6 (20秒超高频) 保活服务" + fi + else + if [ -f /etc/init.d/netwiz-keepalive ]; then + /etc/init.d/netwiz-keepalive stop >/dev/null 2>&1 + /etc/init.d/netwiz-keepalive disable >/dev/null 2>&1 + rm -f /etc/init.d/netwiz-keepalive + logger -t Netwiz "[保活引擎] 智能联动: 无任何外网穿透需求,已彻底销毁保活服务" + fi + fi +} +# ------------------------------ + +case "$1" in + list) + echo '{ "get_list": { "show_conns": "boolean" }, "bind": { "mac": "string", "ip": "string", "name": "string", "dept": "string", "no_reload": "boolean" }, "unbind": { "mac": "string", "no_reload": "boolean" }, "wake_device": { "mac": "string" }, "apply_dhcp": {}, "get_depts": {}, "save_depts": { "data": "string" }, "get_smart_ranges": {}, "save_smart_ranges": { "data": "string" }, "v6_keep_alive": { "mac": "string", "name": "string" }, "reset_all": {}, "analyze_conns": { "ip": "string" }, "export_config": {}, "import_config": { "data": "string", "file": "string" }, "list_backups": {}, "auto_backup": {}, "fw_set": { "mac": "string", "ip": "string", "blk_en": "boolean", "iso_en": "boolean", "dmz_en": "boolean", "expose_en": "boolean", "expose_proto": "string" } }' + exit 0 + ;; + call) + case "$2" in + get_list) + input=$(cat) + show_conns=$(echo "$input" | jsonfilter -e '@.show_conns' || echo "false") + do_rescan=$(echo "$input" | jsonfilter -e '@.do_rescan' || echo "false") + + # --- 主动唤醒扫描 --- + if [ "$do_rescan" = "true" ] || [ "$do_rescan" = "1" ]; then + # 动态取得 LAN 网段 + lan_prefix=$(uci -q get network.lan.ipaddr | cut -d. -f1-3) + if [ -n "$lan_prefix" ]; then + # Ping 封包,取MAC + for i in $(seq 1 254); do + (ping -c 1 -W 1 -q "$lan_prefix.$i" >/dev/null 2>&1) & + done + + # --- 向多播地址和 APIPA 广播,169.254 自闭设备现身 --- + (ping -c 1 -W 1 -q 224.0.0.1 >/dev/null 2>&1) & + (ping -c 1 -W 1 -I br-lan 169.254.255.255 >/dev/null 2>&1) & + # ----------------------------------------------------------- + + # 等待 1 秒写入 ARP 表 + sleep 1 + fi + fi + # ---------------------------------------- + + rm -f /tmp/nw_conns_cache.txt + touch /tmp/nw_conns_cache.txt + if [ "$show_conns" = "true" ] || [ "$show_conns" = "1" ]; then + if [ -f /proc/net/nf_conntrack ]; then + awk '{src="";proto=$3;dport=0;s_done=0;d_done=0; for(i=1;i<=NF;i++){if(!s_done && $i~/^src=/){split($i,a,"=");src=a[2];s_done=1;} if(!d_done && $i~/^dport=/){split($i,a,"=");dport=a[2]+0;d_done=1;} if(s_done && d_done) break;} if(src!=""){t[src]++;if(dport==80||dport==443)w[src]++;else if(dport==53||dport==5353||dport==123)d[src]++;else if(proto=="udp")u[src]++;else if(dport>10000)p[src]++;else o[src]++;}} END{for(i in t) print i"="t[i]","w[i]+0","d[i]+0","u[i]+0","p[i]+0","o[i]+0}' /proc/net/nf_conntrack 2>/dev/null > /tmp/nw_conns_cache.txt + elif command -v conntrack >/dev/null 2>&1; then + conntrack -L 2>/dev/null | awk '{src="";proto=$3;dport=0;s_done=0;d_done=0; for(i=1;i<=NF;i++){if(!s_done && $i~/^src=/){split($i,a,"=");src=a[2];s_done=1;} if(!d_done && $i~/^dport=/){split($i,a,"=");dport=a[2]+0;d_done=1;} if(s_done && d_done) break;} if(src!=""){t[src]++;if(dport==80||dport==443)w[src]++;else if(dport==53||dport==5353||dport==123)d[src]++;else if(proto=="udp")u[src]++;else if(dport>10000)p[src]++;else o[src]++;}} END{for(i in t) print i"="t[i]","w[i]+0","d[i]+0","u[i]+0","p[i]+0","o[i]+0}' > /tmp/nw_conns_cache.txt + fi + fi + + fw_str=$(uci show firewall 2>/dev/null || true) + ipv6_neigh=$(ip -6 neigh show 2>/dev/null || true) + + # --- IPv6 存活探测过滤 --- + raw_v6_data=$(ip -6 neigh show 2>/dev/null | grep -v "FAILED") + v6_candidates=$(echo "$raw_v6_data" | awk '{print $1}' | grep ":" | sort -u) + + # 1. 删除 邻居表公网 IPv6 + for v6_addr in $v6_candidates; do + if [[ "$v6_addr" != fe80* ]]; then + # 找到网卡 + dev_name=$(echo "$raw_v6_data" | grep -w "^$v6_addr" | awk '{print $3}' | head -n 1) + [ -n "$dev_name" ] && ip -6 neigh del "$v6_addr" dev "$dev_name" 2>/dev/null + fi + done + + # 2. Ping + for v6_addr in $v6_candidates; do + if [[ "$v6_addr" != fe80* ]]; then + ping6 -c 1 -W 1 "$v6_addr" >/dev/null 2>&1 & + fi + done + wait + + # 3. 延时 + sleep 0.5 + + # 4. 重新抓取邻居表 + ipv6_neigh=$(ip -6 neigh show 2>/dev/null | grep -v -E "FAILED|INCOMPLETE" || true) + # -------------------------- + wifi_macs=$(iwinfo 2>/dev/null | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}' | tr 'A-Z' 'a-z' || true) + arp_cache=$(ip -4 neigh show 2>/dev/null || cat /proc/net/arp 2>/dev/null | tail -n +2 || true) + leases=$(cat /tmp/dhcp.leases /var/dhcp.leases /tmp/run/dnsmasq/dhcp.leases /var/lib/misc/dnsmasq.leases 2>/dev/null || true) + hosts_str=$(cat /tmp/hosts/* /etc/hosts 2>/dev/null || true) + uci_hosts=$(uci show dhcp 2>/dev/null | grep "\.@host\[" || true) + ubus_leases=$(ubus call dhcp ipv4leases 2>/dev/null || ubus call odhcpd ipv4leases 2>/dev/null || true) + gw_ip=$(ip -4 route show default 2>/dev/null | awk '/default/ {print $3}' | head -n1 | tr -d ' \r\n') + lan_ip=$(ip addr show br-lan 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1 | tr -d ' \r\n') + [ -z "$lan_ip" ] && lan_ip=$(uci -q get network.lan.ipaddr | cut -d/ -f1 | tr -d ' \r\n') + lan_mac=$(ip link show br-lan 2>/dev/null | awk '/link\/ether/ {print $2}' | head -n1 | tr 'A-Z' 'a-z') + [ -z "$lan_mac" ] && lan_mac=$(ip link | awk '/link\/ether/ {print $2}' | head -n1 | tr 'A-Z' 'a-z') + local_ips=$(ip -4 addr show 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | tr '\n' ' ') + visitor_ips=$(netstat -nt 2>/dev/null | awk '$4 ~ /:(80|443)$/ && $6 ~ /ESTABLISHED/ {split($5, a, ":"); if(a[1]!="") print a[1]}' | sort -u | tr '\n' ' ') + curr_time=$(date +%s) + + # --- IPv6 与 LAN NAT 冲突检测 --- + lan_zone_idx=$(uci show firewall 2>/dev/null | grep -E "\.name='?lan'?" | cut -d'.' -f2 | head -n 1) + lan_nat_conflict="false" + if [ -n "$lan_zone_idx" ]; then + lan_masq=$(uci -q get firewall.$lan_zone_idx.masq) + if [ "$lan_masq" = "1" ]; then + lan_nat_conflict="true" + fi + fi + + # 后端查 IPv6 有没有开 + ipv6_enabled="false" + v6_dhcp=$(uci -q get dhcp.lan.dhcpv6) + if [ "$v6_dhcp" = "server" ] || [ "$v6_dhcp" = "relay" ]; then + ipv6_enabled="true" + fi + # ------------------------------------------ + + # 提取本机活跃的全局 IPv6 地址 (排除 fd/fe80 等内网或链路地址) + lan_ip6=$(ip -o -6 addr show br-lan scope global 2>/dev/null | grep -v "deprecated" | grep -v " fd" | awk '{print $4}' | cut -d/ -f1 | tr '\n' ' ' | sed 's/ $//') + + # 注意:下面这一行多加了一个传参 -v lan_ip6="$lan_ip6" + json_data=$(awk -v current_time="$curr_time" -v lan_nat_conflict="$lan_nat_conflict" -v ipv6_enabled="$ipv6_enabled" -v wifi_str="$wifi_macs" -v arp_str="$arp_cache" -v lease_str="$leases" -v hosts_str="$hosts_str" -v ubus_str="$ubus_leases" -v uci_str="$uci_hosts" -v gw_ip="$gw_ip" -v lan_ip="$lan_ip" -v lan_ip6="$lan_ip6" -v lan_mac="$lan_mac" -v local_ips_str="$local_ips" -v visitor_ips_str="$visitor_ips" -v fw_str_input="$fw_str" -v ipv6_str="$ipv6_neigh" ' + BEGIN { + now_ts = current_time; + exp_ts = 2592000; # 30天过期时间 + while ("uci -q show netwiz 2>/dev/null" | getline cfg_line) { + if (index(cfg_line, "netwiz.seen_") == 1 || index(cfg_line, "netwiz.v6pc_") == 1) { + split(cfg_line, eq_parts, "="); + split(eq_parts[1], dot_parts, "."); + split(dot_parts[2], _parts, "_"); + _mac_clean = tolower(_parts[2]); + if (length(_mac_clean) == 12) { + _m = substr(_mac_clean,1,2) ":" substr(_mac_clean,3,2) ":" substr(_mac_clean,5,2) ":" substr(_mac_clean,7,2) ":" substr(_mac_clean,9,2) ":" substr(_mac_clean,11,2); + in_config_map[_m] = 1; + } + } + if (index(cfg_line, "netwiz.v6pc_") == 1) { + split(cfg_line, eq_parts, "="); + if (eq_parts[2] != "") { + key_part = eq_parts[1]; val_part = eq_parts[2]; gsub(/[\047"\r\n]/, "", val_part); + split(key_part, dot_parts, "."); sec_name = dot_parts[2]; prop_name = dot_parts[3]; + if (prop_name == "mac") uci_pc_macs[sec_name] = tolower(val_part); + else if (prop_name == "ts") uci_pc_ts[sec_name] = val_part + 0; + else if (prop_name == "hostname") uci_pc_name[sec_name] = val_part; + } + } + } + # 只有在30天以内的,才给它打上“PC”的身份标签 + for (sec in uci_pc_macs) { + if (now_ts - uci_pc_ts[sec] <= exp_ts) is_pc_v6_map[uci_pc_macs[sec]] = 1; + } + close("uci -q show netwiz 2>/dev/null"); + + gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, "", gw_ip); + gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, "", lan_ip); + gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, "", lan_mac); + + split(local_ips_str, lip_arr, " "); + for(i in lip_arr) if(lip_arr[i] != "" && lip_arr[i] != "127.0.0.1") router_ips[lip_arr[i]] = 1; + + split(visitor_ips_str, vip_arr, " "); + for(i in vip_arr) if(vip_arr[i] != "" && vip_arr[i] != "127.0.0.1") visitor_ips[vip_arr[i]] = 1; + + if (lan_mac != "" && lan_ip != "") { + all_macs[lan_mac] = 1; + arp_ip[lan_mac] = lan_ip; arp_online[lan_mac] = 1; ubus_name[lan_mac] = "OpenWrt (Local)"; + # 本机的 IPv6 写入字典,弥补邻居表中可能缺失的情况 + if (lan_ip6 != "") ipv6_map[lan_mac] = lan_ip6; + } + + split(wifi_str, warr, "\n"); + for(i in warr) if(warr[i]!="") wifi_online[warr[i]]=1; + + split(ipv6_str, v6arr, "\n"); + for(i in v6arr) { + n_f = split(v6arr[i], f, " "); + if(f[1]!="" && f[1]!~/dev/) { + v6_ip = f[1]; mac = ""; state_str = v6arr[i]; + for(j=1; j<=n_f; j++) { + if(f[j]=="lladdr" && n_f>=j+1) mac=tolower(f[j+1]); + else if(f[j] ~ /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) mac=tolower(f[j]); + } + if(mac!="" && mac!="00:00:00:00:00:00" && state_str!~/FAILED/) { + if(ipv6_map[mac]=="") ipv6_map[mac] = v6_ip; + else if(index(ipv6_map[mac], v6_ip) == 0) ipv6_map[mac] = ipv6_map[mac] " " v6_ip; + all_macs[mac]=1; + if(state_str ~ /REACHABLE|DELAY|PROBE|router/) arp_online[mac]=1; + } + } + } + + split(arp_str, aarr, "\n"); + for(i in aarr) { + n_f = split(aarr[i], f, " "); + if(f[1]!="" && f[1]!~/IP/) { + mac=""; state_str=aarr[i]; + for(j=1; j<=n_f; j++) { + if(f[j]=="lladdr" && n_f>=j+1) mac=tolower(f[j+1]); + else if(f[j] ~ /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) mac=tolower(f[j]); + } + if(mac!="" && mac!="00:00:00:00:00:00") { + if (f[1] ~ /^169\.254\./) { + if (arp_ip[mac] == "") arp_ip[mac] = f[1]; + } else { + arp_ip[mac] = f[1]; + } + if(state_str ~ /REACHABLE|DELAY|PROBE|0x2/) arp_online[mac]=1; + all_macs[mac]=1; + } + } + } + + split(ubus_str, ulines, "\n"); + cur_mac = ""; cur_ip = ""; cur_name = ""; cur_valid = -1; + for(i=1; i<=length(ulines); i++) { + line = ulines[i]; + if (line ~ /"mac":/) { split(line, p, "\""); cur_mac = tolower(p[4]); } + else if (line ~ /"ip":/) { split(line, p, "\""); cur_ip = p[4]; } + else if (line ~ /"hostname":/) { split(line, p, "\""); cur_name = p[4]; } + else if (line ~ /"valid":/ || line ~ /"expires":/ || line ~ /"leasetime":/) { + split(line, p, ":"); val=p[2]; gsub(/[^0-9]/, "", val); val = val + 0; + if (val > 1000000000) { cur_valid = (val > current_time) ? val - current_time : 0; } + else { cur_valid = val; } + } + else if (line ~ /}/) { + if (cur_mac != "") { + if (cur_ip != "") leases_ip[cur_mac] = cur_ip; + if (cur_name != "" && cur_name != "*" && cur_name != "?") ubus_name[cur_mac] = cur_name; + if (cur_valid > 0) { + h = int(cur_valid / 3600); m = int((cur_valid % 3600) / 60); s = cur_valid % 60; + leases_time[cur_mac] = sprintf("%dh %dm %ds", h, m, s); + } else if (cur_valid == 0) { leases_time[cur_mac] = "Expired"; } + all_macs[cur_mac] = 1; + } + cur_mac = ""; cur_ip = ""; cur_name = ""; cur_valid = -1; + } + } + + split(lease_str, larr, "\n"); + for(i in larr) { + n_f = split(larr[i], f, " "); + if(n_f >= 4 && f[2]!="") { + # 忽略 dhcp.leases 中的 IPv6 租期,防止過期的死 IP 出现 + if (f[3] ~ /:/) { + continue; + } + + # 原本的 IPv4 解析邏輯保持完全不變 + mac=tolower(f[2]); + if(f[3]!="") leases_ip[mac]=f[3]; + name = f[4]; + for(k=5; k<=n_f-1; k++) { name = name " " f[k]; } + if(name!="" && name!="*" && name!="?") leases_name[mac]=name; + all_macs[mac]=1; + exp_time = f[1] + 0; + if (leases_time[mac] == "") { + if (exp_time > 1000000000) { + if (exp_time > current_time) { + left = exp_time - current_time; + h = int(left / 3600); m = int((left % 3600) / 60); s = left % 60; + leases_time[mac] = sprintf("%dh %dm %ds", h, m, s); + } else leases_time[mac] = "Expired"; + } else if (exp_time > 0 && exp_time <= 1000000000) { + left = exp_time; + h = int(left / 3600); m = int((left % 3600) / 60); s = left % 60; + leases_time[mac] = sprintf("%dh %dm %ds", h, m, s); + } else if (exp_time == 0) leases_time[mac] = "Infinite"; + } + } + } + + split(hosts_str, harr, "\n"); + for(i in harr) { + split(harr[i], f, " "); + if(f[1]!="" && f[2]!="") { + if (f[1] ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { + name = f[2]; sub(/\.lan$/, "", name); + if (name != "localhost" && name != "*" && name != "?") ip_to_name[f[1]] = name; + } + } + } + + split(uci_str, uarr, "\n"); + for(i in uarr) { + idx=index(uarr[i],"="); + if(idx>0) { + key=substr(uarr[i],1,idx-1); val=substr(uarr[i],idx+1); gsub(/^\047|\047$/, "", val); + split(key, kp, "."); + if(kp[1]=="dhcp" && kp[2]!="" && kp[3]!="") { + h_idx=kp[2]; prop=kp[3]; + if(prop=="mac"){ + gsub(/["\r\n]/, "", val); split(tolower(val), mparts, " "); + for(m in mparts) uci_mac_list[h_idx] = (uci_mac_list[h_idx]=="" ? "" : uci_mac_list[h_idx] " ") mparts[m]; + } + else if(prop=="ip") uci_ip_map[h_idx]=val; + else if(prop=="name") uci_name_map[h_idx]=val; + else if(prop=="netwiz_dept") uci_dept_map[h_idx]=val; + } + } + } + + for (h_idx in uci_mac_list) { + split(uci_mac_list[h_idx], mparts, " "); + for (m in mparts) { + m_mac = mparts[m]; all_macs[m_mac] = 1; is_static_mac[m_mac] = 1; + if (uci_ip_map[h_idx] != "") static_ip[m_mac] = uci_ip_map[h_idx]; + if (uci_name_map[h_idx] != "") uci_name[m_mac] = uci_name_map[h_idx]; + if (uci_dept_map[h_idx] != "") static_dept[m_mac] = uci_dept_map[h_idx]; + } + } + + split(fw_str_input, ffw_arr, "\n"); + for(i in ffw_arr) { + line = ffw_arr[i]; + if (index(line, "firewall.@rule[") == 1) { + eq_idx = index(line, "="); + if (eq_idx > 0) { + prop_full = substr(line, 1, eq_idx - 1); + val = substr(line, eq_idx + 1); gsub(/[\047"\r\n]/, "", val); + split(prop_full, kp, "."); + if (kp[1] == "firewall" && kp[2] != "" && kp[3] != "") { + obj = kp[2]; prop = kp[3]; + if (prop == "name" && val ~ /^nw_/) fw_name[obj] = val; + else if (prop == "family") fw_family[obj] = val; # 新增:讀取協議狀態 + } + } + } + if (index(line, "firewall.@redirect[") == 1) { + eq_idx = index(line, "="); + if (eq_idx > 0) { + prop_full = substr(line, 1, eq_idx - 1); val = substr(line, eq_idx + 1); gsub(/[\047"\r\n]/, "", val); + split(prop_full, kp, "."); + if (kp[1] == "firewall" && kp[2] != "" && kp[3] == "name" && val ~ /^nw_dmz_/) fw_name_dmz[kp[2]] = val; + } + } + } + + for (obj in fw_name_dmz) { + name = fw_name_dmz[obj]; + split(name, parts, "_"); mac_clean = tolower(parts[3]); + if (length(mac_clean) == 12) { mac = substr(mac_clean,1,2) ":" substr(mac_clean,3,2) ":" substr(mac_clean,5,2) ":" substr(mac_clean,7,2) ":" substr(mac_clean,9,2) ":" substr(mac_clean,11,2); + fw_dmz[mac] = 1; } + } + + for (obj in fw_name) { + name = fw_name[obj]; + split(name, parts, "_"); type = parts[2]; mac_clean = tolower(parts[3]); + if (length(mac_clean) == 12) { + mac = substr(mac_clean,1,2) ":" substr(mac_clean,3,2) ":" substr(mac_clean,5,2) ":" substr(mac_clean,7,2) ":" substr(mac_clean,9,2) ":" substr(mac_clean,11,2); + if (type == "blk") fw_blk[mac] = 1; else if (type == "iso") fw_iso[mac] = 1; + else if (type == "expose" || type == "v6expose") { + fw_expose[mac] = 1; + fw_expose_proto[mac] = (fw_family[obj] != "") ? fw_family[obj] : "ipv6"; + } + } + } + + while ((getline line < "/tmp/nw_conns_cache.txt") > 0) { + split(line, p, "="); + if(p[1]!="") conn_count[p[1]] = p[2]; + } + close("/tmp/nw_conns_cache.txt"); + # 输出的 JSON 头部带上 local_ip 和 local_ip6,前端直接可用 + printf "{\"lan_nat_conflict\": %s, \"ipv6_enabled\": %s, \"local_ip\": \"%s\", \"local_ip6\": \"%s\", \"devices\": [", lan_nat_conflict, ipv6_enabled, lan_ip, lan_ip6; + first=1; + for(mac in all_macs) { + is_online = (arp_online[mac]==1 || wifi_online[mac]==1) ? 1 : 0; + ip = arp_ip[mac]; + if(ip == "" || ip ~ /^169\.254\./) { + if(leases_ip[mac] != "") ip = leases_ip[mac]; + else if(static_ip[mac] != "") ip = static_ip[mac]; + } + gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, "", ip); + if(ip=="" && is_online==0) continue; + if(ip=="") ip = "Unknown IP"; + + name = uci_name[mac]; + if(name=="") name = ip_to_name[ip]; + if(name=="") name = ubus_name[mac]; + if(name=="") name = leases_name[mac]; + if((name=="" || name=="Unknown") && uci_pc_name["v6pc_" toupper(mac)] != "") { + name = uci_pc_name["v6pc_" toupper(mac)]; + } + + if(name=="") name = "Unknown"; + + l_time = leases_time[mac]; + if(l_time=="") { + if (is_static_mac[mac]==1) l_time = "Static"; + else if (arp_online[mac]==1) l_time = "Device Addressed IP"; + else l_time = "-"; + } + + v6_val = ipv6_map[mac]; + gsub(/["\\]/, "", mac); gsub(/\047/, "", mac); gsub(/[\r\n\t]/, "", mac); + gsub(/["\\]/, "", ip); gsub(/\047/, "", ip); gsub(/[\r\n\t]/, "", ip); + gsub(/["\\]/, "", name); gsub(/\047/, "", name); gsub(/[\r\n\t]/, "", name); + gsub(/["\\]/, "", l_time); gsub(/\047/, "", l_time); gsub(/[\r\n\t]/, "", l_time); + gsub(/["\\]/, "", v6_val); gsub(/\047/, "", v6_val); gsub(/[\r\n\t]/, "", v6_val); + + is_static = (is_static_mac[mac]==1) ? "true" : "false"; + is_gw = (ip != "" && ip == gw_ip) ? "true" : "false"; + is_local = (ip != "" && router_ips[ip] == 1) ? "true" : "false"; + is_visitor = (ip != "" && visitor_ips[ip] == 1) ? "true" : "false"; + b_ip = static_ip[mac]; b_dept = static_dept[mac]; + gsub(/["\\]/, "", b_ip); gsub(/\047/, "", b_ip); gsub(/[\r\n\t]/, "", b_ip); + gsub(/["\\]/, "", b_dept); gsub(/\047/, "", b_dept); gsub(/[\r\n\t]/, "", b_dept); + + # 圆饼图 + c_str = (conn_count[ip] != "") ? conn_count[ip] : "0,0,0,0,0,0"; + split(c_str, c_arr, ","); + c_total = c_arr[1]+0; c_web = c_arr[2]+0; c_dns = c_arr[3]+0; c_udp = c_arr[4]+0; c_p2p = c_arr[5]+0; c_oth = c_arr[6]+0; + + f_blk = (fw_blk[mac]==1) ? "true" : "false"; + f_iso = (fw_iso[mac]==1) ? "true" : "false"; + f_dmz = (fw_dmz[mac]==1) ? "true" : "false"; + f_expose = (fw_expose[mac]==1) ? "true" : "false"; + p_expose = (fw_expose_proto[mac] != "") ? fw_expose_proto[mac] : "ipv6"; + + if(!first) printf ", "; + + is_pc_flag = (is_pc_v6_map[mac] == 1) ? "true" : "false"; + is_new_unknown = "false"; + if (is_online == 1 && is_static_mac[mac] != 1 && is_local == "false" && is_gw == "false") { + is_new_unknown = "true"; + } + + in_config_flag = (in_config_map[mac] == 1) ? "true" : "false"; + printf "{\"mac\": \"%s\", \"ip\": \"%s\", \"bound_ip\": \"%s\", \"dept\": \"%s\", \"name\": \"%s\", \"online\": %s, \"is_static\": %s, \"is_gw\": %s, \"is_local\": %s, \"is_visitor\": %s, \"lease\": \"%s\", \"conn_count\": %d, \"conn_web\": %d, \"conn_dns\": %d, \"conn_udp\": %d, \"conn_p2p\": %d, \"conn_oth\": %d, \"fw_block\": %s, \"fw_isolate\": %s, \"fw_dmz\": %s, \"fw_expose\": %s, \"fw_expose_proto\": \"%s\", \"ipv6\": \"%s\", \"is_pc_v6\": %s, \"is_new_unknown\": %s, \"in_config\": %s}", mac, ip, b_ip, b_dept, name, (is_online?"true":"false"), is_static, is_gw, is_local, is_visitor, l_time, c_total, c_web, c_dns, c_udp, c_p2p, c_oth, f_blk, f_iso, f_dmz, f_expose, p_expose, v6_val, is_pc_flag, is_new_unknown, in_config_flag; + first=0; + } + printf "]}"; + }') + + # --- 靜默清理与更新 --- + ( + NOW=$(date +%s) + EXP_TIME=2592000 # 30 天超时清理 + UPDATE_THRESHOLD=604800 # 配置文件netwiz_dev的写入阈值:7天 + + NEEDS_COMMIT=0 + + # 1. 活跃时间戳更新 (合并 ARP 与 IPv6 邻居) + ip neigh show 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) print tolower($i)}' | sort -u | while read mac; do + [ -z "$mac" ] || [ "$mac" = "00:00:00:00:00:00" ] && continue + MAC_SAFE=$(echo "$mac" | tr -d ':' | tr 'a-z' 'A-Z') + + # 判断是否为 PC 特权设备 + if uci -q get netwiz.v6pc_${MAC_SAFE} >/dev/null; then + OLD_TS=$(uci -q get netwiz.v6pc_${MAC_SAFE}.ts) + # 只有没记录过,或距离上次记录超过 1 天,才允许更新 + if [ -z "$OLD_TS" ] || [ $((NOW - OLD_TS)) -gt $UPDATE_THRESHOLD ]; then + uci -q set netwiz.v6pc_${MAC_SAFE}.ts="$NOW" + NEEDS_COMMIT=1 + fi + else + # 普通设备 + OLD_TS=$(uci -q get netwiz.seen_${MAC_SAFE}.ts) + if [ -z "$OLD_TS" ] || [ $((NOW - OLD_TS)) -gt $UPDATE_THRESHOLD ]; then + uci -q set netwiz.seen_${MAC_SAFE}="netwiz_dev" + uci -q set netwiz.seen_${MAC_SAFE}.ts="$NOW" + NEEDS_COMMIT=1 + fi + fi + done + + # 2. 30 天没上线清理 (同时扫描 seen_ 和 v6pc_) + uci -q show netwiz | grep -E '=netwiz_dev|=v6_pc' | while read line; do + sec=$(echo "$line" | cut -d'=' -f1 | cut -d'.' -f2) + mac_safe=$(echo "$sec" | cut -d'_' -f2) + mac_lower=$(echo "$mac_safe" | tr 'A-Z' 'a-z') + + ts=$(uci -q get netwiz."$sec".ts) + + if [ -n "$ts" ] && [ $((NOW - ts)) -gt $EXP_TIME ]; then + # 殭屍设备,清理防火墙和 DMZ 规则 + delete_fw_rule "nw_blk_${mac_lower}" "rule" + delete_fw_rule "nw_iso_${mac_lower}" "rule" + delete_fw_rule "nw_dmz_${mac_lower}" "redirect" + delete_fw_rule "nw_expose_${mac_lower}" "rule" + + # 删除过期记录 + uci -q delete netwiz."$sec" + NEEDS_COMMIT=1 + logger -t Netwiz "🧹 Garbage Collection: Cleared old rules for $mac_safe" + fi + done + + # 设备更新了时间戳,或者有垃圾被清理时,写入与重启 + if [ "$NEEDS_COMMIT" -eq 1 ]; then + uci commit firewall + uci commit netwiz + evaluate_keepalive # 触发保活引擎 + /etc/init.d/firewall reload >/dev/null 2>&1 + fi + ) >/dev/null 2>&1 & + # ---------------------------------------------------- + + echo "$json_data" + exit 0 + ;; + fw_set) + input=$(cat) + mac=$(echo "$input" | jsonfilter -e '@.mac' | tr 'A-Z' 'a-z') + mac_clean=$(echo "$mac" | tr -d ':') + ip=$(echo "$input" | jsonfilter -e '@.ip') + blk_en=$(echo "$input" | jsonfilter -e '@.blk_en') + iso_en=$(echo "$input" | jsonfilter -e '@.iso_en') + dmz_en=$(echo "$input" | jsonfilter -e '@.dmz_en') + expose_en=$(echo "$input" | jsonfilter -e '@.expose_en') + expose_proto=$(echo "$input" | jsonfilter -e '@.expose_proto') + [ -z "$expose_proto" ] && expose_proto="ipv6" + + [ -z "$mac" ] && exit 0 + + delete_fw_rule "nw_blk_${mac_clean}" "rule" + if [ "$blk_en" = "true" ] || [ "$blk_en" = "1" ]; then + # 阻断外网转发 + uci add firewall rule >/dev/null + uci set firewall.@rule[-1].name="nw_blk_${mac_clean}" + uci set firewall.@rule[-1].src="lan" + uci set firewall.@rule[-1].dest="wan" + uci set firewall.@rule[-1].src_mac="$mac" + uci set firewall.@rule[-1].family="any" + uci set firewall.@rule[-1].target="DROP" + uci set firewall.@rule[-1].enabled="1" + + # 阻断透明代理劫持和路由器 DNS + uci add firewall rule >/dev/null + uci set firewall.@rule[-1].name="nw_blk_${mac_clean}_in" + uci set firewall.@rule[-1].src="lan" + uci set firewall.@rule[-1].src_mac="$mac" + uci set firewall.@rule[-1].family="any" + uci set firewall.@rule[-1].target="DROP" + uci set firewall.@rule[-1].enabled="1" + fi + + delete_fw_rule "nw_iso_${mac_clean}" "rule" + if [ "$iso_en" = "true" ] || [ "$iso_en" = "1" ]; then + uci add firewall rule >/dev/null + uci set firewall.@rule[-1].name="nw_iso_${mac_clean}" + uci set firewall.@rule[-1].src="lan" + uci set firewall.@rule[-1].dest="lan" + uci set firewall.@rule[-1].src_mac="$mac" + uci set firewall.@rule[-1].family="any" + uci set firewall.@rule[-1].target="DROP" + uci set firewall.@rule[-1].enabled="1" + fi + + if [ "$dmz_en" = "true" ] || [ "$dmz_en" = "1" ]; then + existing_dmz=$(uci -q show firewall | grep -E "\.name='nw_dmz_" | grep -v "nw_dmz_${mac_clean}'" | head -n 1) + if [ -n "$existing_dmz" ]; then echo '{"error":"DMZ_OCCUPIED_BY_ANOTHER_DEVICE"}'; exit 1; fi + delete_fw_rule "nw_dmz" "redirect" + [ -n "$ip" ] && [ "$ip" != "Unknown IP" ] && { + uci add firewall redirect >/dev/null + uci set firewall.@redirect[-1].name="nw_dmz_${mac_clean}" + uci set firewall.@redirect[-1].src="wan" + uci set firewall.@redirect[-1].dest="lan" + uci set firewall.@redirect[-1].dest_ip="$ip" + uci set firewall.@redirect[-1].proto="all" + uci set firewall.@redirect[-1].target="DNAT" + uci set firewall.@redirect[-1].enabled="1" + } + else + delete_fw_rule "nw_dmz_${mac_clean}" "redirect" + fi + + # --- 放行IPv6外网直连设备 --- + delete_fw_rule "nw_v6expose_${mac_clean}" "rule" + delete_fw_rule "nw_expose_${mac_clean}" "rule" + if [ "$expose_en" = "true" ] || [ "$expose_en" = "1" ]; then + uci add firewall rule >/dev/null + uci set firewall.@rule[-1].name="nw_expose_${mac_clean}" + uci set firewall.@rule[-1].src="wan" + uci set firewall.@rule[-1].dest="lan" + uci set firewall.@rule[-1].family="$expose_proto" + uci set firewall.@rule[-1].target="ACCEPT" + + # 它現在的作用是給探針小弟看的「追蹤标志」 + uci set firewall.@rule[-1].dest_mac="$mac" + + # 2:点击保存的瞬间,立即尝试从邻居表抓取真实的公网IPv6。 + target_ipv6=$(ip -6 neigh show dev br-lan 2>/dev/null | grep -i "$mac" | grep -vE "^(fd|fe80|FAILED)" | awk '{print $1}' | head -n 1) + if [ -n "$target_ipv6" ]; then + uci set firewall.@rule[-1].dest_ip="$target_ipv6" + fi + + uci set firewall.@rule[-1].enabled="1" + fi + # ----------------------------------- + + uci commit firewall + evaluate_keepalive + + # 清空连线 + if command -v conntrack >/dev/null 2>&1; then + [ -n "$ip" ] && [ "$ip" != "Unknown IP" ] && conntrack -D -s "$ip" >/dev/null 2>&1 + v6_ips=$(ip -6 neigh show 2>/dev/null | grep -i "$mac" | awk '{print $1}') + for v6 in $v6_ips; do + conntrack -D -s "$v6" >/dev/null 2>&1 + done + fi + for vap in $(ubus list | grep hostapd); do ubus call "$vap" del_client '{"addr":"'"$mac"'", "reason":5, "deauth":true, "ban_time":0}' >/dev/null 2>&1; done + + # 防火墙重载再中断 + ( + + /etc/init.d/firewall reload >/dev/null 2>&1 + + if [ "$blk_en" = "true" ] || [ "$blk_en" = "1" ]; then + if command -v iptables >/dev/null 2>&1; then + iptables -I FORWARD 1 -m mac --mac-source $mac -j DROP >/dev/null 2>&1 + iptables -I INPUT 1 -m mac --mac-source $mac -j DROP >/dev/null 2>&1 + iptables -t mangle -I PREROUTING 1 -m mac --mac-source $mac -j DROP >/dev/null 2>&1 + fi + if command -v ip6tables >/dev/null 2>&1; then + ip6tables -I FORWARD 1 -m mac --mac-source $mac -j DROP >/dev/null 2>&1 + ip6tables -I INPUT 1 -m mac --mac-source $mac -j DROP >/dev/null 2>&1 + ip6tables -t mangle -I PREROUTING 1 -m mac --mac-source $mac -j DROP >/dev/null 2>&1 + fi + if command -v nft >/dev/null 2>&1; then + nft insert rule inet fw4 forward ether saddr $mac drop >/dev/null 2>&1 + nft insert rule inet fw4 input ether saddr $mac drop >/dev/null 2>&1 + nft insert rule inet fw4 mangle_prerouting ether saddr $mac drop >/dev/null 2>&1 || true + fi + fi + if [ "$iso_en" = "true" ] || [ "$iso_en" = "1" ]; then + if command -v iptables >/dev/null 2>&1; then iptables -I FORWARD 1 -m mac --mac-source $mac -d 192.168.0.0/16 -j DROP >/dev/null 2>&1; fi + if command -v nft >/dev/null 2>&1; then nft insert rule inet fw4 forward ether saddr $mac ip daddr 192.168.0.0/16 drop >/dev/null 2>&1; fi + fi + ) >/dev/null 2>&1 & + + echo '{"result":0}' + exit 0 + ;; + bind) + input=$(cat) + mac=$(echo "$input" | jsonfilter -e '@.mac' | tr 'A-Z' 'a-z') + ip=$(echo "$input" | jsonfilter -e '@.ip') + name=$(echo "$input" | jsonfilter -e '@.name') + dept=$(echo "$input" | jsonfilter -e '@.dept') + no_reload=$(echo "$input" | jsonfilter -e '@.no_reload') + + [ -z "$mac" ] || [ -z "$ip" ] && exit 0 + + if ! echo "$ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then + echo '{"error":"INVALID_IP_FORMAT"}' + exit 1 + fi + + safe_name=$(echo "$name" | sed 's/[^a-zA-Z0-9-]/-/g' | sed 's/-\{2,\}/-/g' | sed 's/^-//; s/-$//') + if [ -z "$safe_name" ] || [ "$safe_name" = "Unknown" ]; then + safe_name="Dev-$(echo "$mac" | tr -d ':' | cut -c 9-12)" + fi + name="$safe_name" + + mac_clean=$(echo "$mac" | tr -d ':' | tr 'A-Z' 'a-z') + + live_duid=$(extract_device_duid "$mac") + [ -n "$live_duid" ] && echo "$mac_clean $live_duid" >> /tmp/netwiz_duid_cache + + matches=$(uci -q show dhcp 2>/dev/null | grep -i -e "\.mac=.*$mac" -e "\.ip=.*$ip" | cut -d'.' -f2 | sed 's/[^0-9]//g' | sort -rn | uniq) + for idx in $matches; do + old_duid=$(uci -q get dhcp.@host[$idx].duid) + if [ -n "$old_duid" ]; then + echo "$mac_clean $old_duid" >> /tmp/netwiz_duid_cache + + # 解绑前,将珍贵的 DUID 转移到永久元数据保存 + MAC_SAFE=$(echo "$mac" | tr -d ':' | tr 'a-z' 'A-Z') + uci -q set netwiz.v6pc_$MAC_SAFE=v6_pc + uci -q set netwiz.v6pc_$MAC_SAFE.mac="$mac" + uci -q set netwiz.v6pc_$MAC_SAFE.duid="$old_duid" + uci -q set netwiz.v6pc_$MAC_SAFE.ts="$(date +%s)" + fi + uci -q delete dhcp.@host[$idx] + done + + # 静态 IP 设定 + new_sect=$(uci add dhcp host) + uci set dhcp.$new_sect.name="$name" + uci set dhcp.$new_sect.mac="$mac" + uci set dhcp.$new_sect.ip="$ip" + ip_suffix=${ip##*.} + hex_suffix=$(printf '%x' "$ip_suffix") + uci set dhcp.$new_sect.hostid="::$hex_suffix" + + auto_duid="" + if [ -f "/tmp/netwiz_duid_cache" ]; then + auto_duid=$(grep -i "^$mac_clean " /tmp/netwiz_duid_cache | awk '{print $2}' | head -n 1 | tr 'A-Z' 'a-z') + fi + + if [ -z "$auto_duid" ]; then + auto_duid=$(extract_device_duid "$mac") + fi + + MAC_SAFE=$(echo "$mac" | tr -d ':' | tr 'a-z' 'A-Z') + + # 1. 如果动态提取不到,从永久元数据里找 + if [ -z "$auto_duid" ]; then + auto_duid=$(uci -q get netwiz.v6pc_$MAC_SAFE.duid) + fi + + if [ -n "$auto_duid" ]; then + echo "$mac_clean $auto_duid" >> /tmp/netwiz_duid_cache + uci set dhcp.$new_sect.duid="$auto_duid" + + # 2. 将 DUID 和 Hostname 写入永久元数据,防丢失 + uci -q set netwiz.v6pc_$MAC_SAFE=v6_pc + uci -q set netwiz.v6pc_$MAC_SAFE.mac="$mac" + uci -q set netwiz.v6pc_$MAC_SAFE.duid="$auto_duid" + uci -q set netwiz.v6pc_$MAC_SAFE.ts="$(date +%s)" + [ -n "$name" ] && [ "$name" != "Unknown" ] && uci -q set netwiz.v6pc_$MAC_SAFE.hostname="$name" + fi + + ip_suffix=${ip##*.} + if [ -n "$ip_suffix" ] && [ "$ip_suffix" -eq "$ip_suffix" ] 2>/dev/null; then + hostid_val=$(printf "%04d" "$ip_suffix") + uci set dhcp.$new_sect.hostid="$hostid_val" + fi + + if [ -n "$dept" ] && [ "$dept" != "none" ]; then uci set dhcp.$new_sect.netwiz_dept="$dept"; fi + + if [ "$no_reload" != "1" ] && [ "$no_reload" != "true" ]; then + uci commit dhcp + uci commit firewall + evaluate_keepalive + + # 清理 odhcpd 租期 + rm -f /tmp/hosts/odhcpd 2>/dev/null + + ( + /etc/init.d/odhcpd restart + /etc/init.d/dnsmasq restart + /etc/init.d/firewall reload + + # 重启 dnsmasq,唤醒代理和 DNS + sleep 2 + wakeup_apps + ) >/dev/null 2>&1 & + fi + echo '{"result":0}' + exit 0 + ;; + unbind) + input=$(cat) + mac=$(echo "$input" | jsonfilter -e '@.mac' | tr 'A-Z' 'a-z') + no_reload=$(echo "$input" | jsonfilter -e '@.no_reload') + [ -z "$mac" ] && exit 0 + + mac_clean=$(echo "$mac" | tr -d ':' | tr 'A-Z' 'a-z') + + live_duid=$(extract_device_duid "$mac") + [ -n "$live_duid" ] && echo "$mac_clean $live_duid" >> /tmp/netwiz_duid_cache + + matches=$(uci -q show dhcp 2>/dev/null | grep -i "\.mac=.*$mac" | cut -d'.' -f2 | sed 's/[^0-9]//g' | sort -rn) + for idx in $matches; do + old_duid=$(uci -q get dhcp.@host[$idx].duid) + if [ -n "$old_duid" ]; then + echo "$mac_clean $old_duid" >> /tmp/netwiz_duid_cache + + # 解绑前,将珍贵的 DUID 转移到永久元数据保存 + MAC_SAFE=$(echo "$mac" | tr -d ':' | tr 'a-z' 'A-Z') + uci -q set netwiz.v6pc_$MAC_SAFE=v6_pc + uci -q set netwiz.v6pc_$MAC_SAFE.mac="$mac" + uci -q set netwiz.v6pc_$MAC_SAFE.duid="$old_duid" + uci -q set netwiz.v6pc_$MAC_SAFE.ts="$(date +%s)" + fi + uci -q delete dhcp.@host[$idx] + done + + delete_fw_rule "nw_blk_${mac_clean}" "rule" + delete_fw_rule "nw_iso_${mac_clean}" "rule" + delete_fw_rule "nw_dmz_${mac_clean}" "redirect" + delete_fw_rule "nw_expose_${mac_clean}" "rule" + if [ "$no_reload" != "1" ] && [ "$no_reload" != "true" ]; then + uci commit dhcp + uci commit firewall + + # 清理 odhcpd 缓存 + rm -f /tmp/hosts/odhcpd 2>/dev/null + + ( + /etc/init.d/odhcpd restart + /etc/init.d/dnsmasq restart + /etc/init.d/firewall reload + + sleep 2 + wakeup_apps + ) >/dev/null 2>&1 & + fi + echo '{"result":0}' + exit 0 + ;; + + apply_dhcp) + uci commit dhcp + uci commit firewall + + # 清理 odhcpd 缓存 + rm -f /tmp/hosts/odhcpd 2>/dev/null + + ( + /etc/init.d/odhcpd restart + /etc/init.d/dnsmasq restart + /etc/init.d/firewall reload + + sleep 2 + wakeup_apps + ) >/dev/null 2>&1 & + + echo '{"result":0}' + exit 0 + ;; + + get_depts) + val=$(uci -q get netwiz.global.depts_json) + [ -z "$val" ] && val="[]" + echo "{\"depts\": $val}" + exit 0 + ;; + + save_depts) + input=$(cat) + json_str=$(echo "$input" | jsonfilter -e '@.data') + uci -q delete netwiz.global + uci set netwiz.global=netwiz + uci set netwiz.global.depts_json="$json_str" + + uci commit netwiz + echo '{"result":0}' + exit 0 + ;; + + get_smart_ranges) + val=$(uci -q get netwiz.smart.ranges_json) + [ -z "$val" ] && val="{}" + echo "{\"ranges\": $val}" + exit 0 + ;; + save_smart_ranges) + input=$(cat) + json_str=$(echo "$input" | jsonfilter -e '@.data') + uci -q set netwiz.smart=netwiz + if [ "$json_str" = "{}" ] || [ -z "$json_str" ]; then + uci -q delete netwiz.smart.ranges_json + else + uci -q set netwiz.smart.ranges_json="$json_str" + fi + uci commit netwiz + echo '{"result":0}' + exit 0 + ;; + + v6_keep_alive) + input=$(cat) + mac=$(echo "$input" | jsonfilter -e '@.mac' | tr 'A-Z' 'a-z') + name=$(echo "$input" | jsonfilter -e '@.name') + [ -z "$mac" ] && exit 0 + + MAC_SAFE=$(echo "$mac" | tr -d ':' | tr 'a-z' 'A-Z') + NOW=$(date +%s) + UPDATE_THRESHOLD=604800 # 7天 + + OLD_TS=$(uci -q get netwiz.v6pc_$MAC_SAFE.ts) + OLD_DUID=$(uci -q get netwiz.v6pc_$MAC_SAFE.duid) + OLD_NAME=$(uci -q get netwiz.v6pc_$MAC_SAFE.hostname) + live_duid=$(extract_device_duid "$mac") + + NEEDS_UPDATE=0 + # 1. 超过7天没更新时间戳 + [ -z "$OLD_TS" ] || [ $((NOW - OLD_TS)) -gt $UPDATE_THRESHOLD ] && NEEDS_UPDATE=1 + + # 2. 抓到 DUID,且和以前保存的不一样 + [ -n "$live_duid" ] && [ "$live_duid" != "$OLD_DUID" ] && NEEDS_UPDATE=1 + + # 3. 传来了新名字,且不是 Unknown,且和以前保存的不一样 + if [ -n "$name" ] && [ "$name" != "Unknown" ]; then + safe_name=$(echo "$name" | sed 's/[^a-zA-Z0-9_-]//g') + [ -n "$safe_name" ] && [ "$safe_name" != "$OLD_NAME" ] && NEEDS_UPDATE=1 + fi + + # 不一样时才写入闪存 + if [ "$NEEDS_UPDATE" -eq 1 ]; then + uci -q set netwiz.v6pc_$MAC_SAFE=v6_pc + uci -q set netwiz.v6pc_$MAC_SAFE.mac="$mac" + uci -q set netwiz.v6pc_$MAC_SAFE.ts="$NOW" + + [ -n "$live_duid" ] && uci -q set netwiz.v6pc_$MAC_SAFE.duid="$live_duid" + [ -n "$safe_name" ] && uci -q set netwiz.v6pc_$MAC_SAFE.hostname="$safe_name" + + uci -q delete netwiz.seen_$MAC_SAFE + uci commit netwiz + fi + + echo '{"result":0}' + exit 0 + ;; + + reset_all) + # 初始化前备份 + mkdir -p /root/netwiz_bak + backup_file="/root/netwiz_bak/netwiz_pre_reset_$(date +%Y%m%d_%H%M%S).tar.gz" + + FILE_LIST="" + for f in network wireless dhcp firewall netwiz; do + if [ -s "/etc/config/$f" ]; then FILE_LIST="$FILE_LIST etc/config/$f"; fi + done + tar -czf "$backup_file" -C / $FILE_LIST 2>/dev/null + + logger -t Netwiz "📦 Pre-reset backup created: $backup_file" + + # 1. 净化防火墙 + for rule in $(uci -q show firewall 2>/dev/null | grep "^firewall\.@rule\[[0-9]*\]\.name=.*nw_" | cut -d'.' -f2 | sed 's/[^0-9]//g' | sort -rn | uniq); + do + uci -q delete firewall.@rule[$rule] + done + for redir in $(uci -q show firewall 2>/dev/null | grep "^firewall\.@redirect\[[0-9]*\]\.name=.*nw_dmz_" | cut -d'.' -f2 | sed 's/[^0-9]//g' | sort -rn | uniq); + do + uci -q delete firewall.@redirect[$redir] + done + uci commit firewall + + # 2. 净化 DHCP (删除所有的静态 IP 绑定) + for host in $(uci -q show dhcp 2>/dev/null | grep "^dhcp\.@host\[[0-9]*\]=host" | cut -d'.' -f2 | sed 's/[^0-9]//g' | sort -rn | uniq); + do + uci -q delete dhcp.@host[$host] + done + uci commit dhcp + + # 3. 淨化 Netwiz 配置 + rm -f /etc/config/netwiz + touch /etc/config/netwiz + uci -q batch </dev/null 2>&1 & + + exit 0 + ;; + + export_config) + + FILE_LIST="" + for f in network wireless dhcp firewall netwiz; do + if [ -s "/etc/config/$f" ]; then + FILE_LIST="$FILE_LIST etc/config/$f" + fi + done + + if [ -n "$FILE_LIST" ]; then + timestamp=$(date +%Y%m%d_%H%M%S | tr -d '\r\n') + dl_name="NetWiz_NetConf_${timestamp}.tar.gz" + + backup_file="/www/luci-static/${dl_name}" + + rm -f /www/luci-static/NetWiz_NetConf_*.tar.gz 2>/dev/null + + # 打包文件 + tar -czf "$backup_file" -C / $FILE_LIST 2>/dev/null + + if [ -s "$backup_file" ]; then + chmod 644 "$backup_file" 2>/dev/null + printf '{"url":"/luci-static/%s", "filename":"%s"}\n' "$dl_name" "$dl_name" + + # 120 秒后删除 + ( sleep 120; rm -f "$backup_file" ) >/dev/null 2>&1 & + else + printf '{"error":"Archive creation failed"}\n' + fi + else + printf '{"error":"No files to backup"}\n' + fi + exit 0 + ;; + + wake_device) + input=$(cat) + mac=$(echo "$input" | jsonfilter -e '@.mac' | tr 'A-Z' 'a-z') + [ -n "$mac" ] && { + # 使用 OpenWrt 內建的唤醒 + if command -v etherwake >/dev/null 2>&1; then + etherwake -b -i br-lan "$mac" >/dev/null 2>&1 + elif command -v ether-wake >/dev/null 2>&1; then + ether-wake -b -i br-lan "$mac" >/dev/null 2>&1 + fi + } + echo '{"result":0}' + exit 0 + ;; + + list_backups) + mkdir -p /root/netwiz_bak + # 按时间倒序获取压缩包名 + files=$(ls -t /root/netwiz_bak/*.tar.gz 2>/dev/null | awk -F/ '{print $NF}') + + printf '{"backups":[' + first=1 + for f in $files; do + if [ "$first" -eq 1 ]; then first=0; else printf ','; fi + printf '"%s"' "$f" + done + printf ']}\n' + exit 0 + ;; + + import_config) + input=$(cat) + b64data=$(echo "$input" | jsonfilter -e '@.data') + file_name=$(echo "$input" | jsonfilter -e '@.file') + + restore_file="/tmp/netwiz_restore.tar.gz" + has_file=0 + + # data 是电脑上传,file_name 是选择路由器本地备份 + if [ -n "$b64data" ]; then + echo "$b64data" | base64 -d > "$restore_file" + has_file=1 + elif [ -n "$file_name" ] && [ -f "/root/netwiz_bak/$file_name" ]; then + cp "/root/netwiz_bak/$file_name" "$restore_file" + has_file=1 + fi + + if [ "$has_file" -eq 1 ]; then + mkdir -p /root/netwiz_bak + + backup_file="/root/netwiz_bak/netwiz_pre_import_$(date +%Y%m%d_%H%M%S).tar.gz" + + FILE_LIST="" + for f in network wireless dhcp firewall netwiz; do + if [ -s "/etc/config/$f" ]; then FILE_LIST="$FILE_LIST etc/config/$f"; fi + done + tar -czf "$backup_file" -C / $FILE_LIST 2>/dev/null + + # --- 开始安全沙盒防呆/防黑客逻辑 --- + mkdir -p /tmp/nw_import_tmp + tar -xzf "$restore_file" -C /tmp/nw_import_tmp/ >/dev/null 2>&1 + + # 1. 检查解压是否报错 + if [ $? -ne 0 ]; then + rm -rf /tmp/nw_import_tmp "$restore_file" + echo '{"error":"Invalid archive format"}' + exit 0 + fi + + # 2. 特征码强校验 + if [ ! -f "/tmp/nw_import_tmp/etc/config/network" ] && [ ! -f "/tmp/nw_import_tmp/etc/config/dhcp" ] && [ ! -f "/tmp/nw_import_tmp/etc/config/netwiz" ]; then + rm -rf /tmp/nw_import_tmp "$restore_file" + echo '{"error":"Missing signature files"}' + exit 0 + fi + + # 3. 白名单安全覆盖 + for f in network wireless dhcp firewall netwiz; do + if [ -f "/tmp/nw_import_tmp/etc/config/$f" ]; then + cp -f "/tmp/nw_import_tmp/etc/config/$f" "/etc/config/$f" + fi + done + + # 清理沙盒与残留 + rm -rf /tmp/nw_import_tmp "$restore_file" + # --- 🛡️ 安全逻辑结束 --- + + echo '{"result":0}' + + # 延时 2 秒后重启网络相关服务生效配置 + ( + sleep 2 + /etc/init.d/network restart + /etc/init.d/dnsmasq restart + /etc/init.d/odhcpd restart + /etc/init.d/firewall restart + + sleep 3 + wakeup_apps + ) >/dev/null 2>&1 & + else + echo '{"error":"No valid backup data or file provided"}' + fi + exit 0 + ;; + + auto_backup) + # 备份目录 + mkdir -p /root/netwiz_bak + + # 1. 计算 MD5 特征码 + current_md5=$(cat /etc/config/network /etc/config/wireless /etc/config/dhcp /etc/config/firewall /etc/config/netwiz 2>/dev/null | md5sum | awk '{print $1}') + # 将 MD5 记录文件也移入专属目录 + last_md5=$(cat /root/netwiz_bak/.netwiz_last_md5 2>/dev/null) + + # 2. MD5 发生变化备份 + if [ "$current_md5" != "$last_md5" ]; then + timestamp=$(date +%Y%m%d_%H%M%S) + backup_file="/root/netwiz_bak/netwiz_auto_${timestamp}.tar.gz" + + FILE_LIST="" + for f in network wireless dhcp firewall netwiz; do + if [ -s "/etc/config/$f" ]; then FILE_LIST="$FILE_LIST etc/config/$f"; fi + done + tar -czf "$backup_file" -C / $FILE_LIST 2>/dev/null + + # 记录最新的 MD5 + echo "$current_md5" > /root/netwiz_bak/.netwiz_last_md5 + logger -t Netwiz "✅ Auto-backup created: $backup_file" + + # 3. 保留最近的 7 份自动备份 + ls -t /root/netwiz_bak/netwiz_auto_*.tar.gz 2>/dev/null | tail -n +8 | xargs rm -f 2>/dev/null + # 保留5 份高危操作前备份 + ls -t /root/netwiz_bak/netwiz_pre_*.tar.gz 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null + else + logger -t Netwiz "💤 Auto-backup skipped: Config unchanged" + fi + + echo '{"result":0}' + exit 0 + ;; + + analyze_conns) + input=$(cat) + ip=$(echo "$input" | jsonfilter -e '@.ip') + + if ! echo "$ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then + echo '{"error":"INVALID_IP"}' + exit 1 + fi + + raw_file="/tmp/nw_conns_${ip}.txt" + + if [ -f /proc/net/nf_conntrack ]; then + awk -v ip="$ip" '{for(i=1;i<=NF;i++)if($i~/^src=/){split($i,a,"="); if(a[2]==ip) print $0; break}}' /proc/net/nf_conntrack 2>/dev/null > "$raw_file" + elif command -v conntrack >/dev/null 2>&1; then + conntrack -L 2>/dev/null | awk -v ip="$ip" '{for(i=1;i<=NF;i++)if($i~/^src=/){split($i,a,"="); if(a[2]==ip) print $0; break}}' > "$raw_file" + fi + + # 分类统計 + stats=$(awk ' + BEGIN { web=0; dns=0; udp_media=0; p2p=0; other=0; total=0 } + { + total++; + proto = $3; + dport = 0; + for(i=1;i<=NF;i++) { + if($i ~ /^dport=/) { split($i,a,"="); dport=a[2]+0; break; } + } + + if (dport == 80 || dport == 443) { web++; } + else if (dport == 53 || dport == 5353 || dport == 123) { dns++; } + else if (proto == "udp") { udp_media++; } + else if (dport > 10000) { p2p++; } + else { other++; } + } + END { + printf "{\"total\":%d, \"web\":%d, \"dns\":%d, \"udp_media\":%d, \"p2p\":%d, \"other\":%d}", total, web, dns, udp_media, p2p, other + }' "$raw_file") + + # 截取前 50 条原始数据給前端黑框 + raw_logs=$(head -n 50 "$raw_file" | sed 's/"/\\"/g' | awk '{printf "\"%s\",", $0}' | sed 's/,$//') + + rm -f "$raw_file" + echo "{\"stats\": $stats, \"raw\": [$raw_logs]}" + exit 0 + ;; + esac + ;; +esac +exit 1 diff --git a/luci-app-netwiz/luci-app-netwiz/root/usr/share/luci/menu.d/luci-app-netwiz.json b/luci-app-netwiz/luci-app-netwiz/root/usr/share/luci/menu.d/luci-app-netwiz.json new file mode 100644 index 000000000..d7cef09a4 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/usr/share/luci/menu.d/luci-app-netwiz.json @@ -0,0 +1,23 @@ +{ + "admin/netwiz": { + "title": "Network Wizard", + "order": 99, + "action": { + "type": "view", + "path": "netwiz" + }, + "depends": { + "acl": [ "luci-app-netwiz" ] + } + }, + "admin/netwiz_dev": { + "action": { + "type": "view", + "path": "netwiz_dev" + }, + "depends": { + "acl": [ "luci-app-netwiz" ] + }, + "hidden": true + } +} diff --git a/luci-app-netwiz/luci-app-netwiz/root/usr/share/rpcd/acl.d/luci-app-netwiz.json b/luci-app-netwiz/luci-app-netwiz/root/usr/share/rpcd/acl.d/luci-app-netwiz.json new file mode 100644 index 000000000..187571ec6 --- /dev/null +++ b/luci-app-netwiz/luci-app-netwiz/root/usr/share/rpcd/acl.d/luci-app-netwiz.json @@ -0,0 +1,17 @@ +{ + "luci-app-netwiz": { + "description": "Grant access to netwiz and device manager", + "read": { + "ubus": { + "netwiz": [ "*" ], + "netwiz_dev": [ "*" ] + } + }, + "write": { + "ubus": { + "netwiz": [ "*" ], + "netwiz_dev": [ "*" ] + } + } + } +} diff --git a/luci-app-netwiz/probe.py b/luci-app-netwiz/probe.py new file mode 100644 index 000000000..486d27cb9 --- /dev/null +++ b/luci-app-netwiz/probe.py @@ -0,0 +1,234 @@ +# Copyright (C) 2026 huchd0 +# Licensed under the GNU General Public License v3.0 + +# ================================================== +# English | IPv6 Watchdog - (VPS Edition) +# ================================================== +# [Environment Requirements] +# 1. Ultra-lightweight: Uses built-in Python 3 on Linux (heavyweight environments like Node.js, Docker, or Nginx can also be used). +# 2. The VPS must have IPv6 network outbound capability. +# +# [Native Daemon] +# Uses systemd for process management, enabling auto-start on boot and sub-second self-healing on crashes. +# +# [Ultra-lightweight Deployment Steps] +# 1. Save the file: +# Copy all the code inside the `probe.py` file in this project and save it to your VPS at /root/probe.py +# +# 2. Create the daemon service: +# Execute the command in the terminal: nano /etc/systemd/system/netwiz-probe.service +# Paste the content inside the dashed lines below and save (Ctrl+O, Enter, Ctrl+X): +# ---------------------------------- +# [Unit] +# Description=Netwiz IPv6 Probe Service +# After=network.target +# +# [Service] +# Type=simple +# User=root +# ExecStart=/usr/bin/python3 /root/probe.py +# Restart=always +# RestartSec=3 +# +# [Install] +# WantedBy=multi-user.target +# ---------------------------------- +# +# 3. Activate and start the probe (execute sequentially): +# systemctl daemon-reload +# systemctl enable netwiz-probe +# systemctl start netwiz-probe +# +# 4. Plugin Configuration: +# In the <📡 IPv6 Watchdog> section of the Netwiz Advanced Utility Settings, enter: http://YOUR_VPS_IP:18080 +# (Note: Port 18080 can be freely modified in the basic configuration below: LISTEN_PORT = 18080. +# Please ensure the VPS firewall and cloud provider security group have allowed port 18080) +# +# [Common Management Commands] +# Check status: systemctl status netwiz-probe +# Stop probe: systemctl stop netwiz-probe +# Restart probe: systemctl restart netwiz-probe +# Real-time log: journalctl -u netwiz-probe -f +# +# +# ================================================== +# 简体中文 | IPv6 深度保活 - (VPS 版) +# ================================================== +# 【环境要求】 +# 1. 极致轻量:使用 Linux 自带的 Python 3(当然也可以使用 Node.js、Docker 或 Nginx 等庞大环境)。 +# 2. VPS 必须具备 IPv6 网络出口能力。 +# +# 【原生守护】 +# 采用 systemd 进程守护,开机自启、崩溃秒级自愈。 +# +# 【极致轻量部署步骤】 +# 1. 保存文件: +# 复制本项目中的 `probe.py` 里的全部代码,保存到 VPS 的 /root/probe.py +# +# 2. 创建守护服务: +# 在终端执行命令: nano /etc/systemd/system/netwiz-probe.service +# 将下方虚线内的内容贴入并保存 (Ctrl+O, Enter, Ctrl+X): +# ---------------------------------- +# [Unit] +# Description=Netwiz IPv6 Probe Service +# After=network.target +# +# [Service] +# Type=simple +# User=root +# ExecStart=/usr/bin/python3 /root/probe.py +# Restart=always +# RestartSec=3 +# +# [Install] +# WantedBy=multi-user.target +# ---------------------------------- +# +# 3. 激活并启动探针(依次执行): +# systemctl daemon-reload +# systemctl enable netwiz-probe +# systemctl start netwiz-probe +# +# 4. 插件填写: +# 在 Netwiz 高级实用设置的 <📡 IPv6 深度保活> 设置中,填写:http://您的VPS_IP:18080 +# (注:18080 端口可以在下方的基础配置:LISTEN_PORT = 18080 随意修改,请确保 VPS 防火墙与云服务商安全组已放行 18080 端口) +# +# 【常用管理命令】 +# 查看状态: systemctl status netwiz-probe +# 停止探针: systemctl stop netwiz-probe +# 重启探针: systemctl restart netwiz-probe +# 实时日志: journalctl -u netwiz-probe -f +# +# +# ================================================== +# 繁體中文 | IPv6 深度保活 - (VPS 版) +# ================================================== +# 【環境要求】 +# 1. 極致輕量:使用 Linux 內建的 Python 3(當然也可以使用 Node.js、Docker 或 Nginx 等龐大環境)。 +# 2. VPS 必須具備 IPv6 網路出口能力。 +# +# 【原生守護】 +# 採用 systemd 進程守護,開機自啟、崩潰秒級自癒。 +# +# 【極致輕量部署步驟】 +# 1. 保存文件: +# 複製本項目中的 `probe.py` 裡的全部代碼,保存到 VPS 的 /root/probe.py +# +# 2. 創建守護服務: +# 在終端執行命令: nano /etc/systemd/system/netwiz-probe.service +# 將下方虛線內的內容貼入並保存 (Ctrl+O, Enter, Ctrl+X): +# ---------------------------------- +# [Unit] +# Description=Netwiz IPv6 Probe Service +# After=network.target +# +# [Service] +# Type=simple +# User=root +# ExecStart=/usr/bin/python3 /root/probe.py +# Restart=always +# RestartSec=3 +# +# [Install] +# WantedBy=multi-user.target +# ---------------------------------- +# +# 3. 激活並啟動探針(依次執行): +# systemctl daemon-reload +# systemctl enable netwiz-probe +# systemctl start netwiz-probe +# +# 4. 外掛填寫: +# 在 Netwiz 高級實用設置的 <📡 IPv6 深度保活> 設置中,填寫:http://您的VPS_IP:18080 +# (註:18080 端口可以在下方的基礎配置:LISTEN_PORT = 18080 隨意修改,請確保 VPS 防火牆與雲端服務商安全組已放行 18080 端口) +# +# 【常用管理命令】 +# 查看狀態: systemctl status netwiz-probe +# 停止探針: systemctl stop netwiz-probe +# 重啟探針: systemctl restart netwiz-probe +# 即時日誌: journalctl -u netwiz-probe -f +# +# ============================ END ============================ + +# 基础配置 (可随意修改) +LISTEN_PORT = 18080 # 绑定您要求的端口,可随时更改 + +import urllib.request +import urllib.error +import urllib.parse +import socket +import re +from http.server import HTTPServer, BaseHTTPRequestHandler + +# 强制绑定双栈 (支持 IPv6 和 IPv4) +class HTTPServerV6(HTTPServer): + address_family = socket.AF_INET6 + allow_reuse_address = True # 允许端口复用,防止重启时报 Address already in use 错误 + +class ProbeHandler(BaseHTTPRequestHandler): + # 屏蔽默认的终端日志输出,保持清爽 + def log_message(self, format, *args): + pass + + def do_GET(self): + # 统一返回 200 状态码和纯文本 + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + + # 检测到当前系统没有 IPv6 网络出口,返回提示 + try: + # 创建 UDP 套接字,探测本地是否有 IPv6 路由 + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.connect(('2404:6800:4005:805::200e', 80)) + s.close() + except Exception: + self.wfile.write(b"FAIL_VPS_NO_IPV6_SUPPORT") + return + + # 1. 解码 URL (防止 IPv6 的方括号被浏览器转义为 %5B 和 %5D) + decoded_path = urllib.parse.unquote(self.path) + + # 2. 解析探针目标: 直接剥离掉最前面的 '/' 即可,不再需要任何前缀 + target = decoded_path.lstrip('/') + + # 没带目标时,返回提示 + if not target: + self.wfile.write(b"MISSING_TARGET") + return + + # 3. 协议补全 (默认补上 http://) + if not target.startswith('http://') and not target.startswith('https://'): + target = 'http://' + target + + # 4. 提取 Hostname + parsed_url = urllib.parse.urlparse(target) + hostname = parsed_url.hostname or '' + + # 5. 终极防呆:拦截私有局域网 IP + private_ip_regex = r'^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.|0\.|\[?fd[0-9a-f]{2}:)' + if re.match(private_ip_regex, hostname, re.I): + self.wfile.write(b"FAIL_PRIVATE_IP_BLOCKED") + return + + # 6. 发起真实探测 + try: + req = urllib.request.Request(target) + # 设定 6 秒超时 (与 CF Worker 代码保持一致) + with urllib.request.urlopen(req, timeout=6) as res: + self.wfile.write(b"OK") + except urllib.error.HTTPError as e: + # 收到密码拦截 (401/403) 也视为网络畅通 + if e.code in [401, 403]: + self.wfile.write(b"OK") + else: + self.wfile.write(f"FAIL_HTTP_{e.code}".encode('utf-8')) + except Exception as e: + # 连接超时、拒绝连接、无法解析等网络层错误 + self.wfile.write(f"FAIL_NETWORK_VPS: {str(e)}".encode('utf-8')) + +if __name__ == '__main__': + print(f"Netwiz Probe 正在运行 (监听端口: {LISTEN_PORT})...") + # 启动监听 + server = HTTPServerV6(('::', LISTEN_PORT), ProbeHandler) + server.serve_forever() diff --git a/luci-app-netwiz/worker.js b/luci-app-netwiz/worker.js new file mode 100644 index 000000000..3629f700e --- /dev/null +++ b/luci-app-netwiz/worker.js @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2026 huchd0 + * Licensed under the GNU General Public License v3.0 +*/ + +/* +# ================================================== +# English | IPv6 Watchdog - (Cloudflare Edition) +# ================================================== + +Developed specifically to address the instability of IPv6 direct-connected devices. Leveraging Cloudflare's global edge network, it allows you to deploy quickly with zero cost and zero configuration. + +### Perfectly integrates with the router's `luci-app-netwiz` to achieve enterprise-grade IPv6 direct connection keep-alive and self-healing. + +--- + +## ✨ Core Features + +- **🎉 Free**: Hosted on Cloudflare Workers, providing up to 100,000 free probe requests per day, which is more than enough for home or normal usage. +- **⚡ Native IPv6 Ultra-fast Response**: Utilizes Cloudflare's massive global IPv6 backbone network for precise and fast connections. +- **🛡️ Privacy & Anti-abuse**: Pure probing logic with no user data collection. Built-in strict LAN IP protection (SSRF interception) to eliminate the risk of malicious scanning. + +--- + +## 🚀 Deployment Guide + +No complex local environment needed. You can build your exclusive probe directly through your browser. + +### Step 1: Get the Probe Source Code +Copy all the code inside the `worker.js` file in this project to use in the next step. + +### Step 2: One-click Deployment to Cloudflare +1. Visit the **[Cloudflare Dashboard](https://dash.cloudflare.com/)** and log in to your account. +2. Find and click on **`Workers & Pages`** in the left navigation bar. +3. Click **`Create application`**, then click **`Create Worker`**. +4. Give your probe a name (e.g., `netwiz-probe`) and click **`Deploy`**. +5. Once deployed successfully, click **`Edit code`**. +6. Delete all default code in the input box, **select all and paste the entire content of `worker.js`** into it. +7. Click **`Save and deploy`** in the top right corner. +8. Note down the exclusive probe domain assigned to you by Cloudflare (e.g., `netwiz-probe.yourusername.workers.dev`). + +### Step 3: Configure the Router Dashboard +1. Copy the **probe base domain** you just obtained (e.g., `netwiz-probe.xxxx.workers.dev`). +2. Log in to your router dashboard and navigate to the **Netwiz** advanced settings panel. +3. Enable the **"📡 IPv6 Watchdog"** feature. +4. Paste the copied domain directly into the **"Probe Target URL"** input box. +5. Click **Save & Apply**. + +🎉 **All done!** Your router is now equipped with self-healing capabilities for IPv6 direct connections! + + +# ================================================== +# 简体中文 | IPv6 深度保活 - (Cloudflare 版) +# ================================================== + +专门针对 IPv6 直连设备时不稳定的痛点开发,借助 Cloudflare 覆盖全球的边缘节点网络优势,让您可以零成本、免配置地快速部署。 + +### 完美配合路由器的 `luci-app-netwiz`,实现企业级的 IPv6 直联设备保活与故障自愈。 + +--- + +## ✨ 核心特性 + +- **🎉 免费**: 基于 Cloudflare Workers 托管,每天提供高达 10 万次的免费探测额度,足够家庭或正常用户使用。 +- **⚡ 原生 IPv6 极速响应**: 利用 Cloudflare 全球最庞大的 IPv6 骨干网进行精准连接。 +- **🛡️ 隐私与防滥用**: 纯净探测逻辑,不收集任何用户隐私数据,内置严格的局域网 IP 防护(SSRF 拦截),杜绝被恶意扫描的风险。 + +--- + +## 🚀 部署指南 + +无需复杂的本地环境,通过浏览器即可完成专属探针的搭建。 + +### 第一步:获取探针源码 +复制本项目中的 `worker.js` 里的全部代码,准备在下一步使用。 + +### 第二步:一键部署到 Cloudflare +1. 访问 **[Cloudflare 控制台](https://dash.cloudflare.com/)** 并登录您的账号。 +2. 在左侧导航栏找到并点击 **`Workers 和 Pages`**。 +3. 点击 **`创建应用程序 (Create application)`**,然后点击 **`创建 Worker (Create Worker)`**。 +4. 为您的探针起一个名字(例如:`netwiz-probe`),点击 **`部署 (Deploy)`**。 +5. 部署成功后,点击 **`编辑代码 (Edit code)`**。 +6. 将原来输入框里默认的代码全部删除,**全选并复制当前文件全部内容**粘贴进去。 +7. 点击右上角的 **`保存并部署 (Save and deploy)`**。 +8. 记录下 Cloudflare 为您分配的专属探针域名(例如:`netwiz-probe.您的用户名.workers.dev`)。 + +### 第三步:配置路由器后台 +1. 复制刚刚获取的**探针基础域名**(例如:`netwiz-probe.xxxx.workers.dev`)。 +2. 登录您的路由器后台,进入 **Netwiz 插件设置**高级面板。 +3. 开启 **“📡 IPv6 深度保活 (IPv6 Watchdog)”** 功能。 +4. 将复制的域名直接粘贴到 **“探测源目标网址 (Probe Target URL)”** 输入框中。 +5. 点击**保存并应用**。 + +🎉 **完成!** 现在您的路由器已经具备了 IPv6 直联的自愈能力! + + +# ================================================== +# 繁體中文 | IPv6 深度保活 - (Cloudflare 版) +# ================================================== + +專門針對 IPv6 直連設備時不穩定的痛點開發,借助 Cloudflare 覆蓋全球的邊緣節點網路優勢,讓您可以零成本、免配置地快速部署。 + +### 完美配合路由器的 `luci-app-netwiz`,實現企業級的 IPv6 直聯設備保活與故障自癒。 + +--- + +## ✨ 核心特性 + +- **🎉 免費**: 基於 Cloudflare Workers 託管,每天提供高達 10 萬次的免費探測額度,足夠家庭或正常用戶使用。 +- **⚡ 原生 IPv6 極速響應**: 利用 Cloudflare 全球最龐大的 IPv6 骨幹網進行精準連接。 +- **🛡️ 隱私與防濫用**: 純淨探測邏輯,不收集任何用戶隱私數據,內置嚴格的區域網 IP 防護(SSRF 攔截),杜絕被惡意掃描的風險。 + +--- + +## 🚀 部署指南 + +無需複雜的本地環境,透過瀏覽器即可完成專屬探針的搭建。 + +### 第一步:獲取探針源碼 +複製本項目中的 `worker.js` 裡的全部代碼,準備在下一步使用。 + +### 第二步:一鍵部署到 Cloudflare +1. 訪問 **[Cloudflare 控制台](https://dash.cloudflare.com/)** 並登入您的帳號。 +2. 在左側導航列找到並點擊 **`Workers 和 Pages`**。 +3. 點擊 **`創建應用程式 (Create application)`**,然後點擊 **`創建 Worker (Create Worker)`**。 +4. 為您的探針起一個名字(例如:`netwiz-probe`),點擊 **`部署 (Deploy)`**。 +5. 部署成功後,點擊 **`編輯代碼 (Edit code)`**。 +6. 將原來輸入框裡預設的代碼全部刪除,**全選並複製當前文件全部內容**貼上進去。 +7. 點擊右上角的 **`保存並部署 (Save and deploy)`**。 +8. 記錄下 Cloudflare 為您分配的專屬探針域名(例如:`netwiz-probe.您的用戶名.workers.dev`)。 + +### 第三步:配置路由器後台 +1. 複製剛剛獲取的**探針基礎域名**(例如:`netwiz-probe.xxxx.workers.dev`)。 +2. 登入您的路由器後台,進入 **Netwiz 外掛設定**高級面板。 +3. 開啟 **「📡 IPv6 深度保活 (IPv6 Watchdog)」** 功能。 +4. 將複製的域名直接貼到 **「探測源目標網址 (Probe Target URL)」** 輸入框中。 +5. 點擊**保存並應用**。 + +🎉 **完成!** 現在您的路由器已經具備了 IPv6 直聯的自癒能力! + +# ================================== END ================================== +*/ + + +// ========================================== +// 核心引擎 (通用探測版) +// ========================================== +async function runProbe(rawTarget) { + if (!rawTarget) return "MISSING_TARGET"; + + // 1. 协议补全与斜杠修复 + // 修复可能被浏览器或代理压缩的斜杠,例如把 http:/[2408...] 还原成 http://[2408...] + let target = rawTarget.replace(/^(https?):\/+/i, '$1://'); + + let fetchUrl = target; + // 如果用户没自带协议,默认补上 http:// + if (!fetchUrl.startsWith('http://') && !fetchUrl.startsWith('https://')) { + fetchUrl = 'http://' + fetchUrl; + } + + // 2. 提取 Hostname 进行局域网 IP 防护 + let parsedUrl; + try { + parsedUrl = new URL(fetchUrl); + } catch (e) { + return "FAIL_INVALID_URL"; + } + + // 防呆:拦截私有局域网 IP + const privateIPRegex = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.|0\.|\[?fd[0-9a-f]{2}:)/i; + // 使用 parsedUrl.hostname 进行检测,比单纯测字符串更精准 + if (privateIPRegex.test(parsedUrl.hostname)) return "FAIL_PRIVATE_IP_BLOCKED"; + + // 3. 发起真实探测 + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 6000); + + const response = await fetch(fetchUrl, { signal: controller.signal }); + clearTimeout(timeoutId); + + // 状态码为 200~299,或 401/403 (密码拦截) 均视为网络畅通 + if (response.ok || response.status === 401 || response.status === 403) { + return "OK"; + } else { + return "FAIL_HTTP_" + response.status; + } + } catch (err) { + return "FAIL_NETWORK_CF: " + err.message + " | " + err.name; + } +} + + +// 以下Cloudflare的「专属接头」,可以更换成其它平台的接头: +// ========================================== +// Cloudflare 专属接头 +// ========================================== +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + + // 提取域名后面的路径(不包含最前面的 '/') + let targetPath = url.pathname.substring(1); + + if (!targetPath) return new Response("MISSING_TARGET"); + + // 防止 IPv6 的方括号 [] 等特殊字符被 URL 编码导致解析失败 + try { + targetPath = decodeURIComponent(targetPath); + } catch (e) { + // 忽略解码错误 + } + + return new Response(await runProbe(targetPath)); + } +}; +// === 结束 === diff --git a/luci-app-wwand/Makefile b/luci-app-wwand/Makefile index e35c7cec4..04cdeaf0e 100644 --- a/luci-app-wwand/Makefile +++ b/luci-app-wwand/Makefile @@ -17,12 +17,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-wwand -PKG_VERSION:=1.6.8_p8 -PKG_RELEASE:=31 +PKG_VERSION:=1.6.8_p13 +PKG_RELEASE:=32 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git -PKG_SOURCE_VERSION:=ee53e2130cc68616aabb0f8e9343d124ae5dfe63 +PKG_SOURCE_VERSION:=210f4be24144a605276c108c0abb56c55e29eef2 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only @@ -79,7 +79,7 @@ endef # statistics graph definition would have been installed on boxes with no # luci-app-statistics. A new resource has to be added here, and that is the # point of listing them. -LUCI_WWAND_VIEW:=modems.js settings.js status.js +LUCI_WWAND_VIEW:=modems.js settings.js sims.js status.js LUCI_WWAND_RES:=bands.js esim.js format.js graph.js logbox.js mccmnc.js \ modemopts.js modemsid.js netsel.js rpc.js signal.svg simlist.js diff --git a/luci-proto-wwand/Makefile b/luci-proto-wwand/Makefile index 5a5c21f05..5a090bac7 100644 --- a/luci-proto-wwand/Makefile +++ b/luci-proto-wwand/Makefile @@ -20,12 +20,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-proto-wwand -PKG_VERSION:=1.6.8_p1 -PKG_RELEASE:=15 +PKG_VERSION:=1.6.8_p2 +PKG_RELEASE:=16 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git -PKG_SOURCE_VERSION:=43c71ef81faa7c2a164a9026ab526d3e2544a88e +PKG_SOURCE_VERSION:=d2b2e85f5885581547f5863379fc44d64e9c9051 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only diff --git a/wwand-ipa/Makefile b/wwand-ipa/Makefile index 4098654c1..a17e5bd6a 100644 --- a/wwand-ipa/Makefile +++ b/wwand-ipa/Makefile @@ -13,8 +13,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand-ipa -PKG_VERSION:=0.1.0_p2 -PKG_RELEASE:=5 +PKG_VERSION:=0.1.0_p6 +PKG_RELEASE:=6 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand-ipa.git diff --git a/wwand-ipad/Makefile b/wwand-ipad/Makefile index b047ab16a..ca91efaa6 100644 --- a/wwand-ipad/Makefile +++ b/wwand-ipad/Makefile @@ -15,8 +15,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand-ipad -PKG_VERSION:=0.1.0_p21 -PKG_RELEASE:=6 +PKG_VERSION:=0.1.0_p40 +PKG_RELEASE:=7 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/ipad.git diff --git a/wwand-rsim/Makefile b/wwand-rsim/Makefile index 9533340c7..2ace65821 100644 --- a/wwand-rsim/Makefile +++ b/wwand-rsim/Makefile @@ -21,8 +21,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand-rsim -PKG_VERSION:=0.0.1_p37 -PKG_RELEASE:=4 +PKG_VERSION:=0.0.1_p55 +PKG_RELEASE:=5 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand-rsim.git @@ -151,8 +151,10 @@ define Package/rsim-card/description libusb) or the SIM of a modem it reaches over its AT port (at:, AT+CSIM; that modem's radio is switched off while its card is lent), and speaks JSON lines on stdin/stdout; a paired phone's SIM over the - Bluetooth SIM Access Profile (bt:
    , kernel sockets only). - `rsim-card --list` shows what the machine offers; `rsim-card --serve` is + Bluetooth SIM Access Profile (bt:
    , kernel sockets only); or a + card in an osmo-remsim SIM bank, as its RSPRO client + (rspro:[:port][/:], used by the router itself — never + served over SSH). `rsim-card --list` shows what the machine offers; `rsim-card --serve` is the forced command for the router's SSH key. Install it on a router that only holds the reader or the modem (a SIM host); the router with the using modem reaches it over SSH (rsim_reader ssh:@:). Needs diff --git a/wwand/Makefile b/wwand/Makefile index 64ad15279..6257c45b8 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -50,12 +50,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand -PKG_VERSION:=1.6.8_p56 -PKG_RELEASE:=60 +PKG_VERSION:=1.6.8_p66 +PKG_RELEASE:=61 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git -PKG_SOURCE_VERSION:=ed275d3cf5ab201fcbdf213b4665a443ca776c5c +PKG_SOURCE_VERSION:=d91a638f92d8f058456d943d74f5afa033f792f9 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only