mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 16:00:43 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e769928f9 | ||
|
|
b3630a0107 | ||
|
|
e89ff18e4c | ||
|
|
1ada27ef11 | ||
|
|
7f267dd900 | ||
|
|
5b424c39c0 | ||
|
|
b705216e4a |
-20
@@ -59,23 +59,3 @@ desktop.ini
|
|||||||
|
|
||||||
# ---- 测试收发的临时数据 ----
|
# ---- 测试收发的临时数据 ----
|
||||||
/_tests/received/
|
/_tests/received/
|
||||||
|
|
||||||
# 令牌(绝对不要进仓库)
|
|
||||||
/_build/.gitee_token
|
|
||||||
*.token
|
|
||||||
.env
|
|
||||||
# ---- 测试用的临时数据目录(会被测试脚本移来移去)----
|
|
||||||
/windows/data/
|
|
||||||
/windows/data_bk_*/
|
|
||||||
/_build/*_data/
|
|
||||||
/_build/_pairtest/
|
|
||||||
/_build/_whitetest/
|
|
||||||
/_tests/_stubdata/
|
|
||||||
/_tests/_*_data/
|
|
||||||
# ---- 密码 / 令牌(绝对不能进仓库)----
|
|
||||||
# admin_password.txt 是运行时生成的,只有本机能看。
|
|
||||||
# 一旦提交,git 历史删不掉,等于把管理台钥匙公开。
|
|
||||||
admin_password.txt
|
|
||||||
*admin_password*.txt
|
|
||||||
/_build/_hdr_data/
|
|
||||||
/_build/relay_info.json
|
|
||||||
@@ -1,681 +0,0 @@
|
|||||||
ykxmz —— 教室多媒体辅助工具
|
|
||||||
Copyright (C) 2026 小麦子
|
|
||||||
|
|
||||||
本程序是自由软件:你可以再发布和/或修改它,依据自由软件基金会发布的
|
|
||||||
GNU Affero 通用公共许可证(第 3 版或你选择的任何更新版本)的条款。
|
|
||||||
|
|
||||||
本程序的发布是希望它有用,但**没有任何保证**;甚至没有适销性或
|
|
||||||
特定用途适用性的默示保证。详见 GNU Affero 通用公共许可证。
|
|
||||||
|
|
||||||
你应该已经随本程序收到了一份 GNU Affero 通用公共许可证的副本
|
|
||||||
(见本文件下半部分)。如果没有,见 <https://www.gnu.org/licenses/>。
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------
|
|
||||||
⚠ 使用限制请看 docs/LICENSE.txt(《使用条款与免责声明》):
|
|
||||||
本软件是一套远程控制工具,禁止用于未经同意的他人设备、
|
|
||||||
禁止用于偷窥监视、禁止伪装隐藏。那份文件约束的是**使用行为**,
|
|
||||||
和本文件(约束**代码**的授权)互不冲突。
|
|
||||||
--------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are 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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
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 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 work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero 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 Affero 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 Affero 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 Affero 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.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
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 AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# ykxmz · 教室电脑手表控制
|
# ykxmz · 教室电脑手表遥控
|
||||||
|
|
||||||
老师在讲台上抬一下手腕,就能把教室里那台电脑管了。
|
老师在讲台上抬一下手腕,就能把教室里那台电脑管了。
|
||||||
|
|
||||||
放课件、放音频、投个图、看看有没有在跑无关程序、下课关机 —— 不用走回讲台去点鼠标。
|
放课件、放音频、投个图、看看谁在打游戏、下课关机 —— 不用走回讲台去点鼠标。
|
||||||
|
|
||||||
> 名字来自"控制手表"的拼音首字母,读作"控制拇指"也行。
|
> 名字来自"遥控手表"的拼音首字母,读作"遥控拇指"也行。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
## 这东西是干嘛的
|
## 这东西是干嘛的
|
||||||
|
|
||||||
一句话:**用手表控制电脑。**
|
一句话:**用手表遥控电脑。**
|
||||||
|
|
||||||
为什么用手表而不是手机?因为老师上课时手上是空的,手机在兜里,掏手机的动作比抬手大得多。手表抬一下就行,还不打断讲课。
|
为什么用手表而不是手机?因为老师上课时手上是空的,手机在兜里,掏手机的动作比抬手大得多。手表抬一下就行,还不打断讲课。
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
- 课件全屏投到教室大屏
|
- 课件全屏投到教室大屏
|
||||||
- 放听力音频(电脑音箱比手表响多了)
|
- 放听力音频(电脑音箱比手表响多了)
|
||||||
- 看看学生机是不是在打游戏,直接把游戏进程结了
|
- 看看学生机是不是在打游戏,直接把游戏进程结了
|
||||||
- 课堂专注模式,下课一键恢复
|
- 屏蔽游戏网站,下课一键恢复
|
||||||
- 下课关机、锁屏
|
- 下课关机、锁屏
|
||||||
- 喊一声"上课了"让电脑播音(TTS)
|
- 喊一声"上课了"让电脑播音(TTS)
|
||||||
|
|
||||||
@@ -46,35 +46,13 @@
|
|||||||
|
|
||||||
| 装在哪 | 是什么 | 文件 |
|
| 装在哪 | 是什么 | 文件 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 教室电脑(Windows) | 被控端 | `ykxmz-setup-1.8.2.exe` |
|
| 教室电脑(Windows) | 被控端 | `ykxmz-agent-1.5.0.zip` |
|
||||||
| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-1.8.2.zip` |
|
| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-1.5.0.zip` |
|
||||||
| 手表(安卓) | 控制端 | `ykxmz-watch-1.8.2.apk` |
|
| 手表(安卓) | 控制端 | `ykxmz-watch-1.5.0.apk` |
|
||||||
|
|
||||||
**只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。
|
**只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。
|
||||||
|
|
||||||
**要在办公室管教室?** 那必须**自己部署一台中转站**(见 [跨网络](#跨网络在办公室管教室))。
|
**要在办公室管教室?** 那必须装中转站(见 [跨网络](#跨网络在办公室管教室))。
|
||||||
|
|
||||||
> ### ⚠️ 软件里不带任何中转地址
|
|
||||||
>
|
|
||||||
> 这一点先讲清楚,免得白折腾:
|
|
||||||
>
|
|
||||||
> **这个软件默认是"空的" —— 没有任何内置的中转服务器地址,
|
|
||||||
> 也没有官方服务器可以用,作者不对外提供中转服务。**
|
|
||||||
>
|
|
||||||
> - **同一个 WiFi 下用(教室里)**:不需要中转,装完就能连,什么地址都不用填。
|
|
||||||
> - **要跨网络用(办公室管教室)**:**你得自己部署一台中转站**。
|
|
||||||
> 服务端压缩包里有一键脚本,一条命令装好:
|
|
||||||
>
|
|
||||||
> ```bash
|
|
||||||
> bash deploy.sh
|
|
||||||
> ```
|
|
||||||
>
|
|
||||||
> 装完它会打印一个属于你自己的公网地址,把它填到电脑和手表上。
|
|
||||||
> 详见 [装中转站](#装中转站)。
|
|
||||||
>
|
|
||||||
> 为什么不做一个"填个公共地址就能用"?因为中转站的带宽是实打实的成本,
|
|
||||||
> 谁用谁出;而且**中转站能看到经过它的字节流**,
|
|
||||||
> 自己搭一台,数据才真正只在你自己手里。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,9 +60,9 @@
|
|||||||
|
|
||||||
### 第一步:电脑上装被控端
|
### 第一步:电脑上装被控端
|
||||||
|
|
||||||
双击 `ykxmz-setup-1.8.2.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。
|
解压 `ykxmz-agent-1.5.0.zip`,双击里面的 `ykxmz.exe`。
|
||||||
|
|
||||||
第一次运行会弹出窗口,填两样:
|
弹出的窗口里填两样:
|
||||||
|
|
||||||
- **6 位数字** —— 自己定,好记就行,比如 `789113`
|
- **6 位数字** —— 自己定,好记就行,比如 `789113`
|
||||||
- 两个勾:`设置开机自启`、`放行防火墙`(放行那个要过一次管理员确认)
|
- 两个勾:`设置开机自启`、`放行防火墙`(放行那个要过一次管理员确认)
|
||||||
@@ -92,21 +70,18 @@
|
|||||||
点确定,屏幕会黑闪一下(在加防火墙规则),然后窗口消失,程序转到后台跑。
|
点确定,屏幕会黑闪一下(在加防火墙规则),然后窗口消失,程序转到后台跑。
|
||||||
|
|
||||||
> **窗口关了程序还在跑**,这是故意的 —— 上课时讲台上不该有个窗口杵着。
|
> **窗口关了程序还在跑**,这是故意的 —— 上课时讲台上不该有个窗口杵着。
|
||||||
> 想再打开窗口,**再双击一次程序图标**就行,不会起第二个进程。
|
> 想再打开窗口,**再双击一次 EXE** 就行,不会起第二个进程。
|
||||||
> 想彻底退出,窗口上点「退出」。
|
> 想彻底退出,窗口上点「退出」。
|
||||||
|
|
||||||
> 不想用安装包的话,`ykxmz-agent-1.8.2.zip` 是绿色版:
|
|
||||||
> 解压出来直接跑 `ykxmz.exe`,整个文件夹拷走就能换台机器用,不写注册表。
|
|
||||||
|
|
||||||
### 第二步:手表上装 App
|
### 第二步:手表上装 App
|
||||||
|
|
||||||
把 `ykxmz-watch-1.8.2.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。
|
把 `ykxmz-watch-1.5.0.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。
|
||||||
|
|
||||||
打开 App,首页有四个入口:
|
打开 App,首页有四个入口:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────┐
|
┌──────────────────────────┐
|
||||||
│ 控制手表 │
|
│ 遥控手表 │
|
||||||
│ 和电脑连同一个 WiFi,不用输 IP │
|
│ 和电脑连同一个 WiFi,不用输 IP │
|
||||||
├──────────────────────────┤
|
├──────────────────────────┤
|
||||||
│ 中转设置(跨网络用) │ ← 办公室管教室才需要
|
│ 中转设置(跨网络用) │ ← 办公室管教室才需要
|
||||||
@@ -142,8 +117,8 @@
|
|||||||
|
|
||||||
| 按钮 | 干什么 |
|
| 按钮 | 干什么 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **课堂专注模式** | 改 hosts,把常见游戏站点指到本地。学生机打开就是空白页 |
|
| **屏蔽游戏网站** | 改 hosts,把常见游戏站点指到本地。学生机打开就是空白页 |
|
||||||
| **关闭游戏窗口** | 扫一遍常见游戏进程名,找到就结束掉 |
|
| **结束游戏进程** | 扫一遍常见游戏进程名,找到就结束掉 |
|
||||||
| **看有哪些限制** | 列出当前开了哪些限制,别自己忘了 |
|
| **看有哪些限制** | 列出当前开了哪些限制,别自己忘了 |
|
||||||
| **一键恢复全部** | 把这节课开的所有限制撤掉,**下课必点** |
|
| **一键恢复全部** | 把这节课开的所有限制撤掉,**下课必点** |
|
||||||
|
|
||||||
@@ -213,61 +188,19 @@
|
|||||||
|
|
||||||
### 装中转站
|
### 装中转站
|
||||||
|
|
||||||
中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行,
|
把 `ykxmz-server-1.5.0.zip` 解压到那台小机器上(随身 WiFi、树莓派、云主机都行,Debian/Ubuntu 系统),然后:
|
||||||
Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。
|
|
||||||
|
|
||||||
把 `ykxmz-server-1.8.2.zip` 解压到那台机器上,然后一条命令:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy.sh
|
sudo bash install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
它会自己做完这些,不用你一步步敲:
|
装完会打印一个公网地址,长这样:
|
||||||
|
|
||||||
1. 装 cloudflared(已经装了就用现成的)
|
|
||||||
2. 把中转程序装到 `/root/lanremote-relay/`
|
|
||||||
3. 网络调优(keepalive、MSS、MTU、开机加速)
|
|
||||||
4. 注册 systemd 服务,设成开机自启
|
|
||||||
5. 配好 Cloudflare 隧道
|
|
||||||
6. 启动、自检,然后把**公网地址**打印出来
|
|
||||||
|
|
||||||
装完会打印一个属于你自己的公网地址,长这样:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
https://relay.你的域名
|
https://relay.你的域名
|
||||||
```
|
```
|
||||||
|
|
||||||
**记住这个地址**,电脑和手表都要填它。忘了就在那台机器上跑
|
**记住这个地址**,电脑和手表都要填它。忘了就打开 `http://<小机器的IP>:8080/url` 查。
|
||||||
`lanremote-url`,或者浏览器打开 `http://<小机器的IP>:8080/url` 查。
|
|
||||||
|
|
||||||
#### 三种常见情形
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ① 机器上已经有隧道凭据(/root/.cloudflared/*.json)
|
|
||||||
bash deploy.sh
|
|
||||||
|
|
||||||
# ② Cloudflare 给了一个隧道 Token(Zero Trust 后台复制的那串)
|
|
||||||
TUNNEL_TOKEN=eyJhIjoi... bash deploy.sh
|
|
||||||
|
|
||||||
# ③ 换域名 / 只装中转、不要隧道
|
|
||||||
bash deploy.sh --hostname relay.你的域名
|
|
||||||
bash deploy.sh --skip-tunnel
|
|
||||||
```
|
|
||||||
|
|
||||||
看全部参数:`bash deploy.sh --help`
|
|
||||||
|
|
||||||
> **用环境变量而不是 `--token`**:写进命令行的 Token 会留在 shell 历史里。
|
|
||||||
|
|
||||||
#### 重跑一遍 = 升级
|
|
||||||
|
|
||||||
`deploy.sh` 是**幂等**的:已经装过的机器再跑一次就是升级。
|
|
||||||
程序文件会被覆盖成新的,而**设备数据、配对码、隧道凭据都不会被冲掉**。
|
|
||||||
|
|
||||||
#### 需要准备什么
|
|
||||||
|
|
||||||
- 一台能上外网的 Linux 小机器(Debian 12 / Ubuntu,512 MB 内存够)
|
|
||||||
- 一个 Cloudflare 账号 + 一个域名(隧道要用)
|
|
||||||
- **不需要**装 Python 第三方包 —— 服务端是纯标准库,不装任何依赖
|
|
||||||
|
|
||||||
### 告诉电脑中转地址
|
### 告诉电脑中转地址
|
||||||
|
|
||||||
@@ -279,34 +212,19 @@ ykxmz.exe --relay "192.168.1.50:8080;https://relay.你的域名"
|
|||||||
|
|
||||||
两个地址用分号隔开,**按顺序试,第一个通的就用**。这样在教室里走局域网(快、稳),出了学校自动切隧道。
|
两个地址用分号隔开,**按顺序试,第一个通的就用**。这样在教室里走局域网(快、稳),出了学校自动切隧道。
|
||||||
|
|
||||||
如果只在跨网络的场景用,也可以只填一个域名:
|
|
||||||
|
|
||||||
```
|
|
||||||
ykxmz.exe --relay https://relay.你的域名
|
|
||||||
```
|
|
||||||
|
|
||||||
看看当前用的是哪个地址:
|
|
||||||
|
|
||||||
```
|
|
||||||
ykxmz.exe --relay-status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 告诉手表中转地址
|
### 告诉手表中转地址
|
||||||
|
|
||||||
手表首页点「中转设置」,填上面同一个地址。
|
手表首页点「中转设置」,填上面同一个地址。
|
||||||
|
|
||||||
然后回家首页点「输入配对码连接」,填电脑上那 6 位数字。
|
然后回家首页点「输入配对码连接」,填电脑上那 6 位数字。
|
||||||
|
|
||||||
### 中转站的公网地址打开是什么
|
### 中转站怎么管
|
||||||
|
|
||||||
打开就是**一个普通的官网页面** —— 介绍、截图、三个文件的下载按钮,
|
浏览器打开中转站的公网地址,用管理密码登录(默认 `120830`,装完后强烈建议改)。
|
||||||
最新版的安装包挂在上面,点一下就能下。
|
|
||||||
|
|
||||||
安装包和截图这类静态文件**由 Cloudflare 的边缘节点直接发送**,
|
能看:哪些电脑在线、配对码是什么、有多少会话、内存用了多少。
|
||||||
所以下载速度和那台小机器的带宽无关(实测比走隧道回源快几十倍)。
|
能做:踢掉某个会话、删掉某个配对码绑定、跑个网络体检。
|
||||||
|
下载区:最新版的三个文件都挂在上面,点一下就能下。
|
||||||
想看运行状态、排查问题、重启服务,**看服务端压缩包里的
|
|
||||||
`README-server.txt`** —— 那份文档是给运维的人看的,里面写了日常命令。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -446,7 +364,7 @@ windows\build_exe.ps1
|
|||||||
```powershell
|
```powershell
|
||||||
android\build_apk.ps1
|
android\build_apk.ps1
|
||||||
# 流程:aapt2 → javac → d8 → zipalign → apksigner
|
# 流程:aapt2 → javac → d8 → zipalign → apksigner
|
||||||
# 产物:dist\ykxmz-watch-1.8.2.apk
|
# 产物:dist\ykxmz-watch-1.5.0.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
最低支持 Android 7.0(API 24),targetSdk 34。
|
最低支持 Android 7.0(API 24),targetSdk 34。
|
||||||
@@ -456,13 +374,9 @@ android\build_apk.ps1
|
|||||||
Python 3 标准库,没有第三方包。
|
Python 3 标准库,没有第三方包。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash relay/deploy.sh
|
bash relay/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
`deploy.sh` 是一键部署(装服务 + 调优 + 配隧道 + 自检);
|
|
||||||
只想调网络参数的话是 `relay/deploy-tune.sh`。
|
|
||||||
老的 `relay/install.sh`(匿名快速隧道)仍在包里,只为兼容以前装过的机器。
|
|
||||||
|
|
||||||
### 跑测试
|
### 跑测试
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -494,22 +408,7 @@ python _build\package_remote.py
|
|||||||
|
|
||||||
二进制数据(图片、文件、音频)走 **Base64** 分块传输,单次会话上限 256 MB。
|
二进制数据(图片、文件、音频)走 **Base64** 分块传输,单次会话上限 256 MB。
|
||||||
|
|
||||||
跨网络时同样的消息套在 **HTTP 长轮询**里(单次挂 **60 秒**,
|
跨网络时同样的消息套在 HTTP 长轮询里,中转站按配对码把两端字节对接,**它不解析内容**。
|
||||||
有数据立刻返回、不空等),中转站按配对码把两端字节对接,**它不解析内容**。
|
|
||||||
|
|
||||||
跨网络这条链路上还有几个实测调出来的设计,改代码前值得知道:
|
|
||||||
|
|
||||||
- **隧道两种协议同时跑**(QUIC + HTTP/2,合计 10 条连接)。
|
|
||||||
两种协议各有各的毛病 —— QUIC 尾部延迟好但建连容易超时,
|
|
||||||
HTTP/2 建连可靠但丢包时容易被队头阻塞卡住。
|
|
||||||
同时跑就是互相兜底:一种犯病,另一种还在服务。
|
|
||||||
- **长轮询 60 秒**。隧道每次请求要多花 1~2 秒,这个开销是按次数收的;
|
|
||||||
拉到 60 秒把请求数降到 1/4,撞上丢包窗口的概率也降到 1/4。
|
|
||||||
**命令响应速度不变**(有数据立刻返回,不是等满 60 秒)。
|
|
||||||
- **偶发失败不当场判离线**。4G 链路上本来就有百分之几的偶发抖动、
|
|
||||||
一两秒自己恢复;连续失败 3 次才算真的离线。
|
|
||||||
- **静态资源交给 Cloudflare 边缘缓存**。下载走边缘节点,
|
|
||||||
不用穿隧道回那台小机器,快几十倍也更省流量。
|
|
||||||
|
|
||||||
Java 和 Python 两端都实现了同一套协议,`_tests/java/` 下有互相打通的测试。
|
Java 和 Python 两端都实现了同一套协议,`_tests/java/` 下有互相打通的测试。
|
||||||
|
|
||||||
@@ -530,18 +429,7 @@ Java 和 Python 两端都实现了同一套协议,`_tests/java/` 下有互相
|
|||||||
不会。只要 `data/` 目录还在,服务器就认原来那个码。母盘部署时注意别把 `data/` 一起发出去。
|
不会。只要 `data/` 目录还在,服务器就认原来那个码。母盘部署时注意别把 `data/` 一起发出去。
|
||||||
|
|
||||||
**问:走中转会不会很慢?**
|
**问:走中转会不会很慢?**
|
||||||
实测局域网 3 毫秒左右;跨网络要绕到中转站再回来,约 1~2 秒(取决于你那台小机器的线路)。所以**同一个 WiFi 一定走局域网**,程序会优先选它。
|
实测局域网 3 毫秒左右,公网中转 1~3 秒(取决于你那台小机器的线路)。所以**同一个 WiFi 一定走局域网**,程序会优先选它。
|
||||||
|
|
||||||
**问:必须自己搭中转站吗?能用作者的服务器吗?**
|
|
||||||
必须自己搭。**软件不内置任何中转地址,作者也不对外提供中转服务。**
|
|
||||||
跨网络的话,服务端 zip 里有 `deploy.sh`,一条命令装好(见 [装中转站](#装中转站))。
|
|
||||||
只在同一个 WiFi 下用的话,压根不需要中转站。
|
|
||||||
|
|
||||||
**问:中转站会不会看到我的数据?**
|
|
||||||
它转发的是加密前的字节流,所以**技术上能**。这正是"自己搭一台"的理由 —— 搭在你自己手上,数据就只经过你自己的机器。
|
|
||||||
|
|
||||||
**问:下载安装包为什么比以前快这么多?**
|
|
||||||
最新版把静态文件交给 Cloudflare 的边缘节点缓存了。原来每次下载都要穿过隧道回到那台小机器上取,实测只有几 KB/s,15 MB 的安装包根本下不动;现在从离你最近的边缘节点直接发,实测快了四十多倍,也不占小机器的流量。
|
|
||||||
|
|
||||||
**问:能在手机上用吗?**
|
**问:能在手机上用吗?**
|
||||||
协议是通的,但界面是按手表的小屏幕设计的(大按钮、两列、可滚动),手机上会显得按钮特别大。要做手机版得重写布局。
|
协议是通的,但界面是按手表的小屏幕设计的(大按钮、两列、可滚动),手机上会显得按钮特别大。要做手机版得重写布局。
|
||||||
@@ -559,7 +447,7 @@ Java 和 Python 两端都实现了同一套协议,`_tests/java/` 下有互相
|
|||||||
|
|
||||||
1. 手表上显示的那句错误(App 现在是中文的,直接念出来就行)
|
1. 手表上显示的那句错误(App 现在是中文的,直接念出来就行)
|
||||||
2. 电脑上 `工具箱.bat` → 选 `5` 的日志内容
|
2. 电脑上 `工具箱.bat` → 选 `5` 的日志内容
|
||||||
3. 跨网络的话,把你那台中转站的公网地址发过来(不用给任何密码)
|
3. 中转站的公网地址(不用给密码)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
## 2.1.0
|
|
||||||
|
|
||||||
- **网站挂了会自己救**:中转服务器上常驻一个监测单元,每 60 秒从**公网**
|
|
||||||
查一次(不是查本机 —— 本机通不代表用户能打开)。连续 3 次不通就按阶梯
|
|
||||||
自动抢救:
|
|
||||||
|
|
||||||
| 级 | 动作 | 代价 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 重启隧道 | 不动中转,在线设备不掉线 |
|
|
||||||
| 2 | 重启中转服务 | 设备要重连,但很快 |
|
|
||||||
| 3 | 重新施加网络调优 | MSS / MTU 跑偏时用 |
|
|
||||||
| 4 | 重启无线网卡 | 4G / WiFi 卡死时用 |
|
|
||||||
| 5 | 重启整机 | 最后的底牌,**默认不开** |
|
|
||||||
|
|
||||||
每做完一级立刻复验,好了就停。救完 10 分钟内不再动手(防抖 ——
|
|
||||||
否则预案刚重启完隧道还没连上,下一次检查又救一遍,
|
|
||||||
来回重启反而永远连不上)。
|
|
||||||
|
|
||||||
- **网站出问题会自动留下诊断报告**:万一五级都没救回来,服务端会把现场
|
|
||||||
(内存、进程、隧道连接数、路由和 MTU、三份日志尾部、内核异常)
|
|
||||||
整理成一份 txt 留在服务端,需要排查时直接取来看就行。
|
|
||||||
只保留最近 30 份,不会把设备塞满。
|
|
||||||
|
|
||||||
每次自动抢救成功,也会把「第几级奏效」记进知识库,
|
|
||||||
下次同类问题优先试历史上管用的那一级。
|
|
||||||
|
|
||||||
- **网络热门音效**:手表上新增「音效」按钮,点一下电脑就响。
|
|
||||||
内置 8 个(冰冰冰 / 啊哈哈哈 / 老牧师 / 叮 / 当当当 / 警报 / 鼓点 / 弹簧)。
|
|
||||||
|
|
||||||
> 说明一下**版权**:网上那些「冰冰冰」「啊哈哈哈」是有版权的真实录音,
|
|
||||||
> 不能打包进安装包。所以内置的是**程序合成**的版本(正弦/方波现场算),
|
|
||||||
> 名字对着感觉来,但不是那些录音本身。
|
|
||||||
> 想要一模一样的一手素材,把 mp3/wav 丢进电脑的 `data\sfx\` 目录,
|
|
||||||
> 点「刷新」就出现在列表里,照样能远程播。目录里放了一份说明。
|
|
||||||
|
|
||||||
- **修「安卓设置页一片空白」**:设置页建好了但**忘了挂到窗口上**
|
|
||||||
(少了 `root.addView`),`showOnly()` 让它可见也没用 —— 它根本不在
|
|
||||||
视图树里。已修,并加了断言盯着「每个面板都必须挂到 root、
|
|
||||||
并且纳入 showOnly()」—— 这类漏挂编译不报错、只在运行时表现为空白。
|
|
||||||
|
|
||||||
- **全量测试改成后台静默**:会开窗口的 6 个阶段(含开**全屏**那个)
|
|
||||||
现在跑在独立的 Windows 隐藏桌面上 —— 窗口是**真的**(全屏、可见、
|
|
||||||
几何、Win32 枚举都真实),但屏幕上什么都看不到,也不会锁你的屏。
|
|
||||||
|
|
||||||
顺带修了一个一直误判的检测:判断「锁屏」时只看 `OpenInputDesktop`
|
|
||||||
成不成功是不对的 —— **锁屏时它同样成功**(那时输入桌面是 Winlogon)。
|
|
||||||
所以截屏类测试在锁屏时被记成 FAIL 而不是 SKIP,把真问题淹掉了。
|
|
||||||
改成看输入桌面的**名字**。现在锁屏也能跑全量:该 SKIP 的 SKIP,
|
|
||||||
该跑的照跑。
|
|
||||||
|
|
||||||
## 2.0.0
|
|
||||||
|
|
||||||
- **安装时就能设中转服务器**:安装向导那一页现在是两个输入框 ——
|
|
||||||
**配对码**和**中转服务器**。装的时候是"第一次设置",那时候最该一次
|
|
||||||
把地址填好,不然装完还得去双击 exe 找设置窗口,很容易漏。
|
|
||||||
只填域名就行(自动补 `https://`),留空 = 只走局域网。
|
|
||||||
批量部署同样支持:`/VERYSILENT /CODE=246810 /RELAY=relay.你的域名`
|
|
||||||
|
|
||||||
- **手表端新增「设置」页**:以前设置散在首页(只有一个"中转设置"按钮),
|
|
||||||
现在统一收进来,在里面调:
|
|
||||||
- **中转服务器**(填写 / 一键清空)
|
|
||||||
- **记住的配对码**(显示 / 清除)
|
|
||||||
- **已配对的电脑**(看数量 / 清空)
|
|
||||||
- **关于**(版本号 + 连接顺序说明)
|
|
||||||
|
|
||||||
- **中转地址改成手动填,默认留空**:以前有一版把作者的服务器地址写死成
|
|
||||||
默认值,第一次运行就存进了偏好设置,装新版本也不会清 ——
|
|
||||||
看起来就像"默认还是别人的中转站"。现在默认是空的,
|
|
||||||
并且会做**一次性迁移**把那个旧的内置地址清掉
|
|
||||||
(只清完全相等的,你自己填的不会动)。
|
|
||||||
|
|
||||||
- **修「局域网连上后要推出去点设备才能控制」**:手表上有四个页面,
|
|
||||||
切换时靠手工藏上一页,新加的「局域网连接」页漏藏了 ——
|
|
||||||
所以从它连上之后控制面板其实已经显示,却被盖住看不见。
|
|
||||||
|
|
||||||
- **修「双击 exe 没反应」**:装完之后双击现在会打开设置窗口。
|
|
||||||
|
|
||||||
- 新增**版本号一致性检查**和**中转地址默认值检查**两道断言,
|
|
||||||
防止这类"漏改一处"的问题再出现。
|
|
||||||
|
|
||||||
## 1.9.0
|
|
||||||
|
|
||||||
- **被控端终于有设置窗口了**:装完之后双击 `ykxmz.exe` 就能打开,里面可以改
|
|
||||||
**配对码**、**中转服务器**、**允许新设备接入**、开机自启。
|
|
||||||
以前双击是"什么都不弹",只能靠命令行参数改配置。
|
|
||||||
|
|
||||||
> 顺带修了一个很隐蔽的打包问题:PyInstaller 的 `--hidden-import` 里漏了
|
|
||||||
> `lanremote.ui`(设置窗口那套 tkinter 代码)。源码跑得好好的,
|
|
||||||
> 但**打包出来的 exe 一跑就崩**,用户看到的还是"双击没反应"。
|
|
||||||
|
|
||||||
- **修「局域网连上后没反应」**:手表上有四个页面,切换时靠手工藏上一页,
|
|
||||||
而新加的「局域网连接」页**漏藏了** —— 所以从它连上之后,控制面板其实
|
|
||||||
在底下显示了,却被盖住看不见。必须"推出去点一下设备"才看到。
|
|
||||||
根子是"每处切换都手写一串 setVisibility",改成统一入口 `showOnly()`,
|
|
||||||
以后加页面不会再漏。
|
|
||||||
|
|
||||||
- **修「按一下没反应」的真因**:中转那条长轮询(最长 60 秒)和用户操作
|
|
||||||
共用了同一个 HTTP 连接,长轮询占着连接时操作请求发不出去,
|
|
||||||
线程直接死掉,之后所有操作都没反应。改成各用各的连接。
|
|
||||||
|
|
||||||
- **中转地址默认留空**:需要跨网络时自己填。只在一个 WiFi 里用就什么都不用配,
|
|
||||||
那样手表和电脑直接连,最快也不耗流量。
|
|
||||||
|
|
||||||
- 新增**版本号一致性检查**:版本号散落在 8 个地方(exe 文件属性、安装包、
|
|
||||||
APK、许可页…),靠脚本+人工替换很容易漏。实测就漏了两处:
|
|
||||||
`version_info.txt` 和许可页都还写着 1.7.1 —— 用户在"文件属性"里
|
|
||||||
看到一个错三个版本的号。现在有断言钉住。
|
|
||||||
|
|
||||||
## 1.8.2
|
|
||||||
|
|
||||||
- **下载快了几十倍**:安装包和 APK 改成由 Cloudflare 的边缘节点直接发送。
|
|
||||||
原来每次下载都要穿过隧道回到那台 4G 小机器上取文件,实测吞吐只有
|
|
||||||
3~67 KB/s,15 MB 的安装包根本下不动(还会中途断掉)。
|
|
||||||
现在改走边缘缓存:同一个文件实测从 **17 KB/s 提到 695 KB/s**,
|
|
||||||
APK 从 3 KB/s 提到 101 KB/s,并且不再消耗 4G 流量。
|
|
||||||
|
|
||||||
- **跨网络连接更稳**:隧道改成**两种协议同时跑**,一共 10 条连接。
|
|
||||||
两种协议各有各的毛病 —— 实测这条线路上运营商对 UDP 有限制,
|
|
||||||
纯 QUIC 的拨号成功率只有 72%;而纯 HTTP/2 一旦丢包就容易卡住
|
|
||||||
(一条 TCP 上丢一个包,所有请求一起等)。
|
|
||||||
两个一起跑就是互相兜底:QUIC 拨号失败的那部分由 HTTP/2 顶着,
|
|
||||||
HTTP/2 卡住的时候 QUIC 的恢复更快。
|
|
||||||
实测 25 分钟连续可用率 **50/50 = 100%**(改之前是 96~98%),
|
|
||||||
尾部延迟 P99 从 3295ms 降到 2632ms。
|
|
||||||
|
|
||||||
- 修**网卡 MTU 和 QUIC 不匹配**:之前只给 TCP 做了 MSS 钳制,
|
|
||||||
而 QUIC 走 UDP 完全不受保护,包一大就被中间设备悄悄丢掉。
|
|
||||||
把网卡 MTU 降到 1400 之后,QUIC 建齐 10 条连接的时间从
|
|
||||||
**28 秒缩到 12 秒**。
|
|
||||||
|
|
||||||
- 修**响应头不一致**:下载接口之前对 GET 和 HEAD 报的缓存信息不一样,
|
|
||||||
部分下载工具(IDM、迅雷)探测时会把文件误判成不可缓存甚至空文件。
|
|
||||||
|
|
||||||
- 修**偶尔闪一下"已断开"**:原来只要有一次请求失败就立刻显示离线,
|
|
||||||
而 4G 链路上本来就有 2~4% 的偶发失败、一两秒就自己恢复。
|
|
||||||
现在改成**连续失败 3 次**才判离线 —— 真的断了仍然如实显示。
|
|
||||||
|
|
||||||
## 1.8.1
|
|
||||||
|
|
||||||
- 修**局域网连接被中转劫持**(重要):
|
|
||||||
只要「中转设置」里填了地址,点「局域网连接」填好 IP、端口、
|
|
||||||
配对码之后,程序**仍然走中转**,完全无视你填的局域网地址。
|
|
||||||
中转慢的时候要等 40 秒才报错,看起来就像"点了一点反应都没有"。
|
|
||||||
现在改成:局域网优先直连(6 秒超时),连不上才退回中转
|
|
||||||
|
|
||||||
- 修**已配对电脑的地址过期不提示**:
|
|
||||||
电脑换了网络之后(比如从 192.168.2.x 换到 192.168.10.x),
|
|
||||||
列表里还是旧地址,点一下必然失败。现在会标出「⚠ 地址可能已过期」,
|
|
||||||
并且跳过注定失败的直连,直接通过中转查询最新地址
|
|
||||||
|
|
||||||
- **长轮询从 15 秒延长到 60 秒**,跨网络更稳:
|
|
||||||
隧道每次请求要多花 1~2 秒(边缘路由 + 回源),而这个开销是按
|
|
||||||
**次数**收的 —— 每次都要吃一遍。拉长到 60 秒把请求数降到 1/4,
|
|
||||||
撞上丢包/重连窗口的概率也降到 1/4。
|
|
||||||
**命令响应速度不变**:有数据时立刻返回,不是等满 60 秒。
|
|
||||||
|
|
||||||
- 修局域网直连成功后打出误导性日志的问题
|
|
||||||
|
|
||||||
## 1.8.0
|
|
||||||
|
|
||||||
**这一版主要是「修看不见的稳定性问题」。**
|
|
||||||
|
|
||||||
- 修**跨网络老断**(Error 1033 / 502 / 530 的真因):
|
|
||||||
4G 链路会**静默断开 TCP 连接**(不发 FIN/RST,双方都以为还活着),
|
|
||||||
Cloudflare 把请求发到这种"僵尸连接"上就报错。
|
|
||||||
而 Linux 默认要 **2 小时**才发现连接死了 —— 中间这段时间
|
|
||||||
服务一直"看着在线、实际打不开"。
|
|
||||||
现在改成 30 秒探测、60 秒内发现并重连
|
|
||||||
|
|
||||||
- 修**跨网络抖动**:隧道连接数从 2 提到 4。
|
|
||||||
只剩 1 条连接时跨机房回源会失败,实测可用率 0/12;
|
|
||||||
提到 4 条后稳定 12/12
|
|
||||||
|
|
||||||
- 修**掉电重启后恢复太慢**:原来开机要 **2 分 10 秒**公网才恢复,
|
|
||||||
那段时间浏览器看到的就是 Error 1033。
|
|
||||||
根因是系统里 NetworkManager 和 systemd-networkd 同时在跑,
|
|
||||||
后者等一个不归它管的网络,干等 2 分钟超时失败,
|
|
||||||
把启动流程一起拖住了。改完 **17 秒**就绪,掉电重启后 24 秒公网可用
|
|
||||||
|
|
||||||
- 修**看门狗自己制造不稳定**:它每隔 50 秒重启一次隧道,
|
|
||||||
但隧道重建 4 条连接要 30~60 秒 —— 每次还没建齐就被下一次打断,
|
|
||||||
永远停在"只剩 1 条"的状态。现在重启后有宽限期,
|
|
||||||
并且会区分"没建好过"和"建好过又垮",后者立刻重启
|
|
||||||
|
|
||||||
- **官网重新设计**:层次更清楚,白色简洁风格
|
|
||||||
|
|
||||||
- **官网新增产品展示**:手表界面预览(颜色和布局与真实手表一致)、
|
|
||||||
电脑端真实截图、两种连接方式对比图
|
|
||||||
|
|
||||||
- 清理**僵尸设备记录**:设备列表原来只增不减,
|
|
||||||
实测一天攒了 75 条无效记录,拖慢手表和网页访问。
|
|
||||||
现在超过 6 小时没上报的自动清掉,占用的配对码一起释放
|
|
||||||
|
|
||||||
- 修下载文件用 `HEAD` 探测时长度报 0(部分下载器会误判成空文件)
|
|
||||||
|
|
||||||
- 支持 `HEAD` 请求(原来一律返回 501)
|
|
||||||
|
|
||||||
## 1.7.1
|
|
||||||
|
|
||||||
- 修**卸载卡死**:从控制面板卸载时进度条永远走不完。
|
|
||||||
根因是被控端在无控制台的隐藏进程里调 `AllocConsole` 和
|
|
||||||
读控制台输入,会永久阻塞
|
|
||||||
- 修**安装回滚**:旧版本程序还在后台跑时,安装器关不掉它,
|
|
||||||
弹「无法关闭所有应用程序」,静默安装会整个回滚 ——
|
|
||||||
用户看到的是"装了没反应"
|
|
||||||
- 修卸载残留:开机自启注册表项没删掉、安装目录留个空壳
|
|
||||||
- 修手表连上后**一片白屏**:分页初始化函数从没被调用过,
|
|
||||||
标签栏里只剩一个孤立的指示器
|
|
||||||
- 修「被控端已关闭新设备配对」把重装手表的人永久挡在外面:
|
|
||||||
配对码本身就是钥匙,拿着正确的码就该放行
|
|
||||||
- 手表端应用名改成 ykxmz
|
|
||||||
- 修跨网络**延迟高**:隧道退回了 QUIC(`--protocol http2` 参数丢失),
|
|
||||||
可用率只有 48%;恢复 HTTP/2 后可用率 100%
|
|
||||||
- 网络调优持久化:TCP 缓冲 4 MB、FastOpen、
|
|
||||||
空闲后不重置拥塞窗口(长轮询场景关键)
|
|
||||||
- 手表提示改成自诊断式:连不上时直接打出双方地址,
|
|
||||||
一眼看出是网段不同还是连错了 WiFi
|
|
||||||
- **官网重新设计**:导航 / 功能 / 三步上手 / 下载 / 常见问题
|
|
||||||
- 官网改成白色简洁风格
|
|
||||||
|
|
||||||
## 1.7.0
|
|
||||||
|
|
||||||
- 电脑端改成 **Windows 安装包**(Inno Setup),双击安装,不再需要解压 zip
|
|
||||||
- 安装向导里填 6 位配对码,装完手表直接连,不用再跑一次设置
|
|
||||||
- 安装包支持静默批量部署:`ykxmz-setup-1.7.0.exe /VERYSILENT /CODE=123456`
|
|
||||||
- 三份说明文档合并进安装向导,必须接受才能继续
|
|
||||||
- 修配置分裂问题:管理员装的配对码,普通用户账号看不到(数据目录改成按安装位置判断)
|
|
||||||
- 修装到 Program Files 后报「拒绝访问」:ProgramData 数据目录自动授权
|
|
||||||
- 修安装卡死:`--code` 命令处理完不返回
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# ykxmz 1.8.0 发布说明
|
|
||||||
|
|
||||||
**发布日期**:2026-10-07
|
|
||||||
**官网**:https://relay.xxmxxs.top
|
|
||||||
**产物**:
|
|
||||||
- Windows 被控端:`ykxmz-setup-1.8.0.exe`(14.37 MB,安装包)
|
|
||||||
- 服务端:`ykxmz-server-1.8.0.zip`(0.21 MB,随身 WiFi / Linux)
|
|
||||||
- 手表端:`ykxmz-watch-1.8.0.apk`(0.19 MB)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、修「跨网络老断」的真因(Error 1033 / 502 / 530)
|
|
||||||
|
|
||||||
这一版最主要的东西。现象是:手机/手表打开中转地址,时不时看到
|
|
||||||
Cloudflare 的 Error 1033 或者 502、530,过一会又自己好了。
|
|
||||||
|
|
||||||
**根因是 4G 链路会静默断开 TCP 连接** —— 不发 FIN 也不发 RST,
|
|
||||||
双方都以为连接还活着。Cloudflare 把请求发到这种「僵尸连接」上就报错。
|
|
||||||
|
|
||||||
而 Linux 默认要 **2 小时**才发现连接已经死了,中间这段时间
|
|
||||||
服务一直处于「看着在线、实际打不开」的状态。
|
|
||||||
|
|
||||||
**改了什么**
|
|
||||||
- 把 TCP 存活探测改成 30 秒一次、连续 3 次没回应就断开重连
|
|
||||||
→ 最多 60 秒发现僵尸连接
|
|
||||||
- 隧道连接数从 2 条提到 4 条。实测只剩 1 条连接时,
|
|
||||||
跨机房回源会失败(可用率 0/12),4 条时稳定 12/12
|
|
||||||
|
|
||||||
## 二、修「掉电重启后要等两分钟」
|
|
||||||
|
|
||||||
原来设备重启后,要 **2 分 10 秒**公网才能访问,那段时间
|
|
||||||
浏览器看到的就是 Error 1033。
|
|
||||||
|
|
||||||
原因是系统里有两个网络管理服务同时在跑,其中一个在等
|
|
||||||
一个不归它管的网络,干等 2 分钟超时失败,把整个启动流程拖住了。
|
|
||||||
|
|
||||||
**改完:开机 17 秒就绪,掉电重启后 24 秒公网可用。**
|
|
||||||
|
|
||||||
## 三、看门狗重做
|
|
||||||
|
|
||||||
看门狗负责在隧道不通时自动重启它。这一版修了它自己的两个问题:
|
|
||||||
|
|
||||||
- 之前它每 50 秒重启一次隧道,但隧道重建连接要 30~60 秒,
|
|
||||||
**每次还没建好就被下一次重启打断**,反而永远恢复不了
|
|
||||||
- 加了宽限期;又发现宽限期太长会在链路持续不好时干等
|
|
||||||
- 最终逻辑:**建好过又断 = 真故障,立刻重启;一直没建好 = 它在努力,继续等**
|
|
||||||
|
|
||||||
## 四、官网重新设计
|
|
||||||
|
|
||||||
导航结构更清楚,整体白色简洁风格。
|
|
||||||
|
|
||||||
新增产品展示区:
|
|
||||||
- **手表界面预览**(颜色和布局与真实手表一致)
|
|
||||||
- **电脑端真实截图**
|
|
||||||
- **两种连接方式对比图**(局域网直连 vs 跨网络中转)
|
|
||||||
|
|
||||||
## 五、清理僵尸设备记录
|
|
||||||
|
|
||||||
设备列表原来只增不减,实测一天攒了 **75 条**无效记录,
|
|
||||||
拖慢手表和网页访问,配对码占用数也虚高。
|
|
||||||
|
|
||||||
现在超过 6 小时没上报的设备自动清掉,它占用的配对码一起释放。
|
|
||||||
|
|
||||||
## 六、其它
|
|
||||||
|
|
||||||
- 修下载文件用 `HEAD` 探测时长度报 0(部分下载器会误判成空文件)
|
|
||||||
- 支持 `HEAD` 请求(原来一律返回 501)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验收结果
|
|
||||||
|
|
||||||
| 项目 | 结果 |
|
|
||||||
|---|---|
|
|
||||||
| 全量回归 | **33 个阶段全部通过** |
|
|
||||||
| 安装包完整周期 | **20/20**(静默安装 7 秒、卸载 0.6 秒、无残留) |
|
|
||||||
| 网页响应头 | **12/12** |
|
|
||||||
| 公网可用率 | 26/30 = 86%(底层 4G 丢包 30%,这是链路的上限) |
|
|
||||||
|
|
||||||
> 说明:可用率受限于现场 4G 网络质量。换有线网络或把中转
|
|
||||||
> 放到有公网 IP 的服务器上可以显著改善。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 下载
|
|
||||||
|
|
||||||
**https://relay.xxmxxs.top**
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
passed=34 failed=0
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
passed=18 failed=0
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
gBp85DSPF9vKr12jTb6XIPt3
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"lantest": {
|
|
||||||
"name": "测试手表",
|
|
||||||
"token": "6ec9b2878357045a389acd97bab84871",
|
|
||||||
"paired_at": 1791303182.5310695,
|
|
||||||
"last_seen": 1791303182.5336223
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"port": 8875,
|
|
||||||
"server_name": "DESKTOP-RN5I625",
|
|
||||||
"allow_run": true,
|
|
||||||
"allow_power": true,
|
|
||||||
"allow_screen": true,
|
|
||||||
"start_minimized": false,
|
|
||||||
"allow_new_pairing": false,
|
|
||||||
"image_timeout": 0,
|
|
||||||
"image_display": "window",
|
|
||||||
"image_custom_width": 0,
|
|
||||||
"image_custom_height": 0,
|
|
||||||
"image_window_title": "",
|
|
||||||
"tts_voice": "",
|
|
||||||
"tts_rate": 0,
|
|
||||||
"mp3_dir": "",
|
|
||||||
"file_roots": "",
|
|
||||||
"extra_apps": "",
|
|
||||||
"extra_commands": "",
|
|
||||||
"download_dir": "received",
|
|
||||||
"screen_max_width": 480,
|
|
||||||
"screen_quality": 72,
|
|
||||||
"screen_fps": 12,
|
|
||||||
"pairing_code": "246810",
|
|
||||||
"pairing_created": 1791303175.5928898,
|
|
||||||
"setup_done": true,
|
|
||||||
"close_pair_after_first": false,
|
|
||||||
"relay_last_good": "http://192.168.2.156:8080",
|
|
||||||
"relay_device_id": "DESKTOP-RN5I625-832c5e0b202b"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"watch-reinstalled": {
|
|
||||||
"name": "手表",
|
|
||||||
"token": "45be12c203e6d66d5a042091e9ba5900",
|
|
||||||
"paired_at": 1791299138.656342,
|
|
||||||
"last_seen": 1791299138.656342
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"port": 8873,
|
|
||||||
"server_name": "DESKTOP-RN5I625",
|
|
||||||
"allow_run": true,
|
|
||||||
"allow_power": true,
|
|
||||||
"allow_screen": true,
|
|
||||||
"start_minimized": false,
|
|
||||||
"allow_new_pairing": false,
|
|
||||||
"image_timeout": 0,
|
|
||||||
"image_display": "window",
|
|
||||||
"image_custom_width": 0,
|
|
||||||
"image_custom_height": 0,
|
|
||||||
"image_window_title": "",
|
|
||||||
"tts_voice": "",
|
|
||||||
"tts_rate": 0,
|
|
||||||
"mp3_dir": "",
|
|
||||||
"file_roots": "",
|
|
||||||
"extra_apps": "",
|
|
||||||
"extra_commands": "",
|
|
||||||
"download_dir": "received",
|
|
||||||
"screen_max_width": 480,
|
|
||||||
"screen_quality": 72,
|
|
||||||
"screen_fps": 12,
|
|
||||||
"pairing_code": "135790",
|
|
||||||
"pairing_created": 1791299136.6660163,
|
|
||||||
"setup_done": true,
|
|
||||||
"close_pair_after_first": false,
|
|
||||||
"relay_last_good": "http://192.168.2.156:8080",
|
|
||||||
"relay_device_id": "DESKTOP-RN5I625-59e6fdd69b09"
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@echo off␍
|
|
||||||
"C:\Program Files\ykxmz\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"port": 8898,
|
|
||||||
"server_name": "DESKTOP-RN5I625",
|
|
||||||
"allow_run": true,
|
|
||||||
"allow_power": true,
|
|
||||||
"allow_screen": true,
|
|
||||||
"start_minimized": false,
|
|
||||||
"allow_new_pairing": true,
|
|
||||||
"image_timeout": 0,
|
|
||||||
"image_display": "window",
|
|
||||||
"image_custom_width": 0,
|
|
||||||
"image_custom_height": 0,
|
|
||||||
"image_window_title": "",
|
|
||||||
"tts_voice": "",
|
|
||||||
"tts_rate": 0,
|
|
||||||
"mp3_dir": "",
|
|
||||||
"file_roots": "",
|
|
||||||
"extra_apps": "",
|
|
||||||
"extra_commands": "",
|
|
||||||
"download_dir": "received",
|
|
||||||
"screen_max_width": 480,
|
|
||||||
"screen_quality": 72,
|
|
||||||
"screen_fps": 12,
|
|
||||||
"pairing_code": "242524",
|
|
||||||
"pairing_created": 1791335816.802805
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"port": 8898,
|
|
||||||
"server_name": "DESKTOP-RN5I625",
|
|
||||||
"allow_run": true,
|
|
||||||
"allow_power": true,
|
|
||||||
"allow_screen": true,
|
|
||||||
"start_minimized": false,
|
|
||||||
"allow_new_pairing": true,
|
|
||||||
"image_timeout": 0,
|
|
||||||
"image_display": "window",
|
|
||||||
"image_custom_width": 0,
|
|
||||||
"image_custom_height": 0,
|
|
||||||
"image_window_title": "",
|
|
||||||
"tts_voice": "",
|
|
||||||
"tts_rate": 0,
|
|
||||||
"mp3_dir": "",
|
|
||||||
"file_roots": "",
|
|
||||||
"extra_apps": "",
|
|
||||||
"extra_commands": "",
|
|
||||||
"download_dir": "received",
|
|
||||||
"screen_max_width": 480,
|
|
||||||
"screen_quality": 72,
|
|
||||||
"screen_fps": 12,
|
|
||||||
"pairing_code": "598318",
|
|
||||||
"pairing_created": 1791335863.3186042
|
|
||||||
}
|
|
||||||
-158
@@ -1,158 +0,0 @@
|
|||||||
============================================================
|
|
||||||
deploy.sh 实测
|
|
||||||
============================================================
|
|
||||||
仓库: /c/Users/shu/Desktop/deepseekagent/LanRemote
|
|
||||||
脚本: /c/Users/shu/Desktop/deepseekagent/LanRemote/relay/deploy.sh
|
|
||||||
|
|
||||||
── 1. 语法检查(bash -n)──
|
|
||||||
[PASS] deploy.sh 语法正确
|
|
||||||
[PASS] deploy-tune.sh 语法正确
|
|
||||||
|
|
||||||
── 2. 换行符 / BOM(.sh 必须 LF + 无 BOM)──
|
|
||||||
[PASS] deploy.sh 是 LF 且无 BOM
|
|
||||||
[PASS] deploy-tune.sh 是 LF 且无 BOM
|
|
||||||
|
|
||||||
── 3. --help ──
|
|
||||||
[PASS] --help 退出码为 0
|
|
||||||
[PASS] --help 有「用法」段
|
|
||||||
[PASS] --help 列出了 --prefix
|
|
||||||
[PASS] --help 提到了 TUNNEL_TOKEN
|
|
||||||
[PASS] --help 没有副作用
|
|
||||||
|
|
||||||
── 3b. 非法参数要报错退出 ──
|
|
||||||
[PASS] 未知参数退出码为 2
|
|
||||||
[PASS] 非法协议退出码为 2
|
|
||||||
|
|
||||||
── 4. 搭沙箱(桩命令 + 临时安装根)──
|
|
||||||
沙箱: /tmp/tmp.jb8lwoeXgw
|
|
||||||
安装根: /tmp/tmp.jb8lwoeXgw/root
|
|
||||||
桩命令: cloudflared curl free getent ip iptables nmcli python3 systemctl
|
|
||||||
|
|
||||||
── 5. 第一次部署(--skip-tunnel,不碰隧道)──
|
|
||||||
[PASS] 第一次退出码为 0
|
|
||||||
[PASS] relay.py 已部署
|
|
||||||
[PASS] admin.py 已部署
|
|
||||||
[PASS] landing.py 已部署
|
|
||||||
[PASS] showcase.py 已部署
|
|
||||||
[PASS] favicon.ico 已部署
|
|
||||||
[PASS] deploy-tune.sh 已部署
|
|
||||||
[PASS] watchdog.sh 已部署
|
|
||||||
[PASS] site_shots 已部署
|
|
||||||
[PASS] data 目录已建
|
|
||||||
[PASS] downloads 目录已建
|
|
||||||
[PASS] lanremote-relay.service 已生成
|
|
||||||
[PASS] lanremote-nettune.service 已生成
|
|
||||||
[PASS] lanremote-tunnel.service 已生成
|
|
||||||
[PASS] 备用 http2 单元已生成
|
|
||||||
[PASS] 看门狗 timer 已生成
|
|
||||||
|
|
||||||
生成的 relay unit 里应该有的关键项:
|
|
||||||
[PASS] relay unit 含 RELAY_POLL=60
|
|
||||||
[PASS] relay unit 含 RELAY_PORT=8080
|
|
||||||
[PASS] relay unit 含 python3
|
|
||||||
|
|
||||||
两个隧道单元的内容(默认主 quic+8 / 副本 http2+2):
|
|
||||||
[PASS] 主隧道单元用 quic
|
|
||||||
[PASS] 主隧道 8 条连接(实测最优配比)
|
|
||||||
[PASS] 副本隧道单元用 http2
|
|
||||||
[PASS] 副本隧道 2 条连接(实测最优配比)
|
|
||||||
[PASS] 主隧道 metrics 钉死 20241
|
|
||||||
[PASS] 副本隧道 metrics 钉死 20242
|
|
||||||
[PASS] nettune 只有一个入口脚本
|
|
||||||
[PASS] unit 里没有明文 Token
|
|
||||||
[PASS] 便捷命令 lanremote-url 已生成
|
|
||||||
[PASS] 便捷命令 lanremote-key 已生成
|
|
||||||
[PASS] 便捷命令 lanremote-status 已生成
|
|
||||||
|
|
||||||
── 6. 幂等性:放几个「不许被冲掉」的东西,再跑第二遍 ──
|
|
||||||
[PASS] 第二次退出码为 0
|
|
||||||
[PASS] 设备数据没被冲掉
|
|
||||||
[PASS] 管理钥匙没被冲掉
|
|
||||||
[PASS] 管理密码没被冲掉
|
|
||||||
[PASS] 公网地址没被冲掉
|
|
||||||
[PASS] relay_state.json 没被改(allowNew 保住了)
|
|
||||||
[PASS] allowNew 仍然是 false(没被拨回 true)
|
|
||||||
[PASS] 手工 config.yml 没被覆盖
|
|
||||||
[PASS] relay.py 内容稳定
|
|
||||||
[PASS] 第二遍没有新增 ✗ 错误
|
|
||||||
|
|
||||||
── 6b. 没变化时不该重启服务(别打断在线连接)──
|
|
||||||
[PASS] 第二遍没有重启中转(无变化)
|
|
||||||
[PASS] 第二遍没有重启隧道(无变化)
|
|
||||||
[PASS] 第二遍没有多余的 daemon-reload
|
|
||||||
[PASS] 输出里明确说了「内容没变、保持运行」
|
|
||||||
|
|
||||||
── 7. 凭据模式:给出域名时要能生成 config.yml ──
|
|
||||||
[PASS] 凭据模式退出码为 0
|
|
||||||
[PASS] config.yml 已生成
|
|
||||||
[PASS] config.yml 里的隧道 ID 取自凭据文件名
|
|
||||||
[PASS] config.yml 写入了域名
|
|
||||||
[PASS] config.yml 有兜底 404 规则(没有它 cloudflared 起不来)
|
|
||||||
[PASS] config.yml 指向本地中转端口
|
|
||||||
|
|
||||||
── 7c. 默认:两个隧道单元都要启用(协议级故障隔离)──
|
|
||||||
[PASS] 默认(不传 --protocol)退出码为 0
|
|
||||||
[PASS] 默认启用了主隧道
|
|
||||||
[PASS] 默认启用了副本隧道
|
|
||||||
[PASS] 默认没有关掉任何一个隧道
|
|
||||||
|
|
||||||
── 7d. --protocol quic:只跑主隧道 ──
|
|
||||||
[PASS] --protocol quic 退出码为 0
|
|
||||||
[PASS] quic 模式启用了主隧道
|
|
||||||
[PASS] quic 模式停用了副本隧道
|
|
||||||
[PASS] 输出说明了为什么停用副本
|
|
||||||
|
|
||||||
── 7e. --protocol http2:只跑副本隧道 ──
|
|
||||||
[PASS] --protocol http2 退出码为 0
|
|
||||||
[PASS] http2 模式启用了副本隧道
|
|
||||||
[PASS] http2 模式停用了主隧道
|
|
||||||
|
|
||||||
── 7f. --protocol 的非法值要报错 ──
|
|
||||||
[PASS] 非法协议退出码为 2
|
|
||||||
[PASS] both 是合法值
|
|
||||||
|
|
||||||
── 7b. 再跑一次(不给 --hostname),config.yml 不该被改 ──
|
|
||||||
[PASS] 再次运行退出码为 0
|
|
||||||
[PASS] 没给 --hostname 时 config.yml 保持不变
|
|
||||||
|
|
||||||
── 8. Token 模式 ──
|
|
||||||
[PASS] Token 模式退出码为 0
|
|
||||||
[PASS] Token 写进了 tunnel.env
|
|
||||||
[PASS] tunnel.env 内容正确
|
|
||||||
[PASS] unit 里没有明文 Token(只有 EnvironmentFile 引用)
|
|
||||||
[PASS] unit 用了 EnvironmentFile 读 Token
|
|
||||||
[PASS] unit 的 ExecStart 里用 --token ${TUNNEL_TOKEN}
|
|
||||||
|
|
||||||
── 8b. tunnel.env 权限应该是 600 ──
|
|
||||||
tunnel.env 权限: 644(Linux 上应为 600)
|
|
||||||
|
|
||||||
── 9. 缺文件时要明确报错,不能静默 ──
|
|
||||||
[PASS] 文件不全时明确失败(退出码 1)
|
|
||||||
[PASS] 报错信息说清了是「程序文件不全」
|
|
||||||
|
|
||||||
── 10. 桩命令确实被调用过(证明流程真的走了)──
|
|
||||||
[PASS] 调用过 systemctl enable
|
|
||||||
[PASS] 调用过 iptables
|
|
||||||
[PASS] 第一次安装做了 daemon-reload
|
|
||||||
|
|
||||||
── 11. 脚本里不许有写死的域名 ──
|
|
||||||
[PASS] deploy.sh 里没有写死的域名
|
|
||||||
[PASS] deploy-tune.sh 里没有写死的域名
|
|
||||||
[PASS] net-tune.sh 里没有写死的域名
|
|
||||||
[PASS] 示例用的是「你的域名」占位符
|
|
||||||
|
|
||||||
── 12. 有凭据但没给域名 → 不许乱猜,必须明确提示 ──
|
|
||||||
[调试] TMP=/tmp/tmp.jb8lwoeXgw
|
|
||||||
[调试] 凭据目录内容: deadbeef-0000-1111-2222-333344445555.json
|
|
||||||
[PASS] 没域名时退出码为 0(中转照装,只是隧道不启)
|
|
||||||
[PASS] 没域名时没有生成 config.yml(没有乱猜)
|
|
||||||
[PASS] 明确提示了「需要你提供公网域名」
|
|
||||||
[PASS] 提示里给了怎么补域名
|
|
||||||
[PASS] 提示里给了 Token 模式这个替代办法
|
|
||||||
[PASS] 隧道没配好时没有去启动它
|
|
||||||
[PASS] 中转仍然正常安装并启用
|
|
||||||
|
|
||||||
============================================================
|
|
||||||
结果: 通过 98 项,失败 0 项
|
|
||||||
============================================================
|
|
||||||
沙箱目录(想自己看就留着): /tmp/tmp.jb8lwoeXgw
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=LanRemote Cloudflare 具名隧道
|
||||||
|
After=network-online.target lanremote-relay.service
|
||||||
|
Wants=network-online.target lanremote-relay.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --protocol http2 --edge-ip-version 4 --ha-connections 1 --retries 5 --config /root/.cloudflared/config.yml run lanremote
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
MemoryMax=128M
|
||||||
|
StandardOutput=append:/root/lanremote-relay/tunnel.log
|
||||||
|
StandardError=append:/root/lanremote-relay/tunnel.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"watch-probe-1": {
|
|
||||||
"name": "手表",
|
|
||||||
"token": "79ce7c72c1ad0526a6efe153375cc22c",
|
|
||||||
"paired_at": 1791298726.377039,
|
|
||||||
"last_seen": 1791298726.377039
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"port": 8871,
|
|
||||||
"server_name": "DESKTOP-RN5I625",
|
|
||||||
"allow_run": true,
|
|
||||||
"allow_power": true,
|
|
||||||
"allow_screen": true,
|
|
||||||
"start_minimized": false,
|
|
||||||
"allow_new_pairing": false,
|
|
||||||
"image_timeout": 0,
|
|
||||||
"image_display": "window",
|
|
||||||
"image_custom_width": 0,
|
|
||||||
"image_custom_height": 0,
|
|
||||||
"image_window_title": "",
|
|
||||||
"tts_voice": "",
|
|
||||||
"tts_rate": 0,
|
|
||||||
"mp3_dir": "",
|
|
||||||
"file_roots": "",
|
|
||||||
"extra_apps": "",
|
|
||||||
"extra_commands": "",
|
|
||||||
"download_dir": "received",
|
|
||||||
"screen_max_width": 480,
|
|
||||||
"screen_quality": 72,
|
|
||||||
"screen_fps": 12,
|
|
||||||
"pairing_code": "717525",
|
|
||||||
"pairing_created": 1791298724.895755,
|
|
||||||
"close_pair_after_first": false,
|
|
||||||
"relay_last_good": "http://192.168.2.156:8080",
|
|
||||||
"relay_device_id": "DESKTOP-RN5I625-e35d64d5d85b"
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# quic+6 vs http2+4:用**真实长轮询**对照,方法完全一致。
|
|
||||||
#
|
|
||||||
# 为什么必须用长轮询测:
|
|
||||||
# 之前用 /v1/health 那种"每次新建连接"的方式测,
|
|
||||||
# 量到的是冷启动开销,和真实业务(复用连接的长轮询)差很多。
|
|
||||||
# 而且那种方式会看到 18 秒的尾巴 —— 长轮询完全没有。
|
|
||||||
#
|
|
||||||
# 为什么这次结果可信:
|
|
||||||
# 上一轮 A/B 里 quic 那组因为**两次探针用了同一个配对码**,
|
|
||||||
# 第二次注册被"一个码只能一台设备"规则拒掉,token 拿到空的,
|
|
||||||
# 8 次全 401 —— 那一轮数据作废。这次每个探针都用唯一的新码。
|
|
||||||
#
|
|
||||||
# 为什么值得测 QUIC:
|
|
||||||
# 刚才查出来:隧道连接 30 分钟 0 掉线、0 重连,很稳;
|
|
||||||
# 但请求级偶发 000(10 分钟 6 次)。
|
|
||||||
# 这是 HTTP/2 over TCP 的**队头阻塞** ——
|
|
||||||
# 一条 TCP 上丢一个包,那条连接上所有流一起卡。
|
|
||||||
# QUIC 每个流独立恢复,理论上正好治这个。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
PUB=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
BACKUP=/root/lanremote-tunnel.service.bak.q.$(date +%s)
|
|
||||||
cp -a "$SVC" "$BACKUP"
|
|
||||||
ROUNDS=${1:-10}
|
|
||||||
|
|
||||||
set_params() {
|
|
||||||
local proto="$1" conns="$2"
|
|
||||||
if grep -q -- '--protocol' "$SVC"; then
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
|
||||||
fi
|
|
||||||
if grep -q -- '--ha-connections' "$SVC"; then
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
|
||||||
fi
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_ready() {
|
|
||||||
local want="$1"
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
sleep 5
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
[ "${n:-0}" -ge "$want" ] && return 0
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
new_probe() {
|
|
||||||
# 每次都用全新的 deviceId + 全新的配对码,避免被"一码一机"拒掉
|
|
||||||
local stamp
|
|
||||||
stamp=$(date +%s%N | cut -c1-13)
|
|
||||||
local dev="qprobe-${stamp}"
|
|
||||||
local code
|
|
||||||
code=$(printf "%06d" $(( (stamp / 7) % 1000000 )))
|
|
||||||
local reg
|
|
||||||
reg=$(curl -s --max-time 30 -X POST "$PUB/v1/agent/register" \
|
|
||||||
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
|
|
||||||
-d "{\"deviceId\":\"$dev\",\"code\":\"$code\",\"name\":\"qprobe\",\"version\":\"1.8.1\"}")
|
|
||||||
local tok
|
|
||||||
tok=$(echo "$reg" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
|
||||||
PROBE_DEV="$dev"
|
|
||||||
PROBE_TOK="$tok"
|
|
||||||
PROBE_CODE="$code"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_probe() {
|
|
||||||
local key
|
|
||||||
key=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
[ -n "${PROBE_DEV:-}" ] && curl -s --max-time 20 -X POST \
|
|
||||||
"http://127.0.0.1:8080/v1/admin/delete" -H "X-Admin: $key" \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d "{\"deviceId\":\"$PROBE_DEV\"}" > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
run_round() {
|
|
||||||
local label="$1" proto="$2" conns="$3"
|
|
||||||
|
|
||||||
echo "════════ $label ════════"
|
|
||||||
set_params "$proto" "$conns"
|
|
||||||
if wait_ready "$conns"; then
|
|
||||||
echo " 连接就绪($(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') 条)"
|
|
||||||
else
|
|
||||||
echo " ! 连接没建齐,仍然继续测"
|
|
||||||
fi
|
|
||||||
echo " 落点:"
|
|
||||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep 'server_locations.*} 1' | sed 's/^/ /'
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
new_probe
|
|
||||||
if [ -z "${PROBE_TOK:-}" ]; then
|
|
||||||
echo " [失败] 探针注册没拿到 token,这轮作废"
|
|
||||||
echo "$label|REGFAIL|0|0|0" >> /tmp/qab.txt
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
echo " 探针 code=$PROBE_CODE token=${#PROBE_TOK} 字符"
|
|
||||||
|
|
||||||
local mark
|
|
||||||
mark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
local ok=0 fail=0 times=() fails=""
|
|
||||||
for i in $(seq 1 "$ROUNDS"); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 100 \
|
|
||||||
-X POST "$PUB/v1/agent/poll" -H "X-Token: $PROBE_TOK" \
|
|
||||||
-H "User-Agent: $UA")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
if [ "$code" = "200" ]; then
|
|
||||||
ok=$((ok+1)); times+=("$ms")
|
|
||||||
printf " %2d: 200 %6dms\n" "$i" "$ms"
|
|
||||||
else
|
|
||||||
fail=$((fail+1)); fails="$fails ${i}:${code}"
|
|
||||||
printf " %2d: %-4s **失败**\n" "$i" "$code"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
local stats
|
|
||||||
if [ "${#times[@]}" -gt 0 ]; then
|
|
||||||
stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}')
|
|
||||||
else
|
|
||||||
stats="无数据"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local reconn
|
|
||||||
reconn=$(tail -n +$((mark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \
|
|
||||||
| grep -c 'Registered tunnel connection' || echo 0)
|
|
||||||
|
|
||||||
echo " == $ok/$ROUNDS 成功 耗时: $stats ms 本轮重连 $reconn 次"
|
|
||||||
echo " 失败:$fails"
|
|
||||||
echo "$label|$ok|$ROUNDS|$((ok*100/ROUNDS))|$stats|$reconn" >> /tmp/qab.txt
|
|
||||||
cleanup_probe
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/qab.txt
|
|
||||||
|
|
||||||
# 先基线,再候选 —— 同一网络条件下先后测,尽量公平
|
|
||||||
run_round "http2+4" http2 4 || true
|
|
||||||
run_round "quic+6" quic 6 || true
|
|
||||||
|
|
||||||
echo "════════ 汇总 ════════"
|
|
||||||
printf " %-10s %-10s %-8s %-32s %s\n" "配置" "成功" "可用率" "耗时" "本轮重连"
|
|
||||||
while IFS='|' read -r label ok total pct stats reconn; do
|
|
||||||
if [ "$ok" = "REGFAIL" ]; then
|
|
||||||
printf " %-10s 探针注册失败,作废\n" "$label"
|
|
||||||
else
|
|
||||||
printf " %-10s %-10s %-8s %-32s %s\n" "$label" "$ok/$total" "${pct}%" "$stats" "$reconn"
|
|
||||||
fi
|
|
||||||
done < /tmp/qab.txt
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 决策 ════════"
|
|
||||||
B_OK=$(grep '^http2+4' /tmp/qab.txt | cut -d'|' -f4)
|
|
||||||
Q_OK=$(grep '^quic+6' /tmp/qab.txt | cut -d'|' -f4)
|
|
||||||
B_REC=$(grep '^http2+4' /tmp/qab.txt | cut -d'|' -f6)
|
|
||||||
Q_REC=$(grep '^quic+6' /tmp/qab.txt | cut -d'|' -f6)
|
|
||||||
echo " 基线: 可用率 ${B_OK:-?}% 重连 ${B_REC:-?}"
|
|
||||||
echo " 候选: 可用率 ${Q_OK:-?}% 重连 ${Q_REC:-?}"
|
|
||||||
if [ "${Q_OK:-0}" -ge "${B_OK:-0}" ] && [ "${Q_OK:-0}" -ge 90 ]; then
|
|
||||||
echo " → 采用 quic+6"
|
|
||||||
else
|
|
||||||
echo " → 保留 http2+4"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
fi
|
|
||||||
sleep 15
|
|
||||||
echo
|
|
||||||
echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')"
|
|
||||||
echo " 备份: $BACKUP"
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# TCP 重传调优的受控 A/B。
|
|
||||||
#
|
|
||||||
# 背景(实测):
|
|
||||||
# TCPTimeouts: 3145 · TCPLostRetransmit: 2010
|
|
||||||
# 这些多秒/多分钟的尾巴全是 TCP 重传超时造成的。
|
|
||||||
# 这台机器内核是定制的,**没有 BBR**(只有 reno/cubic),
|
|
||||||
# 也没有任何替代 qdisc(只有 noqueue),所以只能在
|
|
||||||
# 现有 cubic 上调 TCP 的重传行为。
|
|
||||||
#
|
|
||||||
# 三个候选开关(都对症"细流 + 高丢包"这个场景):
|
|
||||||
# tcp_thin_linear_timeouts 细流不搞 RTO 指数退避,重传来得更快
|
|
||||||
# tcp_fack 前向确认,丢包恢复更积极
|
|
||||||
# tcp_retries2 重传几次就放弃(8 太大,卡十几分钟)
|
|
||||||
#
|
|
||||||
# 每轮:设参数 -> 打 60 次 HTTP -> 记录成功率/耗时分布 -> 下一轮
|
|
||||||
# 全部跑完回滚,最后按数据决定采用哪组。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
N=60
|
|
||||||
|
|
||||||
# 保存原始值,跑完恢复
|
|
||||||
declare -A ORIG
|
|
||||||
for k in tcp_thin_linear_timeouts tcp_fack tcp_retries2 tcp_autocorking; do
|
|
||||||
ORIG[$k]=$(sysctl -n net.ipv4.$k 2>/dev/null || echo "")
|
|
||||||
done
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
for k in "${!ORIG[@]}"; do
|
|
||||||
[ -n "${ORIG[$k]}" ] && sysctl -qw net.ipv4.$k="${ORIG[$k]}" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
measure() {
|
|
||||||
local label="$1"
|
|
||||||
local ok=0 fail=0 times=()
|
|
||||||
for i in $(seq 1 "$N"); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
|
||||||
-H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
c=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
if [ "$c" = "200" ]; then ok=$((ok+1)); times+=("$ms"); else fail=$((fail+1)); fi
|
|
||||||
done
|
|
||||||
local pct=$((ok * 100 / N))
|
|
||||||
local stats="无数据"
|
|
||||||
if [ "${#times[@]}" -gt 0 ]; then
|
|
||||||
stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
|
|
||||||
fi
|
|
||||||
printf " %-34s %3d%% (%d/%d) %s ms\n" "$label" "$pct" "$ok" "$N" "$stats"
|
|
||||||
echo "$label|$pct|$ok|$stats" >> /tmp/tcpab.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/tcpab.txt
|
|
||||||
|
|
||||||
echo "════════ 原始值 ════════"
|
|
||||||
for k in "${!ORIG[@]}"; do printf " %-30s = %s\n" "$k" "${ORIG[$k]}"; done
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "════════ 各组合实测(每轮 $N 次)════════"
|
|
||||||
|
|
||||||
echo " [1/6] 基线(不改)"
|
|
||||||
measure "基线"
|
|
||||||
|
|
||||||
echo " [2/6] thin_linear_timeouts=1"
|
|
||||||
sysctl -qw net.ipv4.tcp_thin_linear_timeouts=1
|
|
||||||
measure "thin=1"
|
|
||||||
|
|
||||||
echo " [3/6] thin=1 + fack=1"
|
|
||||||
sysctl -qw net.ipv4.tcp_fack=1
|
|
||||||
measure "thin=1 fack=1"
|
|
||||||
|
|
||||||
echo " [4/6] thin=1 fack=1 + retries2=5"
|
|
||||||
sysctl -qw net.ipv4.tcp_retries2=5
|
|
||||||
measure "thin=1 fack=1 retries2=5"
|
|
||||||
|
|
||||||
echo " [5/6] thin=1 fack=1 retries2=5 + autocorking=0"
|
|
||||||
sysctl -qw net.ipv4.tcp_autocorking=0
|
|
||||||
measure "全部开启"
|
|
||||||
|
|
||||||
echo " [6/6] 回滚到基线再测一次(排除时间漂移)"
|
|
||||||
restore
|
|
||||||
measure "回滚后基线"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 汇总(按可用率排序)════════"
|
|
||||||
sort -t'|' -k2 -rn /tmp/tcpab.txt | while IFS='|' read -r label pct ok stats; do
|
|
||||||
printf " %-34s %3s%% %s\n" "$label" "$pct" "$stats"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 按数据决定 ════════"
|
|
||||||
BEST=$(sort -t'|' -k2 -rn /tmp/tcpab.txt | head -1)
|
|
||||||
BEST_LABEL=$(echo "$BEST" | cut -d'|' -f1)
|
|
||||||
BEST_PCT=$(echo "$BEST" | cut -d'|' -f2)
|
|
||||||
echo " 最好的是: $BEST_LABEL ($BEST_PCT%)"
|
|
||||||
|
|
||||||
case "$BEST_LABEL" in
|
|
||||||
"基线"|"回滚后基线")
|
|
||||||
echo " → 调优没有收益,保持默认(只留已验证过的 keepalive 那几项)"
|
|
||||||
restore
|
|
||||||
;;
|
|
||||||
"thin=1")
|
|
||||||
sysctl -qw net.ipv4.tcp_thin_linear_timeouts=1
|
|
||||||
echo " → 只启用 thin_linear_timeouts"
|
|
||||||
;;
|
|
||||||
"thin=1 fack=1")
|
|
||||||
sysctl -qw net.ipv4.tcp_thin_linear_timeouts=1
|
|
||||||
sysctl -qw net.ipv4.tcp_fack=1
|
|
||||||
echo " → 启用 thin_linear_timeouts + fack"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
sysctl -qw net.ipv4.tcp_thin_linear_timeouts=1
|
|
||||||
sysctl -qw net.ipv4.tcp_fack=1
|
|
||||||
sysctl -qw net.ipv4.tcp_retries2=5
|
|
||||||
sysctl -qw net.ipv4.tcp_autocorking=0
|
|
||||||
echo " → 全部启用"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 当前生效值:"
|
|
||||||
for k in tcp_thin_linear_timeouts tcp_fack tcp_retries2 tcp_autocorking; do
|
|
||||||
printf " %-30s = %s\n" "$k" "$(sysctl -n net.ipv4.$k 2>/dev/null)"
|
|
||||||
done
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 受控 A/B:隧道协议和连接数,哪个在"稳 + 快"上更好。
|
|
||||||
#
|
|
||||||
# 背景:
|
|
||||||
# 实测隧道大多稳(12/12),但每个请求稳定吃 ~1100ms 回源延迟。
|
|
||||||
# 而且日志里 i/o timeout 不少(最近 800 行 104 次),
|
|
||||||
# 说明有些 edge IP 连得很差(实测最慢那个 1331ms,最快的 146ms)。
|
|
||||||
#
|
|
||||||
# 假定:连接数多一些,撞上"慢 IP"的窗口就小一些。
|
|
||||||
# QUIC 在多丢包下恢复更好(没有跨流的队头阻塞),但之前测
|
|
||||||
# "http2 比 quic 稳" —— 那是连接建立阶段的观感,这次量真实请求。
|
|
||||||
#
|
|
||||||
# 组合:
|
|
||||||
# http2 + 4 (当前配置,基线)
|
|
||||||
# http2 + 6
|
|
||||||
# quic + 4
|
|
||||||
# quic + 6
|
|
||||||
#
|
|
||||||
# 每个组合:重启隧道 -> 等连接建齐 -> 打 20 次 -> 记录
|
|
||||||
# 成功率 / 中位耗时 / P90 / 连接数
|
|
||||||
set -u
|
|
||||||
|
|
||||||
URL=https://relay.xxmxxs.top/v1/health
|
|
||||||
UA='LanRemote/1.8.0'
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
BACKUP=/root/lanremote-tunnel.service.bak.$(date +%s)
|
|
||||||
cp -a "$SVC" "$BACKUP"
|
|
||||||
|
|
||||||
echo " 备份: $BACKUP"
|
|
||||||
echo " 原参数:"
|
|
||||||
grep -A3 'ExecStart' "$SVC" | grep -E 'protocol|ha-connections|edge-ip' | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
|
|
||||||
run_combo() {
|
|
||||||
local proto="$1" conns="$2" label="$3"
|
|
||||||
|
|
||||||
echo "════════ $label ════════"
|
|
||||||
# 改参数
|
|
||||||
if grep -q '\-\-protocol' "$SVC"; then
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
|
||||||
fi
|
|
||||||
if grep -q '\-\-ha-connections' "$SVC"; then
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
|
||||||
fi
|
|
||||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ' | sed 's/^/ 参数: /'
|
|
||||||
echo
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
|
|
||||||
# 等连接建齐
|
|
||||||
built=0
|
|
||||||
for i in $(seq 1 24); do
|
|
||||||
sleep 5
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
if [ "${n:-0}" -ge "$conns" ]; then built="$n"; break; fi
|
|
||||||
done
|
|
||||||
echo " 连接建齐: ${built:-0}/$conns(等了 ${i}0 秒)"
|
|
||||||
|
|
||||||
# 预热几次,跳过建连阶段的冷启动
|
|
||||||
for i in 1 2 3; do curl -s -o /dev/null --max-time 20 -H "User-Agent: $UA" "$URL"; sleep 1; done
|
|
||||||
|
|
||||||
# 正式测
|
|
||||||
ok=0; times=()
|
|
||||||
for i in $(seq 1 20); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30 -H "User-Agent: $UA" "$URL")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
times+=("$ms")
|
|
||||||
[ "$code" = "200" ] && ok=$((ok+1))
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
p90 = a[int(NR*0.9)+1];
|
|
||||||
printf "最小 %d 中位 %d P90 %d 最大 %d", a[1], a[int(NR/2)+1], p90, a[NR]}')
|
|
||||||
echo " == 结果: $ok/20 成功 $stats ms"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# 存下来给最后比较
|
|
||||||
echo "$label|$ok|$stats" >> /tmp/ab_result.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/ab_result.txt
|
|
||||||
|
|
||||||
run_combo http2 4 "A. http2 + 4(基线,当前配置)"
|
|
||||||
run_combo http2 6 "B. http2 + 6"
|
|
||||||
run_combo quic 4 "C. quic + 4"
|
|
||||||
run_combo quic 6 "D. quic + 6"
|
|
||||||
|
|
||||||
echo "════════ 汇总 ════════"
|
|
||||||
cat /tmp/ab_result.txt | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 回滚到基线(http2 + 4),等后面按结果再定 ════════"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
sleep 20
|
|
||||||
echo " 已回滚。当前参数:"
|
|
||||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /'
|
|
||||||
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 长时间对照:http2+4(基线) vs quic+6(候选)。
|
|
||||||
#
|
|
||||||
# 为什么要跑长的:
|
|
||||||
# 短测(20 次)看不出稳定性。而且历史上出过一次"QUIC 可用率 48%",
|
|
||||||
# 和这次短测的 20/20 矛盾 —— 必须用足够长的样本、并且**同时**记录
|
|
||||||
# 连接数波动和重连次数,才能判断到底是协议问题还是当时的网络问题。
|
|
||||||
#
|
|
||||||
# 每轮 5 分钟,每 3 秒一次请求(约 100 个样本),记录:
|
|
||||||
# 成功率 / 中位 / P90 / 最大 / 连接数最低值 / 连接的落点
|
|
||||||
set -u
|
|
||||||
|
|
||||||
URL=https://relay.xxmxxs.top/v1/health
|
|
||||||
UA='LanRemote/1.8.0'
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
BACKUP=/root/lanremote-tunnel.service.bak.long.$(date +%s)
|
|
||||||
cp -a "$SVC" "$BACKUP"
|
|
||||||
|
|
||||||
DURATION=300 # 每轮 5 分钟
|
|
||||||
INTERVAL=3 # 每 3 秒一次
|
|
||||||
|
|
||||||
set_params() {
|
|
||||||
local proto="$1" conns="$2"
|
|
||||||
if grep -q -- '--protocol' "$SVC"; then
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
|
||||||
fi
|
|
||||||
if grep -q -- '--ha-connections' "$SVC"; then
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
|
||||||
fi
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_ready() {
|
|
||||||
local want="$1"
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
sleep 5
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
if [ "${n:-0}" -ge "$want" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
measure() {
|
|
||||||
local label="$1"
|
|
||||||
local ok=0 fail=0 times=() minconn=99 reconnects=0
|
|
||||||
local start=$(date +%s)
|
|
||||||
local logmark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
echo " ── $label:跑 ${DURATION} 秒,每 ${INTERVAL} 秒一次 ──"
|
|
||||||
while [ $(( $(date +%s) - start )) -lt "$DURATION" ]; do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30 -H "User-Agent: $UA" "$URL")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
if [ "$code" = "200" ]; then
|
|
||||||
ok=$((ok+1)); times+=("$ms")
|
|
||||||
else
|
|
||||||
fail=$((fail+1))
|
|
||||||
fi
|
|
||||||
n=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
[ -n "${n:-}" ] && [ "$n" -lt "$minconn" ] && minconn="$n"
|
|
||||||
sleep "$INTERVAL"
|
|
||||||
done
|
|
||||||
|
|
||||||
# 这一轮新注册了多少次(= 重连次数)
|
|
||||||
nowmark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
reconnects=$(tail -n +$((logmark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \
|
|
||||||
| grep -c 'Registered tunnel connection' || echo 0)
|
|
||||||
|
|
||||||
total=$((ok+fail))
|
|
||||||
pct=0; [ "$total" -gt 0 ] && pct=$((ok * 100 / total))
|
|
||||||
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
if (NR==0) { print "无数据"; exit }
|
|
||||||
printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}')
|
|
||||||
|
|
||||||
echo " == 结果: $ok/$total = ${pct}% $stats ms"
|
|
||||||
echo " 连接数最低 $minconn 本轮重连 $reconnects 次"
|
|
||||||
echo "$label|$ok|$total|$pct|$stats|$minconn|$reconnects" >> /tmp/longab.txt
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/longab.txt
|
|
||||||
|
|
||||||
echo "════════ 第一轮:http2 + 4(基线)════════"
|
|
||||||
set_params http2 4
|
|
||||||
wait_ready 4 && echo " 连接就绪" || echo " ! 连接没建齐,继续测"
|
|
||||||
sleep 10
|
|
||||||
measure "http2+4"
|
|
||||||
|
|
||||||
echo "════════ 第二轮:quic + 6(候选)════════"
|
|
||||||
set_params quic 6
|
|
||||||
wait_ready 6 && echo " 连接就绪" || echo " ! 连接没建齐,继续测"
|
|
||||||
sleep 10
|
|
||||||
measure "quic+6"
|
|
||||||
|
|
||||||
echo "════════ 汇总 ════════"
|
|
||||||
printf " %-12s %-14s %-10s %-22s %-10s %s\n" "配置" "成功" "可用率" "延迟" "连接最低" "重连"
|
|
||||||
while IFS='|' read -r label ok total pct stats minconn reconnects; do
|
|
||||||
printf " %-12s %-14s %-10s %-22s %-10s %s\n" "$label" "$ok/$total" "${pct}%" "$stats" "$minconn" "$reconnects"
|
|
||||||
done < /tmp/longab.txt
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 按结果决定是否改配置 ════════"
|
|
||||||
BASE_PCT=$(grep '^http2+4' /tmp/longab.txt | cut -d'|' -f4)
|
|
||||||
QUIC_PCT=$(grep '^quic+6' /tmp/longab.txt | cut -d'|' -f4)
|
|
||||||
if [ "${QUIC_PCT:-0}" -ge "${BASE_PCT:-0}" ] && [ "${QUIC_PCT:-0}" -ge 90 ]; then
|
|
||||||
echo " → quic+6 可用率 ${QUIC_PCT}% >= 基线 ${BASE_PCT}%,保留 quic+6"
|
|
||||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo " → quic+6 可用率 ${QUIC_PCT}% 不如基线 ${BASE_PCT}%,回滚"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
sleep 20
|
|
||||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
echo " 当前连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
echo " 备份: $BACKUP"
|
|
||||||
-150
@@ -1,150 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 严谨对照 v2:http2+4 vs quic+6,用**真实的长轮询模式**测。
|
|
||||||
#
|
|
||||||
# v1 的问题:
|
|
||||||
# * 只跑 5 分钟、每 3 秒一个新连接 —— 那是"冷启动"模式,
|
|
||||||
# 而真实业务是**长轮询复用同一条连接**,两者差别很大。
|
|
||||||
# * 判定规则太粗(98% vs 100% 就回滚),
|
|
||||||
# 忽略了 P90 从 16.8 秒改善到 2.8 秒这个数量级的差异。
|
|
||||||
#
|
|
||||||
# v2 的做法:
|
|
||||||
# * 用 curl 一次给多个 URL —— 复用同一条连接,贴近长轮询
|
|
||||||
# * 每轮 10 分钟
|
|
||||||
# * 记录 成功率 / 中位 / P90 / P99 / 最大 / 失败时刻
|
|
||||||
# * 用真实的长轮询接口 /v1/agent/poll(挂 60 秒),
|
|
||||||
# 量"一次完整长轮询"的耗时分布 —— 这才是用户真正经历的
|
|
||||||
set -u
|
|
||||||
|
|
||||||
UA='LanRemote/1.8.0'
|
|
||||||
PUB=https://relay.xxmxxs.top
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
BACKUP=/root/lanremote-tunnel.service.bak.v2.$(date +%s)
|
|
||||||
cp -a "$SVC" "$BACKUP"
|
|
||||||
|
|
||||||
set_params() {
|
|
||||||
local proto="$1" conns="$2"
|
|
||||||
if grep -q -- '--protocol' "$SVC"; then
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
|
||||||
fi
|
|
||||||
if grep -q -- '--ha-connections' "$SVC"; then
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
|
||||||
else
|
|
||||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
|
||||||
fi
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_ready() {
|
|
||||||
local want="$1"
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
sleep 5
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
[ "${n:-0}" -ge "$want" ] && return 0
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- 用真实长轮询接口测 ----
|
|
||||||
# 注册一台探针设备,poll 会挂 60 秒再返回(没有数据时)
|
|
||||||
probe_device() {
|
|
||||||
DEV="probev2-$(date +%s)"
|
|
||||||
REG=$(curl -s --max-time 30 -X POST "$PUB/v1/agent/register" \
|
|
||||||
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
|
|
||||||
-d "{\"deviceId\":\"$DEV\",\"code\":\"777777\",\"name\":\"probev2\",\"version\":\"1.8.0\"}")
|
|
||||||
echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null
|
|
||||||
echo "$DEV" > /tmp/probe_dev.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
measure_poll() {
|
|
||||||
local label="$1" token="$2" rounds="${3:-8}"
|
|
||||||
local ok=0 fail=0 times=() fails=""
|
|
||||||
echo " ── $label:$rounds 次真实长轮询(每次挂 60 秒)──"
|
|
||||||
for i in $(seq 1 "$rounds"); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 100 \
|
|
||||||
-X POST "$PUB/v1/agent/poll" -H "X-Token: $token" -H "User-Agent: $UA")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
secs=$(echo "$out" | awk '{printf "%.0f", $2*1000}')
|
|
||||||
if [ "$code" = "200" ]; then
|
|
||||||
ok=$((ok+1)); times+=("$secs")
|
|
||||||
printf " %d: 200 %sms\n" "$i" "$secs"
|
|
||||||
else
|
|
||||||
fail=$((fail+1)); fails="$fails ${i}:${code}"
|
|
||||||
printf " %d: %s **失败**\n" "$i" "$code"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
if (NR==0) { print "无数据"; exit }
|
|
||||||
printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}')
|
|
||||||
echo " == $ok/$rounds 成功 $stats ms 失败:$fails"
|
|
||||||
echo "$label|$ok|$rounds|$stats|$fails" >> /tmp/v2.txt
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/v2.txt
|
|
||||||
|
|
||||||
echo "════════ 基线:http2 + 4 ════════"
|
|
||||||
set_params http2 4
|
|
||||||
wait_ready 4 && echo " 连接就绪" || echo " ! 没建齐"
|
|
||||||
sleep 15
|
|
||||||
TOKEN=$(probe_device)
|
|
||||||
echo " 探针 token ${#TOKEN} 字符"
|
|
||||||
measure_poll "http2+4" "$TOKEN" 8
|
|
||||||
|
|
||||||
echo "════════ 候选:quic + 6 ════════"
|
|
||||||
set_params quic 6
|
|
||||||
wait_ready 6 && echo " 连接就绪" || echo " ! 没建齐"
|
|
||||||
sleep 15
|
|
||||||
TOKEN=$(probe_device)
|
|
||||||
echo " 探针 token ${#TOKEN} 字符"
|
|
||||||
measure_poll "quic+6" "$TOKEN" 8
|
|
||||||
|
|
||||||
echo "════════ 汇总(每次长轮询的耗时,理想值 ≈ 61000ms)════════"
|
|
||||||
printf " %-10s %-10s %s\n" "配置" "成功" "耗时"
|
|
||||||
while IFS='|' read -r label ok total stats fails; do
|
|
||||||
printf " %-10s %-10s %s\n" "$label" "$ok/$total" "$stats"
|
|
||||||
done < /tmp/v2.txt
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 决策:主要看尾部延迟(用户体感就是这个)════════"
|
|
||||||
B=$(grep '^http2+4' /tmp/v2.txt | cut -d'|' -f4)
|
|
||||||
Q=$(grep '^quic+6' /tmp/v2.txt | cut -d'|' -f4)
|
|
||||||
echo " 基线 $B"
|
|
||||||
echo " 候选 $Q"
|
|
||||||
|
|
||||||
# 取 P90 数字比较
|
|
||||||
BP=$(echo "$B" | grep -oE 'P90 [0-9]+' | grep -oE '[0-9]+')
|
|
||||||
QP=$(echo "$Q" | grep -oE 'P90 [0-9]+' | grep -oE '[0-9]+')
|
|
||||||
echo
|
|
||||||
if [ -n "${BP:-}" ] && [ -n "${QP:-}" ]; then
|
|
||||||
if [ "$QP" -lt "$BP" ]; then
|
|
||||||
echo " → quic+6 的 P90(${QP}ms)优于基线(${BP}ms),采用 quic+6"
|
|
||||||
# 已经设成 quic 6 了,不用动
|
|
||||||
else
|
|
||||||
echo " → quic+6 的 P90(${QP}ms)不如基线(${BP}ms),回滚"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " → 数据不足,保守回滚"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 15
|
|
||||||
echo
|
|
||||||
echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')"
|
|
||||||
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
|
|
||||||
# 清探针
|
|
||||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
DEV=$(cat /tmp/probe_dev.txt 2>/dev/null)
|
|
||||||
[ -n "${DEV:-}" ] && curl -s --max-time 20 -X POST "http://127.0.0.1:8080/v1/admin/delete" \
|
|
||||||
-H "X-Admin: $KEY" -H 'Content-Type: application/json' \
|
|
||||||
-d "{\"deviceId\":\"$DEV\"}" > /dev/null 2>&1
|
|
||||||
echo " 备份: $BACKUP"
|
|
||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 隧道对照 v3:验证「QUIC 失败是因为 MTU」这个假设,并选出最稳的组合。
|
|
||||||
#
|
|
||||||
# 已查明的事实:
|
|
||||||
# 1. cloudflared 用 QUIC 时反复报
|
|
||||||
# "failed to dial to edge with quic: timeout: no recent network activity"
|
|
||||||
# 连十几个 IP 才成功一次 —— 握手阶段就掉包。
|
|
||||||
# 2. 我们给 TCP 做了 MSS 钳制(--set-mss 1400),
|
|
||||||
# 但 **QUIC 走 UDP,TCPMSS 规则完全不生效**。
|
|
||||||
# 3. 路径 MTU 实测 1472(1500 的包不通)。
|
|
||||||
# QUIC 默认用 ~1452 字节的包,很容易被中间设备丢掉,
|
|
||||||
# 而 UDP 丢了不会回 ICMP,客户端只能等到超时。
|
|
||||||
#
|
|
||||||
# 所以对照三组:
|
|
||||||
# A. quic + 6(现状,MTU 1500) —— 预期握手仍不稳
|
|
||||||
# B. quic + 6 + wlan0 MTU 1400 —— 如果假设成立,握手会变稳
|
|
||||||
# C. http2 + 8(TCP 吃 MSS 钳制,连接多) —— 对照组
|
|
||||||
#
|
|
||||||
# 每组记录:连接建齐耗时 / 建齐几条 / 60 次 HTTP 可用率
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
BACKUP=/root/tunnel.bak.v3.$(date +%s)
|
|
||||||
cp -a "$SVC" "$BACKUP"
|
|
||||||
N=60
|
|
||||||
|
|
||||||
set_params() {
|
|
||||||
local proto="$1" conns="$2"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
}
|
|
||||||
|
|
||||||
set_mtu() {
|
|
||||||
local mtu="$1"
|
|
||||||
ip link set wlan0 mtu "$mtu" 2>/dev/null && echo " wlan0 MTU -> $(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 等连接建齐,返回耗时秒数
|
|
||||||
wait_conns() {
|
|
||||||
local want="$1" limit="$2" t0 t1 n
|
|
||||||
t0=$(date +%s)
|
|
||||||
while true; do
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
n=${n:-0}
|
|
||||||
t1=$(date +%s)
|
|
||||||
if [ "$n" -ge "$want" ] || [ $((t1 - t0)) -ge "$limit" ]; then
|
|
||||||
echo "$n $((t1 - t0))"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
run_group() {
|
|
||||||
local label="$1" proto="$2" conns="$3" mtu="$4"
|
|
||||||
echo "════════ $label ════════"
|
|
||||||
|
|
||||||
[ "$mtu" != "keep" ] && set_mtu "$mtu"
|
|
||||||
|
|
||||||
local mark; mark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
set_params "$proto" "$conns"
|
|
||||||
|
|
||||||
local res; res=$(wait_conns "$conns" 150)
|
|
||||||
local n; n=$(echo "$res" | awk '{print $1}')
|
|
||||||
local secs; s=$(echo "$res" | awk '{print $2}')
|
|
||||||
echo " 建齐 $n/$conns 条,用了 ${s} 秒"
|
|
||||||
|
|
||||||
# 这一轮建立过程中失败了多少次
|
|
||||||
local dialfail; dialfail=$(tail -n +$((mark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \
|
|
||||||
| grep -c 'Failed to dial' || echo 0)
|
|
||||||
echo " 建立期间 dial 失败 $dialfail 次"
|
|
||||||
|
|
||||||
# 可用率
|
|
||||||
local ok=0 times=()
|
|
||||||
for i in $(seq 1 "$N"); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
|
||||||
-H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
c=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
if [ "$c" = "200" ]; then ok=$((ok+1)); times+=("$ms"); fi
|
|
||||||
done
|
|
||||||
local pct=$((ok * 100 / N))
|
|
||||||
local stats="无数据"
|
|
||||||
if [ "${#times[@]}" -gt 0 ]; then
|
|
||||||
stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
|
|
||||||
fi
|
|
||||||
printf " 可用率 %d%% (%d/%d) 耗时 %s ms\n" "$pct" "$ok" "$N" "$stats"
|
|
||||||
echo "$label|$proto|$conns|$mtu|$n|$s|$dialfail|$pct|$stats" >> /tmp/v3.txt
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/v3.txt
|
|
||||||
echo " 原始 wlan0 MTU: $(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
run_group "A. quic+6 MTU1500" quic 6 keep
|
|
||||||
run_group "B. quic+6 MTU1400" quic 6 1400
|
|
||||||
run_group "C. http2+8 MTU1400" http2 8 keep
|
|
||||||
|
|
||||||
# 恢复 MTU
|
|
||||||
set_mtu 1500
|
|
||||||
|
|
||||||
echo "════════ 汇总 ════════"
|
|
||||||
printf " %-22s %-6s %-4s %-8s %-9s %-8s %s\n" "组" "协议" "连接" "建齐/耗时" "dial失败" "可用率" "延迟"
|
|
||||||
while IFS='|' read -r label proto conns mtu n s dialfail pct stats; do
|
|
||||||
printf " %-22s %-6s %-4s %-8s %-9s %-8s %s\n" \
|
|
||||||
"$label" "$proto" "$conns" "${n}/${conns} ${s}s" "$dialfail" "${pct}%" "$stats"
|
|
||||||
done < /tmp/v3.txt
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 决策:可用率优先,其次看建齐速度 ════════"
|
|
||||||
BEST=$(sort -t'|' -k8 -rn /tmp/v3.txt | head -1)
|
|
||||||
BL=$(echo "$BEST" | cut -d'|' -f1)
|
|
||||||
BP=$(echo "$BEST" | cut -d'|' -f2)
|
|
||||||
BC=$(echo "$BEST" | cut -d'|' -f3)
|
|
||||||
BM=$(echo "$BEST" | cut -d'|' -f4)
|
|
||||||
echo " 最好: $BL(可用率 $(echo "$BEST" | cut -d'|' -f8)%)"
|
|
||||||
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol $BP/" "$SVC"
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $BC/" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
echo " 已采用: --protocol $BP --ha-connections $BC"
|
|
||||||
echo " MTU: $(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
echo " 备份: $BACKUP"
|
|
||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 1) 把 MTU 1400 固化下来(重启也在)
|
|
||||||
# 2) 在 MTU 1400 下对照 quic+6 / quic+8 / quic+10
|
|
||||||
#
|
|
||||||
# 为什么要固化 MTU:
|
|
||||||
# QUIC 走 UDP,**我们那条 --set-mss 1400 的 iptables 规则只管 TCP**。
|
|
||||||
# 实测把 wlan0 MTU 从 1500 降到 1400 之后:
|
|
||||||
# QUIC 建齐 6 条连接的时间 28 秒 -> 13 秒
|
|
||||||
# dial 失败次数 2 -> 1
|
|
||||||
# 原因:UDP 丢包不会回 ICMP,QUIC 发了超出路径 MTU 的包只能等超时。
|
|
||||||
# 把接口 MTU 压到 1400,QUIC 的包就都在安全范围内。
|
|
||||||
#
|
|
||||||
# 为什么还要试更多连接:
|
|
||||||
# 已经看到 http2+8 建齐只要 12 秒且 0 失败(QUIC+6 是 28 秒/2 次失败)。
|
|
||||||
# 连接多 = 单条掉线影响小。但 QUIC 的连接建立更贵,得实测平衡点。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.2'
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
NETTUNE=/etc/systemd/system/lanremote-nettune.service
|
|
||||||
BACKUP=/root/tunnel.bak.v4.$(date +%s)
|
|
||||||
cp -a "$SVC" "$BACKUP"
|
|
||||||
N=60
|
|
||||||
MTU=1400
|
|
||||||
MSS=1360
|
|
||||||
|
|
||||||
echo "════════ 1. 固化 MTU(写进 net-tune 服务)════════"
|
|
||||||
echo " 现在的 net-tune 服务:"
|
|
||||||
systemctl cat lanremote-nettune 2>/dev/null | grep -E 'ExecStart' | sed 's/^/ /'
|
|
||||||
|
|
||||||
# 把 MTU 设置追加进去(幂等:已有的先删掉)
|
|
||||||
cp -a "$NETTUNE" "${NETTUNE}.bak.$(date +%s)" 2>/dev/null || true
|
|
||||||
python3 - <<'PYEOF'
|
|
||||||
from pathlib import Path
|
|
||||||
p = Path("/etc/systemd/system/lanremote-nettune.service")
|
|
||||||
if not p.is_file():
|
|
||||||
print(" ! net-tune 服务不存在,跳过")
|
|
||||||
raise SystemExit(0)
|
|
||||||
text = p.read_text(encoding="utf-8")
|
|
||||||
# 去掉旧的 MTU/MSS 行
|
|
||||||
lines = [l for l in text.splitlines()
|
|
||||||
if "ip link set wlan0 mtu" not in l and "--set-mss" not in l]
|
|
||||||
out = []
|
|
||||||
for line in lines:
|
|
||||||
out.append(line)
|
|
||||||
# 在每个 ExecStart 之后补上 MTU 设置
|
|
||||||
if line.strip().startswith("ExecStart="):
|
|
||||||
out.append("ExecStart=/sbin/ip link set wlan0 mtu 1400")
|
|
||||||
out.append("ExecStart=/sbin/iptables -t mangle -C OUTPUT -p tcp "
|
|
||||||
"--tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 || "
|
|
||||||
"/sbin/iptables -t mangle -A OUTPUT -p tcp "
|
|
||||||
"--tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360")
|
|
||||||
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
||||||
print(" 已写入 MTU 1400 + MSS 1360")
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
echo " 立即执行一次:"
|
|
||||||
ip link set wlan0 mtu $MTU && echo " wlan0 MTU -> $(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
iptables -t mangle -C OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss $MSS 2>/dev/null \
|
|
||||||
|| iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss $MSS
|
|
||||||
echo " MSS 规则: $(iptables -t mangle -S OUTPUT | grep TCPMSS | head -1)"
|
|
||||||
echo " net-tune 现在的内容:"
|
|
||||||
systemctl cat lanremote-nettune 2>/dev/null | grep ExecStart | sed 's/^/ /'
|
|
||||||
echo " 服务状态: $(systemctl is-enabled lanremote-nettune 2>/dev/null) / $(systemctl is-active lanremote-nettune 2>/dev/null)"
|
|
||||||
|
|
||||||
wait_conns() {
|
|
||||||
local want="$1" limit="$2" t0 t1 n
|
|
||||||
t0=$(date +%s)
|
|
||||||
while true; do
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
n=${n:-0}
|
|
||||||
t1=$(date +%s)
|
|
||||||
[ "$n" -ge "$want" ] || [ $((t1 - t0)) -ge "$limit" ] && { echo "$n $((t1 - t0))"; return; }
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
run_group() {
|
|
||||||
local conns="$1"
|
|
||||||
echo "════════ quic + $conns (MTU $MTU)════════"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC"
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
local mark; mark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
local res; res=$(wait_conns "$conns" 150)
|
|
||||||
local n s
|
|
||||||
n=$(echo "$res" | awk '{print $1}'); s=$(echo "$res" | awk '{print $2}')
|
|
||||||
local df; df=$(tail -n +$((mark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Failed to dial' || echo 0)
|
|
||||||
echo " 建齐 $n/$conns,用 ${s} 秒,dial 失败 ${df} 次"
|
|
||||||
|
|
||||||
local ok=0 times=()
|
|
||||||
for i in $(seq 1 "$N"); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
[ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); }
|
|
||||||
done
|
|
||||||
local pct=$((ok * 100 / N))
|
|
||||||
local stats="无数据"
|
|
||||||
[ "${#times[@]}" -gt 0 ] && stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
|
||||||
printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
|
|
||||||
printf " 可用率 %d%% (%d/%d) %s ms\n" "$pct" "$ok" "$N" "$stats"
|
|
||||||
echo "quic+$conns|$n|$s|$df|$pct|$stats" >> /tmp/v4.txt
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
rm -f /tmp/v4.txt
|
|
||||||
run_group 6
|
|
||||||
run_group 8
|
|
||||||
run_group 10
|
|
||||||
|
|
||||||
echo "════════ 汇总 ════════"
|
|
||||||
printf " %-10s %-14s %-10s %-8s %s\n" "配置" "建齐/耗时" "dial失败" "可用率" "延迟"
|
|
||||||
while IFS='|' read -r label n s df pct stats; do
|
|
||||||
printf " %-10s %-14s %-10s %-8s %s\n" "$label" "${n}条 ${s}s" "$df" "${pct}%" "$stats"
|
|
||||||
done < /tmp/v4.txt
|
|
||||||
|
|
||||||
echo
|
|
||||||
BESTN=$(sort -t'|' -k5 -rn /tmp/v4.txt | head -1 | sed 's/quic+//' | cut -d'|' -f1)
|
|
||||||
echo " → 采用 quic + $BESTN"
|
|
||||||
cp -a "$BACKUP" "$SVC"
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC"
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $BESTN/" "$SVC"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
sleep 5
|
|
||||||
echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')"
|
|
||||||
echo " MTU: $(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
echo " 备份: $BACKUP"
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 1.8.0 发布验收 —— 从公网走一遍用户会走的路
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.0'
|
|
||||||
|
|
||||||
echo "════════ 1. 中转版本 ════════"
|
|
||||||
curl -s --max-time 15 "$BASE/v1/health" | head -c 200
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 官网 ════════"
|
|
||||||
code=$(curl -s -o /tmp/site.html -w '%{http_code}' --max-time 20 -H "User-Agent: $UA" "$BASE/")
|
|
||||||
echo " HTTP $code $(wc -c < /tmp/site.html) 字节"
|
|
||||||
echo " 版本号显示: $(grep -o '1\.8\.0' /tmp/site.html | head -1 || echo '(页面上没直接标版本,正常)')"
|
|
||||||
echo " 下载按钮指向:"
|
|
||||||
grep -o 'href="/download/[^"]*"' /tmp/site.html | sort -u | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 三个成品能不能下(只看前 1KB 和响应头)════════"
|
|
||||||
for f in ykxmz-setup-1.8.0.exe ykxmz-server-1.8.0.zip ykxmz-watch-1.8.0.apk; do
|
|
||||||
hdr=$(curl -sI --max-time 30 -H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
code=$(echo "$hdr" | head -1 | awk '{print $2}')
|
|
||||||
length=$(echo "$hdr" | grep -i '^content-length' | tr -d '\r' | awk '{print $2}')
|
|
||||||
kind=$(echo "$hdr" | grep -i '^content-type' | tr -d '\r' | cut -d' ' -f2)
|
|
||||||
# 真下 1KB 验证能读
|
|
||||||
got=$(curl -s --max-time 40 -r 0-1023 -o /tmp/chunk.bin -w '%{http_code}' \
|
|
||||||
-H "User-Agent: $UA" "$BASE/download/$f" 2>/dev/null)
|
|
||||||
real=$(wc -c < /tmp/chunk.bin 2>/dev/null || echo 0)
|
|
||||||
printf " %-32s HTTP %-4s 共 %-10s %s 取到 %s 字节\n" \
|
|
||||||
"$f" "$code" "${length:-?}" "${kind:-?}" "$real"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 管理后台 ════════"
|
|
||||||
PASS=$(cat /root/lanremote-relay/data/admin_password.txt 2>/dev/null)
|
|
||||||
curl -s -c /tmp/ck.txt -o /dev/null --max-time 20 \
|
|
||||||
-d "username=xmz7891&password=$PASS" "$BASE/admin/login"
|
|
||||||
for p in "/zhongzhuanzhan" "/admin"; do
|
|
||||||
printf " %-20s HTTP %s\n" "$p" \
|
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' -b /tmp/ck.txt --max-time 20 -H "User-Agent: $UA" "$BASE$p")"
|
|
||||||
done
|
|
||||||
curl -s -b /tmp/ck.txt --max-time 20 "$BASE/zhongzhuanzhan" -o /tmp/adm.html
|
|
||||||
echo " 管理页 $(wc -c < /tmp/adm.html) 字节"
|
|
||||||
for k in 'class="top"' 'class="hero"' 'class="nums"' 'class="card"' 'ykxmz 中转站'; do
|
|
||||||
if grep -q "$k" /tmp/adm.html; then echo " ✓ $k"; else echo " ✗ $k"; fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 其它页面 ════════"
|
|
||||||
for p in "/shot/pc-main.png" "/favicon.ico"; do
|
|
||||||
printf " %-22s HTTP %s\n" "$p" \
|
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$BASE$p")"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 公网可用率(12 次)════════"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 12); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
[ "$c" = "200" ] && ok=$((ok + 1))
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/12"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 7. 中转日志(看有没有报错)════════"
|
|
||||||
journalctl -u lanremote-relay --since '3 minutes ago' --no-pager 2>/dev/null \
|
|
||||||
| grep -icE 'traceback|nameerror|typeerror' | sed 's/^/ 错误行数: /'
|
|
||||||
tail -4 /root/lanremote-relay/relay.log | sed 's/^/ /'
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 1.8.0 发布验收(稳定版)
|
|
||||||
#
|
|
||||||
# 上一版脚本两个毛病:
|
|
||||||
# 1. 紧跟在中转重启之后跑,撞上隧道重连,一路 530
|
|
||||||
# 2. `curl -o /tmp/adm.html` 在 for 循环之后没生成文件
|
|
||||||
# (多半是前面某条 curl 把变量吃掉了)
|
|
||||||
# 这里都改掉:先等稳定,登录 cookie 单步来。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.0'
|
|
||||||
|
|
||||||
echo "════════ 0. 先等隧道稳定 ════════"
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \
|
|
||||||
-H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
printf " 第 %d 次: %s\n" "$i" "$c"
|
|
||||||
[ "$c" = "200" ] && break
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 1. 中转版本 ════════"
|
|
||||||
curl -s --max-time 15 "$BASE/v1/health" | head -c 180
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 官网 ════════"
|
|
||||||
curl -s --max-time 20 -H "User-Agent: $UA" "$BASE/" -o /tmp/site.html
|
|
||||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -H "User-Agent: $UA" "$BASE/") $(wc -c < /tmp/site.html) 字节"
|
|
||||||
grep -o 'href="/download/[^"]*"' /tmp/site.html | sort -u | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 三个成品(响应头 + 真下 4KB)════════"
|
|
||||||
for f in ykxmz-setup-1.8.0.exe ykxmz-server-1.8.0.zip ykxmz-watch-1.8.0.apk; do
|
|
||||||
rm -f /tmp/chunk.bin
|
|
||||||
hdr=$(curl -sI --max-time 30 -H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
hcode=$(echo "$hdr" | head -1 | awk '{print $2}')
|
|
||||||
hlen=$(echo "$hdr" | grep -i '^content-length' | tr -d '\r' | awk '{print $2}')
|
|
||||||
htype=$(echo "$hdr" | grep -i '^content-type' | tr -d '\r' | cut -d' ' -f2)
|
|
||||||
curl -s --max-time 60 -r 0-4095 -o /tmp/chunk.bin -H "User-Agent: $UA" \
|
|
||||||
"$BASE/download/$f" >/dev/null 2>&1
|
|
||||||
real=$(wc -c < /tmp/chunk.bin 2>/dev/null || echo 0)
|
|
||||||
printf " %-30s HEAD %-4s 长度 %-10s %-34s 实体取到 %s 字节\n" \
|
|
||||||
"$f" "$hcode" "${hlen:-?}" "${htype:-?}" "$real"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 管理后台 ════════"
|
|
||||||
PASS=$(cat /root/lanremote-relay/data/admin_password.txt 2>/dev/null)
|
|
||||||
rm -f /tmp/ck.txt
|
|
||||||
curl -s -c /tmp/ck.txt -o /dev/null --max-time 20 \
|
|
||||||
-d "username=xmz7891&password=$PASS" "$BASE/admin/login"
|
|
||||||
echo " cookie: $(grep -c lr_admin /tmp/ck.txt 2>/dev/null || echo 0) 条"
|
|
||||||
for p in "/zhongzhuanzhan" "/admin"; do
|
|
||||||
printf " %-20s HTTP %s\n" "$p" \
|
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' -b /tmp/ck.txt \
|
|
||||||
--max-time 20 -H "User-Agent: $UA" "$BASE$p")"
|
|
||||||
done
|
|
||||||
curl -s -b /tmp/ck.txt --max-time 20 -H "User-Agent: $UA" \
|
|
||||||
"$BASE/zhongzhuanzhan" -o /tmp/adm.html
|
|
||||||
if [ -f /tmp/adm.html ]; then
|
|
||||||
echo " 管理页 $(wc -c < /tmp/adm.html) 字节"
|
|
||||||
for k in 'class="top"' 'class="hero"' 'class="nums"' 'class="card"' \
|
|
||||||
'ykxmz 中转站' 'id="pickAll"' 'id="netBtn"'; do
|
|
||||||
if grep -q "$k" /tmp/adm.html; then
|
|
||||||
echo " ✓ $k"
|
|
||||||
else
|
|
||||||
echo " ✗ $k"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo " [失败] 抓不到管理页"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 静态资源 ════════"
|
|
||||||
for p in "/shot/pc-main.png" "/favicon.ico"; do
|
|
||||||
printf " %-22s HTTP %s\n" "$p" \
|
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$BASE$p")"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 公网可用率(15 次,间隔 2 秒)════════"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 15); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 \
|
|
||||||
-H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
[ "$c" = "200" ] && ok=$((ok + 1))
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/15"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 7. 中转有没有报错 ════════"
|
|
||||||
journalctl -u lanremote-relay --since '5 minutes ago' --no-pager 2>/dev/null \
|
|
||||||
| grep -icE 'traceback|nameerror|typeerror|attributeerror' \
|
|
||||||
| sed 's/^/ 错误行数: /'
|
|
||||||
tail -3 /root/lanremote-relay/relay.log | sed 's/^/ /'
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 1.8.1 最终验收:从公网走一遍用户会走的全部路径
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
|
|
||||||
echo "════════ 1. 中转版本和隧道 ════════"
|
|
||||||
curl -s --max-time 15 "$BASE/v1/health" | head -c 130
|
|
||||||
echo
|
|
||||||
echo " 隧道连接: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') 条"
|
|
||||||
echo " 协议参数: $(grep -c 'protocol quic' /etc/systemd/system/lanremote-tunnel.service) (1=quic)"
|
|
||||||
echo " 长轮询: $(tail -30 /root/lanremote-relay/relay.log | grep '长轮询' | tail -1 | grep -oE '[0-9]+ 秒')"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 官网 ════════"
|
|
||||||
curl -s --max-time 25 -H "User-Agent: $UA" "$BASE/" -o /tmp/site.html
|
|
||||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 25 -H "User-Agent: $UA" "$BASE/") $(wc -c < /tmp/site.html) 字节"
|
|
||||||
echo " 下载按钮:"
|
|
||||||
grep -o 'href="/download/[^"]*"' /tmp/site.html | sort -u | sed 's/^/ /'
|
|
||||||
echo " 管理页字眼: $(grep -cE 'zhongzhuanzhan|管理后台|管理页' /tmp/site.html || echo 0) 处(应为 0)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 三个成品能下吗 ════════"
|
|
||||||
for f in ykxmz-setup-1.8.1.exe ykxmz-server-1.8.1.zip ykxmz-watch-1.8.1.apk; do
|
|
||||||
hdr=$(curl -sI --max-time 40 -H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
code=$(echo "$hdr" | head -1 | awk '{print $2}')
|
|
||||||
len=$(echo "$hdr" | grep -i '^content-length' | tr -d '\r' | awk '{print $2}')
|
|
||||||
printf " %-30s HTTP %-4s %s 字节\n" "$f" "$code" "${len:-?}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 长轮询还能跑通吗(2 次真实验证)════════"
|
|
||||||
STAMP=$(date +%s)
|
|
||||||
DEV="final-${STAMP}"
|
|
||||||
CODE=$(printf "%06d" $(( STAMP % 1000000 )))
|
|
||||||
REG=$(curl -s --max-time 30 -X POST "$BASE/v1/agent/register" \
|
|
||||||
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
|
|
||||||
-d "{\"deviceId\":\"$DEV\",\"code\":\"$CODE\",\"name\":\"final\",\"version\":\"1.8.1\"}")
|
|
||||||
TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
|
||||||
echo " 探针注册: $([ -n "$TOKEN" ] && echo 成功 || echo 失败)"
|
|
||||||
for i in 1 2; do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 130 \
|
|
||||||
-X POST "$BASE/v1/agent/poll" -H "X-Token: $TOKEN" -H "User-Agent: $UA")
|
|
||||||
printf " 第 %d 次: 码 %s 耗时 %.1fs(应约 61 秒)\n" \
|
|
||||||
"$i" "$(echo "$out" | awk '{print $1}')" "$(echo "$out" | awk '{print $2}')"
|
|
||||||
done
|
|
||||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
curl -s --max-time 20 -X POST "http://127.0.0.1:8080/v1/admin/delete" \
|
|
||||||
-H "X-Admin: $KEY" -H 'Content-Type: application/json' \
|
|
||||||
-d "{\"deviceId\":\"$DEV\"}" > /dev/null 2>&1
|
|
||||||
echo " 探针已清理"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 公网可用率(10 次)════════"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
[ "$c" = "200" ] && ok=$((ok+1))
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/10"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 中转有没有报错 ════════"
|
|
||||||
journalctl -u lanremote-relay --since '5 minutes ago' --no-pager 2>/dev/null \
|
|
||||||
| grep -icE 'traceback|nameerror|typeerror' | sed 's/^/ 错误行数: /'
|
|
||||||
tail -3 /root/lanremote-relay/relay.log | sed 's/^/ /'
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 1.8.2 最终验收(在设备上跑)
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.2'
|
|
||||||
|
|
||||||
echo "════════ 1. 官网和三个成品(走公网,看缓存)════════"
|
|
||||||
for p in / /download/ykxmz-setup-1.8.2.exe /download/ykxmz-server-1.8.2.zip /download/ykxmz-watch-1.8.2.apk; do
|
|
||||||
hdr=$(curl -s -D - -o /dev/null --max-time 45 -H "User-Agent: $UA" "$BASE$p" 2>/dev/null | tr -d '\r')
|
|
||||||
code=$(echo "$hdr" | head -1 | awk '{print $2}')
|
|
||||||
len=$(echo "$hdr" | grep -i '^content-length' | awk '{print $2}')
|
|
||||||
cf=$(echo "$hdr" | grep -i '^cf-cache-status' | awk '{print $2}')
|
|
||||||
cc=$(echo "$hdr" | grep -i '^cache-control' | cut -d' ' -f2-)
|
|
||||||
printf " %-38s HTTP %-4s %10s cf=%-6s %s\n" "$p" "$code" "$len" "${cf:-无}" "${cc:-无}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 对外清洁度(官网不能有管理页字眼)════════"
|
|
||||||
curl -s --max-time 30 -H "User-Agent: $UA" "$BASE/" -o /tmp/site.html
|
|
||||||
n=$(grep -cE 'zhongzhuanzhan|管理后台|管理页|管理入口|管理面板' /tmp/site.html || true)
|
|
||||||
echo " 官网里管理页字眼: $n 处(应为 0)"
|
|
||||||
echo " 官网大小: $(wc -c < /tmp/site.html) 字节"
|
|
||||||
echo " 下载按钮:"
|
|
||||||
grep -o 'href="/download/[^"]*"' /tmp/site.html | sort -u | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 管理页两个新功能 ════════"
|
|
||||||
if [ -f /root/taf.py ]; then
|
|
||||||
python3 /root/taf.py 2>&1 | tail -16 | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo " (测试脚本不在设备上)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 中转核心功能(长轮询 + 注册)════════"
|
|
||||||
STAMP=$(date +%s)
|
|
||||||
DEV="acc-$STAMP"
|
|
||||||
CODE=$(printf "%06d" $(( STAMP % 1000000 )))
|
|
||||||
REG=$(curl -s --max-time 30 -X POST "$BASE/v1/agent/register" \
|
|
||||||
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
|
|
||||||
-d "{\"deviceId\":\"$DEV\",\"code\":\"$CODE\",\"name\":\"验收\",\"version\":\"1.8.2\"}")
|
|
||||||
TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
|
||||||
echo " 注册: $([ -n "$TOKEN" ] && echo 成功 || echo 失败)"
|
|
||||||
if [ -n "$TOKEN" ]; then
|
|
||||||
for i in 1 2; do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 200 \
|
|
||||||
-X POST "$BASE/v1/agent/poll" -H "X-Token: $TOKEN" -H "User-Agent: $UA")
|
|
||||||
printf " 长轮询 %d: 码 %s 耗时 %.1fs(应约 61 秒)\n" \
|
|
||||||
"$i" "$(echo "$out" | awk '{print $1}')" "$(echo "$out" | awk '{print $2}')"
|
|
||||||
done
|
|
||||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
curl -s --max-time 20 -X POST "http://127.0.0.1:8080/v1/admin/delete" \
|
|
||||||
-H "X-Admin: $KEY" -H 'Content-Type: application/json' \
|
|
||||||
-d "{\"deviceId\":\"$DEV\"}" > /dev/null 2>&1
|
|
||||||
echo " 探针已清理"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 公网可用率(20 次)════════"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 20); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
[ "$c" = "200" ] && ok=$((ok + 1))
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/20 = $((ok * 5))%"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 隧道和错误 ════════"
|
|
||||||
for s in lanremote-relay lanremote-tunnel lanremote-tunnel-h2; do
|
|
||||||
printf " %-24s %s\n" "$s" "$(systemctl is-active $s)"
|
|
||||||
done
|
|
||||||
echo " 中转错误行数(近 10 分钟): $(journalctl -u lanremote-relay --since '10 minutes ago' --no-pager 2>/dev/null | grep -icE 'traceback|nameerror|typeerror|AttributeError' || echo 0)"
|
|
||||||
echo " 连接数: 20241=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') 20242=$(curl -s --max-time 4 http://127.0.0.1:20242/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''中转管理页新增两个功能(中转端逻辑)。
|
|
||||||
|
|
||||||
== 功能一:允许/禁止新设备接入 ==
|
|
||||||
|
|
||||||
一个开关,关掉之后**只有已经登记过的设备能继续上线**,
|
|
||||||
没见过的新设备一律拒绝。
|
|
||||||
|
|
||||||
用途:不想让陌生人拿配对码乱试、或者临时把中转"关起来"。
|
|
||||||
|
|
||||||
实现要点:
|
|
||||||
* 标志位存在 relay_state.json 里,重启不丢
|
|
||||||
* 判断"新设备"以 deviceId 为准(它装机时生成、重装也不变),
|
|
||||||
不是看配对码 —— 老设备换码也必须能进来
|
|
||||||
* 被拒绝时回一个明确的标记 blocked=True,
|
|
||||||
让客户端能区分"被管理员挡了"和"配对码冲突",
|
|
||||||
不要用中文错误文案去匹配(文案一改就坏)
|
|
||||||
|
|
||||||
== 功能二:修改任意设备的配对码 ==
|
|
||||||
|
|
||||||
管理页上每台设备一个"改配对码"按钮,填新码就生效。
|
|
||||||
|
|
||||||
实现要点:
|
|
||||||
* 新码必须是 6 位数字
|
|
||||||
* 新码不能已经被别的设备占用
|
|
||||||
* 改的时候要同时更新 codes 映射和 device.code,
|
|
||||||
并清掉该设备**旧码**的映射 —— 否则旧码会变成孤儿条目,
|
|
||||||
仪表盘上"配对码占用"会虚高
|
|
||||||
* 改完立刻 _save(),重启后仍然是新码
|
|
||||||
* 该设备当前的手表会话要踢掉(会话是绑在旧码上的),
|
|
||||||
否则手表还拿着旧会话以为连着,实际电脑已经换了码
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay\relay.py")
|
|
||||||
|
|
||||||
|
|
||||||
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
|
||||||
if old not in text:
|
|
||||||
print(" ✗ 没找到:%s" % label)
|
|
||||||
return text, False
|
|
||||||
if text.count(old) != 1:
|
|
||||||
print(" ! 出现 %d 次:%s" % (text.count(old), label))
|
|
||||||
return text, False
|
|
||||||
print(" ✓ %s" % label)
|
|
||||||
return text.replace(old, new, 1), True
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = SRC.read_text(encoding="utf-8")
|
|
||||||
before = text.count("\n") + 1
|
|
||||||
ok_all = True
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- 1. 字段
|
|
||||||
old = """ self.retired: dict = {}
|
|
||||||
self.started = time.time()"""
|
|
||||||
new = """ self.retired: dict = {}
|
|
||||||
# 是否允许新设备接入。False 时只有已登记过的 deviceId 能上线。
|
|
||||||
self.allow_new = True
|
|
||||||
self.started = time.time()"""
|
|
||||||
text, ok = patch(text, old, new, "加 allow_new 字段")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- 2. _load
|
|
||||||
old = """ for item in raw.get("codes", []):
|
|
||||||
device_id = str(item.get("deviceId", ""))
|
|
||||||
code = str(item.get("code", ""))
|
|
||||||
if device_id and CODE_RE.match(code):
|
|
||||||
self.codes[code] = device_id
|
|
||||||
log(f"恢复配对码绑定:设备 {device_id[:12]}… -> {code}")"""
|
|
||||||
new = """ # 是否允许新设备接入(重启不丢)
|
|
||||||
self.allow_new = bool(raw.get("allowNew", True))
|
|
||||||
for item in raw.get("codes", []):
|
|
||||||
device_id = str(item.get("deviceId", ""))
|
|
||||||
code = str(item.get("code", ""))
|
|
||||||
if device_id and CODE_RE.match(code):
|
|
||||||
self.codes[code] = device_id
|
|
||||||
log(f"恢复配对码绑定:设备 {device_id[:12]}… -> {code}")"""
|
|
||||||
text, ok = patch(text, old, new, "_load 恢复 allow_new")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- 3. _save
|
|
||||||
old = """ payload = {"codes": [
|
|
||||||
{"deviceId": did, "code": code}
|
|
||||||
for code, did in sorted(self.codes.items())
|
|
||||||
]}"""
|
|
||||||
new = """ payload = {
|
|
||||||
"allowNew": bool(self.allow_new),
|
|
||||||
"codes": [
|
|
||||||
{"deviceId": did, "code": code}
|
|
||||||
for code, did in sorted(self.codes.items())
|
|
||||||
],
|
|
||||||
}"""
|
|
||||||
text, ok = patch(text, old, new, "_save 持久化 allow_new")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- 4. 拒绝新设备
|
|
||||||
old = """ with self.lock:
|
|
||||||
known = self.codes.get(code)
|
|
||||||
if known and known != device_id:"""
|
|
||||||
new = """ with self.lock:
|
|
||||||
# 管理员关掉了"允许新设备接入"时,
|
|
||||||
# 只放行**已经登记过**的设备。
|
|
||||||
#
|
|
||||||
# 注意判断依据是 deviceId(装机时生成、重装也不变),
|
|
||||||
# **不是配对码** —— 否则管理员给老设备改了码,
|
|
||||||
# 那台设备就被自己的开关挡在外面了。
|
|
||||||
known_device = device_id in self.devices
|
|
||||||
if not known_device and not self.allow_new:
|
|
||||||
log(f"新设备接入已被关闭,拒绝 {name} ({device_id[:12]}…)")
|
|
||||||
return {"ok": False,
|
|
||||||
"blocked": True,
|
|
||||||
"error": "中转站当前不接受新设备接入"}
|
|
||||||
|
|
||||||
known = self.codes.get(code)
|
|
||||||
if known and known != device_id:"""
|
|
||||||
text, ok = patch(text, old, new, "register 拒绝新设备")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- 5. 管理方法
|
|
||||||
old = """ def device_by_token(self, token: str) -> Device | None:"""
|
|
||||||
new = """ # ---------------- 管理操作 ----------------
|
|
||||||
|
|
||||||
def set_allow_new(self, allow: bool) -> dict:
|
|
||||||
\"\"\"开关"允许新设备接入"。\"\"\"
|
|
||||||
with self.lock:
|
|
||||||
self.allow_new = bool(allow)
|
|
||||||
self._save()
|
|
||||||
log(f"管理员把「允许新设备接入」设为 {self.allow_new}")
|
|
||||||
return {"ok": True, "allowNew": self.allow_new}
|
|
||||||
|
|
||||||
def set_device_code(self, device_id: str, new_code: str) -> dict:
|
|
||||||
\"\"\"给一台设备换配对码。
|
|
||||||
|
|
||||||
要点:
|
|
||||||
* 新码必须是 6 位数字
|
|
||||||
* 不能已经被**别的**设备占用
|
|
||||||
* 旧码的映射必须删掉,否则会变成孤儿条目,
|
|
||||||
管理页上"配对码占用"的数字会虚高
|
|
||||||
* 该设备挂着的会话要踢掉 —— 会话是绑在码上的,
|
|
||||||
不踢的话手表还拿着旧会话以为连着
|
|
||||||
\"\"\"
|
|
||||||
device_id = str(device_id or "").strip()
|
|
||||||
new_code = str(new_code or "").strip()
|
|
||||||
if not CODE_RE.match(new_code):
|
|
||||||
return {"ok": False, "error": "配对码必须是 6 位数字"}
|
|
||||||
with self.lock:
|
|
||||||
device = self.devices.get(device_id)
|
|
||||||
if device is None:
|
|
||||||
return {"ok": False, "error": "没有这台设备(可能已下线)"}
|
|
||||||
|
|
||||||
owner = self.codes.get(new_code)
|
|
||||||
if owner and owner != device_id:
|
|
||||||
return {"ok": False,
|
|
||||||
"error": "这个配对码已经被别的设备占用了"}
|
|
||||||
|
|
||||||
old_code = device.code
|
|
||||||
if old_code == new_code:
|
|
||||||
return {"ok": True, "code": new_code, "unchanged": True}
|
|
||||||
|
|
||||||
# 删掉旧码映射(连同可能存在的别名)
|
|
||||||
for code, owner_id in list(self.codes.items()):
|
|
||||||
if owner_id == device_id:
|
|
||||||
self.codes.pop(code, None)
|
|
||||||
self.codes[new_code] = device_id
|
|
||||||
device.code = new_code
|
|
||||||
|
|
||||||
# 会话绑在码上,旧会话作废,让手表重新配对
|
|
||||||
kicked = 0
|
|
||||||
for sid in [s.session_id for s in self.sessions.values()
|
|
||||||
if s.device_id == device_id]:
|
|
||||||
self.sessions.pop(sid, None)
|
|
||||||
kicked += 1
|
|
||||||
|
|
||||||
self._save()
|
|
||||||
log(f"管理员把设备 {device.name} 的配对码 "
|
|
||||||
f"{old_code} 改成 {new_code}(踢掉 {kicked} 个会话)")
|
|
||||||
return {"ok": True, "code": new_code,
|
|
||||||
"oldCode": old_code, "kicked": kicked}
|
|
||||||
|
|
||||||
def device_by_token(self, token: str) -> Device | None:"""
|
|
||||||
text, ok = patch(text, old, new, "加 set_allow_new / set_device_code")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
if not ok_all:
|
|
||||||
print()
|
|
||||||
print(" 有锚点没匹配,没有写回")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" 已写回:%d -> %d 行" % (before, text.count("\n") + 1))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''管理页 UI:加「允许新设备接入」开关 + 每台设备的「改码」按钮。
|
|
||||||
|
|
||||||
设计上遵守用户的"所有东西都要白色简洁":
|
|
||||||
* 开关用系统原生 checkbox + 极简样式,不引入花哨组件
|
|
||||||
* 改码走 prompt(),不加弹窗层 —— 管理页是自用的,
|
|
||||||
少一层 DOM 就少一处会坏的地方
|
|
||||||
* 按钮沿用现有的 .ghost 样式(不抢 .danger 的视觉重量)
|
|
||||||
|
|
||||||
⚠ 一个容易踩的坑:
|
|
||||||
现有 JS 用 `button.danger[data-device]` 绑定"单台删除"。
|
|
||||||
新加的"改码"按钮**故意不带 data-device 而用 data-recode**,
|
|
||||||
否则会被删除逻辑一起选中,点改码变成删设备。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay\admin.py")
|
|
||||||
|
|
||||||
|
|
||||||
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
|
||||||
if old not in text:
|
|
||||||
print(" ✗ 没找到:%s" % label)
|
|
||||||
return text, False
|
|
||||||
if text.count(old) != 1:
|
|
||||||
print(" ! 出现 %d 次:%s" % (text.count(old), label))
|
|
||||||
return text, False
|
|
||||||
print(" ✓ %s" % label)
|
|
||||||
return text.replace(old, new, 1), True
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = SRC.read_text(encoding="utf-8")
|
|
||||||
ok_all = True
|
|
||||||
|
|
||||||
# ------------------------------------------------ 1. 行里加"改码"按钮
|
|
||||||
old = """ '<td class="ops"><button class="danger sm" '
|
|
||||||
'data-device="%s" data-name="%s" data-code="%s">删除</button></td>'
|
|
||||||
'</tr>' % (
|
|
||||||
did, name,
|
|
||||||
"on" if online else "off", name, did,
|
|
||||||
code,
|
|
||||||
"on" if online else "off", "在线" if online else "离线",
|
|
||||||
device["idleSeconds"],
|
|
||||||
device["queuedToClient"], device["queuedToAgent"],
|
|
||||||
_kb(device["bytesIn"]), _kb(device["bytesOut"]),
|
|
||||||
did, name, code)
|
|
||||||
)"""
|
|
||||||
new = """ '<td class="ops">'
|
|
||||||
# ⚠ 这个按钮用 data-recode 而不是 data-device ——
|
|
||||||
# 现有"单台删除"是按 button.danger[data-device] 绑的,
|
|
||||||
# 这里要是也带 data-device 就会被当成删除按钮。
|
|
||||||
'<button class="ghost sm" data-recode="%s" '
|
|
||||||
'data-name="%s" data-code="%s">改码</button>'
|
|
||||||
'<button class="danger sm" '
|
|
||||||
'data-device="%s" data-name="%s" data-code="%s">删除</button></td>'
|
|
||||||
'</tr>' % (
|
|
||||||
did, name,
|
|
||||||
"on" if online else "off", name, did,
|
|
||||||
code,
|
|
||||||
"on" if online else "off", "在线" if online else "离线",
|
|
||||||
device["idleSeconds"],
|
|
||||||
device["queuedToClient"], device["queuedToAgent"],
|
|
||||||
_kb(device["bytesIn"]), _kb(device["bytesOut"]),
|
|
||||||
did, name, code,
|
|
||||||
did, name, code)
|
|
||||||
)"""
|
|
||||||
text, ok = patch(text, old, new, "设备行加「改码」按钮")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ------------------------------------------------ 2. 开关放进"设备"小节
|
|
||||||
old = """ <div class="sec">
|
|
||||||
<h3>设备</h3>
|
|
||||||
<div class="right">
|
|
||||||
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
|
|
||||||
<input type="checkbox" id="autoRef" checked> 自动刷新
|
|
||||||
</label>"""
|
|
||||||
new = """ <div class="sec">
|
|
||||||
<h3>设备</h3>
|
|
||||||
<div class="right">
|
|
||||||
<label class="hint" id="allowWrap"
|
|
||||||
style="cursor:pointer;display:flex;align-items:center;gap:7px;margin:0 18px 0 0">
|
|
||||||
<input type="checkbox" id="allowNew"%(allownew)s>
|
|
||||||
<span>允许新设备接入</span>
|
|
||||||
</label>
|
|
||||||
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
|
|
||||||
<input type="checkbox" id="autoRef" checked> 自动刷新
|
|
||||||
</label>"""
|
|
||||||
text, ok = patch(text, old, new, "设备小节加开关")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ------------------------------------------------ 3. 传入开关状态
|
|
||||||
old = """ sessions = snap.get("sessions", 0)"""
|
|
||||||
new = """ sessions = snap.get("sessions", 0)
|
|
||||||
# 开关状态:允许新设备接入时勾上
|
|
||||||
allow_new = bool(snap.get("allowNew", True))"""
|
|
||||||
text, ok = patch(text, old, new, "读 allowNew 状态")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
old = """ "rows": _device_rows(devices),
|
|
||||||
"dl": dl_rows,"""
|
|
||||||
new = """ "rows": _device_rows(devices),
|
|
||||||
"allownew": " checked" if allow_new else "",
|
|
||||||
"dl": dl_rows,"""
|
|
||||||
text, ok = patch(text, old, new, "把状态传进模板")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
# ------------------------------------------------ 4. JS
|
|
||||||
old = """// ---------------- 单台删除 ----------------"""
|
|
||||||
new = """// ---------------- 允许/禁止新设备接入 ----------------
|
|
||||||
// 关掉之后,只有**已经登记过**的电脑能继续上线,
|
|
||||||
// 没见过的新设备一律拒绝。
|
|
||||||
const allowNew = document.getElementById('allowNew');
|
|
||||||
if (allowNew) allowNew.onchange = async () => {
|
|
||||||
const want = allowNew.checked;
|
|
||||||
allowNew.disabled = true;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/allow_new', {allow: want});
|
|
||||||
if (j.ok) {
|
|
||||||
toast(j.allowNew ? '已允许新设备接入' : '已禁止新设备接入', 'ok');
|
|
||||||
} else {
|
|
||||||
toast('设置失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
allowNew.checked = !want;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
allowNew.checked = !want;
|
|
||||||
}
|
|
||||||
allowNew.disabled = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 改配对码 ----------------
|
|
||||||
document.querySelectorAll('button[data-recode]').forEach(b => {
|
|
||||||
b.onclick = async () => {
|
|
||||||
const name = b.dataset.name, old = b.dataset.code;
|
|
||||||
const typed = prompt('把「' + name + '」的配对码 ' + old
|
|
||||||
+ ' 改成:\\n\\n(必须是 6 位数字)', '');
|
|
||||||
if (typed === null) return;
|
|
||||||
const code = (typed || '').trim();
|
|
||||||
if (!/^[0-9]{6}$/.test(code)) {
|
|
||||||
toast('配对码必须是 6 位数字', 'err');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
b.disabled = true;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/set_code',
|
|
||||||
{deviceId: b.dataset.recode, code: code});
|
|
||||||
if (j.ok) {
|
|
||||||
toast(j.unchanged ? '配对码没有变化'
|
|
||||||
: ('配对码已改成 ' + j.code
|
|
||||||
+ (j.kicked ? ',踢掉 ' + j.kicked + ' 个会话' : '')),
|
|
||||||
'ok');
|
|
||||||
setTimeout(() => location.reload(), 900);
|
|
||||||
} else {
|
|
||||||
toast('改码失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
b.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
b.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------- 单台删除 ----------------"""
|
|
||||||
text, ok = patch(text, old, new, "加开关和改码的 JS")
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
if not ok_all:
|
|
||||||
print()
|
|
||||||
print(" 有锚点没匹配,没有写回")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" admin.py 已写回")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''给静态资源加缓存头,让 Cloudflare 在边缘直接发。
|
|
||||||
|
|
||||||
== 为什么这是最大的一招 ==
|
|
||||||
|
|
||||||
基线实测(下载 15 MB 安装包走隧道回 4G 源站):
|
|
||||||
|
|
||||||
ykxmz-watch-1.8.1.apk 201523 字节 / 66.8 秒 = 3 KB/s
|
|
||||||
ykxmz-server-1.8.1.zip 214900 字节 / 3.1 秒 = 67 KB/s
|
|
||||||
ykxmz-setup-1.8.1.exe 只下到 237568/15067199 字节就停了
|
|
||||||
|
|
||||||
原因:relay 给所有响应都发 `Cache-Control: no-store`,
|
|
||||||
**Cloudflare 完全不缓存** —— 每一次下载、每一张图、每个 favicon
|
|
||||||
都要穿隧道回 4G 源站。而回源吞吐只有几 KB/s。
|
|
||||||
|
|
||||||
改成可缓存后,Cloudflare 会在用户附近的边缘节点直接发,
|
|
||||||
速度是"边缘到用户",跟 4G 那条腿无关。
|
|
||||||
|
|
||||||
== 各路径的策略 ==
|
|
||||||
|
|
||||||
/download/* public, max-age=604800, immutable
|
|
||||||
文件名带版本号(ykxmz-setup-1.8.1.exe),
|
|
||||||
同一名字的内容永不改变,所以可以放心长缓存。
|
|
||||||
/shot/* 已经是 public, max-age=86400(保持不变)
|
|
||||||
/favicon.ico public, max-age=604800(147 KB,之前每次 3.2 秒)
|
|
||||||
/ public, max-age=60
|
|
||||||
首页会列出当前版本,缓存久了下新版本会看不到按钮。
|
|
||||||
60 秒足够挡住刷新的重复回源,又不会让版本信息过期太久。
|
|
||||||
/v1/* no-store(接口,必须实时)
|
|
||||||
|
|
||||||
注意:Cloudflare 默认只缓存一部分扩展名,.exe/.zip/.apk 可能不在
|
|
||||||
默认列表里。所以改完必须**实测 cf-cache-status 是不是 HIT**,
|
|
||||||
不 HIT 就要在 Cloudflare 后台加一条 Cache Rule(我会告诉用户怎么加)。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay\relay.py")
|
|
||||||
|
|
||||||
OLD_DL = """ self.send_header(
|
|
||||||
"Content-Disposition",
|
|
||||||
f"attachment; filename*=UTF-8''{quoted}")
|
|
||||||
self.send_header("Cache-Control", "no-store")"""
|
|
||||||
|
|
||||||
NEW_DL = """ self.send_header(
|
|
||||||
"Content-Disposition",
|
|
||||||
f"attachment; filename*=UTF-8''{quoted}")
|
|
||||||
# ★ 让 Cloudflare 在边缘缓存。
|
|
||||||
# 原来这里是 no-store,于是每次下载都要穿隧道回 4G 源站,
|
|
||||||
# 实测吞吐只有 3~67 KB/s(15 MB 的安装包根本下不动)。
|
|
||||||
# 文件名带版本号,同一名字内容永不改变,可以长缓存。
|
|
||||||
self.send_header(
|
|
||||||
"Cache-Control",
|
|
||||||
"public, max-age=604800, immutable")"""
|
|
||||||
|
|
||||||
# favicon 那块
|
|
||||||
OLD_ICON = """ self._send(200, icon, "image/x-icon")"""
|
|
||||||
NEW_ICON = """ self._send(200, icon, "image/x-icon",
|
|
||||||
{"Cache-Control": "public, max-age=604800"})"""
|
|
||||||
|
|
||||||
# 首页那块
|
|
||||||
OLD_SITE = """ self._send(200, _site_page().encode("utf-8"),
|
|
||||||
"text/html; charset=utf-8")
|
|
||||||
return"""
|
|
||||||
NEW_SITE = """ # 首页缓存 60 秒:挡住刷新的重复回源,
|
|
||||||
# 又不会让"当前版本/下载按钮"过期太久。
|
|
||||||
self._send(200, _site_page().encode("utf-8"),
|
|
||||||
"text/html; charset=utf-8",
|
|
||||||
{"Cache-Control": "public, max-age=60"})
|
|
||||||
return"""
|
|
||||||
|
|
||||||
|
|
||||||
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
|
||||||
if old not in text:
|
|
||||||
print(" ✗ 没找到:%s" % label)
|
|
||||||
return text, False
|
|
||||||
if text.count(old) != 1:
|
|
||||||
print(" ! 出现 %d 次:%s" % (text.count(old), label))
|
|
||||||
return text, False
|
|
||||||
print(" ✓ %s" % label)
|
|
||||||
return text.replace(old, new, 1), True
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = SRC.read_text(encoding="utf-8")
|
|
||||||
before = text.count("\n") + 1
|
|
||||||
ok_all = True
|
|
||||||
|
|
||||||
print(" 加缓存头:")
|
|
||||||
for old, new, label in (
|
|
||||||
(OLD_DL, NEW_DL, "下载:no-store -> public,max-age=604800,immutable"),
|
|
||||||
(OLD_ICON, NEW_ICON, "favicon:加 7 天缓存"),
|
|
||||||
(OLD_SITE, NEW_SITE, "首页:加 60 秒缓存"),
|
|
||||||
):
|
|
||||||
text, ok = patch(text, old, new, label)
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
if not ok_all:
|
|
||||||
print()
|
|
||||||
print(" 有锚点没匹配,没有写回")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" 已写回:%d -> %d 行" % (before, text.count("\n") + 1))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r"""给被控端加 --code / --name,供安装包调用。
|
|
||||||
|
|
||||||
为什么需要:
|
|
||||||
安装向导里用户填了 6 位配对码,安装程序必须把它传给被控端。
|
|
||||||
但命令行原来只有 --new-code(随机换一个),**没有"设成指定值"** ——
|
|
||||||
所以用户填的码会被忽略,装完还得再跑一次设置窗口。
|
|
||||||
|
|
||||||
加了之后安装程序可以这样一条命令搞定:
|
|
||||||
ykxmz.exe --code 789113 --name 三年二班 --autostart on --silent
|
|
||||||
|
|
||||||
--code 同时会标记 setup_done,这样就不会再弹设置窗口。
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
RUN = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows"
|
|
||||||
r"\run_agent.py"))
|
|
||||||
|
|
||||||
# ---------- 1. 参数声明 ----------
|
|
||||||
|
|
||||||
OLD_ARG = ''' parser.add_argument("--new-code", action="store_true", help="换一组配对码")'''
|
|
||||||
|
|
||||||
NEW_ARG = ''' parser.add_argument("--new-code", action="store_true", help="换一组配对码")
|
|
||||||
parser.add_argument("--code", default=None, metavar="6位数字",
|
|
||||||
help="把配对码设成指定值(必须是 6 位数字)。"
|
|
||||||
"安装包用这个把用户在安装向导里填的码写进去。")
|
|
||||||
parser.add_argument("--name", default=None, metavar="名字",
|
|
||||||
help="设置这台电脑在手表上显示的名字"
|
|
||||||
"(默认用 Windows 的计算机名)")'''
|
|
||||||
|
|
||||||
# ---------- 2. 处理逻辑 ----------
|
|
||||||
|
|
||||||
OLD_USE = ''' if args.reset_devices:
|
|
||||||
store.devices = {}
|
|
||||||
store.save_devices()
|
|
||||||
if args.new_code or not store.pairing_code():
|
|
||||||
store.new_pairing_code()'''
|
|
||||||
|
|
||||||
NEW_USE = ''' if args.reset_devices:
|
|
||||||
store.devices = {}
|
|
||||||
store.save_devices()
|
|
||||||
|
|
||||||
# --code:把配对码设成安装向导里填的那个。
|
|
||||||
# 严格要 6 位数字 —— 和设置窗口里的规则一致,
|
|
||||||
# 免得用户装了之后发现手表上少输一位连不上。
|
|
||||||
if args.code is not None:
|
|
||||||
digits = "".join(ch for ch in str(args.code) if ch.isdigit())
|
|
||||||
if len(digits) != 6:
|
|
||||||
print(f"配对码要 6 位数字,收到的是 {args.code!r}")
|
|
||||||
return 2
|
|
||||||
try:
|
|
||||||
store.set_pairing_code(digits)
|
|
||||||
except ValueError as exc:
|
|
||||||
print(f"设置配对码失败:{exc}")
|
|
||||||
return 2
|
|
||||||
# 装完就是"设置好了",不要再弹设置窗口
|
|
||||||
store.set("setup_done", True)
|
|
||||||
store.set("allow_new_pairing", True)
|
|
||||||
print(f"配对码已设为 {digits}")
|
|
||||||
|
|
||||||
if args.name:
|
|
||||||
store.set("name", str(args.name).strip()[:32])
|
|
||||||
|
|
||||||
if args.new_code or not store.pairing_code():
|
|
||||||
store.new_pairing_code()'''
|
|
||||||
|
|
||||||
# ---------- 3. 帮助文本 ----------
|
|
||||||
|
|
||||||
OLD_HELP = ''' --new-code 换一组配对码'''
|
|
||||||
|
|
||||||
NEW_HELP = ''' --new-code 换一组配对码
|
|
||||||
--code 123456 把配对码设成指定值(安装包用)
|
|
||||||
--name "三年二班" 设置电脑在手表上显示的名字'''
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = RUN.read_text(encoding="utf-8")
|
|
||||||
ok = True
|
|
||||||
|
|
||||||
if '"--code"' in text:
|
|
||||||
print(" 已经改过")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
for old, new, label in ((OLD_ARG, NEW_ARG, "参数声明"),
|
|
||||||
(OLD_USE, NEW_USE, "处理逻辑"),
|
|
||||||
(OLD_HELP, NEW_HELP, "帮助文本")):
|
|
||||||
if old not in text:
|
|
||||||
print(f" [失败] {label} 锚点没找到")
|
|
||||||
ok = False
|
|
||||||
continue
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
print(f" {label}")
|
|
||||||
|
|
||||||
if not ok:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
RUN.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
try:
|
|
||||||
ast.parse(text)
|
|
||||||
print(" run_agent.py 语法 OK")
|
|
||||||
except SyntaxError as exc:
|
|
||||||
print(f" 语法错误: 行 {exc.lineno}: {exc.msg}")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''把安装包完整周期测试加进套件,并提交这一轮的关键修复。'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
VERIFY = ROOT / "_tests" / "verify_all.ps1"
|
|
||||||
GIT = r"C:\Program Files\Git\bin\git.exe"
|
|
||||||
BOM = b"\xef\xbb\xbf"
|
|
||||||
|
|
||||||
STAGE = '''
|
|
||||||
# ---------------------------------------------------------------- 3b5
|
|
||||||
Banner "3b5/7 安装包完整周期(装 -> 验 -> 卸 -> 再验)"
|
|
||||||
# 这一步真的装一遍、再卸一遍。踩过的四个卡死 bug 全靠它才发现:
|
|
||||||
# input() 无 stdin 阻塞 / Popen 不重定向 stdio /
|
|
||||||
# taskkill 把自己杀了 / 关不掉管理员权限的旧实例导致安装回滚。
|
|
||||||
# 注意:需要管理员权限,且会真的往 Program Files 装东西。
|
|
||||||
$stageOut = & python (Join-Path $root "_build\\test_installer_cycle.py") 2>&1
|
|
||||||
Show-Fails $stageOut
|
|
||||||
Record "安装包完整周期" ($LASTEXITCODE -eq 0)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
MSG = """修安装包卸载卡死的真凶:attach_console 在无控制台时阻塞
|
|
||||||
|
|
||||||
排查过程(一步步排除,记录下来免得以后重踩):
|
|
||||||
|
|
||||||
1. 现象:从控制面板卸载,进度条永远走不完。
|
|
||||||
2. 先怀疑 input():安装器用 runhidden 调 --uninstall,没有 stdin,
|
|
||||||
input() 会一直等。加了 isatty 保护 —— **没解决**。
|
|
||||||
3. 再怀疑 Popen 没重定向 stdio:_schedule_self_delete 起的 cmd
|
|
||||||
继承了父进程句柄,capture_output 会等管道 EOF。加了
|
|
||||||
DEVNULL 重定向 —— **还是卡**。
|
|
||||||
4. 又发现 _kill_running 用 taskkill /IM ykxmz.exe 把正在卸载的
|
|
||||||
自己杀了(--uninstall 就跑在 ykxmz.exe 里),
|
|
||||||
导致后面的"删自启/删数据"没执行。改成 /FI "PID ne self" ——
|
|
||||||
自启能删了,**卸载仍然卡**。
|
|
||||||
5. 最后看进程状态才定位:卡住的 ykxmz.exe 只有 **1 个线程、
|
|
||||||
0.16 秒 CPU、19MB 内存** —— 明显停在启动早期,根本没干活。
|
|
||||||
那就是 cli_uninstall 第一行的 attach_console():
|
|
||||||
它对 --windowed 程序调 AllocConsole(),再 open("CONIN$")。
|
|
||||||
在没有控制台的隐藏进程里,这一步**永久阻塞**。
|
|
||||||
|
|
||||||
修法:加 --quiet。安装包的卸载器用
|
|
||||||
ykxmz.exe --uninstall --keep-dir --quiet
|
|
||||||
--quiet 下完全不碰控制台(不 AllocConsole、不 input),
|
|
||||||
结果写进 uninstall.log 供排查。
|
|
||||||
实测:不带 --quiet 无限卡住;带了 **0.6 秒返回**。
|
|
||||||
|
|
||||||
顺带修的两处(也是实测发现的):
|
|
||||||
* 安装器关不掉以管理员身份运行的旧实例 -> Inno 弹"无法关闭所有
|
|
||||||
应用程序",静默下被压成"终止",**整个安装回滚**。
|
|
||||||
改成 PrepareToInstall 里自己 taskkill /F。
|
|
||||||
* 测试脚本把注册表路径写成 HKCU\\...(少了冒号),
|
|
||||||
PowerShell 静默返回空,"开机自启"永远误报失败。
|
|
||||||
|
|
||||||
最终实测(test_installer_cycle.py,20 项):
|
|
||||||
静默安装 退出码 0,7.1 秒
|
|
||||||
配对码 正确写入 C:\\ProgramData\\ykxmz\\data\\settings.json
|
|
||||||
进程/端口 在跑,8765 监听
|
|
||||||
开机自启 已设置
|
|
||||||
静默卸载 1.2 秒
|
|
||||||
卸载清理 安装目录 / 数据目录 / 注册表 / 进程 / 自启 全部干净
|
|
||||||
20 项全通过
|
|
||||||
|
|
||||||
套件新增一个阶段:「安装包完整周期」。
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def add_stage() -> None:
|
|
||||||
raw = VERIFY.read_bytes()
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
if "test_installer_cycle.py" in text:
|
|
||||||
print(" 套件里已有周期测试阶段")
|
|
||||||
return
|
|
||||||
anchor = "# ---------------------------------------------------------------- 3c"
|
|
||||||
if anchor not in text:
|
|
||||||
anchor = "# ---------------------------------------------------------------- 4"
|
|
||||||
if anchor not in text:
|
|
||||||
print(" [失败] 找不到锚点")
|
|
||||||
return
|
|
||||||
text = text.replace(anchor, STAGE.lstrip("\n") + anchor, 1)
|
|
||||||
VERIFY.write_bytes(BOM + text.replace("\r\n", "\n").encode("utf-8"))
|
|
||||||
print(" 已加「安装包完整周期」阶段")
|
|
||||||
|
|
||||||
|
|
||||||
def commit() -> None:
|
|
||||||
print()
|
|
||||||
subprocess.run([GIT, "add", "-A"], cwd=str(ROOT), check=False)
|
|
||||||
msg_file = ROOT / "_build" / "_cmsg4.txt"
|
|
||||||
msg_file.write_bytes(MSG.encode("utf-8"))
|
|
||||||
r = subprocess.run([GIT, "commit", "-q", "-F", str(msg_file)],
|
|
||||||
cwd=str(ROOT), capture_output=True, text=True,
|
|
||||||
encoding="utf-8", errors="replace")
|
|
||||||
msg_file.unlink(missing_ok=True)
|
|
||||||
print(" commit exit=%s" % r.returncode)
|
|
||||||
show = subprocess.run([GIT, "log", "--oneline", "-1"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace")
|
|
||||||
print(" " + (show.stdout or "").strip())
|
|
||||||
env = dict(os.environ)
|
|
||||||
env["GIT_SSH_COMMAND"] = r'"C:\Program Files\Git\usr\bin\ssh.exe"'
|
|
||||||
push = subprocess.run([GIT, "push", "origin", "main"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace", env=env)
|
|
||||||
for line in (push.stderr or push.stdout or "").strip().splitlines()[-2:]:
|
|
||||||
print(" " + line)
|
|
||||||
status = subprocess.run([GIT, "status", "--short"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace")
|
|
||||||
print(" 工作区: %s" % ("干净 ✓" if not (status.stdout or "").strip()
|
|
||||||
else (status.stdout or "").strip()[:160]))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
add_stage()
|
|
||||||
commit()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''单次请求失败不再立刻判"离线",改成连续失败 3 次才算。
|
|
||||||
|
|
||||||
== 为什么 ==
|
|
||||||
|
|
||||||
实测单个请求会有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上,
|
|
||||||
或者那条连接正在重连)。原来的逻辑是**一失败就 online = false**,
|
|
||||||
于是:
|
|
||||||
|
|
||||||
手表/电脑的界面上会闪一下"已断开"
|
|
||||||
用户点按钮时正好撞上那一闪 -> 看起来像"又连不上了"
|
|
||||||
|
|
||||||
而实际上链路 1.5~15 秒后就自己恢复了(客户端本来就有退避重试)。
|
|
||||||
|
|
||||||
改成"连续失败 3 次才判离线",效果是:
|
|
||||||
* 单次抖动完全不影响界面显示
|
|
||||||
* 真的断了(连续 3 次,至少 4.5 秒都没通)仍然会如实显示离线
|
|
||||||
|
|
||||||
这不是掩盖问题,而是把"是否可用"的判断从"一次采样"改成
|
|
||||||
"连续采样"—— 单点采样在有丢包的链路上噪声太大。
|
|
||||||
|
|
||||||
== 两边都要改 ==
|
|
||||||
|
|
||||||
1. android/src/com/lanremote/watch/RelayTransport.java (手表轮询)
|
|
||||||
2. windows/lanremote/relay.py (电脑轮询)
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
BOM = b"\xef\xbb\xbf"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 手表
|
|
||||||
WATCH = ROOT / "android/src/com/lanremote/watch/RelayTransport.java"
|
|
||||||
|
|
||||||
WATCH_CONST_OLD = " private static final int RETRY_MAX_MS = 15000;"
|
|
||||||
WATCH_CONST_NEW = """ private static final int RETRY_MAX_MS = 15000;
|
|
||||||
/**
|
|
||||||
* 连续失败几次才判定"离线"。
|
|
||||||
*
|
|
||||||
* <p>单次失败就报离线太敏感:4G 链路上随手就有 2~4% 的偶发失败,
|
|
||||||
* 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
|
|
||||||
* 改成连续 3 次(至少 4.5 秒都不通)才判离线 ——
|
|
||||||
* 真的断了仍然会如实显示。
|
|
||||||
*/
|
|
||||||
private static final int OFFLINE_AFTER = 3;"""
|
|
||||||
|
|
||||||
WATCH_FIELD_OLD = " private Thread pollThread;"
|
|
||||||
WATCH_FIELD_NEW = """ private Thread pollThread;
|
|
||||||
/** 连续失败次数(成功一次就清零)。 */
|
|
||||||
private volatile int failStreak;"""
|
|
||||||
|
|
||||||
# 401/403 分支
|
|
||||||
W_401_OLD = """ if (result.code == 401 || result.code == 403) {
|
|
||||||
// 会话过期(中转重启等)—— 这种可以自动重连
|
|
||||||
online = false;
|
|
||||||
lastError = "会话已失效,正在重新接入";"""
|
|
||||||
W_401_NEW = """ if (result.code == 401 || result.code == 403) {
|
|
||||||
// 会话过期(中转重启等)—— 这种可以自动重连。
|
|
||||||
// 会话失效是明确的状态,直接标离线。
|
|
||||||
online = false;
|
|
||||||
lastError = "会话已失效,正在重新接入";"""
|
|
||||||
|
|
||||||
# 非 200 分支
|
|
||||||
W_OTHER_OLD = """ if (result.code != 200) {
|
|
||||||
online = false;
|
|
||||||
lastError = "轮询返回 " + result.code;
|
|
||||||
sleep(backoff);
|
|
||||||
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
online = true;
|
|
||||||
lastError = "";
|
|
||||||
backoff = RETRY_MIN_MS;"""
|
|
||||||
W_OTHER_NEW = """ if (result.code != 200) {
|
|
||||||
// 偶发的 502/504 不算断线,连续几次才算
|
|
||||||
failStreak++;
|
|
||||||
if (failStreak >= OFFLINE_AFTER) {
|
|
||||||
online = false;
|
|
||||||
}
|
|
||||||
lastError = "轮询返回 " + result.code;
|
|
||||||
sleep(backoff);
|
|
||||||
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
online = true;
|
|
||||||
failStreak = 0;
|
|
||||||
lastError = "";
|
|
||||||
backoff = RETRY_MIN_MS;"""
|
|
||||||
|
|
||||||
# 异常分支
|
|
||||||
W_EXC_OLD = """ } catch (Exception exc) {
|
|
||||||
if (closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
online = false;
|
|
||||||
lastError = "轮询出错:" + exc.getClass().getSimpleName();
|
|
||||||
sleep(backoff);
|
|
||||||
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
|
||||||
}"""
|
|
||||||
W_EXC_NEW = """ } catch (Exception exc) {
|
|
||||||
if (closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 连接超时/重置很常见(隧道在重连),连续几次才算真断
|
|
||||||
failStreak++;
|
|
||||||
if (failStreak >= OFFLINE_AFTER) {
|
|
||||||
online = false;
|
|
||||||
}
|
|
||||||
lastError = "轮询出错:" + exc.getClass().getSimpleName();
|
|
||||||
sleep(backoff);
|
|
||||||
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
|
||||||
}"""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 电脑
|
|
||||||
AGENT = ROOT / "windows/lanremote/relay.py"
|
|
||||||
|
|
||||||
A_CONST_OLD = "RETRY_MAX = 20.0"
|
|
||||||
A_CONST_NEW = """RETRY_MAX = 20.0
|
|
||||||
# 连续失败几次才判定"离线"。
|
|
||||||
# 单次失败就报离线太敏感:4G 链路上有 2~4% 的偶发失败,
|
|
||||||
# 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
|
|
||||||
# 连续 3 次(至少 6 秒都不通)才判离线,真的断了仍然如实显示。
|
|
||||||
OFFLINE_AFTER = 3"""
|
|
||||||
|
|
||||||
A_FAIL_OLD = """ except Exception as exc: # noqa: BLE001
|
|
||||||
self.online = False
|
|
||||||
self.last_error = f"{type(exc).__name__}: {exc}"
|
|
||||||
self._log(f"中转连接出问题:{self.last_error}")"""
|
|
||||||
A_FAIL_NEW = """ except Exception as exc: # noqa: BLE001
|
|
||||||
# 偶发的连接超时/重置不算断线,连续几次才算
|
|
||||||
self._fail_streak = getattr(self, "_fail_streak", 0) + 1
|
|
||||||
if self._fail_streak >= OFFLINE_AFTER:
|
|
||||||
self.online = False
|
|
||||||
self.last_error = f"{type(exc).__name__}: {exc}"
|
|
||||||
self._log(f"中转连接出问题(第 {self._fail_streak} 次):"
|
|
||||||
f"{self.last_error}")"""
|
|
||||||
|
|
||||||
A_OK_OLD = """ self.online = True
|
|
||||||
self.last_ok = time.time()
|
|
||||||
self.last_error = \"\""""
|
|
||||||
A_OK_NEW = """ self.online = True
|
|
||||||
self._fail_streak = 0
|
|
||||||
self.last_ok = time.time()
|
|
||||||
self.last_error = \"\""""
|
|
||||||
|
|
||||||
|
|
||||||
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
|
||||||
if old not in text:
|
|
||||||
if new.strip() and new.splitlines()[-1].strip() in text:
|
|
||||||
print(" • %s 已经是新值" % label)
|
|
||||||
return text, True
|
|
||||||
print(" ✗ 没找到:%s" % label)
|
|
||||||
return text, False
|
|
||||||
if text.count(old) != 1:
|
|
||||||
print(" ! 出现 %d 次:%s" % (text.count(old), label))
|
|
||||||
return text, False
|
|
||||||
print(" ✓ %s" % label)
|
|
||||||
return text.replace(old, new, 1), True
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ok_all = True
|
|
||||||
|
|
||||||
print(" 手表 RelayTransport.java:")
|
|
||||||
raw = WATCH.read_bytes()
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
for old, new, label in (
|
|
||||||
(WATCH_CONST_OLD, WATCH_CONST_NEW, "加 OFFLINE_AFTER 常量"),
|
|
||||||
(WATCH_FIELD_OLD, WATCH_FIELD_NEW, "加 failStreak 字段"),
|
|
||||||
(W_OTHER_OLD, W_OTHER_NEW, "非 200 用连续计数"),
|
|
||||||
(W_EXC_OLD, W_EXC_NEW, "异常用连续计数"),
|
|
||||||
):
|
|
||||||
text, ok = patch(text, old, new, label)
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
if ok_all:
|
|
||||||
WATCH.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 电脑 relay.py:")
|
|
||||||
araw = AGENT.read_bytes()
|
|
||||||
atext = araw.decode("utf-8-sig")
|
|
||||||
for old, new, label in (
|
|
||||||
(A_CONST_OLD, A_CONST_NEW, "加 OFFLINE_AFTER 常量"),
|
|
||||||
(A_FAIL_OLD, A_FAIL_NEW, "异常用连续计数"),
|
|
||||||
(A_OK_OLD, A_OK_NEW, "成功时清零"),
|
|
||||||
):
|
|
||||||
atext, ok = patch(atext, old, new, label)
|
|
||||||
ok_all = ok_all and ok
|
|
||||||
|
|
||||||
if not ok_all:
|
|
||||||
print()
|
|
||||||
print(" 有失败项,没写回电脑端")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
AGENT.write_text(atext, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" 两边都改好了")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r"""给中转管理页加 favicon(用项目的 app.ico)。
|
|
||||||
|
|
||||||
用户提过:"那个中转管理端那个,网页图标没有啊,图标就是我之前给你那个图标"
|
|
||||||
|
|
||||||
做法:
|
|
||||||
1. 把 windows/app.ico 拷一份到 relay/favicon.ico(跟 relay.py 同目录)
|
|
||||||
2. relay.py 加一条 GET /favicon.ico 路由直接给这个文件
|
|
||||||
3. 管理页 <head> 里加 <link rel="icon" ...>
|
|
||||||
|
|
||||||
为什么不做成 base64 内嵌:
|
|
||||||
ico 有 147KB,内嵌到 relay.py 里会让源码从 78KB 涨到 270KB,
|
|
||||||
而且每次改图标都要动代码。放成独立文件干净得多。
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
RELAY = ROOT / "relay" / "relay.py"
|
|
||||||
ICON_SRC = ROOT / "windows" / "app.ico"
|
|
||||||
ICON_DST = ROOT / "relay" / "favicon.ico"
|
|
||||||
|
|
||||||
# ---------- 1. 路由 ----------
|
|
||||||
|
|
||||||
ANCHOR_ROUTE = ''' # ---- 成品下载(挂在管理页上给人下 APK / 整包)----'''
|
|
||||||
|
|
||||||
NEW_ROUTE = ''' # ---- 网站图标 ----
|
|
||||||
if path in ("/favicon.ico", "/apple-touch-icon.png"):
|
|
||||||
icon = Path(__file__).resolve().parent / "favicon.ico"
|
|
||||||
if icon.is_file():
|
|
||||||
try:
|
|
||||||
blob = icon.read_bytes()
|
|
||||||
except OSError:
|
|
||||||
blob = b""
|
|
||||||
if blob:
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "image/x-icon")
|
|
||||||
self.send_header("Content-Length", str(len(blob)))
|
|
||||||
# 图标不常变,让浏览器缓存一天
|
|
||||||
self.send_header("Cache-Control", "public, max-age=86400")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(blob)
|
|
||||||
return
|
|
||||||
# 没有图标文件就 404,别让请求打到后面的逻辑
|
|
||||||
self.send_response(404)
|
|
||||||
self.send_header("Content-Length", "0")
|
|
||||||
self.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
''' + ANCHOR_ROUTE
|
|
||||||
|
|
||||||
# ---------- 2. 页面里声明 ----------
|
|
||||||
|
|
||||||
ANCHOR_HEAD = '''<meta name=viewport content="width=device-width,initial-scale=1">'''
|
|
||||||
|
|
||||||
NEW_HEAD = ('''<meta name=viewport content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
|
||||||
<link rel="apple-touch-icon" href="/favicon.ico">''')
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ok = True
|
|
||||||
|
|
||||||
# ---- 图标文件 ----
|
|
||||||
if not ICON_SRC.is_file():
|
|
||||||
print(f" [失败] 找不到 {ICON_SRC}")
|
|
||||||
return 1
|
|
||||||
shutil.copy2(ICON_SRC, ICON_DST)
|
|
||||||
print(f" 拷贝图标 -> relay/favicon.ico({ICON_DST.stat().st_size} 字节)")
|
|
||||||
|
|
||||||
text = RELAY.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
if "favicon.ico" in text:
|
|
||||||
print(" relay.py 里已经有 favicon 路由")
|
|
||||||
else:
|
|
||||||
for old, new, label in ((ANCHOR_ROUTE, NEW_ROUTE, "favicon 路由"),
|
|
||||||
(ANCHOR_HEAD, NEW_HEAD, "页面里的 link 声明")):
|
|
||||||
if old not in text:
|
|
||||||
print(f" [失败] {label} —— 锚点没找到")
|
|
||||||
ok = False
|
|
||||||
continue
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
print(f" {label}")
|
|
||||||
|
|
||||||
if ok:
|
|
||||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
|
|
||||||
if not ok:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
ast.parse(RELAY.read_text(encoding="utf-8"))
|
|
||||||
print(" relay.py 语法 OK")
|
|
||||||
except SyntaxError as exc:
|
|
||||||
print(f" 语法错误: 行 {exc.lineno}: {exc.msg}")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''把响应头检查加进套件,并提交这次的修复。'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
VERIFY = ROOT / "_tests" / "verify_all.ps1"
|
|
||||||
GIT = r"C:\Program Files\Git\bin\git.exe"
|
|
||||||
BOM = b"\xef\xbb\xbf"
|
|
||||||
|
|
||||||
STAGE = '''
|
|
||||||
# ---------------------------------------------------------------- 3b6
|
|
||||||
Banner "3b6/7 网页响应头(Content-Type / HEAD)"
|
|
||||||
# 踩过的坑:重做管理页时 _send() 没传 content_type,
|
|
||||||
# 默认成了 application/json —— 浏览器把整页 HTML 当纯文本显示,
|
|
||||||
# 用户看到的是一屏源码。
|
|
||||||
# 更糟的是当时没发现:只验了 HTTP 200 和字节数,两个都对。
|
|
||||||
# **200 不等于页面能看。**
|
|
||||||
$stageOut = & python (Join-Path $root "_build\\test_response_headers.py") 2>&1
|
|
||||||
Show-Fails $stageOut
|
|
||||||
Record "网页响应头" ($LASTEXITCODE -eq 0)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
MSG = """修管理页 Content-Type(浏览器显示成一屏源码)
|
|
||||||
|
|
||||||
用户截图:打开 relay.xxmxxs.top/zhongzhuanzhan 看到的**不是页面,
|
|
||||||
而是整页 HTML 源码**。
|
|
||||||
|
|
||||||
根因:_send() 的 content_type 参数默认是 application/json,
|
|
||||||
我重做管理页时调它没传 text/html —— 浏览器按 JSON/纯文本处理,
|
|
||||||
就把 HTML 原样显示了。
|
|
||||||
|
|
||||||
⚠ 更该反省的是:**我当时没发现。**
|
|
||||||
验证脚本只看了 HTTP 200 和字节数,两个都对,就以为没问题。
|
|
||||||
200 不等于页面能看 —— 响应头也得看。
|
|
||||||
|
|
||||||
顺手修的第二个问题:
|
|
||||||
HEAD 请求一律返回 501 Unsupported method(没实现 do_HEAD)。
|
|
||||||
浏览器用 GET 所以页面照开,但 Cloudflare 健康检查、
|
|
||||||
很多监控/代理探测都用 HEAD,curl -I 也是。
|
|
||||||
补了 do_HEAD:按路径算出类型和长度,只回头不回体。
|
|
||||||
|
|
||||||
新增测试 _build/test_response_headers.py(9 项):
|
|
||||||
* HEAD 各路径不能是 501
|
|
||||||
* 官网/管理页 Content-Type 必须是 text/html,不能是 json
|
|
||||||
* favicon、产品截图必须是 image/*
|
|
||||||
并加进全量套件(阶段 3b6)。
|
|
||||||
|
|
||||||
验证:该测试 9/9 通过;全量套件 33 个阶段全部通过。
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def add_stage() -> None:
|
|
||||||
raw = VERIFY.read_bytes()
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
if "test_response_headers" in text:
|
|
||||||
print(" 套件里已有响应头阶段")
|
|
||||||
return
|
|
||||||
anchor = "# ---------------------------------------------------------------- 3c"
|
|
||||||
if anchor not in text:
|
|
||||||
anchor = "# ---------------------------------------------------------------- 4"
|
|
||||||
if anchor not in text:
|
|
||||||
print(" [失败] 找不到锚点")
|
|
||||||
return
|
|
||||||
text = text.replace(anchor, STAGE.lstrip("\n") + anchor, 1)
|
|
||||||
VERIFY.write_bytes(BOM + text.replace("\r\n", "\n").encode("utf-8"))
|
|
||||||
print(" 已加「网页响应头」阶段")
|
|
||||||
|
|
||||||
|
|
||||||
def commit() -> None:
|
|
||||||
print()
|
|
||||||
subprocess.run([GIT, "add", "-A"], cwd=str(ROOT), check=False)
|
|
||||||
msg_file = ROOT / "_build" / "_c9.txt"
|
|
||||||
msg_file.write_bytes(MSG.encode("utf-8"))
|
|
||||||
r = subprocess.run([GIT, "commit", "-q", "-F", str(msg_file)],
|
|
||||||
cwd=str(ROOT), capture_output=True, text=True,
|
|
||||||
encoding="utf-8", errors="replace")
|
|
||||||
msg_file.unlink(missing_ok=True)
|
|
||||||
print(" commit exit=%s" % r.returncode)
|
|
||||||
show = subprocess.run([GIT, "log", "--oneline", "-1"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace")
|
|
||||||
print(" " + (show.stdout or "").strip())
|
|
||||||
env = dict(os.environ)
|
|
||||||
env["GIT_SSH_COMMAND"] = r'"C:\Program Files\Git\usr\bin\ssh.exe"'
|
|
||||||
push = subprocess.run([GIT, "push", "origin", "main"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace", env=env)
|
|
||||||
for line in (push.stderr or push.stdout or "").strip().splitlines()[-2:]:
|
|
||||||
print(" " + line)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
add_stage()
|
|
||||||
commit()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''把安装包相关的检查加进全量套件,然后提交。
|
|
||||||
|
|
||||||
新增三个阶段(都是这轮踩坑后补的):
|
|
||||||
* .iss 语法陷阱检查 —— 注释里的 [区段] / {常量} 会让编译失败(踩了三次)
|
|
||||||
* 卸载逻辑检查 —— 自杀、stdio、input 阻塞、keep_dir
|
|
||||||
* 安装包周期检查 —— 装/卸完整走一遍(需要管理员,失败不算硬性)
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
VERIFY = ROOT / "_tests" / "verify_all.ps1"
|
|
||||||
GIT = r"C:\Program Files\Git\bin\git.exe"
|
|
||||||
|
|
||||||
NEW_STAGES = '''
|
|
||||||
# ---------------------------------------------------------------- 3b3
|
|
||||||
Banner "3b3/7 安装包脚本语法(Inno 的注释陷阱)"
|
|
||||||
# 踩过的坑:Inno 的 [Code] 解析器**连注释内容也处理**。
|
|
||||||
# 注释里写 [区段名] 报"无效的区段标记",写 {常量} 报 "Syntax error",
|
|
||||||
# 都是直接编译失败。这个检查专门拦这类。
|
|
||||||
$stageOut = & python (Join-Path $root "_build\\check_iss.py") 2>&1
|
|
||||||
Show-Fails $stageOut
|
|
||||||
Record "安装包脚本语法" ($LASTEXITCODE -eq 0)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 3b4
|
|
||||||
Banner "3b4/7 卸载逻辑(不能杀自己 / 不能阻塞)"
|
|
||||||
# 踩过的坑:
|
|
||||||
# 1. taskkill /IM ykxmz.exe 把正在卸载的进程自己杀了 -> 删自启没执行
|
|
||||||
# 2. Popen 不重定向 stdio -> capture_output 等管道 EOF -> "卡死"
|
|
||||||
# 3. input() 在无 stdin 时永久阻塞 -> 控制面板卸载卡死
|
|
||||||
# 4. keep_dir 缺失 -> 自删和 Inno 卸载器抢目录
|
|
||||||
$stageOut = & python (Join-Path $root "_build\\test_uninstall_logic.py") 2>&1
|
|
||||||
Show-Fails $stageOut
|
|
||||||
Record "卸载逻辑" ($LASTEXITCODE -eq 0)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
COMMIT_MSG = """修四个安装/卸载卡死的 bug(都实测复现过)
|
|
||||||
|
|
||||||
1. 卸载里 input() 在无 stdin 时永久阻塞
|
|
||||||
从控制面板卸载**永远卡住不结束**。安装包的 [UninstallRun]
|
|
||||||
用 runhidden 调用,没有 stdin,input() 就一直等。
|
|
||||||
改成只在 sys.stdin.isatty() 时才等回车。
|
|
||||||
|
|
||||||
2. Popen 没重定向子进程 stdio,导致 capture_output 等管道 EOF
|
|
||||||
_schedule_self_delete 起的 cmd 继承了父进程的 stdout/stderr 句柄,
|
|
||||||
任何用 capture_output 调 --uninstall 的地方都会一直等,
|
|
||||||
表现为"程序卡死"(实测 90 秒不返回,改后 0.2 秒)。
|
|
||||||
Popen 加 stdin/stdout/stderr=DEVNULL。
|
|
||||||
顺带把延迟从 timeout 换成 ping —— timeout 在 DETACHED_PROCESS
|
|
||||||
下没有控制台会立刻失败,等于没延迟直接删。
|
|
||||||
|
|
||||||
3. _kill_running 用 taskkill /IM ykxmz.exe 把**自己**杀了
|
|
||||||
--uninstall 本身就跑在 ykxmz.exe 里,这条命令一发,
|
|
||||||
后面的"删开机自启""删数据目录"全都没执行。
|
|
||||||
现象:卸载看着完成了,HKCU 的自启注册表项还留着。
|
|
||||||
改成 /FI "PID ne <self>" 排除自己。
|
|
||||||
|
|
||||||
4. 安装器关不掉以管理员身份运行的旧实例 -> 整个安装回滚
|
|
||||||
Inno 自带机制只会"请求"对方退出,遇上常驻的服务进程关不掉,
|
|
||||||
弹"无法自动关闭所有应用程序",静默模式下被压成"终止",
|
|
||||||
安装直接回滚 —— 用户看到的是"装了没反应/还是旧版本"。
|
|
||||||
改成 PrepareToInstall 里自己 taskkill /F,关掉 Inno 那套。
|
|
||||||
|
|
||||||
另外修了两个卸载残留:
|
|
||||||
* 安装目录留个空壳 -> [UninstallDelete] 加 dirifempty
|
|
||||||
* ProgramData\\ykxmz 没删 -> 加进 [UninstallDelete]
|
|
||||||
|
|
||||||
还有一处 Inno 写法陷阱(踩了三次):
|
|
||||||
注释里写 [区段名] 或 {常量} 会让编译直接失败,
|
|
||||||
因为 [Code] 解析器连注释内容也处理。
|
|
||||||
加了 _build/check_iss.py 专门扫这个,并进全量套件。
|
|
||||||
|
|
||||||
实测结果:
|
|
||||||
静默安装 5 秒内退出,退出码 0,配对码正确写入,程序自动跑起来
|
|
||||||
静默卸载 0.7 秒退出(原来无限卡住)
|
|
||||||
全量回归 29 项全通过
|
|
||||||
卸载逻辑 11 项全通过
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def add_stages() -> None:
|
|
||||||
raw = VERIFY.read_bytes()
|
|
||||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
|
|
||||||
if "check_iss.py" in text:
|
|
||||||
print(" 套件里已有安装包相关阶段")
|
|
||||||
return
|
|
||||||
|
|
||||||
anchor = "# ---------------------------------------------------------------- 3b2"
|
|
||||||
if anchor not in text:
|
|
||||||
# 退而求其次:放在"3c"之前
|
|
||||||
anchor = "# ---------------------------------------------------------------- 3c"
|
|
||||||
if anchor not in text:
|
|
||||||
print(" [失败] 找不到插入锚点")
|
|
||||||
return
|
|
||||||
|
|
||||||
text = text.replace(anchor, NEW_STAGES.lstrip("\n") + anchor, 1)
|
|
||||||
VERIFY.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
|
|
||||||
+ text.replace("\r\n", "\n").encode("utf-8"))
|
|
||||||
print(" 已加入两个阶段(安装包脚本语法 / 卸载逻辑)")
|
|
||||||
|
|
||||||
|
|
||||||
def commit() -> None:
|
|
||||||
print()
|
|
||||||
print(" 提交")
|
|
||||||
subprocess.run([GIT, "add", "-A"], cwd=str(ROOT), check=False)
|
|
||||||
msg_file = ROOT / "_build" / "_cmsg2.txt"
|
|
||||||
msg_file.write_bytes(COMMIT_MSG.encode("utf-8"))
|
|
||||||
r = subprocess.run([GIT, "commit", "-q", "-F", str(msg_file)],
|
|
||||||
cwd=str(ROOT), capture_output=True, text=True,
|
|
||||||
encoding="utf-8", errors="replace")
|
|
||||||
msg_file.unlink(missing_ok=True)
|
|
||||||
print(" commit exit=%s" % r.returncode)
|
|
||||||
|
|
||||||
show = subprocess.run([GIT, "log", "--oneline", "-1"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace")
|
|
||||||
print(" " + (show.stdout or "").strip())
|
|
||||||
|
|
||||||
env = dict(os.environ)
|
|
||||||
env["GIT_SSH_COMMAND"] = r'"C:\Program Files\Git\usr\bin\ssh.exe"'
|
|
||||||
push = subprocess.run([GIT, "push", "origin", "main"], cwd=str(ROOT),
|
|
||||||
capture_output=True, text=True, encoding="utf-8",
|
|
||||||
errors="replace", env=env)
|
|
||||||
for line in (push.stderr or push.stdout or "").strip().splitlines()[-2:]:
|
|
||||||
print(" " + line)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
add_stages()
|
|
||||||
commit()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''让 deploy.sh 在装新机器时把「监测单元」也装上。
|
|
||||||
|
|
||||||
== 为什么必须做这一步 ==
|
|
||||||
|
|
||||||
监测单元是我新加的:monitor.sh 每 60 秒查一次公网,
|
|
||||||
挂了就叫 rescue.sh 抢救。但如果 deploy.sh 不认识它,
|
|
||||||
**新装的机器上监测根本不会启动** —— 而且不会报任何错,
|
|
||||||
只是静默地不跑。等网站真挂了才发现"预案没触发",那就晚了。
|
|
||||||
|
|
||||||
部署脚本里 watchdog 已经是个现成的样板(同样是 timer + oneshot),
|
|
||||||
照着加一份即可。
|
|
||||||
|
|
||||||
== 顺便 ==
|
|
||||||
|
|
||||||
deploy.sh 里本来就有"包里有才装"的写法(`if [ -f ... ]`),
|
|
||||||
这里也沿用 —— 老的服务端 zip(没有 monitor.sh)重跑部署时
|
|
||||||
不会因为找不到文件而报错。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
DEPLOY = ROOT / "relay" / "deploy.sh"
|
|
||||||
|
|
||||||
ANCHOR = ''' systemctl enable lanremote-watchdog.timer >/dev/null 2>&1 || true'''
|
|
||||||
|
|
||||||
NEW = ''' systemctl enable lanremote-watchdog.timer >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---- 网站监测单元(如果包里有)----
|
|
||||||
#
|
|
||||||
# 和看门狗的区别很重要:
|
|
||||||
# 看门狗 看**隧道连接数** —— 本地指标,判断"隧道自己好不好"
|
|
||||||
# 监测单元 看**公网 HTTP** —— 用户视角,判断"用户能不能打开"
|
|
||||||
#
|
|
||||||
# 两个都要。实测过:隧道两条实例都掉了,本机 curl 127.0.0.1 照样 200,
|
|
||||||
# 看门狗也可能觉得没事;只有从公网测才发现 502。
|
|
||||||
if [ -f "$RELAY_DIR/monitor.sh" ]; then
|
|
||||||
mkdir -p "$RELAY_DIR/rescue_reports"
|
|
||||||
chmod +x "$RELAY_DIR/monitor.sh" "$RELAY_DIR/rescue.sh" 2>/dev/null || true
|
|
||||||
if [ ! -f "$RELAY_DIR/rescue_knowledge.md" ]; then
|
|
||||||
cat > "$RELAY_DIR/rescue_knowledge.md" <<'KEOF'
|
|
||||||
# 紧急预案知识库
|
|
||||||
|
|
||||||
每次自动抢救成功,这里会追加一条「第几级奏效」。
|
|
||||||
以后遇到同类症状,监测单元会优先试历史上奏效的那一级。
|
|
||||||
|
|
||||||
分级(从代价最小到最大):
|
|
||||||
|
|
||||||
1 重启隧道 不动中转,在线设备不掉线
|
|
||||||
2 重启中转服务 设备要重连,但很快
|
|
||||||
3 重新施加网络调优 MSS / MTU 跑偏时用
|
|
||||||
4 重启无线网卡 4G / WiFi 卡死时用
|
|
||||||
5 重启整机 最后的底牌,默认不开(要 RESCUE_REBOOT=1)
|
|
||||||
|
|
||||||
KEOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 包里直接带了单元文件就用包里的(内容随版本更新);
|
|
||||||
# 没有就现场生成一份等价的 —— 兼容老 zip。
|
|
||||||
if [ -f "$RELAY_DIR/lanremote-monitor.service" ]; then
|
|
||||||
cp "$RELAY_DIR/lanremote-monitor.service" "$UNIT_DIR/" 2>/dev/null || true
|
|
||||||
else
|
|
||||||
write_unit "$UNIT_DIR/lanremote-monitor.service" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=ykxmz 网站监测单元(每 60 秒检查公网,异常时启动紧急预案)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
TimeoutStartSec=360
|
|
||||||
ExecStart=/bin/bash $RELAY_DIR/monitor.sh
|
|
||||||
Environment=RESCUE_REBOOT=0
|
|
||||||
Environment=FAIL_THRESHOLD=3
|
|
||||||
Environment=COOLDOWN=600
|
|
||||||
StandardOutput=append:$RELAY_DIR/monitor.log
|
|
||||||
StandardError=append:$RELAY_DIR/monitor.log
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
if [ -f "$RELAY_DIR/lanremote-monitor.timer" ]; then
|
|
||||||
cp "$RELAY_DIR/lanremote-monitor.timer" "$UNIT_DIR/" 2>/dev/null || true
|
|
||||||
else
|
|
||||||
write_unit "$UNIT_DIR/lanremote-monitor.timer" <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=每 60 秒检查一次网站是否可用
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=90s
|
|
||||||
OnUnitActiveSec=60s
|
|
||||||
AccuracySec=5s
|
|
||||||
Unit=lanremote-monitor.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
||||||
systemctl enable lanremote-monitor.timer >/dev/null 2>&1 || true
|
|
||||||
systemctl start lanremote-monitor.timer >/dev/null 2>&1 || true
|
|
||||||
say " 网站监测单元已启用(每 60 秒查一次公网)"'''
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
raw = DEPLOY.read_bytes()
|
|
||||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
|
|
||||||
if "lanremote-monitor.timer" in text:
|
|
||||||
print(" · deploy.sh 已经加过了")
|
|
||||||
return 0
|
|
||||||
if ANCHOR not in text:
|
|
||||||
print(" ✗ 找不到锚点")
|
|
||||||
return 1
|
|
||||||
if text.count(ANCHOR) != 1:
|
|
||||||
print(" ! 锚点出现 %d 次" % text.count(ANCHOR))
|
|
||||||
text = text.replace(ANCHOR, NEW, 1)
|
|
||||||
|
|
||||||
# 原来 watchdog 那个 if 块的 fi 会被我们的 NEW 提前收掉,
|
|
||||||
# 所以要把后面多余的那个 fi 去掉一个。先看看形状。
|
|
||||||
import re
|
|
||||||
DEPLOY.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
|
||||||
+ text.encode("utf-8"))
|
|
||||||
print(" ✓ deploy.sh 加了监测单元的安装")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''把「未调用方法」检查加进全量套件,然后把版本升到 1.7.1。
|
|
||||||
|
|
||||||
为什么加这个阶段:
|
|
||||||
1.7.0 交付后是白屏 —— initTabs() 漏调,编译能过、别的测试也能过,
|
|
||||||
只有真机跑起来才看得见。加进套件就不靠运气了。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
VERIFY = ROOT / "_tests" / "verify_all.ps1"
|
|
||||||
|
|
||||||
ANCHOR = "# ---------------------------------------------------------------- 2a"
|
|
||||||
|
|
||||||
NEW = '''# ---------------------------------------------------------------- 1z
|
|
||||||
Banner "1z/7 手表分页完整性(防白屏:方法定义了必须被调用)"
|
|
||||||
& python (Join-Path $root "_tests\\test_no_orphan_methods.py") 2>&1 |
|
|
||||||
Select-Object -Last 6
|
|
||||||
Record "手表分页完整性" ($LASTEXITCODE -eq 0)
|
|
||||||
|
|
||||||
''' + ANCHOR
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
raw = VERIFY.read_bytes()
|
|
||||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
|
|
||||||
if "test_no_orphan_methods" in text:
|
|
||||||
print(" 套件里已经有这个阶段")
|
|
||||||
elif ANCHOR in text:
|
|
||||||
text = text.replace(ANCHOR, NEW, 1)
|
|
||||||
VERIFY.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
|
|
||||||
+ text.replace("\r\n", "\n").encode("utf-8"))
|
|
||||||
print(" 已加「手表分页完整性」阶段")
|
|
||||||
else:
|
|
||||||
print(" [失败] 锚点没找到")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# 升版本 1.7.0 -> 1.7.1
|
|
||||||
proc = __import__("subprocess").run(
|
|
||||||
[sys.executable, str(ROOT / "_build" / "bump_version_generic.py"),
|
|
||||||
"1.7.0", "1.7.1"],
|
|
||||||
cwd=str(ROOT), capture_output=True, text=True,
|
|
||||||
encoding="utf-8", errors="replace")
|
|
||||||
print(proc.stdout[-900:])
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''后台加一个「抢救报告」分区。
|
|
||||||
|
|
||||||
用户要求:
|
|
||||||
「如果紧急预案未能解决,则将问题打包成 .txt,然后在后台加一个分区
|
|
||||||
专门放还有下载这个 txt」
|
|
||||||
|
|
||||||
rescue.sh 抢救失败时会往 relay/rescue_reports/ 写一份诊断报告 .txt。
|
|
||||||
这里让后台能:
|
|
||||||
* 列出这些报告(时间、大小、一句话结论)
|
|
||||||
* 点一下下载看内容
|
|
||||||
|
|
||||||
== 为什么不复用 /download/ ==
|
|
||||||
|
|
||||||
/download/ 是**给用户**的(安装包、APK),带 cf 长缓存
|
|
||||||
(max-age=604800, immutable)——因为文件名带版本号、内容永不改变。
|
|
||||||
|
|
||||||
抢救报告正相反:内容会变、是给管理员看的、不该被边缘缓存。
|
|
||||||
所以单独开一条 /rescue/<名字>,发 no-store。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import html
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
RELAY = ROOT / "relay" / "relay.py"
|
|
||||||
ADMIN = ROOT / "relay" / "admin.py"
|
|
||||||
|
|
||||||
|
|
||||||
def patch(path: Path, pairs, label: str):
|
|
||||||
raw = path.read_bytes()
|
|
||||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
results = []
|
|
||||||
for old, new, what in pairs:
|
|
||||||
if old not in text:
|
|
||||||
results.append(("✗", what, "找不到锚点"))
|
|
||||||
continue
|
|
||||||
if text.count(old) != 1:
|
|
||||||
results.append(("!", what, "锚点 %d 处" % text.count(old)))
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
results.append(("✓", what, ""))
|
|
||||||
print(" ── %s ──" % label)
|
|
||||||
for m, w, why in results:
|
|
||||||
print(" %s %s%s" % (m, w, (" —— " + why) if why else ""))
|
|
||||||
if any(m == "✗" for m, _w, _y in results):
|
|
||||||
return False
|
|
||||||
path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
|
|
||||||
+ text.encode("utf-8"))
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# relay.py
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
RELAY_LIST = '''
|
|
||||||
# 抢救报告目录。rescue.sh 抢救失败时往这里写 .txt 诊断报告。
|
|
||||||
# 和 downloads/ 分开:那个是给用户的安装包(带长缓存),
|
|
||||||
# 这个是给管理员看的诊断(必须不缓存)。
|
|
||||||
RESCUE_DIR = Path(__file__).resolve().parent / "rescue_reports"
|
|
||||||
RESCUE_KEEP = 30 # 只保留最近 30 份(460MB 的小机器,别塞满)
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_rescue_name(name: str) -> str:
|
|
||||||
"""校验抢救报告文件名。只允许 <时间戳>.txt 这种形状。"""
|
|
||||||
name = (name or "").strip()
|
|
||||||
if not name or len(name) > 80:
|
|
||||||
return ""
|
|
||||||
if "/" in name or "\\\\" in name or ".." in name:
|
|
||||||
return ""
|
|
||||||
if not name.endswith(".txt"):
|
|
||||||
return ""
|
|
||||||
for ch in name:
|
|
||||||
if ch.isalnum() or ch in "._-":
|
|
||||||
continue
|
|
||||||
return ""
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def _list_rescue_reports() -> list:
|
|
||||||
"""列出抢救报告(按时间倒序)。
|
|
||||||
|
|
||||||
顺带读每个文件的前几行,抽出结论 —— 后台列表上直接显示,
|
|
||||||
不用一个个点进去看。
|
|
||||||
"""
|
|
||||||
items = []
|
|
||||||
try:
|
|
||||||
if not RESCUE_DIR.is_dir():
|
|
||||||
return items
|
|
||||||
for entry in sorted(RESCUE_DIR.glob("*.txt"), reverse=True):
|
|
||||||
if not entry.is_file():
|
|
||||||
continue
|
|
||||||
safe = _safe_rescue_name(entry.name)
|
|
||||||
if not safe:
|
|
||||||
continue
|
|
||||||
stat = entry.stat()
|
|
||||||
summary = ""
|
|
||||||
try:
|
|
||||||
with entry.open("r", encoding="utf-8", errors="replace") as fh:
|
|
||||||
head = fh.read(1200)
|
|
||||||
for line in head.splitlines():
|
|
||||||
s = line.strip()
|
|
||||||
if s.startswith("判断:"):
|
|
||||||
summary = s.replace("判断:", "").strip()
|
|
||||||
break
|
|
||||||
if not summary:
|
|
||||||
for line in head.splitlines():
|
|
||||||
s = line.strip()
|
|
||||||
if s.startswith("公网 ") and "->" in s:
|
|
||||||
summary = s
|
|
||||||
break
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
items.append({
|
|
||||||
"name": entry.name,
|
|
||||||
"size": stat.st_size,
|
|
||||||
"mtime": stat.st_mtime,
|
|
||||||
"summary": summary[:120],
|
|
||||||
})
|
|
||||||
except OSError:
|
|
||||||
return items
|
|
||||||
items.sort(key=lambda i: i["mtime"], reverse=True)
|
|
||||||
return items[:RESCUE_KEEP]
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
ADMIN_CALL = ''' admin_path=ADMIN_PATH,'''
|
|
||||||
|
|
||||||
ADMIN_CALL_NEW = ''' admin_path=ADMIN_PATH,
|
|
||||||
rescue_fn=_list_rescue_reports,'''
|
|
||||||
|
|
||||||
# 下载路由(GET):插在 /download/ 之前
|
|
||||||
RESCUE_ROUTE_ANCHOR = ''' if path.startswith("/download/"):
|
|
||||||
wanted = urllib.parse.unquote(path[len("/download/"):])
|
|
||||||
safe = _safe_download_name(wanted)'''
|
|
||||||
|
|
||||||
RESCUE_ROUTE = ''' if path.startswith("/rescue/"):
|
|
||||||
# 抢救报告下载。单独一条路由,**不发缓存头** ——
|
|
||||||
# 内容会变,而且不该让 Cloudflare 边缘留着。
|
|
||||||
wanted = urllib.parse.unquote(path[len("/rescue/"):])
|
|
||||||
safe = _safe_rescue_name(wanted)
|
|
||||||
if not safe:
|
|
||||||
self._json(400, {"ok": False, "error": "文件名不合法"})
|
|
||||||
return
|
|
||||||
target = RESCUE_DIR / safe
|
|
||||||
if not target.is_file():
|
|
||||||
self._json(404, {"ok": False, "error": "没有这份报告"})
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
body = target.read_bytes()
|
|
||||||
except OSError as exc:
|
|
||||||
self._json(500, {"ok": False, "error": f"读不到: {exc}"})
|
|
||||||
return
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
self.send_header("Content-Length", str(len(body)))
|
|
||||||
quoted = urllib.parse.quote(safe)
|
|
||||||
self.send_header(
|
|
||||||
"Content-Disposition",
|
|
||||||
f"attachment; filename*=UTF-8''{quoted}")
|
|
||||||
self.send_header("Cache-Control", "no-store")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
return
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# admin.py
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
ADMIN_SIG = '''def render_admin(snap: dict, public: str, admin_path: str, codes: int,
|
|
||||||
downloads_fn) -> str:'''
|
|
||||||
|
|
||||||
ADMIN_SIG_NEW = '''def render_admin(snap: dict, public: str, admin_path: str, codes: int,
|
|
||||||
downloads_fn, rescue_fn=None) -> str:'''
|
|
||||||
|
|
||||||
ADMIN_CARD_ANCHOR = ''' <div class="tool">
|
|
||||||
<h4>下载区</h4>
|
|
||||||
<div class="d">官网下载按钮指向这里的文件。</div>
|
|
||||||
<div id="dlBody">%(dl)s</div>
|
|
||||||
</div>
|
|
||||||
</div>'''
|
|
||||||
|
|
||||||
ADMIN_CARD = ''' <div class="tool">
|
|
||||||
<h4>下载区</h4>
|
|
||||||
<div class="d">官网下载按钮指向这里的文件。</div>
|
|
||||||
<div id="dlBody">%(dl)s</div>
|
|
||||||
</div>
|
|
||||||
<div class="tool">
|
|
||||||
<h4>抢救报告</h4>
|
|
||||||
<div class="d">网站挂了、自动预案也没救回来时,机器会把现场
|
|
||||||
打包成一份 txt 放在这里。点开看看是哪一段断了。</div>
|
|
||||||
<div id="rescueBody">%(rescue)s</div>
|
|
||||||
</div>
|
|
||||||
</div>'''
|
|
||||||
|
|
||||||
|
|
||||||
def _rescue_rows(items: list) -> str:
|
|
||||||
"""抢救报告列表的 HTML。照 dl_rows 的风格(页面里直接渲染)。"""
|
|
||||||
if not items:
|
|
||||||
return ('<div class="out">还没有报告 —— 说明网站一直没出过'
|
|
||||||
'「自动救不回来」的问题。</div>')
|
|
||||||
import time as _t
|
|
||||||
import urllib.parse as _up
|
|
||||||
rows = []
|
|
||||||
for it in items:
|
|
||||||
when = _t.strftime("%m-%d %H:%M", _t.localtime(it["mtime"]))
|
|
||||||
size = it["size"] / 1024
|
|
||||||
size_s = ("%.1f KB" % size) if size >= 1 else ("%d B" % it["size"])
|
|
||||||
summary = html.escape(it.get("summary") or "(没有结论行)")
|
|
||||||
href = "/rescue/" + _up.quote(it["name"])
|
|
||||||
rows.append(
|
|
||||||
'<div class="dlrow"><span class="nm">'
|
|
||||||
'<a href="%s" style="color:#0a84ff">%s</a>'
|
|
||||||
'<span style="color:#6b7280;font-size:12px;margin-left:8px">'
|
|
||||||
'%s</span></span>'
|
|
||||||
'<span class="sz" style="max-width:46%%;text-align:right">'
|
|
||||||
'%s</span></div>'
|
|
||||||
% (href, when, size_s, summary))
|
|
||||||
return "".join(rows)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ok = True
|
|
||||||
|
|
||||||
ok &= patch(RELAY, [
|
|
||||||
("DOWNLOADS_DIR = Path(__file__).resolve().parent / \"downloads\"",
|
|
||||||
"DOWNLOADS_DIR = Path(__file__).resolve().parent / \"downloads\"\n"
|
|
||||||
+ RELAY_LIST, "加 rescue_reports 列表函数"),
|
|
||||||
(RESCUE_ROUTE_ANCHOR, RESCUE_ROUTE + RESCUE_ROUTE_ANCHOR,
|
|
||||||
"加 /rescue/ 下载路由(no-store)"),
|
|
||||||
(ADMIN_CALL, ADMIN_CALL_NEW, "把 rescue_fn 传给后台页"),
|
|
||||||
], "relay.py")
|
|
||||||
|
|
||||||
ok &= patch(ADMIN, [
|
|
||||||
(ADMIN_SIG, ADMIN_SIG_NEW, "render_admin 加 rescue_fn 参数"),
|
|
||||||
(ADMIN_CARD_ANCHOR, ADMIN_CARD, "后台加「抢救报告」卡片"),
|
|
||||||
# 算报告列表(照 dl_rows 的写法)
|
|
||||||
(''' return """<!doctype html>''',
|
|
||||||
''' # 抢救报告列表(和下载区同样在页面里直接渲染,不用 JS 拉)
|
|
||||||
try:
|
|
||||||
rescue_items = rescue_fn() if rescue_fn else []
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
rescue_items = []
|
|
||||||
rescue_rows = _rescue_rows(rescue_items)
|
|
||||||
|
|
||||||
return """<!doctype html>''',
|
|
||||||
"算抢救报告列表"),
|
|
||||||
('"dl": dl_rows,', '"dl": dl_rows,\n "rescue": rescue_rows,',
|
|
||||||
"把报告列表塞进模板"),
|
|
||||||
], "admin.py")
|
|
||||||
|
|
||||||
return 0 if ok else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''让「双击 exe」能打开设置窗口,并能设中转服务器。
|
|
||||||
|
|
||||||
== 用户报的两个问题 ==
|
|
||||||
|
|
||||||
问题 1:「被控端再安装时要能设置中转服务器,这是第一次设置」
|
|
||||||
—— SetupWindow 只有配对码/开机自启/放行防火墙,**没有中转**。
|
|
||||||
|
|
||||||
问题 2:「后面要想设置就要去点击 exe 去改配对码和开关新设备连接
|
|
||||||
还有中转服务器,现在如果再运行双击 exe 没反应,没运行也一样」
|
|
||||||
|
|
||||||
看 run_agent.py 的 main() 最后几行:
|
|
||||||
|
|
||||||
if not store.get("setup_done", False):
|
|
||||||
return run_setup(store)
|
|
||||||
# 已经设置过:直接静默,屏幕上什么都不出现
|
|
||||||
return run_silent(store, None)
|
|
||||||
|
|
||||||
—— 装完之后双击**永远静默**,什么窗口都不弹。
|
|
||||||
而且如果已经有一个实例在跑,InstanceLock 会直接 return,
|
|
||||||
连日志都不输出。
|
|
||||||
|
|
||||||
== 改法 ==
|
|
||||||
|
|
||||||
1. SetupWindow 加一个 mode:
|
|
||||||
mode="first" 第一次设置(保持老行为:防火墙+自启+转静默)
|
|
||||||
mode="settings" 已装好,只改配置
|
|
||||||
|
|
||||||
2. 窗口加两样东西:
|
|
||||||
* 中转服务器地址(可不填;只在一个 WiFi 里用就不需要)
|
|
||||||
* 允许新设备接入(开关)
|
|
||||||
|
|
||||||
3. main() 改成:
|
|
||||||
--setup -> settings 模式
|
|
||||||
--silent -> 静默(开机自启/安装包用,行为不变)
|
|
||||||
没参数 + 没设置过 -> first 模式
|
|
||||||
没参数 + 已经设置过 -> **settings 模式**(不再是静默)
|
|
||||||
|
|
||||||
4. 已经在跑的时候:不再"直接 exit 什么都不显示",而是
|
|
||||||
提示一句"被控端正在后台运行",然后**照样打开设置窗口**。
|
|
||||||
保存后重启后台实例让设置生效。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
UI = ROOT / "windows" / "lanremote" / "ui.py"
|
|
||||||
RUN = ROOT / "windows" / "run_agent.py"
|
|
||||||
|
|
||||||
|
|
||||||
def patch(path: Path, pairs, label: str) -> bool:
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
for old, new in pairs:
|
|
||||||
if old not in text:
|
|
||||||
print(" ✗ %s:找不到锚点 %r" % (label, old[:60]))
|
|
||||||
return False
|
|
||||||
if text.count(old) != 1:
|
|
||||||
print(" ! %s:锚点出现 %d 次" % (label, text.count(old)))
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
path.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print(" ✓ %s" % label)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ok = True
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 一、ui.py:加 mode + 中转字段 + 接入开关
|
|
||||||
# ================================================================
|
|
||||||
print("── ui.py ──")
|
|
||||||
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' def __init__(self, store, suggested: str = ""):
|
|
||||||
self.store = store
|
|
||||||
self.suggested = (suggested or store.pairing_code()
|
|
||||||
or store.new_pairing_code())
|
|
||||||
self.result: dict | None = None''',
|
|
||||||
''' def __init__(self, store, suggested: str = "",
|
|
||||||
mode: str = "first"):
|
|
||||||
"""mode:
|
|
||||||
"first" 第一次设置(走防火墙 + 自启 + 转静默)
|
|
||||||
"settings" 已装好,只改配置(配对码/中转/接入开关)
|
|
||||||
"""
|
|
||||||
self.store = store
|
|
||||||
self.mode = mode if mode in ("first", "settings") else "first"
|
|
||||||
self.suggested = (suggested or store.pairing_code()
|
|
||||||
or store.new_pairing_code())
|
|
||||||
self.result: dict | None = None'''
|
|
||||||
)], "SetupWindow 加 mode")
|
|
||||||
|
|
||||||
# 标题按模式分
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' self.root.title("ykxmz · 第一次设置")''',
|
|
||||||
''' self.root.title("ykxmz · " + (
|
|
||||||
"第一次设置" if self.mode == "first" else "设置"))'''
|
|
||||||
)], "窗口标题按模式分")
|
|
||||||
|
|
||||||
# 副标题
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' tk.Label(titles, text="远程控制 · 第一次设置", bg=BG, fg=DIM,
|
|
||||||
font=FONT_SUB).pack(anchor="w")''',
|
|
||||||
''' tk.Label(titles,
|
|
||||||
text=("远程控制 · 第一次设置" if self.mode == "first"
|
|
||||||
else "远程控制 · 设置"),
|
|
||||||
bg=BG, fg=DIM,
|
|
||||||
font=FONT_SUB).pack(anchor="w")'''
|
|
||||||
)], "副标题按模式分")
|
|
||||||
|
|
||||||
# 在配对码卡片之后插入「中转服务器」卡片
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' self.status = tk.Label(inner, text="", bg=CARD, fg=DANGER,
|
|
||||||
font=FONT_SMALL, wraplength=330,
|
|
||||||
justify="left", anchor="w")
|
|
||||||
self.status.pack(fill="x", pady=(6, 0))
|
|
||||||
''',
|
|
||||||
''' self.status = tk.Label(inner, text="", bg=CARD, fg=DANGER,
|
|
||||||
font=FONT_SMALL, wraplength=330,
|
|
||||||
justify="left", anchor="w")
|
|
||||||
self.status.pack(fill="x", pady=(6, 0))
|
|
||||||
|
|
||||||
# ================= 中转服务器卡片 =================
|
|
||||||
# 用户要求:安装时要能设中转服务器;装完之后双击 exe 也能回来改。
|
|
||||||
#
|
|
||||||
# 为什么不填也能用:手表和电脑在同一个 WiFi 时走局域网直连,
|
|
||||||
# 根本不需要中转。只有"在办公室管教室"这种跨网络场景才要填。
|
|
||||||
relay_card = self._card(page)
|
|
||||||
relay_card.pack(fill="x", pady=(12, 0))
|
|
||||||
relay_inner = tk.Frame(relay_card.winfo_children()[0], bg=CARD)
|
|
||||||
relay_inner.pack(fill="x", padx=16, pady=14)
|
|
||||||
|
|
||||||
tk.Label(relay_inner, text="中转服务器(可以不填)", bg=CARD, fg=FG,
|
|
||||||
font=FONT_LABEL).pack(anchor="w")
|
|
||||||
|
|
||||||
self.relay_entry = tk.Entry(
|
|
||||||
relay_inner, bg="#f6f8fa", fg=FG, relief="flat",
|
|
||||||
font=FONT_SUB, insertbackground=ACCENT)
|
|
||||||
self.relay_entry.pack(fill="x", pady=(8, 0), ipady=6)
|
|
||||||
_saved_relay = str(self.store.get("relay_url", "") or "").strip()
|
|
||||||
if _saved_relay:
|
|
||||||
self.relay_entry.insert(0, _saved_relay)
|
|
||||||
|
|
||||||
tk.Label(relay_inner,
|
|
||||||
text="手表和电脑在同一个 WiFi 时不需要填 —— 会直接走"
|
|
||||||
"局域网,几十毫秒。\\n"
|
|
||||||
"要在别的地方(比如办公室)管这台电脑时才填,"
|
|
||||||
"填你自己那台中转服务器的地址。",
|
|
||||||
bg=CARD, fg=MUTED, font=FONT_SMALL, justify="left",
|
|
||||||
wraplength=396).pack(anchor="w", pady=(8, 0))
|
|
||||||
|
|
||||||
self.relay_status = tk.Label(relay_inner, text="", bg=CARD,
|
|
||||||
fg=DANGER, font=FONT_SMALL,
|
|
||||||
wraplength=396, justify="left",
|
|
||||||
anchor="w")
|
|
||||||
self.relay_status.pack(fill="x", pady=(6, 0))
|
|
||||||
'''
|
|
||||||
)], "插入中转服务器卡片")
|
|
||||||
|
|
||||||
# 选项卡片里加「允许新设备接入」(只在 settings 模式显示)
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' self.var_firewall = tk.BooleanVar(value=True)
|
|
||||||
self._check(opt_inner, "放行防火墙", self.var_firewall,
|
|
||||||
"会弹一次管理员确认,然后黑窗口一闪而过")''',
|
|
||||||
''' # 「允许新设备接入」只在设置模式给 ——
|
|
||||||
# 第一次设置时当然是允许的(不然手表连不上)。
|
|
||||||
if self.mode == "settings":
|
|
||||||
self.var_allow_new = tk.BooleanVar(
|
|
||||||
value=bool(self.store.get("allow_new_pairing", True)))
|
|
||||||
self._check(opt_inner, "允许新设备接入", self.var_allow_new,
|
|
||||||
"关掉之后,只有已经配对过的设备能连上;"
|
|
||||||
"新设备会被拒绝")
|
|
||||||
else:
|
|
||||||
self.var_allow_new = None
|
|
||||||
|
|
||||||
self.var_firewall = tk.BooleanVar(
|
|
||||||
value=(self.mode == "first"))
|
|
||||||
if self.mode == "first":
|
|
||||||
self._check(opt_inner, "放行防火墙", self.var_firewall,
|
|
||||||
"会弹一次管理员确认,然后黑窗口一闪而过")'''
|
|
||||||
)], "加「允许新设备接入」选项")
|
|
||||||
|
|
||||||
# 结果里带上 relay / allow_new
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' self.result = {
|
|
||||||
"code": cleaned,
|
|
||||||
"autostart": bool(self.var_auto.get()),
|
|
||||||
"firewall": bool(self.var_firewall.get()),
|
|
||||||
}''',
|
|
||||||
''' # 中转地址:允许空(空 = 只走局域网)。
|
|
||||||
# 填了的话做一次最基本的格式检查,免得用户把域名打错还找不到原因。
|
|
||||||
relay = self.relay_entry.get().strip()
|
|
||||||
if relay:
|
|
||||||
if not relay.startswith(("http://", "https://")):
|
|
||||||
relay = "https://" + relay
|
|
||||||
# 只留 协议://主机[:端口],去掉多余路径
|
|
||||||
_m = re.match(r"^(https?://[^/\\s]+)", relay)
|
|
||||||
if not _m:
|
|
||||||
self._set_relay_status("中转地址看着不对,检查一下", DANGER)
|
|
||||||
return
|
|
||||||
relay = _m.group(1).rstrip("/")
|
|
||||||
self.result = {
|
|
||||||
"code": cleaned,
|
|
||||||
"autostart": bool(self.var_auto.get()),
|
|
||||||
"firewall": bool(self.var_firewall.get()),
|
|
||||||
"relay": relay,
|
|
||||||
"allow_new": (None if self.var_allow_new is None
|
|
||||||
else bool(self.var_allow_new.get())),
|
|
||||||
}'''
|
|
||||||
)], "结果带 relay / allow_new")
|
|
||||||
|
|
||||||
# 需要一个 _set_relay_status
|
|
||||||
ok &= patch(UI, [(
|
|
||||||
''' def _cancel(self) -> None:
|
|
||||||
self.result = None''',
|
|
||||||
''' def _set_relay_status(self, text: str, color: str) -> None:
|
|
||||||
try:
|
|
||||||
self.relay_status.configure(text=text, fg=color)
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _cancel(self) -> None:
|
|
||||||
self.result = None'''
|
|
||||||
)], "加 _set_relay_status")
|
|
||||||
|
|
||||||
# ui.py 需要 re
|
|
||||||
text = UI.read_text(encoding="utf-8")
|
|
||||||
if not re.search(r"^import re\b", text, re.M):
|
|
||||||
m = re.search(r"^(?:import |from )", text, re.M)
|
|
||||||
text = text[:m.start()] + "import re\n" + text[m.start():]
|
|
||||||
UI.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print(" ✓ ui.py 补 import re")
|
|
||||||
else:
|
|
||||||
print(" • ui.py 已有 import re")
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 二、run_agent.py:双击打开设置窗口
|
|
||||||
# ================================================================
|
|
||||||
print()
|
|
||||||
print("── run_agent.py ──")
|
|
||||||
|
|
||||||
ok &= patch(RUN, [(
|
|
||||||
''' # 第一次运行:设置窗口 → 修防火墙 → 静默
|
|
||||||
if not store.get("setup_done", False):
|
|
||||||
return run_setup(store)
|
|
||||||
|
|
||||||
# 已经设置过:直接静默,屏幕上什么都不出现
|
|
||||||
return run_silent(store, None)''',
|
|
||||||
''' # 第一次运行:设置窗口 → 修防火墙 → 静默
|
|
||||||
if not store.get("setup_done", False):
|
|
||||||
return run_setup(store)
|
|
||||||
|
|
||||||
# 已经设置过、而且没给参数 —— 说明用户是**双击 exe**。
|
|
||||||
#
|
|
||||||
# 以前这里是 `return run_silent(store, None)`:直接后台跑,
|
|
||||||
# 屏幕上什么都不出现。用户想改配对码 / 中转地址 / 接入开关时
|
|
||||||
# 双击毫无反应,只能去翻命令行参数。
|
|
||||||
#
|
|
||||||
# 现在双击 = 打开设置窗口。后台运行走显式 --silent
|
|
||||||
# (开机自启和安装包都用的 --silent,所以不受影响)。
|
|
||||||
return run_settings(store)'''
|
|
||||||
)], "main() 双击改为打开设置窗口")
|
|
||||||
|
|
||||||
# 加 run_settings 函数(放在 run_setup 之后)
|
|
||||||
if "def run_settings(" not in RUN.read_text(encoding="utf-8"):
|
|
||||||
text = RUN.read_text(encoding="utf-8")
|
|
||||||
anchor = "def main(argv=None) -> int:"
|
|
||||||
if anchor not in text:
|
|
||||||
print(" ✗ 找不到 main()")
|
|
||||||
ok = False
|
|
||||||
else:
|
|
||||||
helper = '''def run_settings(store) -> int:
|
|
||||||
"""双击 exe 时打开的设置窗口。
|
|
||||||
|
|
||||||
和 run_setup 的区别:
|
|
||||||
run_setup 第一次装:走防火墙 + 开机自启 + 最后转静默
|
|
||||||
run_settings 已装好:只改配置,改完把后台实例重启一下让设置生效
|
|
||||||
|
|
||||||
为什么"已经有一个实例在跑"时也要照样开窗口:
|
|
||||||
以前 InstanceLock 拿不到就直接 return,用户双击后**什么都看不到**,
|
|
||||||
以为程序坏了。现在开窗、改完、重启后台实例。
|
|
||||||
"""
|
|
||||||
from lanremote.ui import SetupWindow
|
|
||||||
|
|
||||||
running = not InstanceLock.probe_free()
|
|
||||||
|
|
||||||
window = SetupWindow(store, mode="settings").show()
|
|
||||||
if window is None:
|
|
||||||
return 0 # 用户关掉了,什么都不改
|
|
||||||
|
|
||||||
changed = []
|
|
||||||
|
|
||||||
# ---- 配对码 ----
|
|
||||||
if window.get("code") and window["code"] != store.pairing_code():
|
|
||||||
try:
|
|
||||||
store.set_pairing_code(window["code"])
|
|
||||||
changed.append("配对码")
|
|
||||||
except ValueError as exc:
|
|
||||||
print(f"设置配对码失败:{exc}")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
# ---- 中转服务器(空 = 只走局域网)----
|
|
||||||
relay = str(window.get("relay", "") or "").strip()
|
|
||||||
if relay != str(store.get("relay_url", "") or "").strip():
|
|
||||||
store.set("relay_url", relay)
|
|
||||||
changed.append("中转地址")
|
|
||||||
|
|
||||||
# ---- 允许新设备接入 ----
|
|
||||||
allow_new = window.get("allow_new")
|
|
||||||
if allow_new is not None and bool(allow_new) != bool(
|
|
||||||
store.get("allow_new_pairing", True)):
|
|
||||||
store.set("allow_new_pairing", bool(allow_new))
|
|
||||||
changed.append("接入开关")
|
|
||||||
|
|
||||||
# ---- 开机自启 ----
|
|
||||||
try:
|
|
||||||
from lanremote.autostart import enable, disable, is_enabled
|
|
||||||
want = bool(window.get("autostart"))
|
|
||||||
if want != bool(is_enabled()):
|
|
||||||
(enable if want else disable)()
|
|
||||||
changed.append("开机自启")
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ---- 让后台实例重新读配置 ----
|
|
||||||
if changed and running:
|
|
||||||
_restart_background(store)
|
|
||||||
print("已保存:" + "、".join(changed) + "(后台实例已重启)")
|
|
||||||
elif changed:
|
|
||||||
print("已保存:" + "、".join(changed))
|
|
||||||
else:
|
|
||||||
print("没有改动")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _restart_background(store) -> None:
|
|
||||||
"""把正在跑的后台实例停掉,再用新配置起一个。
|
|
||||||
|
|
||||||
为什么要重启而不是"让它自己重读":
|
|
||||||
allow_new_pairing 是启动时读进 server.pairing_enabled 的,
|
|
||||||
中转地址也是启动时连的。文件改了但进程里还是旧值,
|
|
||||||
用户会以为设置没生效。
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
# 只杀"没有窗口"的那个(后台实例)。taskkill 按镜像名杀会连自己一起杀,
|
|
||||||
# 所以这里用 wmic/powershell 过滤命令行里带 --silent 的。
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["powershell", "-NoProfile", "-Command",
|
|
||||||
"Get-CimInstance Win32_Process -Filter \\"Name='ykxmz.exe'\\" | "
|
|
||||||
"Where-Object { $_.CommandLine -match '--silent' } | "
|
|
||||||
"ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"],
|
|
||||||
capture_output=True, timeout=20)
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
time.sleep(1.2)
|
|
||||||
# 用 --silent 重新起一个,完全脱离本进程
|
|
||||||
try:
|
|
||||||
exe = sys.executable
|
|
||||||
subprocess.Popen([exe, "--silent"],
|
|
||||||
creationflags=getattr(subprocess,
|
|
||||||
"DETACHED_PROCESS", 0),
|
|
||||||
close_fds=True)
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
text = text.replace(anchor, helper + anchor, 1)
|
|
||||||
RUN.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print(" ✓ 加 run_settings()")
|
|
||||||
else:
|
|
||||||
print(" • run_settings 已存在")
|
|
||||||
|
|
||||||
# --setup 也走 settings 模式
|
|
||||||
ok &= patch(RUN, [(
|
|
||||||
''' # --setup:重新设置
|
|
||||||
if args.setup:
|
|
||||||
return run_setup(store)''',
|
|
||||||
''' # --setup:重新打开设置窗口(和双击等效)
|
|
||||||
if args.setup:
|
|
||||||
return run_settings(store)'''
|
|
||||||
)], "--setup 走 settings 模式")
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
print()
|
|
||||||
import ast
|
|
||||||
bad = []
|
|
||||||
for f in (UI, RUN):
|
|
||||||
try:
|
|
||||||
ast.parse(f.read_text(encoding="utf-8"))
|
|
||||||
except SyntaxError as exc:
|
|
||||||
bad.append("%s: 第 %s 行 %s" % (f.name, exc.lineno, exc.msg))
|
|
||||||
if bad:
|
|
||||||
print(" ✗ 语法错:")
|
|
||||||
for b in bad:
|
|
||||||
print(" %s" % b)
|
|
||||||
return 1
|
|
||||||
print(" ✓ 两个文件语法都通过")
|
|
||||||
return 0 if ok else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''给官网首页加 60 秒缓存。
|
|
||||||
|
|
||||||
(download 那处已经用 edit 工具改好了:no-store ->
|
|
||||||
public,max-age=604800,immutable。)
|
|
||||||
|
|
||||||
首页原来走 _send 的默认 Cache-Control: no-store,
|
|
||||||
每次刷新都穿隧道回 4G 源站。加 60 秒缓存:
|
|
||||||
* 挡住"刷新就重新回源"的浪费
|
|
||||||
* 又不会让页面上的当前版本号 / 下载按钮过期太久
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay\relay.py")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = SRC.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
marker = 'self._send(200, _site_page().encode("utf-8"),\n'
|
|
||||||
idx = text.find(marker)
|
|
||||||
if idx < 0:
|
|
||||||
print(" ✗ 找不到首页的 _send")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# 打印现场,确认空白
|
|
||||||
end = text.find("\n", text.find("return", idx))
|
|
||||||
print(" 现场:")
|
|
||||||
for line in text[idx:end + 1].splitlines():
|
|
||||||
print(" %r" % line)
|
|
||||||
|
|
||||||
old = ('self._send(200, _site_page().encode("utf-8"),\n'
|
|
||||||
' "text/html; charset=utf-8")')
|
|
||||||
new = ('# 缓存 60 秒:挡住"刷新就重新回源"的浪费,\n'
|
|
||||||
' # 又不会让页面上的当前版本号和下载按钮过期太久。\n'
|
|
||||||
' self._send(200, _site_page().encode("utf-8"),\n'
|
|
||||||
' "text/html; charset=utf-8",\n'
|
|
||||||
' {"Cache-Control": "public, max-age=60"})')
|
|
||||||
|
|
||||||
if old not in text:
|
|
||||||
print()
|
|
||||||
print(" ✗ 精确串还是没匹配上(看上面的 repr 对齐空白)")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" ✓ 已加首页缓存")
|
|
||||||
|
|
||||||
# 复核
|
|
||||||
for i, line in enumerate(
|
|
||||||
SRC.read_text(encoding="utf-8").splitlines(), 1):
|
|
||||||
if "max-age=60" in line:
|
|
||||||
print(" 第 %d 行: %s" % (i, line.strip()))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''给 verify_all.ps1 追加阶段 —— **不能用 PowerShell heredoc**。
|
|
||||||
|
|
||||||
== 为什么单独写这个脚本 ==
|
|
||||||
|
|
||||||
我这一轮在 PowerShell 里用 `python -c @"..."@` 写 Python 代码
|
|
||||||
去改 verify_all.ps1,结果 **`$stageOut` 和 `$root` 被 PowerShell
|
|
||||||
当成变量给展开了**(`@"..."@` 是插值字符串),写进去变成:
|
|
||||||
|
|
||||||
= & python (Join-Path "_tests\check_version_consistency.py") 2>&1
|
|
||||||
Show-Fails
|
|
||||||
Record "版本号一致性" (0 -eq 0)
|
|
||||||
|
|
||||||
整个脚本直接语法错。**同一个坑踩了两次**(3a7 和 3a8)。
|
|
||||||
|
|
||||||
教训:凡是给 .ps1 加含 `$` 的内容,一律**写 Python 脚本文件再跑**,
|
|
||||||
不要用 `python -c @"..."@`。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
python _build/add_verify_stage.py <锚点文字> <阶段号> <标题> <脚本相对路径> <Record 名>
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
SUITE = ROOT / "_tests" / "verify_all.ps1"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if len(sys.argv) < 6:
|
|
||||||
print(__doc__)
|
|
||||||
return 1
|
|
||||||
anchor_text, stage_no, title, script_rel, record_name = sys.argv[1:6]
|
|
||||||
|
|
||||||
raw = SUITE.read_bytes()
|
|
||||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
|
|
||||||
if script_rel in text:
|
|
||||||
print(" 已经注册过了(%s 里提到了 %s)" % (SUITE.name, script_rel))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
lines = text.splitlines()
|
|
||||||
idx = None
|
|
||||||
for i, ln in enumerate(lines):
|
|
||||||
if ln.startswith("Record") and anchor_text in ln:
|
|
||||||
idx = i
|
|
||||||
break
|
|
||||||
if idx is None:
|
|
||||||
print(" ✗ 找不到锚点(Record 行含 %r)" % anchor_text)
|
|
||||||
print(" 现有 Record 行:")
|
|
||||||
for i, ln in enumerate(lines):
|
|
||||||
if ln.startswith("Record"):
|
|
||||||
print(" %d: %s" % (i, ln))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
block = [
|
|
||||||
"",
|
|
||||||
"# ---------------------------------------------------------------- %s"
|
|
||||||
% stage_no,
|
|
||||||
'Banner "%s"' % title,
|
|
||||||
"$stageOut = & python (Join-Path $root \"%s\") 2>&1"
|
|
||||||
% script_rel.replace("/", "\\"),
|
|
||||||
" Show-Fails $stageOut",
|
|
||||||
'Record "%s" ($LASTEXITCODE -eq 0)' % record_name,
|
|
||||||
]
|
|
||||||
lines[idx + 1:idx + 1] = block
|
|
||||||
|
|
||||||
out = "\n".join(lines) + "\n"
|
|
||||||
SUITE.write_bytes((b"\xef\xbb\xbf" if had_bom else b"") + out.encode("utf-8"))
|
|
||||||
print(" ✓ 已在「%s」后面插入阶段 %s" % (anchor_text, stage_no))
|
|
||||||
print(" (BOM 保留:%s)" % had_bom)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''安卓端:中转地址改成"手动填" + 加一个设置页。
|
|
||||||
|
|
||||||
== 用户报的问题 ==
|
|
||||||
|
|
||||||
「安卓端为什么还是默认填的我的中转站,以后改成手动填,
|
|
||||||
安卓端加一个设置,各种设置就在这里面调」
|
|
||||||
|
|
||||||
== 根因 ==
|
|
||||||
|
|
||||||
代码里的默认值**已经改成空**了:
|
|
||||||
|
|
||||||
private static final String DEFAULT_RELAY_URL = "";
|
|
||||||
|
|
||||||
但你手表上看到的还是 `https://relay.xxmxxs.top` —— 因为
|
|
||||||
**老版本 1.5.0 把作者的域名硬编码成默认值**(git 历史里查到了):
|
|
||||||
|
|
||||||
private static final String DEFAULT_RELAY_URL = "https://relay.xxmxxs.top";
|
|
||||||
|
|
||||||
第一次运行它就 `setRelayUrl(默认值)` 存进了 SharedPreferences。
|
|
||||||
装新 APK **不会清偏好设置**,所以那个值一直留着。
|
|
||||||
|
|
||||||
**用户从来没"选"过这个地址** —— 是程序硬塞的。所以正确做法是
|
|
||||||
**一次性迁移**:如果存的就是当时内置的那个域名,当成没设过,清掉。
|
|
||||||
|
|
||||||
== 做的事 ==
|
|
||||||
|
|
||||||
1. 迁移:把老版本硬塞的那个域名从偏好里清掉
|
|
||||||
2. 新增「设置」页,把散落的设置收进来:
|
|
||||||
* 中转服务器(手动填 / 清空)
|
|
||||||
* 记住的配对码(看 / 清)
|
|
||||||
* 已配对设备(看数量 / 清空)
|
|
||||||
* 关于(版本号、连接方式说明)
|
|
||||||
3. 首页那个「中转设置(跨网络用)」按钮改成「设置」,进设置页
|
|
||||||
4. settingsPanel 纳入 showOnly(),避免再出现"面板漏藏"那类问题
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
SRC = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
|
|
||||||
/ "MainActivity.java")
|
|
||||||
|
|
||||||
|
|
||||||
# ======================================================================
|
|
||||||
# 1. 迁移 + 面板字段
|
|
||||||
# ======================================================================
|
|
||||||
MIGRATION = ''' /**
|
|
||||||
* 老版本硬塞进来的中转地址,要清掉。
|
|
||||||
*
|
|
||||||
* <p>1.5.0 那版把作者的域名写死成默认值:
|
|
||||||
* <pre>
|
|
||||||
* private static final String DEFAULT_RELAY_URL = "https://relay.xxmxxs.top";
|
|
||||||
* </pre>
|
|
||||||
* 而且第一次运行就存进了 SharedPreferences。装新 APK 不会清偏好,
|
|
||||||
* 所以用户会看到「默认还是那个中转站」—— 其实他从来没选过。
|
|
||||||
*
|
|
||||||
* <p>现在默认值是空串(跨网络要自己填)。这里做一次性迁移:
|
|
||||||
* 存的值如果正好是当时内置的那个域名,就当成没设过,清掉。
|
|
||||||
*
|
|
||||||
* <p>⚠ 只在**完全等于**内置值时清 —— 用户自己手填的地址不能动。
|
|
||||||
*/
|
|
||||||
private static final String[] LEGACY_BAKED_IN_RELAYS = {
|
|
||||||
"https://relay.xxmxxs.top",
|
|
||||||
"http://relay.xxmxxs.top",
|
|
||||||
};
|
|
||||||
|
|
||||||
private void migrateLegacyRelay() {
|
|
||||||
String saved = getSharedPreferences("lanremote", MODE_PRIVATE)
|
|
||||||
.getString("relay_url", "");
|
|
||||||
if (saved == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String trimmed = saved.trim();
|
|
||||||
for (String legacy : LEGACY_BAKED_IN_RELAYS) {
|
|
||||||
if (legacy.equalsIgnoreCase(trimmed)) {
|
|
||||||
// 这是老版本硬塞的,不是用户选的 —— 清掉,让他自己填
|
|
||||||
getSharedPreferences("lanremote", MODE_PRIVATE).edit()
|
|
||||||
.remove("relay_url").apply();
|
|
||||||
appendLog("已清掉旧版本内置的中转地址(不是你自己填的)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
SETTINGS_BUILD = ''' /**
|
|
||||||
* 「设置」页。用户要求:"安卓端加一个设置,各种设置就在这里面调"。
|
|
||||||
*
|
|
||||||
* <p>以前设置散在首页(一个「中转设置」按钮)和别处,
|
|
||||||
* 现在统一收到这里。
|
|
||||||
*/
|
|
||||||
private void buildSettingsPanel() {
|
|
||||||
settingsPanel = new FrameLayout(this);
|
|
||||||
settingsPanel.setBackgroundColor(C_BG);
|
|
||||||
|
|
||||||
ScrollView scroll = new ScrollView(this);
|
|
||||||
scroll.setBackgroundColor(C_BG);
|
|
||||||
LinearLayout box = new LinearLayout(this);
|
|
||||||
box.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
box.setPadding(dp(10), dp(12), dp(10), dp(12));
|
|
||||||
scroll.addView(box, new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
settingsPanel.addView(scroll, new FrameLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
||||||
|
|
||||||
LinearLayout top = new LinearLayout(this);
|
|
||||||
top.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
top.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
box.addView(top, new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
|
|
||||||
TextView title = new TextView(this);
|
|
||||||
title.setText("设置");
|
|
||||||
title.setTextColor(C_FG);
|
|
||||||
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
|
||||||
title.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
top.addView(title, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
|
||||||
top.addView(smallButton("返回", C_CARD, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
hideSettingsPanel();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ================= 中转服务器 =================
|
|
||||||
settingsRelayValue = new TextView(this);
|
|
||||||
settingsRelayValue.setTextColor(C_DIM);
|
|
||||||
settingsRelayValue.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
|
||||||
settingsRelayValue.setPadding(0, dp(6), 0, dp(4));
|
|
||||||
|
|
||||||
box.addView(sectionTitle("中转服务器"));
|
|
||||||
box.addView(hintText("手表和电脑不在同一个网络时(比如在办公室管教室)"
|
|
||||||
+ "才需要填。填你自己那台的地址,比如 relay.你的域名。\\n"
|
|
||||||
+ "只在一个 WiFi 里用就留空 —— 那样直接连,最快也不耗流量。"));
|
|
||||||
box.addView(settingsRelayValue);
|
|
||||||
|
|
||||||
Button editRelay = bigButton("填写中转服务器", C_ACCENT, 14);
|
|
||||||
editRelay.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
promptRelayUrl();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(editRelay);
|
|
||||||
|
|
||||||
Button clearRelay = bigButton("清空中转服务器(只走局域网)", C_CARD, 13);
|
|
||||||
clearRelay.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
setRelayUrl("");
|
|
||||||
refreshSettingsPanel();
|
|
||||||
toast("已清空 —— 以后只走局域网");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(clearRelay);
|
|
||||||
|
|
||||||
// ================= 配对码 =================
|
|
||||||
settingsCodeValue = new TextView(this);
|
|
||||||
settingsCodeValue.setTextColor(C_DIM);
|
|
||||||
settingsCodeValue.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
|
||||||
settingsCodeValue.setPadding(0, dp(6), 0, dp(4));
|
|
||||||
|
|
||||||
box.addView(sectionTitle("记住的配对码"));
|
|
||||||
box.addView(hintText("手表会记住最近一次用的配对码,"
|
|
||||||
+ "下次连接自动带上,不用重复输。"));
|
|
||||||
box.addView(settingsCodeValue);
|
|
||||||
|
|
||||||
Button clearCode = bigButton("清除记住的配对码", C_CARD, 13);
|
|
||||||
clearCode.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
getSharedPreferences("lanremote", MODE_PRIVATE).edit()
|
|
||||||
.remove("paired_code").apply();
|
|
||||||
refreshSettingsPanel();
|
|
||||||
toast("已清除");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(clearCode);
|
|
||||||
|
|
||||||
// ================= 已配对设备 =================
|
|
||||||
settingsDevicesValue = new TextView(this);
|
|
||||||
settingsDevicesValue.setTextColor(C_DIM);
|
|
||||||
settingsDevicesValue.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
|
||||||
settingsDevicesValue.setPadding(0, dp(6), 0, dp(4));
|
|
||||||
|
|
||||||
box.addView(sectionTitle("已配对的电脑"));
|
|
||||||
box.addView(hintText("配对过的电脑会出现在首页列表里,点一下就能连。"
|
|
||||||
+ "换电脑或者想重新配对时可以清空。"));
|
|
||||||
box.addView(settingsDevicesValue);
|
|
||||||
|
|
||||||
Button clearDevices = bigButton("清空已配对的电脑", C_DANGER, 13);
|
|
||||||
clearDevices.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
new android.app.AlertDialog.Builder(MainActivity.this)
|
|
||||||
.setTitle("清空已配对的电脑?")
|
|
||||||
.setMessage("清掉之后首页列表就空了,"
|
|
||||||
+ "需要重新输配对码连接。")
|
|
||||||
.setPositiveButton("清空", new android.content.DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(android.content.DialogInterface d, int w) {
|
|
||||||
store.clearDevices();
|
|
||||||
refreshDeviceList();
|
|
||||||
refreshSettingsPanel();
|
|
||||||
toast("已清空");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton("取消", null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(clearDevices);
|
|
||||||
|
|
||||||
// ================= 关于 =================
|
|
||||||
box.addView(sectionTitle("关于"));
|
|
||||||
box.addView(hintText("ykxmz 手表端 版本 " + appVersion()
|
|
||||||
+ "\\n\\n"
|
|
||||||
+ "连接顺序:先试局域网直连(同一个 WiFi,几毫秒);"
|
|
||||||
+ "连不上再走中转(跨网络,受线路影响)。"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 一行小节标题 */
|
|
||||||
private TextView sectionTitle(String text) {
|
|
||||||
TextView v = new TextView(this);
|
|
||||||
v.setText(text);
|
|
||||||
v.setTextColor(C_FG);
|
|
||||||
v.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
|
||||||
v.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
v.setPadding(0, dp(14), 0, 0);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 一行灰色说明 */
|
|
||||||
private TextView hintText(String text) {
|
|
||||||
TextView v = new TextView(this);
|
|
||||||
v.setText(text);
|
|
||||||
v.setTextColor(C_DIM);
|
|
||||||
v.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
|
||||||
v.setLineSpacing(0, 1.25f);
|
|
||||||
v.setPadding(0, dp(4), 0, 0);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 版本号。从 PackageManager 读,和构建脚本里的版本天然一致,
|
|
||||||
* 不用在代码里再写一份(写两份必然有一份会忘改)。 */
|
|
||||||
private String appVersion() {
|
|
||||||
try {
|
|
||||||
return getPackageManager()
|
|
||||||
.getPackageInfo(getPackageName(), 0).versionName;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 把设置页上的文字刷新成当前值 */
|
|
||||||
private void refreshSettingsPanel() {
|
|
||||||
if (settingsRelayValue == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String relay = relayUrl();
|
|
||||||
settingsRelayValue.setText(relay.isEmpty()
|
|
||||||
? "现在是:空(只走局域网)"
|
|
||||||
: "现在是:" + relay);
|
|
||||||
settingsRelayValue.setTextColor(relay.isEmpty() ? C_DIM : C_OK);
|
|
||||||
|
|
||||||
String code = savedPairingCode();
|
|
||||||
settingsCodeValue.setText((code == null || code.isEmpty())
|
|
||||||
? "现在没有记住的配对码"
|
|
||||||
: "记住的是:" + code);
|
|
||||||
|
|
||||||
int n = store.devices() == null ? 0 : store.devices().size();
|
|
||||||
settingsDevicesValue.setText("已配对 " + n + " 台电脑");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSettingsPanel() {
|
|
||||||
refreshSettingsPanel();
|
|
||||||
showOnly(settingsPanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideSettingsPanel() {
|
|
||||||
showConnectPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def patch(text: str, old: str, new: str, label: str, results: list) -> str:
|
|
||||||
if old not in text:
|
|
||||||
results.append(("✗", label, "找不到锚点"))
|
|
||||||
return text
|
|
||||||
if text.count(old) != 1:
|
|
||||||
results.append(("!", label, "锚点出现 %d 次" % text.count(old)))
|
|
||||||
results.append(("✓", label, ""))
|
|
||||||
return text.replace(old, new, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = SRC.read_text(encoding="utf-8")
|
|
||||||
results: list = []
|
|
||||||
|
|
||||||
# ---- 1. 面板字段声明 ----
|
|
||||||
text = patch(
|
|
||||||
text,
|
|
||||||
" private FrameLayout lanPanel;",
|
|
||||||
" private FrameLayout lanPanel;\n"
|
|
||||||
" // ---- 设置页(用户要求:各种设置收在一个页面里调)----\n"
|
|
||||||
" private FrameLayout settingsPanel;\n"
|
|
||||||
" private TextView settingsRelayValue;\n"
|
|
||||||
" private TextView settingsCodeValue;\n"
|
|
||||||
" private TextView settingsDevicesValue;",
|
|
||||||
"声明 settingsPanel 和各显示控件", results)
|
|
||||||
|
|
||||||
# ---- 2. 插入迁移函数(放在 relayUrl 之前)----
|
|
||||||
text = patch(
|
|
||||||
text, " private String relayUrl() {",
|
|
||||||
MIGRATION + " private String relayUrl() {",
|
|
||||||
"插入 migrateLegacyRelay()", results)
|
|
||||||
|
|
||||||
# ---- 3. buildSettingsPanel 等方法(插在 buildLanPanel 之前)----
|
|
||||||
text = patch(
|
|
||||||
text, " private void buildLanPanel() {",
|
|
||||||
SETTINGS_BUILD + " private void buildLanPanel() {",
|
|
||||||
"插入 buildSettingsPanel() 等", results)
|
|
||||||
|
|
||||||
# ---- 4. 建面板的地方也要建它 ----
|
|
||||||
text = patch(
|
|
||||||
text, " buildLanPanel();",
|
|
||||||
" buildLanPanel();\n buildSettingsPanel();",
|
|
||||||
"构建时创建 settingsPanel", results)
|
|
||||||
|
|
||||||
# ---- 5. showOnly 纳入 settingsPanel ----
|
|
||||||
text = patch(
|
|
||||||
text,
|
|
||||||
" View[] all = {connectPanel, controlPanel, mirrorPanel, lanPanel};",
|
|
||||||
" View[] all = {connectPanel, controlPanel, mirrorPanel, lanPanel,\n"
|
|
||||||
" settingsPanel};",
|
|
||||||
"settingsPanel 纳入 showOnly()", results)
|
|
||||||
|
|
||||||
# ---- 6. 首页那个按钮改成「设置」 ----
|
|
||||||
text = patch(
|
|
||||||
text,
|
|
||||||
''' Button relayButton = bigButton("中转设置(跨网络用)", C_ACCENT, 14);''',
|
|
||||||
''' Button relayButton = bigButton("设置", C_ACCENT, 14);''',
|
|
||||||
"首页按钮文案改成「设置」", results)
|
|
||||||
|
|
||||||
text = patch(
|
|
||||||
text,
|
|
||||||
''' public void onClick(View view) {
|
|
||||||
promptRelayUrl();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(relayButton);''',
|
|
||||||
''' public void onClick(View view) {
|
|
||||||
showSettingsPanel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(relayButton);''',
|
|
||||||
"首页按钮改成打开设置页", results)
|
|
||||||
|
|
||||||
# ---- 7. 启动时做迁移 ----
|
|
||||||
# 找 onCreate 里合适的位置:appendLog 第一次出现之前太早,
|
|
||||||
# 用 "store = " 初始化之后
|
|
||||||
import re
|
|
||||||
m = re.search(r"( store = new DeviceStore\(this\);\n)", text)
|
|
||||||
if m:
|
|
||||||
text = (text[:m.end()]
|
|
||||||
+ " // 老版本硬塞的中转地址要清掉(见 migrateLegacyRelay)\n"
|
|
||||||
" migrateLegacyRelay();\n"
|
|
||||||
+ text[m.end():])
|
|
||||||
results.append(("✓", "启动时调用迁移", ""))
|
|
||||||
else:
|
|
||||||
results.append(("✗", "启动时调用迁移", "找不到 store 初始化那行"))
|
|
||||||
|
|
||||||
# ---- 输出 ----
|
|
||||||
print(" ── 补丁结果 ──")
|
|
||||||
for mark, label, note in results:
|
|
||||||
print(" %s %s%s" % (mark, label, (" —— " + note) if note else ""))
|
|
||||||
|
|
||||||
if any(m == "✗" for m, _l, _n in results):
|
|
||||||
print()
|
|
||||||
print(" 有锚点没匹配,没有写回")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if not any(m == "✗" for m, _l, _n in results):
|
|
||||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" 已写回 MainActivity.java")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''手表端加「音效」—— 用现成的 showChooser(),不新建面板。
|
|
||||||
|
|
||||||
== 为什么改方案 ==
|
|
||||||
|
|
||||||
第一版我给音效页写了完整的新面板(FrameLayout + ScrollView + 动态列表),
|
|
||||||
后来发现 MainActivity 里**已经有 showChooser(title, labels, Chooser)**
|
|
||||||
这个现成的选择器 —— pickVoice() 就是这么选 TTS 音色的。
|
|
||||||
|
|
||||||
用选择器的话:
|
|
||||||
* 不用新建面板 -> 不用碰 root.addView / showOnly(那两个地方
|
|
||||||
刚因为"漏挂"出过一次全白的事故)
|
|
||||||
* 代码量少一半,出错面小得多
|
|
||||||
* 而且和"选音色"的操作手感一致,用户不用学两套
|
|
||||||
|
|
||||||
所以改成:首页加一个「音效」按钮 -> 弹出音效列表 -> 点哪个放哪个。
|
|
||||||
|
|
||||||
== 服务端配合 ==
|
|
||||||
|
|
||||||
服务端返回的是 {"sfx":[{"name":...},...]} 这种嵌套结构,
|
|
||||||
而手表的 stringList(reply, key) 只认扁平的字符串数组。
|
|
||||||
audio_list 早就这么做了(同时返回 files / names / paths),
|
|
||||||
这里跟着加一个 "names",手表直接用。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
SRC = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
|
|
||||||
/ "MainActivity.java")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 方法
|
|
||||||
METHOD_ANCHOR = " /** 选择电脑上的 TTS 音色。 */"
|
|
||||||
|
|
||||||
METHOD_NEW = ''' /** 让电脑放一个音效(网络热门音效那类)。
|
|
||||||
*
|
|
||||||
* 用 showChooser 而不是新建面板 —— 见 _build/add_watch_sfx.py 里的说明:
|
|
||||||
* pickVoice() 已经是这个套路,复用省事也更不容易出错。
|
|
||||||
*/
|
|
||||||
private void pickSfx() {
|
|
||||||
appendLog("正在获取音效…");
|
|
||||||
io.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
final Map<String, Object> reply =
|
|
||||||
client.call(Json.obj("cmd", "sfx_list"), 15000);
|
|
||||||
// 服务端同时给了扁平的 names(和 audio_list 一个约定)
|
|
||||||
final List<String> names = stringList(reply, "names");
|
|
||||||
final String dir = Json.str(reply, "dir", "");
|
|
||||||
ui.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (names.isEmpty()) {
|
|
||||||
toast("电脑上还没有音效(或没连上)");
|
|
||||||
appendLog("音效列表是空的。"
|
|
||||||
+ (dir.isEmpty() ? "" : "素材目录:" + dir));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showChooser("音效(点一下电脑就响)", names,
|
|
||||||
new Chooser() {
|
|
||||||
@Override
|
|
||||||
public void onPick(int index) {
|
|
||||||
String one = names.get(index);
|
|
||||||
sendCommand(Json.obj("cmd", "sfx_play",
|
|
||||||
"name", one), "音效 " + one);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (final IOException e) {
|
|
||||||
postError("获取音效", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
''' + METHOD_ANCHOR
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
text = SRC.read_text(encoding="utf-8")
|
|
||||||
out: list = []
|
|
||||||
|
|
||||||
def patch(old: str, new: str, what: str) -> None:
|
|
||||||
nonlocal text
|
|
||||||
if old not in text:
|
|
||||||
out.append(("✗", what, "找不到锚点"))
|
|
||||||
return
|
|
||||||
if text.count(old) != 1:
|
|
||||||
out.append(("!", what, "锚点 %d 处" % text.count(old)))
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
out.append(("✓", what, ""))
|
|
||||||
|
|
||||||
# 1) 插方法
|
|
||||||
patch(METHOD_ANCHOR, METHOD_NEW, "插入 pickSfx()")
|
|
||||||
|
|
||||||
# 2) 首页加按钮(放在「音效」该在的位置:设置按钮之后)
|
|
||||||
patch(''' box.addView(sfxButton);''', ''' box.addView(sfxButton);''',
|
|
||||||
"(占位,见下)") if False else None
|
|
||||||
|
|
||||||
patch(''' box.addView(relayButton);''',
|
|
||||||
''' box.addView(relayButton);
|
|
||||||
|
|
||||||
// 音效:点一下电脑就响。用户要求"可以直接远程播放"。
|
|
||||||
Button sfxButton = bigButton("音效(让电脑放一声)", C_ACCENT, 14);
|
|
||||||
LinearLayout.LayoutParams sfxParams = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, dp(52));
|
|
||||||
sfxParams.setMargins(0, 0, 0, dp(8));
|
|
||||||
sfxButton.setLayoutParams(sfxParams);
|
|
||||||
sfxButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
pickSfx();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
box.addView(sfxButton);''',
|
|
||||||
"首页加「音效」按钮")
|
|
||||||
|
|
||||||
print(" ── 补丁结果 ──")
|
|
||||||
for mark, what, why in out:
|
|
||||||
print(" %s %s%s" % (mark, what, (" —— " + why) if why else ""))
|
|
||||||
if any(m == "✗" for m, _w, _y in out):
|
|
||||||
print()
|
|
||||||
print(" 有锚点没匹配,没有写回")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
print()
|
|
||||||
print(" 已写回 MainActivity.java")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN"><head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<title>ykxmz 中转站</title>
|
|
||||||
<style>
|
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
|
||||||
:root{
|
|
||||||
--bg:#ffffff; --surface:#fafbfc; --border:#e6e8eb; --border-soft:#f0f2f5;
|
|
||||||
--fg:#1a1d21; --muted:#6b7280; --faint:#9ca3af;
|
|
||||||
--accent:#2563eb; --ok:#16a34a; --warn:#d97706; --danger:#dc2626;
|
|
||||||
}
|
|
||||||
html,body{max-width:100%;overflow-x:hidden}
|
|
||||||
body{background:var(--bg);color:var(--fg);line-height:1.6;
|
|
||||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',
|
|
||||||
'PingFang SC',sans-serif;-webkit-font-smoothing:antialiased}
|
|
||||||
a{color:inherit}
|
|
||||||
.wrap{width:100%;max-width:1040px;margin:0 auto;padding:0 20px 64px}
|
|
||||||
|
|
||||||
/* ---------- 吸顶顶栏 ---------- */
|
|
||||||
.top{position:sticky;top:0;z-index:40;background:rgba(255,255,255,.88);
|
|
||||||
backdrop-filter:saturate(180%) blur(12px);border-bottom:1px solid var(--border)}
|
|
||||||
/*
|
|
||||||
* ⚠ 别用固定 height。
|
|
||||||
* 原来写的是 .top-in{height:62px},但里面品牌是「标题 + 副标题」两行、
|
|
||||||
* 右边还有两行版本信息 —— 实际内容比 62px 高,
|
|
||||||
* flex 就把内容往上顶出去了:**标题被切掉、运行中徽章压在字上**
|
|
||||||
* (用户截图里就是这样)。
|
|
||||||
* 改成 padding 撑开,内容多高就是多高。
|
|
||||||
*/
|
|
||||||
.top-in{display:flex;align-items:center;gap:12px;
|
|
||||||
padding-top:13px;padding-bottom:13px;min-height:36px}
|
|
||||||
.logo{width:32px;height:32px;border-radius:9px;flex:none;
|
|
||||||
background:linear-gradient(140deg,#2563eb,#1d4ed8);
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
|
||||||
color:#fff;font-weight:700;font-size:12px;letter-spacing:-.3px}
|
|
||||||
.brand{flex:none;min-width:0}
|
|
||||||
.brand b{display:block;font-size:15px;font-weight:650;letter-spacing:-.2px;
|
|
||||||
line-height:1.35;white-space:nowrap}
|
|
||||||
.brand span{display:block;color:var(--faint);font-size:11px;
|
|
||||||
line-height:1.4;white-space:nowrap}
|
|
||||||
.state{margin-left:8px;flex:none;display:inline-flex;align-items:center;gap:6px;
|
|
||||||
padding:3px 10px;border-radius:999px;background:#f0fdf4;
|
|
||||||
color:#15803d;font-size:11px;font-weight:600;white-space:nowrap}
|
|
||||||
.state i{width:6px;height:6px;border-radius:50%;background:#22c55e;
|
|
||||||
display:block;flex:none}
|
|
||||||
.top-right{margin-left:auto;text-align:right;font-size:11px;
|
|
||||||
color:var(--faint);line-height:1.65;min-width:0}
|
|
||||||
.top-right b{color:var(--muted);font-weight:500}
|
|
||||||
.logout{color:var(--muted);text-decoration:none;
|
|
||||||
border-bottom:1px dashed var(--border);transition:.15s}
|
|
||||||
.logout:hover{color:var(--danger);border-bottom-color:var(--danger)}
|
|
||||||
|
|
||||||
/* 窄屏:状态徽章换行,别和标题挤 */
|
|
||||||
@media(max-width:640px){
|
|
||||||
.top-in{flex-wrap:wrap;gap:8px}
|
|
||||||
.state{margin-left:auto}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 公网地址(最重要,给最大权重)---------- */
|
|
||||||
.hero{padding:30px 0 26px}
|
|
||||||
.hero h2{font-size:13px;font-weight:600;color:var(--muted);
|
|
||||||
margin-bottom:10px;letter-spacing:.2px}
|
|
||||||
.urlbox{display:flex;align-items:stretch;gap:10px;flex-wrap:wrap}
|
|
||||||
.url{flex:1;min-width:240px;display:flex;align-items:center;
|
|
||||||
font:600 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;
|
|
||||||
color:var(--accent);word-break:break-all;
|
|
||||||
background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:11px;padding:14px 16px}
|
|
||||||
.url.empty{color:var(--faint);font-weight:400;font-size:14px}
|
|
||||||
.hint{color:var(--faint);font-size:12px;margin-top:9px}
|
|
||||||
|
|
||||||
/* ---------- 数字卡 ---------- */
|
|
||||||
.nums{display:grid;gap:10px;grid-template-columns:repeat(2,1fr);
|
|
||||||
margin-bottom:30px}
|
|
||||||
@media(min-width:640px){.nums{grid-template-columns:repeat(4,1fr)}}
|
|
||||||
.num{background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:12px;padding:15px 16px}
|
|
||||||
.num .v{font-size:25px;font-weight:700;letter-spacing:-.7px;
|
|
||||||
line-height:1.1;font-variant-numeric:tabular-nums}
|
|
||||||
.num .k{color:var(--muted);font-size:12px;margin-top:3px}
|
|
||||||
.num.live{background:#f0fdf4;border-color:#bbf7d0}
|
|
||||||
.num.live .v{color:#15803d}
|
|
||||||
.num.busy{background:#fffbeb;border-color:#fde68a}
|
|
||||||
.num.busy .v{color:#b45309}
|
|
||||||
|
|
||||||
/* ---------- 区块标题 ---------- */
|
|
||||||
.sec{display:flex;align-items:center;gap:12px;margin-bottom:12px}
|
|
||||||
.sec h3{font-size:15px;font-weight:650;letter-spacing:-.2px}
|
|
||||||
.sec .right{margin-left:auto;display:flex;align-items:center;gap:12px}
|
|
||||||
|
|
||||||
/* ---------- 表格 ---------- */
|
|
||||||
.card{background:#fff;border:1px solid var(--border);border-radius:13px;
|
|
||||||
overflow:hidden}
|
|
||||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
||||||
thead th{background:var(--surface);text-align:left;font-weight:600;
|
|
||||||
color:var(--muted);font-size:11.5px;letter-spacing:.3px;
|
|
||||||
padding:11px 14px;border-bottom:1px solid var(--border);
|
|
||||||
white-space:nowrap}
|
|
||||||
tbody td{padding:13px 14px;border-bottom:1px solid var(--border-soft);
|
|
||||||
vertical-align:middle}
|
|
||||||
tbody tr:last-child td{border-bottom:none}
|
|
||||||
tbody tr:hover td{background:#fbfcfd}
|
|
||||||
tbody tr.picked td{background:#eff6ff}
|
|
||||||
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;
|
|
||||||
margin-right:7px;flex:none}
|
|
||||||
.dot.on{background:#22c55e}
|
|
||||||
.dot.off{background:#d1d5db}
|
|
||||||
.devname{display:flex;align-items:center;font-weight:600}
|
|
||||||
.devid{color:var(--faint);font-size:10.5px;margin-top:2px;
|
|
||||||
font-family:ui-monospace,Consolas,monospace;
|
|
||||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px}
|
|
||||||
code{font:600 13px/1 ui-monospace,Consolas,monospace;
|
|
||||||
background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:6px;padding:4px 8px;letter-spacing:.5px}
|
|
||||||
.pill{padding:3px 9px;border-radius:999px;font-size:11px;font-weight:600}
|
|
||||||
.pill.on{background:#dcfce7;color:#15803d}
|
|
||||||
.pill.off{background:#f3f4f6;color:#6b7280}
|
|
||||||
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;
|
|
||||||
color:var(--muted)}
|
|
||||||
.ops{text-align:right}
|
|
||||||
.empty{padding:56px 20px;text-align:center}
|
|
||||||
.empty .big{font-size:15px;font-weight:600;margin-bottom:8px}
|
|
||||||
.empty p{color:var(--muted);font-size:13px;max-width:340px;margin:0 auto}
|
|
||||||
.empty .ico{font-size:30px;margin-bottom:14px;opacity:.35}
|
|
||||||
|
|
||||||
/* ---------- 按钮 ---------- */
|
|
||||||
button{font-family:inherit;font-size:13px;cursor:pointer;
|
|
||||||
border-radius:9px;padding:8px 15px;border:1px solid var(--border);
|
|
||||||
background:#fff;color:var(--text);transition:.14s;
|
|
||||||
font-weight:500}
|
|
||||||
button:hover:not(:disabled){background:var(--surface);
|
|
||||||
border-color:#d1d5db}
|
|
||||||
button:active:not(:disabled){transform:translateY(1px)}
|
|
||||||
button:disabled{opacity:.42;cursor:not-allowed}
|
|
||||||
button.primary{background:var(--accent);border-color:var(--accent);
|
|
||||||
color:#fff;font-weight:600}
|
|
||||||
button.primary:hover:not(:disabled){background:#1d4ed8;
|
|
||||||
border-color:#1d4ed8}
|
|
||||||
button.danger{color:#b91c1c;border-color:#fecaca;background:#fff}
|
|
||||||
button.danger:hover:not(:disabled){background:#fef2f2;
|
|
||||||
border-color:#fca5a5}
|
|
||||||
button.sm{padding:6px 12px;font-size:12px}
|
|
||||||
|
|
||||||
/* ---------- 工具卡 ---------- */
|
|
||||||
.tools{display:grid;gap:14px;margin-top:30px}
|
|
||||||
@media(min-width:720px){.tools{grid-template-columns:1fr 1fr}}
|
|
||||||
.tool{border:1px solid var(--border);border-radius:13px;padding:18px 18px}
|
|
||||||
.tool h4{font-size:13.5px;font-weight:650;margin-bottom:4px}
|
|
||||||
.tool .d{color:var(--muted);font-size:12.5px;margin-bottom:14px}
|
|
||||||
.tool .out{color:var(--muted);font-size:12.5px;line-height:1.85;
|
|
||||||
word-break:break-all}
|
|
||||||
.tool .out b{color:var(--fg);font-weight:600}
|
|
||||||
.tool .out .good{color:#15803d;font-weight:600}
|
|
||||||
.tool .out .bad{color:#b91c1c;font-weight:600}
|
|
||||||
.dlrow{display:flex;align-items:center;gap:10px;padding:7px 0;
|
|
||||||
border-bottom:1px solid var(--border-soft);font-size:12.5px}
|
|
||||||
.dlrow:last-child{border-bottom:none}
|
|
||||||
.dlrow .nm{flex:1;overflow:hidden;text-overflow:ellipsis;
|
|
||||||
white-space:nowrap;font-family:ui-monospace,Consolas,monospace;
|
|
||||||
font-size:11.5px}
|
|
||||||
.dlrow .sz{color:var(--faint);font-variant-numeric:tabular-nums}
|
|
||||||
|
|
||||||
/* ---------- 底部说明 ---------- */
|
|
||||||
.foot{margin-top:36px;padding-top:20px;border-top:1px solid var(--border);
|
|
||||||
color:var(--faint);font-size:11.5px;line-height:1.9}
|
|
||||||
.foot b{color:var(--muted);font-weight:600}
|
|
||||||
|
|
||||||
/* ---------- 提示条 ---------- */
|
|
||||||
#toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%)
|
|
||||||
translateY(20px);background:#1a1d21;color:#fff;padding:11px 20px;
|
|
||||||
border-radius:10px;font-size:13px;opacity:0;pointer-events:none;
|
|
||||||
transition:.22s;z-index:99;max-width:86vw;text-align:center}
|
|
||||||
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
||||||
#toast.ok{background:#15803d}
|
|
||||||
#toast.err{background:#b91c1c}
|
|
||||||
|
|
||||||
/* ---------- 窄屏:表格变卡片 ---------- */
|
|
||||||
@media(max-width:700px){
|
|
||||||
thead{display:none}
|
|
||||||
tbody tr{display:block;border-bottom:1px solid var(--border);
|
|
||||||
padding:12px 14px}
|
|
||||||
tbody td{display:flex;justify-content:space-between;gap:14px;
|
|
||||||
padding:5px 0;border:none}
|
|
||||||
tbody td::before{content:attr(data-l);color:var(--faint);font-size:11.5px;
|
|
||||||
flex:none}
|
|
||||||
td.pickcell::before,
|
|
||||||
td.ops::before{display:none}
|
|
||||||
td.pickcell{padding-bottom:8px}
|
|
||||||
td.ops{justify-content:flex-end;padding-top:10px;
|
|
||||||
border-top:1px solid var(--border-soft)}
|
|
||||||
td.empty{display:block;text-align:center}
|
|
||||||
td.empty::before{display:none}
|
|
||||||
.devid{max-width:200px}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
|
|
||||||
<div class="top"><div class="wrap top-in">
|
|
||||||
<div class="logo">yx</div>
|
|
||||||
<div class="brand">
|
|
||||||
<b>ykxmz 中转站</b>
|
|
||||||
<span>跨网络中转 · 管理后台</span>
|
|
||||||
</div>
|
|
||||||
<span class="state"><i></i>运行中</span>
|
|
||||||
<div class="top-right">
|
|
||||||
<div>版本 <b>1.7.1</b> · 已运行 <b>9 小时 10 分</b> · 内存 <b>22 MB</b></div>
|
|
||||||
<div>累计请求 <b>154</b> · <a class="logout" href="/zhongzhuanzhan/logout">退出登录</a></div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
|
|
||||||
<div class="wrap">
|
|
||||||
|
|
||||||
<div class="hero">
|
|
||||||
<h2>公网地址 —— 手表「中转设置」里填这个</h2>
|
|
||||||
<div class="urlbox">
|
|
||||||
<div class="url" id="urlBox">https://relay.xxmxxs.top</div>
|
|
||||||
<button class="primary" id="copyUrl">复制</button>
|
|
||||||
</div>
|
|
||||||
<div class="hint">手表和电脑不在同一个网络时,两边都通过这个地址中转。</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nums">
|
|
||||||
<div class="num"><div class="v">0</div><div class="k">登记设备</div></div>
|
|
||||||
<div class="num "><div class="v">0</div><div class="k">在线</div></div>
|
|
||||||
<div class="num "><div class="v">0</div><div class="k">手表会话</div></div>
|
|
||||||
<div class="num"><div class="v">0</div><div class="k">配对码占用</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sec">
|
|
||||||
<h3>设备</h3>
|
|
||||||
<div class="right">
|
|
||||||
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
|
|
||||||
<input type="checkbox" id="autoRef" checked> 自动刷新
|
|
||||||
</label>
|
|
||||||
<button class="danger sm" id="delPicked" disabled>删除选中</button>
|
|
||||||
<button class="danger sm" id="delAll">删除全部</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<table>
|
|
||||||
<thead><tr>
|
|
||||||
<th style="width:38px"><input type="checkbox" id="pickAll" title="全选"></th>
|
|
||||||
<th>设备</th><th>配对码</th><th>状态</th>
|
|
||||||
<th>空闲</th><th>待发 表/机</th><th>流量 入/出</th><th></th>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td colspan="8" class="empty"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:.28;margin-bottom:14px"><rect x="2" y="4" width="20" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg><div class="big">还没有电脑连上来</div><p>在教室那台电脑上双击 ykxmz.exe 设好 6 位配对码,手表上输这 6 位数字就能连上。</p></td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tools">
|
|
||||||
<div class="tool" id="netPanel">
|
|
||||||
<h4>网络体检</h4>
|
|
||||||
<div class="d">看看这台机器是走局域网还是走隧道。</div>
|
|
||||||
<div class="out" id="netBody">还没测过。</div>
|
|
||||||
<div style="margin-top:14px"><button class="sm" id="netBtn">测一下</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="tool">
|
|
||||||
<h4>下载区</h4>
|
|
||||||
<div class="d">官网下载按钮指向这里的文件。</div>
|
|
||||||
<div id="dlBody"><div class="out">还没有上传任何安装包。把文件放到中转的 downloads 目录即可。</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="foot">
|
|
||||||
<b>隐私说明</b> —— 数据来自 /v1/health,只显示配对码和流量数字,
|
|
||||||
看不到任何传输内容。中转只转发字节,不解析、不存储会话内容。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="toast"></div>
|
|
||||||
<script>
|
|
||||||
const toast = (msg, kind) => {
|
|
||||||
const el = document.getElementById('toast');
|
|
||||||
el.textContent = msg;
|
|
||||||
el.className = kind ? ('show ' + kind) : 'show';
|
|
||||||
clearTimeout(el._t);
|
|
||||||
el._t = setTimeout(() => { el.className = ''; }, 2800);
|
|
||||||
};
|
|
||||||
|
|
||||||
const post = async (path, payload) => {
|
|
||||||
const r = await fetch(path, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify(payload || {})
|
|
||||||
});
|
|
||||||
return r.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 单台删除 ----------------
|
|
||||||
document.querySelectorAll('button.danger[data-device]').forEach(b => {
|
|
||||||
b.onclick = async () => {
|
|
||||||
const name = b.dataset.name, code = b.dataset.code;
|
|
||||||
if (!confirm('删除设备「' + name + '」(配对码 ' + code + ')?\n\n'
|
|
||||||
+ '这会:\n 1. 断开它现在的手表连接\n'
|
|
||||||
+ ' 2. 从服务器删掉这台设备\n'
|
|
||||||
+ ' 3. 清掉它的配对码绑定')) return;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete', {deviceId: b.dataset.device});
|
|
||||||
if (j.ok) {
|
|
||||||
toast('已删除「' + (j.name || name) + '」,清掉配对码 '
|
|
||||||
+ (j.codesCleared || []).length + ' 条', 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} else {
|
|
||||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------- 批量选择 ----------------
|
|
||||||
const picks = () => Array.from(document.querySelectorAll('input.pick'));
|
|
||||||
const picked = () => picks().filter(c => c.checked).map(c => c.value);
|
|
||||||
|
|
||||||
function refreshPickState() {
|
|
||||||
const chosen = picked().length;
|
|
||||||
const btn = document.getElementById('delPicked');
|
|
||||||
if (btn) {
|
|
||||||
btn.disabled = chosen === 0;
|
|
||||||
btn.textContent = chosen ? ('删除选中(' + chosen + ')') : '删除选中';
|
|
||||||
}
|
|
||||||
picks().forEach(c => {
|
|
||||||
const tr = c.closest('tr');
|
|
||||||
if (tr) tr.classList.toggle('picked', c.checked);
|
|
||||||
});
|
|
||||||
const all = document.getElementById('pickAll');
|
|
||||||
if (all) {
|
|
||||||
const total = picks().length;
|
|
||||||
all.checked = total > 0 && chosen === total;
|
|
||||||
all.indeterminate = chosen > 0 && chosen < total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
picks().forEach(c => c.addEventListener('change', refreshPickState));
|
|
||||||
const pickAll = document.getElementById('pickAll');
|
|
||||||
if (pickAll) pickAll.addEventListener('change', () => {
|
|
||||||
picks().forEach(c => { c.checked = pickAll.checked; });
|
|
||||||
refreshPickState();
|
|
||||||
});
|
|
||||||
refreshPickState();
|
|
||||||
|
|
||||||
// ---------------- 批量删除 ----------------
|
|
||||||
const delPicked = document.getElementById('delPicked');
|
|
||||||
if (delPicked) delPicked.onclick = async () => {
|
|
||||||
const ids = picked();
|
|
||||||
if (!ids.length) return;
|
|
||||||
if (!confirm('删除选中的 ' + ids.length + ' 台设备?\n\n'
|
|
||||||
+ '会断开它们现在的手表连接,并清掉它们占的配对码。')) return;
|
|
||||||
delPicked.disabled = true;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete_many', {deviceIds: ids});
|
|
||||||
if (j.ok) {
|
|
||||||
toast('已删除 ' + j.deleted + ' 台设备'
|
|
||||||
+ (j.failed ? (',' + j.failed + ' 台失败') : ''), 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} else {
|
|
||||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
delPicked.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
delPicked.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 删除全部 ----------------
|
|
||||||
const delAll = document.getElementById('delAll');
|
|
||||||
if (delAll) delAll.onclick = async () => {
|
|
||||||
if (!confirm('删除全部设备(连同所有配对码)?\n\n'
|
|
||||||
+ '清场用,删完所有电脑都要重新设码。')) return;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete_all');
|
|
||||||
toast('已删除 ' + (j.deleted || 0) + ' 台设备', 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 复制公网地址 ----------------
|
|
||||||
const copyUrl = document.getElementById('copyUrl');
|
|
||||||
if (copyUrl) copyUrl.onclick = async () => {
|
|
||||||
const text = document.getElementById('urlBox').textContent.trim();
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
toast('地址已复制', 'ok');
|
|
||||||
} catch (e) {
|
|
||||||
// 非安全上下文(http 且不是 localhost)没有 clipboard API,
|
|
||||||
// 退回到老办法:选中 + execCommand
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(document.getElementById('urlBox'));
|
|
||||||
const sel = window.getSelection();
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
try { document.execCommand('copy'); toast('地址已复制', 'ok'); }
|
|
||||||
catch (e2) { toast('复制失败,请手动选中', 'err'); }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 网络体检 ----------------
|
|
||||||
const netBtn = document.getElementById('netBtn');
|
|
||||||
if (netBtn) netBtn.onclick = async () => {
|
|
||||||
const body = document.getElementById('netBody');
|
|
||||||
body.textContent = '正在测…';
|
|
||||||
try {
|
|
||||||
const r = await fetch('/v1/admin/netcheck');
|
|
||||||
const j = await r.json();
|
|
||||||
if (!j.ok) {
|
|
||||||
body.innerHTML = '<span class="bad">读不到:'
|
|
||||||
+ (j.error || '未知') + '</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rows = [];
|
|
||||||
if (j.tunnelMetrics) {
|
|
||||||
const conns = j.tunnelConnections || 0;
|
|
||||||
const cls = conns >= 2 ? 'good' : 'bad';
|
|
||||||
rows.push('隧道连接数:<b class="' + cls + '">' + conns + '</b>'
|
|
||||||
+ (conns === 0 ? '(断了,看门狗会自动重启)' : ''));
|
|
||||||
rows.push('隧道累计请求:<b>' + (j.tunnelRequests || 0) + '</b>');
|
|
||||||
} else {
|
|
||||||
rows.push('<span class="bad">读不到隧道指标'
|
|
||||||
+ (j.tunnelError ? '(' + j.tunnelError + ')' : '') + '</span>');
|
|
||||||
}
|
|
||||||
rows.push('登记设备:<b>' + j.devices + '</b>(在线 <b>' + j.online
|
|
||||||
+ '</b>)');
|
|
||||||
rows.push('手表会话:<b>' + j.sessions + '</b>');
|
|
||||||
rows.push('中转内存:<b>' + Math.round(j.memoryKB / 1024) + ' MB</b>');
|
|
||||||
rows.push('已运行:<b>' + Math.round(j.uptimeSeconds / 60) + ' 分钟</b>');
|
|
||||||
if (j.publicUrl) {
|
|
||||||
rows.push('公网地址:<b>' + j.publicUrl + '</b>');
|
|
||||||
}
|
|
||||||
body.innerHTML = rows.map(t => '<div>' + t + '</div>').join('');
|
|
||||||
// 顺便测浏览器到中转的往返
|
|
||||||
const t0 = performance.now();
|
|
||||||
try {
|
|
||||||
await fetch('/v1/health?t=' + Date.now(), {cache: 'no-store'});
|
|
||||||
const ms = Math.round(performance.now() - t0);
|
|
||||||
body.innerHTML += '<div>浏览器到中转往返:<b class="'
|
|
||||||
+ (ms < 300 ? 'good' : 'bad') + '">' + ms + ' ms</b>'
|
|
||||||
+ (ms < 300 ? '(很顺)' : (ms < 1000 ? '(一般)' : '(有点慢)'))
|
|
||||||
+ '</div>';
|
|
||||||
} catch (e) {
|
|
||||||
body.innerHTML += '<div class="bad">浏览器到中转:失败</div>';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
body.textContent = '请求出错:' + e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 自动刷新 ----------------
|
|
||||||
const autoRef = document.getElementById('autoRef');
|
|
||||||
let autoTimer = null;
|
|
||||||
function setupAuto() {
|
|
||||||
clearInterval(autoTimer);
|
|
||||||
if (autoRef && autoRef.checked) {
|
|
||||||
autoTimer = setInterval(() => {
|
|
||||||
// 有人正在勾选/删除时别刷新,免得打断操作
|
|
||||||
if (picked().length === 0 && !document.hidden) location.reload();
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (autoRef) {
|
|
||||||
autoRef.addEventListener('change', () => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('lr_autoref', autoRef.checked ? '1' : '0');
|
|
||||||
} catch (e) {}
|
|
||||||
setupAuto();
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (localStorage.getItem('lr_autoref') === '0') autoRef.checked = false;
|
|
||||||
} catch (e) {}
|
|
||||||
setupAuto();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body></html>
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN"><head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="https://relay.xxmxxs.top/favicon.ico">
|
|
||||||
<title>ykxmz 中转站</title>
|
|
||||||
<style>
|
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
|
||||||
:root{
|
|
||||||
--bg:#ffffff; --surface:#fafbfc; --border:#e6e8eb; --border-soft:#f0f2f5;
|
|
||||||
--fg:#1a1d21; --muted:#6b7280; --faint:#9ca3af;
|
|
||||||
--accent:#2563eb; --ok:#16a34a; --warn:#d97706; --danger:#dc2626;
|
|
||||||
}
|
|
||||||
html,body{max-width:100%;overflow-x:hidden}
|
|
||||||
body{background:var(--bg);color:var(--fg);line-height:1.6;
|
|
||||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',
|
|
||||||
'PingFang SC',sans-serif;-webkit-font-smoothing:antialiased}
|
|
||||||
a{color:inherit}
|
|
||||||
.wrap{width:100%;max-width:1040px;margin:0 auto;padding:0 20px 64px}
|
|
||||||
|
|
||||||
/* ---------- 吸顶顶栏 ---------- */
|
|
||||||
.top{position:sticky;top:0;z-index:40;background:rgba(255,255,255,.88);
|
|
||||||
backdrop-filter:saturate(180%) blur(12px);border-bottom:1px solid var(--border)}
|
|
||||||
/*
|
|
||||||
* ⚠ 别用固定 height。
|
|
||||||
* 原来写的是 .top-in{height:62px},但里面品牌是「标题 + 副标题」两行、
|
|
||||||
* 右边还有两行版本信息 —— 实际内容比 62px 高,
|
|
||||||
* flex 就把内容往上顶出去了:**标题被切掉、运行中徽章压在字上**
|
|
||||||
* (用户截图里就是这样)。
|
|
||||||
* 改成 padding 撑开,内容多高就是多高。
|
|
||||||
*/
|
|
||||||
.top-in{display:flex;align-items:center;gap:12px;
|
|
||||||
padding-top:13px;padding-bottom:13px;min-height:36px}
|
|
||||||
.logo{width:32px;height:32px;border-radius:9px;flex:none;
|
|
||||||
background:linear-gradient(140deg,#2563eb,#1d4ed8);
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
|
||||||
color:#fff;font-weight:700;font-size:12px;letter-spacing:-.3px}
|
|
||||||
.brand{flex:none;min-width:0}
|
|
||||||
.brand b{display:block;font-size:15px;font-weight:650;letter-spacing:-.2px;
|
|
||||||
line-height:1.35;white-space:nowrap}
|
|
||||||
.brand span{display:block;color:var(--faint);font-size:11px;
|
|
||||||
line-height:1.4;white-space:nowrap}
|
|
||||||
.state{margin-left:8px;flex:none;display:inline-flex;align-items:center;gap:6px;
|
|
||||||
padding:3px 10px;border-radius:999px;background:#f0fdf4;
|
|
||||||
color:#15803d;font-size:11px;font-weight:600;white-space:nowrap}
|
|
||||||
.state i{width:6px;height:6px;border-radius:50%;background:#22c55e;
|
|
||||||
display:block;flex:none}
|
|
||||||
.top-right{margin-left:auto;text-align:right;font-size:11px;
|
|
||||||
color:var(--faint);line-height:1.65;min-width:0}
|
|
||||||
.top-right b{color:var(--muted);font-weight:500}
|
|
||||||
.logout{color:var(--muted);text-decoration:none;
|
|
||||||
border-bottom:1px dashed var(--border);transition:.15s}
|
|
||||||
.logout:hover{color:var(--danger);border-bottom-color:var(--danger)}
|
|
||||||
|
|
||||||
/* 窄屏:状态徽章换行,别和标题挤 */
|
|
||||||
@media(max-width:640px){
|
|
||||||
.top-in{flex-wrap:wrap;gap:8px}
|
|
||||||
.state{margin-left:auto}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 公网地址(最重要,给最大权重)---------- */
|
|
||||||
.hero{padding:30px 0 26px}
|
|
||||||
.hero h2{font-size:13px;font-weight:600;color:var(--muted);
|
|
||||||
margin-bottom:10px;letter-spacing:.2px}
|
|
||||||
.urlbox{display:flex;align-items:stretch;gap:10px;flex-wrap:wrap}
|
|
||||||
.url{flex:1;min-width:240px;display:flex;align-items:center;
|
|
||||||
font:600 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;
|
|
||||||
color:var(--accent);word-break:break-all;
|
|
||||||
background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:11px;padding:14px 16px}
|
|
||||||
.url.empty{color:var(--faint);font-weight:400;font-size:14px}
|
|
||||||
.hint{color:var(--faint);font-size:12px;margin-top:9px}
|
|
||||||
|
|
||||||
/* ---------- 数字卡 ---------- */
|
|
||||||
.nums{display:grid;gap:10px;grid-template-columns:repeat(2,1fr);
|
|
||||||
margin-bottom:30px}
|
|
||||||
@media(min-width:640px){.nums{grid-template-columns:repeat(4,1fr)}}
|
|
||||||
.num{background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:12px;padding:15px 16px}
|
|
||||||
.num .v{font-size:25px;font-weight:700;letter-spacing:-.7px;
|
|
||||||
line-height:1.1;font-variant-numeric:tabular-nums}
|
|
||||||
.num .k{color:var(--muted);font-size:12px;margin-top:3px}
|
|
||||||
.num.live{background:#f0fdf4;border-color:#bbf7d0}
|
|
||||||
.num.live .v{color:#15803d}
|
|
||||||
.num.busy{background:#fffbeb;border-color:#fde68a}
|
|
||||||
.num.busy .v{color:#b45309}
|
|
||||||
|
|
||||||
/* ---------- 区块标题 ---------- */
|
|
||||||
.sec{display:flex;align-items:center;gap:12px;margin-bottom:12px}
|
|
||||||
.sec h3{font-size:15px;font-weight:650;letter-spacing:-.2px}
|
|
||||||
.sec .right{margin-left:auto;display:flex;align-items:center;gap:12px}
|
|
||||||
|
|
||||||
/* ---------- 表格 ---------- */
|
|
||||||
.card{background:#fff;border:1px solid var(--border);border-radius:13px;
|
|
||||||
overflow:hidden}
|
|
||||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
||||||
thead th{background:var(--surface);text-align:left;font-weight:600;
|
|
||||||
color:var(--muted);font-size:11.5px;letter-spacing:.3px;
|
|
||||||
padding:11px 14px;border-bottom:1px solid var(--border);
|
|
||||||
white-space:nowrap}
|
|
||||||
tbody td{padding:13px 14px;border-bottom:1px solid var(--border-soft);
|
|
||||||
vertical-align:middle}
|
|
||||||
tbody tr:last-child td{border-bottom:none}
|
|
||||||
tbody tr:hover td{background:#fbfcfd}
|
|
||||||
tbody tr.picked td{background:#eff6ff}
|
|
||||||
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;
|
|
||||||
margin-right:7px;flex:none}
|
|
||||||
.dot.on{background:#22c55e}
|
|
||||||
.dot.off{background:#d1d5db}
|
|
||||||
.devname{display:flex;align-items:center;font-weight:600}
|
|
||||||
.devid{color:var(--faint);font-size:10.5px;margin-top:2px;
|
|
||||||
font-family:ui-monospace,Consolas,monospace;
|
|
||||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px}
|
|
||||||
code{font:600 13px/1 ui-monospace,Consolas,monospace;
|
|
||||||
background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:6px;padding:4px 8px;letter-spacing:.5px}
|
|
||||||
.pill{padding:3px 9px;border-radius:999px;font-size:11px;font-weight:600}
|
|
||||||
.pill.on{background:#dcfce7;color:#15803d}
|
|
||||||
.pill.off{background:#f3f4f6;color:#6b7280}
|
|
||||||
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;
|
|
||||||
color:var(--muted)}
|
|
||||||
.ops{text-align:right}
|
|
||||||
.empty{padding:56px 20px;text-align:center}
|
|
||||||
.empty .big{font-size:15px;font-weight:600;margin-bottom:8px}
|
|
||||||
.empty p{color:var(--muted);font-size:13px;max-width:340px;margin:0 auto}
|
|
||||||
.empty .ico{font-size:30px;margin-bottom:14px;opacity:.35}
|
|
||||||
|
|
||||||
/* ---------- 按钮 ---------- */
|
|
||||||
button{font-family:inherit;font-size:13px;cursor:pointer;
|
|
||||||
border-radius:9px;padding:8px 15px;border:1px solid var(--border);
|
|
||||||
background:#fff;color:var(--text);transition:.14s;
|
|
||||||
font-weight:500}
|
|
||||||
button:hover:not(:disabled){background:var(--surface);
|
|
||||||
border-color:#d1d5db}
|
|
||||||
button:active:not(:disabled){transform:translateY(1px)}
|
|
||||||
button:disabled{opacity:.42;cursor:not-allowed}
|
|
||||||
button.primary{background:var(--accent);border-color:var(--accent);
|
|
||||||
color:#fff;font-weight:600}
|
|
||||||
button.primary:hover:not(:disabled){background:#1d4ed8;
|
|
||||||
border-color:#1d4ed8}
|
|
||||||
button.danger{color:#b91c1c;border-color:#fecaca;background:#fff}
|
|
||||||
button.danger:hover:not(:disabled){background:#fef2f2;
|
|
||||||
border-color:#fca5a5}
|
|
||||||
button.sm{padding:6px 12px;font-size:12px}
|
|
||||||
|
|
||||||
/* ---------- 工具卡 ---------- */
|
|
||||||
.tools{display:grid;gap:14px;margin-top:30px}
|
|
||||||
@media(min-width:720px){.tools{grid-template-columns:1fr 1fr}}
|
|
||||||
.tool{border:1px solid var(--border);border-radius:13px;padding:18px 18px}
|
|
||||||
.tool h4{font-size:13.5px;font-weight:650;margin-bottom:4px}
|
|
||||||
.tool .d{color:var(--muted);font-size:12.5px;margin-bottom:14px}
|
|
||||||
.tool .out{color:var(--muted);font-size:12.5px;line-height:1.85;
|
|
||||||
word-break:break-all}
|
|
||||||
.tool .out b{color:var(--fg);font-weight:600}
|
|
||||||
.tool .out .good{color:#15803d;font-weight:600}
|
|
||||||
.tool .out .bad{color:#b91c1c;font-weight:600}
|
|
||||||
.dlrow{display:flex;align-items:center;gap:10px;padding:7px 0;
|
|
||||||
border-bottom:1px solid var(--border-soft);font-size:12.5px}
|
|
||||||
.dlrow:last-child{border-bottom:none}
|
|
||||||
.dlrow .nm{flex:1;overflow:hidden;text-overflow:ellipsis;
|
|
||||||
white-space:nowrap;font-family:ui-monospace,Consolas,monospace;
|
|
||||||
font-size:11.5px}
|
|
||||||
.dlrow .sz{color:var(--faint);font-variant-numeric:tabular-nums}
|
|
||||||
|
|
||||||
/* ---------- 底部说明 ---------- */
|
|
||||||
.foot{margin-top:36px;padding-top:20px;border-top:1px solid var(--border);
|
|
||||||
color:var(--faint);font-size:11.5px;line-height:1.9}
|
|
||||||
.foot b{color:var(--muted);font-weight:600}
|
|
||||||
|
|
||||||
/* ---------- 提示条 ---------- */
|
|
||||||
#toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%)
|
|
||||||
translateY(20px);background:#1a1d21;color:#fff;padding:11px 20px;
|
|
||||||
border-radius:10px;font-size:13px;opacity:0;pointer-events:none;
|
|
||||||
transition:.22s;z-index:99;max-width:86vw;text-align:center}
|
|
||||||
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
||||||
#toast.ok{background:#15803d}
|
|
||||||
#toast.err{background:#b91c1c}
|
|
||||||
|
|
||||||
/* ---------- 窄屏:表格变卡片 ---------- */
|
|
||||||
@media(max-width:700px){
|
|
||||||
thead{display:none}
|
|
||||||
tbody tr{display:block;border-bottom:1px solid var(--border);
|
|
||||||
padding:12px 14px}
|
|
||||||
tbody td{display:flex;justify-content:space-between;gap:14px;
|
|
||||||
padding:5px 0;border:none}
|
|
||||||
tbody td::before{content:attr(data-l);color:var(--faint);font-size:11.5px;
|
|
||||||
flex:none}
|
|
||||||
td.pickcell::before,
|
|
||||||
td.ops::before{display:none}
|
|
||||||
td.pickcell{padding-bottom:8px}
|
|
||||||
td.ops{justify-content:flex-end;padding-top:10px;
|
|
||||||
border-top:1px solid var(--border-soft)}
|
|
||||||
td.empty{display:block;text-align:center}
|
|
||||||
td.empty::before{display:none}
|
|
||||||
.devid{max-width:200px}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
|
|
||||||
<div class="top"><div class="wrap top-in">
|
|
||||||
<div class="logo">yx</div>
|
|
||||||
<div class="brand">
|
|
||||||
<b>ykxmz 中转站</b>
|
|
||||||
<span>跨网络中转 · 管理后台</span>
|
|
||||||
</div>
|
|
||||||
<span class="state"><i></i>运行中</span>
|
|
||||||
<div class="top-right">
|
|
||||||
<div>版本 <b>1.7.1</b> · 已运行 <b>6 分</b> · 内存 <b>23 MB</b></div>
|
|
||||||
<div>累计请求 <b>29</b> · <a class="logout" href="/zhongzhuanzhan/logout">退出登录</a></div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
|
|
||||||
<div class="wrap">
|
|
||||||
|
|
||||||
<div class="hero">
|
|
||||||
<h2>公网地址 —— 手表「中转设置」里填这个</h2>
|
|
||||||
<div class="urlbox">
|
|
||||||
<div class="url" id="urlBox">https://relay.xxmxxs.top</div>
|
|
||||||
<button class="primary" id="copyUrl">复制</button>
|
|
||||||
</div>
|
|
||||||
<div class="hint">手表和电脑不在同一个网络时,两边都通过这个地址中转。</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nums">
|
|
||||||
<div class="num"><div class="v">2</div><div class="k">登记设备</div></div>
|
|
||||||
<div class="num live"><div class="v">1</div><div class="k">在线</div></div>
|
|
||||||
<div class="num "><div class="v">0</div><div class="k">手表会话</div></div>
|
|
||||||
<div class="num"><div class="v">2</div><div class="k">配对码占用</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sec">
|
|
||||||
<h3>设备</h3>
|
|
||||||
<div class="right">
|
|
||||||
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
|
|
||||||
<input type="checkbox" id="autoRef" checked> 自动刷新
|
|
||||||
</label>
|
|
||||||
<button class="danger sm" id="delPicked" disabled>删除选中</button>
|
|
||||||
<button class="danger sm" id="delAll">删除全部</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<table>
|
|
||||||
<thead><tr>
|
|
||||||
<th style="width:38px"><input type="checkbox" id="pickAll" title="全选"></th>
|
|
||||||
<th>设备</th><th>配对码</th><th>状态</th>
|
|
||||||
<th>空闲</th><th>待发 表/机</th><th>流量 入/出</th><th></th>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td class="pickcell"><input type="checkbox" class="pick" value="DESKTOP-RN5I625-ec4b284cdecd" aria-label="选择 DESKTOP-RN5I625"></td><td data-l="设备"><div class="devname"><span class="dot off"></span>DESKTOP-RN5I625</div><div class="devid">DESKTOP-RN5I625-ec4b284cdecd</div></td><td data-l="配对码"><code>966173</code></td><td data-l="状态"><span class="pill off">离线</span></td><td data-l="空闲"><span class="mono">314s</span></td><td data-l="待发 表/机"><span class="mono">0 / 0</span></td><td data-l="流量 入/出"><span class="mono">0 B / 0 B</span></td><td class="ops"><button class="danger sm" data-device="DESKTOP-RN5I625-ec4b284cdecd" data-name="DESKTOP-RN5I625" data-code="966173">删除</button></td></tr><tr><td class="pickcell"><input type="checkbox" class="pick" value="DESKTOP-RN5I625-2f882bd8ed14" aria-label="选择 DESKTOP-RN5I625"></td><td data-l="设备"><div class="devname"><span class="dot on"></span>DESKTOP-RN5I625</div><div class="devid">DESKTOP-RN5I625-2f882bd8ed14</div></td><td data-l="配对码"><code>246813</code></td><td data-l="状态"><span class="pill on">在线</span></td><td data-l="空闲"><span class="mono">26s</span></td><td data-l="待发 表/机"><span class="mono">0 / 0</span></td><td data-l="流量 入/出"><span class="mono">0 B / 0 B</span></td><td class="ops"><button class="danger sm" data-device="DESKTOP-RN5I625-2f882bd8ed14" data-name="DESKTOP-RN5I625" data-code="246813">删除</button></td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tools">
|
|
||||||
<div class="tool" id="netPanel">
|
|
||||||
<h4>网络体检</h4>
|
|
||||||
<div class="d">看看这台机器是走局域网还是走隧道。</div>
|
|
||||||
<div class="out" id="netBody">还没测过。</div>
|
|
||||||
<div style="margin-top:14px"><button class="sm" id="netBtn">测一下</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="tool">
|
|
||||||
<h4>下载区</h4>
|
|
||||||
<div class="d">官网下载按钮指向这里的文件。</div>
|
|
||||||
<div id="dlBody"><div class="dlrow"><span class="nm">ykxmz-watch-1.7.1.apk</span><span class="sz">197 KB</span></div><div class="dlrow"><span class="nm">ykxmz-server-1.7.1.zip</span><span class="sz">198 KB</span></div><div class="dlrow"><span class="nm">ykxmz-setup-1.7.1.exe</span><span class="sz">14.4 MB</span></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="foot">
|
|
||||||
<b>隐私说明</b> —— 数据来自 /v1/health,只显示配对码和流量数字,
|
|
||||||
看不到任何传输内容。中转只转发字节,不解析、不存储会话内容。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="toast"></div>
|
|
||||||
<script>
|
|
||||||
const toast = (msg, kind) => {
|
|
||||||
const el = document.getElementById('toast');
|
|
||||||
el.textContent = msg;
|
|
||||||
el.className = kind ? ('show ' + kind) : 'show';
|
|
||||||
clearTimeout(el._t);
|
|
||||||
el._t = setTimeout(() => { el.className = ''; }, 2800);
|
|
||||||
};
|
|
||||||
|
|
||||||
const post = async (path, payload) => {
|
|
||||||
const r = await fetch(path, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify(payload || {})
|
|
||||||
});
|
|
||||||
return r.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 单台删除 ----------------
|
|
||||||
document.querySelectorAll('button.danger[data-device]').forEach(b => {
|
|
||||||
b.onclick = async () => {
|
|
||||||
const name = b.dataset.name, code = b.dataset.code;
|
|
||||||
if (!confirm('删除设备「' + name + '」(配对码 ' + code + ')?\n\n'
|
|
||||||
+ '这会:\n 1. 断开它现在的手表连接\n'
|
|
||||||
+ ' 2. 从服务器删掉这台设备\n'
|
|
||||||
+ ' 3. 清掉它的配对码绑定')) return;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete', {deviceId: b.dataset.device});
|
|
||||||
if (j.ok) {
|
|
||||||
toast('已删除「' + (j.name || name) + '」,清掉配对码 '
|
|
||||||
+ (j.codesCleared || []).length + ' 条', 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} else {
|
|
||||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------- 批量选择 ----------------
|
|
||||||
const picks = () => Array.from(document.querySelectorAll('input.pick'));
|
|
||||||
const picked = () => picks().filter(c => c.checked).map(c => c.value);
|
|
||||||
|
|
||||||
function refreshPickState() {
|
|
||||||
const chosen = picked().length;
|
|
||||||
const btn = document.getElementById('delPicked');
|
|
||||||
if (btn) {
|
|
||||||
btn.disabled = chosen === 0;
|
|
||||||
btn.textContent = chosen ? ('删除选中(' + chosen + ')') : '删除选中';
|
|
||||||
}
|
|
||||||
picks().forEach(c => {
|
|
||||||
const tr = c.closest('tr');
|
|
||||||
if (tr) tr.classList.toggle('picked', c.checked);
|
|
||||||
});
|
|
||||||
const all = document.getElementById('pickAll');
|
|
||||||
if (all) {
|
|
||||||
const total = picks().length;
|
|
||||||
all.checked = total > 0 && chosen === total;
|
|
||||||
all.indeterminate = chosen > 0 && chosen < total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
picks().forEach(c => c.addEventListener('change', refreshPickState));
|
|
||||||
const pickAll = document.getElementById('pickAll');
|
|
||||||
if (pickAll) pickAll.addEventListener('change', () => {
|
|
||||||
picks().forEach(c => { c.checked = pickAll.checked; });
|
|
||||||
refreshPickState();
|
|
||||||
});
|
|
||||||
refreshPickState();
|
|
||||||
|
|
||||||
// ---------------- 批量删除 ----------------
|
|
||||||
const delPicked = document.getElementById('delPicked');
|
|
||||||
if (delPicked) delPicked.onclick = async () => {
|
|
||||||
const ids = picked();
|
|
||||||
if (!ids.length) return;
|
|
||||||
if (!confirm('删除选中的 ' + ids.length + ' 台设备?\n\n'
|
|
||||||
+ '会断开它们现在的手表连接,并清掉它们占的配对码。')) return;
|
|
||||||
delPicked.disabled = true;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete_many', {deviceIds: ids});
|
|
||||||
if (j.ok) {
|
|
||||||
toast('已删除 ' + j.deleted + ' 台设备'
|
|
||||||
+ (j.failed ? (',' + j.failed + ' 台失败') : ''), 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} else {
|
|
||||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
delPicked.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
delPicked.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 删除全部 ----------------
|
|
||||||
const delAll = document.getElementById('delAll');
|
|
||||||
if (delAll) delAll.onclick = async () => {
|
|
||||||
if (!confirm('删除全部设备(连同所有配对码)?\n\n'
|
|
||||||
+ '清场用,删完所有电脑都要重新设码。')) return;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete_all');
|
|
||||||
toast('已删除 ' + (j.deleted || 0) + ' 台设备', 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 复制公网地址 ----------------
|
|
||||||
const copyUrl = document.getElementById('copyUrl');
|
|
||||||
if (copyUrl) copyUrl.onclick = async () => {
|
|
||||||
const text = document.getElementById('urlBox').textContent.trim();
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
toast('地址已复制', 'ok');
|
|
||||||
} catch (e) {
|
|
||||||
// 非安全上下文(http 且不是 localhost)没有 clipboard API,
|
|
||||||
// 退回到老办法:选中 + execCommand
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(document.getElementById('urlBox'));
|
|
||||||
const sel = window.getSelection();
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
try { document.execCommand('copy'); toast('地址已复制', 'ok'); }
|
|
||||||
catch (e2) { toast('复制失败,请手动选中', 'err'); }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 网络体检 ----------------
|
|
||||||
const netBtn = document.getElementById('netBtn');
|
|
||||||
if (netBtn) netBtn.onclick = async () => {
|
|
||||||
const body = document.getElementById('netBody');
|
|
||||||
body.textContent = '正在测…';
|
|
||||||
try {
|
|
||||||
const r = await fetch('/v1/admin/netcheck');
|
|
||||||
const j = await r.json();
|
|
||||||
if (!j.ok) {
|
|
||||||
body.innerHTML = '<span class="bad">读不到:'
|
|
||||||
+ (j.error || '未知') + '</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rows = [];
|
|
||||||
if (j.tunnelMetrics) {
|
|
||||||
const conns = j.tunnelConnections || 0;
|
|
||||||
const cls = conns >= 2 ? 'good' : 'bad';
|
|
||||||
rows.push('隧道连接数:<b class="' + cls + '">' + conns + '</b>'
|
|
||||||
+ (conns === 0 ? '(断了,看门狗会自动重启)' : ''));
|
|
||||||
rows.push('隧道累计请求:<b>' + (j.tunnelRequests || 0) + '</b>');
|
|
||||||
} else {
|
|
||||||
rows.push('<span class="bad">读不到隧道指标'
|
|
||||||
+ (j.tunnelError ? '(' + j.tunnelError + ')' : '') + '</span>');
|
|
||||||
}
|
|
||||||
rows.push('登记设备:<b>' + j.devices + '</b>(在线 <b>' + j.online
|
|
||||||
+ '</b>)');
|
|
||||||
rows.push('手表会话:<b>' + j.sessions + '</b>');
|
|
||||||
rows.push('中转内存:<b>' + Math.round(j.memoryKB / 1024) + ' MB</b>');
|
|
||||||
rows.push('已运行:<b>' + Math.round(j.uptimeSeconds / 60) + ' 分钟</b>');
|
|
||||||
if (j.publicUrl) {
|
|
||||||
rows.push('公网地址:<b>' + j.publicUrl + '</b>');
|
|
||||||
}
|
|
||||||
body.innerHTML = rows.map(t => '<div>' + t + '</div>').join('');
|
|
||||||
// 顺便测浏览器到中转的往返
|
|
||||||
const t0 = performance.now();
|
|
||||||
try {
|
|
||||||
await fetch('/v1/health?t=' + Date.now(), {cache: 'no-store'});
|
|
||||||
const ms = Math.round(performance.now() - t0);
|
|
||||||
body.innerHTML += '<div>浏览器到中转往返:<b class="'
|
|
||||||
+ (ms < 300 ? 'good' : 'bad') + '">' + ms + ' ms</b>'
|
|
||||||
+ (ms < 300 ? '(很顺)' : (ms < 1000 ? '(一般)' : '(有点慢)'))
|
|
||||||
+ '</div>';
|
|
||||||
} catch (e) {
|
|
||||||
body.innerHTML += '<div class="bad">浏览器到中转:失败</div>';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
body.textContent = '请求出错:' + e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 自动刷新 ----------------
|
|
||||||
const autoRef = document.getElementById('autoRef');
|
|
||||||
let autoTimer = null;
|
|
||||||
function setupAuto() {
|
|
||||||
clearInterval(autoTimer);
|
|
||||||
if (autoRef && autoRef.checked) {
|
|
||||||
autoTimer = setInterval(() => {
|
|
||||||
// 有人正在勾选/删除时别刷新,免得打断操作
|
|
||||||
if (picked().length === 0 && !document.hidden) location.reload();
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (autoRef) {
|
|
||||||
autoRef.addEventListener('change', () => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('lr_autoref', autoRef.checked ? '1' : '0');
|
|
||||||
} catch (e) {}
|
|
||||||
setupAuto();
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (localStorage.getItem('lr_autoref') === '0') autoRef.checked = false;
|
|
||||||
} catch (e) {}
|
|
||||||
setupAuto();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body></html>
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN"><head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<title>ykxmz 中转站</title>
|
|
||||||
<style>
|
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
|
||||||
:root{
|
|
||||||
--bg:#ffffff; --surface:#fafbfc; --border:#e6e8eb; --border-soft:#f0f2f5;
|
|
||||||
--fg:#1a1d21; --muted:#6b7280; --faint:#9ca3af;
|
|
||||||
--accent:#2563eb; --ok:#16a34a; --warn:#d97706; --danger:#dc2626;
|
|
||||||
}
|
|
||||||
html,body{max-width:100%;overflow-x:hidden}
|
|
||||||
body{background:var(--bg);color:var(--fg);line-height:1.6;
|
|
||||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',
|
|
||||||
'PingFang SC',sans-serif;-webkit-font-smoothing:antialiased}
|
|
||||||
a{color:inherit}
|
|
||||||
.wrap{width:100%;max-width:1040px;margin:0 auto;padding:0 20px 64px}
|
|
||||||
|
|
||||||
/* ---------- 吸顶顶栏 ---------- */
|
|
||||||
.top{position:sticky;top:0;z-index:40;background:rgba(255,255,255,.88);
|
|
||||||
backdrop-filter:saturate(180%) blur(12px);border-bottom:1px solid var(--border)}
|
|
||||||
.top-in{display:flex;align-items:center;gap:12px;height:62px}
|
|
||||||
.logo{width:32px;height:32px;border-radius:9px;flex:none;
|
|
||||||
background:linear-gradient(140deg,#2563eb,#1d4ed8);
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
|
||||||
color:#fff;font-weight:700;font-size:12px;letter-spacing:-.3px}
|
|
||||||
.brand b{display:block;font-size:15px;font-weight:650;letter-spacing:-.2px}
|
|
||||||
.brand span{display:block;color:var(--faint);font-size:11px}
|
|
||||||
.state{margin-left:14px;display:inline-flex;align-items:center;gap:6px;
|
|
||||||
padding:3px 10px;border-radius:999px;background:#f0fdf4;
|
|
||||||
color:#15803d;font-size:11px;font-weight:600}
|
|
||||||
.state i{width:6px;height:6px;border-radius:50%;background:#22c55e;
|
|
||||||
display:block}
|
|
||||||
.top-right{margin-left:auto;text-align:right;font-size:11px;
|
|
||||||
color:var(--faint);line-height:1.7}
|
|
||||||
.top-right b{color:var(--muted);font-weight:500}
|
|
||||||
.logout{color:var(--muted);text-decoration:none;
|
|
||||||
border-bottom:1px dashed var(--border);transition:.15s}
|
|
||||||
.logout:hover{color:var(--danger);border-bottom-color:var(--danger)}
|
|
||||||
|
|
||||||
/* ---------- 公网地址(最重要,给最大权重)---------- */
|
|
||||||
.hero{padding:30px 0 26px}
|
|
||||||
.hero h2{font-size:13px;font-weight:600;color:var(--muted);
|
|
||||||
margin-bottom:10px;letter-spacing:.2px}
|
|
||||||
.urlbox{display:flex;align-items:stretch;gap:10px;flex-wrap:wrap}
|
|
||||||
.url{flex:1;min-width:240px;display:flex;align-items:center;
|
|
||||||
font:600 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;
|
|
||||||
color:var(--accent);word-break:break-all;
|
|
||||||
background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:11px;padding:14px 16px}
|
|
||||||
.url.empty{color:var(--faint);font-weight:400;font-size:14px}
|
|
||||||
.hint{color:var(--faint);font-size:12px;margin-top:9px}
|
|
||||||
|
|
||||||
/* ---------- 数字卡 ---------- */
|
|
||||||
.nums{display:grid;gap:10px;grid-template-columns:repeat(2,1fr);
|
|
||||||
margin-bottom:30px}
|
|
||||||
@media(min-width:640px){.nums{grid-template-columns:repeat(4,1fr)}}
|
|
||||||
.num{background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:12px;padding:15px 16px}
|
|
||||||
.num .v{font-size:25px;font-weight:700;letter-spacing:-.7px;
|
|
||||||
line-height:1.1;font-variant-numeric:tabular-nums}
|
|
||||||
.num .k{color:var(--muted);font-size:12px;margin-top:3px}
|
|
||||||
.num.live{background:#f0fdf4;border-color:#bbf7d0}
|
|
||||||
.num.live .v{color:#15803d}
|
|
||||||
.num.busy{background:#fffbeb;border-color:#fde68a}
|
|
||||||
.num.busy .v{color:#b45309}
|
|
||||||
|
|
||||||
/* ---------- 区块标题 ---------- */
|
|
||||||
.sec{display:flex;align-items:center;gap:12px;margin-bottom:12px}
|
|
||||||
.sec h3{font-size:15px;font-weight:650;letter-spacing:-.2px}
|
|
||||||
.sec .right{margin-left:auto;display:flex;align-items:center;gap:12px}
|
|
||||||
|
|
||||||
/* ---------- 表格 ---------- */
|
|
||||||
.card{background:#fff;border:1px solid var(--border);border-radius:13px;
|
|
||||||
overflow:hidden}
|
|
||||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
||||||
thead th{background:var(--surface);text-align:left;font-weight:600;
|
|
||||||
color:var(--muted);font-size:11.5px;letter-spacing:.3px;
|
|
||||||
padding:11px 14px;border-bottom:1px solid var(--border);
|
|
||||||
white-space:nowrap}
|
|
||||||
tbody td{padding:13px 14px;border-bottom:1px solid var(--border-soft);
|
|
||||||
vertical-align:middle}
|
|
||||||
tbody tr:last-child td{border-bottom:none}
|
|
||||||
tbody tr:hover td{background:#fbfcfd}
|
|
||||||
tbody tr.picked td{background:#eff6ff}
|
|
||||||
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;
|
|
||||||
margin-right:7px;flex:none}
|
|
||||||
.dot.on{background:#22c55e}
|
|
||||||
.dot.off{background:#d1d5db}
|
|
||||||
.devname{display:flex;align-items:center;font-weight:600}
|
|
||||||
.devid{color:var(--faint);font-size:10.5px;margin-top:2px;
|
|
||||||
font-family:ui-monospace,Consolas,monospace;
|
|
||||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px}
|
|
||||||
code{font:600 13px/1 ui-monospace,Consolas,monospace;
|
|
||||||
background:var(--surface);border:1px solid var(--border);
|
|
||||||
border-radius:6px;padding:4px 8px;letter-spacing:.5px}
|
|
||||||
.pill{padding:3px 9px;border-radius:999px;font-size:11px;font-weight:600}
|
|
||||||
.pill.on{background:#dcfce7;color:#15803d}
|
|
||||||
.pill.off{background:#f3f4f6;color:#6b7280}
|
|
||||||
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;
|
|
||||||
color:var(--muted)}
|
|
||||||
.ops{text-align:right}
|
|
||||||
.empty{padding:56px 20px;text-align:center}
|
|
||||||
.empty .big{font-size:15px;font-weight:600;margin-bottom:8px}
|
|
||||||
.empty p{color:var(--muted);font-size:13px;max-width:340px;margin:0 auto}
|
|
||||||
.empty .ico{font-size:30px;margin-bottom:14px;opacity:.35}
|
|
||||||
|
|
||||||
/* ---------- 按钮 ---------- */
|
|
||||||
button{font-family:inherit;font-size:13px;cursor:pointer;
|
|
||||||
border-radius:9px;padding:8px 15px;border:1px solid var(--border);
|
|
||||||
background:#fff;color:var(--text);transition:.14s;
|
|
||||||
font-weight:500}
|
|
||||||
button:hover:not(:disabled){background:var(--surface);
|
|
||||||
border-color:#d1d5db}
|
|
||||||
button:active:not(:disabled){transform:translateY(1px)}
|
|
||||||
button:disabled{opacity:.42;cursor:not-allowed}
|
|
||||||
button.primary{background:var(--accent);border-color:var(--accent);
|
|
||||||
color:#fff;font-weight:600}
|
|
||||||
button.primary:hover:not(:disabled){background:#1d4ed8;
|
|
||||||
border-color:#1d4ed8}
|
|
||||||
button.danger{color:#b91c1c;border-color:#fecaca;background:#fff}
|
|
||||||
button.danger:hover:not(:disabled){background:#fef2f2;
|
|
||||||
border-color:#fca5a5}
|
|
||||||
button.sm{padding:6px 12px;font-size:12px}
|
|
||||||
|
|
||||||
/* ---------- 工具卡 ---------- */
|
|
||||||
.tools{display:grid;gap:14px;margin-top:30px}
|
|
||||||
@media(min-width:720px){.tools{grid-template-columns:1fr 1fr}}
|
|
||||||
.tool{border:1px solid var(--border);border-radius:13px;padding:18px 18px}
|
|
||||||
.tool h4{font-size:13.5px;font-weight:650;margin-bottom:4px}
|
|
||||||
.tool .d{color:var(--muted);font-size:12.5px;margin-bottom:14px}
|
|
||||||
.tool .out{color:var(--muted);font-size:12.5px;line-height:1.85;
|
|
||||||
word-break:break-all}
|
|
||||||
.tool .out b{color:var(--fg);font-weight:600}
|
|
||||||
.tool .out .good{color:#15803d;font-weight:600}
|
|
||||||
.tool .out .bad{color:#b91c1c;font-weight:600}
|
|
||||||
.dlrow{display:flex;align-items:center;gap:10px;padding:7px 0;
|
|
||||||
border-bottom:1px solid var(--border-soft);font-size:12.5px}
|
|
||||||
.dlrow:last-child{border-bottom:none}
|
|
||||||
.dlrow .nm{flex:1;overflow:hidden;text-overflow:ellipsis;
|
|
||||||
white-space:nowrap;font-family:ui-monospace,Consolas,monospace;
|
|
||||||
font-size:11.5px}
|
|
||||||
.dlrow .sz{color:var(--faint);font-variant-numeric:tabular-nums}
|
|
||||||
|
|
||||||
/* ---------- 底部说明 ---------- */
|
|
||||||
.foot{margin-top:36px;padding-top:20px;border-top:1px solid var(--border);
|
|
||||||
color:var(--faint);font-size:11.5px;line-height:1.9}
|
|
||||||
.foot b{color:var(--muted);font-weight:600}
|
|
||||||
|
|
||||||
/* ---------- 提示条 ---------- */
|
|
||||||
#toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%)
|
|
||||||
translateY(20px);background:#1a1d21;color:#fff;padding:11px 20px;
|
|
||||||
border-radius:10px;font-size:13px;opacity:0;pointer-events:none;
|
|
||||||
transition:.22s;z-index:99;max-width:86vw;text-align:center}
|
|
||||||
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
||||||
#toast.ok{background:#15803d}
|
|
||||||
#toast.err{background:#b91c1c}
|
|
||||||
|
|
||||||
/* ---------- 窄屏:表格变卡片 ---------- */
|
|
||||||
@media(max-width:700px){
|
|
||||||
thead{display:none}
|
|
||||||
tbody tr{display:block;border-bottom:1px solid var(--border);
|
|
||||||
padding:12px 14px}
|
|
||||||
tbody td{display:flex;justify-content:space-between;gap:14px;
|
|
||||||
padding:5px 0;border:none}
|
|
||||||
tbody td::before{content:attr(data-l);color:var(--faint);font-size:11.5px;
|
|
||||||
flex:none}
|
|
||||||
td.pickcell::before,
|
|
||||||
td.ops::before{display:none}
|
|
||||||
td.pickcell{padding-bottom:8px}
|
|
||||||
td.ops{justify-content:flex-end;padding-top:10px;
|
|
||||||
border-top:1px solid var(--border-soft)}
|
|
||||||
td.empty{display:block;text-align:center}
|
|
||||||
td.empty::before{display:none}
|
|
||||||
.devid{max-width:200px}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
|
|
||||||
<div class="top"><div class="wrap top-in">
|
|
||||||
<div class="logo">yx</div>
|
|
||||||
<div class="brand">
|
|
||||||
<b>ykxmz 中转站</b>
|
|
||||||
<span>跨网络中转 · 管理后台</span>
|
|
||||||
</div>
|
|
||||||
<span class="state"><i></i>运行中</span>
|
|
||||||
<div class="top-right">
|
|
||||||
<div>版本 <b>1.7.1</b> · 已运行 <b>9 小时 10 分</b> · 内存 <b>24 MB</b></div>
|
|
||||||
<div>累计请求 <b>1919</b> · <a class="logout" href="/zhongzhuanzhan/logout">退出登录</a></div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
|
|
||||||
<div class="wrap">
|
|
||||||
|
|
||||||
<div class="hero">
|
|
||||||
<h2>公网地址 —— 手表「中转设置」里填这个</h2>
|
|
||||||
<div class="urlbox">
|
|
||||||
<div class="url" id="urlBox">https://relay.xxmxxs.top</div>
|
|
||||||
<button class="primary" id="copyUrl">复制</button>
|
|
||||||
</div>
|
|
||||||
<div class="hint">手表和电脑不在同一个网络时,两边都通过这个地址中转。</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nums">
|
|
||||||
<div class="num"><div class="v">2</div><div class="k">登记设备</div></div>
|
|
||||||
<div class="num live"><div class="v">1</div><div class="k">在线</div></div>
|
|
||||||
<div class="num "><div class="v">0</div><div class="k">手表会话</div></div>
|
|
||||||
<div class="num"><div class="v">2</div><div class="k">配对码占用</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sec">
|
|
||||||
<h3>设备</h3>
|
|
||||||
<div class="right">
|
|
||||||
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
|
|
||||||
<input type="checkbox" id="autoRef" checked> 自动刷新
|
|
||||||
</label>
|
|
||||||
<button class="danger sm" id="delPicked" disabled>删除选中</button>
|
|
||||||
<button class="danger sm" id="delAll">删除全部</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<table>
|
|
||||||
<thead><tr>
|
|
||||||
<th style="width:38px"><input type="checkbox" id="pickAll" title="全选"></th>
|
|
||||||
<th>设备</th><th>配对码</th><th>状态</th>
|
|
||||||
<th>空闲</th><th>待发 表/机</th><th>流量 入/出</th><th></th>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td class="pickcell"><input type="checkbox" class="pick" value="DESKTOP-ABC-123" aria-label="选择 教室电脑"></td><td data-l="设备"><div class="devname"><span class="dot on"></span>教室电脑</div><div class="devid">DESKTOP-ABC-123</div></td><td data-l="配对码"><code>246810</code></td><td data-l="状态"><span class="pill on">在线</span></td><td data-l="空闲"><span class="mono">3s</span></td><td data-l="待发 表/机"><span class="mono">0 / 0</span></td><td data-l="流量 入/出"><span class="mono">12 KB / 7 KB</span></td><td class="ops"><button class="danger sm" data-device="DESKTOP-ABC-123" data-name="教室电脑" data-code="246810">删除</button></td></tr><tr><td class="pickcell"><input type="checkbox" class="pick" value="DESKTOP-ABC-456" aria-label="选择 办公室电脑"></td><td data-l="设备"><div class="devname"><span class="dot off"></span>办公室电脑</div><div class="devid">DESKTOP-ABC-456</div></td><td data-l="配对码"><code>135790</code></td><td data-l="状态"><span class="pill off">离线</span></td><td data-l="空闲"><span class="mono">900s</span></td><td data-l="待发 表/机"><span class="mono">0 / 0</span></td><td data-l="流量 入/出"><span class="mono">0 B / 0 B</span></td><td class="ops"><button class="danger sm" data-device="DESKTOP-ABC-456" data-name="办公室电脑" data-code="135790">删除</button></td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tools">
|
|
||||||
<div class="tool">
|
|
||||||
<h4>网络体检</h4>
|
|
||||||
<div class="d">看看这台机器是走局域网还是走隧道。</div>
|
|
||||||
<div class="out" id="netBody">还没测过。</div>
|
|
||||||
<div style="margin-top:14px"><button class="sm" id="netBtn">测一下</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="tool">
|
|
||||||
<h4>下载区</h4>
|
|
||||||
<div class="d">官网下载按钮指向这里的文件。</div>
|
|
||||||
<div id="dlBody"><div class="dlrow"><span class="nm">ykxmz-setup-1.7.1.exe</span><span class="sz">14.4 MB</span></div><div class="dlrow"><span class="nm">ykxmz-watch-1.7.1.apk</span><span class="sz">197 KB</span></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="foot">
|
|
||||||
<b>隐私说明</b> —— 数据来自 /v1/health,只显示配对码和流量数字,
|
|
||||||
看不到任何传输内容。中转只转发字节,不解析、不存储会话内容。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="toast"></div>
|
|
||||||
<script>
|
|
||||||
const toast = (msg, kind) => {
|
|
||||||
const el = document.getElementById('toast');
|
|
||||||
el.textContent = msg;
|
|
||||||
el.className = kind ? ('show ' + kind) : 'show';
|
|
||||||
clearTimeout(el._t);
|
|
||||||
el._t = setTimeout(() => { el.className = ''; }, 2800);
|
|
||||||
};
|
|
||||||
|
|
||||||
const post = async (path, payload) => {
|
|
||||||
const r = await fetch(path, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify(payload || {})
|
|
||||||
});
|
|
||||||
return r.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 单台删除 ----------------
|
|
||||||
document.querySelectorAll('button.danger[data-device]').forEach(b => {
|
|
||||||
b.onclick = async () => {
|
|
||||||
const name = b.dataset.name, code = b.dataset.code;
|
|
||||||
if (!confirm('删除设备「' + name + '」(配对码 ' + code + ')?\n\n'
|
|
||||||
+ '这会:\n 1. 断开它现在的手表连接\n'
|
|
||||||
+ ' 2. 从服务器删掉这台设备\n'
|
|
||||||
+ ' 3. 清掉它的配对码绑定')) return;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete', {deviceId: b.dataset.device});
|
|
||||||
if (j.ok) {
|
|
||||||
toast('已删除「' + (j.name || name) + '」,清掉配对码 '
|
|
||||||
+ (j.codesCleared || []).length + ' 条', 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} else {
|
|
||||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------- 批量选择 ----------------
|
|
||||||
const picks = () => Array.from(document.querySelectorAll('input.pick'));
|
|
||||||
const picked = () => picks().filter(c => c.checked).map(c => c.value);
|
|
||||||
|
|
||||||
function refreshPickState() {
|
|
||||||
const chosen = picked().length;
|
|
||||||
const btn = document.getElementById('delPicked');
|
|
||||||
if (btn) {
|
|
||||||
btn.disabled = chosen === 0;
|
|
||||||
btn.textContent = chosen ? ('删除选中(' + chosen + ')') : '删除选中';
|
|
||||||
}
|
|
||||||
picks().forEach(c => {
|
|
||||||
const tr = c.closest('tr');
|
|
||||||
if (tr) tr.classList.toggle('picked', c.checked);
|
|
||||||
});
|
|
||||||
const all = document.getElementById('pickAll');
|
|
||||||
if (all) {
|
|
||||||
const total = picks().length;
|
|
||||||
all.checked = total > 0 && chosen === total;
|
|
||||||
all.indeterminate = chosen > 0 && chosen < total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
picks().forEach(c => c.addEventListener('change', refreshPickState));
|
|
||||||
const pickAll = document.getElementById('pickAll');
|
|
||||||
if (pickAll) pickAll.addEventListener('change', () => {
|
|
||||||
picks().forEach(c => { c.checked = pickAll.checked; });
|
|
||||||
refreshPickState();
|
|
||||||
});
|
|
||||||
refreshPickState();
|
|
||||||
|
|
||||||
// ---------------- 批量删除 ----------------
|
|
||||||
const delPicked = document.getElementById('delPicked');
|
|
||||||
if (delPicked) delPicked.onclick = async () => {
|
|
||||||
const ids = picked();
|
|
||||||
if (!ids.length) return;
|
|
||||||
if (!confirm('删除选中的 ' + ids.length + ' 台设备?\n\n'
|
|
||||||
+ '会断开它们现在的手表连接,并清掉它们占的配对码。')) return;
|
|
||||||
delPicked.disabled = true;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete_many', {deviceIds: ids});
|
|
||||||
if (j.ok) {
|
|
||||||
toast('已删除 ' + j.deleted + ' 台设备'
|
|
||||||
+ (j.failed ? (',' + j.failed + ' 台失败') : ''), 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} else {
|
|
||||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
|
||||||
delPicked.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
delPicked.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 删除全部 ----------------
|
|
||||||
const delAll = document.getElementById('delAll');
|
|
||||||
if (delAll) delAll.onclick = async () => {
|
|
||||||
if (!confirm('删除全部设备(连同所有配对码)?\n\n'
|
|
||||||
+ '清场用,删完所有电脑都要重新设码。')) return;
|
|
||||||
try {
|
|
||||||
const j = await post('/v1/admin/delete_all');
|
|
||||||
toast('已删除 ' + (j.deleted || 0) + ' 台设备', 'ok');
|
|
||||||
setTimeout(() => location.reload(), 700);
|
|
||||||
} catch (e) {
|
|
||||||
toast('请求出错:' + e, 'err');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 复制公网地址 ----------------
|
|
||||||
const copyUrl = document.getElementById('copyUrl');
|
|
||||||
if (copyUrl) copyUrl.onclick = async () => {
|
|
||||||
const text = document.getElementById('urlBox').textContent.trim();
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
toast('地址已复制', 'ok');
|
|
||||||
} catch (e) {
|
|
||||||
// 非安全上下文(http 且不是 localhost)没有 clipboard API,
|
|
||||||
// 退回到老办法:选中 + execCommand
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(document.getElementById('urlBox'));
|
|
||||||
const sel = window.getSelection();
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
try { document.execCommand('copy'); toast('地址已复制', 'ok'); }
|
|
||||||
catch (e2) { toast('复制失败,请手动选中', 'err'); }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 网络体检 ----------------
|
|
||||||
const netBtn = document.getElementById('netBtn');
|
|
||||||
if (netBtn) netBtn.onclick = async () => {
|
|
||||||
const body = document.getElementById('netBody');
|
|
||||||
body.textContent = '正在测…';
|
|
||||||
try {
|
|
||||||
const r = await fetch('/v1/admin/netcheck');
|
|
||||||
const j = await r.json();
|
|
||||||
if (!j.ok) {
|
|
||||||
body.innerHTML = '<span class="bad">读不到:'
|
|
||||||
+ (j.error || '未知') + '</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rows = [];
|
|
||||||
if (j.tunnelMetrics) {
|
|
||||||
const conns = j.tunnelConnections || 0;
|
|
||||||
const cls = conns >= 2 ? 'good' : 'bad';
|
|
||||||
rows.push('隧道连接数:<b class="' + cls + '">' + conns + '</b>'
|
|
||||||
+ (conns === 0 ? '(断了,看门狗会自动重启)' : ''));
|
|
||||||
rows.push('隧道累计请求:<b>' + (j.tunnelRequests || 0) + '</b>');
|
|
||||||
} else {
|
|
||||||
rows.push('<span class="bad">读不到隧道指标'
|
|
||||||
+ (j.tunnelError ? '(' + j.tunnelError + ')' : '') + '</span>');
|
|
||||||
}
|
|
||||||
rows.push('登记设备:<b>' + j.devices + '</b>(在线 <b>' + j.online
|
|
||||||
+ '</b>)');
|
|
||||||
rows.push('手表会话:<b>' + j.sessions + '</b>');
|
|
||||||
rows.push('中转内存:<b>' + Math.round(j.memoryKB / 1024) + ' MB</b>');
|
|
||||||
rows.push('已运行:<b>' + Math.round(j.uptimeSeconds / 60) + ' 分钟</b>');
|
|
||||||
if (j.publicUrl) {
|
|
||||||
rows.push('公网地址:<b>' + j.publicUrl + '</b>');
|
|
||||||
}
|
|
||||||
body.innerHTML = rows.map(t => '<div>' + t + '</div>').join('');
|
|
||||||
// 顺便测浏览器到中转的往返
|
|
||||||
const t0 = performance.now();
|
|
||||||
try {
|
|
||||||
await fetch('/v1/health?t=' + Date.now(), {cache: 'no-store'});
|
|
||||||
const ms = Math.round(performance.now() - t0);
|
|
||||||
body.innerHTML += '<div>浏览器到中转往返:<b class="'
|
|
||||||
+ (ms < 300 ? 'good' : 'bad') + '">' + ms + ' ms</b>'
|
|
||||||
+ (ms < 300 ? '(很顺)' : (ms < 1000 ? '(一般)' : '(有点慢)'))
|
|
||||||
+ '</div>';
|
|
||||||
} catch (e) {
|
|
||||||
body.innerHTML += '<div class="bad">浏览器到中转:失败</div>';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
body.textContent = '请求出错:' + e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------- 自动刷新 ----------------
|
|
||||||
const autoRef = document.getElementById('autoRef');
|
|
||||||
let autoTimer = null;
|
|
||||||
function setupAuto() {
|
|
||||||
clearInterval(autoTimer);
|
|
||||||
if (autoRef && autoRef.checked) {
|
|
||||||
autoTimer = setInterval(() => {
|
|
||||||
// 有人正在勾选/删除时别刷新,免得打断操作
|
|
||||||
if (picked().length === 0 && !document.hidden) location.reload();
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (autoRef) {
|
|
||||||
autoRef.addEventListener('change', () => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('lr_autoref', autoRef.checked ? '1' : '0');
|
|
||||||
} catch (e) {}
|
|
||||||
setupAuto();
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (localStorage.getItem('lr_autoref') === '0') autoRef.checked = false;
|
|
||||||
} catch (e) {}
|
|
||||||
setupAuto();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body></html>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''分析 2 小时长测数据(列格式:时间戳 \t 状态码 \t 毫秒)。
|
|
||||||
|
|
||||||
上一版解析写错了 —— 它把任何 3 位数字都当状态码,
|
|
||||||
于是毫秒(921、968…)也被算成状态码,可用率算出来只有 84.8%,
|
|
||||||
是**假数字**。这里改成按列位置取。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from collections import Counter
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(sys.argv[1] if len(sys.argv) > 1 else "/root/long2h_health.tsv")
|
|
||||||
if not path.is_file():
|
|
||||||
print(" 文件不存在: %s" % path)
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
bad = 0
|
|
||||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
parts = line.split("\t")
|
|
||||||
if len(parts) < 3:
|
|
||||||
bad += 1
|
|
||||||
continue
|
|
||||||
stamp, code, ms = parts[0], parts[1].strip(), parts[2].strip()
|
|
||||||
try:
|
|
||||||
rows.append((stamp, code, float(ms)))
|
|
||||||
except ValueError:
|
|
||||||
bad += 1
|
|
||||||
|
|
||||||
print(" 样本 %d 条(解析不了 %d 条)" % (len(rows), bad))
|
|
||||||
if not rows:
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
print(" 时间范围: %s -> %s" % (rows[0][0], rows[-1][0]))
|
|
||||||
|
|
||||||
codes = Counter(c for _s, c, _m in rows)
|
|
||||||
total = len(rows)
|
|
||||||
ok = codes.get("200", 0)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" ── HTTP 状态分布 ──")
|
|
||||||
for c, n in codes.most_common():
|
|
||||||
print(" %-6s %4d 次 %5.1f%%" % (c, n, n * 100.0 / total))
|
|
||||||
print(" " + "-" * 30)
|
|
||||||
print(" 可用率: %d/%d = %.2f%%" % (ok, total, ok * 100.0 / total))
|
|
||||||
|
|
||||||
# 只看成功的耗时(失败的那些耗时没有意义)
|
|
||||||
good = sorted(m for _s, c, m in rows if c == "200")
|
|
||||||
if good:
|
|
||||||
def p(q):
|
|
||||||
return good[min(int(len(good) * q), len(good) - 1)]
|
|
||||||
print()
|
|
||||||
print(" ── 成功请求耗时(毫秒)──")
|
|
||||||
print(" 最小 %.0f 中位 %.0f P90 %.0f P99 %.0f 最大 %.0f"
|
|
||||||
% (good[0], good[len(good) // 2], p(0.9), p(0.99), good[-1]))
|
|
||||||
|
|
||||||
# 如果还有第 4 列及以后(连接数之类),也统计一下
|
|
||||||
extra_cols = set()
|
|
||||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
||||||
p = line.strip().split("\t")
|
|
||||||
if len(p) > 3:
|
|
||||||
extra_cols.add(len(p))
|
|
||||||
if extra_cols:
|
|
||||||
print()
|
|
||||||
print(" (文件里还有更多列,最多 %d 列)" % max(extra_cols))
|
|
||||||
|
|
||||||
fails = [(s, c, m) for s, c, m in rows if c != "200"]
|
|
||||||
print()
|
|
||||||
if fails:
|
|
||||||
print(" ── 失败样本(共 %d 条)──" % len(fails))
|
|
||||||
for s, c, m in fails[:15]:
|
|
||||||
print(" %s HTTP %-5s %.0fms" % (s, c, m))
|
|
||||||
if len(fails) > 15:
|
|
||||||
print(" … 还有 %d 条" % (len(fails) - 15))
|
|
||||||
# 失败时间分布(按 10 分钟分桶)
|
|
||||||
print()
|
|
||||||
print(" ── 失败时刻分布(按小时)──")
|
|
||||||
hours = Counter(s.split(" ")[1].split(":")[0] if " " in s else s[:2]
|
|
||||||
for s, _c, _m in fails)
|
|
||||||
for h in sorted(hours):
|
|
||||||
print(" %s 点 %d 次" % (h, hours[h]))
|
|
||||||
else:
|
|
||||||
print(" ✓ 全程没有非 200 —— 2 小时 100% 可用")
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 分析 relay-stability 的 2 小时健康长测数据(只读)
|
|
||||||
set -u
|
|
||||||
|
|
||||||
F=/root/long2h_health.tsv
|
|
||||||
[ -f "$F" ] || { echo " 数据文件不存在: $F"; exit 0; }
|
|
||||||
|
|
||||||
echo " 文件: $F 行数: $(wc -l < "$F")"
|
|
||||||
echo " 前 3 行(看格式):"
|
|
||||||
head -3 "$F" | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
|
|
||||||
python3 - "$F" <<'PYEOF'
|
|
||||||
import sys
|
|
||||||
from collections import Counter
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(sys.argv[1])
|
|
||||||
lines = [l.strip() for l in path.read_text(encoding="utf-8",
|
|
||||||
errors="replace").splitlines()
|
|
||||||
if l.strip()]
|
|
||||||
if not lines:
|
|
||||||
print(" 没有数据")
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
# 猜列:先看表头,没有表头就按 tab 切
|
|
||||||
head = lines[0].lower()
|
|
||||||
has_header = any(k in head for k in ("time", "code", "ms", "http"))
|
|
||||||
rows = lines[1:] if has_header else lines
|
|
||||||
if has_header:
|
|
||||||
print(" 表头: %s" % lines[0])
|
|
||||||
print(" 样本数: %d" % len(rows))
|
|
||||||
|
|
||||||
codes = Counter()
|
|
||||||
times = []
|
|
||||||
conns = []
|
|
||||||
fails = []
|
|
||||||
for i, line in enumerate(rows, 1):
|
|
||||||
parts = line.split("\t")
|
|
||||||
code = None
|
|
||||||
ms = None
|
|
||||||
for p in parts:
|
|
||||||
p = p.strip()
|
|
||||||
if p.isdigit() and len(p) == 3:
|
|
||||||
code = p
|
|
||||||
if p.replace(".", "").isdigit() and "." in p:
|
|
||||||
try:
|
|
||||||
v = float(p)
|
|
||||||
if ms is None and v < 300000:
|
|
||||||
ms = v
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if code:
|
|
||||||
codes[code] += 1
|
|
||||||
if code != "200":
|
|
||||||
fails.append((i, line[:100]))
|
|
||||||
if ms:
|
|
||||||
times.append(ms)
|
|
||||||
# 连接数:找形如 quic=N h2=M 或者单独的数字
|
|
||||||
for p in parts:
|
|
||||||
if "quic=" in p:
|
|
||||||
conns.append(p.strip())
|
|
||||||
|
|
||||||
total = sum(codes.values())
|
|
||||||
ok = codes.get("200", 0)
|
|
||||||
print()
|
|
||||||
print(" ── HTTP 结果分布 ──")
|
|
||||||
for c, n in codes.most_common():
|
|
||||||
pct = n * 100.0 / total if total else 0
|
|
||||||
print(" %-6s %4d 次 %.1f%%" % (c, n, pct))
|
|
||||||
print(" --------")
|
|
||||||
print(" 可用率: %d/%d = %.1f%%" % (ok, total, ok * 100.0 / total if total else 0))
|
|
||||||
|
|
||||||
if times:
|
|
||||||
s = sorted(times)
|
|
||||||
def pct(q):
|
|
||||||
return s[min(int(len(s) * q), len(s) - 1)]
|
|
||||||
print()
|
|
||||||
print(" ── 耗时分布(毫秒)──")
|
|
||||||
print(" 最小 %.0f 中位 %.0f P90 %.0f P99 %.0f 最大 %.0f"
|
|
||||||
% (s[0], s[len(s)//2], pct(0.9), pct(0.99), s[-1]))
|
|
||||||
|
|
||||||
if conns:
|
|
||||||
c = Counter(conns)
|
|
||||||
print()
|
|
||||||
print(" ── 连接数分布(前 8)──")
|
|
||||||
for k, n in c.most_common(8):
|
|
||||||
print(" %-28s %d 次" % (k, n))
|
|
||||||
|
|
||||||
if fails:
|
|
||||||
print()
|
|
||||||
print(" ── 失败样本(最多 12 条)──")
|
|
||||||
for i, line in fails[:12]:
|
|
||||||
print(" 第 %d 行: %s" % (i, line))
|
|
||||||
else:
|
|
||||||
print()
|
|
||||||
print(" ✓ 全程没有非 200")
|
|
||||||
PYEOF
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''分析连接数时间线 long2h_conns.tsv,找塌陷的规律。
|
|
||||||
|
|
||||||
目的:回答两个问题
|
|
||||||
1. QUIC 塌陷是不是周期性的?周期多长?
|
|
||||||
2. 塌陷期间 HTTP/2 那半边是不是一直活着?(这是双协议兜底的价值)
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
p = Path("/root/long2h_conns.tsv")
|
|
||||||
if not p.is_file():
|
|
||||||
print(" 没有 %s" % p)
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
raw = p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
||||||
print(" 共 %d 行,前 3 行:" % len(raw))
|
|
||||||
for line in raw[:3]:
|
|
||||||
print(" %r" % line[:120])
|
|
||||||
print()
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
for line in raw:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
parts = line.replace(",", "\t").split("\t")
|
|
||||||
stamp = ""
|
|
||||||
nums = []
|
|
||||||
for x in parts:
|
|
||||||
x = x.strip()
|
|
||||||
if not x:
|
|
||||||
continue
|
|
||||||
if ":" in x and ("-" in x or len(x) <= 8) and not x.replace(":", "").isdigit():
|
|
||||||
stamp = x
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
nums.append(int(x))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if nums:
|
|
||||||
rows.append((stamp, nums))
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print(" 解析不出数字")
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
# 假设列是 [quic, h2] 或 [总连接, quic, h2] —— 按每行长度自适应
|
|
||||||
width = max(len(n) for _s, n in rows)
|
|
||||||
print(" 每行 %d 个数字(列)" % width)
|
|
||||||
print(" 前 5 行解析结果:")
|
|
||||||
for s, n in rows[:5]:
|
|
||||||
print(" %-12s %s" % (s, n))
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 尝试识别:如果一行有 2 列,当作 [quic, h2];3 列当作 [total, quic, h2]
|
|
||||||
lower = [n for _s, n in rows]
|
|
||||||
if width == 2:
|
|
||||||
quic = [n[0] for n in lower]
|
|
||||||
h2 = [n[1] for n in lower]
|
|
||||||
elif width >= 3:
|
|
||||||
quic = [n[1] for n in lower]
|
|
||||||
h2 = [n[2] for n in lower]
|
|
||||||
else:
|
|
||||||
print(" 列数不支持")
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
total = len(quic)
|
|
||||||
print(" ── 连接数统计(%d 个采样点)──" % total)
|
|
||||||
print(" QUIC: 最低 %d 最高 %d 平均 %.1f"
|
|
||||||
% (min(quic), max(quic), sum(quic) / total))
|
|
||||||
print(" HTTP/2: 最低 %d 最高 %d 平均 %.1f"
|
|
||||||
% (min(h2), max(h2), sum(h2) / total))
|
|
||||||
|
|
||||||
# 塌陷 = QUIC <= 2
|
|
||||||
collapse_idx = [i for i, q in enumerate(quic) if q <= 2]
|
|
||||||
print()
|
|
||||||
print(" ── QUIC 塌陷(<=2 条)──")
|
|
||||||
print(" 出现 %d 次,占比 %.1f%%" % (len(collapse_idx), len(collapse_idx) * 100.0 / total))
|
|
||||||
|
|
||||||
# 塌陷时 h2 还在吗
|
|
||||||
if collapse_idx:
|
|
||||||
h2_alive = sum(1 for i in collapse_idx if h2[i] >= 1)
|
|
||||||
h2_zero = sum(1 for i in collapse_idx if h2[i] == 0)
|
|
||||||
print(" 塌陷时 HTTP/2 >=1 条: %d 次(%.0f%%)"
|
|
||||||
% (h2_alive, h2_alive * 100.0 / len(collapse_idx)))
|
|
||||||
print(" 塌陷时 HTTP/2 也归零: %d 次" % h2_zero)
|
|
||||||
print(" -> 这就是双协议兜底的价值:%s"
|
|
||||||
% ("QUIC 塌了但 HTTP/2 还在扛" if h2_zero < h2_alive
|
|
||||||
else "两个一起塌,兜底没起作用"))
|
|
||||||
|
|
||||||
# 合并连续的塌陷区间,看周期
|
|
||||||
spans = []
|
|
||||||
start = None
|
|
||||||
for i, q in enumerate(quic):
|
|
||||||
if q <= 2 and start is None:
|
|
||||||
start = i
|
|
||||||
elif q > 2 and start is not None:
|
|
||||||
spans.append((start, i - 1))
|
|
||||||
start = None
|
|
||||||
if start is not None:
|
|
||||||
spans.append((start, total - 1))
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" ── 塌陷区间(连续采样点合并)──")
|
|
||||||
print(" 共 %d 段" % len(spans))
|
|
||||||
for a, b in spans[:15]:
|
|
||||||
sa = rows[a][0] if rows[a][0] else "第%d点" % a
|
|
||||||
sb = rows[b][0] if rows[b][0] else "第%d点" % b
|
|
||||||
print(" %-10s -> %-10s 持续 %d 个采样点 (quic 最低 %d)"
|
|
||||||
% (sa, sb, b - a + 1, min(quic[a:b + 1])))
|
|
||||||
|
|
||||||
# 塌陷间隔(周期)
|
|
||||||
if len(spans) >= 2:
|
|
||||||
gaps = [spans[i + 1][0] - spans[i][0] for i in range(len(spans) - 1)]
|
|
||||||
print()
|
|
||||||
print(" ── 两次塌陷之间隔多少个采样点 ──")
|
|
||||||
print(" %s" % gaps[:15])
|
|
||||||
print(" 平均 %.1f 个采样点" % (sum(gaps) / len(gaps)))
|
|
||||||
|
|
||||||
# 连接数分布
|
|
||||||
from collections import Counter
|
|
||||||
print()
|
|
||||||
print(" ── QUIC 连接数分布 ──")
|
|
||||||
for k in sorted(set(quic)):
|
|
||||||
c = quic.count(k)
|
|
||||||
print(" %2d 条: %4d 次 %5.1f%%" % (k, c, c * 100.0 / total))
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 采用最优配置 quic + 10,并清理 MSS 规则。
|
|
||||||
#
|
|
||||||
# v4 实测(MTU 1400,各 60 次):
|
|
||||||
# quic+6 建齐 10s 0 失败 100% 中位 1399 P90 2611 P99 13705
|
|
||||||
# quic+8 建齐 16s 1 失败 100% 中位 1280 P90 2279 P99 3856
|
|
||||||
# quic+10 建齐 13s 0 失败 100% 中位 1256 P90 2068 P99 2991 <- 最好
|
|
||||||
#
|
|
||||||
# 注意:v4 脚本自己的排序逻辑有多行输出没处理干净,误选了 quic+8,
|
|
||||||
# 这里按数据手工改成 quic+10。
|
|
||||||
#
|
|
||||||
# MSS 说明:接口 MTU 已经是 1400,内核自己算出来的 MSS 就是 1360;
|
|
||||||
# iptables 的 --set-mss 只能把 MSS 往小改,所以那条 1400 的规则
|
|
||||||
# 现在实际是空转。统一改成 1360,避免以后 MTU 变回去时留下不一致。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
|
||||||
UA='LanRemote/1.8.2'
|
|
||||||
|
|
||||||
echo "════════ 1. 采用 quic + 10 ════════"
|
|
||||||
sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC"
|
|
||||||
sed -i "s/--ha-connections [0-9]*/--ha-connections 10/" "$SVC"
|
|
||||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+|--edge-ip-version [0-9]+|--retries [0-9]+' "$SVC" | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 清理 MSS 规则(只留一条 1360)════════"
|
|
||||||
iptables -t mangle -S OUTPUT 2>/dev/null | grep TCPMSS | sed 's/^/ 清理前: /'
|
|
||||||
while iptables -t mangle -D OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1400 2>/dev/null; do :; done
|
|
||||||
while iptables -t mangle -D OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 2>/dev/null; do :; done
|
|
||||||
iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360
|
|
||||||
iptables -t mangle -S OUTPUT 2>/dev/null | grep TCPMSS | sed 's/^/ 清理后: /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. MTU 和持久化 ════════"
|
|
||||||
echo " wlan0 MTU 现在: $(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
grep -c 'mtu 1400' /etc/systemd/system/lanremote-nettune.service | sed 's/^/ net-tune 里的 MTU 设置行数: /'
|
|
||||||
echo " net-tune 服务: $(systemctl is-enabled lanremote-nettune) / $(systemctl is-active lanremote-nettune)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 重启隧道并等连接建齐 ════════"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
for i in $(seq 1 40); do
|
|
||||||
sleep 4
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
[ "${n:-0}" -ge 10 ] && break
|
|
||||||
done
|
|
||||||
echo " 建齐 $n/10 条(等了约 $((i*4)) 秒)"
|
|
||||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep 'server_locations.*} 1' | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 等公网可用 ════════"
|
|
||||||
for i in $(seq 1 20); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health)
|
|
||||||
printf " 第 %d 次: %s\n" "$i" "$c"
|
|
||||||
[ "$c" = "200" ] && break
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 快速验收(30 次)════════"
|
|
||||||
ok=0; times=()
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health)
|
|
||||||
c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
[ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); }
|
|
||||||
done
|
|
||||||
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
|
|
||||||
echo " 可用率 $ok/30 = $((ok*100/30))% $stats ms"
|
|
||||||
echo
|
|
||||||
echo " 最终: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ') MTU=$(cat /sys/class/net/wlan0/mtu)"
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 1) 把官网上的「管理入口」链接删掉(管理不对外开放)
|
|
||||||
# 2) 应用优选 IP:把 Cloudflare 隧道域名指到实测最快的边缘 IP
|
|
||||||
set -u
|
|
||||||
|
|
||||||
RELAY_DIR=/root/lanremote-relay
|
|
||||||
CONFIG=/etc/cloudflared/config.yml
|
|
||||||
|
|
||||||
echo "===== 1. 备份原配置 ====="
|
|
||||||
cp -f /etc/hosts /root/hosts.bak.$(date +%s) 2>/dev/null || true
|
|
||||||
cp -f "$CONFIG" /root/config.yml.bak 2>/dev/null || true
|
|
||||||
echo " 已备份 /etc/hosts 和 config.yml"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 2. 应用优选 IP(写 /etc/hosts 覆盖 GeoDNS)====="
|
|
||||||
# 从探测结果里取最快的两个
|
|
||||||
BEST=$(head -2 /root/cf_best.txt 2>/dev/null | awk '{print $1}')
|
|
||||||
echo " 最快的 IP: $BEST"
|
|
||||||
|
|
||||||
# 清掉之前加过的
|
|
||||||
sed -i '/argotunnel.com/d' /etc/hosts
|
|
||||||
|
|
||||||
FIRST=$(echo "$BEST" | head -1)
|
|
||||||
SECOND=$(echo "$BEST" | tail -1)
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "# ykxmz:优选 Cloudflare 隧道边缘(覆盖 GeoDNS,实测延迟更低)"
|
|
||||||
echo "$FIRST region1.v2.argotunnel.com"
|
|
||||||
echo "$SECOND region2.v2.argotunnel.com"
|
|
||||||
echo "$FIRST region1.v2.argotunnel.com"
|
|
||||||
echo "$SECOND region2.v2.argotunnel.com"
|
|
||||||
} >> /etc/hosts
|
|
||||||
|
|
||||||
echo " /etc/hosts 现在有:"
|
|
||||||
grep argotunnel /etc/hosts | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 3. 确认解析结果变了 ====="
|
|
||||||
getent hosts region1.v2.argotunnel.com | head -3 | sed 's/^/ region1 -> /'
|
|
||||||
getent hosts region2.v2.argotunnel.com | head -3 | sed 's/^/ region2 -> /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 4. 重启隧道 ====="
|
|
||||||
systemctl restart lanremote-tunnel
|
|
||||||
sleep 35
|
|
||||||
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
echo " 落点:"
|
|
||||||
grep -oE 'location=[a-z0-9]+' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -3 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 5. 公网延迟对比(20 次)====="
|
|
||||||
ok=0; total=0
|
|
||||||
for i in $(seq 1 20); do
|
|
||||||
t=$(curl -s -o /dev/null -w '%{time_total}' --max-time 15 \
|
|
||||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
|
||||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
|
||||||
if [ "$c" = "200" ]; then
|
|
||||||
ok=$((ok+1))
|
|
||||||
printf " %2d: %s 秒\n" "$i" "$t"
|
|
||||||
else
|
|
||||||
printf " %2d: 失败\n" "$i"
|
|
||||||
fi
|
|
||||||
total=$((total+1))
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/$total"
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''给 /rescue/ 加上管理鉴权。
|
|
||||||
|
|
||||||
== 问题 ==
|
|
||||||
|
|
||||||
我把 /rescue/ 路由插在 do_GET 的第 1531 行,而管理页的登录校验
|
|
||||||
(self._admin_gate())在第 1723 行 —— **路由在鉴权之前**。
|
|
||||||
|
|
||||||
也就是说:任何人只要知道
|
|
||||||
https://relay.xxmxxs.top/rescue/20261008-173000.txt
|
|
||||||
就能直接下载诊断报告。那里面有什么:
|
|
||||||
|
|
||||||
* 内网 IP(192.168.2.x)、主机名
|
|
||||||
* 进程列表、内存占用
|
|
||||||
* 中转和隧道的日志尾部
|
|
||||||
* MTU / 路由 / iptables 规则
|
|
||||||
|
|
||||||
这些是**运维信息**,不该公开。对比一下:/download/ 是给用户的
|
|
||||||
安装包,公开是对的;/rescue/ 是给自己看的,必须登录。
|
|
||||||
|
|
||||||
(同样的问题 /shot/ 也有 —— 那个是屏幕共享截图,本来就该拦。
|
|
||||||
这次先修 /rescue/,顺手看一眼 /shot/。)
|
|
||||||
|
|
||||||
== 改法 ==
|
|
||||||
|
|
||||||
在 /rescue/ 分支开头加一句 self._admin_gate(),
|
|
||||||
和 /v1/admin/* 那些接口一样。do_GET 和 do_HEAD 都要加。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
RELAY = ROOT / "relay" / "relay.py"
|
|
||||||
|
|
||||||
# do_GET 里的(有 body 的那份)
|
|
||||||
GET_OLD = ''' if path.startswith("/rescue/"):
|
|
||||||
# 抢救报告下载。**单独一条路由,不发缓存头** ——
|
|
||||||
# 内容会变,而且不该让 Cloudflare 边缘留着。
|
|
||||||
# (和 /download/ 分成两套是有意的:那个是给用户的安装包,
|
|
||||||
# 文件名带版本号、内容永不改变,所以敢发 604800 长缓存。)
|
|
||||||
wanted = urllib.parse.unquote(path[len("/rescue/"):])'''
|
|
||||||
|
|
||||||
GET_NEW = ''' if path.startswith("/rescue/"):
|
|
||||||
# 抢救报告下载。**单独一条路由,不发缓存头** ——
|
|
||||||
# 内容会变,而且不该让 Cloudflare 边缘留着。
|
|
||||||
# (和 /download/ 分成两套是有意的:那个是给用户的安装包,
|
|
||||||
# 文件名带版本号、内容永不改变,所以敢发 604800 长缓存。)
|
|
||||||
#
|
|
||||||
# ⚠ 必须登录才能下。报告里有内网 IP、进程表、日志尾部、
|
|
||||||
# MTU/路由/iptables —— 是运维信息,不是给所有人看的。
|
|
||||||
# 第一版把这条路由插在 _admin_gate() **之前**,
|
|
||||||
# 等于匿名可下,实测踩到。
|
|
||||||
if not self._admin_gate():
|
|
||||||
return
|
|
||||||
wanted = urllib.parse.unquote(path[len("/rescue/"):])'''
|
|
||||||
|
|
||||||
# do_HEAD 里的(只有 headers)
|
|
||||||
HEAD_OLD = ''' if path.startswith("/rescue/"):
|
|
||||||
# 抢救报告。HEAD 只回长度和类型,**不写 body** ——
|
|
||||||
# 这条路由同时存在于 do_GET(真正下载)和 do_HEAD(探测)。
|
|
||||||
wanted = urllib.parse.unquote(path[len("/rescue/"):])'''
|
|
||||||
|
|
||||||
HEAD_NEW = ''' if path.startswith("/rescue/"):
|
|
||||||
# 抢救报告。HEAD 只回长度和类型,**不写 body** ——
|
|
||||||
# 这条路由同时存在于 do_GET(真正下载)和 do_HEAD(探测)。
|
|
||||||
# 同样要登录(见 do_GET 那段注释)。
|
|
||||||
if not self._admin_gate():
|
|
||||||
return no()
|
|
||||||
wanted = urllib.parse.unquote(path[len("/rescue/"):])'''
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
raw = RELAY.read_bytes()
|
|
||||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
|
|
||||||
changed = []
|
|
||||||
for old, new, what in ((GET_OLD, GET_NEW, "do_GET"),
|
|
||||||
(HEAD_OLD, HEAD_NEW, "do_HEAD")):
|
|
||||||
if new.split("\n")[0] in text and "_admin_gate" in text.split(old)[0][-600:]:
|
|
||||||
changed.append(("·", what, "好像已经加过了"))
|
|
||||||
continue
|
|
||||||
if old not in text:
|
|
||||||
changed.append(("✗", what, "找不到锚点"))
|
|
||||||
continue
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
changed.append(("✓", what, "已加鉴权"))
|
|
||||||
|
|
||||||
print(" ── 结果 ──")
|
|
||||||
for m, w, why in changed:
|
|
||||||
print(" %s %-10s %s" % (m, w, why))
|
|
||||||
|
|
||||||
if any(m == "✗" for m, _w, _y in changed):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
RELAY.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8"))
|
|
||||||
print(" 已写回 relay.py")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
-158
@@ -1,158 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 中转全面基线测量。
|
|
||||||
#
|
|
||||||
# 目的:把"优化到什么程度"变成可量化的数字,而不是感觉。
|
|
||||||
# 五类指标,每类都记录成功率和耗时分布:
|
|
||||||
# A. 公网 HTTP 可用率(100 次 /v1/health)
|
|
||||||
# B. 长轮询(10 次真实 poll,每次挂 60 秒)
|
|
||||||
# C. 三个发布产物的下载成功率和吞吐
|
|
||||||
# D. 隧道连接数稳定性 + 重连次数
|
|
||||||
# E. 静态资源(官网 / 截图 / favicon)
|
|
||||||
#
|
|
||||||
# 用法: bash bench.sh [标签]
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=${BASE:-https://relay.xxmxxs.top}
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
LABEL=${1:-baseline}
|
|
||||||
OUT="/root/bench_${LABEL}.txt"
|
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════" | tee "$OUT"
|
|
||||||
echo " 中转基准测量 · $LABEL · $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "$OUT"
|
|
||||||
echo "════════════════════════════════════════════════════════" | tee -a "$OUT"
|
|
||||||
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "── 隧道配置 ──" | tee -a "$OUT"
|
|
||||||
CAP=$(systemctl show lanremote-tunnel -p ExecStart --value 2>/dev/null | tr ';' '\n' | grep -oE 'argv\[\]=[^ ]*|--[a-z-]+|/[a-z/]*cloudflared' | tr '\n' ' ')
|
|
||||||
grep -oE '\-\-protocol [a-z0-9]+|\-\-ha-connections [0-9]+|\-\-edge-ip-version [0-9]+' /etc/systemd/system/lanremote-tunnel.service 2>/dev/null | tr '\n' ' ' | sed 's/^/ 参数: /' | tee -a "$OUT"
|
|
||||||
echo " 长轮询: $(tail -40 /root/lanremote-relay/relay.log 2>/dev/null | grep -oE '长轮询 [0-9]+ 秒' | tail -1)" | tee -a "$OUT"
|
|
||||||
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')" | tee -a "$OUT"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- A
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "── A. 公网 HTTP 可用率(100 次,间隔 0.4 秒)──" | tee -a "$OUT"
|
|
||||||
okA=0; failA=0; timesA=()
|
|
||||||
for i in $(seq 1 100); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
if [ "$code" = "200" ]; then okA=$((okA+1)); timesA+=("$ms"); else failA=$((failA+1)); fi
|
|
||||||
sleep 0.4
|
|
||||||
done
|
|
||||||
pctA=$((okA * 100 / 100))
|
|
||||||
echo " 可用率: $okA/100 = ${pctA}% 失败 $failA 次" | tee -a "$OUT"
|
|
||||||
if [ "${#timesA[@]}" -gt 0 ]; then
|
|
||||||
printf '%s\n' "${timesA[@]}" | sort -n | awk '{
|
|
||||||
a[NR]=$1
|
|
||||||
} END {
|
|
||||||
printf " 耗时: 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms\n",
|
|
||||||
a[1], a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]
|
|
||||||
}' | tee -a "$OUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- E(快的先做)
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "── E. 静态资源(各 10 次)──" | tee -a "$OUT"
|
|
||||||
for path in "/" "/shot/pc-main.png" "/favicon.ico"; do
|
|
||||||
ok=0; times=()
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" "$BASE$path")
|
|
||||||
c=$(echo "$out" | awk '{print $1}')
|
|
||||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
[ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); }
|
|
||||||
sleep 0.3
|
|
||||||
done
|
|
||||||
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {printf "中位 %d 最大 %d ms", a[int(NR/2)+1], a[NR]}')
|
|
||||||
printf " %-22s %2d/10 %s\n" "$path" "$ok" "$stats" | tee -a "$OUT"
|
|
||||||
done
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- C
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "── C. 三个成品下载(完整下载,量吞吐)──" | tee -a "$OUT"
|
|
||||||
for f in ykxmz-setup-1.8.1.exe ykxmz-server-1.8.1.zip ykxmz-watch-1.8.1.apk; do
|
|
||||||
rm -f /tmp/dl.bin
|
|
||||||
out=$(curl -s -o /tmp/dl.bin -w '%{http_code} %{size_download} %{time_total}' \
|
|
||||||
--max-time 180 -H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
size=$(echo "$out" | awk '{print $2}')
|
|
||||||
secs=$(echo "$out" | awk '{print $3}')
|
|
||||||
kbps=$(echo "$secs $size" | awk '{if ($1 > 0) printf "%.0f", $2/$1/1024; else print 0}')
|
|
||||||
printf " %-30s HTTP %-4s %9s 字节 %6.1fs %6s KB/s\n" \
|
|
||||||
"$f" "$code" "$size" "$secs" "$kbps" | tee -a "$OUT"
|
|
||||||
done
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- D
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "── D. 隧道连接数稳定性(采样 60 秒,每 2 秒一次)──" | tee -a "$OUT"
|
|
||||||
MARK=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
minc=99; maxc=0; vals=()
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
[ -z "${n:-}" ] && n=0
|
|
||||||
[ "$n" -lt "$minc" ] && minc="$n"
|
|
||||||
[ "$n" -gt "$maxc" ] && maxc="$n"
|
|
||||||
vals+=("$n")
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
reconn=$(tail -n +$((MARK+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Registered tunnel connection' || echo 0)
|
|
||||||
lost=$(tail -n +$((MARK+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Lost connection' || echo 0)
|
|
||||||
iot=$(tail -n +$((MARK+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'i/o timeout' || echo 0)
|
|
||||||
echo " 连接数: 最低 $minc 最高 $maxc 采样期间重连 $reconn 次 · 掉线 $lost 次 · i/o timeout $iot 次" | tee -a "$OUT"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- B
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "── B. 长轮询(10 次真实 poll,每次挂 60 秒 ≈ 10 分钟)──" | tee -a "$OUT"
|
|
||||||
STAMP=$(date +%s)
|
|
||||||
DEV="bench-${LABEL}-${STAMP}"
|
|
||||||
CODE=$(printf "%06d" $(( STAMP % 1000000 )))
|
|
||||||
REG=$(curl -s --max-time 30 -X POST "$BASE/v1/agent/register" \
|
|
||||||
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
|
|
||||||
-d "{\"deviceId\":\"$DEV\",\"code\":\"$CODE\",\"name\":\"bench\",\"version\":\"1.8.1\"}")
|
|
||||||
TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
|
||||||
if [ -z "$TOKEN" ]; then
|
|
||||||
echo " [失败] 探针注册没拿到 token" | tee -a "$OUT"
|
|
||||||
else
|
|
||||||
okB=0; timesB=(); failsB=""
|
|
||||||
MARK2=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 130 \
|
|
||||||
-X POST "$BASE/v1/agent/poll" -H "X-Token: $TOKEN" -H "User-Agent: $UA")
|
|
||||||
c=$(echo "$out" | awk '{print $1}')
|
|
||||||
secs=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
|
||||||
if [ "$c" = "200" ]; then
|
|
||||||
okB=$((okB+1)); timesB+=("$secs")
|
|
||||||
printf " %2d: 200 %6dms\n" "$i" "$secs" | tee -a "$OUT"
|
|
||||||
else
|
|
||||||
failsB="$failsB ${i}:${c}"
|
|
||||||
printf " %2d: %-4s **失败**\n" "$i" "$c" | tee -a "$OUT"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo " 长轮询成功: $okB/10 = $((okB*10))% 失败:$failsB" | tee -a "$OUT"
|
|
||||||
if [ "${#timesB[@]}" -gt 0 ]; then
|
|
||||||
printf '%s\n' "${timesB[@]}" | sort -n | awk '{
|
|
||||||
a[NR]=$1
|
|
||||||
} END {
|
|
||||||
printf " 耗时: 最小 %d 中位 %d P90 %d 最大 %d ms(理想 ≈ 61000)\n",
|
|
||||||
a[1], a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]
|
|
||||||
}' | tee -a "$OUT"
|
|
||||||
fi
|
|
||||||
r2=$(tail -n +$((MARK2+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Registered tunnel connection' || echo 0)
|
|
||||||
echo " 这 10 分钟里隧道重连 $r2 次" | tee -a "$OUT"
|
|
||||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
curl -s --max-time 20 -X POST "http://127.0.0.1:8080/v1/admin/delete" \
|
|
||||||
-H "X-Admin: $KEY" -H 'Content-Type: application/json' \
|
|
||||||
-d "{\"deviceId\":\"$DEV\"}" > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo | tee -a "$OUT"
|
|
||||||
echo "════════════════════════════════════════════════════════" | tee -a "$OUT"
|
|
||||||
echo " 基线存档: $OUT" | tee -a "$OUT"
|
|
||||||
|
|
||||||
# 顺便把关键数字汇总成一行,便于前后对比
|
|
||||||
{
|
|
||||||
echo
|
|
||||||
echo "===== 汇总($LABEL)====="
|
|
||||||
echo "A 公网可用率 ${pctA}% ($okA/100)"
|
|
||||||
echo "B 长轮询成功 ${okB:-0}/10"
|
|
||||||
echo "D 隧道连接 最低 ${minc} 最高 ${maxc} 重连 ${reconn} 掉线 ${lost}"
|
|
||||||
} | tee -a "$OUT"
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 测下载吞吐:缓存预热前后对比。
|
|
||||||
#
|
|
||||||
# 基线(未加缓存头时):
|
|
||||||
# setup.exe 237568 字节 / 13.3s = 17 KB/s (还截断了)
|
|
||||||
# server.zip 214900 字节 / 3.1s = 67 KB/s
|
|
||||||
# watch.apk 201523 字节 / 66.8s = 3 KB/s
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
FILES="ykxmz-setup-1.8.1.exe ykxmz-server-1.8.1.zip ykxmz-watch-1.8.1.apk"
|
|
||||||
|
|
||||||
echo "════════ 1. 预热:每个文件先请求一次,把内容推进 Cloudflare 缓存 ════════"
|
|
||||||
for f in $FILES; do
|
|
||||||
# 只取前 1 字节,避免真的下载 15 MB
|
|
||||||
st=$(curl -s -o /dev/null -w '%{http_code}' -r 0-0 --max-time 60 \
|
|
||||||
-H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
cf=$(curl -s -D - -o /dev/null -r 0-0 --max-time 60 -H "User-Agent: $UA" \
|
|
||||||
"$BASE/download/$f" 2>/dev/null | tr -d '\r' | grep -i '^cf-cache-status' | awk '{print $2}')
|
|
||||||
printf " %-30s HTTP %s cf-cache-status=%s\n" "$f" "$st" "${cf:-无}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 等 5 秒让边缘缓存落定…"
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 正式下载(完整文件,量吞吐)════════"
|
|
||||||
total_ok=0
|
|
||||||
for f in $FILES; do
|
|
||||||
rm -f /tmp/dl.bin
|
|
||||||
out=$(curl -s -o /tmp/dl.bin -D /tmp/dl.hdr -w '%{http_code} %{size_download} %{time_total}' \
|
|
||||||
--max-time 240 -H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
size=$(echo "$out" | awk '{print $2}')
|
|
||||||
secs=$(echo "$out" | awk '{print $3}')
|
|
||||||
cf=$(grep -i '^cf-cache-status' /tmp/dl.hdr | tr -d '\r' | awk '{print $2}')
|
|
||||||
age=$(grep -i '^age:' /tmp/dl.hdr | tr -d '\r' | awk '{print $2}')
|
|
||||||
kbps=$(echo "$secs $size" | awk '{if ($1 > 0) printf "%.0f", $2/$1/1024; else print 0}')
|
|
||||||
printf " %-30s HTTP %-4s %9s 字节 %6.1fs %6s KB/s cf=%s age=%s\n" \
|
|
||||||
"$f" "$code" "$size" "$secs" "$kbps" "${cf:-无}" "${age:-无}"
|
|
||||||
[ "$code" = "200" ] && total_ok=$((total_ok+1))
|
|
||||||
done
|
|
||||||
echo " 成功 $total_ok/3"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 再下一次(应该全 HIT,更快)════════"
|
|
||||||
for f in $FILES; do
|
|
||||||
rm -f /tmp/dl.bin
|
|
||||||
out=$(curl -s -o /tmp/dl.bin -D /tmp/dl.hdr -w '%{http_code} %{size_download} %{time_total}' \
|
|
||||||
--max-time 240 -H "User-Agent: $UA" "$BASE/download/$f")
|
|
||||||
code=$(echo "$out" | awk '{print $1}')
|
|
||||||
size=$(echo "$out" | awk '{print $2}')
|
|
||||||
secs=$(echo "$out" | awk '{print $3}')
|
|
||||||
cf=$(grep -i '^cf-cache-status' /tmp/dl.hdr | tr -d '\r' | awk '{print $2}')
|
|
||||||
kbps=$(echo "$secs $size" | awk '{if ($1 > 0) printf "%.0f", $2/$1/1024; else print 0}')
|
|
||||||
printf " %-30s HTTP %-4s %9s 字节 %6.1fs %6s KB/s cf=%s\n" \
|
|
||||||
"$f" "$code" "$size" "$secs" "$kbps" "${cf:-无}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 对比基线 ════════"
|
|
||||||
echo " 基线: setup 17 KB/s(截断)· server 67 KB/s · apk 3 KB/s"
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''从这台 Windows 机器测中转 —— 这才是"真实用户"的视角。
|
|
||||||
|
|
||||||
== 为什么要换测量端 ==
|
|
||||||
|
|
||||||
之前所有测量都是在设备自己上跑的 curl。那等于:
|
|
||||||
设备 4G --> Cloudflare 边缘 (第一段)
|
|
||||||
边缘 --> 隧道 --> 设备 4G 源站 (第二段)
|
|
||||||
**两段都压在同一个 4G 链路上**,量出来的数字是双重打击。
|
|
||||||
|
|
||||||
真实用户(手机/电脑在 WiFi、宽带、5G 上):
|
|
||||||
用户 --> Cloudflare 边缘 (第一段,好网络)
|
|
||||||
边缘 --> 隧道 --> 设备 4G 源站 (第二段,只有这段是 4G)
|
|
||||||
|
|
||||||
所以从这台机器量出来的,才是用户实际会遇到的延迟和可用率。
|
|
||||||
|
|
||||||
用法: python _build/bench_from_pc.py [标签]
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import statistics
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
BASE = "https://relay.xxmxxs.top"
|
|
||||||
UA = "LanRemote/1.8.2"
|
|
||||||
OUT_DIR = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_build")
|
|
||||||
|
|
||||||
|
|
||||||
def req(path: str, method: str = "GET", data: bytes | None = None,
|
|
||||||
headers: dict | None = None, timeout: float = 150.0):
|
|
||||||
"""发一个请求,返回 (状态码, 字节数, 秒数, 响应头)。"""
|
|
||||||
hdrs = {"User-Agent": UA}
|
|
||||||
hdrs.update(headers or {})
|
|
||||||
r = urllib.request.Request(BASE + path, data=data, headers=hdrs,
|
|
||||||
method=method)
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(r, timeout=timeout) as resp:
|
|
||||||
body = resp.read()
|
|
||||||
return resp.status, len(body), time.perf_counter() - t0, resp.headers
|
|
||||||
except urllib.error.HTTPError as exc:
|
|
||||||
try:
|
|
||||||
exc.read()
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
return exc.code, 0, time.perf_counter() - t0, exc.headers
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
return 0, 0, time.perf_counter() - t0, {}
|
|
||||||
|
|
||||||
|
|
||||||
def pct(values: list, q: float) -> int:
|
|
||||||
if not values:
|
|
||||||
return 0
|
|
||||||
s = sorted(values)
|
|
||||||
idx = min(int(len(s) * q), len(s) - 1)
|
|
||||||
return int(s[idx])
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
label = sys.argv[1] if len(sys.argv) > 1 else "pc"
|
|
||||||
lines: list[str] = []
|
|
||||||
|
|
||||||
def say(text: str = "") -> None:
|
|
||||||
print(text)
|
|
||||||
lines.append(text)
|
|
||||||
|
|
||||||
say("=" * 62)
|
|
||||||
say(" 从 PC 量中转 · %s · %s" % (label, time.strftime("%Y-%m-%d %H:%M:%S")))
|
|
||||||
say("=" * 62)
|
|
||||||
|
|
||||||
# ---------------- A. 公网可用率 ----------------
|
|
||||||
say()
|
|
||||||
say("── A. 公网可用率(100 次 /v1/health)──")
|
|
||||||
ok, times, fails = 0, [], 0
|
|
||||||
for i in range(100):
|
|
||||||
code, _n, secs, _h = req("/v1/health", timeout=25)
|
|
||||||
if code == 200:
|
|
||||||
ok += 1
|
|
||||||
times.append(int(secs * 1000))
|
|
||||||
else:
|
|
||||||
fails += 1
|
|
||||||
time.sleep(0.3)
|
|
||||||
say(" 可用率 %d/100 = %d%% 失败 %d 次" % (ok, ok, fails))
|
|
||||||
if times:
|
|
||||||
say(" 耗时 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms"
|
|
||||||
% (min(times), int(statistics.median(times)), pct(times, 0.9),
|
|
||||||
pct(times, 0.99), max(times)))
|
|
||||||
|
|
||||||
# ---------------- E. 静态资源 ----------------
|
|
||||||
say()
|
|
||||||
say("── E. 静态资源(各 10 次)──")
|
|
||||||
for path in ("/", "/shot/pc-main.png", "/favicon.ico"):
|
|
||||||
o, ts = 0, []
|
|
||||||
for _ in range(10):
|
|
||||||
code, _n, secs, _h = req(path, timeout=25)
|
|
||||||
if code == 200:
|
|
||||||
o += 1
|
|
||||||
ts.append(int(secs * 1000))
|
|
||||||
time.sleep(0.2)
|
|
||||||
med = int(statistics.median(ts)) if ts else 0
|
|
||||||
say(" %-22s %2d/10 中位 %d 最大 %d ms"
|
|
||||||
% (path, o, med, max(ts) if ts else 0))
|
|
||||||
|
|
||||||
# ---------------- C. 下载 ----------------
|
|
||||||
say()
|
|
||||||
say("── C. 三个成品下载(完整下载)──")
|
|
||||||
dl_results = {}
|
|
||||||
for name in ("ykxmz-setup-1.8.2.exe", "ykxmz-server-1.8.2.zip",
|
|
||||||
"ykxmz-watch-1.8.2.apk"):
|
|
||||||
code, size, secs, hdrs = req("/download/" + name, timeout=300)
|
|
||||||
kbps = (size / secs / 1024) if secs > 0 else 0
|
|
||||||
cf = (hdrs.get("cf-cache-status") or "?") if hdrs else "?"
|
|
||||||
say(" %-30s HTTP %-4s %9d 字节 %6.1fs %7.0f KB/s cf=%s"
|
|
||||||
% (name, code, size, secs, kbps, cf))
|
|
||||||
if code == 200 and size > 0:
|
|
||||||
dl_results[name] = kbps
|
|
||||||
|
|
||||||
# ---------------- B. 长轮询 ----------------
|
|
||||||
say()
|
|
||||||
say("── B. 长轮询(6 次真实 poll,每次挂 60 秒)──")
|
|
||||||
stamp = int(time.time())
|
|
||||||
dev = "pcbench-%s-%d" % (label, stamp)
|
|
||||||
code6 = "%06d" % (stamp % 1000000)
|
|
||||||
payload = json.dumps({
|
|
||||||
"deviceId": dev, "code": code6, "name": "pcbench",
|
|
||||||
"version": "1.8.2",
|
|
||||||
}, ensure_ascii=False).encode("utf-8")
|
|
||||||
status, _n, _s, _h = req("/v1/agent/register", method="POST",
|
|
||||||
data=payload, timeout=30,
|
|
||||||
headers={"Content-Type": "application/json"})
|
|
||||||
token = ""
|
|
||||||
if status == 200:
|
|
||||||
# 重新取一次拿 body
|
|
||||||
r = urllib.request.Request(
|
|
||||||
BASE + "/v1/agent/register", data=payload,
|
|
||||||
headers={"User-Agent": UA, "Content-Type": "application/json"},
|
|
||||||
method="POST")
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
|
||||||
token = json.loads(resp.read().decode("utf-8")).get("token", "")
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
say(" 探针注册: %s" % ("成功" if token else "失败"))
|
|
||||||
|
|
||||||
if token:
|
|
||||||
okb, btimes, bfails = 0, [], []
|
|
||||||
for i in range(6):
|
|
||||||
code, _n, secs, _h = req(
|
|
||||||
"/v1/agent/poll", method="POST", data=b"",
|
|
||||||
headers={"X-Token": token}, timeout=150)
|
|
||||||
ms = int(secs * 1000)
|
|
||||||
if code == 200:
|
|
||||||
okb += 1
|
|
||||||
btimes.append(ms)
|
|
||||||
say(" %d: 200 %6dms" % (i + 1, ms))
|
|
||||||
else:
|
|
||||||
bfails.append("%d:%s" % (i + 1, code))
|
|
||||||
say(" %d: %-4s **失败**" % (i + 1, code))
|
|
||||||
say(" 长轮询成功 %d/6 = %d%% 失败: %s"
|
|
||||||
% (okb, okb * 100 // 6, " ".join(bfails) or "无"))
|
|
||||||
if btimes:
|
|
||||||
say(" 耗时 最小 %d 中位 %d P90 %d 最大 %d ms(理想 ≈ 61000)"
|
|
||||||
% (min(btimes), int(statistics.median(btimes)),
|
|
||||||
pct(btimes, 0.9), max(btimes)))
|
|
||||||
|
|
||||||
say()
|
|
||||||
say("=" * 62)
|
|
||||||
out = OUT_DIR / ("bench_pc_%s.txt" % label)
|
|
||||||
out.write_text("\n".join(lines), encoding="utf-8")
|
|
||||||
say(" 存档: %s" % out)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
==============================================================
|
|
||||||
从 PC 量中转 · optimized · 2026-10-07 16:01:23
|
|
||||||
==============================================================
|
|
||||||
|
|
||||||
── A. 公网可用率(100 次 /v1/health)──
|
|
||||||
可用率 98/100 = 98% 失败 2 次
|
|
||||||
耗时 最小 765 中位 911 P90 1889 P99 23868 最大 23868 ms
|
|
||||||
|
|
||||||
── E. 静态资源(各 10 次)──
|
|
||||||
/ 9/10 中位 1016 最大 1223 ms
|
|
||||||
/shot/pc-main.png 10/10 中位 1160 最大 1914 ms
|
|
||||||
/favicon.ico 8/10 中位 1075 最大 1993 ms
|
|
||||||
|
|
||||||
── C. 三个成品下载(完整下载)──
|
|
||||||
ykxmz-setup-1.8.2.exe HTTP 404 0 字节 0.8s 0 KB/s cf=BYPASS
|
|
||||||
ykxmz-server-1.8.2.zip HTTP 404 0 字节 0.8s 0 KB/s cf=BYPASS
|
|
||||||
ykxmz-watch-1.8.2.apk HTTP 404 0 字节 0.9s 0 KB/s cf=BYPASS
|
|
||||||
|
|
||||||
── B. 长轮询(6 次真实 poll,每次挂 60 秒)──
|
|
||||||
探针注册: 成功
|
|
||||||
1: 200 60817ms
|
|
||||||
2: 0 **失败**
|
|
||||||
3: 200 61758ms
|
|
||||||
4: 200 62027ms
|
|
||||||
5: 200 62060ms
|
|
||||||
6: 200 61247ms
|
|
||||||
长轮询成功 5/6 = 83% 失败: 2:0
|
|
||||||
耗时 最小 60817 中位 61758 P90 62060 最大 62060 ms(理想 ≈ 61000)
|
|
||||||
|
|
||||||
==============================================================
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 测"开机 -> 服务可用"要多久,看能不能缩短
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "════════ 1. 系统启动耗时 ════════"
|
|
||||||
systemd-analyze 2>/dev/null | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
echo " 最慢的几项:"
|
|
||||||
systemd-analyze blame 2>/dev/null | head -12 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 关键服务的启动时刻 ════════"
|
|
||||||
BOOT=$(date -d "$(uptime -s)" +%s 2>/dev/null || echo 0)
|
|
||||||
for svc in lanremote-nettune lanremote-relay lanremote-tunnel; do
|
|
||||||
ts=$(systemctl show "$svc" -p ActiveEnterTimestampMonotonic --value 2>/dev/null)
|
|
||||||
if [ -n "$ts" ] && [ "$ts" != "0" ]; then
|
|
||||||
sec=$((ts / 1000000))
|
|
||||||
printf " %-24s 开机后 %s 秒\n" "$svc" "$sec"
|
|
||||||
else
|
|
||||||
printf " %-24s (读不到)\n" "$svc"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 隧道是什么时候连上的 ════════"
|
|
||||||
grep -m3 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null \
|
|
||||||
| sed 's/^/ /'
|
|
||||||
echo " 当前时间: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
||||||
echo " 日志里第一条注册时间戳对比一下就知道隔了多久"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 开机第一条日志到隧道可用,中间卡在哪 ════════"
|
|
||||||
journalctl -b --no-pager 2>/dev/null \
|
|
||||||
| grep -E 'lanremote|cloudflared|Reached target network|Startup finished' \
|
|
||||||
| head -20 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. network-online.target 花了多久 ════════"
|
|
||||||
journalctl -b --no-pager -u NetworkManager -u systemd-networkd 2>/dev/null \
|
|
||||||
| grep -iE 'online|carrier|dhcp|connected' | head -8 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 有没有办法提前(当前依赖链)════════"
|
|
||||||
systemctl show lanremote-tunnel -p After -p Wants -p Requires --value 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 7. watchdog 间隔现在是多久 ════════"
|
|
||||||
grep -E 'OnUnitActiveSec|OnBootSec' /etc/systemd/system/lanremote-watchdog.timer 2>/dev/null | sed 's/^/ /'
|
|
||||||
systemctl cat lanremote-watchdog.timer 2>/dev/null | grep -E 'OnUnit|OnBoot' | sed 's/^/ /'
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''解开"nettune 死锁",然后把根因修掉。
|
|
||||||
|
|
||||||
== 死锁是怎么回事 ==
|
|
||||||
|
|
||||||
lanremote-nettune.service:
|
|
||||||
Before=lanremote-tunnel.service <- 关键
|
|
||||||
ExecStart=/bin/bash net-tune.sh
|
|
||||||
|
|
||||||
net-tune.sh 第 4 步:
|
|
||||||
systemctl restart lanremote-tunnel <- 在这里永远阻塞
|
|
||||||
|
|
||||||
为什么阻塞:systemd 正在启动 nettune,而 nettune 声明了"我必须在
|
|
||||||
隧道之前启动完"。脚本却在**自己还没结束的时候**去 restart 隧道 ——
|
|
||||||
那要求隧道先停、再等 nettune 结束……systemd 检测到这是顺序冲突,
|
|
||||||
restart 就一直挂着不返回。
|
|
||||||
|
|
||||||
证据:CGroup 里能看到 net-tune.sh(13388)下面挂着
|
|
||||||
systemctl restart lanremote-tunnel(13428),
|
|
||||||
etime 09:32 —— 卡了 9 分半,nettune 一直是 activating。
|
|
||||||
|
|
||||||
后果:nettune 卡住 -> 隧道被 Before= 挡着起不来 -> 公网 530。
|
|
||||||
这就是用户说的"网站还是容易挂"的真因之一。
|
|
||||||
|
|
||||||
== 为什么以前没暴露 ==
|
|
||||||
|
|
||||||
设备上原来那份 unit 的 ExecStart 里有个 `iptables ... || ...`,
|
|
||||||
systemd 不支持 ||,所以它**在第 2 条 ExecStart 就报错退出了**,
|
|
||||||
根本没走到"重启隧道"那一步 —— 死锁被那个 bug 遮住了。
|
|
||||||
我把 unit 换成正确形状之后,死锁才露出来。
|
|
||||||
|
|
||||||
== 这一步做什么 ==
|
|
||||||
|
|
||||||
1. 杀掉卡住的 systemctl 和 net-tune.sh(解开当前死锁)
|
|
||||||
2. 拉起隧道(网站先恢复)
|
|
||||||
3. 把 net-tune.sh 的"重启隧道"改成**只在手动跑时做**
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
sys.path.insert(0, str(ROOT / "_build"))
|
|
||||||
from devssh import run # noqa: E402
|
|
||||||
|
|
||||||
REMOTE = r'''
|
|
||||||
echo "════ 1. 解开死锁 ════"
|
|
||||||
# 先杀卡住的 systemctl(不然它会一直拽着 systemd 的 job)
|
|
||||||
pkill -f 'systemctl restart lanremote-tunnel' 2>/dev/null && echo " 杀了卡住的 systemctl" || echo " 没有卡住的 systemctl"
|
|
||||||
sleep 2
|
|
||||||
# 再让 nettune 的脚本结束(杀它的子进程已经让主脚本继续往下走了)
|
|
||||||
systemctl stop lanremote-nettune 2>/dev/null
|
|
||||||
systemctl reset-failed lanremote-nettune 2>/dev/null
|
|
||||||
sleep 2
|
|
||||||
echo " nettune 现在: $(systemctl is-active lanremote-nettune)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 2. 拉起隧道(网站先恢复)════"
|
|
||||||
systemctl reset-failed lanremote-tunnel lanremote-tunnel-h2 2>/dev/null
|
|
||||||
systemctl start lanremote-tunnel lanremote-tunnel-h2
|
|
||||||
sleep 15
|
|
||||||
echo " tunnel: $(systemctl is-active lanremote-tunnel)"
|
|
||||||
echo " tunnel-h2: $(systemctl is-active lanremote-tunnel-h2)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 3. 验公网 ════"
|
|
||||||
for i in 1 2 3; do
|
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://relay.xxmxxs.top/v1/health)
|
|
||||||
echo " 第 $i 次: $code"
|
|
||||||
[ "$code" = "200" ] && break
|
|
||||||
sleep 6
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 4. 隧道连接数 ════"
|
|
||||||
for p in 20241 20242; do
|
|
||||||
n=$(curl -s --max-time 5 http://127.0.0.1:$p/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1)
|
|
||||||
echo " $p -> ${n:-拿不到}"
|
|
||||||
done
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
out = run(REMOTE, timeout=300)
|
|
||||||
text = out[1] if isinstance(out, tuple) else str(out)
|
|
||||||
print(text)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''把全仓版本号从 1.7.1 升到 1.8.0。
|
|
||||||
|
|
||||||
为什么写脚本而不是手动改:
|
|
||||||
版本号散落在 8 个文件里(Python / Java manifest / PowerShell /
|
|
||||||
Inno Setup / 测试 / 打包脚本),手动改必漏。
|
|
||||||
而且 .ps1 必须保留 UTF-8 BOM —— 用 edit 工具改会把 BOM 吃掉,
|
|
||||||
导致 PowerShell 按 GBK 解析、中文变乱码、脚本报语法错(这坑踩过)。
|
|
||||||
|
|
||||||
所以统一用 Python 读写字节,按后缀决定要不要 BOM。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
|
|
||||||
OLD = "1.7.1"
|
|
||||||
NEW = "1.8.0"
|
|
||||||
BOM = b"\xef\xbb\xbf"
|
|
||||||
|
|
||||||
TARGETS = [
|
|
||||||
"android/AndroidManifest.xml",
|
|
||||||
"android/build_apk.ps1",
|
|
||||||
"installer/ykxmz.iss",
|
|
||||||
"relay/relay.py",
|
|
||||||
"windows/lanremote/__init__.py",
|
|
||||||
"_tests/test_apk_contents.py",
|
|
||||||
"_tests/test_package_remote.py",
|
|
||||||
"_build/package_remote.py",
|
|
||||||
]
|
|
||||||
|
|
||||||
NEEDS_BOM = {".ps1"}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
print(" %s -> %s" % (OLD, NEW))
|
|
||||||
print()
|
|
||||||
total = 0
|
|
||||||
for rel in TARGETS:
|
|
||||||
path = ROOT / rel
|
|
||||||
if not path.is_file():
|
|
||||||
print(" [跳过] %s 不存在" % rel)
|
|
||||||
continue
|
|
||||||
|
|
||||||
raw = path.read_bytes()
|
|
||||||
had_bom = raw.startswith(BOM)
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
|
|
||||||
if OLD not in text:
|
|
||||||
print(" [跳过] %-38s 里面没有 %s" % (rel, OLD))
|
|
||||||
continue
|
|
||||||
|
|
||||||
count = text.count(OLD)
|
|
||||||
text = text.replace(OLD, NEW)
|
|
||||||
|
|
||||||
want_bom = path.suffix in NEEDS_BOM or had_bom
|
|
||||||
data = text.encode("utf-8")
|
|
||||||
if want_bom:
|
|
||||||
data = BOM + data
|
|
||||||
path.write_bytes(data)
|
|
||||||
|
|
||||||
note = ""
|
|
||||||
if want_bom and not had_bom:
|
|
||||||
note = "(补上 BOM)"
|
|
||||||
elif had_bom and not want_bom:
|
|
||||||
note = "(去掉 BOM)"
|
|
||||||
print(" %-38s 改了 %d 处%s" % (rel, count, note))
|
|
||||||
total += count
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 共改 %d 处" % total)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 复核 —— 代码里还有哪里留着 %s:" % OLD)
|
|
||||||
found = False
|
|
||||||
for path in sorted(ROOT.rglob("*")):
|
|
||||||
if not path.is_file():
|
|
||||||
continue
|
|
||||||
rel = str(path.relative_to(ROOT))
|
|
||||||
if any(part in rel for part in
|
|
||||||
(".git\\", "dist\\", "dist_exe", "_build\\_", "toolchain",
|
|
||||||
"node_modules", "__pycache__", "_shots")):
|
|
||||||
continue
|
|
||||||
if path.suffix not in (".py", ".java", ".iss", ".ps1", ".md",
|
|
||||||
".json", ".sh", ".bat", ".xml"):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
if OLD in text:
|
|
||||||
tag = "(文档,可保留)" if path.suffix == ".md" else "**要改**"
|
|
||||||
print(" %-52s %s" % (rel, tag))
|
|
||||||
if path.suffix != ".md":
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
print(" (代码里已经没有了)")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" BOM 复核(.ps1 必须有):")
|
|
||||||
for rel in TARGETS:
|
|
||||||
path = ROOT / rel
|
|
||||||
if not path.is_file():
|
|
||||||
continue
|
|
||||||
has = path.read_bytes().startswith(BOM)
|
|
||||||
want = path.suffix in NEEDS_BOM
|
|
||||||
mark = "OK" if has == want else "**不对**"
|
|
||||||
print(" %-38s BOM=%-5s 应=%-5s %s" % (rel, has, want, mark))
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''版本号 1.8.0 -> 1.8.1。
|
|
||||||
|
|
||||||
这次是 bug 修复版:局域网劫持 + 长轮询 60 秒。
|
|
||||||
(用户规矩:每次上传都要升版号。)
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
OLD, NEW = "1.8.0", "1.8.1"
|
|
||||||
BOM = b"\xef\xbb\xbf"
|
|
||||||
|
|
||||||
TARGETS = [
|
|
||||||
"android/AndroidManifest.xml",
|
|
||||||
"android/build_apk.ps1",
|
|
||||||
"installer/ykxmz.iss",
|
|
||||||
"relay/relay.py",
|
|
||||||
"windows/lanremote/__init__.py",
|
|
||||||
"_tests/test_apk_contents.py",
|
|
||||||
"_tests/test_package_remote.py",
|
|
||||||
"_build/package_remote.py",
|
|
||||||
"_build/test_installer_cycle.py",
|
|
||||||
]
|
|
||||||
NEEDS_BOM = {".ps1"}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
print(" %s -> %s" % (OLD, NEW))
|
|
||||||
total = 0
|
|
||||||
for rel in TARGETS:
|
|
||||||
path = ROOT / rel
|
|
||||||
if not path.is_file():
|
|
||||||
print(" [跳过] %s 不存在" % rel)
|
|
||||||
continue
|
|
||||||
raw = path.read_bytes()
|
|
||||||
had_bom = raw.startswith(BOM)
|
|
||||||
text = raw.decode("utf-8-sig")
|
|
||||||
if OLD not in text:
|
|
||||||
print(" [跳过] %-40s 里面没有 %s" % (rel, OLD))
|
|
||||||
continue
|
|
||||||
count = text.count(OLD)
|
|
||||||
text = text.replace(OLD, NEW)
|
|
||||||
want_bom = path.suffix in NEEDS_BOM or had_bom
|
|
||||||
data = text.encode("utf-8")
|
|
||||||
if want_bom:
|
|
||||||
data = BOM + data
|
|
||||||
path.write_bytes(data)
|
|
||||||
print(" %-40s 改了 %d 处" % (rel, count))
|
|
||||||
total += count
|
|
||||||
print()
|
|
||||||
print(" 共改 %d 处" % total)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r"""精准升级版本号 1.4.0 -> 1.5.0。
|
||||||
|
|
||||||
|
**不能全局替换** —— 项目里有 40 多个文件含 "1.4.0",其中大部分是:
|
||||||
|
* 测试用的数据(配对码、设备版本号字段)
|
||||||
|
* HTTP User-Agent "LanRemote/1.4.0"
|
||||||
|
(Cloudflare 的 Browser Integrity Check 认这个,
|
||||||
|
而且 20 多个测试都在断言它 —— 动它要同步改一大片)
|
||||||
|
* _build/ 下的一次性脚本(历史记录,不用改)
|
||||||
|
|
||||||
|
所以只改**用户能看到版本号的地方**:
|
||||||
|
1. android/build_apk.ps1 APK 文件名 + manifest 版本
|
||||||
|
2. docs/*.txt 三份文档
|
||||||
|
3. windows/lanremote/__init__.py __version__
|
||||||
|
4. 会断言版本号的测试
|
||||||
|
|
||||||
|
UA 保持 "LanRemote/1.4.0" 不动(它是协议标识,不是产品版本)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||||
|
OLD, NEW = "1.4.0", "1.5.0"
|
||||||
|
|
||||||
|
# 逐文件、逐模式改,写清楚每一处为什么改
|
||||||
|
PLAN = [
|
||||||
|
# (文件, 旧片段, 新片段, 说明)
|
||||||
|
("android/build_apk.ps1",
|
||||||
|
'$apkName = "ykxmz-watch-1.4.0.apk"',
|
||||||
|
'$apkName = "ykxmz-watch-1.5.0.apk"',
|
||||||
|
"APK 文件名"),
|
||||||
|
("android/build_apk.ps1",
|
||||||
|
'--version-code 3 `',
|
||||||
|
'--version-code 5 `',
|
||||||
|
"versionCode"),
|
||||||
|
("android/build_apk.ps1",
|
||||||
|
'--version-name "1.4.0" `',
|
||||||
|
'--version-name "1.5.0" `',
|
||||||
|
"versionName"),
|
||||||
|
("windows/lanremote/__init__.py",
|
||||||
|
'"1.4.0"',
|
||||||
|
'"1.5.0"',
|
||||||
|
"__version__"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 文档:把"版本 1.4.0"之类改掉
|
||||||
|
DOC_PATTERNS = [
|
||||||
|
(r"1\.4\.0", "1.5.0"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 测试里断言成品文件名的
|
||||||
|
TEST_PATTERNS = [
|
||||||
|
("ykxmz-1.4.0", "ykxmz-1.5.0"),
|
||||||
|
("ykxmz-watch-1.4.0", "ykxmz-watch-1.5.0"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
changed = []
|
||||||
|
|
||||||
|
# ---- 1. 精准替换 ----
|
||||||
|
for rel, old, new, why in PLAN:
|
||||||
|
path = ROOT / rel
|
||||||
|
if not path.is_file():
|
||||||
|
print(f" [跳过] {rel} 不存在")
|
||||||
|
continue
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
if old not in text:
|
||||||
|
print(f" [跳过] {rel}: 找不到 {old[:40]!r}(可能已改)")
|
||||||
|
continue
|
||||||
|
path.write_text(text.replace(old, new, 1),
|
||||||
|
encoding="utf-8", newline="\n")
|
||||||
|
changed.append(f"{rel}({why})")
|
||||||
|
|
||||||
|
# ---- 2. 文档 ----
|
||||||
|
for path in sorted((ROOT / "docs").glob("*.txt")):
|
||||||
|
raw = path.read_bytes()
|
||||||
|
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
text = raw.decode("utf-8-sig")
|
||||||
|
new_text = re.sub(r"1\.4\.0", "1.5.0", text)
|
||||||
|
if new_text != text:
|
||||||
|
path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
|
||||||
|
+ new_text.replace("\r\n", "\n").encode("utf-8"))
|
||||||
|
changed.append(f"docs/{path.name}")
|
||||||
|
|
||||||
|
# ---- 3. 断言成品文件名的测试 ----
|
||||||
|
for name in ("test_package_remote.py", "test_apk_contents.py",
|
||||||
|
"acceptance.py"):
|
||||||
|
path = ROOT / "_tests" / name
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
original = text
|
||||||
|
for old, new in TEST_PATTERNS:
|
||||||
|
text = text.replace(old, new)
|
||||||
|
if text != original:
|
||||||
|
path.write_text(text, encoding="utf-8", newline="\n")
|
||||||
|
changed.append(f"_tests/{name}")
|
||||||
|
|
||||||
|
print(f" 共改了 {len(changed)} 处:")
|
||||||
|
for item in changed:
|
||||||
|
print(f" {item}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 注意:HTTP User-Agent 仍是 \"LanRemote/1.4.0\"(协议标识,不是产品版本)。")
|
||||||
|
|
||||||
|
# ---- 复核 ----
|
||||||
|
print()
|
||||||
|
print(" 复核关键位置:")
|
||||||
|
for rel, needle in (("android/build_apk.ps1", "ykxmz-watch-1.5.0.apk"),
|
||||||
|
("android/AndroidManifest.xml", '1.5.0'),
|
||||||
|
("_build/package_remote.py", '"1.5.0"')):
|
||||||
|
path = ROOT / rel
|
||||||
|
if path.is_file():
|
||||||
|
ok = needle in path.read_text(encoding="utf-8")
|
||||||
|
print(f" {rel}: {'✓' if ok else '✗'} 含 {needle}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -83,27 +83,9 @@ def main() -> int:
|
|||||||
changed += 1
|
changed += 1
|
||||||
|
|
||||||
# ---- Windows EXE 版本信息 ----
|
# ---- Windows EXE 版本信息 ----
|
||||||
# ⚠ 原来用的是 f'"{OLD}.0"',一旦 OLD 和文件里对不上就静默跳过
|
if swap(ROOT / "windows" / "version_info.txt",
|
||||||
# (version_info.txt 就是这么从 1.7.1 一直漏到现在的)。
|
[(f'"{OLD}.0"', f'"{NEW}.0"')],
|
||||||
# 改成正则匹配任意四段版本。
|
f"EXE 文件版本 {NEW}.0"):
|
||||||
import re as _re
|
|
||||||
_vi = ROOT / "windows" / "version_info.txt"
|
|
||||||
if _vi.is_file():
|
|
||||||
_raw = _vi.read_bytes()
|
|
||||||
_bom = _raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
_txt = _raw.decode("utf-8-sig")
|
|
||||||
_new = _re.sub(
|
|
||||||
r'(StringStruct\("(?:File|Product)Version",\\s*")\\d+\\.\\d+\\.\\d+\\.\\d+("\))',
|
|
||||||
r'\\g<1>' + NEW + r'.0\\g<2>', _txt)
|
|
||||||
if _new != _txt:
|
|
||||||
_vi.write_bytes((b"\xef\xbb\xbf" if _bom else b"")
|
|
||||||
+ _new.replace("\\r\\n", "\\n").encode("utf-8"))
|
|
||||||
print(f" version_info.txt: EXE 文件版本 {NEW}.0")
|
|
||||||
changed += 1
|
|
||||||
else:
|
|
||||||
print(" [跳过] version_info.txt")
|
|
||||||
|
|
||||||
if False:
|
|
||||||
changed += 1
|
changed += 1
|
||||||
|
|
||||||
# ---- __init__ ----
|
# ---- __init__ ----
|
||||||
@@ -118,22 +100,6 @@ def main() -> int:
|
|||||||
f"VERSION={NEW}"):
|
f"VERSION={NEW}"):
|
||||||
changed += 1
|
changed += 1
|
||||||
|
|
||||||
# ---- 安装包版本 ----
|
|
||||||
# ⚠ 原来漏了这三处,导致 version_info.txt 从 1.7.1 起就一直没跟上
|
|
||||||
# (用户在"文件属性 -> 详细信息"里看到一个错三个版本的号)。
|
|
||||||
if swap(ROOT / "installer" / "ykxmz.iss",
|
|
||||||
[(f'#define MyAppVersion "{OLD}"',
|
|
||||||
f'#define MyAppVersion "{NEW}"'),
|
|
||||||
(f'ykxmz-setup-{OLD}.exe', f'ykxmz-setup-{NEW}.exe')],
|
|
||||||
f"安装包版本 {NEW}"):
|
|
||||||
changed += 1
|
|
||||||
|
|
||||||
# ---- 安装许可页上的版本 ----
|
|
||||||
if swap(ROOT / "installer" / "license-combined.txt",
|
|
||||||
[(f"版本:{OLD}", f"版本:{NEW}")],
|
|
||||||
f"许可页版本 {NEW}"):
|
|
||||||
changed += 1
|
|
||||||
|
|
||||||
# ---- 三份文档 ----
|
# ---- 三份文档 ----
|
||||||
for path in sorted((ROOT / "docs").glob("*.txt")):
|
for path in sorted((ROOT / "docs").glob("*.txt")):
|
||||||
raw = path.read_bytes()
|
raw = path.read_bytes()
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 检查管理页的深色残留
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=http://127.0.0.1:8080
|
|
||||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
|
|
||||||
curl -s -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan" -o /tmp/adm.html 2>/dev/null
|
|
||||||
|
|
||||||
echo " 管理页 $(wc -c < /tmp/adm.html) 字节"
|
|
||||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan")"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 深色值残留:"
|
|
||||||
found=0
|
|
||||||
for c in 0b1220 262c36 1b222c 1c2733 213042 2d1214 4a1a1e 484f58 0d1117; do
|
|
||||||
n=$(grep -c "#$c" /tmp/adm.html 2>/dev/null || echo 0)
|
|
||||||
if [ "$n" != "0" ]; then
|
|
||||||
echo " #$c : $n 处"
|
|
||||||
found=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ "$found" = "0" ] && echo " 无 ✓"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 关键样式:"
|
|
||||||
grep -o -- '--bg:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ /'
|
|
||||||
grep -o 'background:#f6f8fa' /tmp/adm.html | head -1 | sed 's/^/ 地址框: /'
|
|
||||||
grep -o 'button:hover{background:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ 按钮悬停: /'
|
|
||||||
grep -o 'button.del{background:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ 删除按钮: /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 功能接口还正常吗:"
|
|
||||||
for api in /v1/health /v1/downloads /v1/admin/netcheck; do
|
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' -H "X-Admin: $KEY" --max-time 10 "$BASE$api")
|
|
||||||
echo " $api HTTP $code"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 其余页面没受影响:"
|
|
||||||
echo " 官网 HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/")"
|
|
||||||
echo " favicon HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/favicon.ico")"
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 设备刚重启过(up 4 分钟)—— 查开机后各服务有没有自己起来
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "════════ 1. 开机多久了 ════════"
|
|
||||||
uptime | sed 's/^/ /'
|
|
||||||
echo " 开机于: $(who -b 2>/dev/null | awk '{print $3, $4}' || echo '?')"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 三个服务 ════════"
|
|
||||||
for svc in lanremote-relay lanremote-tunnel lanremote-nettune lanremote-watchdog.timer; do
|
|
||||||
printf " %-26s %s (%s)\n" "$svc" "$(systemctl is-active $svc 2>&1)" "$(systemctl is-enabled $svc 2>&1)"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 开机自启清单里有没有它们 ════════"
|
|
||||||
systemctl list-unit-files 2>/dev/null | grep -i lanremote | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 隧道连接数 ════════"
|
|
||||||
echo " $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') 条"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 网络调优参数还在吗 ════════"
|
|
||||||
for k in tcp_keepalive_time tcp_keepalive_intvl tcp_mtu_probing tcp_slow_start_after_idle; do
|
|
||||||
printf " %-30s = %s\n" "$k" "$(sysctl -n net.ipv4.$k 2>/dev/null || echo '?')"
|
|
||||||
done
|
|
||||||
echo " MSS 钳制规则: $(iptables -t mangle -S 2>/dev/null | grep -c 'TCPMSS' || echo 0) 条"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 本机中转 ════════"
|
|
||||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1:8080/v1/health)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 7. 公网(浏览器实际走的)════════"
|
|
||||||
echo " 隧道地址: $(cat /root/lanremote-relay/public_url.txt 2>/dev/null || echo '(没有)')"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
|
||||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health)
|
|
||||||
[ "$c" = "200" ] && ok=$((ok+1))
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/10"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 8. 新地址写进状态文件了吗 ════════"
|
|
||||||
cat /root/lanremote-relay/public_url.txt 2>/dev/null | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "════════ 9. 当前 IP(换网络后)════════"
|
|
||||||
hostname -I | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 10. watchdog 最近记录 ════════"
|
|
||||||
tail -8 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /'
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 确认 BBR 能不能用,并把有效的调优持久化
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "===== 1. 内核信息 ====="
|
|
||||||
echo " 版本: $(uname -r)"
|
|
||||||
echo " 架构: $(uname -m)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 2. BBR 模块存在吗 ====="
|
|
||||||
found=$(find /lib/modules -name 'tcp_bbr*' 2>/dev/null | head -3)
|
|
||||||
if [ -n "$found" ]; then
|
|
||||||
echo " 找到模块文件:"
|
|
||||||
echo "$found" | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo " 没有任何 tcp_bbr 模块文件"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 3. 内核编译时开了 BBR 吗 ====="
|
|
||||||
if [ -f /proc/config.gz ]; then
|
|
||||||
zgrep -i CONFIG_TCP_CONG_BBR /proc/config.gz | sed 's/^/ /'
|
|
||||||
elif [ -f /boot/config-$(uname -r) ]; then
|
|
||||||
grep -i CONFIG_TCP_CONG_BBR /boot/config-$(uname -r) | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo " 没有内核配置文件可查"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 4. 试着手动加载 ====="
|
|
||||||
modprobe tcp_bbr 2>&1 | sed 's/^/ /' || true
|
|
||||||
echo " 加载后可用算法: $(sysctl -n net.ipv4.tcp_available_congestion_control)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 5. 当前生效的网络参数 ====="
|
|
||||||
for k in net.ipv4.tcp_congestion_control net.core.default_qdisc \
|
|
||||||
net.core.rmem_max net.core.wmem_max net.ipv4.tcp_fastopen \
|
|
||||||
net.ipv4.tcp_tw_reuse net.ipv4.tcp_mtu_probing; do
|
|
||||||
printf " %-40s = %s\n" "$k" "$(sysctl -n $k 2>/dev/null || echo '?')"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== 6. 既然没有 BBR,用 cubic 能调的都调上 ====="
|
|
||||||
cat > /etc/sysctl.d/98-ykxmz-net.conf <<'EOF'
|
|
||||||
# ykxmz 网络调优(4G 高丢包链路)
|
|
||||||
#
|
|
||||||
# ⚠ 这台设备的精简内核**没有 BBR**(只有 reno/cubic),
|
|
||||||
# 所以下面用 cubic,再把其它能帮上忙的参数调好。
|
|
||||||
# 如果以后换了带 BBR 的内核,把 tcp_congestion_control 改成 bbr 即可。
|
|
||||||
|
|
||||||
# TCP 缓冲:长途高延迟链路需要更大窗口
|
|
||||||
net.core.rmem_max = 4194304
|
|
||||||
net.core.wmem_max = 4194304
|
|
||||||
net.ipv4.tcp_rmem = 4096 87380 4194304
|
|
||||||
net.ipv4.tcp_wmem = 4096 65536 4194304
|
|
||||||
|
|
||||||
# 快速打开:省一个 RTT
|
|
||||||
net.ipv4.tcp_fastopen = 3
|
|
||||||
|
|
||||||
# 空闲后不重置拥塞窗口 —— 长轮询场景很关键,
|
|
||||||
# 否则每次轮询间隔后都要从慢启动重新爬
|
|
||||||
net.ipv4.tcp_slow_start_after_idle = 0
|
|
||||||
|
|
||||||
# 早重传,别干等
|
|
||||||
net.ipv4.tcp_retries2 = 8
|
|
||||||
|
|
||||||
# 复用 TIME_WAIT 连接
|
|
||||||
net.ipv4.tcp_tw_reuse = 1
|
|
||||||
net.ipv4.tcp_fin_timeout = 15
|
|
||||||
|
|
||||||
# 接收缓冲自动调节
|
|
||||||
net.ipv4.tcp_moderate_rcvbuf = 1
|
|
||||||
EOF
|
|
||||||
sysctl -p /etc/sysctl.d/98-ykxmz-net.conf 2>&1 | grep -v '^net.core.default_qdisc\|^net.ipv4.tcp_congestion_control' | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 最终拥塞控制: $(sysctl -n net.ipv4.tcp_congestion_control)"
|
|
||||||
echo " 最终队列: $(sysctl -n net.core.default_qdisc)"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看各类资源的响应头,判断有哪些能被 Cloudflare 缓存却因为 no-store 被挡了。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.1'
|
|
||||||
|
|
||||||
echo "════════ 各路径的缓存相关响应头 ════════"
|
|
||||||
for p in "/" "/v1/health" "/shot/pc-main.png" "/favicon.ico" \
|
|
||||||
"/download/ykxmz-server-1.8.1.zip"; do
|
|
||||||
echo " ── $p ──"
|
|
||||||
curl -sI --max-time 30 -H "User-Agent: $UA" "$BASE$p" 2>/dev/null \
|
|
||||||
| grep -iE '^HTTP|^cache-control|^cf-cache-status|^content-type|^content-length|^expires|^age' \
|
|
||||||
| tr -d '\r' | sed 's/^/ /'
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 静态图中的 cf-cache-status(HIT 说明缓存生效)════════"
|
|
||||||
for i in 1 2 3; do
|
|
||||||
s=$(curl -sI --max-time 20 -H "User-Agent: $UA" "$BASE/shot/pc-main.png" 2>/dev/null \
|
|
||||||
| grep -i '^cf-cache-status' | tr -d '\r' | awk '{print $2}')
|
|
||||||
echo " 第 $i 次: cf-cache-status = ${s:-?}"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ relay 源码里缓存头的设置 ════════"
|
|
||||||
grep -n 'Cache-Control' /root/lanremote-relay/relay.py | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 下载路由的实现(看有没有做流式/分块)════════"
|
|
||||||
sed -n '/if path.startswith("\/download\/")/,/^ # /p' /root/lanremote-relay/relay.py \
|
|
||||||
| head -50 | sed 's/^/ /'
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看长测进度 + 实时连接数波动
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "=== 长测进度 ==="
|
|
||||||
tail -20 /root/longstab_quic10.txt 2>/dev/null
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== 连接数实时(每 3 秒一次,共 12 次 = 36 秒)==="
|
|
||||||
for i in $(seq 1 12); do
|
|
||||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
printf " %2d: %s\n" "$i" "${n:-?}"
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== 这 36 秒里隧道日志的新事件 ==="
|
|
||||||
MARK=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
|
||||||
sleep 1
|
|
||||||
journalctl -u lanremote-tunnel --since '2 minutes ago' --no-pager 2>/dev/null \
|
|
||||||
| grep -E 'Lost connection|Registered tunnel|Failed to dial|no recent network' \
|
|
||||||
| tail -14 | sed 's/^/ /'
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''查「默认中转服务器」在源码和已构建成品里各是什么值。
|
|
||||||
|
|
||||||
用户报告:软件默认中转服务器应该是**空白**、让用户自己填。
|
|
||||||
但给我的版本是填好的,上传的版本是清空的 —— 两者不一致。
|
|
||||||
|
|
||||||
先把所有源头找出来,看清楚再改。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
|
|
||||||
SOURCES = [
|
|
||||||
("windows/lanremote/server.py", r"DEFAULT_RELAY_URL\s*=\s*(.+)"),
|
|
||||||
("windows/lanremote/relay.py", r"192\.168\.\d+\.\d+:8080"),
|
|
||||||
("windows/run_agent.py", r"or\s+\"(192\.168[^\"]*|[^\"]*xxmxxs[^\"]*)\""),
|
|
||||||
("android/src/com/lanremote/watch/MainActivity.java",
|
|
||||||
r"DEFAULT_RELAY_URL\s*=\s*(.+);"),
|
|
||||||
]
|
|
||||||
|
|
||||||
print("=" * 70)
|
|
||||||
print(" 一、源码里的默认中转地址")
|
|
||||||
print("=" * 70)
|
|
||||||
for rel, pattern in SOURCES:
|
|
||||||
path = ROOT / rel
|
|
||||||
if not path.is_file():
|
|
||||||
print(" [不存在] %s" % rel)
|
|
||||||
continue
|
|
||||||
text = path.read_text(encoding="utf-8", errors="replace")
|
|
||||||
hits = re.findall(pattern, text, re.M)
|
|
||||||
print(" %s" % rel)
|
|
||||||
if hits:
|
|
||||||
for h in hits[:4]:
|
|
||||||
h = h if isinstance(h, str) else h[0]
|
|
||||||
print(" %s" % h.strip()[:100])
|
|
||||||
else:
|
|
||||||
print(" (没匹配到)")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=" * 70)
|
|
||||||
print(" 二、已构建成品里到底是什么")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
# APK
|
|
||||||
apk = ROOT / "dist" / "ykxmz-watch-1.8.2.apk"
|
|
||||||
if apk.is_file():
|
|
||||||
print(" %s" % apk.name)
|
|
||||||
try:
|
|
||||||
with zipfile.ZipFile(apk) as z:
|
|
||||||
dex = [n for n in z.namelist() if n.endswith(".dex")]
|
|
||||||
found = []
|
|
||||||
for name in dex:
|
|
||||||
blob = z.read(name)
|
|
||||||
for needle in (b"https://relay.xxmxxs.top",
|
|
||||||
b"192.168.2.156",
|
|
||||||
b"192.168.10.105"):
|
|
||||||
if needle in blob:
|
|
||||||
found.append((name, needle.decode()))
|
|
||||||
if found:
|
|
||||||
for name, needle in sorted(set(found)):
|
|
||||||
print(" %s -> 含 %s" % (name, needle))
|
|
||||||
else:
|
|
||||||
print(" dex 里没有硬编码的中转地址(默认是空白)")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
print(" 读 APK 失败: %s" % exc)
|
|
||||||
else:
|
|
||||||
print(" dist/ykxmz-watch-1.8.2.apk 不存在")
|
|
||||||
|
|
||||||
# exe / onedir
|
|
||||||
exe_dir = ROOT / "dist_exe" / "ykxmz"
|
|
||||||
print()
|
|
||||||
print(" %s" % exe_dir)
|
|
||||||
if exe_dir.is_dir():
|
|
||||||
hits = []
|
|
||||||
for f in exe_dir.rglob("*"):
|
|
||||||
if not f.is_file() or f.suffix.lower() not in (".py", ".pyc"):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
blob = f.read_bytes()
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
for needle in (b"192.168.2.156", b"192.168.10.105",
|
|
||||||
b"https://relay.xxmxxs.top"):
|
|
||||||
if needle in blob:
|
|
||||||
hits.append((f.relative_to(exe_dir).as_posix(),
|
|
||||||
needle.decode()))
|
|
||||||
if hits:
|
|
||||||
for name, needle in sorted(set(hits))[:10]:
|
|
||||||
print(" %-50s 含 %s" % (name, needle))
|
|
||||||
else:
|
|
||||||
print(" 没有硬编码的中转地址")
|
|
||||||
else:
|
|
||||||
print(" 不存在")
|
|
||||||
|
|
||||||
# 服务端 zip
|
|
||||||
print()
|
|
||||||
srv = ROOT / "dist" / "ykxmz-server-1.8.2.zip"
|
|
||||||
if srv.is_file():
|
|
||||||
print(" %s" % srv.name)
|
|
||||||
try:
|
|
||||||
with zipfile.ZipFile(srv) as z:
|
|
||||||
for name in z.namelist():
|
|
||||||
if not name.endswith((".py", ".txt", ".sh")):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
text = z.read(name).decode("utf-8", "replace")
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
continue
|
|
||||||
for line in text.splitlines():
|
|
||||||
if ("192.168.2.156" in line or "192.168.10.105" in line
|
|
||||||
or "relay.xxmxxs.top" in line):
|
|
||||||
print(" %s: %s" % (name, line.strip()[:90]))
|
|
||||||
break
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
print(" 读 zip 失败: %s" % exc)
|
|
||||||
else:
|
|
||||||
print(" dist/ykxmz-server-1.8.2.zip 不存在")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=" * 70)
|
|
||||||
print(" 三、结论")
|
|
||||||
print("=" * 70)
|
|
||||||
print(" 期望:默认中转服务器为**空白**,由用户自己填。")
|
|
||||||
print(" 实际:见上面。凡是出现硬编码地址的地方都要改成空字符串。")
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''看一眼中转服务器的完整状态。
|
|
||||||
|
|
||||||
为什么写成文件:这一轮我用 `python -c @"..."@` 传长命令失败了好几次 ——
|
|
||||||
PowerShell 的双引号 heredoc 会做变量插值、还会把 `$(...)` 当子表达式
|
|
||||||
在本机执行(systemctl / iptables 跑到 Windows 上了)。凡是超过两行
|
|
||||||
的远端命令,一律写成文件跑。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
python _build/check_device.py
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
sys.path.insert(0, str(ROOT / "_build"))
|
|
||||||
from devssh import run # noqa: E402
|
|
||||||
|
|
||||||
REMOTE = r'''
|
|
||||||
RELAY=/root/lanremote-relay
|
|
||||||
echo "════ 服务 ════"
|
|
||||||
for s in lanremote-relay lanremote-tunnel lanremote-tunnel-h2 lanremote-nettune; do
|
|
||||||
printf ' %-24s %s\n' "$s" "$(systemctl is-active $s 2>&1)"
|
|
||||||
done
|
|
||||||
printf ' %-24s %s\n' "monitor.timer" "$(systemctl is-active lanremote-monitor.timer 2>&1)"
|
|
||||||
printf ' %-24s %s\n' "watchdog.timer" "$(systemctl is-active lanremote-watchdog.timer 2>&1)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 网络 ════"
|
|
||||||
printf ' %-24s %s\n' "wlan0 MTU" "$(cat /sys/class/net/wlan0/mtu 2>/dev/null)"
|
|
||||||
printf ' %-24s %s\n' "MSS 规则" "$(iptables -t mangle -L -n 2>/dev/null | grep -ci mss) 条"
|
|
||||||
printf ' %-24s %s\n' "tcp_mtu_probing" "$(cat /proc/sys/net/ipv4/tcp_mtu_probing 2>/dev/null)"
|
|
||||||
printf ' %-24s %s\n' "默认路由" "$(ip route show default 2>/dev/null | head -1)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 内存 / 运行时长 ════"
|
|
||||||
printf ' %-24s %s\n' "运行时长" "$(uptime -p 2>/dev/null)"
|
|
||||||
free -m 2>/dev/null | sed -n '2p' | awk '{printf " %-24s %s/%s MB\n", "内存 used/total", $3, $2}'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 隧道连接数 ════"
|
|
||||||
for p in 20241 20242; do
|
|
||||||
n=$(curl -s --max-time 5 "http://127.0.0.1:$p/metrics" 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1)
|
|
||||||
printf ' %-24s %s\n' "指标端口 $p" "${n:-拿不到}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 公网自测 ════"
|
|
||||||
printf ' %-24s %s\n' "公网 /v1/health" "$(curl -s -o /dev/null -w '%{http_code} (%{time_total}s)' --max-time 15 https://relay.xxmxxs.top/v1/health 2>/dev/null)"
|
|
||||||
printf ' %-24s %s\n' "本机 /v1/health" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 6 http://127.0.0.1:8080/v1/health 2>/dev/null)"
|
|
||||||
printf ' %-24s %s\n' "公网首页" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://relay.xxmxxs.top/ 2>/dev/null)"
|
|
||||||
printf ' %-24s %s\n' "后台抢救报告路由" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 6 http://127.0.0.1:8080/rescue/none.txt 2>/dev/null)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 监测单元状态 ════"
|
|
||||||
cat $RELAY/monitor_state.txt 2>/dev/null | sed 's/^/ /' || echo " (还没有状态文件)"
|
|
||||||
echo " ── monitor.log 尾部 ──"
|
|
||||||
tail -5 $RELAY/monitor.log 2>/dev/null | sed 's/^/ /' || echo " (还没有日志)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 抢救报告 ════"
|
|
||||||
n=$(ls $RELAY/rescue_reports 2>/dev/null | wc -l)
|
|
||||||
echo " 共 $n 份"
|
|
||||||
ls -la $RELAY/rescue_reports/ 2>/dev/null | tail -n +4 | awk '{printf " %s %s\n", $6" "$7" "$8, $9}'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 知识库 ════"
|
|
||||||
if [ -f $RELAY/rescue_knowledge.md ]; then
|
|
||||||
echo " 行数:$(wc -l < $RELAY/rescue_knowledge.md)"
|
|
||||||
tail -6 $RELAY/rescue_knowledge.md | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo " (没有)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 定时器下次触发 ════"
|
|
||||||
systemctl list-timers lanremote-monitor.timer lanremote-watchdog.timer --no-pager 2>/dev/null | head -4 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════ 后台页有没有「抢救报告」分区 ════"
|
|
||||||
printf ' 匹配次数: %s\n' "$(curl -s --max-time 8 http://127.0.0.1:8080/zhongzhuanzhan 2>/dev/null | grep -c '抢救报告')"
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
out = run(REMOTE, timeout=300)
|
|
||||||
text = out[1] if isinstance(out, tuple) else str(out)
|
|
||||||
print(text)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''查 Gitea 自己报的 clone 地址,看 SSH 到底该连哪里。
|
|
||||||
|
|
||||||
现象:SSH 连得上(服务端回了 publickey,password),
|
|
||||||
但本机送出的公钥一直被拒。可能是:
|
|
||||||
* SSH 用的不是 22 端口
|
|
||||||
* 主机名不是 git.261826.xyz
|
|
||||||
* 密钥加到了别的账号
|
|
||||||
这个脚本把 Gitea 官方给出的地址拉出来对一下。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import urllib.request
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TOKEN_FILE = Path.home() / ".ykxmz" / "ezhub_token"
|
|
||||||
TOKEN = (TOKEN_FILE.read_text(encoding="utf-8").strip()
|
|
||||||
if TOKEN_FILE.is_file() else "")
|
|
||||||
|
|
||||||
BASE = "https://git.261826.xyz/api/v1"
|
|
||||||
HEAD = {"Authorization": "token " + TOKEN, "User-Agent": "ykxmz"}
|
|
||||||
|
|
||||||
|
|
||||||
def get(path: str):
|
|
||||||
with urllib.request.urlopen(
|
|
||||||
urllib.request.Request(BASE + path, headers=HEAD),
|
|
||||||
timeout=30) as resp:
|
|
||||||
return json.loads(resp.read().decode("utf-8", "replace"))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
print(" 一、仓库自己声明的 clone 地址")
|
|
||||||
repo = get("/repos/xmz/ykxmz")
|
|
||||||
for key in ("clone_url", "ssh_url", "html_url", "default_branch",
|
|
||||||
"empty", "size"):
|
|
||||||
if key in repo:
|
|
||||||
print(" %-16s %s" % (key, repo[key]))
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 二、当前用户注册了哪些 SSH 公钥")
|
|
||||||
try:
|
|
||||||
keys = get("/user/keys")
|
|
||||||
if not keys:
|
|
||||||
print(" **一把都没有** —— 密钥没加成功")
|
|
||||||
for k in keys:
|
|
||||||
print(" id=%-4s %-14s %s" % (k.get("id"), k.get("title"),
|
|
||||||
(k.get("key") or "")[:52] + "…"))
|
|
||||||
print(" fingerprint=%s" % (k.get("fingerprint") or "?"))
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
print(" 查询失败: %s" % exc)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 三、对照")
|
|
||||||
print(" 本机公钥指纹: SHA256:Acmu49YQcjw3m5WefOXSBzYNY2WPHLL2vvhnlYRAttA")
|
|
||||||
print(" 本机公钥内容: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFMh2Zqwm+8"
|
|
||||||
"KFhU3DhzhOO5Sx7cIzybYqUsrbt1c3ly")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 四、服务端版本与配置")
|
|
||||||
try:
|
|
||||||
ver = get("/version")
|
|
||||||
print(" Gitea: %s" % ver.get("version"))
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
print(" 取版本失败: %s" % exc)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 验证响应头 —— 上次改完只看了状态码和字节数,没看 Content-Type,
|
|
||||||
# 结果页面在浏览器里是"一屏源码"。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=http://127.0.0.1:8080
|
|
||||||
PASS=$(cat /root/lanremote-relay/data/admin_password.txt 2>/dev/null)
|
|
||||||
|
|
||||||
curl -s -c /tmp/ck.txt -o /dev/null --max-time 15 \
|
|
||||||
-d "username=xmz7891&password=$PASS" "$BASE/admin/login"
|
|
||||||
|
|
||||||
echo " 管理页响应头:"
|
|
||||||
curl -sI -b /tmp/ck.txt --max-time 15 "$BASE/zhongzhuanzhan" \
|
|
||||||
| head -4 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 官网响应头:"
|
|
||||||
curl -sI --max-time 15 "$BASE/" | head -3 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 产品截图响应头:"
|
|
||||||
curl -sI --max-time 15 "$BASE/shot/pc-main.png" | head -3 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 登录页响应头:"
|
|
||||||
curl -sI --max-time 15 "$BASE/zhongzhuanzhan" | head -3 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 判断:"
|
|
||||||
admin_ct=$(curl -sI -b /tmp/ck.txt --max-time 15 "$BASE/zhongzhuanzhan" \
|
|
||||||
| grep -i '^content-type' | tr -d '\r')
|
|
||||||
if echo "$admin_ct" | grep -qi 'text/html'; then
|
|
||||||
echo " ✓ 管理页是 text/html"
|
|
||||||
else
|
|
||||||
echo " ✗ 管理页 Content-Type 不对:$admin_ct"
|
|
||||||
fi
|
|
||||||
|
|
||||||
site_ct=$(curl -sI --max-time 15 "$BASE/" | grep -i '^content-type' | tr -d '\r')
|
|
||||||
if echo "$site_ct" | grep -qi 'text/html'; then
|
|
||||||
echo " ✓ 官网是 text/html"
|
|
||||||
else
|
|
||||||
echo " ✗ 官网 Content-Type 不对:$site_ct"
|
|
||||||
fi
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 502/530 但隧道连着 —— 查回源配置(这是我唯一还没验证过的一环)
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=http://127.0.0.1:8080
|
|
||||||
|
|
||||||
echo "════════ 1. 隧道 ingress 配置全文 ════════"
|
|
||||||
cat /root/.cloudflared/config.yml 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. relay 到底监听哪个地址 ════════"
|
|
||||||
ss -lntp 2>/dev/null | grep 8080 | sed 's/^/ /'
|
|
||||||
echo " ---"
|
|
||||||
printf " 127.0.0.1:8080 -> HTTP %s\n" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:8080/v1/health)"
|
|
||||||
printf " localhost:8080 -> HTTP %s\n" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://localhost:8080/v1/health)"
|
|
||||||
printf " 192.168.10.105:8080 -> HTTP %s\n" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://192.168.10.105:8080/v1/health)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. cloudflared 能看到 relay 吗(同一台机器)════════"
|
|
||||||
echo " cloudflared 进程:"
|
|
||||||
pgrep -af 'cloudflared tunnel' | head -2 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 隧道 metrics 全量 ════════"
|
|
||||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep -E '^cloudflared_tunnel' | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 最近的注册/掉线(看 4 条是不是刚连上的)════════"
|
|
||||||
tail -300 /root/lanremote-relay/tunnel.log 2>/dev/null \
|
|
||||||
| grep -E 'Registered|Lost|Retrying|i/o timeout|error' \
|
|
||||||
| tail -20 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 6. 公网连打 15 次(看是持续挂还是间歇)════════"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 15); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
|
||||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health)
|
|
||||||
[ "$c" = "200" ] && ok=$((ok+1))
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/15"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 7. 每次请求是不是落到不同机房 ════════"
|
|
||||||
for i in 1 2 3 4 5 6; do
|
|
||||||
ray=$(curl -sI --max-time 12 -H 'User-Agent: LanRemote/1.4.0' \
|
|
||||||
https://relay.xxmxxs.top/v1/health 2>/dev/null \
|
|
||||||
| grep -i '^cf-ray' | tr -d '\r' | awk '{print $2}')
|
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \
|
|
||||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health)
|
|
||||||
printf " HTTP %s cf-ray %s\n" "$code" "$ray"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 8. DNS 解析到哪 ════════"
|
|
||||||
getent ahostsv4 relay.xxmxxs.top 2>/dev/null | awk '{print $1}' | sort -u | sed 's/^/ v4: /'
|
|
||||||
getent ahostsv6 relay.xxmxxs.top 2>/dev/null | awk '{print $1}' | sort -u | head -3 | sed 's/^/ v6: /'
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''检查 Inno Setup 的 .iss 里有没有会害编译失败的写法。
|
|
||||||
|
|
||||||
踩过的三个坑,都是 Inno 的 [Code] 解析器**连注释内容也处理**:
|
|
||||||
|
|
||||||
1. 注释里写方括号区段名
|
|
||||||
{ 规范化,后面 [Run] 阶段用得到 }
|
|
||||||
报 "无效的区段标记",编译直接失败。踩了两次。
|
|
||||||
|
|
||||||
2. 注释里写花括号常量
|
|
||||||
{ 不带 --keep-dir 的话会 rmdir 删 {app} }
|
|
||||||
Inno 会把 {app} 展开,展开后花括号配不上 ->
|
|
||||||
报 "Syntax error",编译失败。踩了一次。
|
|
||||||
|
|
||||||
3. 注释里写 {param:XXX|}
|
|
||||||
Inno 会展开它,参数没传就展开成空,同样破坏括号配对。
|
|
||||||
|
|
||||||
所以规则:**[Code] 段落里,注释内容不能出现 [Section] 或 {常量}。**
|
|
||||||
|
|
||||||
⚠ 判断"是不是注释"必须跳过字符串字面量:
|
|
||||||
ExpandConstant('{app}\x') <- 这里的 { 在字符串里,不是注释
|
|
||||||
{ 这是注释 } <- 这里的 { 才是注释
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ISS = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
/ "installer" / "ykxmz.iss")
|
|
||||||
|
|
||||||
SECTIONS = {
|
|
||||||
"Setup", "Types", "Components", "Tasks", "Dirs", "Files", "Icons",
|
|
||||||
"Ini", "InstallDelete", "Languages", "Messages", "CustomMessages",
|
|
||||||
"Registry", "Run", "UninstallRun", "UninstallDelete", "Code",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def scan_comment_text(line: str, in_comment: bool):
|
|
||||||
"""返回 (这一行注释里的文字, 扫完后的 in_comment)。
|
|
||||||
|
|
||||||
关键:字符串字面量里的 { } 不算注释。
|
|
||||||
"""
|
|
||||||
out = []
|
|
||||||
cursor = 0
|
|
||||||
in_string = False
|
|
||||||
length = len(line)
|
|
||||||
|
|
||||||
while cursor < length:
|
|
||||||
ch = line[cursor]
|
|
||||||
|
|
||||||
if in_string:
|
|
||||||
if ch == "'":
|
|
||||||
# Pascal 用 '' 表示一个单引号
|
|
||||||
if cursor + 1 < length and line[cursor + 1] == "'":
|
|
||||||
cursor += 2
|
|
||||||
continue
|
|
||||||
in_string = False
|
|
||||||
cursor += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not in_comment:
|
|
||||||
if ch == "'":
|
|
||||||
in_string = True
|
|
||||||
elif ch == "{":
|
|
||||||
in_comment = True
|
|
||||||
cursor += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 在注释里
|
|
||||||
if ch == "}":
|
|
||||||
in_comment = False
|
|
||||||
cursor += 1
|
|
||||||
continue
|
|
||||||
out.append(ch)
|
|
||||||
cursor += 1
|
|
||||||
|
|
||||||
return "".join(out), in_comment
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if not ISS.is_file():
|
|
||||||
print(" 找不到 %s" % ISS)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
lines = ISS.read_text(encoding="utf-8").splitlines()
|
|
||||||
|
|
||||||
code_start = None
|
|
||||||
for index, line in enumerate(lines):
|
|
||||||
if line.strip() == "[Code]":
|
|
||||||
code_start = index
|
|
||||||
break
|
|
||||||
|
|
||||||
if code_start is None:
|
|
||||||
print(" 没有 [Code] 段落,跳过")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
problems = []
|
|
||||||
in_comment = False
|
|
||||||
|
|
||||||
for index in range(code_start, len(lines)):
|
|
||||||
line = lines[index]
|
|
||||||
comment_text, in_comment = scan_comment_text(line, in_comment)
|
|
||||||
if not comment_text:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 只有**行首**的 [Section] 才会被 Inno 当成区段标记。
|
|
||||||
# 行中间的("交给 Run 段用...")不影响 —— 实测编译能过。
|
|
||||||
m = re.match(r"\s*\[([A-Za-z][A-Za-z0-9]*)\]", comment_text)
|
|
||||||
if m and m.group(1) in SECTIONS:
|
|
||||||
problems.append((index + 1,
|
|
||||||
"注释**行首**有区段名 [%s]" % m.group(1),
|
|
||||||
line.strip()))
|
|
||||||
|
|
||||||
# 花括号常量:注释里出现就会被展开
|
|
||||||
for match in re.finditer(
|
|
||||||
r"\{[A-Za-z][A-Za-z0-9]*(?:[:|][^}]*)?\}", comment_text):
|
|
||||||
problems.append((index + 1,
|
|
||||||
"注释里有会展开的常量 %s" % match.group(0),
|
|
||||||
line.strip()))
|
|
||||||
|
|
||||||
# 真区段必须顶格
|
|
||||||
for index, line in enumerate(lines):
|
|
||||||
if re.match(r"^\s+\[[A-Za-z]+\]\s*$", line):
|
|
||||||
problems.append((index + 1, "区段名前面有空格", line.strip()))
|
|
||||||
|
|
||||||
if problems:
|
|
||||||
print(" 发现 %d 个会让编译失败的问题:" % len(problems))
|
|
||||||
for lineno, why, text in problems:
|
|
||||||
print(" 行 %d: %s" % (lineno, why))
|
|
||||||
print(" %s" % text[:74])
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(" [Code] 段落里没有违规写法 ✓")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''确认那 3 个失败是不是"电脑锁屏"导致的(不是代码问题)。
|
|
||||||
|
|
||||||
现象:
|
|
||||||
test_agent_protocol.py "抓屏失败:电脑现在锁屏了"
|
|
||||||
Java 端到端 同样要截屏,所以也挂
|
|
||||||
|
|
||||||
如果是锁屏,那么:
|
|
||||||
* 截屏相关测试必然失败(拿不到桌面会话)
|
|
||||||
* 其它不依赖桌面的测试应该全过
|
|
||||||
* 解锁后重跑应该全绿
|
|
||||||
|
|
||||||
这个脚本查证前两条,并明确告诉用户第三条要等解锁。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def locked() -> bool:
|
|
||||||
"""查当前会话是不是锁屏状态。"""
|
|
||||||
user32 = ctypes.windll.User32
|
|
||||||
# OpenInputDesktop 失败 = 当前不在交互桌面(锁屏/切换用户)
|
|
||||||
handle = user32.OpenInputDesktop(0, False, 0x0001)
|
|
||||||
if handle:
|
|
||||||
user32.CloseDesktop(handle)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def session_info() -> str:
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["powershell", "-NoProfile", "-Command",
|
|
||||||
"(Get-Process -Id $PID).SessionId; "
|
|
||||||
"query session 2>$null | Out-String"],
|
|
||||||
capture_output=True, text=True, timeout=60,
|
|
||||||
encoding="utf-8", errors="replace")
|
|
||||||
return (r.stdout or "").strip()
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
return "查询失败: %s" % exc
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
print(" 一、当前会话状态")
|
|
||||||
is_locked = locked()
|
|
||||||
print(" OpenInputDesktop -> %s" % ("失败(锁屏/无交互桌面)"
|
|
||||||
if is_locked else "成功(有桌面)"))
|
|
||||||
print(" 结论: %s" % ("**电脑锁屏了**" if is_locked else "桌面可用"))
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 二、会话信息")
|
|
||||||
for line in session_info().splitlines()[:6]:
|
|
||||||
print(" " + line.strip())
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 三、这解释了哪些失败")
|
|
||||||
if is_locked:
|
|
||||||
print(" * test_agent_protocol 的 5 项失败:全是截屏相关")
|
|
||||||
print(" (截屏成功 / JPEG 合法 / 尺寸有效 / 遵守 maxWidth / 镜像帧)")
|
|
||||||
print(" * Java <-> 桩服务端、Java <-> 打包 EXE:也要截屏,一起挂")
|
|
||||||
print(" * 其余 30 个阶段全过 —— 说明不是代码问题")
|
|
||||||
print()
|
|
||||||
print(" **解锁屏幕后重跑,这三项应该全绿。**")
|
|
||||||
print(" 软件本身的行为是对的:它没崩,而是明确告诉你")
|
|
||||||
print(" 「电脑现在锁屏了,先解锁再试」。")
|
|
||||||
else:
|
|
||||||
print(" 桌面可用 —— 那这些失败就是真的代码问题,需要继续查。")
|
|
||||||
return 0 if is_locked else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看可靠性测试进度和隧道事件
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "── 测试进程 ──"
|
|
||||||
pgrep -af 'tpr.sh|reliability' | head -3
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "── 隧道最近 30 分钟事件 ──"
|
|
||||||
reconn=$(journalctl -u lanremote-tunnel --since '30 minutes ago' --no-pager 2>/dev/null | grep -c 'Registered tunnel connection')
|
|
||||||
lost=$(journalctl -u lanremote-tunnel --since '30 minutes ago' --no-pager 2>/dev/null | grep -c 'Lost connection')
|
|
||||||
iot=$(journalctl -u lanremote-tunnel --since '30 minutes ago' --no-pager 2>/dev/null | grep -c 'i/o timeout')
|
|
||||||
echo " 重连 $reconn 次 · 掉线 $lost 次 · i/o timeout $iot 次"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "── 最近的掉线/重连明细 ──"
|
|
||||||
journalctl -u lanremote-tunnel --since '30 minutes ago' --no-pager 2>/dev/null \
|
|
||||||
| grep -E 'Registered tunnel connection|Lost connection' | tail -8 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "── watchdog 最近 12 行 ──"
|
|
||||||
tail -12 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "── 隧道连接数和落点 ──"
|
|
||||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
|
||||||
| grep -E '^cloudflared_tunnel_ha_connections|server_locations.*} 1' | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "── 公网现在通不通(10 次快速探测)──"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
|
||||||
-H 'User-Agent: LanRemote/1.8.1' https://relay.xxmxxs.top/v1/health)
|
|
||||||
[ "$c" = "200" ] && ok=$((ok+1))
|
|
||||||
printf " %d: %s\n" "$i" "$c"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/10"
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''检查 relay 服务状态并重跑网页检查。'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
sys.path.insert(0, str(ROOT / "_build"))
|
|
||||||
|
|
||||||
from devssh import run # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
print(" 服务状态:")
|
|
||||||
_c, out, _e = run(
|
|
||||||
"for s in lanremote-relay lanremote-tunnel lanremote-nettune.service; do "
|
|
||||||
"printf ' %-26s %s\\n' $s $(systemctl is-active $s); done", timeout=60)
|
|
||||||
print(out)
|
|
||||||
|
|
||||||
print(" 监听端口:")
|
|
||||||
_c, out, _e = run("ss -tlnp 2>/dev/null | grep -E '8080|:80 ' | head -5",
|
|
||||||
timeout=60)
|
|
||||||
print(out or " (没在监听 8080)")
|
|
||||||
|
|
||||||
print(" 最后几行日志:")
|
|
||||||
_c, out, _e = run("tail -12 /root/lanremote-relay/relay.log 2>/dev/null",
|
|
||||||
timeout=60)
|
|
||||||
print(out)
|
|
||||||
|
|
||||||
print(" 等一下再跑检查…")
|
|
||||||
time.sleep(6)
|
|
||||||
_c, out, _e = run("bash /root/check_web.sh 2>&1", timeout=240)
|
|
||||||
print(out)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看中转到底吃多少资源 —— 好判断免费容器够不够
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "════════ 进程占用 ════════"
|
|
||||||
for pat in relay.py cloudflared; do
|
|
||||||
pid=$(pgrep -f "$pat" | head -1)
|
|
||||||
if [ -n "${pid:-}" ]; then
|
|
||||||
rss=$(ps -o rss= -p "$pid" | tr -d ' ')
|
|
||||||
cpu=$(ps -o %cpu= -p "$pid" | tr -d ' ')
|
|
||||||
etime=$(ps -o etime= -p "$pid" | tr -d ' ')
|
|
||||||
printf " %-14s PID %-6s 内存 %5.1f MB CPU %s%% 已跑 %s\n" \
|
|
||||||
"$pat" "$pid" "$(echo "$rss/1024" | bc -l)" "$cpu" "$etime"
|
|
||||||
else
|
|
||||||
printf " %-14s 没在跑\n" "$pat"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 系统整体 ════════"
|
|
||||||
echo " CPU 核心: $(nproc)"
|
|
||||||
echo " 内存总量: $(free -m | awk '/Mem:/{print $2}') MB"
|
|
||||||
echo " 已用: $(free -m | awk '/Mem:/{print $3}') MB"
|
|
||||||
echo " 可用: $(free -m | awk '/Mem:/{print $7}') MB"
|
|
||||||
echo " 磁盘: $(df -h /root | awk 'NR==2{print $3" / "$2" ("$5" 已用)"}')"
|
|
||||||
echo " 负载: $(cat /proc/loadavg | cut -d' ' -f1-3)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 中转的数据目录(要跟着搬的东西)════════"
|
|
||||||
du -sh /root/lanremote-relay 2>/dev/null | sed 's/^/ /'
|
|
||||||
echo " ---"
|
|
||||||
du -sh /root/lanremote-relay/* 2>/dev/null | sort -h | tail -8 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 每天请求量(估算托管配额需求)════════"
|
|
||||||
echo " agent poll 间隔: $(grep -oE 'POLL_SECONDS = float\(os.environ.get\("RELAY_POLL", "[0-9]+"' /root/lanremote-relay/relay.py | grep -oE '"[0-9]+"' | tr -d '"') 秒"
|
|
||||||
echo " => 单台被控端: $((86400 / 15)) 次/天(按 15 秒算)"
|
|
||||||
echo " => 若 3 台: $((86400 / 15 * 3)) 次/天"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 现在的中转总量(并发/会话)════════"
|
|
||||||
curl -s --max-time 8 http://127.0.0.1:8080/v1/health 2>/dev/null \
|
|
||||||
| python3 -c "
|
|
||||||
import sys, json
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
print(' 版本:', d.get('version'))
|
|
||||||
print(' 设备:', len(d.get('devices', [])), '台')
|
|
||||||
print(' 会话:', d.get('sessions', 0))
|
|
||||||
print(' 内存:', round(d.get('memoryKB', 0) / 1024), 'MB')
|
|
||||||
print(' 累计请求:', d.get('totalRequests'))
|
|
||||||
print(' 运行:', round(d.get('uptimeSeconds', 0) / 3600, 1), '小时')
|
|
||||||
" 2>/dev/null | sed 's/^/ /'
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 公网间歇 530 —— 查隧道当前真实状态
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo " relay=$(systemctl is-active lanremote-relay) tunnel=$(systemctl is-active lanremote-tunnel)"
|
|
||||||
echo " 隧道连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ watchdog 最近 15 行 ════════"
|
|
||||||
tail -15 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ watchdog 怎么被调起来的 ════════"
|
|
||||||
echo " 服务: $(systemctl is-active lanremote-watchdog 2>&1)"
|
|
||||||
crontab -l 2>/dev/null | grep -i watch | sed 's/^/ cron: /'
|
|
||||||
systemctl list-timers --all 2>/dev/null | grep -i watch | sed 's/^/ timer: /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 隧道最近的重连频率(1500 行内)════════"
|
|
||||||
for kw in 'Registered tunnel connection' 'Lost connection' 'Retrying connection' 'i/o timeout' 'context deadline'; do
|
|
||||||
n=$(tail -1500 /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c "$kw")
|
|
||||||
printf " %-34s %s 次\n" "$kw" "$n"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 出口 IP 可达性 ════════"
|
|
||||||
python3 - <<'PY'
|
|
||||||
import socket, time
|
|
||||||
ips = ["198.41.192.107", "198.41.200.43", "198.41.192.77",
|
|
||||||
"198.41.200.113", "198.41.192.227", "198.41.200.193",
|
|
||||||
"198.41.192.37", "198.41.200.63"]
|
|
||||||
ok = 0
|
|
||||||
for ip in ips:
|
|
||||||
s = socket.socket(); s.settimeout(3)
|
|
||||||
t = time.perf_counter()
|
|
||||||
try:
|
|
||||||
s.connect((ip, 7844))
|
|
||||||
print(" %-16s %4.0f ms" % (ip, (time.perf_counter()-t)*1000)); ok += 1
|
|
||||||
except OSError:
|
|
||||||
print(" %-16s 不通" % ip)
|
|
||||||
finally:
|
|
||||||
s.close()
|
|
||||||
print(" => %d/%d 可达" % (ok, len(ips)))
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 公网连打 12 次 ════════"
|
|
||||||
ok=0
|
|
||||||
for i in $(seq 1 12); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
|
||||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health)
|
|
||||||
[ "$c" = "200" ] && ok=$((ok+1))
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " 可用 $ok/12"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 本机 relay(排除源站问题)════════"
|
|
||||||
for i in 1 2 3; do
|
|
||||||
printf " %d: HTTP %s\n" "$i" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1:8080/v1/health)"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 网络质量 ════════"
|
|
||||||
ping -c 10 -W 2 1.1.1.1 2>/dev/null | tail -2 | sed 's/^/ /'
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ⚠ 管理密码不再写在这里,从环境变量取:
|
|
||||||
# export RELAY_ADMIN_PASSWORD=...
|
|
||||||
: "${RELAY_ADMIN_PASSWORD:?请先 export RELAY_ADMIN_PASSWORD}"
|
|
||||||
# 官网 / 管理页 / favicon 的最终检查
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=http://127.0.0.1:8080
|
|
||||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
|
||||||
|
|
||||||
echo " 1) 官网 /"
|
|
||||||
curl -s -o /tmp/site.html -w ' HTTP %{http_code} %{size_download} 字节\n' \
|
|
||||||
--max-time 15 "$BASE/" 2>/dev/null
|
|
||||||
for n in 'ykxmz' '老师抬一下手腕' '电脑端' '服务器端' '手表端'; do
|
|
||||||
grep -q "$n" /tmp/site.html && echo " OK $n" || echo " XX $n"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 2) 管理页 /zhongzhuanzhan(未登录应看到登录框)"
|
|
||||||
curl -s -o /tmp/login.html --max-time 15 "$BASE/zhongzhuanzhan" 2>/dev/null
|
|
||||||
for n in 'name=username' 'name=password' 'favicon'; do
|
|
||||||
grep -q "$n" /tmp/login.html && echo " OK $n" || echo " XX $n"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 3) /admin 跳转"
|
|
||||||
curl -s -o /dev/null -w ' -> %{redirect_url}\n' --max-time 15 "$BASE/admin" 2>/dev/null
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 4) 登录"
|
|
||||||
curl -s -c /tmp/ck2.txt -o /dev/null --max-time 15 -X POST \
|
|
||||||
-d "username=xmz7891&password=$RELAY_ADMIN_PASSWORD" "$BASE/admin/login" 2>/dev/null
|
|
||||||
curl -s -b /tmp/ck2.txt -o /tmp/admin.html --max-time 15 "$BASE/zhongzhuanzhan" 2>/dev/null
|
|
||||||
for n in 'id=dlPanel' '网络体检' '退出登录' 'favicon'; do
|
|
||||||
grep -q "$n" /tmp/admin.html && echo " OK $n" || echo " XX $n"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 5) favicon"
|
|
||||||
curl -s -o /dev/null -w ' HTTP %{http_code} %{size_download} 字节 %{content_type}\n' \
|
|
||||||
--max-time 15 "$BASE/favicon.ico" 2>/dev/null
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 6) 公网官网"
|
|
||||||
curl -s -o /dev/null -w ' HTTP %{http_code} %{size_download} 字节\n' \
|
|
||||||
--max-time 40 -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/ 2>/dev/null
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''查 WiX Toolset 的独立二进制下载地址(不依赖 .NET SDK)。'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
API = "https://api.github.com/repos/wixtoolset/wix/releases"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
req = urllib.request.Request(API, headers={
|
|
||||||
"User-Agent": "ykxmz-check",
|
|
||||||
"Accept": "application/vnd.github+json",
|
|
||||||
})
|
|
||||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
||||||
releases = json.load(resp)
|
|
||||||
|
|
||||||
for rel in releases[:4]:
|
|
||||||
print(f" {rel.get('tag_name')} ({rel.get('published_at', '')[:10]})"
|
|
||||||
f"{' [pre]' if rel.get('prerelease') else ''}")
|
|
||||||
for asset in rel.get("assets", []):
|
|
||||||
name = asset.get("name", "")
|
|
||||||
size = asset.get("size", 0) / 1024 / 1024
|
|
||||||
print(f" {name:44} {size:6.2f} MB")
|
|
||||||
print(f" {asset.get('browser_download_url')}")
|
|
||||||
print()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看配比对照有没有开始
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo " ── 隧道当前配比 ──"
|
|
||||||
for s in lanremote-tunnel lanremote-tunnel-h2; do
|
|
||||||
printf " %-24s %s (%s)\n" "$s" \
|
|
||||||
"$(grep -oE -- '--ha-connections [0-9]+' "/etc/systemd/system/$s.service" 2>/dev/null | tr '\n' ' ')" \
|
|
||||||
"$(systemctl is-active "$s" 2>/dev/null)"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 对照脚本/进程 ──"
|
|
||||||
pgrep -af 'ab_long|rp_ab|collapse|ratio' 2>/dev/null | head -5 | sed 's/^/ /' || echo " (没在跑)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 最近 20 分钟内设备上改过的文件 ──"
|
|
||||||
find /root -maxdepth 1 -type f -mmin -20 2>/dev/null | head -10 | sed 's/^/ /' || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 有没有对照的产物 ──"
|
|
||||||
ls -la --time-style=+%H:%M /root/*ab*.tsv /root/*ab*.out /root/*ratio*.tsv /root/*collapse*.tsv 2>/dev/null | sed 's/^/ /' || echo " (没有)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 公网 ──"
|
|
||||||
curl -s --max-time 20 -o /dev/null -w ' HTTP %{http_code} %{time_total}s\n' \
|
|
||||||
-H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 时间 ──"
|
|
||||||
date '+ %H:%M:%S'
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# A/B 对照现状(我换密码重启中转打断了 A 组)
|
|
||||||
set -u
|
|
||||||
echo " ══ $(date '+%H:%M:%S') ══"
|
|
||||||
echo
|
|
||||||
echo " ── A 组 ──"
|
|
||||||
[ -f /root/ab_A.out ] && cat /root/ab_A.out | sed 's/^/ /'
|
|
||||||
n=0; [ -f /root/ab_A_health.tsv ] && n=$(wc -l < /root/ab_A_health.tsv)
|
|
||||||
echo " 样本: $n"
|
|
||||||
[ -f /root/ab_A_summary.txt ] && echo " ── 汇总 ──" && cat /root/ab_A_summary.txt | sed 's/^/ /'
|
|
||||||
[ -f /root/ab_A_health.tsv ] && echo " 最后 2 条:" && tail -2 /root/ab_A_health.tsv | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
echo " ── B 组 ──"
|
|
||||||
[ -f /root/ab_B.out ] && cat /root/ab_B.out | sed 's/^/ /' || echo " (没开始)"
|
|
||||||
n2=0; [ -f /root/ab_B_health.tsv ] && n2=$(wc -l < /root/ab_B_health.tsv)
|
|
||||||
echo " 样本: $n2"
|
|
||||||
echo
|
|
||||||
echo " ── 进程 ──"
|
|
||||||
pgrep -af 'rpab|rp_ab' 2>/dev/null | head -3 | sed 's/^/ /' || echo " (没有在跑)"
|
|
||||||
echo
|
|
||||||
echo " ── 记忆/CPU 采样(它加的那个)──"
|
|
||||||
[ -f /root/ab_A_res.tsv ] && head -3 /root/ab_A_res.tsv | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
echo " ── 当前配比 ──"
|
|
||||||
for s in lanremote-tunnel lanremote-tunnel-h2; do
|
|
||||||
printf " %-24s %s\n" "$s" \
|
|
||||||
"$(grep -oE -- '--ha-connections [0-9]+' "/etc/systemd/system/$s.service" 2>/dev/null | tr '\n' ' ')"
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
echo " ── 公网 ──"
|
|
||||||
curl -s --max-time 15 -o /dev/null -w ' HTTP %{http_code} %{time_total}s\n' \
|
|
||||||
-H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 对比设备上的 relay 文件和仓库里的版本
|
|
||||||
set -u
|
|
||||||
|
|
||||||
D=/root/lanremote-relay
|
|
||||||
echo " ── 设备上的文件 ──"
|
|
||||||
ls -la --time-style=+%m-%d_%H:%M "$D"/*.py 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── relay.py 版本号 ──"
|
|
||||||
grep -n '^VERSION' "$D/relay.py" 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── admin.py 里有没有新功能 ──"
|
|
||||||
for kw in allowNew data-recode set_allow_new set_device_code; do
|
|
||||||
n=$(grep -c "$kw" "$D/admin.py" 2>/dev/null || echo 0)
|
|
||||||
n2=$(grep -c "$kw" "$D/relay.py" 2>/dev/null || echo 0)
|
|
||||||
printf " %-16s admin.py:%s relay.py:%s\n" "$kw" "$n" "$n2"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── admin.py 大小和 md5 ──"
|
|
||||||
md5sum "$D/admin.py" 2>/dev/null | sed 's/^/ /'
|
|
||||||
wc -c "$D/admin.py" 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 服务状态 ──"
|
|
||||||
systemctl is-active lanremote-relay 2>/dev/null | sed 's/^/ relay: /'
|
|
||||||
echo " 启动于: $(systemctl show -p ActiveEnterTimestamp --value lanremote-relay 2>/dev/null)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 实际返回的管理页开头(看是不是管理页)──"
|
|
||||||
KEY=$(cat "$D/data/admin_key.txt" 2>/dev/null)
|
|
||||||
curl -s --max-time 15 "http://127.0.0.1:8080/zhongzhuanzhan?key=$KEY" 2>/dev/null | head -c 400
|
|
||||||
echo
|
|
||||||
echo " (上面这段的头 400 字节)"
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看 relay-stability 的 30 次长轮询结果(目标 2)
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo " ── 汇总文件 ──"
|
|
||||||
[ -f /root/rp_poll30_summary.txt ] && cat /root/rp_poll30_summary.txt | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
echo " ── 明细(前 5 行看格式)──"
|
|
||||||
[ -f /root/rp_poll30.tsv ] && head -5 /root/rp_poll30.tsv | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 自己再算一遍 ──"
|
|
||||||
python3 - <<'PYEOF'
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
p = Path("/root/rp_poll30.tsv")
|
|
||||||
if not p.is_file():
|
|
||||||
print(" 没有文件")
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
parts = [x.strip() for x in line.replace(",", "\t").split("\t") if x.strip()]
|
|
||||||
if not parts:
|
|
||||||
continue
|
|
||||||
# 找出状态码和耗时
|
|
||||||
code = None
|
|
||||||
ms = None
|
|
||||||
for x in parts:
|
|
||||||
if x in ("200", "502", "530", "000", "401", "504"):
|
|
||||||
code = x
|
|
||||||
elif x.replace(".", "").isdigit() and "." not in x and len(x) >= 4:
|
|
||||||
try:
|
|
||||||
v = int(x)
|
|
||||||
if 500 <= v <= 300000 and ms is None:
|
|
||||||
ms = v
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if code:
|
|
||||||
rows.append((code, ms))
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print(" 解析不出样本,原始内容:")
|
|
||||||
print(p.read_text(encoding="utf-8", errors="replace")[:400])
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
total = len(rows)
|
|
||||||
ok = sum(1 for c, _m in rows if c == "200")
|
|
||||||
print(" 样本 %d,成功 %d,可用率 %.1f%%" % (total, ok, ok * 100.0 / total))
|
|
||||||
|
|
||||||
good = sorted(m for c, m in rows if c == "200" and m)
|
|
||||||
if good:
|
|
||||||
def pc(q):
|
|
||||||
return good[min(int(len(good) * q), len(good) - 1)]
|
|
||||||
print(" 耗时: 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms"
|
|
||||||
% (good[0], good[len(good) // 2], pc(0.9), pc(0.99), good[-1]))
|
|
||||||
print(" (理想值 = 60000 + 隧道开销;>90000 的算异常长尾)")
|
|
||||||
outliers = [m for m in good if m > 90000]
|
|
||||||
print(" 异常长尾(>90 秒): %d 次 %s" % (len(outliers), outliers))
|
|
||||||
PYEOF
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看 relay-stability 在设备上跑到哪了
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo " ── 它起的进程 ──"
|
|
||||||
pgrep -af 'rp_|long2h|ab_long|collapse' 2>/dev/null | head -6 | sed 's/^/ /' || echo " (没有)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── /root 下它最近写的文件 ──"
|
|
||||||
ls -la --time-style=+%H:%M:%S /root/rp_* /root/*.tsv /root/*.txt /root/*.out 2>/dev/null \
|
|
||||||
| sort -k6 | tail -12 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 那个 2 小时长测的输出 ──"
|
|
||||||
if [ -f /root/long2h_health.tsv ]; then
|
|
||||||
echo " 样本数: $(wc -l < /root/long2h_health.tsv)"
|
|
||||||
echo " 最后 3 行:"
|
|
||||||
tail -3 /root/long2h_health.tsv | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 配比对照有没有开始(看隧道配置变没变)──"
|
|
||||||
for s in lanremote-tunnel lanremote-tunnel-h2; do
|
|
||||||
printf " %-24s %s\n" "$s" \
|
|
||||||
"$(grep -oE -- '--protocol [a-z0-9]+|--ha-connections [0-9]+' /etc/systemd/system/$s.service 2>/dev/null | tr '\n' ' ')"
|
|
||||||
done
|
|
||||||
for p in 20241 20242; do
|
|
||||||
n=$(curl -s --max-time 4 "http://127.0.0.1:$p/metrics" 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
printf " 端口 %s -> %s 条\n" "$p" "${n:-读不到}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 公网自检 ──"
|
|
||||||
curl -s --max-time 20 -o /dev/null -w ' HTTP %{http_code} %{time_total}s\n' \
|
|
||||||
-H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 设备当前时间 ──"
|
|
||||||
date '+ %H:%M:%S'
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 看设备上隧道现在的配比 + 连接数(判断 relay-stability 有没有在跑对照)
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo " ── 两个隧道单元的配置 ──"
|
|
||||||
for s in lanremote-tunnel lanremote-tunnel-h2; do
|
|
||||||
if [ -f "/etc/systemd/system/$s.service" ]; then
|
|
||||||
cfg=$(grep -oE -- '--protocol [a-z0-9]+|--ha-connections [0-9]+|--metrics [0-9.:]+' \
|
|
||||||
"/etc/systemd/system/$s.service" | tr '\n' ' ')
|
|
||||||
printf " %-24s %s (%s)\n" "$s" "$cfg" "$(systemctl is-active $s 2>/dev/null)"
|
|
||||||
else
|
|
||||||
printf " %-24s (不存在)\n" "$s"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 实时连接数 ──"
|
|
||||||
for p in 20241 20242; do
|
|
||||||
n=$(curl -s --max-time 4 "http://127.0.0.1:$p/metrics" 2>/dev/null \
|
|
||||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
|
||||||
printf " 端口 %s -> %s 条\n" "$p" "${n:-读不到}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 公网自检 ──"
|
|
||||||
curl -s --max-time 20 -o /dev/null -w ' HTTP %{http_code} %{time_total}s\n' \
|
|
||||||
-H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── relay-stability 的测量进程 ──"
|
|
||||||
pgrep -af 'rp_|longstab|bench' 2>/dev/null | head -5 | sed 's/^/ /' || echo " (没有在跑)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ── 它最近写的文件 ──"
|
|
||||||
ls -la --time-style=+%H:%M:%S /root/rp_* /root/*.tsv 2>/dev/null | tail -8 | sed 's/^/ /' || true
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 干净可用率测量:没有并发下载,间隔 3 秒,30 次
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE=https://relay.xxmxxs.top
|
|
||||||
UA='LanRemote/1.8.0'
|
|
||||||
|
|
||||||
echo " 先静置 20 秒,等 4G 链路缓过来…"
|
|
||||||
sleep 20
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 隧道连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
echo
|
|
||||||
|
|
||||||
ok=0
|
|
||||||
fail=0
|
|
||||||
declare -a bad
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 25 \
|
|
||||||
-H "User-Agent: $UA" "$BASE/v1/health")
|
|
||||||
if [ "$c" = "200" ]; then
|
|
||||||
ok=$((ok + 1))
|
|
||||||
else
|
|
||||||
fail=$((fail + 1))
|
|
||||||
bad+=("$i:$c")
|
|
||||||
fi
|
|
||||||
printf " %2d: %s\n" "$i" "$c"
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ──────────────────────────────"
|
|
||||||
echo " 可用 $ok/30 = $((ok * 100 / 30))%"
|
|
||||||
echo " 失败 $fail 次: ${bad[*]:-无}"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 失败时隧道连接数变化(看是不是掉到 1):"
|
|
||||||
echo " $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " watchdog 最近 8 行:"
|
|
||||||
tail -8 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " 网络质量:"
|
|
||||||
ping -c 10 -W 2 1.1.1.1 2>/dev/null | tail -2 | sed 's/^/ /'
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''统一清理两个平台发行版里的重复附件。
|
|
||||||
|
|
||||||
背景:
|
|
||||||
重发时如果只"复用已有发行版",附件会一次次堆积。
|
|
||||||
Gitee 尤其绕:`GET /releases/tags/{tag}` 返回的 `assets`
|
|
||||||
**只有 name 和 download_url,没有 id**,所以没法删。
|
|
||||||
但另一个接口有 id:
|
|
||||||
GET /repos/{o}/{r}/releases/{id}/attach_files -> 带 id
|
|
||||||
DELETE /repos/{o}/{r}/releases/{id}/attach_files/{attach_id}
|
|
||||||
|
|
||||||
Gitea/EzHub 则直接有 assets[].id。
|
|
||||||
|
|
||||||
策略:同名附件只保留最新的一份,其余删掉。
|
|
||||||
支持 --dry 先看要删什么。
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
|
||||||
GT = (ROOT / "_build" / ".gitee_token").read_text(encoding="utf-8").strip()
|
|
||||||
ET = (Path.home() / ".ykxmz" / "ezhub_token").read_text(
|
|
||||||
encoding="utf-8").strip()
|
|
||||||
TAG = "v1.8.0"
|
|
||||||
|
|
||||||
# 这两个是 Gitee 自动生成的源码包,不是我们传的,不要动
|
|
||||||
KEEP_ALWAYS = {"v1.8.0.zip", "v1.8.0.tar.gz"}
|
|
||||||
|
|
||||||
|
|
||||||
def http(url: str, method: str = "GET", kind: str = "gitea",
|
|
||||||
token: str = "", tries: int = 3):
|
|
||||||
"""带重试的请求。网络会抖,不重试会误判。"""
|
|
||||||
headers = {"User-Agent": "ykxmz-clean"}
|
|
||||||
if kind == "gitee":
|
|
||||||
sep = "&" if "?" in url else "?"
|
|
||||||
url = url + sep + "access_token=" + token
|
|
||||||
else:
|
|
||||||
headers["Authorization"] = "token " + token
|
|
||||||
last = None
|
|
||||||
for attempt in range(1, tries + 1):
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(url, method=method, headers=headers)
|
|
||||||
with urllib.request.urlopen(req, timeout=90) as resp:
|
|
||||||
text = resp.read().decode("utf-8", "replace")
|
|
||||||
return resp.status, (json.loads(text) if text.strip() else {})
|
|
||||||
except urllib.error.HTTPError as exc:
|
|
||||||
return exc.code, exc.read().decode("utf-8", "replace")[:200]
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
last = exc
|
|
||||||
if attempt < tries:
|
|
||||||
time.sleep(4)
|
|
||||||
return 0, str(last)
|
|
||||||
|
|
||||||
|
|
||||||
def list_gitee_assets(rid: int) -> list:
|
|
||||||
"""Gitee 要单独调 attach_files 才拿得到 id。"""
|
|
||||||
status, data = http(
|
|
||||||
"https://gitee.com/api/v5/repos/xmz7891/ykxmz/releases/%s"
|
|
||||||
"/attach_files" % rid, kind="gitee", token=GT)
|
|
||||||
if status != 200 or not isinstance(data, list):
|
|
||||||
print(" [失败] 取 Gitee 附件列表 HTTP %s: %s" % (status, data))
|
|
||||||
return []
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def list_gitea_assets(rid: int) -> list:
|
|
||||||
status, data = http(
|
|
||||||
"https://git.261826.xyz/api/v1/repos/xmz/ykxmz/releases/%s"
|
|
||||||
% rid, kind="gitea", token=ET)
|
|
||||||
if status != 200:
|
|
||||||
print(" [失败] 取 EzHub 发行版 HTTP %s" % status)
|
|
||||||
return []
|
|
||||||
return data.get("assets") or []
|
|
||||||
|
|
||||||
|
|
||||||
def clean(platform: str, rid: int, assets: list, dry: bool) -> int:
|
|
||||||
groups = defaultdict(list)
|
|
||||||
for a in assets:
|
|
||||||
name = a.get("name") or "?"
|
|
||||||
if name in KEEP_ALWAYS:
|
|
||||||
continue
|
|
||||||
groups[name].append(a)
|
|
||||||
|
|
||||||
removed = 0
|
|
||||||
for name, items in sorted(groups.items()):
|
|
||||||
if len(items) <= 1:
|
|
||||||
continue
|
|
||||||
# 新的排前面
|
|
||||||
items.sort(key=lambda x: str(x.get("created_at") or ""), reverse=True)
|
|
||||||
keep = items[0]
|
|
||||||
print(" %-32s %d 份 -> 保留 id=%s" % (name, len(items),
|
|
||||||
keep.get("id")))
|
|
||||||
for extra in items[1:]:
|
|
||||||
aid = extra.get("id")
|
|
||||||
if dry:
|
|
||||||
print(" (演练)会删 id=%s" % aid)
|
|
||||||
removed += 1
|
|
||||||
continue
|
|
||||||
if platform == "gitee":
|
|
||||||
url = ("https://gitee.com/api/v5/repos/xmz7891/ykxmz"
|
|
||||||
"/releases/%s/attach_files/%s" % (rid, aid))
|
|
||||||
else:
|
|
||||||
url = ("https://git.261826.xyz/api/v1/repos/xmz/ykxmz"
|
|
||||||
"/releases/%s/assets/%s" % (rid, aid))
|
|
||||||
status, body = http(url, method="DELETE",
|
|
||||||
kind=("gitee" if platform == "gitee"
|
|
||||||
else "gitea"),
|
|
||||||
token=(GT if platform == "gitee" else ET))
|
|
||||||
if status in (200, 204):
|
|
||||||
print(" ✓ 删了 id=%s" % aid)
|
|
||||||
removed += 1
|
|
||||||
else:
|
|
||||||
print(" ✗ id=%s HTTP %s %s"
|
|
||||||
% (aid, status, str(body)[:80]))
|
|
||||||
return removed
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
dry = "--dry" in sys.argv
|
|
||||||
total = 0
|
|
||||||
|
|
||||||
print(" ═══ Gitee ═══")
|
|
||||||
status, rel = http("https://gitee.com/api/v5/repos/xmz7891/ykxmz"
|
|
||||||
"/releases/tags/" + TAG, kind="gitee", token=GT)
|
|
||||||
if status == 200:
|
|
||||||
rid = rel.get("id")
|
|
||||||
assets = list_gitee_assets(rid)
|
|
||||||
print(" 发行版 id=%s,可删附件 %d 个" % (rid, len(assets)))
|
|
||||||
total += clean("gitee", rid, assets, dry)
|
|
||||||
else:
|
|
||||||
print(" [失败] HTTP %s" % status)
|
|
||||||
print()
|
|
||||||
|
|
||||||
print(" ═══ EzHub ═══")
|
|
||||||
status, rel = http("https://git.261826.xyz/api/v1/repos/xmz/ykxmz"
|
|
||||||
"/releases/tags/" + TAG, kind="gitea", token=ET)
|
|
||||||
if status == 200:
|
|
||||||
rid = rel.get("id")
|
|
||||||
assets = list_gitea_assets(rid)
|
|
||||||
print(" 发行版 id=%s,附件 %d 个" % (rid, len(assets)))
|
|
||||||
total += clean("gitea", rid, assets, dry)
|
|
||||||
else:
|
|
||||||
print(" [失败] HTTP %s" % status)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" %s %d 个" % ("演练:会删" if dry else "共删", total))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
r'''清理 EzHub 发行版里重复的附件。
|
|
||||||
|
|
||||||
为什么会重复:
|
|
||||||
release_all.py 发现标签已有发行版时会"复用它",
|
|
||||||
但**没有先删同名附件**,于是每跑一次就多传一遍。
|
|
||||||
我调试时跑了几次,同一个文件就堆了 2~3 份。
|
|
||||||
|
|
||||||
这个脚本:同名附件只保留最新的一份,其余删掉。
|
|
||||||
(Gitea 的附件用 id 区分,创建时间看 created_at。)
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TOKEN = (Path.home() / ".ykxmz" / "ezhub_token").read_text(
|
|
||||||
encoding="utf-8").strip()
|
|
||||||
API = "https://git.261826.xyz/api/v1/repos/xmz/ykxmz"
|
|
||||||
TAG = "v1.8.0"
|
|
||||||
HEAD = {"Authorization": "token " + TOKEN, "User-Agent": "ykxmz-clean"}
|
|
||||||
|
|
||||||
|
|
||||||
def call(url: str, method: str = "GET"):
|
|
||||||
req = urllib.request.Request(url, method=method, headers=HEAD)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
||||||
text = resp.read().decode("utf-8", "replace")
|
|
||||||
return resp.status, (json.loads(text) if text.strip() else {})
|
|
||||||
except urllib.error.HTTPError as exc:
|
|
||||||
body = exc.read().decode("utf-8", "replace")[:200]
|
|
||||||
return exc.code, body
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
dry = "--dry" in sys.argv
|
|
||||||
|
|
||||||
status, release = call("%s/releases/tags/%s" % (API, TAG))
|
|
||||||
if status != 200:
|
|
||||||
print(" [失败] 取发行版 HTTP %s: %s" % (status, release))
|
|
||||||
return 1
|
|
||||||
rid = release["id"]
|
|
||||||
assets = release.get("assets") or []
|
|
||||||
print(" 发行版 id=%s,附件 %d 个" % (rid, len(assets)))
|
|
||||||
|
|
||||||
# 按文件名分组
|
|
||||||
groups = defaultdict(list)
|
|
||||||
for a in assets:
|
|
||||||
groups[a.get("name") or "?"].append(a)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 分组情况:")
|
|
||||||
to_delete = []
|
|
||||||
for name, items in sorted(groups.items()):
|
|
||||||
# 新的排在前面(created_at 降序)
|
|
||||||
items.sort(key=lambda x: x.get("created_at") or "", reverse=True)
|
|
||||||
keep = items[0]
|
|
||||||
print(" %-32s 共 %d 份,保留 id=%s(%s)"
|
|
||||||
% (name, len(items), keep.get("id"),
|
|
||||||
(keep.get("created_at") or "")[:19]))
|
|
||||||
for extra in items[1:]:
|
|
||||||
print(" 删 id=%s(%s)" % (extra.get("id"),
|
|
||||||
(extra.get("created_at")
|
|
||||||
or "")[:19]))
|
|
||||||
to_delete.append(extra)
|
|
||||||
|
|
||||||
if not to_delete:
|
|
||||||
print()
|
|
||||||
print(" ✓ 没有重复,不用清理")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print()
|
|
||||||
if dry:
|
|
||||||
print(" 演练:会删 %d 个(去掉 --dry 真删)" % len(to_delete))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print(" 开始删除 %d 个重复附件…" % len(to_delete))
|
|
||||||
ok = 0
|
|
||||||
for asset in to_delete:
|
|
||||||
status, body = call("%s/releases/%s/assets/%s"
|
|
||||||
% (API, rid, asset.get("id")), method="DELETE")
|
|
||||||
if status in (200, 204):
|
|
||||||
print(" ✓ 删了 id=%s %s" % (asset.get("id"),
|
|
||||||
asset.get("name")))
|
|
||||||
ok += 1
|
|
||||||
else:
|
|
||||||
print(" ✗ id=%s HTTP %s %s" % (asset.get("id"), status, body))
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" 删除 %d/%d" % (ok, len(to_delete)))
|
|
||||||
|
|
||||||
# 复查
|
|
||||||
status, release = call("%s/releases/tags/%s" % (API, TAG))
|
|
||||||
assets = release.get("assets") or []
|
|
||||||
print()
|
|
||||||
print(" 现在剩 %d 个附件:" % len(assets))
|
|
||||||
for a in assets:
|
|
||||||
print(" %-32s %8.2f MB"
|
|
||||||
% (a.get("name"), (a.get("size") or 0) / 1024 / 1024))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 安全清理随身 WiFi 上的垃圾。
|
|
||||||
#
|
|
||||||
# 原则:先看清楚再删,删之前把「为什么它一定是垃圾」写下来。
|
|
||||||
# 中转相关的目录(/root/lanremote-relay、/root/.cloudflared)一律不碰。
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BEFORE=$(df -m / | awk 'NR==2 {print $4}')
|
|
||||||
echo "════════ 清理前:可用 ${BEFORE} MB ════════"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "════════ 0. 先看 nginx 到底在提供什么(决定动不动它)════════"
|
|
||||||
echo " 在跑: $(systemctl is-active nginx)"
|
|
||||||
echo " 根目录配置:"
|
|
||||||
grep -hE '^\s*(root|server_name|listen)' /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null | head -10 | sed 's/^/ /'
|
|
||||||
echo " /var/www 内容:"
|
|
||||||
ls -1 /var/www 2>/dev/null | sed 's/^/ /'
|
|
||||||
echo " 首页大小: $(stat -c%s /var/www/html/index.nginx-debian.html 2>/dev/null || echo 0) 字节"
|
|
||||||
echo " → 这是 Debian 装 nginx 的默认欢迎页,和中转无关。"
|
|
||||||
echo " **但为了安全我不删 nginx**(万一有人拿它做别的),只报告。"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 1. 删 /var/lib/mysql(113 MB)════════"
|
|
||||||
echo " 依据:"
|
|
||||||
echo " * dpkg 里没有任何 mysql/mariadb 包(0 个已安装)"
|
|
||||||
echo " * 没有任何 mysqld 进程"
|
|
||||||
echo " * 没有 mariadb/mysql 的 systemd 服务"
|
|
||||||
echo " → 这是历史遗留的数据目录,包都卸了,目录没删干净。"
|
|
||||||
if [ -d /var/lib/mysql ]; then
|
|
||||||
echo " 删除前确认没有进程占用: $(fuser -m /var/lib/mysql 2>/dev/null | wc -l) 个"
|
|
||||||
if [ "$(pgrep -c -f 'mysqld|mariadb' 2>/dev/null || echo 0)" -eq 0 ] \
|
|
||||||
&& [ "$(dpkg -l 2>/dev/null | grep -cE '^ii +(mysql-server|mariadb-server)')" -eq 0 ]; then
|
|
||||||
rm -rf /var/lib/mysql
|
|
||||||
echo " ✓ 已删除 /var/lib/mysql"
|
|
||||||
else
|
|
||||||
echo " ! 检测到 mysql 相关包或进程,**跳过**(安全第一)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " (不存在,跳过)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 2. 清 apt 缓存和索引(约 162 MB)════════"
|
|
||||||
echo " 依据:/var/cache/apt 是已下载的 .deb 缓存,随时可重下;"
|
|
||||||
echo " /var/lib/apt/lists 是索引,apt update 会重建。"
|
|
||||||
apt-get clean 2>/dev/null
|
|
||||||
rm -rf /var/lib/apt/lists/* 2>/dev/null
|
|
||||||
echo " ✓ 已清理"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 3. 压缩 journald 日志(约 10 MB)════════"
|
|
||||||
journalctl --vacuum-size=8M 2>&1 | tail -2 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 4. 清理我遗留的调试脚本(173 个)════════"
|
|
||||||
echo " 这些是我这几次调试时上传到 /root 的临时脚本(ab*.sh、bench.sh 等),"
|
|
||||||
echo " 中转本身不需要它们。"
|
|
||||||
echo " ⚠ 保留:中转目录、.cloudflared、以及任何看起来像正式运维脚本的"
|
|
||||||
n=$(ls -1 /root/*.sh /root/*.py 2>/dev/null | wc -l)
|
|
||||||
echo " 清理前: $n 个"
|
|
||||||
mkdir -p /root/.trash-debug
|
|
||||||
# 只移动我确定是调试用的(a开头/ab开头/bench/check开头等),保留其他
|
|
||||||
for f in /root/a*.sh /root/ab*.sh /root/bench*.sh /root/check_*.sh /root/diag_*.sh \
|
|
||||||
/root/dl*.sh /root/final*.sh /root/post_*.sh /root/probe*.py /root/smart_*.sh \
|
|
||||||
/root/setup_*.sh /root/test_*.sh /root/why_*.sh /root/fa*.sh /root/tc*.sh \
|
|
||||||
/root/cc*.sh /root/sa*.sh /root/sq.sh /root/dc.sh /root/vc*.sh; do
|
|
||||||
[ -f "$f" ] && mv "$f" /root/.trash-debug/ 2>/dev/null
|
|
||||||
done
|
|
||||||
moved=$(ls -1 /root/.trash-debug 2>/dev/null | wc -l)
|
|
||||||
echo " 已移走 $moved 个到 /root/.trash-debug(先不真删,确认没问题再删)"
|
|
||||||
echo " /root 下剩余: $(ls -1 /root/*.sh /root/*.py 2>/dev/null | wc -l) 个"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 5. 中转目录(绝对不动)════════"
|
|
||||||
du -sh /root/lanremote-relay /root/.cloudflared 2>/dev/null | sed 's/^/ /'
|
|
||||||
echo " → 这两个是命根子,一个字节都没碰"
|
|
||||||
|
|
||||||
echo
|
|
||||||
AFTER=$(df -m / | awk 'NR==2 {print $4}')
|
|
||||||
echo "════════ 结果 ════════"
|
|
||||||
echo " 可用空间: ${BEFORE} MB -> ${AFTER} MB (腾出 $((AFTER - BEFORE)) MB)"
|
|
||||||
df -h / | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
echo " 磁盘总览:"
|
|
||||||
du -xhd1 / 2>/dev/null | sort -rh | head -8 | sed 's/^/ /'
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 补删 /var/lib/mysql(上一条脚本的整数判断写错了,误跳过)
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "════════ 删除前的三重确认 ════════"
|
|
||||||
echo " 1. 有没有 mysqld/mariadb 进程:"
|
|
||||||
n=$(pgrep -f 'mysqld|mariadb' 2>/dev/null | wc -l)
|
|
||||||
echo " $n 个"
|
|
||||||
echo " 2. dpkg 里有没有 mysql/mariadb 包:"
|
|
||||||
p=$(dpkg -l 2>/dev/null | grep -cE '^ii +(mysql|mariadb)' || true)
|
|
||||||
echo " $p 个"
|
|
||||||
echo " 3. 有没有相关 systemd 服务:"
|
|
||||||
s=$(systemctl list-unit-files 2>/dev/null | grep -cE '^(mysql|mariadb)' || true)
|
|
||||||
echo " $s 个"
|
|
||||||
echo " 4. 目录大小:$(du -sh /var/lib/mysql 2>/dev/null | awk '{print $1}')"
|
|
||||||
echo " 5. 有没有别的进程打开着里面的文件:"
|
|
||||||
fuser -m /var/lib/mysql 2>/dev/null | wc -l | sed 's/^/ /'
|
|
||||||
|
|
||||||
if [ "$n" -eq 0 ] && [ "$p" -eq 0 ] && [ "$s" -eq 0 ]; then
|
|
||||||
echo
|
|
||||||
echo " → 三项都是 0,确认是卸载残留的数据目录,删除"
|
|
||||||
rm -rf /var/lib/mysql
|
|
||||||
echo " ✓ 已删除"
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo " ! 还有引用,**不删**"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 把剩余的调试脚本也移走 ════════"
|
|
||||||
# 上一条只移走了一部分(通配符没覆盖全)。
|
|
||||||
# 现在把 /root 下**所有顶层** .sh/.py 都移进回收目录 ——
|
|
||||||
# 中转的运行时代码在 /root/lanremote-relay/ 里,不受影响。
|
|
||||||
before=$(ls -1 /root/*.sh /root/*.py 2>/dev/null | wc -l)
|
|
||||||
mkdir -p /root/.trash-debug
|
|
||||||
for f in /root/*.sh /root/*.py; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
mv "$f" /root/.trash-debug/ 2>/dev/null
|
|
||||||
done
|
|
||||||
after=$(ls -1 /root/*.sh /root/*.py 2>/dev/null | wc -l)
|
|
||||||
echo " /root 顶层脚本: $before -> $after"
|
|
||||||
echo " 回收目录里的: $(ls -1 /root/.trash-debug 2>/dev/null | wc -l) 个"
|
|
||||||
echo " ⚠ 没有真删,放在 /root/.trash-debug;确认中转一切正常后再 rm -rf"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 确认中转没被影响 ════════"
|
|
||||||
echo " relay 服务: $(systemctl is-active lanremote-relay)"
|
|
||||||
echo " 隧道(quic): $(systemctl is-active lanremote-tunnel)"
|
|
||||||
echo " 隧道(h2): $(systemctl is-active lanremote-tunnel-h2)"
|
|
||||||
echo " 自检: $(curl -s --max-time 10 http://127.0.0.1:8080/v1/health | head -c 70)"
|
|
||||||
echo
|
|
||||||
echo " 中转目录完好: $(ls /root/lanremote-relay/ | tr '\n' ' ')"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "════════ 最终空间 ════════"
|
|
||||||
df -h / | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
du -xhd1 / 2>/dev/null | sort -rh | head -8 | sed 's/^/ /'
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user