diff --git a/hysteria/Makefile b/hysteria/Makefile index 38c70d7c..6184f7bb 100644 --- a/hysteria/Makefile +++ b/hysteria/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=hysteria -PKG_VERSION:=2.7.1 -PKG_RELEASE:=1 +PKG_VERSION:=2.8.0 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/apernet/hysteria/tar.gz/app/v$(PKG_VERSION)? diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js index ca2a0619..42608529 100644 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js +++ b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js @@ -279,6 +279,7 @@ return view.extend({ s.anonymous = true; s.tab('basic', _('Basic Settings')); + s.tab('longconn', _('Long Connection Settings')); s.tab('auth', _('Auth Server Settings')); s.tab('gateway', _('Gateway Settings')); s.tab('advanced', _('Advanced Settings')); @@ -322,6 +323,28 @@ return view.extend({ return this.super('load', [section_id]); }; + o = s.taboption('basic', form.ListValue, 'selected_long_conn', _('Long Connection Profile'), + _('Select which long connection configuration to use. This option is always available regardless of authentication mode.')); + o.rmempty = true; + o.optional = true; + o.load = function (section_id) { + this.keylist = []; + this.vallist = []; + + var sections = uci.sections('wifidogx', 'longconn'); + if (sections.length == 0) { + this.value('', _('Please create a long connection profile first')); + } else { + sections.forEach(L.bind(function (section) { + var name = section['.name']; + var mode = section.long_conn_mode || _('Unknown'); + this.value(name, '%s (%s)'.format(name, mode)); + }, this)); + } + + return this.super('load', [section_id]); + }; + o = s.taboption('basic', widgets.NetworkSelect, 'external_interface', _('External Interface'), _('The external interface of the device, if bypass mode, do not choose.')); o.rmempty = true; @@ -333,8 +356,6 @@ return view.extend({ o.rmempty = false; o.datatype = 'string'; o.optional = false; - o.depends('auth_server_mode', 'cloud'); - o.depends('auth_server_mode', 'bypass'); o = s.taboption('basic', form.Value, 'local_portal', _('Local Portal'), _('The local portal url.')); @@ -479,84 +500,6 @@ return view.extend({ o.default = '/wifidog/'; - o = ss.option(form.ListValue, 'long_conn_mode', _('Persistent Connection Mode'), - _('The persistent connection mode of the device to auth server.')); - o.value('ws', _('WebSocket Connection Mode')); - o.value('wss', _('WebSocket Secure Connection Mode')); - o.value('mqtt', _('MQTT Connection Mode')); - o.value('mqtts', _('MQTT Secure Connection Mode')) - o.rmempty = false; - o.defaulValue = 'wss'; - o.optional = false; - o.modalonly = true; - - - o = ss.option(form.Value, 'ws_server_hostname', _('WebSocket Hostname'), - _('The hostname of the websocket, if the field is left empty, automatically use the same hostname as the auth server.')); - o.datatype = 'or(host,ip4addr)'; - o.rmempty = true; - o.optional = true; - o.depends('long_conn_mode', 'ws'); - o.depends('long_conn_mode', 'wss'); - o.modalonly = true; - - o = ss.option(form.Value, 'ws_server_port', _('WebSocket Port'), - _('The port of the websocket, if the field is left empty, automatically use the same port as the auth server.')); - o.datatype = 'port'; - o.rmempty = true; - o.optional = true; - o.default = '443'; - o.depends('long_conn_mode', 'wss'); - o.depends('long_conn_mode', 'ws'); - o.modalonly = true; - - o = ss.option(form.Value, 'ws_server_path', _('WebSocket URI path'), - _('The URI path of the websocket.')); - o.datatype = 'string'; - o.rmempty = true; - o.optional = true; - o.default = '/ws/wifidogx'; - o.depends('long_conn_mode', 'ws'); - o.depends('long_conn_mode', 'wss'); - o.modalonly = true; - - o = ss.option(form.Value, 'mqtt_server_hostname', _('MQTT Hostname'), - _('The hostname of the mqtt.')); - o.datatype = 'or(host,ip4addr)'; - o.rmempty = true; - o.optional = true; - o.depends('long_conn_mode', 'mqtt'); - o.depends('long_conn_mode', 'mqtts'); - o.modalonly = true; - - o = ss.option(form.Value, 'mqtt_server_port', _('MQTT Port'), - _('The port of the mqtt.')); - o.datatype = 'port'; - o.rmempty = false; - o.optional = false; - o.depends('long_conn_mode', 'mqtt'); - o.depends('long_conn_mode', 'mqtts'); - o.default = 1883; - o.modalonly = true; - - o = ss.option(form.Value, 'mqtt_username', _('MQTT Username'), - _('The username of the mqtt.')); - o.datatype = 'string'; - o.rmempty = true; - o.optional = true; - o.depends('long_conn_mode', 'mqtt'); - o.depends('long_conn_mode', 'mqtts'); - o.modalonly = true; - - o = ss.option(form.Value, 'mqtt_password', _('MQTT Password'), - _('The password of the mqtt.')); - o.datatype = 'string'; - o.rmempty = true; - o.optional = true; - o.depends('long_conn_mode', 'mqtt'); - o.depends('long_conn_mode', 'mqtts'); - o.modalonly = true; - // QoS settings o = s.taboption('qos', form.Flag, 'enable_qos', _('Enable Global QoS'), _('Enable Global QoS.')); @@ -985,6 +928,87 @@ return view.extend({ return true; }; + // Long Connection Settings (长连接配置) + o = s.taboption('longconn', form.SectionValue, '_longconn', form.GridSection, 'longconn', + _('Persistent Connection Profiles'), + _('Create one or more long connection profiles for cloud authentication and OpenClaw intelligent control channels.')); + ss = o.subsection; + ss.addremove = true; + ss.nodescriptions = true; + + o = ss.option(form.ListValue, 'long_conn_mode', _('Connection Mode'), + _('The type of persistent connection to the remote management server.')); + o.value('ws', _('WebSocket (ws://)')); + o.value('wss', _('WebSocket Secure (wss://)')); + o.value('mqtt', _('MQTT')); + o.value('mqtts', _('MQTT Secure')); + o.default = 'ws'; + o.rmempty = false; + + o = ss.option(form.Value, 'ws_server_hostname', _('WebSocket Hostname'), + _('The hostname or IP address of the WebSocket server.')); + o.datatype = 'or(host,ip4addr)'; + o.rmempty = true; + o.depends('long_conn_mode', 'ws'); + o.depends('long_conn_mode', 'wss'); + o.placeholder = 'ws.example.com'; + o.modalonly = true; + + o = ss.option(form.Value, 'ws_server_port', _('WebSocket Port'), + _('The port of the WebSocket server.')); + o.datatype = 'port'; + o.rmempty = true; + o.default = '443'; + o.depends('long_conn_mode', 'ws'); + o.depends('long_conn_mode', 'wss'); + o.modalonly = true; + + o = ss.option(form.Value, 'ws_server_path', _('WebSocket Path'), + _('The URI path for the WebSocket connection.')); + o.datatype = 'string'; + o.rmempty = true; + o.default = '/ws/wifidogx'; + o.depends('long_conn_mode', 'ws'); + o.depends('long_conn_mode', 'wss'); + o.modalonly = true; + + o = ss.option(form.Value, 'mqtt_server_hostname', _('MQTT Hostname'), + _('The hostname or IP address of the MQTT broker.')); + o.datatype = 'or(host,ip4addr)'; + o.rmempty = true; + o.depends('long_conn_mode', 'mqtt'); + o.depends('long_conn_mode', 'mqtts'); + o.placeholder = 'mqtt.example.com'; + o.modalonly = true; + + o = ss.option(form.Value, 'mqtt_server_port', _('MQTT Port'), + _('The port of the MQTT broker.')); + o.datatype = 'port'; + o.rmempty = true; + o.default = '1883'; + o.depends('long_conn_mode', 'mqtt'); + o.depends('long_conn_mode', 'mqtts'); + o.modalonly = true; + + o = ss.option(form.Value, 'mqtt_username', _('MQTT Username'), + _('The username for MQTT authentication.')); + o.datatype = 'string'; + o.rmempty = true; + o.optional = true; + o.depends('long_conn_mode', 'mqtt'); + o.depends('long_conn_mode', 'mqtts'); + o.modalonly = true; + + o = ss.option(form.Value, 'mqtt_password', _('MQTT Password'), + _('The password for MQTT authentication.')); + o.datatype = 'string'; + o.rmempty = true; + o.optional = true; + o.password = true; + o.depends('long_conn_mode', 'mqtt'); + o.depends('long_conn_mode', 'mqtts'); + o.modalonly = true; + // rule settings o = s.taboption('rule', form.DynamicList, 'trusted_wildcard_domains', _('Trusted Wildcard Domains'), _('The trusted wildcard domains of the gateway')); diff --git a/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po b/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po index 7d4ce49f..868c0241 100644 --- a/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po +++ b/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po @@ -1035,3 +1035,61 @@ msgstr "未知错误" msgid "Unknown" msgstr "未知" + +msgid "Long Connection Profile" +msgstr "长连接配置" + +msgid "Select which long connection configuration to use. This option is always available regardless of authentication mode." +msgstr "选择使用的长连接配置。该选项在所有认证模式下都可用。" + +msgid "Please create a long connection profile first" +msgstr "请先创建一个长连接配置" + +msgid "Persistent Connection Profiles" +msgstr "持久连接配置" + +msgid "Create one or more long connection profiles for cloud authentication and OpenClaw intelligent control channels." +msgstr "创建长连接配置用于云认证和OpenClaw(龙虾)智能管控通道" + +msgid "Connection Mode" +msgstr "连接模式" + +msgid "The type of persistent connection to the remote management server." +msgstr "到远程管理服务器的持久连接类型。" + +msgid "WebSocket (ws://)" +msgstr "WebSocket (ws://)" + +msgid "WebSocket Secure (wss://)" +msgstr "WebSocket加密 (wss://)" + +msgid "MQTT Secure" +msgstr "MQTT安全" + +msgid "The hostname or IP address of the WebSocket server." +msgstr "WebSocket服务器的主机名或IP地址。" + +msgid "The port of the WebSocket server." +msgstr "WebSocket服务器的端口。" + +msgid "WebSocket Path" +msgstr "WebSocket路径" + +msgid "The URI path for the WebSocket connection." +msgstr "WebSocket连接的URI路径。" + +msgid "MQTT Hostname" +msgstr "MQTT主机名" + +msgid "The hostname or IP address of the MQTT broker." +msgstr "MQTT代理的主机名或IP地址。" + +msgid "The port of the MQTT broker." +msgstr "MQTT代理的端口。" + +msgid "The username for MQTT authentication." +msgstr "MQTT认证的用户名。" + +msgid "The password for MQTT authentication." +msgstr "MQTT认证的密码。" + diff --git a/luci-app-hypercpe/.gitattributes b/luci-app-hypercpe/.gitattributes deleted file mode 100644 index dfe07704..00000000 --- a/luci-app-hypercpe/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/luci-app-hypercpe/.gitignore b/luci-app-hypercpe/.gitignore deleted file mode 100644 index 6fd0a376..00000000 --- a/luci-app-hypercpe/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Compiled Lua sources -luac.out - -# luarocks build files -*.src.rock -*.zip -*.tar.gz - -# Object files -*.o -*.os -*.ko -*.obj -*.elf - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo -*.def -*.exp - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - diff --git a/luci-app-hypercpe/LICENSE b/luci-app-hypercpe/LICENSE deleted file mode 100644 index e62ec04c..00000000 --- a/luci-app-hypercpe/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ -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-hypercpe/Makefile b/luci-app-hypercpe/Makefile deleted file mode 100644 index e0bf113d..00000000 --- a/luci-app-hypercpe/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -#将openwrt顶层目录下的rules.mk文件中的内容导入进来 -include $(TOPDIR)/rules.mk -#软件包名 -PKG_NAME:=luci-app-hypercpe -#软件包版本 -PKG_VERSION:=1.2.0 -PKG_RELEASE:=1 -#真正编译当前软件包的目录 -PKG_BUILD_DIR:= $(BUILD_DIR)/$(PKG_NAME) - - - #将$(TOPDIR)/include目录下的package.mk文件中的内容导入进来 -include $(INCLUDE_DIR)/package.mk - -define Package/luci-app-hypercpe - PKGARCH:=all - SECTION:=wrtnode - CATEGORY:=luci - SUBMENU :=applications - TITLE:=luci-app-hypercpe - DEPENDS:=+sendat +luci-compat +kmod-usb-net +kmod-usb-net-cdc-ether +kmod-usb-acm \ - +kmod-usb-net-qmi-wwan +kmod-usb-net-rndis +kmod-usb-serial-qualcomm \ - +kmod-usb-net-sierrawireless +kmod-usb-ohci +kmod-usb-serial \ - +kmod-usb-serial-option +kmod-usb-wdm \ - +kmod-usb2 +kmod-usb3 \ - +quectel-CM-5G +kmod-usb-net-cdc-mbim -endef - -PKG_LICENSE:=GPLv3 -PKG_LINCESE_FILES:=LICENSE -PKF_MAINTAINER:=monologue - -include $(TOPDIR)/feeds/luci/luci.mk -# call BuildPackage - OpenWrt buildroot signature - diff --git a/luci-app-hypercpe/README-CN.md b/luci-app-hypercpe/README-CN.md deleted file mode 100644 index e69de29b..00000000 diff --git a/luci-app-hypercpe/README.md b/luci-app-hypercpe/README.md deleted file mode 100644 index 7ced5217..00000000 --- a/luci-app-hypercpe/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# luci-app-hypercpe -luci-app-hypercpe - -luci-app-hypercpe needs sendat to work - -git clone https://github.com/ouyangzq/sendat package/sendat - - diff --git a/luci-app-hypercpe/luasrc/controller/admin/cpe.lua b/luci-app-hypercpe/luasrc/controller/admin/cpe.lua deleted file mode 100644 index a1e874ea..00000000 --- a/luci-app-hypercpe/luasrc/controller/admin/cpe.lua +++ /dev/null @@ -1,165 +0,0 @@ -module("luci.controller.admin.cpe", package.seeall) - -I18N = require "luci.i18n" -translate = I18N.translate - -function index() - entry({"admin", "modem"}, firstchild(), translate("移动数据"), 25).dependent=false - entry({"admin", "modem", "nets"}, template("cpe/net_status"), translate("信号状态"), 0) - entry({"admin", "modem", "get_csq"}, call("action_get_csq")) - entry({"admin", "modem", "send_atcmd"}, call("action_send_atcmd")) - - -- entry({"admin", "modem", "sms"}, template("cpe/sms"), translate("短信信息"), 1) - -- entry({"admin", "modem", "band"}, template("cpe/band"), translate("锁频段/锁PCI"), 1) - entry({"admin", "modem", "at"}, template("cpe/at"), translate("AT工具"), 98) - - if not nixio.fs.access("/etc/config/modem") then - return - end - entry({"admin", "modem", "modem"}, cbi("cpe/modem"), _("模块设置"), 99) - -end - -function action_send_atcmd() - local rv ={} - local file - local p = luci.http.formvalue("p") - local set = luci.http.formvalue("set") - fixed = string.gsub(set, "\"", "~") - port= string.gsub(p, "\"", "~") - rv["at"] = fixed - rv["port"] = port - - os.execute("/usr/share/cpe/atcmd.sh \'" .. port .. "\' \'" .. fixed .. "\'") - result = "/tmp/result.at" - file = io.open(result, "r") - if file ~= nil then - rv["result"] = file:read("*all") - file:close() - else - rv["result"] = " " - end - os.execute("/usr/share/cpe/delatcmd.sh") - luci.http.prepare_content("application/json") - luci.http.write_json(rv) - -end - --- function action_get_csq() --- local file --- stat = "/tmp/cpe_cell.file" --- file = io.open(stat, "r") --- local rv ={} - - - --- rv["modem"] = file:read("*line") --- rv["conntype"] = file:read("*line") --- rv["modid"] = file:read("*line") --- rv["cops"] = file:read("*line") --- rv["port"] = file:read("*line") --- rv["tempur"] = file:read("*line") --- rv["proto"] = file:read("*line") --- file:read("*line") - - - --- rv["imei"] = file:read("*line") --- rv["imsi"] = file:read("*line") --- rv["iccid"] =file:read("*line") --- rv["phone"] = file:read("*line") --- file:read("*line") - - - --- rv["mode"] = file:read("*line") --- rv["csq"] = file:read("*line") --- rv["per"] = file:read("*line") --- rv["rssi"] = file:read("*line") --- rv["ecio"] = file:read("*line") --- rv["ecio1"] = file:read("*line") --- rv["rscp"] = file:read("*line") --- rv["rscp1"] = file:read("*line") --- rv["sinr"] = file:read("*line") --- rv["netmode"] = file:read("*line") --- file:read("*line") - --- rssi = rv["rssi"] --- ecio = rv["ecio"] --- rscp = rv["rscp"] --- ecio1 = rv["ecio1"] --- rscp1 = rv["rscp1"] --- if ecio == nil then --- ecio = "-" --- end --- if ecio1 == nil then --- ecio1 = "-" --- end --- if rscp == nil then --- rscp = "-" --- end --- if rscp1 == nil then --- rscp1 = "-" --- end - --- if ecio ~= "-" then --- rv["ecio"] = ecio .. " dB" --- end --- if rscp ~= "-" then --- rv["rscp"] = rscp .. " dBm" --- end --- if ecio1 ~= " " then --- rv["ecio1"] = " (" .. ecio1 .. " dB)" --- end --- if rscp1 ~= " " then --- rv["rscp1"] = " (" .. rscp1 .. " dBm)" --- end - --- rv["mcc"] = file:read("*line") --- rv["mnc"] = file:read("*line") --- rv["rnc"] = file:read("*line") --- rv["rncn"] = file:read("*line") --- rv["lac"] = file:read("*line") --- rv["lacn"] = file:read("*line") --- rv["cid"] = file:read("*line") --- rv["cidn"] = file:read("*line") --- rv["lband"] = file:read("*line") --- rv["channel"] = file:read("*line") --- rv["pci"] = file:read("*line") - --- rv["date"] = file:read("*line") - --- rv["crate"] = translate("快速(每5秒更新一次)") --- luci.http.prepare_content("application/json") --- luci.http.write_json(rv) --- end - -function action_get_csq() - local stat = "/tmp/cpe_cell.file" - local file = io.open(stat, "r") - - -- 检查文件是否成功打开 - if not file then - -- 处理文件打开失败的情况 - return - end - - local rv = {} - local keys = {"modem", "conntype", "modid", "cops", "port", "tempur", "proto", "skip", "imei", "imsi", "iccid", "phone", "skip", "mode", "csq", "per", "rssi", "ecio", "ecio1", "rscp", "rscp1", "sinr", "netmode", "skip", "mcc", "mnc", "rnc", "rncn", "lac", "lacn", "cid", "cidn", "lband", "channel", "pci", "date", "crate"} - - for i, key in ipairs(keys) do - local value = file:read("*line") - -- 跳过分隔行 - if key ~= "skip" then - rv[key] = value or "" -- 使用空字符串替换 nil(未设置或空行) - end - end - - file:close() - - -- 设置额外的字段 - rv["crate"] = translate("快速(每5秒更新一次)") - - luci.http.prepare_content("application/json") - luci.http.write_json(rv) -end \ No newline at end of file diff --git a/luci-app-hypercpe/luasrc/model/cbi/cpe/modem.lua b/luci-app-hypercpe/luasrc/model/cbi/cpe/modem.lua deleted file mode 100644 index 066ec7f0..00000000 --- a/luci-app-hypercpe/luasrc/model/cbi/cpe/modem.lua +++ /dev/null @@ -1,88 +0,0 @@ -local m, section,s2 - -m = Map("modem", translate("Mobile Network")) -m.description = translate("Modem Server For OpenWrt") - - section = m:section(TypedSection, "ndis", translate("SIM Settings"), translate("Automatic operation upon startup \r\n ooo")) - section.anonymous = true - section.addremove = false - section:tab("general", translate("General Setup")) - section:tab("advanced", translate("Advanced Settings")) - - - enable = section:taboption("general", Flag, "enabled", translate("Enable")) - enable.rmempty = false - - device = section:taboption("general",Value, "device", translate("Modem device")) - device.rmempty = false - local device_suggestions = nixio.fs.glob("/dev/cdc-wdm*") - if device_suggestions then - local node - for node in device_suggestions do - device:value(node) - end - end - apn = section:taboption("general", Value, "apn", translate("APN")) - username = section:taboption("general", Value, "username", translate("PAP/CHAP Username")) - password = section:taboption("general", Value, "password", translate("PAP/CHAP Password")) - password.password = true - pincode = section:taboption("general", Value, "pincode", translate("PIN Code")) - auth = section:taboption("general", Value, "auth", translate("Authentication Type")) - auth.rmempty = true - auth:value("", translate("-- Please choose --")) - auth:value("both", "PAP/CHAP (both)") - auth:value("pap", "PAP") - auth:value("chap", "CHAP") - auth:value("none", "NONE") - tool = section:taboption("general", Value, "tool", translate("Tools")) - tool:value("quectel-CM", "quectel-CM") - tool.rmempty = true - PdpType= section:taboption("general", Value, "pdptype", translate("PdpType")) - PdpType:value("IPV4", "IPV4") - PdpType:value("IPV6", "IPV6") - PdpType:value("IPV4V6", "IPV4V6") - PdpType.rmempty = true - - - - - - - - - -s2 = m:section(TypedSection, "ndis", translate("Network Diagnostics"),translate("Network exception handling: \ -check the network connection in a loop for 5 seconds. If the Ping IP address is not successful, After the network \ -exceeds the abnormal number, restart and search the registered network again.")) -s2.anonymous = true -s2.addremove = false - -en = s2:option(Flag, "en", translate("Enable")) -en.rmempty = false - - - -ipaddress= s2:option(Value, "ipaddress", translate("Ping IP address")) -ipaddress.default = "119.29.29.29" -ipaddress.rmempty=false - -an = s2:option(Value, "an", translate("Abnormal number")) -an.default = "15" -an:value("3", "3") -an:value("5", "5") -an:value("10", "10") -an:value("15", "15") -an:value("20", "20") -an:value("25", "25") -an:value("30", "30") -an.rmempty=false - - - -local apply = luci.http.formvalue("cbi.apply") -if apply then - -- io.popen("/etc/init.d/modeminit restart") - io.popen("/etc/init.d/modem restart") -end - -return m \ No newline at end of file diff --git a/luci-app-hypercpe/luasrc/view/cpe/at.htm b/luci-app-hypercpe/luasrc/view/cpe/at.htm deleted file mode 100644 index 1c9a8a7a..00000000 --- a/luci-app-hypercpe/luasrc/view/cpe/at.htm +++ /dev/null @@ -1,152 +0,0 @@ -<%+header%> -<% -local sys = require "luci.sys" -local utl = require "luci.util" -local fs = require "nixio.fs" -local uci = require "luci.model.uci".cursor() -local s = uci:get("custom", "bandlock", "enabled") -local a = uci:get("custom", "atcmd", "enabled") - -local multilock = uci:get("custom", "multiuser", "multi") or "0" -local rootlock = uci:get("custom", "multiuser", "root") or "0" -nomulti=1 -if (multilock == "0") or (multilock == "1" and rootlock == "1") then - nosms = 1 - if a == "1" then - nosms = 0 - end -else - nosms = 1 - nomulti = 0 -end -block = 1 -if s == "1" then - block = 0 -end - -function showicon(lck) -end - --%> - - - - -
-

<%:AT命令工具%>

-
<%:AT命令工具%>
- - - - -
- <%:AT命令终端%> - - - - - - - - - - - -
<%:模块端口 : %>
- -
<%:AT命令 : %>
- - -  
- - - -
- - - - -<%+footer%> diff --git a/luci-app-hypercpe/luasrc/view/cpe/net_status.htm b/luci-app-hypercpe/luasrc/view/cpe/net_status.htm deleted file mode 100644 index 9b4cf833..00000000 --- a/luci-app-hypercpe/luasrc/view/cpe/net_status.htm +++ /dev/null @@ -1,296 +0,0 @@ -<%+header%> -<% -local fs = require "nixio.fs" -nosms = 1 -if not fs.stat("/etc/nosim") then - nosms = 0 -end -havegps = 0 -if fs.stat("/etc/havegps") then - havegps = 1 -end --%> - - - - - -
-

<%:信号状态/模块信息%>

-
请注意该插件只支持移远模块,未来也不会支持其他模块 -
- - - - -
- <%:综合信息%> - - - - - - -
<%:模块 :%>-
<%:制造商 :%>
<%:温度 : %>
<%:更新时间 : %>
-
- -<% if nosms == 0 then %> -<% end %> - -
- <%:通信模块/SIM卡信息%> - - - - - - -
<%:运营商 : %>
<%:IMEI :%>
<%:IMSI : %>
<%:ICCID : %>
<%:SIM卡号码 : %>
-
- - -
- <%:信号状态%> - - - - - - - - - - -
<%:蜂窝网络类型 :%>
<%:CSQ : %>
<%:信号强度 : %>
<%:信号接收强度 RSSI : %>
<%:参考信号接收质量 RSRQ : %>
<%:参考信号接收功率 RSRP : %>
<%:信噪比 SINR : %>
<%:连接状态监控 : %>
-
- -
- <%:基站信息%> - - - - - - - - - - -
<%:MCC / MNC :%>
<%:eNB ID : %>
<%:TAC : %>
<%:Cell ID : %>
<%:频段 Band : %>
<%:频点 Channel : %>
<%:物理小区标识 PCI : %>
<%:最大Qos级别 Maximum Qos : %>
-
- - -<% if havegps == 1 then %> -
- <%:GPS 定位%> - - - - - - - - - - - -
<%:纬度 :%>
     
    <%:经度 :%>
       
      -
      -<% end %> - - -
      -<%+footer%> - diff --git a/luci-app-hypercpe/po/zh-cn/modem.po b/luci-app-hypercpe/po/zh-cn/modem.po deleted file mode 100644 index 8a10bd41..00000000 --- a/luci-app-hypercpe/po/zh-cn/modem.po +++ /dev/null @@ -1,71 +0,0 @@ -msgid "" -msgstr "" - -msgid "Mobile Network" -msgstr "模块设置" - - -msgid "Automatic operation upon startup ooo" -msgstr "开机自动启动:勾选" - -msgid "Tools" -msgstr "拨号工具" - -msgid "PAP/CHAP Username" -msgstr "PAP/CHAP 用户" - -msgid "PAP/CHAP Password" -msgstr "PAP/CHAP 密码" - - -msgid "Modem Server For OpenWrt" -msgstr "4G/5G模块管理" - - -msgid "SIM Settings" -msgstr "SIM 配置 (联通ANP:3gnet) (电信APN:ctnet) (移动APN:cmnet) (广电APN:cbnet) " - -msgid "PdpType" -msgstr "IP获取方式" - -msgid "AT Port Settings" -msgstr "AT 模块配置" - -msgid "Set tyyUSB port" -msgstr "AT 模块端口配置 (全模块指定端口)" - -msgid "tyyUSB port" -msgstr "ttyUSB 配置ID" - -msgid "Network Diagnostics" -msgstr "网络诊断" - - -msgid "Network Diagnostics" -msgstr "网络诊断" - - -msgid "Network exception handling: check the network connection in a loop for 5 seconds. If the Ping IP address is not successful, After the network exceeds the abnormal number, restart and search the registered network again." -msgstr "网络异常处理:循环检查网络连接5秒。如果Ping IP地址不成功,则在网络超过异常数量后,重新启动并搜索已注册的网络。" - -msgid "Ping IP address" -msgstr "Ping IP地址" - -msgid "Abnormal number" -msgstr "异常次数" - -msgid "Lock Band List" -msgstr "锁定频段列表" - -msgid "Server Type" -msgstr "服务类型" - - -msgid "[1]Automatic start upon startup: Check
      [2] FMFMFM650-CN module default ECM (36) mode. If not, please modify it, otherwise dialing cannot proceed normally
      " -msgstr "【1】 开机自动启动:勾选
      【2】 目前只支持移远模块,并且为QMI或者MBIM
      " - - - - - - diff --git a/luci-app-hypercpe/po/zh_Hans b/luci-app-hypercpe/po/zh_Hans deleted file mode 100644 index 41451e4a..00000000 --- a/luci-app-hypercpe/po/zh_Hans +++ /dev/null @@ -1 +0,0 @@ -zh-cn \ No newline at end of file diff --git a/luci-app-hypercpe/root/etc/config/modem b/luci-app-hypercpe/root/etc/config/modem deleted file mode 100644 index 40fd9287..00000000 --- a/luci-app-hypercpe/root/etc/config/modem +++ /dev/null @@ -1,19 +0,0 @@ - -config ndis - option enabled '1' - option bandlist '0' - option servertype '0' - option ipaddress '119.29.29.29' - option en '1' - option an '5' - option model 'nr5g' - option tyyusb '2' - option tool 'quectel-CM' - option device '/dev/cdc-wdm0' - option pdptype 'IPV4V6' - - - - - - diff --git a/luci-app-hypercpe/root/etc/init.d/cpeinit b/luci-app-hypercpe/root/etc/init.d/cpeinit deleted file mode 100755 index 046cb752..00000000 --- a/luci-app-hypercpe/root/etc/init.d/cpeinit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh /etc/rc.common - -START=99 -USE_PROCD=1 - -start_service() { - procd_open_instance - procd_set_param command /bin/sh /usr/share/cpe/rssi - procd_set_param respawn - procd_close_instance -} diff --git a/luci-app-hypercpe/root/etc/init.d/modem b/luci-app-hypercpe/root/etc/init.d/modem deleted file mode 100755 index 4afa1c85..00000000 --- a/luci-app-hypercpe/root/etc/init.d/modem +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/sh /etc/rc.common -# Copyright (C) 2006-2014 OpenWrt.org - -START=16 -STOP=16 -USE_PROCD=1 - -# 运行模块配置 -runModem() { - local enabled - config_get_bool enabled $1 enabled - if [ "$enabled" != "1" ]; then - echo "Modem not enabled, skipping." >> /tmp/log4g - return - fi - - echo "run runModem" >> /tmp/log4g - - local user password apn auth pincode device tool pdptype tty atcmd - config_get user $1 user - config_get password $1 password - config_get apn $1 apn - config_get auth $1 auth - config_get pincode $1 pincode - config_get device $1 device - config_get tool $1 tool - config_get pdptype $1 pdptype - config_get tty $1 tty - config_get atcmd $1 atcmd - - if [ -z "$device" ] || [ -z "$tool" ]; then - echo "Critical configuration missing: device or tool is not set." >> /tmp/log4g - return - fi - - case "$pdptype" in - "IPV4V6") pdptype='-4 -6' ;; - "IPV6") pdptype='-6' ;; - *) pdptype='' ;; - esac - - local devname=$(basename "$device") - local devpath=$(readlink -f "/sys/class/usbmisc/$devname/device/") - local ifname=$(ls "$devpath/net" 2>/dev/null) - - # 构建命令参数 - local cmd_params="" - if [ -n "$ifname" ]; then - cmd_params="-i $ifname" - fi - cmd_params="$cmd_params -s $apn $pdptype" - - if [ -n "$password" ]; then - cmd_params="$cmd_params $user $password $auth" - fi - - if [ -n "$pincode" ]; then - cmd_params="$cmd_params -p $pincode" - fi - - procd_open_instance - procd_set_param command $tool $cmd_params - procd_set_param respawn - echo "quectel-CM has started." - procd_close_instance -} - - -service_triggers() { - procd_add_reload_trigger "modem" -} - -start_service() { - config_load modem - config_foreach runModem ndis -} - -stop_service() { - echo "runModem stop" >> /tmp/log4g - killall quectel-CM # 考虑使用更精确的方法来停止服务 - echo "quectel-CM has stopped." -} - -reload_service() { - restart -} diff --git a/luci-app-hypercpe/root/etc/uci-defaults/luci-modem b/luci-app-hypercpe/root/etc/uci-defaults/luci-modem deleted file mode 100644 index 1ffc8964..00000000 --- a/luci-app-hypercpe/root/etc/uci-defaults/luci-modem +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -[ ! -f "/usr/share/ucitrack/luci-app-modem.json" ] && { - cat > /usr/share/ucitrack/luci-app-modem.json << EEOF -{ - "config": "modem", - "init": "modem" -} -EEOF -} - -uci -q batch <<-EOF >/dev/null - delete ucitrack.@modem[-1] - add ucitrack modem - set ucitrack.@modem[-1].init=modem - commit ucitrack -EOF - -rm -f /tmp/luci-indexcache -exit 0 \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/bin/bmask128 b/luci-app-hypercpe/root/usr/bin/bmask128 deleted file mode 100755 index 6cc67f42..00000000 --- a/luci-app-hypercpe/root/usr/bin/bmask128 +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -printf "Band 128-bit bandmask\n" - -# 生成 1 到 48 的频段掩码 -for LBAND in $(seq 1 48); do - BBAND=$((1 << (LBAND - 1))) - printf "%-6s%016X%016X\n" "$LBAND" "0" "$BBAND" -done - -# 生成 65 到 85 的频段掩码 -for LBAND in $(seq 65 85); do - BBAND=$((1 << (LBAND - 65))) - printf "%-6s%016X%016X\n" "$LBAND" "$BBAND" "0" -done diff --git a/luci-app-hypercpe/root/usr/bin/chan2band.sh b/luci-app-hypercpe/root/usr/bin/chan2band.sh deleted file mode 100755 index 5b5bff02..00000000 --- a/luci-app-hypercpe/root/usr/bin/chan2band.sh +++ /dev/null @@ -1,317 +0,0 @@ -#!/bin/sh -CHAN=$1 -CHAN=$(echo "$CHAN" | grep -o "[0-9]*") - -decode_lte() { - if [ $CHAN -lt 600 ]; then - BAND="B1" - elif [ $CHAN -lt 1200 ]; then - BAND="B2" - elif [ $CHAN -lt 1950 ]; then - BAND="B3" - elif [ $CHAN -lt 2400 ]; then - BAND="B4" - elif [ $CHAN -lt 2650 ]; then - BAND="B5" - elif [ $CHAN -lt 2750 ]; then - BAND="B6" - elif [ $CHAN -lt 3450 ]; then - BAND="B7" - elif [ $CHAN -lt 3800 ]; then - BAND="B8" - elif [ $CHAN -lt 4150 ]; then - BAND="B9" - elif [ $CHAN -lt 4750 ]; then - BAND="B10" - elif [ $CHAN -lt 4950 ]; then - BAND="B11" - elif [ $CHAN -lt 5010 ]; then - BAND="-" - elif [ $CHAN -lt 5180 ]; then - BAND="B12" - elif [ $CHAN -lt 5280 ]; then - BAND="B13" - elif [ $CHAN -lt 5380 ]; then - BAND="B14" - elif [ $CHAN -lt 5730 ]; then - BAND="-" - elif [ $CHAN -lt 5850 ]; then - BAND="B17" - elif [ $CHAN -lt 6000 ]; then - BAND="B18" - elif [ $CHAN -lt 6150 ]; then - BAND="B19" - elif [ $CHAN -lt 6450 ]; then - BAND="B20" - elif [ $CHAN -lt 6600 ]; then - BAND="B21" - elif [ $CHAN -lt 7400 ]; then - BAND="B22" - elif [ $CHAN -lt 7500 ]; then - BAND="-" - elif [ $CHAN -lt 7700 ]; then - BAND="B23" - elif [ $CHAN -lt 8040 ]; then - BAND="B24" - elif [ $CHAN -lt 8690 ]; then - BAND="B25" - elif [ $CHAN -lt 9040 ]; then - BAND="B26" - elif [ $CHAN -lt 9210 ]; then - BAND="B27" - elif [ $CHAN -lt 9660 ]; then - BAND="B28" - elif [ $CHAN -lt 9770 ]; then - BAND="B29" - elif [ $CHAN -lt 9870 ]; then - BAND="B30" - elif [ $CHAN -lt 9920 ]; then - BAND="B31" - elif [ $CHAN -lt 10400 ]; then - BAND="B32" - elif [ $CHAN -lt 36000 ]; then - BAND="-" - elif [ $CHAN -lt 36200 ]; then - BAND="B33" - elif [ $CHAN -lt 36350 ]; then - BAND="B34" - elif [ $CHAN -lt 36950 ]; then - BAND="B35" - elif [ $CHAN -lt 37550 ]; then - BAND="B36" - elif [ $CHAN -lt 37750 ]; then - BAND="B37" - elif [ $CHAN -lt 38250 ]; then - BAND="B38" - elif [ $CHAN -lt 38650 ]; then - BAND="B39" - elif [ $CHAN -lt 39650 ]; then - BAND="B40" - elif [ $CHAN -lt 41590 ]; then - BAND="B41" - elif [ $CHAN -lt 43590 ]; then - BAND="B42" - elif [ $CHAN -lt 45590 ]; then - BAND="B43" - elif [ $CHAN -lt 46590 ]; then - BAND="B44" - elif [ $CHAN -lt 46790 ]; then - BAND="B45" - elif [ $CHAN -lt 54540 ]; then - BAND="B46" - elif [ $CHAN -lt 55240 ]; then - BAND="B47" - elif [ $CHAN -lt 56740 ]; then - BAND="B48" - elif [ $CHAN -lt 58240 ]; then - BAND="B49" - elif [ $CHAN -lt 59090 ]; then - BAND="B50" - elif [ $CHAN -lt 59140 ]; then - BAND="B51" - elif [ $CHAN -lt 60140 ]; then - BAND="B52" - elif [ $CHAN -lt 60255 ]; then - BAND="B53" - elif [ $CHAN -lt 65536 ]; then - BAND="-" - elif [ $CHAN -lt 66436 ]; then - BAND="B65" - elif [ $CHAN -lt 67336 ]; then - BAND="B66" - elif [ $CHAN -lt 67536 ]; then - BAND="B67" - elif [ $CHAN -lt 67836 ]; then - BAND="B68" - elif [ $CHAN -lt 68336 ]; then - BAND="B69" - elif [ $CHAN -lt 68586 ]; then - BAND="B70" - elif [ $CHAN -lt 68936 ]; then - BAND="B71" - elif [ $CHAN -lt 68986 ]; then - BAND="B72" - elif [ $CHAN -lt 69036 ]; then - BAND="B73" - elif [ $CHAN -lt 69466 ]; then - BAND="B74" - elif [ $CHAN -lt 70316 ]; then - BAND="B75" - elif [ $CHAN -lt 70366 ]; then - BAND="B76" - elif [ $CHAN -lt 70546 ]; then - BAND="B85" - elif [ $CHAN -lt 70596 ]; then - BAND="B87" - elif [ $CHAN -lt 70646 ]; then - BAND="B88" - else - BAND="-" - fi -} - -decode_nr5g() { - if [ $CHAN -lt 123400 ]; then - BAND="-" - elif [ $CHAN -le 130400 ]; then - BAND="n71" - elif [ $CHAN -lt 143400 ]; then - BAND="-" - elif [ $CHAN -lt 145600 ]; then - BAND="n29" - elif [ $CHAN -eq 145600 ]; then - BAND="n29|n85" - elif [ $CHAN -lt 145800 ]; then - BAND="n85" - elif [ $CHAN -eq 145800 ]; then - BAND="n12|n85" - elif [ $CHAN -lt 147600 ]; then - BAND="n12|n85" - elif [ $CHAN -lt 149200 ]; then - BAND="n12|n67|n85" - elif [ $CHAN -eq 149200 ]; then - BAND="n12|n13|n67|n85" - elif [ $CHAN -le 151200 ]; then - BAND="n13|n67" - elif [ $CHAN -lt 151600 ]; then - BAND="n67" - elif [ $CHAN -eq 151600 ]; then - BAND="n14|n28|n67" - elif [ $CHAN -le 153600 ]; then - BAND="n14|n28" - elif [ $CHAN -lt 158200 ]; then - BAND="n28" - elif [ $CHAN -eq 158200 ]; then - BAND="n14|n20|n28" - elif [ $CHAN -le 160600 ]; then - BAND="n20|n28" - elif [ $CHAN -le 164200 ]; then - BAND="n20" - elif [ $CHAN -lt 171800 ]; then - BAND="-" - elif [ $CHAN -lt 172000 ]; then - BAND="n26" - elif [ $CHAN -lt 173800 ]; then - BAND="n18|n26" - elif [ $CHAN -le 175000 ]; then - BAND="n5|n18|n26" - elif [ $CHAN -le 178800 ]; then - BAND="n5|n26" - elif [ $CHAN -lt 185000 ]; then - BAND="-" - elif [ $CHAN -le 192000 ]; then - BAND="n8" - elif [ $CHAN -lt 285400 ]; then - BAND="-" - elif [ $CHAN -lt 286400 ]; then - BAND="n51|n76|n91|n93" - elif [ $CHAN -eq 286400 ]; then - BAND="n50|n51|n75|n76|n91|92|n93|94" - elif [ $CHAN -lt 295000 ]; then - BAND="n50|n75|n92|n94" - elif [ $CHAN -eq 295000 ]; then - BAND="n50|n74|n75|n92|n94" - elif [ $CHAN -le 303400 ]; then - BAND="n50|n74|n75|n92|n94" - elif [ $CHAN -le 303600 ]; then - BAND="n74" - elif [ $CHAN -lt 305000 ]; then - BAND="-" - elif [ $CHAN -le 311800 ]; then - BAND="n24" - elif [ $CHAN -lt 361000 ]; then - BAND="-" - elif [ $CHAN -lt 376000 ]; then - BAND="n3" - elif [ $CHAN -eq 376000 ]; then - BAND="n3|n39" - elif [ $CHAN -le 384000 ]; then - BAND="n39" - elif [ $CHAN -lt 386000 ]; then - BAND="-" - elif [ $CHAN -le 398000 ]; then - BAND="n2|n25" - elif [ $CHAN -lt 399000 ]; then - BAND="n25" - elif [ $CHAN -eq 399000 ]; then - BAND="n25|n70" - elif [ $CHAN -lt 402000 ]; then - BAND="n70" - elif [ $CHAN -eq 402000 ]; then - BAND="n34|n70" - elif [ $CHAN -le 404000 ]; then - BAND="n34|n70" - elif [ $CHAN -le 405000 ]; then - BAND="n34" - elif [ $CHAN -lt 422000 ]; then - BAND="-" - elif [ $CHAN -le 434000 ]; then - BAND="n1|n65|n66" - elif [ $CHAN -le 440000 ]; then - BAND="n65|n66" - elif [ $CHAN -lt 460000 ]; then - BAND="-" - elif [ $CHAN -lt 470000 ]; then - BAND="n40" - elif [ $CHAN -eq 470000 ]; then - BAND="n30|n40" - elif [ $CHAN -le 472000 ]; then - BAND="n30|n40" - elif [ $CHAN -le 480000 ]; then - BAND="n40" - elif [ $CHAN -lt 496700 ]; then - BAND="-" - elif [ $CHAN -le 499000 ]; then - BAND="n53" - elif [ $CHAN -lt 499200 ]; then - BAND="-" - elif [ $CHAN -lt 514000 ]; then - BAND="n41|n90" - elif [ $CHAN -eq 514000 ]; then - BAND="n38|n41|n90" - elif [ $CHAN -lt 524000 ]; then - BAND="n38|n41|n90" - elif [ $CHAN -eq 524000 ]; then - BAND="n7|n38|n41|n90" - elif [ $CHAN -lt 538000 ]; then - BAND="n7|n41|n90" - elif [ $CHAN -eq 538000 ]; then - BAND="n7|n90" - elif [ $CHAN -lt 620000 ]; then - BAND="-" - elif [ $CHAN -lt 636667 ]; then - BAND="n77|n78" - elif [ $CHAN -le 646666 ]; then - BAND="n48|n77|n78" - elif [ $CHAN -le 653333 ]; then - BAND="n77|n78" - elif [ $CHAN -le 680000 ]; then - BAND="n77" - elif [ $CHAN -lt 693334 ]; then - BAND="-" - elif [ $CHAN -le 733333 ]; then - BAND="n79" - elif [ $CHAN -lt 743333 ]; then - BAND="-" - elif [ $CHAN -lt 795000 ]; then - BAND="n46" - elif [ $CHAN -eq 795000 ]; then - BAND="n46|n96" - elif [ $CHAN -le 875000 ]; then - BAND="n96" - else - BAND="-" - fi -} - -if [ -z "$CHAN" ]; then - BAND="-" -elif [ "$CHAN" -lt 123400 ]; then - decode_lte -elif [ "$CHAN" -le 875000 ]; then - decode_nr5g -else - BAND="-" -fi -echo $BAND -exit diff --git a/luci-app-hypercpe/root/usr/bin/encodemask b/luci-app-hypercpe/root/usr/bin/encodemask deleted file mode 100755 index 02edcf0a..00000000 --- a/luci-app-hypercpe/root/usr/bin/encodemask +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/lua - -local MAX_BAND = 128 -local BIT_WIDTH = 4 -local mtab = {} -local vtab = {1, 2, 4, 8} - --- 初始化 mtab -for i = 1, 32 do - mtab[i] = 0 -end - --- 检查和处理每个参数 -for _, band in ipairs(arg) do - local band_num = tonumber(band) - if band_num and band_num <= MAX_BAND then - local idx = math.floor((band_num - 1) / BIT_WIDTH) + 1 - local idxr = 33 - idx - local val = vtab[(band_num - ((idx - 1) * BIT_WIDTH))] - mtab[idxr] = mtab[idxr] + val - else - print("Invalid input: " .. band) - end -end - --- 转换 mtab 到十六进制字符串 -for i = 1, 32 do - mtab[i] = string.format("%X", mtab[i]) -end - -print(table.concat(mtab)) diff --git a/luci-app-hypercpe/root/usr/bin/jkillall b/luci-app-hypercpe/root/usr/bin/jkillall deleted file mode 100755 index 121ce7d9..00000000 --- a/luci-app-hypercpe/root/usr/bin/jkillall +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# 检查参数 -if [ -z "$1" ]; then - echo "Usage: $0 " - exit 1 -fi - -LOOKFOR="$1" - -# 使用 pgrep 来更精确地查找进程 -# pgrep 默认匹配整个进程名称 -# -f 选项使其匹配命令行参数 -# 使用 -o 选项以获得最早启动的进程的 PID -# 你可以根据需要删除 -o,以杀死所有匹配的进程 -PID=$(pgrep -fo "$LOOKFOR") - -# 检查是否找到了进程 -if [ -z "$PID" ]; then - echo "No process found matching '$LOOKFOR'" - exit 1 -fi - -# 杀死进程 -kill "$PID" || { - echo "Failed to kill process with PID $PID" - exit 1 -} - -echo "Process $PID killed successfully" diff --git a/luci-app-hypercpe/root/usr/bin/rsrp2rssi b/luci-app-hypercpe/root/usr/bin/rsrp2rssi deleted file mode 100755 index f1de6f12..00000000 --- a/luci-app-hypercpe/root/usr/bin/rsrp2rssi +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/lua - --- 参数验证 -if #arg < 2 then - print("Usage: script ") - os.exit(1) -end - -rsrp = tonumber(arg[1]) -bw = tonumber(arg[2]) - --- 检查参数是否正确转换为数字 -if rsrp == nil or bw == nil then - print("Error: Invalid input. Both rsrp and bw should be numbers.") - os.exit(1) -end - --- 根据带宽计算 n 的值 -local n -if bw == 1.4 then - n = 6 -else - n = bw * 5 -end - --- 计算 RSSI -local rssi = rsrp + (10 * math.log10(n * 12)) - --- 限制 RSSI 的范围 -if rssi < -113 then - rssi = -113 -elseif rssi > -51 then - rssi = -51 -end - -print(math.floor(rssi)) diff --git a/luci-app-hypercpe/root/usr/bin/set_gpio b/luci-app-hypercpe/root/usr/bin/set_gpio deleted file mode 100755 index 5fddfa7e..00000000 --- a/luci-app-hypercpe/root/usr/bin/set_gpio +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -PIN=$1 -VALUE=$2 - -PIN_FILE=/sys/class/gpio/gpio$PIN - -if [ -z "$PIN" -o -z "$VALUE" ]; then - exit 1 -fi - -echo $PIN >/sys/class/gpio/export - -if [ $(cat $PIN_FILE/direction) = "out" ]; then - echo $VALUE >$PIN_FILE/value -fi - -echo $PIN >/sys/class/gpio/unexport \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/share/cpe/Quectel b/luci-app-hypercpe/root/usr/share/cpe/Quectel deleted file mode 100644 index 768f9fe7..00000000 --- a/luci-app-hypercpe/root/usr/share/cpe/Quectel +++ /dev/null @@ -1,507 +0,0 @@ -#!/bin/sh -ATPORT=2 -#Quectel -lte_bw() { - BW=$(echo $BW | grep -o "[0-5]\{1\}") - case $BW in - "0") - BW="1.4" ;; - "1") - BW="3" ;; - "2"|"3"|"4"|"5") - BW=$((($(echo $BW) - 1) * 5)) ;; - esac -} -#Quectel -nr_bw() { - BW=$(echo $BW | grep -o "[0-9]\{1,2\}") - case $BW in - "0"|"1"|"2"|"3"|"4"|"5") - BW=$((($(echo $BW) + 1) * 5)) ;; - "6"|"7"|"8"|"9"|"10"|"11"|"12") - BW=$((($(echo $BW) - 2) * 10)) ;; - "13") - BW="200" ;; - "14") - BW="400" ;; - esac -} - -#��ѯ��Ϣǿ�� -All_CSQ() -{ - Debug "All_CSQ" - #�ź� - OX=$( sendat $ATPORT "AT+CSQ" |grep "+CSQ:") - OX=$(echo $OX | tr 'a-z' 'A-Z') - CSQ=$(echo "$OX" | grep -o "+CSQ: [0-9]\{1,2\}" | grep -o "[0-9]\{1,2\}") - if [ $CSQ = "99" ]; then - CSQ="" - fi - if [ -n "$CSQ" ]; then - CSQ_PER=$(($CSQ * 100/31))"%" - CSQ_RSSI=$((2 * CSQ - 113))" dBm" - else - CSQ="-" - CSQ_PER="-" - CSQ_RSSI="-" - fi -} - -Quectel_SIMINFO() -{ - Debug "Quectel_SIMINFO" - # ��ȡIMEI - IMEI=$( sendat $ATPORT "AT+CGSN" | sed -n '2p' ) - # ��ȡIMSI - IMSI=$( sendat $ATPORT "AT+CIMI" | sed -n '2p' ) - # ��ȡICCID - ICCID=$( sendat $ATPORT "AT+ICCID" | grep -o "+ICCID:[ ]*[-0-9]\+" | grep -o "[-0-9]\{1,4\}" ) - # ��ȡ�绰���� - phone=$( sendat $ATPORT "AT+CNUM" | grep "+CNUM:" ) - -} -# SIMCOM��ȡ��վ��Ϣ -Quectel_Cellinfo() -{ - # return - #cellinfo0.gcom - OX1=$( sendat $ATPORT "AT+COPS=3,0;+COPS?") - OX2=$( sendat $ATPORT "AT+COPS=3,2;+COPS?") - OX=$OX1" "$OX2 - - #cellinfo.gcom - OY1=$( sendat $ATPORT "AT+CREG=2;+CREG?;+CREG=0") - OY2=$( sendat $ATPORT "AT+CEREG=2;+CEREG?;+CEREG=0") - OY3=$( sendat $ATPORT "AT+C5GREG=2;+C5GREG?;+C5GREG=0") - OY=$OY1" "$OY2" "$OY3 - - - OXx=$OX - OX=$(echo $OX | tr 'a-z' 'A-Z') - OY=$(echo $OY | tr 'a-z' 'A-Z') - OX=$OX" "$OY - - #Debug "$OX" - #Debug "$OY" - - COPS="-" - COPS_MCC="-" - COPS_MNC="-" - COPSX=$(echo $OXx | grep -o "+COPS: [01],0,.\+," | cut -d, -f3 | grep -o "[^\"]\+") - - if [ "x$COPSX" != "x" ]; then - COPS=$COPSX - fi - - COPSX=$(echo $OX | grep -o "+COPS: [01],2,.\+," | cut -d, -f3 | grep -o "[^\"]\+") - - if [ "x$COPSX" != "x" ]; then - COPS_MCC=${COPSX:0:3} - COPS_MNC=${COPSX:3:3} - if [ "$COPS" = "-" ]; then - COPS=$(awk -F[\;] '/'$COPS'/ {print $2}' $ROOTER/signal/mccmnc.data) - [ "x$COPS" = "x" ] && COPS="-" - fi - fi - - if [ "$COPS" = "-" ]; then - COPS=$(echo "$O" | awk -F[\"] '/^\+COPS: 0,0/ {print $2}') - if [ "x$COPS" = "x" ]; then - COPS="-" - COPS_MCC="-" - COPS_MNC="-" - fi - fi - COPS_MNC=" "$COPS_MNC - - OX=$(echo "${OX//[ \"]/}") - CID="" - CID5="" - RAT="" - REGV=$(echo "$OX" | grep -o "+C5GREG:2,[0-9],[A-F0-9]\{2,6\},[A-F0-9]\{5,10\},[0-9]\{1,2\}") - if [ -n "$REGV" ]; then - LAC5=$(echo "$REGV" | cut -d, -f3) - LAC5=$LAC5" ($(printf "%d" 0x$LAC5))" - CID5=$(echo "$REGV" | cut -d, -f4) - CID5L=$(printf "%010X" 0x$CID5) - RNC5=${CID5L:1:6} - RNC5=$RNC5" ($(printf "%d" 0x$RNC5))" - CID5=${CID5L:7:3} - CID5="Short $(printf "%X" 0x$CID5) ($(printf "%d" 0x$CID5)), Long $(printf "%X" 0x$CID5L) ($(printf "%d" 0x$CID5L))" - RAT=$(echo "$REGV" | cut -d, -f5) - fi - REGV=$(echo "$OX" | grep -o "+CEREG:2,[0-9],[A-F0-9]\{2,4\},[A-F0-9]\{5,8\}") - REGFMT="3GPP" - if [ -z "$REGV" ]; then - REGV=$(echo "$OX" | grep -o "+CEREG:2,[0-9],[A-F0-9]\{2,4\},[A-F0-9]\{1,3\},[A-F0-9]\{5,8\}") - REGFMT="SW" - fi - if [ -n "$REGV" ]; then - LAC=$(echo "$REGV" | cut -d, -f3) - LAC=$(printf "%04X" 0x$LAC)" ($(printf "%d" 0x$LAC))" - if [ $REGFMT = "3GPP" ]; then - CID=$(echo "$REGV" | cut -d, -f4) - else - CID=$(echo "$REGV" | cut -d, -f5) - fi - CIDL=$(printf "%08X" 0x$CID) - RNC=${CIDL:1:5} - RNC=$RNC" ($(printf "%d" 0x$RNC))" - CID=${CIDL:6:2} - CID="Short $(printf "%X" 0x$CID) ($(printf "%d" 0x$CID)), Long $(printf "%X" 0x$CIDL) ($(printf "%d" 0x$CIDL))" - - else - REGV=$(echo "$OX" | grep -o "+CREG:2,[0-9],[A-F0-9]\{2,4\},[A-F0-9]\{2,8\}") - if [ -n "$REGV" ]; then - LAC=$(echo "$REGV" | cut -d, -f3) - CID=$(echo "$REGV" | cut -d, -f4) - if [ ${#CID} -gt 4 ]; then - LAC=$(printf "%04X" 0x$LAC)" ($(printf "%d" 0x$LAC))" - CIDL=$(printf "%08X" 0x$CID) - RNC=${CIDL:1:3} - CID=${CIDL:4:4} - CID="Short $(printf "%X" 0x$CID) ($(printf "%d" 0x$CID)), Long $(printf "%X" 0x$CIDL) ($(printf "%d" 0x$CIDL))" - else - LAC="" - fi - else - LAC="" - fi - fi - REGSTAT=$(echo "$REGV" | cut -d, -f2) - if [ "$REGSTAT" == "5" -a "$COPS" != "-" ]; then - COPS_MNC=$COPS_MNC" (Roaming)" - fi - if [ -n "$CID" -a -n "$CID5" ] && [ "$RAT" == "13" -o "$RAT" == "10" ]; then - LAC="4G $LAC, 5G $LAC5" - CID="4G $CID
      5G $CID5" - RNC="4G $RNC, 5G $RNC5" - elif [ -n "$CID5" ]; then - LAC=$LAC5 - CID=$CID5 - RNC=$RNC5 - fi - if [ -z "$LAC" ]; then - LAC="-" - CID="-" - RNC="-" - fi -} - -#Quectel��˾���һ�վAT -Quectel_AT() -{ - Debug "Quectel_AT" - ATPORT - - Quectel_SIMINFO - All_CSQ - - Quectel_Cellinfo - - # - OX=$( sendat $ATPORT 'AT+QENG="servingcell"' | grep "+QENG:" ) - NR_NSA=$(echo $OX | grep -o -i "+QENG:[ ]\?\"NR5G-NSA\",") - NR_SA=$(echo $OX | grep -o -i "+QENG: \"SERVINGCELL\",[^,]\+,\"NR5G-SA\",\"[DFT]\{3\}\",") - if [ -n "$NR_NSA" ]; then - QENG=",,"$(echo $OX" " | grep -o -i "+QENG: \"LTE\".\+\"NR5G-NSA\"," | tr " " ",") - QENG5=$(echo $OX | grep -o -i "+QENG:[ ]\?\"NR5G-NSA\",[0-9]\{3\},[0-9]\{2,3\},[0-9]\{1,5\},-[0-9]\{2,5\},[-0-9]\{1,3\},-[0-9]\{2,3\},[0-9]\{1,7\},[0-9]\{1,3\}.\{1,6\}") - if [ -z "$QENG5" ]; then - QENG5=$(echo $OX | grep -o -i "+QENG:[ ]\?\"NR5G-NSA\",[0-9]\{3\},[0-9]\{2,3\},[0-9]\{1,5\},-[0-9]\{2,3\},[-0-9]\{1,3\},-[0-9]\{2,3\}") - if [ -n "$QENG5" ]; then - QENG5=$QENG5",," - fi - fi - elif [ -n "$NR_SA" ]; then - QENG=$(echo $NR_SA | tr " " ",") - QENG5=$(echo $OX | grep -o -i "+QENG: \"SERVINGCELL\",[^,]\+,\"NR5G-SA\",\"[DFT]\{3\}\",[ 0-9]\{3,4\},[0-9]\{2,3\},[0-9A-F]\{1,10\},[0-9]\{1,5\},[0-9A-F]\{2,6\},[0-9]\{6,7\},[0-9]\{1,3\},[0-9]\{1,2\},-[0-9]\{2,5\},-[0-9]\{2,3\},[-0-9]\{1,3\}") - else - QENG=$(echo $OX" " | grep -o -i "+QENG: [^ ]\+ " | tr " " ",") - fi - - - # Debug "$QENG" - # Debug "$QENG5" - - RAT=$(echo $QENG | cut -d, -f4 | grep -o "[-A-Z5]\{3,7\}") - case $RAT in - "GSM") - MODE="GSM" - ;; - "WCDMA") - MODE="WCDMA" - CHANNEL=$(echo $QENG | cut -d, -f9) - RSCP=$(echo $QENG | cut -d, -f12) - RSCP="-"$(echo $RSCP | grep -o "[0-9]\{1,3\}") - ECIO=$(echo $QENG | cut -d, -f13) - ECIO="-"$(echo $ECIO | grep -o "[0-9]\{1,3\}") - ;; - "LTE"|"CAT-M"|"CAT-NB") - MODE=$(echo $QENG | cut -d, -f5 | grep -o "[DFT]\{3\}") - if [ -n "$MODE" ]; then - MODE="$RAT $MODE" - else - MODE="$RAT" - fi - PCI=$(echo $QENG | cut -d, -f9) - CHANNEL=$(echo $QENG | cut -d, -f10) - LBAND=$(echo $QENG | cut -d, -f11 | grep -o "[0-9]\{1,3\}") - BW=$(echo $QENG | cut -d, -f12) - lte_bw - BWU=$BW - BW=$(echo $QENG | cut -d, -f13) - lte_bw - BWD=$BW - if [ -z "$BWD" ]; then - BWD="unknown" - fi - if [ -z "$BWU" ]; then - BWU="unknown" - fi - if [ -n "$LBAND" ]; then - LBAND="B"$LBAND" (Bandwidth $BWD MHz Down | $BWU MHz Up)" - fi - RSRP=$(echo $QENG | cut -d, -f15 | grep -o "[0-9]\{1,3\}") - if [ -n "$RSRP" ]; then - RSCP="-"$RSRP - RSRPLTE=$RSCP - fi - RSRQ=$(echo $QENG | cut -d, -f16 | grep -o "[0-9]\{1,3\}") - if [ -n "$RSRQ" ]; then - ECIO="-"$RSRQ - fi - RSSI=$(echo $QENG | cut -d, -f17 | grep -o "\-[0-9]\{1,3\}") - if [ -n "$RSSI" ]; then - CSQ_RSSI=$RSSI" dBm" - fi - SINRR=$(echo $QENG | cut -d, -f18 | grep -o "[0-9]\{1,3\}") - if [ -n "$SINRR" ]; then - if [ $SINRR -le 25 ]; then - SINR=$((($(echo $SINRR) * 2) -20))" dB" - fi - fi - - if [ -n "$NR_NSA" ]; then - MODE="LTE/NR EN-DC" - echo "0" > /tmp/modnetwork - if [ -n "$QENG5" ] && [ -n "$LBAND" ] && [ "$RSCP" != "-" ] && [ "$ECIO" != "-" ]; then - PCI="$PCI, "$(echo $QENG5 | cut -d, -f4) - SCHV=$(echo $QENG5 | cut -d, -f8) - SLBV=$(echo $QENG5 | cut -d, -f9) - BW=$(echo $QENG5 | cut -d, -f10 | grep -o "[0-9]\{1,3\}") - if [ -n "$SLBV" ]; then - LBAND=$LBAND"
      n"$SLBV - if [ -n "$BW" ]; then - nr_bw - LBAND=$LBAND" (Bandwidth $BW MHz)" - fi - if [ "$SCHV" -ge 123400 ]; then - CHANNEL=$CHANNEL", "$SCHV - else - CHANNEL=$CHANNEL", -" - fi - else - LBAND=$LBAND"
      nxx (unknown NR5G band)" - CHANNEL=$CHANNEL", -" - fi - RSCP=$RSCP" dBm
      "$(echo $QENG5 | cut -d, -f5) - SINRR=$(echo $QENG5 | cut -d, -f6 | grep -o "[0-9]\{1,3\}") - if [ -n "$SINRR" ]; then - if [ $SINRR -le 30 ]; then - SINR=$SINR"
      "$((($(echo $SINRR) * 2) -20))" dB" - fi - fi - ECIO=$ECIO" (4G) dB
      "$(echo $QENG5 | cut -d, -f7)" (5G) " - fi - fi - if [ -z "$LBAND" ]; then - LBAND="-" - else - if [ -n "$QCA" ]; then - QCA=$(echo $QCA | grep -o "\"S[CS]\{2\}\"[-0-9A-Z,\"]\+") - for QCAL in $(echo "$QCA"); do - if [ $(echo "$QCAL" | cut -d, -f7) = "2" ]; then - SCHV=$(echo $QCAL | cut -d, -f2 | grep -o "[0-9]\+") - SRATP="B" - if [ -n "$SCHV" ]; then - CHANNEL="$CHANNEL, $SCHV" - if [ "$SCHV" -gt 123400 ]; then - SRATP="n" - fi - fi - SLBV=$(echo $QCAL | cut -d, -f6 | grep -o "[0-9]\{1,2\}") - if [ -n "$SLBV" ]; then - LBAND=$LBAND"
      "$SRATP$SLBV - BWD=$(echo $QCAL | cut -d, -f3 | grep -o "[0-9]\{1,3\}") - if [ -n "$BWD" ]; then - UPDOWN=$(echo $QCAL | cut -d, -f13) - case "$UPDOWN" in - "UL" ) - CATYPE="CA"$(printf "\xe2\x86\x91") ;; - "DL" ) - CATYPE="CA"$(printf "\xe2\x86\x93") ;; - * ) - CATYPE="CA" ;; - esac - if [ $BWD -gt 14 ]; then - LBAND=$LBAND" ("$CATYPE", Bandwidth "$(($(echo $BWD) / 5))" MHz)" - else - LBAND=$LBAND" ("$CATYPE", Bandwidth 1.4 MHz)" - fi - fi - LBAND=$LBAND - fi - PCI="$PCI, "$(echo $QCAL | cut -d, -f8) - fi - done - fi - fi - if [ $RAT = "CAT-M" ] || [ $RAT = "CAT-NB" ]; then - LBAND="B$(echo $QENG | cut -d, -f11) ($RAT)" - fi - ;; - "NR5G-SA") - MODE="NR5G-SA" - if [ -n "$QENG5" ]; then - #AT+qnwcfg="NR5G_AMBR" #��ѯ�ٶ� - MODE="$RAT $(echo $QENG5 | cut -d, -f4)" - PCI=$(echo $QENG5 | cut -d, -f8) - CHANNEL=$(echo $QENG5 | cut -d, -f10) - LBAND=$(echo $QENG5 | cut -d, -f11) - BW=$(echo $QENG5 | cut -d, -f12) - nr_bw - LBAND="n"$LBAND" (Bandwidth $BW MHz)" - RSCP=$(echo $QENG5 | cut -d, -f13) - ECIO=$(echo $QENG5 | cut -d, -f14) - if [ "$CSQ_PER" = "-" ]; then - RSSI=$(rsrp2rssi $RSCP $BW) - CSQ_PER=$((100 - (($RSSI + 51) * 100/-62)))"%" - CSQ=$((($RSSI + 113) / 2)) - CSQ_RSSI=$RSSI" dBm" - fi - SINRR=$(echo $QENG5 | cut -d, -f15 | grep -o "[0-9]\{1,3\}") - if [ -n "$SINRR" ]; then - if [ $SINRR -le 30 ]; then - SINR=$((($(echo $SINRR) * 2) -20))" dB" - fi - fi - fi - ;; - esac - - - - - - # - OX=$( sendat $ATPORT "AT+QCAINFO" | grep "+QCAINFO:" ) - QCA=$(echo $OX" " | grep -o -i "+QCAINFO: \"S[CS]\{2\}\".\+NWSCANMODE" | tr " " ",") - - - # - OX=$( sendat $ATPORT 'AT+QCFG="nwscanmode"' | grep "+QCAINFO:" ) - QNSM=$(echo $OX | grep -o -i "+QCFG: \"NWSCANMODE\",[0-9]") - QNSM=$(echo "$QNSM" | grep -o "[0-9]") - if [ -n "$QNSM" ]; then - MODTYPE="6" - case $QNSM in - "0" ) - NETMODE="1" ;; - "1" ) - NETMODE="3" ;; - "2"|"5" ) - NETMODE="5" ;; - "3" ) - NETMODE="7" ;; - esac - fi - if [ -n "$QNWP" ]; then - MODTYPE="6" - case $QNWP in - "AUTO" ) - NETMODE="1" ;; - "WCDMA" ) - NETMODE="5" ;; - "LTE" ) - NETMODE="7" ;; - "LTE:NR5G" ) - NETMODE="8" ;; - "NR5G" ) - NETMODE="9" ;; - esac - fi - - - # - OX=$( sendat $ATPORT 'AT+QNWPREFCFG="mode_pref"' | grep "+QNWPREFCFG:" ) - QNWP=$(echo $OX | grep -o -i "+QNWPREFCFG: \"MODE_PREF\",[A-Z5:]\+" | cut -d, -f2) - - #�¶� - OX=$( sendat $ATPORT 'AT+QTEMP' | grep "+QTEMP:" ) - QTEMP=$(echo $OX | grep -o -i "+QTEMP: [0-9]\{1,3\}") - if [ -z "$QTEMP" ]; then - QTEMP=$(echo $OX | grep -o -i "+QTEMP:[ ]\?\"XO[_-]THERM[_-][^,]\+,[\"]\?[0-9]\{1,3\}" | grep -o "[0-9]\{1,3\}") - fi - if [ -z "$QTEMP" ]; then - QTEMP=$(echo $OX | grep -o -i "+QTEMP:[ ]\?\"MDM-CORE-USR.\+[0-9]\{1,3\}\"" | cut -d\" -f4) - fi - if [ -z "$QTEMP" ]; then - QTEMP=$(echo $OX | grep -o -i "+QTEMP:[ ]\?\"MDMSS.\+[0-9]\{1,3\}\"" | cut -d\" -f4) - fi - if [ -n "$QTEMP" ]; then - CTEMP=$(echo $QTEMP | grep -o -i "[0-9]\{1,3\}")$(printf "\xc2\xb0")"C" - fi - - - - # - OX=$( sendat $ATPORT "AT+QRSRP" | grep "+QRSRP:" ) - QRSRP=$(echo "$OX" | grep -o -i "+QRSRP:[^,]\+,-[0-9]\{1,5\},-[0-9]\{1,5\},-[0-9]\{1,5\}[^ ]*") - if [ -n "$QRSRP" ] && [ "$RAT" != "WCDMA" ]; then - QRSRP1=$(echo $QRSRP | cut -d, -f1 | grep -o "[-0-9]\+") - QRSRP2=$(echo $QRSRP | cut -d, -f2) - QRSRP3=$(echo $QRSRP | cut -d, -f3) - QRSRP4=$(echo $QRSRP | cut -d, -f4) - QRSRPtype=$(echo $QRSRP | cut -d, -f5) - if [ "$QRSRPtype" == "NR5G" ]; then - if [ -n "$NR_SA" ]; then - RSCP=$QRSRP1 - if [ -n "$QRPRP2" -a "$QRSRP2" != "-32768" ]; then - RSCP1="RxD "$QRSRP2 - fi - if [ -n "$QRSRP3" -a "$QRSRP3" != "-32768" ]; then - RSCP=$RSCP" dBm
      "$QRSRP3 - fi - if [ -n "$QRSRP4" -a "$QRSRP4" != "-32768" ]; then - RSCP1="RxD "$QRSRP4 - fi - else - RSCP=$RSRPLTE - if [ -n "$QRSRP1" -a "$QRSRP1" != "-32768" ]; then - RSCP=$RSCP" (4G) dBm
      "$QRSRP1 - if [ -n "$QRSRP2" -a "$QRSRP2" != "-32768" ]; then - RSCP="$RSCP,$QRSRP2" - if [ -n "$QRSRP3" -a "$QRSRP3" != "-32768" ]; then - RSCP="$RSCP,$QRSRP3" - if [ -n "$QRSRP4" -a "$QRSRP4" != "-32768" ]; then - RSCP="$RSCP,$QRSRP4" - fi - fi - RSCP=$RSCP" (5G) " - fi - fi - fi - elif [ "$QRSRP2$QRSRP3$QRSRP4" != "-44-44-44" -a -z "$QENG5" ]; then - RSCP=$QRSRP1 - if [ "$QRSRP3$QRSRP4" == "-140-140" -o "$QRSRP3$QRSRP4" == "-44-44" -o "$QRSRP3$QRSRP4" == "-32768-32768" ]; then - RSCP1="RxD "$(echo $QRSRP | cut -d, -f2) - else - RSCP=$RSCP" dBm (RxD "$QRSRP2" dBm)
      "$QRSRP3 - RSCP1="RxD "$QRSRP4 - fi - fi - fi - - -} \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/share/cpe/atcmd.sh b/luci-app-hypercpe/root/usr/share/cpe/atcmd.sh deleted file mode 100755 index f26a1c9a..00000000 --- a/luci-app-hypercpe/root/usr/share/cpe/atcmd.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# 将 $2 中的所有单引号替换为双引号 - -# 发送处理后的命令 -rec=$(sendat $1 $2) - -# 将结果写入文件 -echo "$rec" >> /tmp/result.at \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/share/cpe/cpedebug b/luci-app-hypercpe/root/usr/share/cpe/cpedebug deleted file mode 100644 index 2254a4b3..00000000 --- a/luci-app-hypercpe/root/usr/share/cpe/cpedebug +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -en=1 #调试开关:0关闭,1打开 ,2输出到文件 -outfile="/tmp/cpe.log" #输出文件 -#日志信息 -Debug() { - tim=$(date "+%Y-%m-%d %H:%M:%S") # 获取系统时间 - if [ "$en" -eq 1 ]; then - echo "$tim $1" # 打印输出 - elif [ "$en" -eq 2 ]; then - echo "$tim $1" >> "$outfile" # 输出到文件 - fi -} - -ATPORT() -{ - MODEMNAME="ALL" - ATPORT=2 -} \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/share/cpe/delatcmd.sh b/luci-app-hypercpe/root/usr/share/cpe/delatcmd.sh deleted file mode 100755 index dd131f0a..00000000 --- a/luci-app-hypercpe/root/usr/share/cpe/delatcmd.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -rm -f /tmp/result.at \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/share/cpe/rssi b/luci-app-hypercpe/root/usr/share/cpe/rssi deleted file mode 100644 index 1e017bb8..00000000 --- a/luci-app-hypercpe/root/usr/share/cpe/rssi +++ /dev/null @@ -1,332 +0,0 @@ -#!/bin/sh -source /usr/share/cpe/cpedebug -#source /usr/share/cpe/SIMCOM -source /usr/share/cpe/Quectel -#source /usr/share/cpe/Fibocom - -#初值化数据结构 -InitData() -{ - Date='' - CHANNEL="-" - ECIO="-" - RSCP="-" - ECIO1=" " - RSCP1=" " - NETMODE="-" - LBAND="-" - PCI="-" - CTEMP="-" - MODE="-" - SINR="-" - IMEI='-' - IMSI='-' - ICCID='-' - phone='-' - conntype='' - Model='' - - -} -#写数据 -SETData() -{ - { - echo $Model #'RM520N-GL' - echo $conntype #'conntype' - echo $quectel_device_id - echo $COPS #运营商 - echo 'ttyUSB2' #端口 - echo $CTEMP #温度 - echo 'QMI' #协议 - echo '---------------------------------' - echo $IMEI #imei - echo $IMSI #imsi - echo $ICCID #iccid - echo $phone #phone - echo '---------------------------------' - - echo $MODE - echo $CSQ - echo $CSQ_PER - echo $CSQ_RSSI - echo $ECIO #参考信号接收质量 RSRQ ecio - echo $ECIO1 #参考信号接收质量 RSRQ ecio1 - echo $RSCP #参考信号接收功率 RSRP rscp0 - echo $RSCP1 #参考信号接收功率 RSRP rscp1 - echo $SINR #信噪比 SINR rv["sinr"] - echo $NETMODE #连接状态监控 rv["netmode"] - echo '---------------------------------' - - - echo $COPS_MCC #MCC - echo $$COPS_MNC #MNC - echo $LAC #eNB ID - echo '' #LAC_NUM - echo $RNC #TAC - echo '' #RNC_NUM - echo $CID - echo '' #CID_NUM - echo $LBAND - echo $CHANNEL - echo $PCI - - echo $Date - - - - echo $MODTYPE - echo $QTEMP - - } > /tmp/cpe_cell.file -} - - - -ATPORT=2 - -# 自动处理模块信号 -AUTO_CPE() { - Debug "------------------------------端口$ATPORT---------------------------" - Debug "AUTO_CPE" - Date=$(date "+%Y-%m-%d %H:%M:%S") - quectel_device_id=$(lsusb | grep -i "Quectel" | awk '{print $6}') - Debug "Quectel 设备 ID: $quectel_device_id" - # 检测设备是否准备好 - if sendat $ATPORT "AT" 500 | grep -q "OK"; then - sleep 1s - else - Debug "设备未准备好" - sleep 5s - return - fi - - Debug "读取模块信息 计算模块" - ATATI=$(sendat $ATPORT "ATI") - Getconntype=$(echo "$ATATI" | sed -n '2p') - - if [ -z "$Getconntype" ]; then - Debug "无法获取模块类型" - sleep 5s - return - fi - - conntype=$Getconntype - Model=$(echo "$ATATI" | sed -n '3p') - - # 检查 SIM 卡状态 - cpin=$(sendat $ATPORT "at+cpin?") - if echo "$cpin" | grep -q "ERROR"; then - Debug "No SIM or SIM Error" - sleep 5s - return - elif ! echo "$cpin" | grep -q "READY"; then - Debug "SIM Locked or Not Ready" - sleep 5s - return - else - Debug "SIM Ready" - fi - - # 根据模块类型执行相应命令或设置不支持的模块消息 - if echo $conntype | grep -qi "Quectel"; then - Quectel_AT - else - Debug "$Model" - fi -} - -#重新联网 -modem_reset() { - echo "Abnormal network restart" - - # 停止 modem 服务 - /etc/init.d/modem stop || echo "Failed to stop modem service." - killall quectel-CM - # 发送 AT 命令重置 modem - sendat $ATPORT "AT+CFUN=1,1" || echo "Failed to send AT+CFUN=1,1 command." - - # 等待 modem 重置 - sleep 10s - - # 无限循环,直到检测到 Quectel 设备 - while true; do - if lsusb | grep -qi "Quectel"; then - echo "Quectel device detected." - # 启动 modem 服务 - sleep 8s - /etc/init.d/modem start || echo "Failed to start modem service." - break # 检测到设备后跳出循环 - else - echo "Waiting for Quectel device..." - sleep 1s # 等待 1 秒后再次检测 - fi - done -} - -#检测SIM卡是否插入,10次检测不到则重启模块 -check_sim() { - while true; do - enabled=$(uci -q get modem.@ndis[0].enabled) - if [ "$enabled" -eq 1 ]; then - echo "Check the sim" - rec=$(sendat 2 "AT+CPIN?") - rec1=$(echo "$rec" | grep -c "READY") - if [ $rec1 -eq 1 ]; then - x=0 - echo "SIM is READY" - else - x=$((x + 1)) - if [ $x -eq 10 ]; then - echo "SIM abnormal restart" - modem_reset || echo "Failed to reset modem after SIM abnormality." - x=0 - fi - fi - fi - sleep 6s - done -} - - -#检测网络状态第一次开机 -check_firstdns() { - while true; do - enabled=$(uci -q get modem.@ndis[0].enabled) - en1=$(uci -q get modem.@ndis[0].en) - if [ "$enabled" -eq 1 ] && [ "$en1" -eq 1 ]; then - echo "------------------------------开启任务---------------------------" - urls="http://wifi.vivo.com.cn/generate_204 http://www.gstatic.com/generate_204" - success=0 - for url in $urls; do - if curl -4 --interface wwan0 -I "$url" 2>/dev/null | grep -q "HTTP/.* 204"; then - echo "网络连接正常" - success=1 - break - fi - done - - if [ $success -eq 1 ]; then - echo "网络连接正常" - xx=0 - return - else - echo "网络连接异常" - xx=$((xx + 1)) - if [ $xx -eq 5 ]; then - echo "Attempting to reset modem due to network connection issue." - modem_reset || echo "Failed to reset modem." - xx=0 - fi - fi - - echo "------------------------------结束任务---------------------------" - fi - sleep 1s - done -} - - - -#检测网络状态 -check_dns() { - en1=$(uci -q get modem.@ndis[0].en) - if [ "$en1" = '1' ]; then - echo "------------------------------开启任务---------------------------" - # 使用 curl 检查 HTTP 状态码 - urls="http://wifi.vivo.com.cn/generate_204 http://www.gstatic.com/generate_204" - success=0 - for url in $urls; do - if curl -4 --interface wwan0 -I "$url" 2>/dev/null | grep -q "HTTP/.* 204"; then - echo "网络连接正常" - success=1 - break - fi - done - - if [ $success -eq 0 ]; then - echo "网络连接异常" - xx=$((xx + 1)) - if [ $xx -eq $(uci -q get modem.@ndis[0].an) ]; then - xx=0 - modem_reset || echo "Modem reset failed." - fi - fi - echo "------------------------------结束任务---------------------------" - fi -} - -check_Task() { - while true; do - enabled=$(uci -q get modem.@ndis[0].enabled) - if [ "$enabled" = '1' ]; then - Debug "------------------------------开启任务---------------------------" - ATPORT || Debug "Failed to execute ATPORT." - AUTO_CPE || Debug "Failed to execute AUTO_CPE." - SETData || Debug "Failed to execute SETData." - check_dns || Debug "Failed to execute check_dns." - Debug "------------------------------结束任务---------------------------" - fi - sleep 5s - done -} - - -#注册网卡 -RegisterNetwork() -{ - Debug "RegisterNetwork 注册网卡 $MODEMNAME" - - # 设置 wwan5g 网络 - if [ "$(uci -q get network.wwan5g.interface)" != "interface" ]; then - uci set network.wwan5g=interface - uci set network.wwan5g.device='wwan0' - uci set network.wwan5g.proto=dhcp - uci commit network - uci set firewall.@zone[1].network="wan wan6 wwan5g wwan5g6 wlan" - uci commit firewall - /etc/init.d/network reload - fi - - # 设置 wwan5g6 网络 - if [ "$(uci -q get network.wwan5g6.interface)" != "interface" ]; then - uci set network.wwan5g6=interface - uci set network.wwan5g6.device='wwan0' - uci set network.wwan5g6.proto='dhcpv6' - uci set network.wwan5g6.reqaddress='try' - uci set network.wwan5g6.reqprefix='auto' - uci set network.wwan5g6._orig_bridge='false' - uci set network.wwan5g6.extendprefix='1' - uci commit network - uci set firewall.@zone[1].network="wan wan6 wwan wwan5g wwan5g6 wlan" - uci commit firewall - /etc/init.d/network reload - fi - - # 更新 wwan5g 和 wwan5g6 接口名称 - for iface in wwan5g wwan5g6; do - if [ "$(uci -q get network.$iface.device)" != "wwan0" ]; then - uci set network.$iface.device='wwan0' - uci commit network - /etc/init.d/network reload - fi - done -} - - - -first() { - Debug "开启RSSI服务" - InitData - Debug "初始化数据完成" - sleep 1s - ATPORT - RegisterNetwork - AUTO_CPE - SETData - modem_reset - check_firstdns - check_Task -} - -first \ No newline at end of file diff --git a/luci-app-hypercpe/root/usr/share/rpcd/acl.d/luci-app-hypercpe.json b/luci-app-hypercpe/root/usr/share/rpcd/acl.d/luci-app-hypercpe.json deleted file mode 100644 index 3dc23bba..00000000 --- a/luci-app-hypercpe/root/usr/share/rpcd/acl.d/luci-app-hypercpe.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "luci-app-hypercpe": { - "description": "Grant UCI access for luci-app-hypercpe", - "read": { - "uci": [ "modem" ] - }, - "write": { - "uci": [ "modem" ] - } - } -} diff --git a/luci-app-netspeedtest/LICENSE b/luci-app-netspeedtest/LICENSE index 0a65610b..7076a2a7 100644 --- a/luci-app-netspeedtest/LICENSE +++ b/luci-app-netspeedtest/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 sirpdboy +Copyright (c) 2022-2026 sirpdboy Hereby grant a license free of charge to anyone who obtains copies This software and related documentation files ("Software") are used to process diff --git a/luci-app-netspeedtest/Makefile b/luci-app-netspeedtest/Makefile index 23ead72c..232654c7 100644 --- a/luci-app-netspeedtest/Makefile +++ b/luci-app-netspeedtest/Makefile @@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-netspeedtest -PKG_VERSION:=5.2.0 -PKG_RELEASE:=3 +PKG_VERSION:=5.2.1 +PKG_RELEASE:=4 LUCI_TITLE:=LuCI Support for netspeedtest LUCI_DEPENDS:=+ookla-speedtest +homebox +python3-light +python3-pkg-resources +python3-xml +python3-email \ diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js index 54f53392..ec05431f 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js @@ -49,20 +49,22 @@ function checkProcess(quick = false) { }); } } - function controlService(action, port) { if (action === 'start') { return fs.exec('/usr/bin/killall', ['homebox']) .catch(function() { return Promise.resolve(); }) .then(function() { - var command = 'nohup /usr/bin/homebox serve --port ' + port + ' > ' + logPath + ' 2>&1 &'; + var command = '/usr/bin/homebox serve --port ' + port + ' > ' + logPath + ' 2>&1 &'; return fs.exec('/bin/sh', ['-c', command]); }); } else { + fs.exec('/usr/bin/killall', ['homebox']); return fs.exec('/etc/init.d/netspeedtest', ['stop']); } } + + function saveConfiguration(newPort, enabled) { const uciContent = `config netspeedtest 'config' \toption homebox_port '${newPort}' @@ -174,14 +176,12 @@ return view.extend({ } toggleBtn.disabled = true; if (state.operationType === 'stop' && !state.running) { - console.log('Stop confirmed - operation complete'); state.operationInProgress = false; state.operationType = null; toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server'); toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply'); toggleBtn.disabled = false; } else if (state.operationType === 'start' && state.running) { - console.log('Start confirmed - operation complete'); state.operationInProgress = false; state.operationType = null; toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server'); @@ -234,16 +234,13 @@ return view.extend({ function doCheck() { checkProcess(true).then(function(isRunning) { if (action === 'stop' && !isRunning) { - console.log('Stop success after', Date.now() - startTime, 'ms'); resolve({ running: false }); } else if (action === 'start' && isRunning) { - console.log('Start success after', Date.now() - startTime, 'ms'); checkProcess().then(resolve).catch(resolve); } else if (checkCount < maxChecks) { checkCount++; setTimeout(doCheck, 200); } else { - console.log('Check timeout, using full check'); checkProcess().then(resolve).catch(resolve); } @@ -283,7 +280,6 @@ return view.extend({ .catch(function(err) { console.error('Service control error:', err); - // 发生错误时重新检查 checkProcess().then(function(res) { state.running = res.running || false; if (res.port) state.port = res.port; diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js index b6433020..b11f7399 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js @@ -21,7 +21,6 @@ async function checkProcess() { return { running: true, pid: res.stdout.trim() }; } - // 回退检查 const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'iperf3', '-o', 'pid=']); const pid = psRes.stdout.trim(); return { @@ -57,7 +56,6 @@ function saveConfiguration(newPort, enabled) { return fs.write('/etc/config/netspeedtest', uciContent) .then(() => { - // 更新开机自启 if (enabled) { return fs.exec('/etc/init.d/netspeedtest', ['enable']); } else { diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js index 1bfcd0f8..89224558 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js @@ -10,7 +10,7 @@ 'require ui'; 'require form'; -var Timeout = 300 * 1000; +var Timeout = 300 * 1000; var ResultFile = '/tmp/netspeedtest_result'; var SpeedtestScript = '/usr/bin/netspeedtest.sh'; @@ -81,6 +81,7 @@ return view.extend({ if (firstLine.match(/Download:/i) || firstLine.match(/Upload:/i)) { return { type: 'speed', data: content.join('\n') }; } + var hasDownload = false; var hasUpload = false; for (var j = 0; j < content.length; j++) { @@ -95,6 +96,22 @@ return view.extend({ return { type: 'unknown', data: content.join('\n') }; }, + startSpeedTest: function(version) { + return fs.write(ResultFile, 'Testing\n') + .then(function() { + var shellCmd = SpeedtestScript; + if (version) { + shellCmd += ' --version ' + version; + } + shellCmd += ' > /dev/null 2>&1 &'; + console.log('Starting test with sh -c:', shellCmd); + return fs.exec('/bin/ash', ['-c', shellCmd]) + .catch(function() { + return fs.exec('/bin/sh', ['-c', shellCmd]); + }); + }); + }, + poll_status(nodes, res) { var result_content = res[2] ? res[2].trim().split("\n") : []; var result_mtime = res[3] ? res[3].mtime * 1000 : 0; @@ -129,7 +146,6 @@ return view.extend({ if (!imageUrl.match(/\.png$/)) { imageUrl = imageUrl + '.png'; } - // 处理speedtest.net的特殊URL if (imageUrl.includes('speedtest.net/result/')) { imageUrl = imageUrl.replace('/result/c/', '/result/'); } @@ -137,7 +153,6 @@ return view.extend({ result_stat.innerHTML = "
      " + "" + "" + "
      " + "
      "; break; @@ -393,22 +408,11 @@ return view.extend({ } var version = versionSelect ? versionSelect.value : (has_ookla ? 'ookla' : 'python'); - return fs.write(ResultFile, 'Testing\n') + + self.startSpeedTest(version) .then(function() { - var cmd = 'nohup ' + SpeedtestScript; - if (version) { - cmd += ' --version ' + version; - } - cmd += ' > /dev/null 2>&1 &'; - - return fs.exec('/bin/sh', ['-c', cmd]); - }) - .then(function() { - ui.addNotification(null, E('p', _('Speed test started. Please wait...')), 'info'); }) .catch(function(e) { - console.error('Failed to start test:', e); - ui.addNotification(null, E('p', _('Failed to start speed test: ') + e.message), 'error'); }); return false; diff --git a/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po b/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po index 2bd1d491..73b6522f 100644 --- a/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po +++ b/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po @@ -145,6 +145,12 @@ msgstr "自动启动" msgid "Port Setting" msgstr "监听端口" -msgid "" -msgstr "" +msgid "Starting..." +msgstr "启动中" + +msgid "Stopping..." +msgstr "停止中" + +msgid "Test failed. Please check log for details." +msgstr "测速失败,请查看日志" diff --git a/luci-app-netspeedtest/root/etc/uci-defaults/zzz_luci-app-netspeedtest b/luci-app-netspeedtest/root/etc/uci-defaults/zzz_luci-app-netspeedtest index bb19560f..22e1baa3 100644 --- a/luci-app-netspeedtest/root/etc/uci-defaults/zzz_luci-app-netspeedtest +++ b/luci-app-netspeedtest/root/etc/uci-defaults/zzz_luci-app-netspeedtest @@ -1,6 +1,6 @@ #!/bin/sh -chmod +x /usr/bin/netspeedtest.sh /usr/bin/speedtest /usr/libexec/rpcd/luci.netspeedtest - +chmod +x /etc/init.d/netspeedtest /usr/bin/netspeedtest.sh /usr/bin/speedtest /usr/libexec/rpcd/luci.netspeedtest +/etc/init.d/rpcd restart rm -f /tmp/luci-indexcache exit 0 diff --git a/luci-app-openclawmgr/Makefile b/luci-app-openclawmgr/Makefile index 66d9dba6..2dceb9b7 100644 --- a/luci-app-openclawmgr/Makefile +++ b/luci-app-openclawmgr/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_VERSION:=0.1.7 -PKG_RELEASE:=8 +PKG_RELEASE:=9 LUCI_TITLE:=LuCI support for OpenClaw Launcher LUCI_PKGARCH:=all diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js index ee8cc459..55801e4b 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js @@ -107,34 +107,57 @@ openai: "openai/gpt-5.2", anthropic: "anthropic/claude-sonnet-4-6", "minimax-cn": "minimax-cn/MiniMax-M2.5", - moonshot: "moonshot/kimi-k2.5" + moonshot: "moonshot/kimi-k2.5", + "custom-provider": "custom-provider/custom-model" }[agent] || "anthropic/claude-sonnet-4-6"; } + function modelPrefixForAgent(agent) { + return { + openai: "openai", + anthropic: "anthropic", + "minimax-cn": "minimax-cn", + moonshot: "moonshot", + "custom-provider": "custom-provider" + }[agent] || "anthropic"; + } + + function normalizeModelValue(agent, value) { + var prefix = modelPrefixForAgent(agent); + value = String(value || "").replace(/^\s+|\s+$/g, ""); + if (!value) { + return modelForAgent(agent); + } + if (value.indexOf(prefix + "/") === 0) { + return value; + } + if (/^[^/]+\/.+/.test(value)) { + return prefix + "/" + value.replace(/^[^/]+\//, ""); + } + return prefix + "/" + value; + } + function modelMatchesAgent(agent, model) { model = String(model || ""); - return { - openai: /^openai\//, - anthropic: /^anthropic\//, - "minimax-cn": /^minimax-cn\//, - moonshot: /^moonshot\// - }[agent] ? ({ - openai: /^openai\//, - anthropic: /^anthropic\//, - "minimax-cn": /^minimax-cn\//, - moonshot: /^moonshot\// - }[agent]).test(model) : false; + return model.indexOf(modelPrefixForAgent(agent) + "/") === 0; } function resolveModelValue(form) { var agent = form && form.default_agent ? form.default_agent : "anthropic"; var value = form && form.default_model ? String(form.default_model) : ""; if (!value || !modelMatchesAgent(agent, value)) { - return modelForAgent(agent); + return normalizeModelValue(agent, value); } return value; } + function modelPlaceholder(agent) { + if (agent === "custom-provider") { + return "例如 gpt-5.1 或 custom-provider/gpt-5.1"; + } + return "请按照/格式填写"; + } + function statusText(status) { if (status.installing) return status.task_op === "upgrade" ? "更新中" : "安装中"; if (!status || !status.installed) return "未安装"; @@ -523,11 +546,14 @@ ["openai", "OpenAI"], ["anthropic", "Anthropic"], ["minimax-cn", "MiniMax CN"], - ["moonshot", "Moonshot CN"] - ]) + '
      如果你的供应商是兼容 OpenAI 等协议,可在下方填写中转地址
      ') + + ["moonshot", "Moonshot CN"], + ["custom-provider", "自定义供应商"] + ]) + '
      ' + escapeHtml(form.default_agent === "custom-provider" + ? "自定义供应商会写入独立的 custom-provider 配置,不会改动内置供应商。" + : "如果你的供应商兼容 OpenAI 协议,可选择“自定义供应商”并在下方填写地址与模型。") + '
      ') + fieldInput("API 密钥", passwordHtml("oclm-api-key", form.provider_api_key || "", "sk-...")) + - fieldInput("中转地址(可选)", '') + - fieldInput("默认模型", '') + + fieldInput(form.default_agent === "custom-provider" ? "服务地址" : "中转地址(可选)", '') + + fieldInput("默认模型", '
      将按照“供应商名称/模型名称”的格式保存
      ') + '
      ' + (state.lastAppliedAt ? '已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置' : '') + '
      ' + '' + @@ -982,13 +1008,10 @@ var agent = root.getElementById("oclm-agent"); if (agent) { agent.onchange = function() { + syncDraftFromDom(); state.form.default_agent = agent.value; - var model = root.getElementById("oclm-model"); - if (model) { - var nextModel = modelForAgent(agent.value); - model.value = nextModel; - state.form.default_model = nextModel; - } + state.form.default_model = modelForAgent(agent.value); + render(); }; } @@ -1073,12 +1096,21 @@ var saveAi = root.getElementById("oclm-save-ai"); if (saveAi) { saveAi.onclick = function() { + var agentValue = root.getElementById("oclm-agent").value; + var modelValue = normalizeModelValue(agentValue, root.getElementById("oclm-model").value); + var baseUrlValue = root.getElementById("oclm-base-url").value; + if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) { + window.alert("自定义供应商必须填写服务地址"); + return; + } var payload = { - default_agent: root.getElementById("oclm-agent").value, - default_model: root.getElementById("oclm-model").value, + default_agent: agentValue, + default_model: modelValue, provider_api_key: root.getElementById("oclm-api-key").value, - provider_base_url: root.getElementById("oclm-base-url").value + provider_base_url: baseUrlValue }; + state.form.default_agent = agentValue; + state.form.default_model = modelValue; state.savingSection = "ai"; render(); postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() { diff --git a/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua b/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua index 1f08e631..8701a934 100644 --- a/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua +++ b/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua @@ -645,6 +645,34 @@ function action_config_data() local uci = require "luci.model.uci".cursor() local model = require "luci.model.openclawmgr" + local function read_json_file(path) + local f = io.open(path, "r") + if not f then return nil end + local raw = f:read("*a") + f:close() + if not raw or raw == "" then return nil end + return jsonc.parse(raw) + end + + local function infer_custom_provider_model(base_dir) + base_dir = tostring(base_dir or "") + if base_dir == "" then return "" end + local cfg = read_json_file(base_dir .. "/data/.openclaw/openclaw.json") or {} + local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary + if type(primary) == "string" and primary:match("^custom%-provider/.+") then + return primary + end + local providers = cfg.models and cfg.models.providers + local custom = providers and providers["custom-provider"] + local models = custom and custom.models + local first = type(models) == "table" and models[1] or nil + local id = first and first.id + if type(id) == "string" and id ~= "" then + return "custom-provider/" .. id + end + return "" + end + if (http.getenv("REQUEST_METHOD") or "GET") == "POST" then if not require_csrf() then return @@ -661,6 +689,8 @@ function action_config_data() return body[key] ~= nil end + local requested_default_agent = tostring(has("default_agent") and body.default_agent or (uci:get("openclawmgr", section, "default_agent") or "anthropic")) + if has("enabled") then uci:set("openclawmgr", section, "enabled", bool_to_uci(body.enabled == true or body.enabled == "1")) end @@ -699,7 +729,7 @@ function action_config_data() if has("default_agent") then local agent = tostring(body.default_agent or "") - if agent ~= "openai" and agent ~= "anthropic" and agent ~= "minimax-cn" and agent ~= "moonshot" then + if agent ~= "openai" and agent ~= "anthropic" and agent ~= "minimax-cn" and agent ~= "moonshot" and agent ~= "custom-provider" then write_json({ ok = false, error = "invalid default_agent" }) return end @@ -720,6 +750,10 @@ function action_config_data() if has("provider_base_url") then local value = tostring(body.provider_base_url or "") + if requested_default_agent == "custom-provider" and value == "" then + write_json({ ok = false, error = "provider_base_url required for custom-provider" }) + return + end if value ~= "" and not value:match("^https?://") then write_json({ ok = false, error = "invalid provider_base_url" }) return @@ -789,6 +823,12 @@ function action_config_data() allowed_origins[#allowed_origins + 1] = item end + local current_default_agent = uci:get("openclawmgr", "main", "default_agent") or "anthropic" + local current_default_model = uci:get("openclawmgr", "main", "default_model") or "" + if current_default_agent == "custom-provider" and current_default_model == "" then + current_default_model = infer_custom_provider_model(base_dir) + end + write_json({ ok = true, config = { @@ -800,8 +840,8 @@ function action_config_data() allowed_origins = allowed_origins, allow_insecure_auth = (uci:get("openclawmgr", "main", "allow_insecure_auth") or "0") == "1", disable_device_auth = (uci:get("openclawmgr", "main", "disable_device_auth") or "0") == "1", - default_agent = uci:get("openclawmgr", "main", "default_agent") or "anthropic", - default_model = uci:get("openclawmgr", "main", "default_model") or "", + default_agent = current_default_agent, + default_model = current_default_model, install_accelerated = (uci:get("openclawmgr", "main", "install_accelerated") or "1") == "1", provider_api_key = uci:get("openclawmgr", "main", "provider_api_key") or "", provider_base_url = uci:get("openclawmgr", "main", "provider_base_url") or "", diff --git a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh index 93ff6fcf..5cb890cb 100755 --- a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh +++ b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh @@ -418,20 +418,106 @@ agent_key_name() { anthropic) echo "ANTHROPIC_API_KEY" ;; minimax-cn) echo "MINIMAX_API_KEY" ;; moonshot) echo "MOONSHOT_API_KEY" ;; + custom-provider) echo "" ;; *) echo "ANTHROPIC_API_KEY" ;; esac } +existing_custom_provider_model() { + local cfg="${DATA_DIR}/.openclaw/openclaw.json" + [ -f "$cfg" ] || return 0 + OPENCLAW_CONFIG_PATH="$cfg" lua - <<'EOF' +local json = require "luci.jsonc" + +local path = os.getenv("OPENCLAW_CONFIG_PATH") +local f = io.open(path, "r") +if not f then + return +end +local raw = f:read("*a") +f:close() +local cfg = json.parse(raw or "") or {} + +local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary +if type(primary) == "string" and primary:match("^custom%-provider/.+") then + io.write(primary) + return +end + +local providers = cfg.models and cfg.models.providers +local custom = providers and providers["custom-provider"] +local models = custom and custom.models +local first = type(models) == "table" and models[1] or nil +local id = first and first.id +if type(id) == "string" and id ~= "" then + io.write("custom-provider/" .. id) +end +EOF +} + agent_default_model() { - if [ -n "${DEFAULT_MODEL:-}" ]; then - echo "$DEFAULT_MODEL" - return 0 - fi case "${DEFAULT_AGENT:-anthropic}" in - openai) echo "openai/gpt-5.2" ;; - anthropic) echo "anthropic/claude-sonnet-4-6" ;; - minimax-cn) echo "minimax-cn/MiniMax-M2.5" ;; - moonshot) echo "moonshot/kimi-k2.5" ;; + openai) + if [ -n "${DEFAULT_MODEL:-}" ]; then + case "$DEFAULT_MODEL" in + openai/*) echo "$DEFAULT_MODEL" ;; + */*) echo "openai/${DEFAULT_MODEL#*/}" ;; + *) echo "openai/$DEFAULT_MODEL" ;; + esac + else + echo "openai/gpt-5.2" + fi + ;; + anthropic) + if [ -n "${DEFAULT_MODEL:-}" ]; then + case "$DEFAULT_MODEL" in + anthropic/*) echo "$DEFAULT_MODEL" ;; + */*) echo "anthropic/${DEFAULT_MODEL#*/}" ;; + *) echo "anthropic/$DEFAULT_MODEL" ;; + esac + else + echo "anthropic/claude-sonnet-4-6" + fi + ;; + minimax-cn) + if [ -n "${DEFAULT_MODEL:-}" ]; then + case "$DEFAULT_MODEL" in + minimax-cn/*) echo "$DEFAULT_MODEL" ;; + */*) echo "minimax-cn/${DEFAULT_MODEL#*/}" ;; + *) echo "minimax-cn/$DEFAULT_MODEL" ;; + esac + else + echo "minimax-cn/MiniMax-M2.5" + fi + ;; + moonshot) + if [ -n "${DEFAULT_MODEL:-}" ]; then + case "$DEFAULT_MODEL" in + moonshot/*) echo "$DEFAULT_MODEL" ;; + */*) echo "moonshot/${DEFAULT_MODEL#*/}" ;; + *) echo "moonshot/$DEFAULT_MODEL" ;; + esac + else + echo "moonshot/kimi-k2.5" + fi + ;; + custom-provider) + if [ -n "${DEFAULT_MODEL:-}" ]; then + case "$DEFAULT_MODEL" in + custom-provider/*) echo "$DEFAULT_MODEL" ;; + */*) echo "custom-provider/${DEFAULT_MODEL#*/}" ;; + *) echo "custom-provider/$DEFAULT_MODEL" ;; + esac + else + local current_model + current_model="$(existing_custom_provider_model)" + if [ -n "$current_model" ]; then + echo "$current_model" + else + echo "custom-provider/custom-model" + fi + fi + ;; *) echo "anthropic/claude-sonnet-4-6" ;; esac } @@ -442,6 +528,7 @@ agent_default_base_url() { anthropic) echo "https://api.anthropic.com" ;; minimax-cn) echo "https://api.minimaxi.com/anthropic" ;; moonshot) echo "https://api.moonshot.cn/v1" ;; + custom-provider) echo "" ;; *) echo "https://api.anthropic.com" ;; esac } @@ -579,7 +666,7 @@ control.dangerouslyAllowHostHeaderOriginFallback = bool_env("GATEWAY_HOST_HEADER local env = {} local selected_env_key = os.getenv("DEFAULT_AGENT_KEY") local selected_api_key = os.getenv("DEFAULT_AGENT_API_KEY") or "" -if selected_env_key and selected_api_key ~= "" then +if selected_env_key and selected_env_key ~= "" and selected_api_key ~= "" then env[selected_env_key] = selected_api_key end cfg.env = next(env) and env or nil @@ -622,11 +709,39 @@ elseif current_provider_id == "moonshot" then current_provider.models = { { id = "kimi-k2.5", name = "Kimi K2.5" }, } +elseif current_provider_id == "custom-provider" then + local primary_model = os.getenv("DEFAULT_AGENT_MODEL") or "custom-provider/custom-model" + local custom_model_id = primary_model:match("^[^/]+/(.+)$") or primary_model + if custom_model_id == "" then + custom_model_id = "custom-model" + end + current_provider.api = "openai-completions" + current_provider.baseUrl = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or current_provider.baseUrl + current_provider.apiKey = selected_api_key + current_provider.authHeader = nil + current_provider.models = { + { + reasoning = false, + name = custom_model_id .. " (Custom Provider)", + cost = { + input = 0, + cacheRead = 0, + cacheWrite = 0, + output = 0, + }, + id = custom_model_id, + maxTokens = 4096, + contextWindow = 16000, + input = { "text" }, + }, + } end local override_base = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or "" local base_url_mode = os.getenv("DEFAULT_AGENT_BASE_URL_MODE") or "default" - if base_url_mode == "override" and override_base ~= "" then + if current_provider_id == "custom-provider" and override_base ~= "" then + current_provider.baseUrl = override_base + elseif base_url_mode == "override" and override_base ~= "" then current_provider.baseUrl = override_base elseif base_url_mode == "default" then current_provider.baseUrl = os.getenv("DEFAULT_AGENT_BASE_URL") or current_provider.baseUrl @@ -665,7 +780,7 @@ f:close() local env_path = cfg_path:gsub("openclaw%.json$", "openclaw.env") local envf = assert(io.open(env_path, "w")) -if selected_env_key and selected_api_key ~= "" then +if selected_env_key and selected_env_key ~= "" and selected_api_key ~= "" then local val = tostring(selected_api_key) val = val:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub('"', '\\"') envf:write(selected_env_key .. '="' .. val .. '"\n') @@ -1305,7 +1420,7 @@ case "$DISABLE_DEVICE_AUTH" in *) DISABLE_DEVICE_AUTH="0" ;; esac case "$DEFAULT_AGENT" in - openai|anthropic|minimax-cn|moonshot) ;; + openai|anthropic|minimax-cn|moonshot|custom-provider) ;; *) DEFAULT_AGENT="anthropic" ;; esac case "$INSTALL_ACCELERATED" in diff --git a/luci-app-partexp/Makefile b/luci-app-partexp/Makefile index a83d58fe..905c9db5 100644 --- a/luci-app-partexp/Makefile +++ b/luci-app-partexp/Makefile @@ -10,8 +10,8 @@ PKG_NAME:=luci-app-partexp LUCI_TITLE:=LuCI Support for Automatic Partition Mount LUCI_PKGARCH:=all LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop -PKG_VERSION:=2.0.4 -PKG_RELEASE:=3 +PKG_VERSION:=2.0.5 +PKG_RELEASE:=4 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Sirpdboy diff --git a/luci-app-partexp/htdocs/luci-static/resources/view/partexp/manual.js b/luci-app-partexp/htdocs/luci-static/resources/view/partexp/manual.js new file mode 100644 index 00000000..b13c0455 --- /dev/null +++ b/luci-app-partexp/htdocs/luci-static/resources/view/partexp/manual.js @@ -0,0 +1,921 @@ +/* + * Copyright (C) 2022-2025 Sirpdboy + * + * Licensed to the public under the Apache License 2.0 + */ + +'use strict'; + +'require form'; +'require fs'; +'require rpc'; +'require uci'; +'require ui'; +'require view'; + +// 声明 RPC 接口 +var callPartExpAutopart = rpc.declare({ + object: 'partexp', + method: 'autopart' +}); + +var callPartExpGetLog = rpc.declare({ + object: 'partexp', + method: 'get_log', + params: ['position'] +}); + +var callPartExpGetDevices = rpc.declare({ + object: 'partexp', + method: 'get_devices' +}); + +var callPartExpGetStatus = rpc.declare({ + object: 'partexp', + method: 'get_status' +}); + +// 添加保存配置的 RPC 声明 +var callPartExpSaveConfig = rpc.declare({ + object: 'partexp', + method: 'save_config', + params: ['target_function', 'target_disk', 'keep_config', 'format_type'] +}); + +return view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(fs.stat('/usr/bin/partexp'), null), + L.resolveDefault(fs.stat('/tmp/partexp.log'), null) + ]); + }, + + render: function(data) { + var container = E('div', { class: 'cbi-map' }); + var htmlParts = [ + '', + '

      ' + _('One click partition expansion mounting tool') + '

      ', + '
      ', + '
      ', + ' ' + _('Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool.') + '
      ', + ' ' + _('For specific usage, see:') + ' ', + ' ', + ' GitHub @partexp', + ' ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ' + _('Select the function to be performed') + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ' + _('Select the hard disk device to operate') + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + ' ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + '
      0%
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + '
      ' + ]; + container.innerHTML = htmlParts.join(''); + + var self = this; + + var uci = self.uci || window.uci; + setTimeout(function() { + self.initDOM(); + self.bindEvents(); + self.loadDevices(); + self.loadSavedConfig(); + self.checkOperationStatus(); + self.loadExistingLog(); + }, 100); + + return container; + }, + + initDOM: function() { + this.dom = { + stateContainer: document.querySelector('#state-container'), + targetFunction: document.querySelector('#target_function'), + targetDisk: document.querySelector('#target_disk'), + keepConfig: document.querySelector('#keep_config'), + formatType: document.querySelector('#format_type'), + executeBtn: document.querySelector('#execute-btn'), + logView: document.querySelector('#log-view'), + progressBar: document.querySelector('#progress-bar'), + progressText: document.querySelector('#progress-text'), + executeStatus: document.querySelector('#execute_status') + }; + + this.logPosition = '0'; + this.logPolling = null; + this.isRunning = false; + this.operationComplete = false; + this.pollErrorCount = 0; + this.pollingStartTime = 0; + this.lastPollTime = 0; + this.currentProgress = 0; + this.autoSaveTimer = null; + this.isNewOperation = false; // 标记是否是新操作 + }, + + bindEvents: function() { + var self = this; + if (this.dom.executeBtn) { + this.dom.executeBtn.addEventListener('click', function(e) { + e.preventDefault(); + self.executeOperation(); + }); + } + + [this.dom.targetFunction, this.dom.targetDisk, this.dom.formatType].forEach(function(element) { + if (element) { + element.addEventListener('change', function() { + self.autoSaveConfig(); + self.updateFormVisibility(); + }); + } + }); + + if (this.dom.keepConfig) { + this.dom.keepConfig.addEventListener('click', function() { + self.autoSaveConfig(); + }); + } + + if (this.dom.targetFunction) { + this.updateFormVisibility(); + } + }, + +loadDevices: function() { + var self = this; + + if (self.dom.targetDisk) { + var loadingOption = document.createElement('option'); + loadingOption.value = ''; + loadingOption.textContent = _('Loading devices...'); + loadingOption.disabled = true; + loadingOption.selected = true; + self.dom.targetDisk.innerHTML = ''; + self.dom.targetDisk.appendChild(loadingOption); + } + function loadDevicesWithRetry(retryCount = 0) { + callPartExpGetDevices().then(function(response) { + if (!response) { + throw new Error('Empty response'); + } + if (self.dom.targetDisk) { + self.dom.targetDisk.innerHTML = ''; + if (response.devices && response.devices.length > 0) { + response.devices.forEach(function(device) { + var option = document.createElement('option'); + option.value = device.name; + option.textContent = device.name + ' (' + device.dev + ', ' + device.size + ' MB)'; + self.dom.targetDisk.appendChild(option); + }); + } else { + + var noDeviceOption = document.createElement('option'); + noDeviceOption.value = ''; + noDeviceOption.textContent = _('no find device'); + noDeviceOption.disabled = true; + noDeviceOption.selected = true; + self.dom.targetDisk.appendChild(noDeviceOption); + } + } + }).catch(function(error) { + console.error('Failed to load devices:', error); + + if (retryCount < 3) { + setTimeout(function() { + loadDevicesWithRetry(retryCount + 1); + }, 1000 * (retryCount + 1)); + } else { + if (self.dom.targetDisk) { + self.dom.targetDisk.innerHTML = ''; + var errorOption = document.createElement('option'); + errorOption.value = ''; + errorOption.textContent = _('load error'); + errorOption.disabled = true; + errorOption.selected = true; + self.dom.targetDisk.appendChild(errorOption); + } + + ui.addNotification({ + title: _('load device error'), + text: _('Failed to load devices:'), + type: 'error', + delay: 5000 + }); + } + }); + } + + loadDevicesWithRetry(); +}, + + + loadExistingLog: function() { + var self = this; + + callPartExpGetLog('0').then(function(response) { + if (response && response.log) { + var logContent = response.log.toString().trim(); + if (logContent && self.dom.logView) { + self.dom.logView.value = logContent; + + setTimeout(function() { + if (self.dom.logView && self.dom.logView.value) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 100); + + if (response.position) { + self.logPosition = response.position; + } + if (!self.isRunning && logContent.includes('正在执行') && !logContent.includes('操作完成')) { + self.isRunning = true; + self.switchState('executing'); + self.startLogPolling(); + } + } + } + }).catch(function(error) { + console.error('Failed to load existing log:', error); + }); + }, + + loadSavedConfig: function() { + var self = this; + + return fs.read('/etc/config/partexp').then(function(content) { + if (!content) { + self.setDefaultConfig(); + return; + } + + var lines = content.split('\n'); + var config = {}; + + lines.forEach(function(line) { + line = line.trim(); + if (line.startsWith('option')) { + var parts = line.split(/\s+/); + if (parts.length >= 3) { + var key = parts[1]; + var value = parts.slice(2).join(' ').replace(/^['"]|['"]$/g, ''); + config[key] = value; + } + } + }); + + if (self.dom.targetFunction) { + self.dom.targetFunction.value = config.target_function || '/opt'; + } + if (self.dom.targetDisk && config.target_disk) { + setTimeout(function() { + if (self.dom.targetDisk) { + self.dom.targetDisk.value = config.target_disk; + } + }, 500); + } + if (self.dom.keepConfig) { + self.dom.keepConfig.checked = (config.keep_config === '1'); + } + if (self.dom.formatType) { + self.dom.formatType.value = config.format_type || '0'; + } + + self.configCache = config; + + self.updateFormVisibility(); + + }).catch(function(error) { + console.log('Failed to load config:', error); + self.setDefaultConfig(); + }); + }, + + setDefaultConfig: function() { + if (this.dom.targetFunction) { + this.dom.targetFunction.value = '/opt'; + } + if (this.dom.formatType) { + this.dom.formatType.value = '0'; + } + if (this.dom.keepConfig) { + this.dom.keepConfig.checked = false; + } + this.updateFormVisibility(); + + this.configCache = { + target_function: '/opt', + target_disk: '', + keep_config: '0', + format_type: '0' + }; + }, + + autoSaveConfig: function() { + var self = this; + + if (this.autoSaveTimer) { + clearTimeout(this.autoSaveTimer); + } + this.autoSaveTimer = setTimeout(function() { + self.saveCurrentConfig(); + }, 1500); + }, + + saveCurrentConfig: function() { + var self = this; + + var targetFunction = this.dom.targetFunction ? this.dom.targetFunction.value : '/opt'; + var targetDisk = this.dom.targetDisk ? this.dom.targetDisk.value : ''; + var keepConfig = this.dom.keepConfig ? this.dom.keepConfig.checked : false; + var formatType = this.dom.formatType ? this.dom.formatType.value : '0'; + if (callPartExpSaveConfig) { + return callPartExpSaveConfig( + targetFunction, + targetDisk, + keepConfig ? '1' : '0', + formatType + ).then(function(response) { + if (response && response.success) { + + self.configCache = { + target_function: targetFunction, + target_disk: targetDisk, + keep_config: keepConfig ? '1' : '0', + format_type: formatType + }; + + return true; + } else { + console.warn('RPC save failed, falling back to file write'); + return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType); + } + }).catch(function(error) { + console.error('RPC save config error:', error); + return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType); + }); + } else { + return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType); + } + }, + + saveConfigToFile: function(targetFunction, targetDisk, keepConfig, formatType) { + var configContent = [ + '# Auto-generated by partexp', + '', + 'config global global', + "\toption target_function '" + targetFunction + "'", + "\toption target_disk '" + targetDisk + "'", + "\toption keep_config '" + (keepConfig ? '1' : '0') + "'", + "\toption format_type '" + formatType + "'", + '' + ].join('\n'); + + return fs.write('/etc/config/partexp', configContent).then(function() { + console.log('Settings saved to file /etc/config/partexp'); + return true; + }).catch(function(error) { + console.error('Failed to save settings to file:', error); + return false; + }); + }, + + executeOperation: function() { + var self = this; + + this.saveCurrentConfig(); + var target_function = this.dom.targetFunction.value; + var target_disk = this.dom.targetDisk.value; + + if (target_function !== '/' && (!target_disk || target_disk.trim() === '')) { + alert(_('Please select a target disk')); + return; + } + + var confirmMessage = _('Are you sure you want to execute partition expansion?') + '\n\n' + + _('Function:') + ' ' + this.getFunctionDescription(target_function) + '\n' + + (target_function !== '/' ? _('Disk:') + ' ' + target_disk + '\n' : '') + + (target_function === '/' || target_function === '/overlay' ? + _('Keep config:') + ' ' + (this.dom.keepConfig.checked ? _('Yes') : _('No')) + '\n' : '') + + (target_function === '/opt' || target_function === '/dev' ? + _('Format type:') + ' ' + this.getFormatTypeDescription(this.dom.formatType.value) + '\n' : '') + + '\n' + _('This operation may take several minutes.'); + + if (!confirm(confirmMessage)) { + return; + } + + this.resetOperationState(); + + this.isNewOperation = true; + + if (this.dom.logView) { + this.dom.logView.value = _('正在启动操作...'); + } + + if (this.dom.executeBtn) { + this.dom.executeBtn.disabled = true; + this.dom.executeBtn.textContent = _('Executing...'); + } + + this.switchState('executing'); + + this.updateProgress(5, _('Starting operation...')); + + callPartExpAutopart() + .then(function(response) { + if (response && response.success) { + self.isRunning = true; + self.operationComplete = false; + self.startLogPolling(); + + if (self.dom.executeStatus) { + self.dom.executeStatus.textContent = _('Operation started successfully'); + } + } else { + var errorMsg = response && response.message ? response.message : _('Operation failed'); + self.handleOperationError(errorMsg); + } + }) + .catch(function(error) { + console.error('Operation failed:', error); + self.handleOperationError(_('Failed to start operation:') + ' ' + (error.message || _('Unknown error'))); + }); + }, + + resetOperationState: function() { + this.logPosition = '0'; + this.isRunning = true; + this.operationComplete = false; + this.pollErrorCount = 0; + this.pollingStartTime = Date.now(); + this.lastPollTime = 0; + this.currentProgress = 0; + + this.updateProgress(0, _('Starting operation...')); + }, + + handleOperationError: function(errorMsg) { + alert(errorMsg); + if (this.dom.executeBtn) { + this.dom.executeBtn.disabled = false; + this.dom.executeBtn.textContent = _('Click to execute'); + } + + this.switchState('ready'); + this.stopLogPolling(); + + if (this.dom.logView) { + var currentLog = this.dom.logView.value || ''; + this.dom.logView.value = currentLog + '\n\n' + _('操作失败:') + ' ' + errorMsg; + setTimeout(() => { + if (this.dom.logView) { + this.dom.logView.scrollTop = this.dom.logView.scrollHeight; + } + }, 100); + } + }, + + updateFormVisibility: function() { + if (!this.dom.targetFunction || !this.dom.targetDisk || + !this.dom.keepConfig || !this.dom.formatType) return; + + var func = this.dom.targetFunction.value; + var diskDiv = this.dom.targetDisk.closest('.cbi-value'); + var keepDiv = this.dom.keepConfig.closest('.cbi-value'); + var formatDiv = this.dom.formatType.closest('.cbi-value'); + + if (!diskDiv || !keepDiv || !formatDiv) return; + + if (func === '/') { + diskDiv.style.display = 'none'; + formatDiv.style.display = 'none'; + keepDiv.style.display = 'block'; + } else if (func === '/overlay') { + diskDiv.style.display = 'block'; + formatDiv.style.display = 'none'; + keepDiv.style.display = 'block'; + } else { + diskDiv.style.display = 'block'; + formatDiv.style.display = 'block'; + keepDiv.style.display = 'none'; + } + }, + + checkOperationStatus: function() { + var self = this; + + callPartExpGetStatus().then(function(response) { + if (response && response.running) { + self.isRunning = true; + self.switchState('executing'); + self.startLogPolling(); + + if (self.dom.executeBtn) { + self.dom.executeBtn.disabled = true; + self.dom.executeBtn.textContent = _('Operation in progress...'); + } + + if (self.dom.executeStatus) { + self.dom.executeStatus.textContent = _('Operation in progress...'); + } + } + }).catch(function(error) { + console.error('Failed to check operation status:', error); + }); + }, + + startLogPolling: function() { + var self = this; + + this.stopLogPolling(); + + this.pollErrorCount = 0; + this.pollingStartTime = Date.now(); + this.lastPollTime = 0; + + this.updateProgress(10, _('Operation in progress...')); + + this.logPolling = setInterval(function() { + if (Date.now() - self.pollingStartTime > 20 * 60 * 1000) { + console.error('Operation timeout'); + self.stopLogPolling(); + self.isRunning = false; + + if (self.dom.logView) { + var currentLog = self.dom.logView.value || ''; + self.dom.logView.value = currentLog + '\n\n[超时] 操作超过20分钟未完成,请检查系统'; + setTimeout(() => { + if (self.dom.logView) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 100); + } + + self.switchState('ready'); + + if (self.dom.executeBtn) { + self.dom.executeBtn.disabled = false; + self.dom.executeBtn.textContent = _('Click to execute'); + } + return; + } + + self.pollLog(); + }, 3000); // 每3秒轮询一次,减少频率 + }, + + pollLog: function() { + var self = this; + + if (!this.isRunning) { + this.stopLogPolling(); + return; + } + + var pollStartTime = Date.now(); + + callPartExpGetLog('0').then(function(response) { + if (!response) { + console.error('No response from log polling'); + return; + } + + if (pollStartTime < self.lastPollTime) { + return; + } + + self.lastPollTime = pollStartTime; + + if (response.log !== undefined) { + var logContent = response.log.toString().trim(); + + if (response.position) { + self.logPosition = response.position; + } + + if (self.dom.logView) { + if (logContent !== '') { + self.dom.logView.value = logContent; + + setTimeout(function() { + if (self.dom.logView && self.dom.logView.value) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 50); + } + } + + self.parseAndUpdateProgress(logContent); + + if (self.checkOperationComplete(logContent)) { + self.handleOperationComplete(); + } + } + + if (response.complete) { + self.handleOperationComplete(); + } + + self.pollErrorCount = 0; + + }).catch(function(error) { + console.error('Log polling error:', error); + + self.pollErrorCount = (self.pollErrorCount || 0) + 1; + if (self.pollErrorCount > 5) { + console.error('Too many polling errors, stopping'); + self.stopLogPolling(); + self.isRunning = false; + self.switchState('ready'); + + if (self.dom.executeBtn) { + self.dom.executeBtn.disabled = false; + self.dom.executeBtn.textContent = _('Click to execute'); + } + if (self.dom.logView) { + var currentLog = self.dom.logView.value || ''; + self.dom.logView.value = currentLog + '\n\n[错误] 日志轮询失败,请刷新页面查看最新状态'; + setTimeout(() => { + if (self.dom.logView) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 100); + } + } + }); + }, + + checkOperationComplete: function(logText) { + if (!logText) return false; + + var completeMarkers = [ + '重启设备', + '操作完成' + ]; + + for (var i = 0; i < completeMarkers.length; i++) { + if (logText.includes(completeMarkers[i])) { + return true; + } + } + + return false; + }, + + handleOperationComplete: function() { + if (this.operationComplete) { + return; + } + + this.operationComplete = true; + this.isRunning = false; + this.isNewOperation = false; + + this.stopLogPolling(); + if (this.dom.logView) { + var currentLog = this.dom.logView.value || ''; + if (!currentLog.includes('操作完成')) { + this.dom.logView.value = currentLog; + setTimeout(() => { + if (this.dom.logView) { + this.dom.logView.scrollTop = this.dom.logView.scrollHeight; + } + }, 100); + } + } + + this.updateProgress(100, _('Operation completed')); + + setTimeout(() => { + if (this.dom.executeBtn) { + this.dom.executeBtn.disabled = false; + this.dom.executeBtn.textContent = _('Click to execute'); + } + + setTimeout(() => { + this.switchState('ready'); + }, 3000); + }, 2000); + }, + + parseAndUpdateProgress: function(logText) { + if (!logText || !this.dom.executeStatus) return; + + var percent = 0; + var statusMessage = _('Operation in progress...'); + + if (logText.includes('100%') || logText.includes('操作完成') || logText.includes('扩容成功')) { + percent = 100; + statusMessage = _('Operation completed'); + } else if ( logText.includes('错误') || logText.includes('error')) { + return; + } else if (logText.includes('分区扩容和挂载到') || logText.includes('正在挂载')) { + percent = 90; + statusMessage = _('Getting device information'); + } else if (logText.includes('检测设备')) { + percent = 60; + statusMessage = _('Checking partition format'); + } else if (logText.includes('开始检测目标')) { + percent = 50; + statusMessage = _('Checking target device'); + } else if (logText.includes('定位到操作目标设备分区')) { + percent = 40; + statusMessage = _('Locating target partition'); + } else if (logText.includes('目标盘') && logText.includes('有剩余空间')) { + percent = 30; + statusMessage = _('Checking free space'); + } else if (logText.includes('操作功能')) { + percent = 20; + statusMessage = _('Starting operation'); + } else if (logText.includes('开始执行') || logText.includes('Starting')) { + percent = 10; + statusMessage = _('Initializing...'); + } + + if (percent > 0) { + this.currentProgress = Math.max(this.currentProgress || 0, percent); + } else { + this.currentProgress = Math.min(90, (this.currentProgress || 0) + 1); + } + + this.updateProgress(this.currentProgress, statusMessage); + }, + + updateProgress: function(percent, message) { + if (!this.dom.progressBar || !this.dom.progressText || !this.dom.executeStatus) { + return; + } + + percent = Math.max(0, Math.min(100, percent)); + + this.dom.progressBar.style.width = percent + '%'; + + this.dom.progressText.textContent = percent + '%'; + + this.dom.executeStatus.textContent = message; + }, + + stopLogPolling: function() { + if (this.logPolling) { + clearInterval(this.logPolling); + this.logPolling = null; + } + }, + + // 切换状态 + switchState: function(to) { + if (!this.dom.stateContainer) return; + + this.dom.stateContainer.classList.remove( + 'state-ctl-ready', + 'state-ctl-executing' + ); + + this.dom.stateContainer.classList.add('state-ctl-' + to); + }, + + getFunctionDescription: function(func) { + switch(func) { + case '/': return _('Extend to root directory'); + case '/overlay': return _('Expand overlay'); + case '/opt': return _('Docker data disk'); + case '/dev': return _('Normal mount'); + default: return func; + } + }, + + getFormatTypeDescription: function(type) { + switch(type) { + case '0': return _('No formatting'); + case 'ext4': return _('EXT4'); + case 'btrfs': return _('Btrfs'); + case 'ntfs': return _('NTFS'); + default: return type; + } + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); \ No newline at end of file diff --git a/luci-app-partexp/htdocs/luci-static/resources/view/partexp/partexp.js b/luci-app-partexp/htdocs/luci-static/resources/view/partexp/partexp.js new file mode 100644 index 00000000..b13c0455 --- /dev/null +++ b/luci-app-partexp/htdocs/luci-static/resources/view/partexp/partexp.js @@ -0,0 +1,921 @@ +/* + * Copyright (C) 2022-2025 Sirpdboy + * + * Licensed to the public under the Apache License 2.0 + */ + +'use strict'; + +'require form'; +'require fs'; +'require rpc'; +'require uci'; +'require ui'; +'require view'; + +// 声明 RPC 接口 +var callPartExpAutopart = rpc.declare({ + object: 'partexp', + method: 'autopart' +}); + +var callPartExpGetLog = rpc.declare({ + object: 'partexp', + method: 'get_log', + params: ['position'] +}); + +var callPartExpGetDevices = rpc.declare({ + object: 'partexp', + method: 'get_devices' +}); + +var callPartExpGetStatus = rpc.declare({ + object: 'partexp', + method: 'get_status' +}); + +// 添加保存配置的 RPC 声明 +var callPartExpSaveConfig = rpc.declare({ + object: 'partexp', + method: 'save_config', + params: ['target_function', 'target_disk', 'keep_config', 'format_type'] +}); + +return view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(fs.stat('/usr/bin/partexp'), null), + L.resolveDefault(fs.stat('/tmp/partexp.log'), null) + ]); + }, + + render: function(data) { + var container = E('div', { class: 'cbi-map' }); + var htmlParts = [ + '', + '

      ' + _('One click partition expansion mounting tool') + '

      ', + '
      ', + '
      ', + ' ' + _('Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool.') + '
      ', + ' ' + _('For specific usage, see:') + ' ', + ' ', + ' GitHub @partexp', + ' ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ' + _('Select the function to be performed') + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ' + _('Select the hard disk device to operate') + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + ' ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + '
      0%
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + '
      ', + ' ', + '
      ', + ' ', + '
      ', + '
      ', + '
      ', + '
      ' + ]; + container.innerHTML = htmlParts.join(''); + + var self = this; + + var uci = self.uci || window.uci; + setTimeout(function() { + self.initDOM(); + self.bindEvents(); + self.loadDevices(); + self.loadSavedConfig(); + self.checkOperationStatus(); + self.loadExistingLog(); + }, 100); + + return container; + }, + + initDOM: function() { + this.dom = { + stateContainer: document.querySelector('#state-container'), + targetFunction: document.querySelector('#target_function'), + targetDisk: document.querySelector('#target_disk'), + keepConfig: document.querySelector('#keep_config'), + formatType: document.querySelector('#format_type'), + executeBtn: document.querySelector('#execute-btn'), + logView: document.querySelector('#log-view'), + progressBar: document.querySelector('#progress-bar'), + progressText: document.querySelector('#progress-text'), + executeStatus: document.querySelector('#execute_status') + }; + + this.logPosition = '0'; + this.logPolling = null; + this.isRunning = false; + this.operationComplete = false; + this.pollErrorCount = 0; + this.pollingStartTime = 0; + this.lastPollTime = 0; + this.currentProgress = 0; + this.autoSaveTimer = null; + this.isNewOperation = false; // 标记是否是新操作 + }, + + bindEvents: function() { + var self = this; + if (this.dom.executeBtn) { + this.dom.executeBtn.addEventListener('click', function(e) { + e.preventDefault(); + self.executeOperation(); + }); + } + + [this.dom.targetFunction, this.dom.targetDisk, this.dom.formatType].forEach(function(element) { + if (element) { + element.addEventListener('change', function() { + self.autoSaveConfig(); + self.updateFormVisibility(); + }); + } + }); + + if (this.dom.keepConfig) { + this.dom.keepConfig.addEventListener('click', function() { + self.autoSaveConfig(); + }); + } + + if (this.dom.targetFunction) { + this.updateFormVisibility(); + } + }, + +loadDevices: function() { + var self = this; + + if (self.dom.targetDisk) { + var loadingOption = document.createElement('option'); + loadingOption.value = ''; + loadingOption.textContent = _('Loading devices...'); + loadingOption.disabled = true; + loadingOption.selected = true; + self.dom.targetDisk.innerHTML = ''; + self.dom.targetDisk.appendChild(loadingOption); + } + function loadDevicesWithRetry(retryCount = 0) { + callPartExpGetDevices().then(function(response) { + if (!response) { + throw new Error('Empty response'); + } + if (self.dom.targetDisk) { + self.dom.targetDisk.innerHTML = ''; + if (response.devices && response.devices.length > 0) { + response.devices.forEach(function(device) { + var option = document.createElement('option'); + option.value = device.name; + option.textContent = device.name + ' (' + device.dev + ', ' + device.size + ' MB)'; + self.dom.targetDisk.appendChild(option); + }); + } else { + + var noDeviceOption = document.createElement('option'); + noDeviceOption.value = ''; + noDeviceOption.textContent = _('no find device'); + noDeviceOption.disabled = true; + noDeviceOption.selected = true; + self.dom.targetDisk.appendChild(noDeviceOption); + } + } + }).catch(function(error) { + console.error('Failed to load devices:', error); + + if (retryCount < 3) { + setTimeout(function() { + loadDevicesWithRetry(retryCount + 1); + }, 1000 * (retryCount + 1)); + } else { + if (self.dom.targetDisk) { + self.dom.targetDisk.innerHTML = ''; + var errorOption = document.createElement('option'); + errorOption.value = ''; + errorOption.textContent = _('load error'); + errorOption.disabled = true; + errorOption.selected = true; + self.dom.targetDisk.appendChild(errorOption); + } + + ui.addNotification({ + title: _('load device error'), + text: _('Failed to load devices:'), + type: 'error', + delay: 5000 + }); + } + }); + } + + loadDevicesWithRetry(); +}, + + + loadExistingLog: function() { + var self = this; + + callPartExpGetLog('0').then(function(response) { + if (response && response.log) { + var logContent = response.log.toString().trim(); + if (logContent && self.dom.logView) { + self.dom.logView.value = logContent; + + setTimeout(function() { + if (self.dom.logView && self.dom.logView.value) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 100); + + if (response.position) { + self.logPosition = response.position; + } + if (!self.isRunning && logContent.includes('正在执行') && !logContent.includes('操作完成')) { + self.isRunning = true; + self.switchState('executing'); + self.startLogPolling(); + } + } + } + }).catch(function(error) { + console.error('Failed to load existing log:', error); + }); + }, + + loadSavedConfig: function() { + var self = this; + + return fs.read('/etc/config/partexp').then(function(content) { + if (!content) { + self.setDefaultConfig(); + return; + } + + var lines = content.split('\n'); + var config = {}; + + lines.forEach(function(line) { + line = line.trim(); + if (line.startsWith('option')) { + var parts = line.split(/\s+/); + if (parts.length >= 3) { + var key = parts[1]; + var value = parts.slice(2).join(' ').replace(/^['"]|['"]$/g, ''); + config[key] = value; + } + } + }); + + if (self.dom.targetFunction) { + self.dom.targetFunction.value = config.target_function || '/opt'; + } + if (self.dom.targetDisk && config.target_disk) { + setTimeout(function() { + if (self.dom.targetDisk) { + self.dom.targetDisk.value = config.target_disk; + } + }, 500); + } + if (self.dom.keepConfig) { + self.dom.keepConfig.checked = (config.keep_config === '1'); + } + if (self.dom.formatType) { + self.dom.formatType.value = config.format_type || '0'; + } + + self.configCache = config; + + self.updateFormVisibility(); + + }).catch(function(error) { + console.log('Failed to load config:', error); + self.setDefaultConfig(); + }); + }, + + setDefaultConfig: function() { + if (this.dom.targetFunction) { + this.dom.targetFunction.value = '/opt'; + } + if (this.dom.formatType) { + this.dom.formatType.value = '0'; + } + if (this.dom.keepConfig) { + this.dom.keepConfig.checked = false; + } + this.updateFormVisibility(); + + this.configCache = { + target_function: '/opt', + target_disk: '', + keep_config: '0', + format_type: '0' + }; + }, + + autoSaveConfig: function() { + var self = this; + + if (this.autoSaveTimer) { + clearTimeout(this.autoSaveTimer); + } + this.autoSaveTimer = setTimeout(function() { + self.saveCurrentConfig(); + }, 1500); + }, + + saveCurrentConfig: function() { + var self = this; + + var targetFunction = this.dom.targetFunction ? this.dom.targetFunction.value : '/opt'; + var targetDisk = this.dom.targetDisk ? this.dom.targetDisk.value : ''; + var keepConfig = this.dom.keepConfig ? this.dom.keepConfig.checked : false; + var formatType = this.dom.formatType ? this.dom.formatType.value : '0'; + if (callPartExpSaveConfig) { + return callPartExpSaveConfig( + targetFunction, + targetDisk, + keepConfig ? '1' : '0', + formatType + ).then(function(response) { + if (response && response.success) { + + self.configCache = { + target_function: targetFunction, + target_disk: targetDisk, + keep_config: keepConfig ? '1' : '0', + format_type: formatType + }; + + return true; + } else { + console.warn('RPC save failed, falling back to file write'); + return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType); + } + }).catch(function(error) { + console.error('RPC save config error:', error); + return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType); + }); + } else { + return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType); + } + }, + + saveConfigToFile: function(targetFunction, targetDisk, keepConfig, formatType) { + var configContent = [ + '# Auto-generated by partexp', + '', + 'config global global', + "\toption target_function '" + targetFunction + "'", + "\toption target_disk '" + targetDisk + "'", + "\toption keep_config '" + (keepConfig ? '1' : '0') + "'", + "\toption format_type '" + formatType + "'", + '' + ].join('\n'); + + return fs.write('/etc/config/partexp', configContent).then(function() { + console.log('Settings saved to file /etc/config/partexp'); + return true; + }).catch(function(error) { + console.error('Failed to save settings to file:', error); + return false; + }); + }, + + executeOperation: function() { + var self = this; + + this.saveCurrentConfig(); + var target_function = this.dom.targetFunction.value; + var target_disk = this.dom.targetDisk.value; + + if (target_function !== '/' && (!target_disk || target_disk.trim() === '')) { + alert(_('Please select a target disk')); + return; + } + + var confirmMessage = _('Are you sure you want to execute partition expansion?') + '\n\n' + + _('Function:') + ' ' + this.getFunctionDescription(target_function) + '\n' + + (target_function !== '/' ? _('Disk:') + ' ' + target_disk + '\n' : '') + + (target_function === '/' || target_function === '/overlay' ? + _('Keep config:') + ' ' + (this.dom.keepConfig.checked ? _('Yes') : _('No')) + '\n' : '') + + (target_function === '/opt' || target_function === '/dev' ? + _('Format type:') + ' ' + this.getFormatTypeDescription(this.dom.formatType.value) + '\n' : '') + + '\n' + _('This operation may take several minutes.'); + + if (!confirm(confirmMessage)) { + return; + } + + this.resetOperationState(); + + this.isNewOperation = true; + + if (this.dom.logView) { + this.dom.logView.value = _('正在启动操作...'); + } + + if (this.dom.executeBtn) { + this.dom.executeBtn.disabled = true; + this.dom.executeBtn.textContent = _('Executing...'); + } + + this.switchState('executing'); + + this.updateProgress(5, _('Starting operation...')); + + callPartExpAutopart() + .then(function(response) { + if (response && response.success) { + self.isRunning = true; + self.operationComplete = false; + self.startLogPolling(); + + if (self.dom.executeStatus) { + self.dom.executeStatus.textContent = _('Operation started successfully'); + } + } else { + var errorMsg = response && response.message ? response.message : _('Operation failed'); + self.handleOperationError(errorMsg); + } + }) + .catch(function(error) { + console.error('Operation failed:', error); + self.handleOperationError(_('Failed to start operation:') + ' ' + (error.message || _('Unknown error'))); + }); + }, + + resetOperationState: function() { + this.logPosition = '0'; + this.isRunning = true; + this.operationComplete = false; + this.pollErrorCount = 0; + this.pollingStartTime = Date.now(); + this.lastPollTime = 0; + this.currentProgress = 0; + + this.updateProgress(0, _('Starting operation...')); + }, + + handleOperationError: function(errorMsg) { + alert(errorMsg); + if (this.dom.executeBtn) { + this.dom.executeBtn.disabled = false; + this.dom.executeBtn.textContent = _('Click to execute'); + } + + this.switchState('ready'); + this.stopLogPolling(); + + if (this.dom.logView) { + var currentLog = this.dom.logView.value || ''; + this.dom.logView.value = currentLog + '\n\n' + _('操作失败:') + ' ' + errorMsg; + setTimeout(() => { + if (this.dom.logView) { + this.dom.logView.scrollTop = this.dom.logView.scrollHeight; + } + }, 100); + } + }, + + updateFormVisibility: function() { + if (!this.dom.targetFunction || !this.dom.targetDisk || + !this.dom.keepConfig || !this.dom.formatType) return; + + var func = this.dom.targetFunction.value; + var diskDiv = this.dom.targetDisk.closest('.cbi-value'); + var keepDiv = this.dom.keepConfig.closest('.cbi-value'); + var formatDiv = this.dom.formatType.closest('.cbi-value'); + + if (!diskDiv || !keepDiv || !formatDiv) return; + + if (func === '/') { + diskDiv.style.display = 'none'; + formatDiv.style.display = 'none'; + keepDiv.style.display = 'block'; + } else if (func === '/overlay') { + diskDiv.style.display = 'block'; + formatDiv.style.display = 'none'; + keepDiv.style.display = 'block'; + } else { + diskDiv.style.display = 'block'; + formatDiv.style.display = 'block'; + keepDiv.style.display = 'none'; + } + }, + + checkOperationStatus: function() { + var self = this; + + callPartExpGetStatus().then(function(response) { + if (response && response.running) { + self.isRunning = true; + self.switchState('executing'); + self.startLogPolling(); + + if (self.dom.executeBtn) { + self.dom.executeBtn.disabled = true; + self.dom.executeBtn.textContent = _('Operation in progress...'); + } + + if (self.dom.executeStatus) { + self.dom.executeStatus.textContent = _('Operation in progress...'); + } + } + }).catch(function(error) { + console.error('Failed to check operation status:', error); + }); + }, + + startLogPolling: function() { + var self = this; + + this.stopLogPolling(); + + this.pollErrorCount = 0; + this.pollingStartTime = Date.now(); + this.lastPollTime = 0; + + this.updateProgress(10, _('Operation in progress...')); + + this.logPolling = setInterval(function() { + if (Date.now() - self.pollingStartTime > 20 * 60 * 1000) { + console.error('Operation timeout'); + self.stopLogPolling(); + self.isRunning = false; + + if (self.dom.logView) { + var currentLog = self.dom.logView.value || ''; + self.dom.logView.value = currentLog + '\n\n[超时] 操作超过20分钟未完成,请检查系统'; + setTimeout(() => { + if (self.dom.logView) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 100); + } + + self.switchState('ready'); + + if (self.dom.executeBtn) { + self.dom.executeBtn.disabled = false; + self.dom.executeBtn.textContent = _('Click to execute'); + } + return; + } + + self.pollLog(); + }, 3000); // 每3秒轮询一次,减少频率 + }, + + pollLog: function() { + var self = this; + + if (!this.isRunning) { + this.stopLogPolling(); + return; + } + + var pollStartTime = Date.now(); + + callPartExpGetLog('0').then(function(response) { + if (!response) { + console.error('No response from log polling'); + return; + } + + if (pollStartTime < self.lastPollTime) { + return; + } + + self.lastPollTime = pollStartTime; + + if (response.log !== undefined) { + var logContent = response.log.toString().trim(); + + if (response.position) { + self.logPosition = response.position; + } + + if (self.dom.logView) { + if (logContent !== '') { + self.dom.logView.value = logContent; + + setTimeout(function() { + if (self.dom.logView && self.dom.logView.value) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 50); + } + } + + self.parseAndUpdateProgress(logContent); + + if (self.checkOperationComplete(logContent)) { + self.handleOperationComplete(); + } + } + + if (response.complete) { + self.handleOperationComplete(); + } + + self.pollErrorCount = 0; + + }).catch(function(error) { + console.error('Log polling error:', error); + + self.pollErrorCount = (self.pollErrorCount || 0) + 1; + if (self.pollErrorCount > 5) { + console.error('Too many polling errors, stopping'); + self.stopLogPolling(); + self.isRunning = false; + self.switchState('ready'); + + if (self.dom.executeBtn) { + self.dom.executeBtn.disabled = false; + self.dom.executeBtn.textContent = _('Click to execute'); + } + if (self.dom.logView) { + var currentLog = self.dom.logView.value || ''; + self.dom.logView.value = currentLog + '\n\n[错误] 日志轮询失败,请刷新页面查看最新状态'; + setTimeout(() => { + if (self.dom.logView) { + self.dom.logView.scrollTop = self.dom.logView.scrollHeight; + } + }, 100); + } + } + }); + }, + + checkOperationComplete: function(logText) { + if (!logText) return false; + + var completeMarkers = [ + '重启设备', + '操作完成' + ]; + + for (var i = 0; i < completeMarkers.length; i++) { + if (logText.includes(completeMarkers[i])) { + return true; + } + } + + return false; + }, + + handleOperationComplete: function() { + if (this.operationComplete) { + return; + } + + this.operationComplete = true; + this.isRunning = false; + this.isNewOperation = false; + + this.stopLogPolling(); + if (this.dom.logView) { + var currentLog = this.dom.logView.value || ''; + if (!currentLog.includes('操作完成')) { + this.dom.logView.value = currentLog; + setTimeout(() => { + if (this.dom.logView) { + this.dom.logView.scrollTop = this.dom.logView.scrollHeight; + } + }, 100); + } + } + + this.updateProgress(100, _('Operation completed')); + + setTimeout(() => { + if (this.dom.executeBtn) { + this.dom.executeBtn.disabled = false; + this.dom.executeBtn.textContent = _('Click to execute'); + } + + setTimeout(() => { + this.switchState('ready'); + }, 3000); + }, 2000); + }, + + parseAndUpdateProgress: function(logText) { + if (!logText || !this.dom.executeStatus) return; + + var percent = 0; + var statusMessage = _('Operation in progress...'); + + if (logText.includes('100%') || logText.includes('操作完成') || logText.includes('扩容成功')) { + percent = 100; + statusMessage = _('Operation completed'); + } else if ( logText.includes('错误') || logText.includes('error')) { + return; + } else if (logText.includes('分区扩容和挂载到') || logText.includes('正在挂载')) { + percent = 90; + statusMessage = _('Getting device information'); + } else if (logText.includes('检测设备')) { + percent = 60; + statusMessage = _('Checking partition format'); + } else if (logText.includes('开始检测目标')) { + percent = 50; + statusMessage = _('Checking target device'); + } else if (logText.includes('定位到操作目标设备分区')) { + percent = 40; + statusMessage = _('Locating target partition'); + } else if (logText.includes('目标盘') && logText.includes('有剩余空间')) { + percent = 30; + statusMessage = _('Checking free space'); + } else if (logText.includes('操作功能')) { + percent = 20; + statusMessage = _('Starting operation'); + } else if (logText.includes('开始执行') || logText.includes('Starting')) { + percent = 10; + statusMessage = _('Initializing...'); + } + + if (percent > 0) { + this.currentProgress = Math.max(this.currentProgress || 0, percent); + } else { + this.currentProgress = Math.min(90, (this.currentProgress || 0) + 1); + } + + this.updateProgress(this.currentProgress, statusMessage); + }, + + updateProgress: function(percent, message) { + if (!this.dom.progressBar || !this.dom.progressText || !this.dom.executeStatus) { + return; + } + + percent = Math.max(0, Math.min(100, percent)); + + this.dom.progressBar.style.width = percent + '%'; + + this.dom.progressText.textContent = percent + '%'; + + this.dom.executeStatus.textContent = message; + }, + + stopLogPolling: function() { + if (this.logPolling) { + clearInterval(this.logPolling); + this.logPolling = null; + } + }, + + // 切换状态 + switchState: function(to) { + if (!this.dom.stateContainer) return; + + this.dom.stateContainer.classList.remove( + 'state-ctl-ready', + 'state-ctl-executing' + ); + + this.dom.stateContainer.classList.add('state-ctl-' + to); + }, + + getFunctionDescription: function(func) { + switch(func) { + case '/': return _('Extend to root directory'); + case '/overlay': return _('Expand overlay'); + case '/opt': return _('Docker data disk'); + case '/dev': return _('Normal mount'); + default: return func; + } + }, + + getFormatTypeDescription: function(type) { + switch(type) { + case '0': return _('No formatting'); + case 'ext4': return _('EXT4'); + case 'btrfs': return _('Btrfs'); + case 'ntfs': return _('NTFS'); + default: return type; + } + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); \ No newline at end of file diff --git a/luci-app-partexp/root/etc/uci-defaults/zzz_luci-app-partexp b/luci-app-partexp/root/etc/uci-defaults/zzz_luci-app-partexp new file mode 100644 index 00000000..1ae04461 --- /dev/null +++ b/luci-app-partexp/root/etc/uci-defaults/zzz_luci-app-partexp @@ -0,0 +1,6 @@ +#!/bin/sh + +chmod +x /usr/bin/partexp /usr/libexec/rpcd/partexp +/etc/init.d/rpcd restart +rm -f /tmp/luci-indexcache +exit 0 diff --git a/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json b/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json index 39fdd815..98e9c076 100644 --- a/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json +++ b/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json @@ -12,3 +12,4 @@ } } } + diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index f00ae406..465f3216 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -304,6 +304,7 @@ function gen_outbound(flag, node, tag, proxy_table) finalmask.tcp[#finalmask.tcp+1] = api.clone(fragment_table) end if noise and noise_table and (TP == "mkcp" or (TP == "xhttp" and node.alpn == "h3")) then + finalmask.udp = finalmask.udp or {} finalmask.udp[#finalmask.udp+1] = api.clone(noise_table) end if node.finalmask and node.finalmask ~= "" then @@ -759,7 +760,7 @@ function gen_config(var) settings = { packets = xray_settings.fragment_packets, length = xray_settings.fragment_length, - delay = string.find(delay, "-") and delay or tonumber(delay), + delay = delay and (delay:find("-", 1, true) and delay or tonumber(delay)) or nil, maxSplit = xray_settings.fragment_maxSplit } } @@ -770,10 +771,10 @@ function gen_config(var) uci:foreach(appname, "xray_noise_packets", function(n) if n.enabled == "1" then local noise = { - rand = (n.type == "rand") and (string.find(n.packet, "-") and n.packet or tonumber(n.packet)) or nil, + rand = (n.type == "rand" and n.packet) and (n.packet:find("-", 1, true) and n.packet or tonumber(n.packet)) or nil, type = (n.type ~= "rand") and n.type or nil, packet = (n.type ~= "rand") and n.packet or nil, - delay = string.find(n.delay, "-") and n.delay or tonumber(n.delay) + delay = n.delay and (n.delay:find("-", 1, true) and n.delay or tonumber(n.delay)) or nil } table.insert(noises, noise) end diff --git a/luci-app-timecontrol/Makefile b/luci-app-timecontrol/Makefile index a82833fe..ff5ea68c 100644 --- a/luci-app-timecontrol/Makefile +++ b/luci-app-timecontrol/Makefile @@ -1,6 +1,6 @@ # # Copyright (C) 2006-2017 OpenWrt.org -# Copyright (C) 2022-2025 sirpdboy +# Copyright (C) 2022-2026 sirpdboy # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. # @@ -17,8 +17,8 @@ LUCI_DESCRIPTION:=LuCI support for Easy timecontrol for nftables(Internet time c LUCI_DEPENDS:=+bc +nftables +bash +conntrack LUCI_PKGARCH:=all -PKG_VERSION:=3.2.1 -PKG_RELEASE:=2 +PKG_VERSION:=3.2.4 +PKG_RELEASE:=3 PKG_MAINTAINER:=sirpdboy define Build/Compile diff --git a/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/basic.js b/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/basic.js index ace6ba6e..fbc46b9e 100644 --- a/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/basic.js +++ b/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/basic.js @@ -1,3 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + * Copyright (C) 2022-2026 sirpdboy + */ 'use strict'; 'require view'; 'require fs'; @@ -31,13 +36,12 @@ function checkTimeControlProcess() { } } - return { running: running, pid: pid }; + return { running: running, pid: null }; }).catch(function() { return { running: false, pid: null }; }); } -// 渲染服务状态显示 function renderServiceStatus(isRunning, pid) { var statusText = isRunning ? _('RUNNING') : _('NOT RUNNING'); var color = isRunning ? 'green' : 'red'; @@ -114,16 +118,20 @@ return view.extend({ load: function() { return Promise.all([ uci.load('timecontrol'), - network.getDevices() + network.getHostHints() ]); }, render: function(data) { var m, s, o; - var hostList = []; + let hosts = data[1]?.hosts; m = new form.Map('timecontrol', _('Internet Time Control'), _('Users can limit their internet usage time through MAC and IP, with available IP ranges such as 192.168.110.00 to 192.168.10.200') + '
      ' + + _('黑名单模式时间控制方式:') + '
      ' + + _('1. 时间段控制: 指定的机器在设定时间段内可以上网,其他时间不能上网') + '
      ' + + _('2. 允许上机时长: 指定的机器上线后可以上网指定时长,超过时长后不能上网') + '
      ' + + _('3. 组合控制: 在时间段内+时长限制(在允许的时间段内限制上网时长)') + '
      ' + _('Suggested feedback:') + ' GitHub @timecontrol'); s = m.section(form.TypedSection); @@ -171,7 +179,6 @@ return view.extend({ ]) ]); }; - s = m.section(form.TypedSection, 'timecontrol'); s.anonymous = true; s.addremove = false; @@ -202,50 +209,113 @@ return view.extend({ o = s.option(form.Flag, 'enable', _('Enabled')); o.rmempty = false; o.default = '1'; - + o = s.option(form.Value, 'mac', _('IP/MAC Address')); o.rmempty = false; - o.placeholder = '192.168.10.100 or 00:11:22:33:44:55'; - - getHostList().then(function(hosts) { - hostList = hosts; - o.value('', _('-- Please select or enter manually --')); + if (hosts) { + var hostOptions = {}; - if (hosts.length > 0) { - hosts.forEach(function(host) { - var displayName = ''; - if (host.name) { - displayName = host.name + ' - '; - } - displayName += host.ipv4 + ' (' + host.mac + ')'; - - // 添加IP选项 - o.value(host.ipv4, displayName); - - // 添加MAC选项 - var macDisplay = host.mac; - if (host.name) { - macDisplay += ' - ' + host.name; - } - if (host.ipv4) { - macDisplay += ' (' + host.ipv4 + ')'; - } - o.value(host.mac, macDisplay); - }); + Object.keys(hosts).forEach(function(mac) { + var host = hosts[mac]; + var name = host.name || _(' '); + var ips = L.toArray(host.ipaddrs || host.ipv4 || []); + + if (ips.length > 0) { + ips.forEach(function(ip) { + var macDisplay = 'MAC: %s (%s - %s)'.format(mac,ip, name); + hostOptions['mac:' + mac] = macDisplay; + var ipDisplay = 'IP: %s (%s - %s)'.format(ip, mac, name); + hostOptions['ip:' + ip] = ipDisplay; + }); + } + }); + var sortedKeys = Object.keys(hostOptions).sort(function(a, b) { + return hostOptions[a].localeCompare(hostOptions[b]); + }); + + sortedKeys.forEach(function(key) { + if (key.startsWith('ip:')) { + o.value(key.substring(3), hostOptions[key]); + } + }); + + sortedKeys.forEach(function(key) { + if (key.startsWith('mac:')) { + o.value(key.substring(4), hostOptions[key]); + } + }); + } + + // 时间控制方式选择 + o = s.option(cbiRichListValue, 'time_mode', _('Time Control Mode')); + o.value('period', _('Time Period Control (allow in period)')); + o.value('duration', _('Allow Duration Control (allow limited time)')); + o.value('combined', _('Combined Control (allow in period + limit duration)')); + o.default = 'period'; + o.rmempty = false; + o.onchange = function(ev, mode) { + var row = this.map.findElement('id', this.cbid(this.section_id)); + if (row) { + // 显示/隐藏相关字段 + var startTime = row.querySelector('[data-field="timestart"]'); + var endTime = row.querySelector('[data-field="timeend"]'); + var duration = row.querySelector('[data-field="duration"]'); + var useDuration = row.querySelector('[data-field="use_duration"]'); + var resetCycle = row.querySelector('[data-field="reset_cycle"]'); + + if (startTime) startTime.parentElement.style.display = + (mode === 'period' || mode === 'combined') ? '' : 'none'; + if (endTime) endTime.parentElement.style.display = + (mode === 'period' || mode === 'combined') ? '' : 'none'; + if (duration) duration.parentElement.style.display = + (mode === 'duration' || mode === 'combined') ? '' : 'none'; + if (useDuration) useDuration.parentElement.style.display = + (mode === 'combined') ? '' : 'none'; + if (resetCycle) resetCycle.parentElement.style.display = + (mode === 'duration' || mode === 'combined') ? '' : 'none'; } - }); + }; - o = s.option(form.Value, 'timestart', _('Start Control Time')); + // 时间段控制字段 + o = s.option(form.Value, 'timestart', _('Allow Start Time')); o.placeholder = '00:00'; o.default = '00:00'; - o.rmempty = false; + o.depends({ 'time_mode': 'period', '!contains': true }); + o.depends({ 'time_mode': 'combined', '!contains': true }); - o = s.option(form.Value, 'timeend', _('Stop Control Time')); + o = s.option(form.Value, 'timeend', _('Allow End Time')); o.placeholder = '00:00'; o.default = '00:00'; - o.rmempty = false; + o.depends({ 'time_mode': 'period', '!contains': true }); + o.depends({ 'time_mode': 'combined', '!contains': true }); - o = s.option(form.ListValue, 'week', _('Week Day (1~7)')); + // 持续时间控制字段 + o = s.option(form.Value, 'duration', _('Allowed Duration (minutes)')); + o.placeholder = '60'; + o.default = '60'; + o.datatype = 'min(1)'; + o.depends({ 'time_mode': 'duration', '!contains': true }); + o.depends({ 'time_mode': 'combined', '!contains': true }); + o.description = _('设备上线后允许上网的分钟数,超过后将被禁止上网'); + + // 重置周期 + o = s.option(cbiRichListValue, 'reset_cycle', _('Reset Cycle')); + o.value('daily', _('Daily Reset')); + o.value('weekly', _('Weekly Reset')); + o.value('monthly', _('Monthly Reset')); + o.value('never', _('Never Reset (until manual reset)')); + o.default = 'daily'; + o.depends({ 'time_mode': 'duration', '!contains': true }); + o.depends({ 'time_mode': 'combined', '!contains': true }); + o.description = _('时长重置周期'); + + // 组合控制:是否在时间段内启用时长限制 + o = s.option(form.Flag, 'use_duration', _('Enable Duration Limit in Period')); + o.default = '0'; + o.depends({ 'time_mode': 'combined', '!contains': true }); + o.description = _('在允许的时间段内限制上网时长'); + + o = s.option(form.Value, 'week', _('Week Day (1~7)')); o.value('0', _('Everyday')); o.value('1', _('Monday')); o.value('2', _('Tuesday')); @@ -258,6 +328,8 @@ return view.extend({ o.value('6,7', _('Rest Day')); o.default = '0'; o.rmempty = false; + o.description = _('允许上网的星期'); + return m.render(); } }); \ No newline at end of file diff --git a/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/log.js b/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/log.js index d66b5381..bb1c13ae 100644 --- a/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/log.js +++ b/luci-app-timecontrol/htdocs/luci-static/resources/view/timecontrol/log.js @@ -1,4 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2022-2026 sirpdboy + */ 'use strict'; 'require dom'; 'require fs'; @@ -10,7 +14,6 @@ return view.extend({ render: function () { var css = ` - /* 日志框文本区域 */ #log_textarea pre { padding: 10px; /* 内边距 */ border-bottom: 1px solid #ddd; /* 边框颜色 */ @@ -20,15 +23,34 @@ return view.extend({ word-wrap: break-word; overflow-y: auto; } - /* 5s 自动刷新文字 */ .cbi-section small { margin-left: 1rem; font-size: small; - color: #666; /* 深灰色文字 */ + } + .log-container { + display: flex; + flex-direction: column; + max-height: 1200px; + overflow-y: auto; + border-radius: 3px; + margin-top: 10px; + padding: 5px; + } + .log-line { + padding: 3px 0; + font-family: monospace; + font-size: 12px; + line-height: 1.4; + } + .log-line:last-child { + border-bottom: none; + } + .log-timestamp { + margin-right: 10px; } `; - var log_textarea = E('div', { 'id': 'log_textarea' }, + var log_container = E('div', { 'class': 'log-container', 'id': 'log_container' }, E('img', { 'src': L.resource(['icons/loading.gif']), 'alt': _('Loading...'), @@ -38,6 +60,76 @@ return view.extend({ var log_path = '/var/log/timecontrol.log'; var lastLogContent = ''; + var lastScrollTop = 0; + var isScrolledToTop = true; + + // 解析日志行的时间戳,用于排序 + function parseLogTimestamp(logLine) { + // 假设日志格式为: [2024-01-01 12:00:00] INFO: some message + var timestampMatch = logLine.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/); + if (timestampMatch) { + return new Date(timestampMatch[1]).getTime(); + } + return Date.now(); + } + + function reverseLogLines(logContent) { + if (!logContent || logContent.trim() === '') { + return logContent; + } + + var lines = logContent.split('\n'); + + lines = lines.filter(function(line) { + return line.trim() !== ''; + }); + + lines.sort(function(a, b) { + var timeA = parseLogTimestamp(a); + var timeB = parseLogTimestamp(b); + return timeB - timeA; // 降序排列 + }); + + return lines.join('\n'); + } + + function formatLogLines(logContent, isNewContent) { + if (!logContent || logContent.trim() === '') { + return E('div', { 'class': 'log-line' }, _('Log is clean.')); + } + + var lines = logContent.split('\n'); + var formattedLines = []; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line === '') continue; + + var timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/); + var timestampSpan = null; + var messageSpan = null; + var lineClass = 'log-line'; + + if (timestampMatch) { + timestampSpan = E('span', { + 'class': 'log-timestamp', + 'title': timestampMatch[1] + }, timestampMatch[0] + ' '); + messageSpan = E('span', {}, line.substring(timestampMatch[0].length + 1)); + } else { + messageSpan = E('span', {}, line); + } + + var lineDiv = E('div', { 'class': lineClass }, [ + timestampSpan, + messageSpan + ].filter(function(el) { return el !== null; })); + + formattedLines.push(lineDiv); + } + + return E('div', {}, formattedLines); + } var clear_log_button = E('div', {}, [ E('button', { @@ -53,9 +145,10 @@ return view.extend({ button.disabled = false; button.textContent = _('Clear Logs'); // 立即刷新日志显示框 - var log = E('pre', { 'wrap': 'pre' }, [_('Log is clean.')]); - dom.content(log_textarea, log); - lastLogContent = ''; + var logContent = _('Log is clean.'); + lastLogContent = logContent; + dom.content(log_container, formatLogLines(logContent, false)); + isScrolledToTop = true; // 清空日志后,保持在顶部 }) .catch(function () { button.textContent = _('Failed to clear log.'); @@ -65,34 +158,62 @@ return view.extend({ } }, _('Clear Logs')) ]); + log_container.addEventListener('scroll', function() { + lastScrollTop = this.scrollTop; + isScrolledToTop = this.scrollTop <= 1; + }); poll.add(L.bind(function () { return fs.read_direct(log_path, 'text') .then(function (res) { - var newContent = res.trim() || _('Log is clean.'); - - if (newContent !== lastLogContent) { - var log = E('pre', { 'wrap': 'pre' }, [newContent]); - dom.content(log_textarea, log); - log.scrollTop = log.scrollHeight; - lastLogContent = newContent; + var logContent = res.trim(); + if (logContent === '') { + logContent = _('Log is clean.'); + } + + // 检查内容是否有变化 + if (logContent !== lastLogContent) { + var isNewContent = lastLogContent !== '' && lastLogContent !== _('Log is clean.'); + var reversedLog = reverseLogLines(logContent); + // 格式化为HTML + var formattedLog = formatLogLines(reversedLog, isNewContent); + + var prevScrollHeight = log_container.scrollHeight; + var prevScrollTop = log_container.scrollTop; + + dom.content(log_container, formattedLog); + lastLogContent = logContent; + + if (isScrolledToTop || isNewContent) { + log_container.scrollTop = 0; + } else { + var newScrollHeight = log_container.scrollHeight; + var heightDiff = newScrollHeight - prevScrollHeight; + log_container.scrollTop = prevScrollTop + heightDiff; + } } }).catch(function (err) { - var log; + var logContent; if (err.toString().includes('NotFoundError')) { - log = E('pre', { 'wrap': 'pre' }, [_('Log file does not exist.')]); + logContent = _('Log file does not exist.'); } else { - log = E('pre', { 'wrap': 'pre' }, [_('Unknown error: %s').format(err)]); + logContent = _('Unknown error: %s').format(err); + } + + if (logContent !== lastLogContent) { + dom.content(log_container, formatLogLines(logContent, false)); + lastLogContent = logContent; } - dom.content(log_textarea, log); }); })); + // 启动轮询 + poll.start(); return E('div', { 'class': 'cbi-map' }, [ E('style', [css]), E('div', { 'class': 'cbi-section' }, [ clear_log_button, - log_textarea, + log_container, E('small', {}, _('Refresh every 5 seconds.').format(L.env.pollinterval)), E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' }) ]), @@ -100,7 +221,7 @@ return view.extend({ E('span', {}, [ _('© github '), E('a', { - 'href': 'https://github.com/sirpdboy/luci-app-timecontrol', + 'href': 'https://github.com/sirpdboy', 'target': '_blank', 'style': 'text-decoration: none;' }, 'by sirpdboy') diff --git a/luci-app-timecontrol/root/etc/init.d/timecontrol b/luci-app-timecontrol/root/etc/init.d/timecontrol index bbb1aea0..3b1da46a 100755 --- a/luci-app-timecontrol/root/etc/init.d/timecontrol +++ b/luci-app-timecontrol/root/etc/init.d/timecontrol @@ -1,6 +1,7 @@ #!/bin/sh /etc/rc.common + # -# Copyright (C) 2023 sirpdboy herboy2008@gmail.com +# Copyright (C) 2022-2026 sirpdboy herboy2008@gmail.com # START=99 @@ -21,9 +22,9 @@ _timecontrol_start() { if [ "$(grep -c 'option enable .1.' /etc/config/$NAME 2>/dev/null)" -gt "0" ]; then touch $LOCK timecontrol start + sleep 2 start_instance else - echo "INFO: ûκιƵ豸" stop_service fi } @@ -40,11 +41,11 @@ service_triggers() { stop_service(){ kill -9 $(busybox ps -w | grep 'timecontrolctrl' | grep -v 'grep' | awk '{print $1}') >/dev/null 2>&1 + killall timecontrolctrl 2>/dev/null rm -f $LOCK 2>/dev/null timecontrol stop } - reload_service() { restart } diff --git a/luci-app-timecontrol/root/etc/uci-defaults/luci-timecontrol b/luci-app-timecontrol/root/etc/uci-defaults/luci-timecontrol index 6fa184d7..e814dc05 100644 --- a/luci-app-timecontrol/root/etc/uci-defaults/luci-timecontrol +++ b/luci-app-timecontrol/root/etc/uci-defaults/luci-timecontrol @@ -7,7 +7,7 @@ } EEOF } -chmod +x /etc/init.d/timecontrol /usr/bin/timecontrol* +chmod +x /etc/init.d/timecontrol /usr/bin/timecontrol* /usr/libexec/timecontrol-call uci -q batch <<-EOF >/dev/null delete ucitrack.@timecontrol[-1] add ucitrack timecontrol @@ -16,5 +16,7 @@ uci -q batch <<-EOF >/dev/null EOF [ -s /etc/config/timecontrol ] || echo "config timecontrol" > /etc/config/timecontrol + +/etc/init.d/rpcd restart rm -f /tmp/luci-indexcache exit 0 diff --git a/luci-app-timecontrol/root/usr/bin/timecontrol b/luci-app-timecontrol/root/usr/bin/timecontrol index 3cc3417c..2a6bfe35 100755 --- a/luci-app-timecontrol/root/usr/bin/timecontrol +++ b/luci-app-timecontrol/root/usr/bin/timecontrol @@ -1,840 +1,597 @@ #!/bin/bash # Copyright (C) 2006 OpenWrt.org -# Copyright 2022-2025 sirpdboy +# Copyright 2022-2026 sirpdboy crrun=$1 crid=$2 NAME=timecontrol -DEBUG=0 - -config_get_type() { - local ret=$(uci -q get "${NAME}.${1}") - echo "${ret:=$2}" -} - -config_n_get() { - local ret=$(uci -q get "${NAME}.${1}.${2}") - echo "${ret:=$3}" -} +DEBUG=1 # 开启调试 config_t_get() { local index=${3:-0} - local default=$4 local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}") - echo "${ret:-$default}" -} - -config_t_set() { - local index=${3:-0} - local ret=$(uci -q set "${NAME}.@${1}[${index}].${2}=${3}") + echo "${ret:-$4}" } +LOG_FILE="/var/log/timecontrol.log" IDLIST="/var/$NAME.idlist" -TMPID="/var/$NAME.tmpid" -LOG_FILE="/var/log/$NAME.log" -list_type=$(config_t_get $NAME list_type 0 ) - -# 获取控制强度配置 -CHAIN=$(config_t_get $NAME chain 0 ) - bin_nft=$(which nft 2>/dev/null) bin_iptables=$(which iptables 2>/dev/null) bin_ip6tables=$(which ip6tables 2>/dev/null) bin_conntrack=$(which conntrack 2>/dev/null) -is_input_chain() { - [ "$CHAIN" = "input" ] -} +# 获取配置 +chain=$(config_t_get timecontrol chain 0 "forward") +list_type=$(config_t_get timecontrol list_type 0 "blacklist") + +if [ "$chain" = "input" ]; then + StrongCHAIN=1 +else + StrongCHAIN=0 +fi dbg() { - if [ "$DEBUG" -eq 1 ] ;then - local d="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$d] DEBUG: $@ " >>$LOG_FILE - echo "DEBUG: $@" # 直接输出到终端以便调试 + if [ "$DEBUG" -eq 1 ]; then + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] FW-DEBUG: $@" >> "$LOG_FILE" + echo "FW-DEBUG: $@" fi } info() { local d="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$d] INFO: $@" >>$LOG_FILE - echo "INFO: $@" + echo "[$d] FW-INFO: $@" >> "$LOG_FILE" + echo "FW-INFO: $@" } +# 地址解析函数 - 修复格式问题 parse_target() { local target="$1" # 去除空格 target=$(echo "${target}" | xargs) - # 1. 单个IP地址 (192.168.1.100) + dbg "解析目标地址: $target" + + # IPv4单个地址 if echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then local octets=(${target//./ }) + local valid=1 for octet in "${octets[@]}"; do - [ "$octet" -le 255 ] || return 1 + if [ "$octet" -gt 255 ] || [ "$octet" -lt 0 ]; then + valid=0 + break + fi done - echo "ipv4:single:${target}" - return 0 + [ "$valid" -eq 1 ] && { + echo "ipv4:single:$target" + return 0 + } - # 2. IP范围 (192.168.1.1-192.168.1.100) + # IPv4范围 elif echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}-([0-9]{1,3}\.){3}[0-9]{1,3}$'; then local start_ip=${target%-*} local end_ip=${target#*-} - - # 验证起始IP和结束IP - local start_octets=(${start_ip//./ }) - local end_octets=(${end_ip//./ }) - for i in {0..3}; do - [ "${start_octets[$i]}" -le 255 ] && [ "${end_octets[$i]}" -le 255 ] || return 1 - done - - echo "ipv4:range:${target}" + echo "ipv4:range:$start_ip-$end_ip" return 0 - # 3. CIDR格式 (192.168.1.0/24) + # CIDR elif echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'; then local ip=${target%/*} local mask=${target#*/} - - # 验证掩码 - if [ "$mask" -ge 0 ] && [ "$mask" -le 32 ]; then - local octets=(${ip//./ }) - for octet in "${octets[@]}"; do - [ "$octet" -le 255 ] || return 1 - done - echo "ipv4:cidr:${target}" + [ "$mask" -le 32 ] && [ "$mask" -ge 0 ] && { + echo "ipv4:cidr:$target" return 0 - fi + } - # 4. MAC地址 (00:11:22:33:44:55) + # MAC地址 elif echo "$target" | grep -qE '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'; then - echo "mac:single:${target,,}" # 转换为小写 + echo "mac:single:$(echo "$target" | tr '[:upper:]' '[:lower:]')" return 0 - # 5. IPv6地址(简单验证) + # IPv6地址 elif echo "$target" | grep -qE '^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$'; then - echo "ipv6:single:${target}" + echo "ipv6:single:$target" return 0 - # 6. IPv6 CIDR + # IPv6 CIDR elif echo "$target" | grep -qE '^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/[0-9]{1,3}$'; then local ipv6=${target%/*} local mask=${target#*/} - if [ "$mask" -ge 0 ] && [ "$mask" -le 128 ]; then - echo "ipv6:cidr:${target}" + [ "$mask" -le 128 ] && [ "$mask" -ge 0 ] && { + echo "ipv6:cidr:$target" return 0 - fi + } fi + dbg "无法解析地址: $target" return 1 } -convert_to_old_format() { - local target="$1" - local parsed_result=$(parse_target "$target") - [ $? -ne 0 ] && return 1 - - IFS=':' read -r type subtype value <<< "$parsed_result" - - case "$type" in - "ipv4") - case "$subtype" in - "single") - echo "ip ipv4_addr $value" - ;; - "range") - # 转换为nftables范围格式 - local start_ip=${value%-*} - local end_ip=${value#*-} - echo "ip ipv4_addr { $start_ip-$end_ip }" - ;; - "cidr") - echo "ip ipv4_addr $value" - ;; - esac - ;; - "mac") - echo "bridge ether_addr $value" - ;; - "ipv6") - case "$subtype" in - "single") echo "ip6 ipv6_addr $value" ;; - "cidr") echo "ip6 ipv6_addr $value" ;; - esac - ;; - esac -} - +# 清理现有连接 flush_connections() { local target="$1" - if [ -x "$bin_conntrack" ]; then - local parsed_result=$(parse_target "${target}") - if [ $? -eq 0 ]; then - IFS=':' read -r type subtype value <<< "${parsed_result}" - - case "${type}" in - "ipv4") - case "$subtype" in - "single") - $bin_conntrack -D -s "${value}" 2>/dev/null || true - $bin_conntrack -D -d "${value}" 2>/dev/null || true - dbg "清理单个IP连接: ${value}" - ;; - "range") - # 对于IP范围,需要转换为CIDR或逐个清理 - # 这里简化处理:提取起始IP的第一个八位组 - local start_ip=${value%-*} - local network="${start_ip%.*}.0/24" # 假设为/24网络 - $bin_conntrack -D -s "${network}" 2>/dev/null || true - dbg "清理IP范围连接: ${value}" - ;; - "cidr") - $bin_conntrack -D -s "${value}" 2>/dev/null || true - $bin_conntrack -D -d "${value}" 2>/dev/null || true - dbg "清理CIDR连接: ${value}" - ;; - esac - info "断开连接: ${target}" - ;; - "mac") - # 从ARP表查找IP - if [ -f "/proc/net/arp" ]; then - local ip_addr=$(grep -i "${value}" /proc/net/arp 2>/dev/null | awk '{print $1}' | head -1) - if [ -n "${ip_addr}" ]; then - $bin_conntrack -D -s "${ip_addr}" 2>/dev/null || true - $bin_conntrack -D -d "${ip_addr}" 2>/dev/null || true - dbg "清理MAC连接: ${value} (IP: ${ip_addr})" - info "断开MAC连接: ${value}" - fi - fi - ;; - "ipv6") - dbg "IPv6连接清理暂不支持: ${value}" - ;; - esac - else - dbg "无法解析目标地址: ${target}" - fi - else - dbg "conntrack未安装,跳过连接清理" - fi -} - -send_tcp_reset() { - local target="$1" + [ -x "$bin_conntrack" ] || { + dbg "conntrack不可用" + return + } - [ "$TCP_RST" != "1" ] && return - - local parsed_result=$(parse_target "${target}") - [ $? -ne 0 ] && return - - IFS=':' read -r type subtype value <<< "${parsed_result}" - [ "$type" != "ipv4" ] && return - - info "发送TCP RST包: ${target}" - - # ============ iptables ============ - if [ -n "$iptables_ver" ]; then - case "$subtype" in - "single"|"cidr") - iptables -I FORWARD -s "$value" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - [ "$StrongCHAIN" = "1" ] && iptables -I INPUT -s "$value" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - ;; - "range") - local start_ip=${value%-*} - local end_ip=${value#*-} - iptables -I FORWARD -m iprange --src-range "$start_ip-$end_ip" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - [ "$StrongCHAIN" = "1" ] && iptables -I INPUT -m iprange --src-range "$start_ip-$end_ip" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - ;; - esac - fi - - # ============ nftables ============ - if [ -n "$nftables_ver" ]; then - case "$subtype" in - "single"|"cidr") - nft add rule inet timecontrol forward ip saddr "$value" tcp flags rst counter reject with tcp reset 2>/dev/null || true - [ "$StrongCHAIN" = "1" ] && nft add rule inet timecontrol input ip saddr "$value" tcp flags rst counter reject with tcp reset 2>/dev/null || true - ;; - "range") - local start_ip=${value%-*} - local end_ip=${value#*-} - nft add rule inet timecontrol forward ip saddr >= "$start_ip" ip saddr <= "$end_ip" tcp flags rst counter reject with tcp reset 2>/dev/null || true - [ "$StrongCHAIN" = "1" ] && nft add rule inet timecontrol input ip saddr >= "$start_ip" ip saddr <= "$end_ip" tcp flags rst counter reject with tcp reset 2>/dev/null || true - ;; - esac - fi - - # 10秒后自动删除 - (sleep 10; remove_tcp_reset "$target") & -} - -remove_tcp_reset() { - local target="$1" local parsed_result=$(parse_target "$target") - [ $? -ne 0 ] && return + [ $? -eq 0 ] || { + dbg "无法解析地址用于清理连接: $target" + return + } IFS=':' read -r type subtype value <<< "$parsed_result" - [ "$type" != "ipv4" ] && return - # iptables删除 - if [ -n "$iptables_ver" ]; then - case "$subtype" in - "single"|"cidr") - iptables -D FORWARD -s "$value" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - [ "$StrongCHAIN" = "1" ] && iptables -D INPUT -s "$value" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - ;; - "range") - local start_ip=${value%-*} - local end_ip=${value#*-} - iptables -D FORWARD -m iprange --src-range "$start_ip-$end_ip" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - [ "$StrongCHAIN" = "1" ] && iptables -D INPUT -m iprange --src-range "$start_ip-$end_ip" -p tcp -j REJECT --reject-with tcp-reset 2>/dev/null || true - ;; - esac - fi + dbg "清理连接: type=$type, value=$value" - # nftables删除(简化:重新加载表) - if [ -n "$nftables_ver" ]; then - # 简单的做法是重新创建表 - nft delete table inet timecontrol 2>/dev/null || true - # 重新初始化 - init_timecontrol - fi + case "$type" in + "ipv4") + $bin_conntrack -D -s "$value" 2>/dev/null && dbg "清理源连接: $value" + $bin_conntrack -D -d "$value" 2>/dev/null && dbg "清理目标连接: $value" + ;; + "mac") + # MAC地址需要先转换为IP + if [ -f "/proc/net/arp" ]; then + local ip_addr=$(grep -i "$value" /proc/net/arp 2>/dev/null | awk '{print $1}' | head -1) + if [ -n "$ip_addr" ]; then + $bin_conntrack -D -s "$ip_addr" 2>/dev/null && dbg "清理MAC源连接: $value -> $ip_addr" + $bin_conntrack -D -d "$ip_addr" 2>/dev/null && dbg "清理MAC目标连接: $value -> $ip_addr" + fi + fi + ;; + esac } - -# Check if nftables/iptables is available -if [ -x "$bin_nft" ]; then - nftables_ver="true" - dbg "nft found: $bin_nft" -elif [ -x "$bin_iptables" ] || [ -x "$bin_ip6tables" ]; then - iptables_ver="true" - dbg "iptables found: $bin_iptables" -else - dbg "No firewall tool found!" - exit 1 -fi - -nft() { - dbg "nft $*" - $bin_nft "$@" 2>/dev/null -} - -iptables() { - dbg "iptables $*" - $bin_iptables "$@" 2>/dev/null -} - -ip6tables() { - dbg "ip6tables $*" - $bin_ip6tables "$@" 2>/dev/null -} - -get_target_info() { - local target="$1" - local result=$(convert_to_old_format "$target") - if [ $? -eq 0 ]; then - echo "$result" - return 0 - fi - target=$(echo "${target}" | xargs) - if echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then - local octets=(${target//./ }) - for octet in "${octets[@]}"; do - [ "$octet" -le 255 ] || return 1 - done - table="ip" - addr_type="ipv4_addr" - elif echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}-([0-9]{1,3}\.){3}[0-9]{1,3}$'; then - local start_ip=${target%-*} - local end_ip=${target#*-} - table="ip" - addr_type="ipv4_addr" - target="{ $start_ip-$end_ip }" - elif echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'; then - local ip=${target%/*} - local mask=${target#*/} - [ "$mask" -le 32 ] || return 1 - table="ip" - addr_type="ipv4_addr" - elif echo "$target" | grep -qE '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'; then - table="bridge" - addr_type="ether_addr" - target=$(echo "$target" | tr '[:upper:]' '[:lower:]') - elif echo "$target" | grep -qE '^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$'; then - table="ip6" - addr_type="ipv6_addr" +# 检查防火墙工具 +check_firewall_tool() { + if [ -x "$bin_nft" ]; then + nftables_ver=1 + dbg "检测到nftables: $bin_nft" + elif [ -x "$bin_iptables" ] && [ -x "$bin_ip6tables" ]; then + iptables_ver=1 + dbg "检测到iptables: $bin_iptables, $bin_ip6tables" else + info "错误: 未找到可用的防火墙工具" return 1 fi - - echo "$table $addr_type $target" + return 0 } -nft_table_exists() { - local table=$1 - nft list tables | grep -q "$table" -} - -nft_set_exists() { - local table=$1 - local set=$2 - nft list sets | grep -q "$table $set" -} - -nft_rule_exists() { - local table=$1 - local chain=$2 - local rule=$3 - nft list chain "$table" "$chain" | grep -q "$rule" -} - -stop_timecontrol() { - remove_tcp_reset - if [ -n "$nftables_ver" ]; then - for chain in "ip" "bridge" "ip6"; do - if nft_table_exists "$chain filter"; then - if [ "$StrongCHAIN" -eq 1 ]; then - nft flush chain "$chain" filter input - nft delete chain "$chain" filter input 2>/dev/null - fi - nft flush chain "$chain" filter forward - nft delete chain "$chain" filter forward 2>/dev/null - - if nft_set_exists "$chain filter" "${list_type}_list"; then - nft delete set "$chain" filter "${list_type}_list" - fi - nft delete table "$chain" filter - fi - done - dbg "All nftables rules have been cleared." - elif [ -n "$iptables_ver" ]; then +# 初始化防火墙 +init_firewall() { + check_firewall_tool || return 1 - + info "初始化防火墙规则 (模式: $list_type, 强度: $chain)" + + if [ -n "$nftables_ver" ]; then + # 使用nftables + dbg "初始化nftables" + + # 删除可能存在的旧表 + nft delete table inet timecontrol 2>/dev/null + sleep 1 + + # 创建新表 + nft add table inet timecontrol + nft add chain inet timecontrol forward "{ type filter hook forward priority -100; policy accept; }" + + # 创建黑名单集合 + nft add set inet timecontrol blacklist "{ type ipv4_addr; flags interval; }" + nft add set inet timecontrol blacklist6 "{ type ipv6_addr; flags interval; }" + nft add set inet timecontrol blacklist_mac "{ type ether_addr; }" + + # 添加规则(黑名单模式:匹配到就DROP) + nft add rule inet timecontrol forward ip saddr @blacklist drop + nft add rule inet timecontrol forward ip6 saddr @blacklist6 drop + nft add rule inet timecontrol forward ether saddr @blacklist_mac drop + + # 强控制模式 + if [ "$StrongCHAIN" -eq 1 ]; then + nft add chain inet timecontrol input "{ type filter hook input priority -100; policy accept; }" + nft add rule inet timecontrol input ip saddr @blacklist drop + nft add rule inet timecontrol input ip6 saddr @blacklist6 drop + nft add rule inet timecontrol input ether saddr @blacklist_mac drop + dbg "已启用强控制模式 (INPUT链)" + fi + + info "nftables初始化完成" + + elif [ -n "$iptables_ver" ]; then + # 使用iptables + dbg "初始化iptables" + + # 创建ipset(如果不存在) + ipset create timecontrol_blacklist hash:net 2>/dev/null || { + ipset flush timecontrol_blacklist + dbg "已存在的ipset timecontrol_blacklist已清空" + } + + ipset create timecontrol_blacklist6 hash:net family inet6 2>/dev/null || { + ipset flush timecontrol_blacklist6 + dbg "已存在的ipset timecontrol_blacklist6已清空" + } + + # 删除可能存在的旧规则 + iptables -D FORWARD -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null + ip6tables -D FORWARD -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null + + # 添加新规则(黑名单模式) + iptables -I FORWARD -m set --match-set timecontrol_blacklist src -j DROP + ip6tables -I FORWARD -m set --match-set timecontrol_blacklist6 src -j DROP + + dbg "已添加FORWARD规则" + + # 强控制模式 if [ "$StrongCHAIN" -eq 1 ]; then iptables -D INPUT -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null - ip6tables -D INPUT -m set --match-set timecontrol_blacklistv6 src -j DROP 2>/dev/null - fi - - iptables -D FORWARD -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null - ip6tables -D FORWARD -m set --match-set timecontrol_blacklistv6 src -j DROP 2>/dev/null - - ipset flush timecontrol_blacklist 2>/dev/null - ipset flush timecontrol_blacklistv6 2>/dev/null - ipset destroy timecontrol_blacklist 2>/dev/null - ipset destroy timecontrol_blacklistv6 2>/dev/null - dbg "Deleted iptables rules and ipsets" - fi - - rm -rf "$IDLIST" "$TMPID" -} - -init_timecontrol() { - info "初始化时间控制" - - if [ -n "$nftables_ver" ]; then - for chain in "ip" "bridge" "ip6"; do - case $chain in - ip) - addr_type="ipv4_addr" - ip_protocol="ip" - ;; - ip6) - addr_type="ipv6_addr" - ip_protocol="ip6" - ;; - bridge) - addr_type="ether_addr" - ip_protocol="ether" - ;; - esac + ip6tables -D INPUT -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null - if ! nft_table_exists "$chain filter"; then - nft add table "$chain" filter - dbg "Created table $chain filter" - fi + iptables -I INPUT -m set --match-set timecontrol_blacklist src -j DROP + ip6tables -I INPUT -m set --match-set timecontrol_blacklist6 src -j DROP - if ! nft_set_exists "$chain filter" "${list_type}_list"; then - nft add set "$chain" filter "${list_type}_list" "{ type $addr_type; flags interval; }" - dbg "Created set ${list_type}_list in $chain filter" - fi - - if [ "$StrongCHAIN" -eq 1 ]; then - if ! nft list chain "$chain" filter input 2>/dev/null; then - nft add chain "$chain" filter input "{ type filter hook input priority -100; }" - dbg "Created INPUT chain in $chain filter (强控制)" - fi - fi - - if ! nft list chain "$chain" filter forward 2>/dev/null; then - nft add chain "$chain" filter forward "{ type filter hook forward priority -100; }" - dbg "Created FORWARD chain in $chain filter" - fi - done - elif [ -n "$iptables_ver" ]; then - ipset create timecontrol_blacklist hash:net 2>/dev/null || ipset flush timecontrol_blacklist - ipset create timecontrol_blacklistv6 hash:net family inet6 2>/dev/null || ipset flush timecontrol_blacklistv6 - - dbg "Created ipsets: timecontrol_blacklist and timecontrol_blacklistv6" - - if [ "$StrongCHAIN" -eq 1 ]; then - if ! iptables -C INPUT -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null; then - iptables -I INPUT -m set --match-set timecontrol_blacklist src -j DROP - dbg "Added INPUT chain rule for IPv4 (强控制)" - fi - - if ! ip6tables -C INPUT -m set --match-set timecontrol_blacklistv6 src -j DROP 2>/dev/null; then - ip6tables -I INPUT -m set --match-set timecontrol_blacklistv6 src -j DROP - dbg "Added INPUT chain rule for IPv6 (强控制)" - fi + dbg "已启用强控制模式 (INPUT链)" fi - if ! iptables -C FORWARD -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null; then - iptables -I FORWARD -m set --match-set timecontrol_blacklist src -j DROP - dbg "Added FORWARD chain rule for IPv4" - fi - - if ! ip6tables -C FORWARD -m set --match-set timecontrol_blacklistv6 src -j DROP 2>/dev/null; then - ip6tables -I FORWARD -m set --match-set timecontrol_blacklistv6 src -j DROP - dbg "Added FORWARD chain rule for IPv6" - fi - fi - - info "初始化完成" -} - - -timeadd() { - local id=$1 - local target=$(config_t_get device mac "$id") - [ -z "$target" ] && return - - info "添加设备控制: $target" - - local parsed_result=$(parse_target "$target") - if [ $? -ne 0 ]; then - dbg "无法解析目标地址: $target" - return - fi - - local target_info=$(convert_to_old_format "$target") - if [ $? -ne 0 ]; then - dbg "无法转换目标格式: $target" - return - fi - - read -r table addr_type target_val <<< "$target_info" - - case $table in - ip) - saddr="ip saddr" - daddr="ip daddr" - ipset_name="timecontrol_blacklist" - ;; - ip6) - saddr="ip6 saddr" - daddr="ip6 daddr" - ipset_name="timecontrol_blacklistv6" - ;; - bridge) - saddr="ether saddr" - daddr="ether daddr" - ipset_name="timecontrol_blacklistbridge" - ;; - *) - dbg "未知的table类型: $table" - return - ;; - esac - - if [ -n "$nftables_ver" ]; then - nft add element "$table" filter "${list_type}_list" "{ $target_val }" - - if [ "$StrongCHAIN" -eq 1 ]; then - if ! nft list chain "$table" filter input 2>/dev/null | grep -q "@${list_type}_list"; then - nft add rule "$table" filter input "$saddr" @"${list_type}_list" drop - dbg "Added INPUT rule for $target in table $table (强控制)" - fi - fi - if ! nft list chain "$table" filter forward 2>/dev/null | grep -q "@${list_type}_list"; then - nft add rule "$table" filter forward "$saddr" @"${list_type}_list" drop - dbg "Added FORWARD rule for $target in table $table" - fi - - elif [ -n "$iptables_ver" ]; then - # 对于iptables,需要根据不同类型处理 - IFS=':' read -r type subtype value <<< "$parsed_result" - - case "$type" in - "ipv4") - case "$subtype" in - "single"|"cidr") - ipset add "$ipset_name" "$value" 2>/dev/null || true - ;; - "range") - # IP范围需要特殊处理 - local start_ip=${value%-*} - local end_ip=${value#*-} - # 将范围转换为多个CIDR或使用iprange模块 - ipset add "$ipset_name" "$start_ip-$end_ip" 2>/dev/null || true - ;; - esac - ;; - "mac") - ipset add "$ipset_name" "$value" 2>/dev/null || true - ;; - esac - dbg "Added $target to ipset $ipset_name" - fi -if [ "$StrongCHAIN" -eq 1 ]; then - flush_connections "$target" - send_tcp_reset "$target" -fi -} - - -timedel() { - local id=$1 - local target=$(config_t_get device mac "$id") - [ -z "$target" ] && return - - info "移除设备控制: $target" - - # 解析目标 - local parsed_result=$(parse_target "$target") - if [ $? -ne 0 ]; then - dbg "无法解析目标地址: $target" - return - fi - - local target_info=$(convert_to_old_format "$target") - if [ $? -ne 0 ]; then - dbg "无法转换目标格式: $target" - return - fi - - read -r table addr_type target_val <<< "$target_info" - - case $table in - ip) ipset_name="timecontrol_blacklist" ;; - ip6) ipset_name="timecontrol_blacklistv6" ;; - bridge) ipset_name="timecontrol_blacklistbridge" ;; - *) return ;; - esac - - if [ -n "$nftables_ver" ]; then - nft delete element "$table" filter "${list_type}_list" "{ $target_val }" 2>/dev/null - dbg "Removed $target_val from ${list_type}_list in table $table" - else - IFS=':' read -r type subtype value <<< "$parsed_result" - ipset del "$ipset_name" "$value" 2>/dev/null || true - dbg "Removed $target from ipset $ipset_name" - fi -} - - -check_time() { - local start=$1 - local end=$2 - local current=$(date +%H%M) - local start_min=$((10#${start:0:2}*60 + 10#${start:3:2})) - local end_min=$((10#${end:0:2}*60 + 10#${end:3:2})) - local current_min=$((10#${current:0:2}*60 + 10#${current:2:2})) - - if [[ $start_min -lt $end_min ]]; then - [[ $current_min -ge $start_min && $current_min -lt $end_min ]] - else - [[ $current_min -ge $start_min || $current_min -lt $end_min ]] - fi -} - -check_list() { - local i=$1 - local start_time=$(config_t_get device timestart "$i") - local end_time=$(config_t_get device timeend "$i") - local wweek=$(config_t_get device week "$i") - local current_weekday=$(date +%u) - - # 检查时间 - check_time "$start_time" "$end_time" || return 1 - - # 检查星期 - if [ "$wweek" != "0" ]; then - echo "$wweek" | grep -qw "$current_weekday" || return 1 + info "iptables初始化完成" fi return 0 } - -test_parse() { - echo "=== 测试地址解析 ===" +# 停止防火墙规则 +stop_firewall() { + info "停止防火墙规则" - local test_cases=( - "192.168.1.100" - "192.168.1.1-192.168.1.100" - "192.168.1.0/24" - "192.168.10.1-192.168.10.254" - "192.168.10.0/24" - "00:11:22:33:44:55" - "2001:db8::1" - "invalid_address" - ) - - for test_case in "${test_cases[@]}"; do - echo -n "测试 '$test_case': " - if parse_target "$test_case" >/dev/null; then - echo "✓ 通过" - parse_target "$test_case" - else - echo "✗ 失败" - fi - echo "" - done -} - - -show_status() { - echo "" - echo "┌────────────────────────────────────────────┐" - echo "│ 时间控制系统状态 │" - echo "├────────────────────────────────────────────┤" - - # 控制模式 - if [ "$StrongCHAIN" -eq 1 ]; then - echo "│ 控制模式: 强力控制 (INPUT链) │" - echo "│ 阻止访问路由器和互联网,立即生效 │" - else - echo "│ 控制模式: 普通控制 (FORWARD链) │" - echo "│ 仅阻止互联网访问,可能有延迟 │" + if [ -n "$nftables_ver" ]; then + nft delete table inet timecontrol 2>/dev/null && info "nftables规则已删除" fi - echo "├────────────────────────────────────────────┤" - - # 当前时间 - echo "│ 当前时间: $(date '+%Y-%m-%d %H:%M:%S') │" - echo "│ 星期: $(date '+%u' | sed 's/1/星期一/;s/2/星期二/;s/3/星期三/;s/4/星期四/;s/5/星期五/;s/6/星期六/;s/7/星期日/') │" - echo "├────────────────────────────────────────────┤" + if [ -n "$iptables_ver" ]; then + # 删除iptables规则 + iptables -D FORWARD -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null + iptables -D INPUT -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null + ip6tables -D FORWARD -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null + ip6tables -D INPUT -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null + + # 删除ipset + ipset destroy timecontrol_blacklist 2>/dev/null + ipset destroy timecontrol_blacklist6 2>/dev/null + + info "iptables规则已删除" + fi - if [ -s "$IDLIST" ]; then - local device_count=$(wc -l < $IDLIST 2>/dev/null || echo "0") - echo "│ 控制设备数: $device_count 个 │" + # 清理ID列表 + rm -f "$IDLIST" +} + +# 添加设备到防火墙 +add_device() { + local id="$1" + local target=$(config_t_get device mac "$id") + [ -z "$target" ] && { + dbg "添加设备失败: ID $id 的目标地址为空" + return + } - # 显示前5个设备 - echo "├────────────────────────────────────────────┤" - echo "│ 控制中的设备: │" - local count=0 - cat "$IDLIST" | sed 's/!//g' | head -5 | while read id; do - count=$((count + 1)) + local comment=$(config_t_get device comment "$id" "设备$id") + info "添加设备到防火墙: $comment ($target)" + + local parsed_result=$(parse_target "$target") + if [ $? -ne 0 ]; then + info "添加失败: 无法解析地址 $target" + return + fi + + IFS=':' read -r type subtype value <<< "$parsed_result" + dbg "解析结果: type=$type, subtype=$subtype, value=$value" + + if [ -n "$nftables_ver" ]; then + # nftables处理 + case "$type" in + "ipv4") + nft add element inet timecontrol blacklist "{ $value }" 2>&1 | while read line; do dbg "nft: $line"; done + dbg "已添加到nftables黑名单(IPv4): $value" + ;; + "ipv6") + nft add element inet timecontrol blacklist6 "{ $value }" 2>&1 | while read line; do dbg "nft: $line"; done + dbg "已添加到nftables黑名单(IPv6): $value" + ;; + "mac") + nft add element inet timecontrol blacklist_mac "{ $value }" 2>&1 | while read line; do dbg "nft: $line"; done + dbg "已添加到nftables黑名单(MAC): $value" + ;; + esac + + elif [ -n "$iptables_ver" ]; then + # iptables处理 + case "$type" in + "ipv4") + ipset add timecontrol_blacklist "$value" 2>&1 | while read line; do dbg "ipset: $line"; done + dbg "已添加到ipset黑名单(IPv4): $value" + ;; + "ipv6") + ipset add timecontrol_blacklist6 "$value" 2>&1 | while read line; do dbg "ipset: $line"; done + dbg "已添加到ipset黑名单(IPv6): $value" + ;; + "mac") + # iptables不支持MAC地址直接过滤,记录日志 + info "警告: iptables不支持MAC地址过滤,设备 $target 可能无法被阻止" + ;; + esac + fi + + # 强控制模式清理连接 + if [ "$StrongCHAIN" -eq 1 ]; then + dbg "强控制模式,清理现有连接" + flush_connections "$target" + fi + + # 验证规则 + verify_firewall_rule "$target" +} + +# 验证防火墙规则 +verify_firewall_rule() { + local target="$1" + + dbg "验证防火墙规则: $target" + + if [ -n "$nftables_ver" ]; then + nft list table inet timecontrol 2>/dev/null | grep -q "$target" && { + dbg "验证成功: $target 在nftables规则中" + return 0 + } + elif [ -n "$iptables_ver" ]; then + ipset test timecontrol_blacklist "$target" 2>/dev/null && { + dbg "验证成功: $target 在ipset中" + return 0 + } + fi + + dbg "验证失败: $target 不在防火墙规则中" + return 1 +} + +# 从防火墙移除设备 +del_device() { + local id="$1" + local target=$(config_t_get device mac "$id") + [ -z "$target" ] && { + dbg "移除设备失败: ID $id 的目标地址为空" + return + } + + local comment=$(config_t_get device comment "$id" "设备$id") + info "从防火墙移除设备: $comment ($target)" + + local parsed_result=$(parse_target "$target") + [ $? -eq 0 ] || { + info "移除失败: 无法解析地址 $target" + return + } + + IFS=':' read -r type subtype value <<< "$parsed_result" + + if [ -n "$nftables_ver" ]; then + case "$type" in + "ipv4") + nft delete element inet timecontrol blacklist "{ $value }" 2>/dev/null + dbg "已从nftables移除(IPv4): $value" + ;; + "ipv6") + nft delete element inet timecontrol blacklist6 "{ $value }" 2>/dev/null + dbg "已从nftables移除(IPv6): $value" + ;; + "mac") + nft delete element inet timecontrol blacklist_mac "{ $value }" 2>/dev/null + dbg "已从nftables移除(MAC): $value" + ;; + esac + + elif [ -n "$iptables_ver" ]; then + case "$type" in + "ipv4") + ipset del timecontrol_blacklist "$value" 2>/dev/null + dbg "已从ipset移除(IPv4): $value" + ;; + "ipv6") + ipset del timecontrol_blacklist6 "$value" 2>/dev/null + dbg "已从ipset移除(IPv6): $value" + ;; + esac + fi +} + +# 显示防火墙状态 +show_firewall_status() { + echo "" + echo "防火墙状态:" + echo "控制模式: $list_type" + echo "控制强度: $chain $( [ "$StrongCHAIN" -eq 1 ] && echo "(强控制)" )" + echo "" + + if [ -n "$nftables_ver" ]; then + echo "nftables规则:" + nft list table inet timecontrol 2>/dev/null || echo " 未找到timecontrol表" + elif [ -n "$iptables_ver" ]; then + echo "iptables规则:" + echo "FORWARD链:" + iptables -L FORWARD -n | grep -i timecontrol || echo " 未找到timecontrol规则" + ip6tables -L FORWARD -n | grep -i timecontrol || echo " 未找到IPv6 timecontrol规则" + + if [ "$StrongCHAIN" -eq 1 ]; then + echo "" + echo "INPUT链:" + iptables -L INPUT -n | grep -i timecontrol || echo " 未找到timecontrol规则" + ip6tables -L INPUT -n | grep -i timecontrol || echo " 未找到IPv6 timecontrol规则" + fi + + echo "" + echo "ipset内容:" + ipset list timecontrol_blacklist 2>/dev/null | head -20 || echo " timecontrol_blacklist未找到" + echo "" + ipset list timecontrol_blacklist6 2>/dev/null | head -20 || echo " timecontrol_blacklist6未找到" + fi +} + +# 诊断函数 +diagnose() { + echo "" + echo "=== 时间控制系统诊断 ===" + echo "" + + # 检查服务 + echo "1. 服务状态:" + if ps | grep -q "timecontrolctrl"; then + echo " ✓ timecontrolctrl 正在运行" + else + echo " ✗ timecontrolctrl 未运行" + fi + + # 检查配置文件 + echo "" + echo "2. 配置文件:" + if [ -f "/etc/config/timecontrol" ]; then + echo " ✓ 配置文件存在" + uci show timecontrol 2>/dev/null | grep -c "device" | while read count; do + echo " 配置了 $count 个设备" + done + else + echo " ✗ 配置文件不存在" + fi + + # 检查防火墙工具 + echo "" + echo "3. 防火墙工具:" + if [ -x "$bin_nft" ]; then + echo " ✓ nftables: $bin_nft" + echo " 版本: $($bin_nft --version 2>/dev/null | head -1)" + elif [ -x "$bin_iptables" ]; then + echo " ✓ iptables: $bin_iptables" + echo " 版本: $($bin_iptables --version 2>/dev/null | head -1)" + else + echo " ✗ 未找到防火墙工具" + fi + + # 显示当前规则 + show_firewall_status + + # 检查ID列表 + echo "" + echo "4. 当前控制列表:" + if [ -f "$IDLIST" ] && [ -s "$IDLIST" ]; then + echo " 当前禁止的设备:" + cat "$IDLIST" | sed 's/!//g' | while read id; do local target=$(config_t_get device mac "$id") local comment=$(config_t_get device comment "$id" "设备$id") - printf "│ %2d %-15s %-20s │\n" $count "$comment" "$target" + echo " ID$id: $comment ($target)" done - - if [ $device_count -gt 5 ]; then - echo "│ ... 还有 $(($device_count - 5)) 个设备 │" - fi else - - echo "│ 控制设备数: 0 个 │" + echo " 当前没有设备被禁止" fi - echo "└────────────────────────────────────────────┘" echo "" + echo "=== 诊断完成 ===" } -show_help() { - echo "上网时间控制系统" - echo "" - echo "支持的地址格式:" - echo " 1. 单个IP: 192.168.1.100" - echo " 2. IP范围: 192.168.1.1-192.168.1.100" - echo " 3. CIDR: 192.168.1.0/24" - echo " 4. MAC地址: 00:11:22:33:44:55" - echo "" - echo "强力控制新增功能:" - echo " - TCP RST包: 立即断开现有TCP连接" - echo " - 连接清理: 清除conntrack表中的连接" - echo "" - echo "用法: $0 {start|stop|add |del |status|test|help}" - echo "" - echo "配置: 编辑 /etc/config/timecontrol" - echo "日志: 查看 /var/log/timecontrol.log" -} - -if is_input_chain; then - StrongCHAIN=1 -else - StrongCHAIN=0 -fi - +# 主命令处理 case "$crrun" in - "stop") - info "停止时间控制服务" - stop_timecontrol - info "所有防火墙规则已清除" - ;; "start") - stop_timecontrol - echo -e "\n====== 启动时间控制 ======" > /var/log/timecontrol.log - touch $IDLIST - idlist=$(uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sed -e 's/^/!/g' -e 's/$/!/g' > "$IDLIST"; cat "$IDLIST" | sed -e 's/!//g') - init_timecontrol - for list in $(echo "$idlist" | sed -e 's/!//g'); do - if check_list "$list"; then - timeadd "$list" - info "设备 $list 已添加到控制列表" - else - if grep -q "!${list}!" "$IDLIST"; then - timedel "$list" - sed -i "/!$list!/d" "$IDLIST" - info "设备 $list 已从控制列表移除" - fi - fi - done - info "时间控制启动完成,控制 $(wc -l < $IDLIST) 个设备" - show_status + info "启动时间控制" + stop_firewall + init_firewall + if [ $? -eq 0 ]; then + info "时间控制启动成功" + show_firewall_status + else + info "时间控制启动失败" + fi ;; + + "stop") + info "停止时间控制" + stop_firewall + info "时间控制已停止" + ;; + "add") - info "添加设备控制" - for list in $(echo "$crid" | sed -e 's/!//g' | sed 's/,/ /g'); do - if check_list "$list"; then - timeadd "$list" - if ! grep -q "!$list!" "$IDLIST"; then - echo "!$list!" >> "$IDLIST" - fi - info "设备 $list 已添加" + [ -z "$crid" ] && { + echo "错误: 需要指定设备ID" + exit 1 + } + info "添加设备控制: ID=$crid" + add_device "$crid" + show_firewall_status + ;; + + "del") + [ -z "$crid" ] && { + echo "错误: 需要指定设备ID" + exit 1 + } + info "移除设备控制: ID=$crid" + del_device "$crid" + show_firewall_status + ;; + + "status") + show_firewall_status + ;; + + "diagnose") + diagnose + ;; + + "test") + # 测试地址解析 + echo "测试地址解析:" + for test in "192.168.1.100" "192.168.1.0/24" "00:11:22:33:44:55" "invalid"; do + echo -n "$test: " + if parse_target "$test" >/dev/null; then + echo "✓ 有效" + parse_target "$test" else - if grep -q "!${list}!" "$IDLIST"; then - timedel "$list" - sed -i "/!$list!/d" "$IDLIST" - info "设备 $list 已移除" - fi + echo "✗ 无效" fi done ;; - "del") - info "移除设备控制" - for list in $(echo "$crid" | sed -e 's/!//g' | sed 's/,/ /g'); do - timedel "$list" - sed -i "/!$list!/d" "$IDLIST" - info "设备 $list 已删除" - done - ;; - "status") - show_status - ;; - "test") - test_parse + + "flush") + # 清理所有连接 + info "清理所有连接" + if [ -x "$bin_conntrack" ]; then + $bin_conntrack -F + info "连接已清理" + else + info "conntrack不可用" + fi ;; + "help"|"") - show_help + echo "时间控制系统命令工具" + echo "" + echo "用法: $0 {start|stop|add |del |status|diagnose|test|flush|help}" + echo "" + echo "命令说明:" + echo " start - 初始化防火墙规则" + echo " stop - 停止并清理所有防火墙规则" + echo " add - 添加设备到控制列表" + echo " del - 从控制列表移除设备" + echo " status - 显示防火墙状态" + echo " diagnose - 系统诊断" + echo " test - 测试地址解析" + echo " flush - 清理所有网络连接" + echo " help - 显示此帮助信息" ;; + *) - dbg "Invalid command: $crrun" - show_help + echo "错误: 未知命令 '$crrun'" + echo "使用: $0 help 查看帮助" exit 1 ;; -esac \ No newline at end of file +esac diff --git a/luci-app-timecontrol/root/usr/bin/timecontrol-log b/luci-app-timecontrol/root/usr/bin/timecontrol-log new file mode 100755 index 00000000..88c390e3 --- /dev/null +++ b/luci-app-timecontrol/root/usr/bin/timecontrol-log @@ -0,0 +1,117 @@ +#!/bin/bash + +# 时间控制日志查看工具 + +NAME=timecontrol +LOG_FILE="/var/log/$NAME.log" +STATUS_LOG="/var/lib/$NAME/status.log" +CONNECTION_LOG="/var/lib/$NAME/connections.log" + +show_realtime_log() { + echo "正在显示实时日志,按 Ctrl+C 退出..." + echo "" + tail -f "$LOG_FILE" | while read line; do + # 高亮显示重要信息 + if echo "$line" | grep -q "STATUS-CHANGE\|TIME_EXCEEDED\|RESET"; then + echo -e "\033[1;31m$line\033[0m" # 红色显示重要变更 + elif echo "$line" | grep -q "ALLOW_ACCESS\|解除限制"; then + echo -e "\033[1;32m$line\033[0m" # 绿色显示允许访问 + elif echo "$line" | grep -q "BLOCK_ACCESS\|添加限制"; then + echo -e "\033[1;33m$line\033[0m" # 黄色显示禁止访问 + else + echo "$line" + fi + done +} + +show_status_log() { + echo "最近状态变更记录:" + echo "────────────────────────────────────────────────────────────────────" + if [ -f "$STATUS_LOG" ]; then + tail -n 20 "$STATUS_LOG" | while read line; do + local time=$(echo "$line" | cut -d']' -f1 | sed 's/\[//') + local message=$(echo "$line" | cut -d']' -f2-) + printf "%-20s %s\n" "$time" "$message" + done + else + echo "暂无状态记录" + fi +} + +show_connection_log() { + echo "设备连接记录:" + echo "────────────────────────────────────────────────────────────────────" + echo "时间 设备 状态" + echo "────────────────────────────────────────────────────────────────────" + + if [ -f "$CONNECTION_LOG" ]; then + tail -n 20 "$CONNECTION_LOG" | while read line; do + local timestamp=$(echo "$line" | cut -d',' -f1) + local target=$(echo "$line" | cut -d',' -f2) + local action=$(echo "$line" | cut -d',' -f3) + + local time_str=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$timestamp") + local action_text="" + + case "$action" in + "connect") action_text="上线" ;; + "disconnect") action_text="下线" ;; + *) action_text="$action" ;; + esac + + printf "%-20s %-18s %s\n" "$time_str" "$target" "$action_text" + done + else + echo "暂无连接记录" + fi +} + +show_summary() { + local summary_file="/tmp/timecontrol_status.txt" + if [ -f "$summary_file" ]; then + cat "$summary_file" + else + echo "状态摘要文件不存在,正在生成..." + timecontrol status + fi +} + +show_help() { + echo "时间控制日志查看工具" + echo "" + echo "用法: timecontrol-log {realtime|status|connections|summary|help}" + echo "" + echo "命令:" + echo " realtime - 实时显示日志(彩色高亮)" + echo " status - 显示状态变更记录" + echo " connections - 显示设备连接记录" + echo " summary - 显示状态摘要" + echo " help - 显示此帮助" + echo "" + echo "示例:" + echo " timecontrol-log realtime # 实时监控" + echo " timecontrol-log status # 查看状态变更" +} + +case "$1" in + "realtime") + show_realtime_log + ;; + "status") + show_status_log + ;; + "connections") + show_connection_log + ;; + "summary") + show_summary + ;; + "help"|"") + show_help + ;; + *) + echo "未知命令: $1" + show_help + exit 1 + ;; +esac diff --git a/luci-app-timecontrol/root/usr/bin/timecontrolctrl b/luci-app-timecontrol/root/usr/bin/timecontrolctrl index 17f42f24..b4f48868 100755 --- a/luci-app-timecontrol/root/usr/bin/timecontrolctrl +++ b/luci-app-timecontrol/root/usr/bin/timecontrolctrl @@ -1,41 +1,60 @@ #!/bin/sh # Copyright (C) 2006 OpenWrt.org -# Copyright 2022-2025 sirpdboy +# Copyright 2022-2026 sirpdboy + NAME=timecontrol +LOG_FILE="/var/log/timecontrol.log" +DEBUG=1 + +# 时长数据库目录 +DURATION_DIR="/var/lib/timecontrol" +DURATION_DB="$DURATION_DIR/duration.db" +CONNECTION_LOG="$DURATION_DIR/connections.log" + +# 状态文件 IDLIST="/var/$NAME.idlist" -TMPID="/var/$NAME.tmpid" +STATUS_DB="$DURATION_DIR/status.db" -idlist=$(uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]') - -config_get_type() { - local ret=$(uci -q get "${NAME}.${1}") - echo "${ret:=$2}" +# 初始化目录 +init_dirs() { + mkdir -p "$DURATION_DIR" + touch "$DURATION_DB" "$CONNECTION_LOG" "$STATUS_DB" 2>/dev/null } -config_n_get() { - local ret=$(uci -q get "${NAME}.${1}.${2}") - echo "${ret:=$3}" +# 日志函数 +dbg() { + [ "$DEBUG" -eq 1 ] && { + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] CTRL-DEBUG: $@" >> "$LOG_FILE" + } } +info() { + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] CTRL-INFO: $@" >> "$LOG_FILE" +} + +# 配置文件读取 config_t_get() { - local index=${3:-0} - local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}") - echo "${ret:=${4}}" + local index=${3:-0} + local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}") + echo "${ret:=${4}}" } -config_t_set() { - local index=${4:-0} - local ret=$(uci -q set "${NAME}.@${1}[${index}].${2}=${3}") +# 获取启用设备 +get_enabled_devices() { + uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -n } +# 时间检查函数 is_time_in_range() { local start_time=$1 local end_time=$2 local current_time=$(date +%H:%M) if [ "$start_time" = "$end_time" ]; then - return 0 + return 0 elif [ "$start_time" \< "$end_time" ]; then [ "$current_time" \> "$start_time" ] && [ "$current_time" \< "$end_time" ] && return 0 else @@ -48,60 +67,269 @@ is_weekday_in_range() { local configured_weekdays=$1 local current_weekday=$(date +%u) + [ "$configured_weekdays" = "0" ] && return 0 + for ww in $(echo $configured_weekdays | sed 's/,/ /g'); do - if [ "$current_weekday" = "$ww" ] || [ "$ww" = "0" ]; then - return 0 - fi + [ "$current_weekday" = "$ww" ] && return 0 done return 1 } -check_device() { - local id=$1 - local start_time=$(config_t_get device timestart $id ) - local end_time=$(config_t_get device timeend $id ) - local weekdays=$(config_t_get device week $id ) - - if is_weekday_in_range "$weekdays" && is_time_in_range "$start_time" "$end_time"; then - return 0 - else - return 1 - fi -} - -update_device_status() { - local id=$1 - local action=$2 - - if [ "$action" = "add" ]; then - timecontrol add $id - echo "!${id}!" >> $IDLIST - elif [ "$action" = "del" ]; then - timecontrol del $id - sed -i "/!$id!/d" $IDLIST >/dev/null 2>&1 - fi -} - -process_devices() { - [ -s $IDLIST ] || touch $IDLIST - - for id in $idlist; do - if check_device $id; then - if ! grep -q "!${id}!" $IDLIST; then - update_device_status $id "add" - fi - else - if grep -q "!${id}!" $IDLIST; then - update_device_status $id "del" - fi +# 时长管理 +record_connection_time() { + local target="$1" + local action="$2" + local timestamp=$(date +%s) + + echo "$timestamp,$target,$action" >> "$CONNECTION_LOG" + + if [ "$action" = "connect" ]; then + if ! grep -q "^$target," "$DURATION_DB" 2>/dev/null; then + echo "$target,$timestamp,0,0" >> "$DURATION_DB" + dbg "初始化时长: $target" fi - done - - cat $IDLIST | sort | uniq > $TMPID - mv $TMPID $IDLIST + elif [ "$action" = "disconnect" ]; then + if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then + local last_connect=$(grep "^$target," "$DURATION_DB" | cut -d',' -f2) + local total_used=$(grep "^$target," "$DURATION_DB" | cut -d',' -f3) + local last_reset=$(grep "^$target," "$DURATION_DB" | cut -d',' -f4) + + local session_duration=$((timestamp - last_connect)) + local new_total=$((total_used + session_duration)) + + sed -i "/^$target,/d" "$DURATION_DB" + echo "$target,$timestamp,$new_total,$last_reset" >> "$DURATION_DB" + fi + fi } -while :; do - process_devices - sleep 60 -done +get_connection_time() { + local target="$1" + local current_time=$(date +%s) + + if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then + local last_connect=$(grep "^$target," "$DURATION_DB" | cut -d',' -f2) + local total_used=$(grep "^$target," "$DURATION_DB" | cut -d',' -f3) + + if tail -n 5 "$CONNECTION_LOG" 2>/dev/null | grep -q "^[0-9]*,$target,connect$"; then + local session_duration=$((current_time - last_connect)) + total_used=$((total_used + session_duration)) + fi + + echo "$total_used" + else + echo "0" + fi +} + +# 重置检查 +should_reset_duration() { + local target="$1" + local reset_cycle="$2" + local last_reset_file="$DURATION_DIR/last_reset_$target" + + [ ! -f "$last_reset_file" ] && return 0 + + local last_reset=$(cat "$last_reset_file" 2>/dev/null) + local current_time=$(date +%s) + + case "$reset_cycle" in + "daily") + local last_date=$(date -d "@$last_reset" +%Y%m%d 2>/dev/null || echo "0") + local current_date=$(date +%Y%m%d) + [ "$last_date" != "$current_date" ] + ;; + "weekly") + local last_week=$(date -d "@$last_reset" +%Y%W 2>/dev/null || echo "0") + local current_week=$(date +%Y%W) + [ "$last_week" != "$current_week" ] + ;; + "monthly") + local last_month=$(date -d "@$last_reset" +%Y%m 2>/dev/null || echo "0") + local current_month=$(date +%Y%m) + [ "$last_month" != "$current_month" ] + ;; + *) + false + ;; + esac +} + +reset_duration_counter() { + local target="$1" + local reset_cycle="$2" + local current_time=$(date +%s) + + if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then + sed -i "/^$target,/d" "$DURATION_DB" + fi + + echo "$target,$current_time,0,$current_time" >> "$DURATION_DB" + echo "$current_time" > "$DURATION_DIR/last_reset_$target" + + info "重置时长: $target (周期: $reset_cycle)" +} + +# 主检查函数 +check_device_control() { + local id="$1" + local target=$(config_t_get device mac "$id") + local time_mode=$(config_t_get device time_mode "$id" "period") + local weekdays=$(config_t_get device week "$id" "0") + + # 检查星期 + is_weekday_in_range "$weekdays" || { + dbg "星期不允许: $target" + return 1 + } + + case "$time_mode" in + "period") + local start_time=$(config_t_get device timestart "$id" "00:00") + local end_time=$(config_t_get device timeend "$id" "00:00") + is_time_in_range "$start_time" "$end_time" || { + dbg "时间段外: $target ($start_time-$end_time)" + return 1 + } + dbg "时间段内: $target" + return 0 + ;; + + "duration") + local duration=$(config_t_get device duration "$id" "60") + local reset_cycle=$(config_t_get device reset_cycle "$id" "daily") + + # 重置检查 + if should_reset_duration "$target" "$reset_cycle"; then + reset_duration_counter "$target" "$reset_cycle" + fi + + # 记录连接 + local last_action=$(grep ",$target," "$CONNECTION_LOG" 2>/dev/null | tail -1 | cut -d',' -f3) + if [ "$last_action" != "connect" ]; then + record_connection_time "$target" "connect" + fi + + # 检查时长 + local used_seconds=$(get_connection_time "$target") + local used_minutes=$((used_seconds / 60)) + local max_minutes=$duration + + dbg "时长检查: $target 已用=${used_minutes}分钟, 限制=${max_minutes}分钟" + + if [ "$used_minutes" -ge "$max_minutes" ]; then + dbg "已超时: $target" + return 1 + fi + dbg "未超时: $target" + return 0 + ;; + + "combined") + local start_time=$(config_t_get device timestart "$id" "00:00") + local end_time=$(config_t_get device timeend "$id" "00:00") + local use_duration=$(config_t_get device use_duration "$id" "0") + + # 时间段检查 + is_time_in_range "$start_time" "$end_time" || { + dbg "时间段外: $target" + return 1 + } + + # 时长检查 + if [ "$use_duration" = "1" ]; then + local duration=$(config_t_get device duration "$id" "60") + local reset_cycle=$(config_t_get device reset_cycle "$id" "daily") + + if should_reset_duration "$target" "$reset_cycle"; then + reset_duration_counter "$target" "$reset_cycle" + fi + + local last_action=$(grep ",$target," "$CONNECTION_LOG" 2>/dev/null | tail -1 | cut -d',' -f3) + if [ "$last_action" != "connect" ]; then + record_connection_time "$target" "connect" + fi + + local used_seconds=$(get_connection_time "$target") + local used_minutes=$((used_seconds / 60)) + local max_minutes=$duration + + if [ "$used_minutes" -ge "$max_minutes" ]; then + dbg "时间段内但已超时: $target" + return 1 + fi + fi + + dbg "时间段内允许: $target" + return 0 + ;; + + *) + # 默认时间段控制 + local start_time=$(config_t_get device timestart "$id" "00:00") + local end_time=$(config_t_get device timeend "$id" "00:00") + is_time_in_range "$start_time" "$end_time" + return $? + ;; + esac +} + +# 更新设备状态 +update_device_status() { + local id="$1" + local should_allow="$2" # 0=允许, 1=禁止 + local target=$(config_t_get device mac "$id") + local comment=$(config_t_get device comment "$id" "设备$id") + + # 检查当前状态 + local current_blocked=0 + if [ -f "$IDLIST" ] && grep -q "!${id}!" "$IDLIST" 2>/dev/null; then + current_blocked=1 + fi + + dbg "设备状态: $target, 应该允许=$should_allow, 当前阻止=$current_blocked" + + if [ "$should_allow" -eq 0 ]; then + # 应该允许 + if [ "$current_blocked" -eq 1 ]; then + dbg "解除阻止: $target" + timecontrol del "$id" + sed -i "/!$id!/d" "$IDLIST" 2>/dev/null + info "允许上网: $comment ($target)" + record_connection_time "$target" "disconnect" + fi + else + # 应该阻止 + if [ "$current_blocked" -eq 0 ]; then + dbg "添加阻止: $target" + timecontrol add "$id" + if ! grep -q "!$id!" "$IDLIST" 2>/dev/null; then + echo "!$id!" >> "$IDLIST" + fi + info "阻止上网: $comment ($target)" + record_connection_time "$target" "disconnect" + fi + fi +} + +# 主处理循环 +main_loop() { + info "时间控制守护进程启动" + init_dirs + while :; do + dbg "开始检查设备" + [ `uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -n | wc -l` eq 0 ] && timecontrol stop && break + for id in $(get_enabled_devices); do + if check_device_control "$id"; then + update_device_status "$id" 0 # 允许 + else + update_device_status "$id" 1 # 阻止 + fi + done + + sleep 60 + done +} + +# 启动 +main_loop diff --git a/luci-app-timecontrol/root/usr/share/luci/menu.d/luci-app-timecontrol.json b/luci-app-timecontrol/root/usr/share/luci/menu.d/luci-app-timecontrol.json index 4a17c832..dba688ae 100644 --- a/luci-app-timecontrol/root/usr/share/luci/menu.d/luci-app-timecontrol.json +++ b/luci-app-timecontrol/root/usr/share/luci/menu.d/luci-app-timecontrol.json @@ -1,11 +1,4 @@ -{ "admin/control": { - "title": "Control", - "order": 44, - "action": { - "type": "firstchild", - "recurse": true - } - }, +{ "admin/control/timecontrol": { "title": "Time Control", "order": 10, diff --git a/luci-app-vnt/Makefile b/luci-app-vnt/Makefile index 88fe796f..26cfb5a9 100644 --- a/luci-app-vnt/Makefile +++ b/luci-app-vnt/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_VERSION:=1.2.16 -PKG_RELEASE:=1 +PKG_RELEASE:=2 LUCI_TITLE:=LuCI support for vnt LUCI_DEPENDS:=+kmod-tun +luci-compat diff --git a/luci-app-vnt/luasrc/model/cbi/vnt.lua b/luci-app-vnt/luasrc/model/cbi/vnt.lua index f6b3f2d0..91feb236 100644 --- a/luci-app-vnt/luasrc/model/cbi/vnt.lua +++ b/luci-app-vnt/luasrc/model/cbi/vnt.lua @@ -2,7 +2,7 @@ local http = luci.http local nixio = require "nixio" m = Map("vnt") -m.description = translate('vnt是一个简便高效的异地组网、内网穿透工具。
      官网:rustvnt.com  项目地址:github.com/vnt-dev/vnt  安卓端、GUI:VntApp  QQ群') +m.description = translate('vnt是一个简便高效的异地组网、内网穿透工具。
      官网:rustvnt.com  项目地址:github.com/vnt-dev/vnt  安卓端、GUI:VntApp  QQ群') -- vnt-cli m:section(SimpleSection).template = "vnt/vnt_status" @@ -129,8 +129,8 @@ client_port = s:taboption("privacy", Value, "client_port", translate("本地监 client_port.placeholder = "0,0" mapping = s:taboption("privacy",DynamicList, "mapping", translate("端口映射"), - translate("端口映射,可以设置多个映射地址,例如 udp:0.0.0.0:80->10.26.0.10:80 和 tcp:0.0.0.0:80->10.26.0.11:81
      表示将本地udp 80端口的数据转发到10.26.0.10:80,将本地tcp 80端口的数据转发到10.26.0.11:81,转发的目的地址可以使用域名+端口")) -mapping.placeholder = "tcp:0.0.0.0:80->10.26.0.10:80" + translate("端口映射,可以设置多个映射地址,例如 udp:0.0.0.0:80-10.26.0.10:80 和 tcp:0.0.0.0:80-10.26.0.11:81
      表示将本地udp 80端口的数据转发到10.26.0.10:80,将本地tcp 80端口的数据转发到10.26.0.11:81,转发的目的地址可以使用域名+端口")) +mapping.placeholder = "tcp:0.0.0.0:80-10.26.0.10:80" mtu = s:taboption("privacy",Value, "mtu", translate("MTU"), translate("设置虚拟网卡的mtu值,大多数情况下(留空)使用默认值效率会更高,也可根据实际情况进行微调,默认值:不加密1450,加密1410")) diff --git a/luci-theme-kucat/Makefile b/luci-theme-kucat/Makefile index 7b4f35b9..16fb7ee4 100644 --- a/luci-theme-kucat/Makefile +++ b/luci-theme-kucat/Makefile @@ -10,8 +10,8 @@ THEME_TITLE:=Kucat Theme PKG_NAME:=luci-theme-$(THEME_NAME) LUCI_TITLE:=Kucat Theme by sirpdboy LUCI_DEPENDS:=+wget +curl +jsonfilter -PKG_VERSION:=3.3.1 -PKG_RELEASE:=1 +PKG_VERSION:=3.3.2 +PKG_RELEASE:=2 define Package/luci-theme-$(THEME_NAME)/conffiles /www/luci-static/resources/background/ diff --git a/luci-theme-kucat/README.md b/luci-theme-kucat/README.md index 7cc1a20c..bef0f61f 100644 --- a/luci-theme-kucat/README.md +++ b/luci-theme-kucat/README.md @@ -120,6 +120,13 @@ wget --no-check-certificate -O luci-i18n-kucat-config-zh-cn_0_all.ipk https://g opkg install luci-app-kucat-config_2.2.0-r20260227_all.ipk luci-i18n-kucat-config-zh-cn_0_all.ipk ``` +### 1、 Switch to display all menus +image + +### 2、 Manually customize menu items + Location: [System] ->[KUCAT Settings] ->[Menu Configuration] +image + ## interface diff --git a/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css b/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css index c693402b..e0fdfd81 100644 --- a/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css +++ b/luci-theme-kucat/htdocs/luci-static/kucat/css/style.css @@ -1302,8 +1302,6 @@ input:not(.cbi-button):hover, select:not([multiple="multiple"]):focus, input:not(.cbi-button):focus, .cbi-dropdown:focus { - - color: var(--inputtext-color); border-bottom: 1px solid rgba(var(--primary-rgbm), 0.9); } @@ -2146,6 +2144,11 @@ body.modal-overlay-active #modal_overlay { margin-bottom: .5em; max-width: 100% } + +.modal>ul { + margin: .5em; +} + .modal>.cbi-map { overflow-x: unset; } @@ -2985,6 +2988,7 @@ body.lang_pl.node-main-login .cbi-value-title { -webkit-backdrop-filter: blur(6px) !important; backdrop-filter: blur(6px) !important; box-shadow: var(--boxshadow); + box-shadow: 0 0.3rem 0.5rem var(--input-boxcolor); background-color: rgba(var(--primary-rgbbody), 0.5) !important; border-radius: var(--radius1); border: none; @@ -3426,10 +3430,6 @@ body.lang_pl.node-main-login .cbi-value-title { [data-page^="admin-services-openclash"] .CodeMirror { font-size: inherit; - background-color: rgba(0, 0, 0, 0.8) !important; - color: var(--body-color) !important; - color: #bbb !important; - font-family: 'Google Sans' !important } [data-page^="admin-services-openclash"] select#CORE_VERSION, diff --git a/mtproxy/Makefile b/mtproxy/Makefile index 28124aa3..4e27ba8d 100644 --- a/mtproxy/Makefile +++ b/mtproxy/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mtproxy -PKG_VERSION:=3.5.4 -PKG_RELEASE:=13 +PKG_VERSION:=3.5.5 +PKG_RELEASE:=14 PKG_MAINTAINER:=Kosntantine Shevlakov PKG_LICENSE:=GPLv2 @@ -10,7 +10,7 @@ PKG_LICENSE_FILES:=LICENSE PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/GetPageSpeed/MTProxy.git -PKG_SOURCE_VERSION:=61c43d02a92f2f7b439cc61f9072640b7efa0486 +PKG_SOURCE_VERSION:=55e5f0f575c2dd5decbf5282f4c1ea1f816a7eb0 PKG_SOURCE_SUBDIR:=$(PKG_NAME) PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz