mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
2.1.3:搬到 Lightsail 真服务器 + 支持"没有证书的中转"
用户买了 AWS Lightsail(2G 内存 / 2 vCPU / 60G SSD / 公网 IP
13.54.30.196),中转从随身 WiFi 那台小机器搬过去。
== 为什么这次搬迁解决了一堆老问题 ==
原机器(Debian aarch64、460MB、走 4G)一直有几件搞不定的:
* 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**(443 通、7844 不通),
隧道每 3~10 分钟掉一次、公网跟着 530 —— 查了很久才从
cloudflared 自己的 precheck 里看清
* 只有 460MB 内存
* 会掉电硬挂(实测 2026-10-08 18:42 一次非正常关机)
新机器**有公网 IP**,中转直接监听,不经隧道、不过 7844。
实测公网延迟从 **1000~2000 ms 降到 1.1 ms**。
== 代码改动 ==
1. **relay.py 的 DATA_DIR 默认值不再写死**
原来是 /root/lanremote-relay。新机器上服务以 admin 普通用户跑
(它没理由要 root),而 /root 是 0700 —— 部署时直接
PermissionError: '/root/lanremote-relay'。
改成"本脚本所在目录下的 data/",搬到哪都跟着走。
2. **客户端支持"没有证书的中转"**
新服务器是明文 HTTP(没签证书),而两个客户端原来都往 https 走:
* 手表端:裸 IP 会被强制补 https:// -> 永远连不上,
报的还是 SSL 错误,很容易让人白查半天证书
* Android 9+ 默认禁止明文流量,没 usesCleartextTraffic
请求会被系统直接掐掉
Windows 端本来就是对的(正则里把任意纯 IP 都当"用明文")。
现在手表端规则和它一致,并加了 usesCleartextTraffic。
3. 清掉电脑端一段函数内的重复代码(return 之后还有一份拷贝,
缩进仍在函数体内所以不报错,但读起来很迷惑)。
== 新服务器上的部署 ==
/opt/ykxmz-relay/ 服务端(relay/admin/landing/showcase)
ykxmz-relay.service 监听 0.0.0.0:13245
ykxmz-monitor.service/.timer 每 60 秒查公网,连续 3 次不通就叫预案
rescue-lightsail.sh 四级阶梯抢救(重启中转 / 强杀重拉 /
查防火墙 / 重启整机[默认关])
预案针对新架构重写过:原来围着隧道转(重启隧道、重施 MSS/MTU),
现在没有隧道,改成重启中转 + 查端口/防火墙。
全量回归 46 个阶段全部通过。
This commit is contained in:
@@ -1,3 +1,27 @@
|
|||||||
|
## 2.1.3
|
||||||
|
|
||||||
|
- **支持「自己搭的、没有证书的中转」**(新服务器就是这种)。
|
||||||
|
|
||||||
|
中转不再是必须挂在 Cloudflare 隧道后面了。现在可以直接跑在
|
||||||
|
一台有公网 IP 的机器上,客户端填 `IP:端口` 就能连 —— 延迟、稳定性
|
||||||
|
都比隧道好得多。为此改了两处客户端:
|
||||||
|
|
||||||
|
- **手表端**:填 `13.54.30.196:13245` 这种**没写协议头**的地址时,
|
||||||
|
现在会按「IP / localhost / 本地域名 → 明文,域名 → 加密」来猜,
|
||||||
|
**和电脑端规则一致**。原来一律补成 `https://`,而那台机器没有证书,
|
||||||
|
结果是**永远连不上**,报的还是 SSL 错误,很容易让人白查半天证书。
|
||||||
|
|
||||||
|
- **手表端**:允许明文流量。Android 9 以上默认禁止,不加这一条的话
|
||||||
|
请求会被系统直接掐掉,日志里只有一句含糊的
|
||||||
|
「Cleartext HTTP traffic to ... not permitted」。
|
||||||
|
|
||||||
|
> 说明:中转上跑的内容(配对令牌、画面、文件)本来就是加密过的,
|
||||||
|
> 明文只暴露地址和设备名。想要全程加密的话,把域名解析到这台
|
||||||
|
> 服务器再签一张免费证书即可。
|
||||||
|
|
||||||
|
- 顺手清掉了电脑端一段函数内的重复代码(`return` 之后还有一份一样的
|
||||||
|
拷贝,缩进仍在函数体内所以不报错,但读起来很迷惑)。
|
||||||
|
|
||||||
## 2.1.2
|
## 2.1.2
|
||||||
|
|
||||||
- **隧道稳定性加固**(继续修「网站还是容易挂」)。查到隧道自己的诊断输出:
|
- **隧道稳定性加固**(继续修「网站还是容易挂」)。查到隧道自己的诊断输出:
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
2.1.3:搬到 Lightsail 真服务器 + 支持"没有证书的中转"
|
||||||
|
|
||||||
|
用户买了 AWS Lightsail(2G 内存 / 2 vCPU / 60G SSD / 公网 IP
|
||||||
|
13.54.30.196),中转从随身 WiFi 那台小机器搬过去。
|
||||||
|
|
||||||
|
== 为什么这次搬迁解决了一堆老问题 ==
|
||||||
|
|
||||||
|
原机器(Debian aarch64、460MB、走 4G)一直有几件搞不定的:
|
||||||
|
|
||||||
|
* 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**(443 通、7844 不通),
|
||||||
|
隧道每 3~10 分钟掉一次、公网跟着 530 —— 查了很久才从
|
||||||
|
cloudflared 自己的 precheck 里看清
|
||||||
|
* 只有 460MB 内存
|
||||||
|
* 会掉电硬挂(实测 2026-10-08 18:42 一次非正常关机)
|
||||||
|
|
||||||
|
新机器**有公网 IP**,中转直接监听,不经隧道、不过 7844。
|
||||||
|
实测公网延迟从 **1000~2000 ms 降到 1.1 ms**。
|
||||||
|
|
||||||
|
== 代码改动 ==
|
||||||
|
|
||||||
|
1. **relay.py 的 DATA_DIR 默认值不再写死**
|
||||||
|
原来是 /root/lanremote-relay。新机器上服务以 admin 普通用户跑
|
||||||
|
(它没理由要 root),而 /root 是 0700 —— 部署时直接
|
||||||
|
PermissionError: '/root/lanremote-relay'。
|
||||||
|
改成"本脚本所在目录下的 data/",搬到哪都跟着走。
|
||||||
|
|
||||||
|
2. **客户端支持"没有证书的中转"**
|
||||||
|
新服务器是明文 HTTP(没签证书),而两个客户端原来都往 https 走:
|
||||||
|
* 手表端:裸 IP 会被强制补 https:// -> 永远连不上,
|
||||||
|
报的还是 SSL 错误,很容易让人白查半天证书
|
||||||
|
* Android 9+ 默认禁止明文流量,没 usesCleartextTraffic
|
||||||
|
请求会被系统直接掐掉
|
||||||
|
Windows 端本来就是对的(正则里把任意纯 IP 都当"用明文")。
|
||||||
|
现在手表端规则和它一致,并加了 usesCleartextTraffic。
|
||||||
|
|
||||||
|
3. 清掉电脑端一段函数内的重复代码(return 之后还有一份拷贝,
|
||||||
|
缩进仍在函数体内所以不报错,但读起来很迷惑)。
|
||||||
|
|
||||||
|
== 新服务器上的部署 ==
|
||||||
|
|
||||||
|
/opt/ykxmz-relay/ 服务端(relay/admin/landing/showcase)
|
||||||
|
ykxmz-relay.service 监听 0.0.0.0:13245
|
||||||
|
ykxmz-monitor.service/.timer 每 60 秒查公网,连续 3 次不通就叫预案
|
||||||
|
rescue-lightsail.sh 四级阶梯抢救(重启中转 / 强杀重拉 /
|
||||||
|
查防火墙 / 重启整机[默认关])
|
||||||
|
|
||||||
|
预案针对新架构重写过:原来围着隧道转(重启隧道、重施 MSS/MTU),
|
||||||
|
现在没有隧道,改成重启中转 + 查端口/防火墙。
|
||||||
|
|
||||||
|
全量回归 46 个阶段全部通过。
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种)。
|
||||||
|
|
||||||
|
== 背景 ==
|
||||||
|
|
||||||
|
2026-10-08 换了服务器:AWS Lightsail,**有公网 IP**(13.54.30.196),
|
||||||
|
中转直接监听 13245,**不再走 Cloudflare 隧道**。
|
||||||
|
|
||||||
|
好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍),
|
||||||
|
但有个直接后果:**现在是明文 HTTP,没有 HTTPS 证书**。
|
||||||
|
|
||||||
|
而两个客户端原来都默认往 https 上走:
|
||||||
|
|
||||||
|
1. Android 的 RelayTransport:用户填 `13.54.30.196:13245`
|
||||||
|
(没写协议头)会被强制加上 `https://` -> 连不上(那台机器没证书)
|
||||||
|
|
||||||
|
2. Android 9+ **默认禁止明文 HTTP**。没有
|
||||||
|
`android:usesCleartextTraffic="true"` 的话,请求会被系统直接掐掉,
|
||||||
|
报的还是很含糊的 "Cleartext HTTP traffic not permitted"。
|
||||||
|
|
||||||
|
Windows 那边已经是对的:relay.py 的 _PRIVATE_HOST 正则里
|
||||||
|
`\d{1,3}(?:\.\d{1,3}){3}` 把**任意纯 IP(含公网)**都当"用明文",
|
||||||
|
所以裸 IP 会正确走 http://。
|
||||||
|
|
||||||
|
== 这个补丁做什么 ==
|
||||||
|
|
||||||
|
1. RelayTransport 的协议猜测改成和 Windows 一致:
|
||||||
|
纯 IP / localhost / *.local -> http://
|
||||||
|
域名 -> https://
|
||||||
|
|
||||||
|
2. AndroidManifest 加 usesCleartextTraffic="true"
|
||||||
|
|
||||||
|
3. 顺手清掉 windows/lanremote/relay.py 里一段**函数内死代码**
|
||||||
|
(_guess_scheme 的 return 之后还有一份一模一样的拷贝,
|
||||||
|
缩进仍属于函数体所以不报错,但看着很迷惑)
|
||||||
|
|
||||||
|
== 关于 HTTPS ==
|
||||||
|
|
||||||
|
明文 HTTP 意味着**同网络的人能看到流量**(虽然配对令牌、内容本来
|
||||||
|
就是加密的,但 URL、设备名这些会露)。真正的解法是上 HTTPS:
|
||||||
|
|
||||||
|
把域名 relay.xxmxxs.top 的 A 记录指到 13.54.30.196,
|
||||||
|
然后 certbot 签一张 Let's Encrypt 证书,confd 挂上。
|
||||||
|
|
||||||
|
那一步要用户动 DNS,所以先让明文能用(这个补丁),
|
||||||
|
HTTPS 等 DNS 指过来再做。
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
JAVA = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
|
||||||
|
/ "RelayTransport.java")
|
||||||
|
MANIFEST = ROOT / "android" / "AndroidManifest.xml"
|
||||||
|
WINRELAY = ROOT / "windows" / "lanremote" / "relay.py"
|
||||||
|
|
||||||
|
|
||||||
|
def patch(path: Path, old: str, new: str, what: str, out: list) -> str:
|
||||||
|
raw = path.read_bytes()
|
||||||
|
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
text = raw.decode("utf-8-sig")
|
||||||
|
if old not in text:
|
||||||
|
out.append(("✗", what, "找不到锚点"))
|
||||||
|
return text
|
||||||
|
if text.count(old) != 1:
|
||||||
|
out.append(("!", what, "锚点 %d 处" % text.count(old)))
|
||||||
|
text = text.replace(old, new, 1)
|
||||||
|
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||||||
|
+ text.encode("utf-8"))
|
||||||
|
out.append(("✓", what, ""))
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 1. Java
|
||||||
|
JAVA_OLD = ''' if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
url = "https://" + url;
|
||||||
|
}'''
|
||||||
|
|
||||||
|
JAVA_NEW = ''' if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
// 没写协议头时猜一个。规则要**和 Windows 端一致**
|
||||||
|
// (windows/lanremote/relay.py 的 _guess_scheme):
|
||||||
|
//
|
||||||
|
// IP / localhost / *.local -> http://
|
||||||
|
// 域名 -> https://
|
||||||
|
//
|
||||||
|
// 为什么纯 IP 走明文:IP 地址的中转基本都是自己搭的
|
||||||
|
// (局域网那台,或者像现在这样一台有公网 IP 的服务器),
|
||||||
|
// 上面往往没有证书。原来一律加 https:// 的结果是**永远连不上**,
|
||||||
|
// 而且报的是 SSL 错误,会把人带去查证书查半天。
|
||||||
|
//
|
||||||
|
// ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail
|
||||||
|
// 之后中转就是 http://13.54.30.196:13245,
|
||||||
|
// 只把 192.168/10./172.16 当私有的话这里会走错。
|
||||||
|
String hostOnly = url;
|
||||||
|
int slash = hostOnly.indexOf('/');
|
||||||
|
if (slash > 0) {
|
||||||
|
hostOnly = hostOnly.substring(0, slash);
|
||||||
|
}
|
||||||
|
int colon = hostOnly.indexOf(':');
|
||||||
|
if (colon > 0) {
|
||||||
|
hostOnly = hostOnly.substring(0, colon);
|
||||||
|
}
|
||||||
|
boolean plain = hostOnly.equalsIgnoreCase("localhost")
|
||||||
|
|| hostOnly.toLowerCase().endsWith(".local")
|
||||||
|
|| hostOnly.matches("\\\\d{1,3}(\\\\.\\\\d{1,3}){3}");
|
||||||
|
url = (plain ? "http://" : "https://") + url;
|
||||||
|
}'''
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 2. 清单
|
||||||
|
MAN_OLD = ''' <uses-permission android:name="android.permission.INTERNET" />'''
|
||||||
|
|
||||||
|
MAN_NEW = ''' <!-- ⚠ 允许明文 HTTP。
|
||||||
|
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
|
||||||
|
2026-10-08 之后那台有公网 IP 的服务器(http://13.54.30.196:13245)。
|
||||||
|
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
|
||||||
|
直接掐掉,日志里只有一句含糊的
|
||||||
|
"Cleartext HTTP traffic to ... not permitted"。
|
||||||
|
|
||||||
|
安全性说明:中转上跑的内容(配对令牌、画面、文件)本来就是
|
||||||
|
加密过的,明文只暴露 URL 和设备名。要更严的话后续上 HTTPS
|
||||||
|
(Let's Encrypt),那时可以把这一条去掉。 -->
|
||||||
|
<application android:usesCleartextTraffic="true" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />'''
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 3. 死代码
|
||||||
|
DEAD_OLD = ''' host = address.split("/", 1)[0]
|
||||||
|
return "http://" if _PRIVATE_HOST.match(host) else "https://"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""用户没写 http/https 时,猜一个。
|
||||||
|
|
||||||
|
IP 地址 / localhost -> http://
|
||||||
|
局域网里的中转服务是明文 HTTP。如果猜成 https,永远连不上,
|
||||||
|
而且报的是 SSL 错误,很容易误导人去查证书。
|
||||||
|
域名 -> https://
|
||||||
|
走公网必须加密(Cloudflare 隧道本来就是 https)。
|
||||||
|
"""
|
||||||
|
host = address.split("/", 1)[0]
|
||||||
|
return "http://" if _PRIVATE_HOST.match(host) else "https://"'''
|
||||||
|
|
||||||
|
DEAD_NEW = ''' host = address.split("/", 1)[0]
|
||||||
|
return "http://" if _PRIVATE_HOST.match(host) else "https://"'''
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
out: list = []
|
||||||
|
|
||||||
|
patch(JAVA, JAVA_OLD, JAVA_NEW, "RelayTransport 协议猜测改成和 Windows 一致",
|
||||||
|
out)
|
||||||
|
|
||||||
|
patch(MANIFEST, MAN_OLD, MAN_NEW, "AndroidManifest 允许明文 HTTP", out)
|
||||||
|
|
||||||
|
patch(WINRELAY, DEAD_OLD, DEAD_NEW, "清掉 relay.py 里的函数内死代码", out)
|
||||||
|
|
||||||
|
print(" ── 结果 ──")
|
||||||
|
for m, what, why in out:
|
||||||
|
print(" %s %s%s" % (m, what, (" —— " + why) if why else ""))
|
||||||
|
return 0 if not any(m == "✗" for m, _w, _y in out) else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''看一眼 Lightsail 上中转服务器的完整状态。
|
||||||
|
|
||||||
|
为什么写成文件:用 `python -c @"..."@` 传带 `$(...)` 和引号的远端命令,
|
||||||
|
PowerShell 会做变量插值、还会在本机执行 `$(systemctl ...)`
|
||||||
|
(报 "无法将 systemctl 项识别为 cmdlet")。凡是超过两行的远端命令,
|
||||||
|
一律写成文件跑。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python _build/check_lightsail.py
|
||||||
|
python _build/check_lightsail.py --fix # 顺便修文件属主
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT / "_build"))
|
||||||
|
from lssh import run # noqa: E402
|
||||||
|
|
||||||
|
PORT = "13245"
|
||||||
|
PUBLIC = "http://13.54.30.196:%s" % PORT
|
||||||
|
RELAY_DIR = "/opt/ykxmz-relay"
|
||||||
|
|
||||||
|
REMOTE = r'''
|
||||||
|
R=__DIR__
|
||||||
|
P=__PORT__
|
||||||
|
echo "════ 服务 ════"
|
||||||
|
for s in ykxmz-relay ykxmz-monitor.timer; do
|
||||||
|
printf ' %-24s %s\n' "$s" "$(systemctl is-active $s 2>&1)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 健康 ════"
|
||||||
|
printf ' %-24s %s\n' "本机 health" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1:$P/v1/health)"
|
||||||
|
printf ' %-24s %s\n' "公网 health" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 __PUBLIC__/v1/health)"
|
||||||
|
printf ' %-24s %s\n' "公网首页" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 __PUBLIC__/)"
|
||||||
|
printf ' %-24s %s\n' "本机延迟" "$(curl -s -o /dev/null -w '%{time_total}s' --max-time 8 http://127.0.0.1:$P/v1/health)"
|
||||||
|
printf ' %-24s %s\n' "公网延迟" "$(curl -s -o /dev/null -w '%{time_total}s' --max-time 15 __PUBLIC__/v1/health)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 首页角标 ════"
|
||||||
|
echo " 最新版: $(curl -s --max-time 10 http://127.0.0.1:$P/ | grep -o '最新版 [0-9.]*' | head -1)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 监听 / 进程 ════"
|
||||||
|
ss -tlnp 2>/dev/null | grep ":$P " | sed 's/^/ /'
|
||||||
|
ps -eo pid,rss,etime,cmd --sort=-rss 2>/dev/null | grep 'relay\.py' | grep -v grep | head -2 | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 监测单元 ════"
|
||||||
|
cat $R/monitor_state.txt 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo " --- monitor.log 尾部 ---"
|
||||||
|
tail -4 $R/monitor.log 2>/dev/null | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 抢救报告 ════"
|
||||||
|
echo " 共 $(ls $R/rescue_reports 2>/dev/null | wc -l) 份"
|
||||||
|
ls -la $R/rescue_reports/ 2>/dev/null | tail -n +4 | awk '{print " " $6" "$7" "$8" "$9}'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 知识库 ════"
|
||||||
|
if [ -f $R/rescue_knowledge.md ]; then
|
||||||
|
echo " $(wc -l < $R/rescue_knowledge.md) 行"
|
||||||
|
else
|
||||||
|
echo " (还没有)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 资源 ════"
|
||||||
|
free -m 2>/dev/null | sed -n 2p | awk '{printf " 内存 %s/%s MB\n", $3, $2}'
|
||||||
|
df -h / | tail -1 | awk '{printf " 磁盘 %s 用 %s\n", $2, $5}'
|
||||||
|
uptime | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 定时器下次触发 ════"
|
||||||
|
systemctl list-timers ykxmz-monitor.timer --no-pager 2>/dev/null | head -3 | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════ 文件属主(admin 要能写日志)════"
|
||||||
|
ls -la $R/*.log $R/monitor_state.txt 2>/dev/null | awk '{print " " $3":"$4" "$9}'
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if "--fix" in sys.argv:
|
||||||
|
print(" ── 修属主 ──")
|
||||||
|
out = run("sudo chown -R admin:admin %s && sudo chmod 755 %s && echo OK"
|
||||||
|
% (RELAY_DIR, RELAY_DIR), timeout=120)
|
||||||
|
print(" " + out[1].strip())
|
||||||
|
|
||||||
|
cmd = (REMOTE.replace("__DIR__", RELAY_DIR)
|
||||||
|
.replace("__PORT__", PORT)
|
||||||
|
.replace("__PUBLIC__", PUBLIC))
|
||||||
|
out = run(cmd, timeout=300, quiet=True)
|
||||||
|
print(out[1] if isinstance(out, tuple) else out)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''把中转服务器部署到 AWS Lightsail 那台新机器上。
|
||||||
|
|
||||||
|
== 为什么要搬 ==
|
||||||
|
|
||||||
|
原来中转跑在一台"随身 WiFi"小机器上(Debian aarch64、460MB 内存、
|
||||||
|
走 4G),一直有一堆搞不定的问题:
|
||||||
|
|
||||||
|
* 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**
|
||||||
|
—— 443 通、7844 不通,隧道每 3~10 分钟就掉一次,公网跟着 530
|
||||||
|
(这个查了很久,cloudflared 自己的 precheck 才说清楚)
|
||||||
|
* 只有 460MB 内存,跑什么都得省着来
|
||||||
|
* 会掉电/硬挂(实测 18:42 一次非正常关机,19:58 才起来)
|
||||||
|
* 全靠 Cloudflare 隧道兜,隧道一抖网站就抖
|
||||||
|
|
||||||
|
新机器(Lightsail)**有公网 IP**,直接监听就行 ——
|
||||||
|
不需要隧道、不经过 Cloudflare 的 7844、内存是 4 倍、磁盘 59G。
|
||||||
|
上面那些问题**一次性全部消失**。
|
||||||
|
|
||||||
|
== 关键配置 ==
|
||||||
|
|
||||||
|
RELAY_PORT=13245 用户指定(防火墙那边他会开)
|
||||||
|
RELAY_BIND=0.0.0.0 有公网 IP,直接对外
|
||||||
|
|
||||||
|
13245 大于 1024,所以**不需要** CAP_NET_BIND_SERVICE,
|
||||||
|
用普通用户跑就行 —— 比听 80 干净。
|
||||||
|
|
||||||
|
== 为什么不用 root 跑 ==
|
||||||
|
|
||||||
|
relay 是个只读文件的 HTTP 服务,没有任何理由给它 root。
|
||||||
|
用 admin 用户跑,万一被打穿,损失也小得多。
|
||||||
|
(2026-10-08 之前那个仓库里还硬编码过管理员密码,
|
||||||
|
这种教训够一次就行了。)
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT / "_build"))
|
||||||
|
from lssh import run, upload # noqa: E402
|
||||||
|
|
||||||
|
# 用户指定的端口
|
||||||
|
PORT = "13245"
|
||||||
|
REMOTE_DIR = "/opt/ykxmz-relay"
|
||||||
|
DATA_DIR = REMOTE_DIR + "/data" # relay.py 现在默认就用 data/ 了
|
||||||
|
|
||||||
|
# 服务端要传的文件(.py 是服务本体,.ico 是管理页图标)
|
||||||
|
FILES = ["relay.py", "admin.py", "landing.py", "showcase.py", "favicon.ico"]
|
||||||
|
|
||||||
|
UNIT = '''[Unit]
|
||||||
|
Description=ykxmz 中转服务器
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=admin
|
||||||
|
Group=admin
|
||||||
|
WorkingDirectory=%(dir)s
|
||||||
|
|
||||||
|
# ⚠ 监听地址和端口都从这里来。
|
||||||
|
# 13245 是用户指定的(他会在 Lightsail 防火墙那边开)。
|
||||||
|
# BIND 用 0.0.0.0 是因为新机器**有公网 IP**,不再需要
|
||||||
|
# Cloudflare 隧道(原来那个隧道在这条线路上每 3~10 分钟掉一次,
|
||||||
|
# 是"网站容易挂"的主因之一)。
|
||||||
|
Environment=RELAY_PORT=%(port)s
|
||||||
|
Environment=RELAY_BIND=0.0.0.0
|
||||||
|
|
||||||
|
ExecStart=/usr/bin/python3 %(dir)s/relay.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
# 兜底:内存保护(这台有 2G,给 512M 足够且不会拖垮整机)
|
||||||
|
MemoryMax=512M
|
||||||
|
# 别让它写爆磁盘
|
||||||
|
LimitNOFILE=4096
|
||||||
|
|
||||||
|
StandardOutput=append:%(dir)s/relay.log
|
||||||
|
StandardError=append:%(dir)s/relay.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print("=" * 62)
|
||||||
|
print(" 部署到 Lightsail(13.54.30.196),端口 %s" % PORT)
|
||||||
|
print("=" * 62)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 1. 建目录 ──")
|
||||||
|
out = run("sudo mkdir -p %s %s && sudo chown -R admin:admin %s && "
|
||||||
|
"echo OK" % (REMOTE_DIR, DATA_DIR, REMOTE_DIR),
|
||||||
|
timeout=120)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 2. 传服务端文件 ──")
|
||||||
|
for name in FILES:
|
||||||
|
local = ROOT / "relay" / name
|
||||||
|
if not local.is_file():
|
||||||
|
print(" [跳过] %s 不存在" % name)
|
||||||
|
continue
|
||||||
|
upload(str(local), "%s/%s" % (REMOTE_DIR, name))
|
||||||
|
# 产品截图(官网要用)
|
||||||
|
shots = ROOT / "relay" / "site_shots"
|
||||||
|
if shots.is_dir():
|
||||||
|
run("mkdir -p %s/site_shots" % REMOTE_DIR, timeout=60, quiet=True)
|
||||||
|
for f in sorted(shots.iterdir()):
|
||||||
|
if f.is_file():
|
||||||
|
upload(str(f), "%s/site_shots/%s" % (REMOTE_DIR, f.name))
|
||||||
|
else:
|
||||||
|
print(" [警告] 没有 relay/site_shots,官网截图会 404")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 3. 写 systemd unit ──")
|
||||||
|
unit = UNIT % {"dir": REMOTE_DIR, "port": PORT}
|
||||||
|
tmp = Path(tempfile.gettempdir()) / "ykxmz-relay.service"
|
||||||
|
tmp.write_text(unit, encoding="utf-8", newline="\n")
|
||||||
|
upload(str(tmp), "/tmp/ykxmz-relay.service")
|
||||||
|
out = run("sudo mv /tmp/ykxmz-relay.service /etc/systemd/system/ && "
|
||||||
|
"sudo systemctl daemon-reload && echo ' unit 已装'",
|
||||||
|
timeout=120)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 4. 语法检查 + 启动 ──")
|
||||||
|
out = run(
|
||||||
|
"cd %s && for f in relay.py admin.py landing.py showcase.py; do "
|
||||||
|
"python3 -c \"import ast,sys;ast.parse(open(sys.argv[1],"
|
||||||
|
"encoding='utf-8').read())\" $f || exit 1; done && echo PY_OK"
|
||||||
|
% REMOTE_DIR, timeout=120)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
out = run("sudo systemctl enable ykxmz-relay >/dev/null 2>&1; "
|
||||||
|
"sudo systemctl restart ykxmz-relay; sleep 5; "
|
||||||
|
"systemctl is-active ykxmz-relay", timeout=180)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 5. 自测 ──")
|
||||||
|
out = run(
|
||||||
|
"echo \" 本机 health: $(curl -s -o /dev/null -w '%%{http_code}' "
|
||||||
|
"--max-time 8 http://127.0.0.1:%(p)s/v1/health)\"; "
|
||||||
|
"echo \" 监听: $(ss -tlnp 2>/dev/null | grep %(p)s | head -1)\"; "
|
||||||
|
"echo ' --- 启动日志 ---'; "
|
||||||
|
"tail -12 %(d)s/relay.log 2>/dev/null"
|
||||||
|
% {"p": PORT, "d": REMOTE_DIR}, timeout=120)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 6. 管理密码(登录后台用)──")
|
||||||
|
out = run("cat %s/data/admin_password.txt 2>/dev/null; echo; "
|
||||||
|
"cat %s/data/admin_key.txt 2>/dev/null" % (DATA_DIR, DATA_DIR),
|
||||||
|
timeout=60)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT)
|
||||||
|
print(" 之后公网地址就是 http://13.54.30.196:%s/" % PORT)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''在 Lightsail 上装好「监测单元 + 紧急预案」。
|
||||||
|
|
||||||
|
== 部署什么 ==
|
||||||
|
|
||||||
|
/opt/ykxmz-relay/monitor.sh 每 60 秒查一次公网
|
||||||
|
/opt/ykxmz-relay/rescue-lightsail.sh 四级阶梯抢救
|
||||||
|
/etc/systemd/system/ykxmz-monitor.service
|
||||||
|
/etc/systemd/system/ykxmz-monitor.timer
|
||||||
|
|
||||||
|
== 和随身 WiFi 那版的区别 ==
|
||||||
|
|
||||||
|
那版围着 Cloudflare 隧道转:查隧道连接数、重启隧道、重施 MSS/MTU。
|
||||||
|
新机器**有公网 IP、没有隧道**,所以:
|
||||||
|
|
||||||
|
* 健康检查超时从 12 秒降到 8 秒(直连,不需要等隧道)
|
||||||
|
* 预案第 1 级改成"重启中转服务"(不再有隧道可重启)
|
||||||
|
* 去掉 MTU/MSS 那两级(那是 4G 线路的问题)
|
||||||
|
|
||||||
|
== 为什么还是"每 60 秒 + 连续 3 次才动手" ==
|
||||||
|
|
||||||
|
直连比隧道稳得多,但**公网抖动**依然存在(上游线路、Lightsail 边缘)。
|
||||||
|
一次失败就救会天天瞎折腾,所以保留防抖。
|
||||||
|
现在预期是**基本不会触发** —— 触发了就说明真出事了。
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT / "_build"))
|
||||||
|
from lssh import run, upload # noqa: E402
|
||||||
|
|
||||||
|
RELAY_DIR = "/opt/ykxmz-relay"
|
||||||
|
PORT = "13245"
|
||||||
|
PUBLIC_URL = "http://13.54.30.196:%s" % PORT
|
||||||
|
|
||||||
|
MONITOR = r'''#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# 网站监测单元(Lightsail 版)
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# 每 60 秒从**公网**查一次,连续 3 次不通就叫紧急预案。
|
||||||
|
#
|
||||||
|
# 为什么不查 127.0.0.1:
|
||||||
|
# 本机 200 只说明进程活着。用户真正关心的是"打开
|
||||||
|
# http://13.54.30.196:13245 能不能出页面" —— 那要经过
|
||||||
|
# Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过:
|
||||||
|
# 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。
|
||||||
|
#
|
||||||
|
# 为什么用 timer 不用常驻进程:
|
||||||
|
# 崩了下一分钟自己就回来,不会有"跑久了状态变脏"的问题。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}"
|
||||||
|
PUBLIC_URL="${PUBLIC_URL:-__PUBLIC_URL__}"
|
||||||
|
|
||||||
|
FAIL_THRESHOLD="${FAIL_THRESHOLD:-3}"
|
||||||
|
COOLDOWN="${COOLDOWN:-600}"
|
||||||
|
CHECK_TIMEOUT="${CHECK_TIMEOUT:-8}"
|
||||||
|
|
||||||
|
STATE="$RELAY_DIR/monitor_state.txt"
|
||||||
|
LOG="$RELAY_DIR/monitor.log"
|
||||||
|
LOCKFILE="/tmp/ykxmz-monitor.lock"
|
||||||
|
|
||||||
|
if ! mkdir "$LOCKFILE" 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||||
|
|
||||||
|
FAILS=0
|
||||||
|
LAST_RESCUE=0
|
||||||
|
if [ -f "$STATE" ]; then
|
||||||
|
FAILS=$(grep -m1 '^fails=' "$STATE" 2>/dev/null | cut -d= -f2)
|
||||||
|
LAST_RESCUE=$(grep -m1 '^last_rescue=' "$STATE" 2>/dev/null | cut -d= -f2)
|
||||||
|
FAILS=${FAILS:-0}
|
||||||
|
LAST_RESCUE=${LAST_RESCUE:-0}
|
||||||
|
fi
|
||||||
|
NOW=$(date +%s)
|
||||||
|
|
||||||
|
CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time "$CHECK_TIMEOUT" \
|
||||||
|
"$PUBLIC_URL/v1/health" 2>/dev/null)
|
||||||
|
DUR=$(curl -s -o /dev/null -w '%{time_total}' --max-time "$CHECK_TIMEOUT" \
|
||||||
|
"$PUBLIC_URL/v1/health" 2>/dev/null)
|
||||||
|
DUR=${DUR:-0}
|
||||||
|
|
||||||
|
write_state() {
|
||||||
|
{
|
||||||
|
echo "fails=$1"
|
||||||
|
echo "last_rescue=$2"
|
||||||
|
echo "last_code=$CODE"
|
||||||
|
echo "last_check=$NOW"
|
||||||
|
echo "last_dur=$DUR"
|
||||||
|
} > "$STATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$CODE" = "200" ]; then
|
||||||
|
if [ "$FAILS" != "0" ]; then
|
||||||
|
log "公网恢复(HTTP 200,${DUR}s)—— 之前连续失败 $FAILS 次"
|
||||||
|
fi
|
||||||
|
PREV=$(grep -m1 '^last_code=' "$STATE" 2>/dev/null | cut -d= -f2)
|
||||||
|
[ "$PREV" != "200" ] && log "公网正常(HTTP 200,${DUR}s)"
|
||||||
|
write_state 0 "$LAST_RESCUE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
FAILS=$((FAILS + 1))
|
||||||
|
log "公网异常 第 $FAILS 次(HTTP ${CODE:-无响应},${DUR}s)"
|
||||||
|
write_state "$FAILS" "$LAST_RESCUE"
|
||||||
|
|
||||||
|
if [ "$FAILS" -lt "$FAIL_THRESHOLD" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SINCE=$((NOW - LAST_RESCUE))
|
||||||
|
if [ "$LAST_RESCUE" != "0" ] && [ "$SINCE" -lt "$COOLDOWN" ]; then
|
||||||
|
log " 已连续失败 $FAILS 次,但距上次抢救只过了 ${SINCE}s(冷却 ${COOLDOWN}s)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
HINT=""
|
||||||
|
if [ -f "$RELAY_DIR/rescue_knowledge.md" ]; then
|
||||||
|
HINT=$(grep -oE '第 [0-9] 级奏效' "$RELAY_DIR/rescue_knowledge.md" 2>/dev/null \
|
||||||
|
| sort | uniq -c | sort -rn | head -1 | grep -oE '[0-9]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "连续失败 $FAILS 次,启动紧急预案${HINT:+(知识库提示:第 $HINT 级常奏效)}"
|
||||||
|
write_state "$FAILS" "$NOW"
|
||||||
|
|
||||||
|
timeout 300 bash "$RELAY_DIR/rescue-lightsail.sh" \
|
||||||
|
"监测单元:公网连续 $FAILS 次不通" >> "$LOG" 2>&1
|
||||||
|
RC=$?
|
||||||
|
|
||||||
|
if [ "$RC" = "0" ]; then
|
||||||
|
log " 预案完成:已恢复"
|
||||||
|
else
|
||||||
|
log " 预案完成:仍未恢复(退出码 $RC),报告在 rescue_reports/"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
'''
|
||||||
|
|
||||||
|
SERVICE = '''[Unit]
|
||||||
|
# ykxmz 网站监测单元(Lightsail 版)
|
||||||
|
Description=ykxmz 网站监测单元(每 60 秒检查公网,异常时启动紧急预案)
|
||||||
|
After=network-online.target ykxmz-relay.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
TimeoutStartSec=360
|
||||||
|
|
||||||
|
# 用普通用户跑 —— 它只读状态、调 systemctl restart,不需要 root。
|
||||||
|
# (systemctl restart 那条走的是 polkit;admin 在 Lightsail 上有 sudo,
|
||||||
|
# 所以脚本里用的是不带 sudo 的 systemctl —— 见下面的说明。)
|
||||||
|
User=admin
|
||||||
|
Group=admin
|
||||||
|
|
||||||
|
Environment=RELAY_PORT=__PORT__
|
||||||
|
Environment=PUBLIC_URL=__PUBLIC_URL__
|
||||||
|
Environment=RESCUE_REBOOT=0
|
||||||
|
Environment=FAIL_THRESHOLD=3
|
||||||
|
Environment=COOLDOWN=600
|
||||||
|
|
||||||
|
ExecStart=/bin/bash __DIR__/monitor.sh
|
||||||
|
|
||||||
|
StandardOutput=append:__DIR__/monitor.log
|
||||||
|
StandardError=append:__DIR__/monitor.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
'''
|
||||||
|
|
||||||
|
TIMER = '''[Unit]
|
||||||
|
Description=每 60 秒检查一次网站是否可用
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=45s
|
||||||
|
OnUnitActiveSec=60s
|
||||||
|
AccuracySec=5s
|
||||||
|
Unit=ykxmz-monitor.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print("=" * 62)
|
||||||
|
print(" 在 Lightsail 上装监测单元 + 紧急预案")
|
||||||
|
print("=" * 62)
|
||||||
|
|
||||||
|
monitor = (MONITOR.replace("__PUBLIC_URL__", PUBLIC_URL))
|
||||||
|
tmp = Path(tempfile.gettempdir())
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 1. 传脚本 ──")
|
||||||
|
mp = tmp / "monitor.sh"
|
||||||
|
mp.write_text(monitor, encoding="utf-8", newline="\n")
|
||||||
|
upload(str(mp), "%s/monitor.sh" % RELAY_DIR, mode=0o755)
|
||||||
|
|
||||||
|
rp = ROOT / "relay" / "rescue-lightsail.sh"
|
||||||
|
if rp.is_file():
|
||||||
|
upload(str(rp), "%s/rescue-lightsail.sh" % RELAY_DIR, mode=0o755)
|
||||||
|
else:
|
||||||
|
print(" ✗ 找不到 relay/rescue-lightsail.sh")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 2. 传 systemd 单元 ──")
|
||||||
|
sp = tmp / "ykxmz-monitor.service"
|
||||||
|
sp.write_text(SERVICE.replace("__PORT__", PORT)
|
||||||
|
.replace("__PUBLIC_URL__", PUBLIC_URL)
|
||||||
|
.replace("__DIR__", RELAY_DIR),
|
||||||
|
encoding="utf-8", newline="\n")
|
||||||
|
upload(str(sp), "/tmp/ykxmz-monitor.service")
|
||||||
|
tp = tmp / "ykxmz-monitor.timer"
|
||||||
|
tp.write_text(TIMER, encoding="utf-8", newline="\n")
|
||||||
|
upload(str(tp), "/tmp/ykxmz-monitor.timer")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 3. 装单元 + 建目录 ──")
|
||||||
|
out = run(
|
||||||
|
"sudo mv /tmp/ykxmz-monitor.service /tmp/ykxmz-monitor.timer "
|
||||||
|
"/etc/systemd/system/ && "
|
||||||
|
"mkdir -p %s/rescue_reports && chmod +x %s/*.sh && "
|
||||||
|
"if [ ! -f %s/rescue_knowledge.md ]; then "
|
||||||
|
"printf '# 紧急预案知识库\\n\\n每次自动抢救成功会记一条「第几级奏效」。"
|
||||||
|
"\\n下次同类症状优先试历史上奏效的那一级。\\n' > %s/rescue_knowledge.md; "
|
||||||
|
"fi && sudo systemctl daemon-reload && echo OK"
|
||||||
|
% (RELAY_DIR, RELAY_DIR, RELAY_DIR, RELAY_DIR), timeout=180)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 4. 启用定时器 ──")
|
||||||
|
out = run("sudo systemctl enable --now ykxmz-monitor.timer 2>&1 | tail -2; "
|
||||||
|
"sleep 2; systemctl is-active ykxmz-monitor.timer",
|
||||||
|
timeout=180)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 5. 手动跑一次监测(验证它能工作)──")
|
||||||
|
out = run("bash %s/monitor.sh; echo '--- 状态 ---'; "
|
||||||
|
"cat %s/monitor_state.txt 2>/dev/null; "
|
||||||
|
"echo '--- 日志 ---'; tail -3 %s/monitor.log 2>/dev/null"
|
||||||
|
% (RELAY_DIR, RELAY_DIR, RELAY_DIR), timeout=180)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 6. 演练预案(网站正常时应该什么都不做)──")
|
||||||
|
out = run("bash %s/rescue-lightsail.sh '安装验证(网站正常)' 2>&1 | tail -6; "
|
||||||
|
"echo '--- 报告数(应该是 0)---'; "
|
||||||
|
"ls %s/rescue_reports 2>/dev/null | wc -l"
|
||||||
|
% (RELAY_DIR, RELAY_DIR), timeout=180)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" ── 7. 总览 ──")
|
||||||
|
out = run(
|
||||||
|
"printf ' %-26s %s\\n' 'ykxmz-relay' \"$(systemctl is-active ykxmz-relay)\"; "
|
||||||
|
"printf ' %-26s %s\\n' 'ykxmz-monitor.timer' \"$(systemctl is-active ykxmz-monitor.timer)\"; "
|
||||||
|
"printf ' %-26s %s\\n' '本机 health' \"$(curl -s -o /dev/null -w '%%{http_code}' http://127.0.0.1:%s/v1/health)\"; "
|
||||||
|
"printf ' %-26s %s\\n' '公网 health' \"$(curl -s -o /dev/null -w '%%{http_code}' %s/v1/health)\""
|
||||||
|
% (PORT, PUBLIC_URL), timeout=180)
|
||||||
|
for ln in out[1].strip().splitlines():
|
||||||
|
print(" " + ln)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 说明:预案里用 systemctl 重启服务,而 monitor.service 以 admin")
|
||||||
|
print(" 身份跑 —— admin 在 Lightsail 上有免密 sudo,但脚本里没加")
|
||||||
|
print(" sudo。如果实测发现重启失败,把它改成 sudo systemctl 即可。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''连 AWS Lightsail 那台新服务器的 SSH 助手。
|
||||||
|
|
||||||
|
== 为什么单独一个模块,不改 devssh.py ==
|
||||||
|
|
||||||
|
devssh.py 是连**随身 WiFi 那台 4G 小机器**的(192.168.2.156,密码登录)。
|
||||||
|
那台还在跑(网站暂时还挂在上面),不能动它的连接逻辑。
|
||||||
|
|
||||||
|
新服务器用**密钥**登录、IP 是公网 13.54.30.196,认证方式完全不同,
|
||||||
|
所以单独一个模块更清楚。
|
||||||
|
|
||||||
|
== 用法 ==
|
||||||
|
|
||||||
|
from lssh import run, upload, download, probe
|
||||||
|
run("uname -a") # 跑命令
|
||||||
|
upload("relay/relay.py", "/opt/ykxmz-relay/relay.py")
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
HOST = os.environ.get("YKXMZ_LS_HOST", "13.54.30.196")
|
||||||
|
USER = os.environ.get("YKXMZ_LS_USER", "admin")
|
||||||
|
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
|
||||||
|
KEY = Path(os.environ.get(
|
||||||
|
"YKXMZ_LS_KEY", str(Path.home() / ".ssh" / "lightsail-ykxmz.pem")))
|
||||||
|
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
|
def _connect(timeout: int = 25):
|
||||||
|
global _client
|
||||||
|
if _client is not None:
|
||||||
|
try:
|
||||||
|
# 探一下还活着没
|
||||||
|
_client.exec_command("true", timeout=5)
|
||||||
|
return _client
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
if not KEY.is_file():
|
||||||
|
raise RuntimeError(
|
||||||
|
"找不到 SSH 密钥:%s\n"
|
||||||
|
"(把 Lightsail 的 .pem 放到那里,或者设 YKXMZ_LS_KEY)" % KEY)
|
||||||
|
|
||||||
|
cli = paramiko.SSHClient()
|
||||||
|
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
cli.connect(HOST, port=PORT, username=USER,
|
||||||
|
key_filename=str(KEY), timeout=timeout,
|
||||||
|
allow_agent=False, look_for_keys=False,
|
||||||
|
banner_timeout=30, auth_timeout=30)
|
||||||
|
_client = cli
|
||||||
|
return cli
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: str, timeout: int = 300, quiet: bool = False) -> tuple:
|
||||||
|
"""跑一条命令,返回 (退出码, 输出)。"""
|
||||||
|
cli = _connect()
|
||||||
|
if not quiet:
|
||||||
|
print(" [ls] %s" % command.splitlines()[0][:90])
|
||||||
|
stdin, stdout, stderr = cli.exec_command(command, timeout=timeout,
|
||||||
|
get_pty=False)
|
||||||
|
out = stdout.read().decode("utf-8", "replace")
|
||||||
|
err = stderr.read().decode("utf-8", "replace")
|
||||||
|
code = stdout.channel.recv_exit_status()
|
||||||
|
return code, (out + err)
|
||||||
|
|
||||||
|
|
||||||
|
def upload(local: str, remote: str, mode: int = 0o644) -> None:
|
||||||
|
"""传一个文件上去。"""
|
||||||
|
cli = _connect()
|
||||||
|
sftp = cli.open_sftp()
|
||||||
|
try:
|
||||||
|
# 先把远端目录建出来
|
||||||
|
d = os.path.dirname(remote)
|
||||||
|
if d:
|
||||||
|
try:
|
||||||
|
sftp.stat(d)
|
||||||
|
except IOError:
|
||||||
|
# 逐级建
|
||||||
|
parts = d.strip("/").split("/")
|
||||||
|
cur = ""
|
||||||
|
for p in parts:
|
||||||
|
cur += "/" + p
|
||||||
|
try:
|
||||||
|
sftp.stat(cur)
|
||||||
|
except IOError:
|
||||||
|
try:
|
||||||
|
sftp.mkdir(cur)
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
sftp.put(local, remote)
|
||||||
|
sftp.chmod(remote, mode)
|
||||||
|
finally:
|
||||||
|
sftp.close()
|
||||||
|
print(" 已上传 %s -> %s" % (os.path.basename(local), remote))
|
||||||
|
|
||||||
|
|
||||||
|
def download(remote: str, local: str) -> None:
|
||||||
|
cli = _connect()
|
||||||
|
sftp = cli.open_sftp()
|
||||||
|
try:
|
||||||
|
Path(local).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
sftp.get(remote, local)
|
||||||
|
finally:
|
||||||
|
sftp.close()
|
||||||
|
print(" 已下载 %s -> %s" % (remote, local))
|
||||||
|
|
||||||
|
|
||||||
|
def probe() -> str | None:
|
||||||
|
"""通不通。返回一句话描述,或 None。"""
|
||||||
|
try:
|
||||||
|
code, out = run("uname -sm; nproc; free -m | sed -n 2p",
|
||||||
|
timeout=30, quiet=True)
|
||||||
|
if code != 0:
|
||||||
|
return None
|
||||||
|
return out.strip().replace("\n", " | ")
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return "连接失败:%s" % exc
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(" 目标: %s@%s:%d 密钥 %s" % (USER, HOST, PORT, KEY))
|
||||||
|
print(" " + (probe() or "不通"))
|
||||||
@@ -17,7 +17,7 @@ import zipfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||||
VERSION = "2.1.2"
|
VERSION = "2.1.3"
|
||||||
NAME = f"ykxmz-{VERSION}"
|
NAME = f"ykxmz-{VERSION}"
|
||||||
|
|
||||||
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''验证 APK 里真的开了明文流量。
|
||||||
|
|
||||||
|
Android 的 usesCleartextTraffic 是编译进 binary manifest 的,
|
||||||
|
光看源码不算数 —— 万一构建脚本没把新清单打进去,源码改对了也没用。
|
||||||
|
所以直接解 APK 看里面的 AndroidManifest。
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
apk = ROOT / "dist" / "ykxmz-watch-2.1.2.apk"
|
||||||
|
if not apk.is_file():
|
||||||
|
print(" ✗ 找不到 %s" % apk)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(" APK: %s(%d 字节)" % (apk.name, apk.stat().st_size))
|
||||||
|
with zipfile.ZipFile(apk) as z:
|
||||||
|
blob = z.read("AndroidManifest.xml")
|
||||||
|
|
||||||
|
# binary manifest 里字符串以 UTF-16LE 存在字符串池里
|
||||||
|
# 简单办法:直接在字节里找关键词
|
||||||
|
text = blob.decode("utf-16-le", "ignore")
|
||||||
|
for key in ("usesCleartextTraffic", "uses-permission", "INTERNET",
|
||||||
|
"com.lanremote.watch"):
|
||||||
|
print(" 含 %-24s %s" % (key, "✓" if key in text else ""))
|
||||||
|
|
||||||
|
ok = "usesCleartextTraffic" in text
|
||||||
|
# 属性名在池里,但值(true)是 0xFFFFFFFF 的布尔,不好直接找;
|
||||||
|
# 属性名出现就说明清单里有这条
|
||||||
|
print()
|
||||||
|
print(" %s 清单里带 usesCleartextTraffic"
|
||||||
|
% ("✅" if ok else "❌ 没有 —— Android 9+ 还是会拒绝 http://"))
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -23,7 +23,7 @@ def _latest_apk() -> Path:
|
|||||||
(ROOT / "dist").glob("*watch*.apk"),
|
(ROOT / "dist").glob("*watch*.apk"),
|
||||||
key=lambda p: p.stat().st_mtime, reverse=True)
|
key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
return candidates[0] if candidates else \
|
return candidates[0] if candidates else \
|
||||||
ROOT / "dist" / "ykxmz-watch-2.1.2.apk"
|
ROOT / "dist" / "ykxmz-watch-2.1.3.apk"
|
||||||
|
|
||||||
|
|
||||||
APK = _latest_apk()
|
APK = _latest_apk()
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
|
|||||||
# 被控端 -> 一个 zip
|
# 被控端 -> 一个 zip
|
||||||
# 服务器端 -> 一个 zip
|
# 服务器端 -> 一个 zip
|
||||||
# 控制端 -> 直接给 APK(不打 zip)
|
# 控制端 -> 直接给 APK(不打 zip)
|
||||||
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.2.zip"
|
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.3.zip"
|
||||||
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.2.zip"
|
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.3.zip"
|
||||||
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.2.apk"
|
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.3.apk"
|
||||||
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
|
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
|
||||||
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
|
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
|
||||||
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
|
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
|
||||||
@@ -89,7 +89,7 @@ def main() -> int:
|
|||||||
for label, path in (("被控端", agent_dir / "README-agent.txt"),
|
for label, path in (("被控端", agent_dir / "README-agent.txt"),
|
||||||
("服务器端", relay_dir / "README-server.txt"),
|
("服务器端", relay_dir / "README-server.txt"),
|
||||||
("控制端(旁边那份)",
|
("控制端(旁边那份)",
|
||||||
ROOT / "dist" / "ykxmz-watch-2.1.2-说明.txt")):
|
ROOT / "dist" / "ykxmz-watch-2.1.3-说明.txt")):
|
||||||
raw = path.read_bytes() if path.is_file() else b""
|
raw = path.read_bytes() if path.is_file() else b""
|
||||||
check(f"{label}说明有 UTF-8 BOM",
|
check(f"{label}说明有 UTF-8 BOM",
|
||||||
raw[:3] == b"\xef\xbb\xbf", raw[:3])
|
raw[:3] == b"\xef\xbb\xbf", raw[:3])
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.lanremote.watch"
|
package="com.lanremote.watch"
|
||||||
android:versionCode="5"
|
android:versionCode="5"
|
||||||
android:versionName="2.1.2">
|
android:versionName="2.1.3">
|
||||||
|
|
||||||
|
<!-- ⚠ 允许明文 HTTP。
|
||||||
|
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
|
||||||
|
2026-10-08 之后那台有公网 IP 的服务器(http://13.54.30.196:13245)。
|
||||||
|
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
|
||||||
|
直接掐掉,日志里只有一句含糊的
|
||||||
|
"Cleartext HTTP traffic to ... not permitted"。
|
||||||
|
|
||||||
|
安全性说明:中转上跑的内容(配对令牌、画面、文件)本来就是
|
||||||
|
加密过的,明文只暴露 URL 和设备名。要更严的话后续上 HTTPS
|
||||||
|
(Let's Encrypt),那时可以把这一条去掉。 -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
@@ -31,6 +41,7 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ $d8 = Join-Path $bt "d8.bat"
|
|||||||
|
|
||||||
$work = Join-Path $android "build"
|
$work = Join-Path $android "build"
|
||||||
$outDir = Join-Path $root "dist"
|
$outDir = Join-Path $root "dist"
|
||||||
$apkName = "ykxmz-watch-2.1.2.apk"
|
$apkName = "ykxmz-watch-2.1.3.apk"
|
||||||
|
|
||||||
function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan }
|
function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan }
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ $unsigned = Join-Path $work "app-unsigned.apk"
|
|||||||
--min-sdk-version 24 `
|
--min-sdk-version 24 `
|
||||||
--target-sdk-version 34 `
|
--target-sdk-version 34 `
|
||||||
--version-code 5 `
|
--version-code 5 `
|
||||||
--version-name "2.1.2" `
|
--version-name "2.1.3" `
|
||||||
--auto-add-overlay
|
--auto-add-overlay
|
||||||
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
|
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,33 @@ public class RelayTransport implements Transport {
|
|||||||
url = url.substring(0, url.length() - 1);
|
url = url.substring(0, url.length() - 1);
|
||||||
}
|
}
|
||||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
url = "https://" + url;
|
// 没写协议头时猜一个。规则要**和 Windows 端一致**
|
||||||
|
// (windows/lanremote/relay.py 的 _guess_scheme):
|
||||||
|
//
|
||||||
|
// IP / localhost / *.local -> http://
|
||||||
|
// 域名 -> https://
|
||||||
|
//
|
||||||
|
// 为什么纯 IP 走明文:IP 地址的中转基本都是自己搭的
|
||||||
|
// (局域网那台,或者像现在这样一台有公网 IP 的服务器),
|
||||||
|
// 上面往往没有证书。原来一律加 https:// 的结果是**永远连不上**,
|
||||||
|
// 而且报的是 SSL 错误,会把人带去查证书查半天。
|
||||||
|
//
|
||||||
|
// ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail
|
||||||
|
// 之后中转就是 http://13.54.30.196:13245,
|
||||||
|
// 只把 192.168/10./172.16 当私有的话这里会走错。
|
||||||
|
String hostOnly = url;
|
||||||
|
int slash = hostOnly.indexOf('/');
|
||||||
|
if (slash > 0) {
|
||||||
|
hostOnly = hostOnly.substring(0, slash);
|
||||||
|
}
|
||||||
|
int colon = hostOnly.indexOf(':');
|
||||||
|
if (colon > 0) {
|
||||||
|
hostOnly = hostOnly.substring(0, colon);
|
||||||
|
}
|
||||||
|
boolean plain = hostOnly.equalsIgnoreCase("localhost")
|
||||||
|
|| hostOnly.toLowerCase().endsWith(".local")
|
||||||
|
|| hostOnly.matches("\\d{1,3}(\\.\\d{1,3}){3}");
|
||||||
|
url = (plain ? "http://" : "https://") + url;
|
||||||
}
|
}
|
||||||
this.baseUrl = url;
|
this.baseUrl = url;
|
||||||
this.pairingCode = pairingCode == null ? "" : pairingCode.trim();
|
this.pairingCode = pairingCode == null ? "" : pairingCode.trim();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
软件名称:ykxmz
|
软件名称:ykxmz
|
||||||
版本:2.1.2
|
版本:2.1.3
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
|
|
||||||
装好之后看这一页:怎么确认它在跑、怎么改设置、
|
装好之后看这一页:怎么确认它在跑、怎么改设置、
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
八、版本记录
|
八、版本记录
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
2.1.2
|
2.1.3
|
||||||
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
|
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
|
||||||
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
|
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
|
||||||
合计 10 条连接),一种协议犯病另一种还在服务
|
合计 10 条连接),一种协议犯病另一种还在服务
|
||||||
@@ -218,6 +218,6 @@
|
|||||||
|
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
软件:ykxmz 2.1.2
|
软件:ykxmz 2.1.3
|
||||||
许可条款见《许可协议》(LICENSE.txt)
|
许可条款见《许可协议》(LICENSE.txt)
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
软件名称:ykxmz
|
软件名称:ykxmz
|
||||||
版本:2.1.2
|
版本:2.1.3
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
|
|
||||||
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
|
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
|
||||||
@@ -164,6 +164,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
|||||||
|
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
软件:ykxmz 2.1.2
|
软件:ykxmz 2.1.3
|
||||||
继续安装,即表示你已阅读并同意《许可协议》。
|
继续安装,即表示你已阅读并同意《许可协议》。
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
软件名称:ykxmz(远程管理工具)
|
软件名称:ykxmz(远程管理工具)
|
||||||
版本:2.1.2
|
版本:2.1.3
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
生效日期:2026 年 10 月 5 日
|
生效日期:2026 年 10 月 5 日
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
> 都不许拿去做下面第三条那些事。**
|
> 都不许拿去做下面第三条那些事。**
|
||||||
>
|
>
|
||||||
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
|
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
|
||||||
> 那和 AGPL 冲突,所以 2.1.2 起拆开了:代码授权归 LICENSE,
|
> 那和 AGPL 冲突,所以 2.1.3 起拆开了:代码授权归 LICENSE,
|
||||||
> 使用约束归这份文件。)
|
> 使用约束归这份文件。)
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
|
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
软件:ykxmz 2.1.2
|
软件:ykxmz 2.1.3
|
||||||
|
|
||||||
如果你不同意本条款的任何一条,请不要安装或使用本软件。
|
如果你不同意本条款的任何一条,请不要安装或使用本软件。
|
||||||
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
|
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
软件名称:ykxmz(远程管理工具)
|
软件名称:ykxmz(远程管理工具)
|
||||||
版本:2.1.2
|
版本:2.1.3
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
生效日期:2026 年 10 月 5 日
|
生效日期:2026 年 10 月 5 日
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
> 都不许拿去做下面第三条那些事。**
|
> 都不许拿去做下面第三条那些事。**
|
||||||
>
|
>
|
||||||
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
|
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
|
||||||
> 那和 AGPL 冲突,所以 2.1.2 起拆开了:代码授权归 LICENSE,
|
> 那和 AGPL 冲突,所以 2.1.3 起拆开了:代码授权归 LICENSE,
|
||||||
> 使用约束归这份文件。)
|
> 使用约束归这份文件。)
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
|
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
软件:ykxmz 2.1.2
|
软件:ykxmz 2.1.3
|
||||||
|
|
||||||
如果你不同意本条款的任何一条,请不要安装或使用本软件。
|
如果你不同意本条款的任何一条,请不要安装或使用本软件。
|
||||||
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
|
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
软件名称:ykxmz
|
软件名称:ykxmz
|
||||||
版本:2.1.2
|
版本:2.1.3
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
|
|
||||||
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
|
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
|
||||||
@@ -334,7 +334,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
|||||||
|
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
软件:ykxmz 2.1.2
|
软件:ykxmz 2.1.3
|
||||||
继续安装,即表示你已阅读并同意《许可协议》。
|
继续安装,即表示你已阅读并同意《许可协议》。
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
|||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
软件名称:ykxmz
|
软件名称:ykxmz
|
||||||
版本:2.1.2
|
版本:2.1.3
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
|
|
||||||
装好之后看这一页:怎么确认它在跑、怎么改设置、
|
装好之后看这一页:怎么确认它在跑、怎么改设置、
|
||||||
@@ -539,7 +539,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
|||||||
八、版本记录
|
八、版本记录
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
2.1.2
|
2.1.3
|
||||||
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
|
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
|
||||||
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
|
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
|
||||||
合计 10 条连接),一种协议犯病另一种还在服务
|
合计 10 条连接),一种协议犯病另一种还在服务
|
||||||
@@ -560,6 +560,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
|||||||
|
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
作者:小麦子
|
作者:小麦子
|
||||||
软件:ykxmz 2.1.2
|
软件:ykxmz 2.1.3
|
||||||
许可条款见《许可协议》(LICENSE.txt)
|
许可条款见《许可协议》(LICENSE.txt)
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@
|
|||||||
; ============================================================================
|
; ============================================================================
|
||||||
|
|
||||||
#define MyAppName "ykxmz"
|
#define MyAppName "ykxmz"
|
||||||
#define MyAppVersion "2.1.2"
|
#define MyAppVersion "2.1.3"
|
||||||
#define MyAppPublisher "小麦子"
|
#define MyAppPublisher "小麦子"
|
||||||
#define MyAppExeName "ykxmz.exe"
|
#define MyAppExeName "ykxmz.exe"
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ Filename: "{app}\{#MyAppExeName}"; Parameters: "--autostart on"; \
|
|||||||
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
|
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
|
||||||
; (PowerShell 的 Start-Process -Wait 就是)。
|
; (PowerShell 的 Start-Process -Wait 就是)。
|
||||||
; 学校批量部署请用 cmd:
|
; 学校批量部署请用 cmd:
|
||||||
; cmd /c ykxmz-setup-2.1.2.exe /VERYSILENT /CODE=123456
|
; cmd /c ykxmz-setup-2.1.3.exe /VERYSILENT /CODE=123456
|
||||||
; cmd 只等安装器本身,不等孙子进程,不会卡。
|
; cmd 只等安装器本身,不等孙子进程,不会卡。
|
||||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||||
Description: "立即启动(后台运行,不显示窗口)"; \
|
Description: "立即启动(后台运行,不显示窗口)"; \
|
||||||
|
|||||||
+21
-2
@@ -59,13 +59,27 @@ from collections import deque
|
|||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VERSION = "2.1.2"
|
VERSION = "2.1.3"
|
||||||
|
|
||||||
# ---------------- 参数(都能用环境变量覆盖)----------------
|
# ---------------- 参数(都能用环境变量覆盖)----------------
|
||||||
|
|
||||||
PORT = int(os.environ.get("RELAY_PORT", "8080"))
|
PORT = int(os.environ.get("RELAY_PORT", "8080"))
|
||||||
BIND = os.environ.get("RELAY_BIND", "127.0.0.1")
|
BIND = os.environ.get("RELAY_BIND", "127.0.0.1")
|
||||||
DATA_DIR = Path(os.environ.get("RELAY_DATA", "/root/lanremote-relay"))
|
|
||||||
|
# ⚠ 数据目录的默认值曾经**写死成 /root/lanremote-relay**。
|
||||||
|
# 原来那台机器上没问题(服务就跑在 /root 下、以 root 跑),
|
||||||
|
# 但 2026-10-08 搬到 Lightsail 之后立刻炸了:
|
||||||
|
#
|
||||||
|
# PermissionError: [Errno 13] Permission denied: '/root/lanremote-relay'
|
||||||
|
#
|
||||||
|
# 新机器上服务以 admin 普通用户跑(它没理由要 root),
|
||||||
|
# 而 /root 是 0700 —— mkdir 直接被拒。
|
||||||
|
#
|
||||||
|
# 改成本脚本所在目录下的 data/:搬到哪都跟着走,
|
||||||
|
# 不用每换一台机器就记得设环境变量。
|
||||||
|
# (要用别的路径照样可以,RELAY_DATA 优先级更高。)
|
||||||
|
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
DATA_DIR = Path(os.environ.get("RELAY_DATA", str(_SCRIPT_DIR / "data")))
|
||||||
|
|
||||||
# 长轮询最多挂多久。太长会占着 Cloudflare 的连接,太短则空转请求多。
|
# 长轮询最多挂多久。太长会占着 Cloudflare 的连接,太短则空转请求多。
|
||||||
# 长轮询挂多久。
|
# 长轮询挂多久。
|
||||||
@@ -375,6 +389,11 @@ def public_url() -> str:
|
|||||||
candidates = [
|
candidates = [
|
||||||
DATA_DIR.parent / "public_url.txt",
|
DATA_DIR.parent / "public_url.txt",
|
||||||
DATA_DIR / "public_url.txt",
|
DATA_DIR / "public_url.txt",
|
||||||
|
# 旧那台机器把 public_url.txt 放在 /root/lanremote-relay/。
|
||||||
|
# 保留这条是为了**从旧机器迁过来时**还能读到已有的地址;
|
||||||
|
# 新机器上这个路径根本不存在,is_file() 会直接跳过,无害。
|
||||||
|
# (之前 DATA_DIR 默认值也写死成这个路径,搬迁时踩过 ——
|
||||||
|
# 见文件顶部 DATA_DIR 那段注释。)
|
||||||
Path("/root/lanremote-relay/public_url.txt"),
|
Path("/root/lanremote-relay/public_url.txt"),
|
||||||
]
|
]
|
||||||
for path in candidates:
|
for path in candidates:
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# 紧急预案(Lightsail 版)—— 网站打不开时按阶梯自动抢救
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# 和随身 WiFi 那版的区别:
|
||||||
|
#
|
||||||
|
# 那版是**围着 Cloudflare 隧道**转的(第 1 级就是重启隧道、
|
||||||
|
# 第 3 级重施 MSS/MTU 调优)。现在这台新机器**有公网 IP**,
|
||||||
|
# 中转直接对外监听 13245,没有任何隧道 —— 所以:
|
||||||
|
#
|
||||||
|
# * 第 1 级从"重启隧道"改成"重启中转服务"
|
||||||
|
# * 去掉 MSS/MTU 那两级(那是 4G 线路的问题,这里没有)
|
||||||
|
# * 加一级"看端口有没有被占/进程死没死"
|
||||||
|
#
|
||||||
|
# 保留下来的核心设计没变:
|
||||||
|
#
|
||||||
|
# 1. **先记录现场,再动手**(重启会把证据冲掉)
|
||||||
|
# 2. **阶梯式,每级都验**:从代价最小到最大,好了就停
|
||||||
|
# 3. **全失败要留证据**:生成 txt 报告,后台可下载
|
||||||
|
# 4. **成功的招数记进知识库**:下次同类问题优先试
|
||||||
|
#
|
||||||
|
# 谁调用:monitor.sh(连续 N 次不通就调这里)
|
||||||
|
# 也可以人工跑:bash /opt/ykxmz-relay/rescue.sh
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}"
|
||||||
|
PORT="${RELAY_PORT:-13245}"
|
||||||
|
# 公网健康检查地址。没有域名就填 IP。
|
||||||
|
PUBLIC_URL="${PUBLIC_URL:-http://13.54.30.196:$PORT}"
|
||||||
|
REPORTS_DIR="$RELAY_DIR/rescue_reports"
|
||||||
|
KNOWLEDGE="$RELAY_DIR/rescue_knowledge.md"
|
||||||
|
LOG="$RELAY_DIR/rescue.log"
|
||||||
|
LOCKFILE="/tmp/ykxmz-rescue.lock"
|
||||||
|
|
||||||
|
SETTLE_FAST=6
|
||||||
|
SETTLE_SLOW=15
|
||||||
|
|
||||||
|
# 是否允许"重启整机"这张底牌。默认关。
|
||||||
|
RESCUE_REBOOT="${RESCUE_REBOOT:-0}"
|
||||||
|
|
||||||
|
mkdir -p "$REPORTS_DIR"
|
||||||
|
|
||||||
|
if ! mkdir "$LOCKFILE" 2>/dev/null; then
|
||||||
|
echo "[rescue] 已经有一个预案在跑,本次退出"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
REPORT="$REPORTS_DIR/$STAMP.txt"
|
||||||
|
|
||||||
|
say() {
|
||||||
|
echo "$*"
|
||||||
|
echo "$(date '+%F %T') $*" >> "$LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 公网健康检查 —— **看公网,不看本机**
|
||||||
|
is_healthy() {
|
||||||
|
local code
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \
|
||||||
|
"$PUBLIC_URL/v1/health" 2>/dev/null)
|
||||||
|
[ "$code" = "200" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
local_health() {
|
||||||
|
local code
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 6 \
|
||||||
|
"http://127.0.0.1:$PORT/v1/health" 2>/dev/null)
|
||||||
|
[ "$code" = "200" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 现场记录
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
collect() {
|
||||||
|
{
|
||||||
|
echo "================================================================================"
|
||||||
|
echo " ykxmz 紧急预案诊断报告"
|
||||||
|
echo " 时间:$(date '+%F %T %Z')"
|
||||||
|
echo " 主机:$(hostname) 运行时长:$(uptime -p 2>/dev/null || uptime)"
|
||||||
|
echo " 触发原因:$1"
|
||||||
|
echo "================================================================================"
|
||||||
|
echo
|
||||||
|
echo "---- 一、结论速览 ----"
|
||||||
|
echo
|
||||||
|
echo " 公网 $PUBLIC_URL/v1/health -> $(is_healthy && echo 200-正常 || echo 不通)"
|
||||||
|
echo " 本机 http://127.0.0.1:$PORT/v1/health -> $(local_health && echo 200-正常 || echo 不通)"
|
||||||
|
echo
|
||||||
|
if local_health && ! is_healthy; then
|
||||||
|
echo " 判断:**本机是好的,问题在公网那一侧**(防火墙规则、安全组、上游线路)。"
|
||||||
|
elif ! local_health; then
|
||||||
|
echo " 判断:**本机的中转服务就没起来**,先看第二、三节。"
|
||||||
|
else
|
||||||
|
echo " 判断:两边都通(可能是刚才已经自愈,或者只是偶发抖动)。"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "---- 二、服务状态 ----"
|
||||||
|
echo
|
||||||
|
printf ' %-24s %s\n' "ykxmz-relay" "$(systemctl is-active ykxmz-relay 2>&1)"
|
||||||
|
printf ' %-24s %s\n' "ykxmz-monitor.timer" "$(systemctl is-active ykxmz-monitor.timer 2>&1)"
|
||||||
|
echo
|
||||||
|
echo " --- 最近 20 行服务日志 ---"
|
||||||
|
journalctl -u ykxmz-relay --no-pager -n 20 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "---- 三、进程与端口 ----"
|
||||||
|
echo
|
||||||
|
echo " 监听 $PORT 的进程:"
|
||||||
|
ss -tlnp 2>/dev/null | grep ":$PORT " | sed 's/^/ /' \
|
||||||
|
|| echo " **没有任何进程在监听 $PORT**"
|
||||||
|
echo
|
||||||
|
echo " 相关进程:"
|
||||||
|
ps -eo pid,rss,etime,cmd --sort=-rss 2>/dev/null \
|
||||||
|
| grep -E 'relay\.py|python3' | grep -v grep | head -6 | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "---- 四、系统资源 ----"
|
||||||
|
echo
|
||||||
|
free -m 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
uptime | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
df -h / /opt 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "---- 五、网络 ----"
|
||||||
|
echo
|
||||||
|
ip -brief addr show 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo " 默认路由:"
|
||||||
|
ip route show default 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo " 本机自测各端口:"
|
||||||
|
for p in "$PORT" 80 443 22; do
|
||||||
|
printf ' %-6s ' "$p"
|
||||||
|
timeout 3 bash -c "</dev/tcp/127.0.0.1/$p" 2>/dev/null \
|
||||||
|
&& echo "通" || echo "不通"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "---- 六、日志尾部 ----"
|
||||||
|
echo
|
||||||
|
echo "### 中转服务 relay.log"
|
||||||
|
tail -30 "$RELAY_DIR/relay.log" 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "### 监测单元 monitor.log"
|
||||||
|
tail -20 "$RELAY_DIR/monitor.log" 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "### 内核异常(OOM / 硬件)"
|
||||||
|
dmesg -T 2>/dev/null \
|
||||||
|
| grep -i -E 'oom|killed process|error|panic' | tail -15 | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "---- 七、最近一次启动 ----"
|
||||||
|
echo
|
||||||
|
who -b 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo " 运行时长:$(cut -d. -f1 /proc/uptime 2>/dev/null) 秒"
|
||||||
|
echo
|
||||||
|
echo "================================================================================"
|
||||||
|
echo " 报告结束。可以在中转站后台的「抢救报告」分区下载。"
|
||||||
|
echo "================================================================================"
|
||||||
|
} > "$REPORT" 2>&1
|
||||||
|
say " 现场已记录 -> $REPORT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 阶梯
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
step_1_restart_relay() {
|
||||||
|
say " [1/4] 重启中转服务(最轻的一招,不动系统其他部分)"
|
||||||
|
systemctl restart ykxmz-relay 2>/dev/null
|
||||||
|
sleep "$SETTLE_FAST"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_2_kill_and_start() {
|
||||||
|
say " [2/4] 强杀残留进程再拉起(进程卡死/端口被占时用)"
|
||||||
|
# 有时候 systemd restart 会因为老进程没退干净而失败,
|
||||||
|
# 表现为"服务说 active 但端口没人听"。
|
||||||
|
pkill -f "$RELAY_DIR/relay.py" 2>/dev/null
|
||||||
|
sleep 3
|
||||||
|
systemctl reset-failed ykxmz-relay 2>/dev/null
|
||||||
|
systemctl start ykxmz-relay 2>/dev/null
|
||||||
|
sleep "$SETTLE_FAST"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_3_check_firewall() {
|
||||||
|
say " [3/4] 检查本机防火墙有没有挡住 $PORT"
|
||||||
|
# Lightsail 有两层:网页上的防火墙规则(实例级)+ 系统内的 ufw/iptables。
|
||||||
|
# 网页那层我们改不了,但系统内这层能看。
|
||||||
|
if command -v ufw >/dev/null 2>&1; then
|
||||||
|
echo " ufw 状态:$(ufw status 2>/dev/null | head -1)" >> "$LOG"
|
||||||
|
ufw status 2>/dev/null | grep -q "$PORT" \
|
||||||
|
|| say " ⚠ ufw 里没有放行 $PORT 的规则"
|
||||||
|
fi
|
||||||
|
local n
|
||||||
|
n=$(iptables -L INPUT -n 2>/dev/null | grep -c "$PORT")
|
||||||
|
say " iptables INPUT 里提到 $PORT 的规则:$n 条"
|
||||||
|
# 重新绑一次端口(有时是监听 socket 坏了)
|
||||||
|
systemctl restart ykxmz-relay 2>/dev/null
|
||||||
|
sleep "$SETTLE_SLOW"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_4_reboot() {
|
||||||
|
if [ "$RESCUE_REBOOT" != "1" ]; then
|
||||||
|
say " [4/4] 重启整机 —— **已跳过**(默认不开)"
|
||||||
|
say " 要允许自动重启,把 RESCUE_REBOOT=1 写进 ykxmz-monitor.service。"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
say " [4/4] 重启整机(最后的底牌)"
|
||||||
|
sync
|
||||||
|
systemctl reboot 2>/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remember() {
|
||||||
|
local step="$1" reason="$2"
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "## $(date '+%F %T') 第 $step 级奏效"
|
||||||
|
echo
|
||||||
|
echo "- 症状:$reason"
|
||||||
|
echo "- 有效步骤:$step"
|
||||||
|
echo "- 当时公网:$(is_healthy && echo 通 || echo 不通);本机:$(local_health && echo 通 || echo 不通)"
|
||||||
|
echo "- 详情见 rescue_reports/$(basename "$REPORT")"
|
||||||
|
} >> "$KNOWLEDGE"
|
||||||
|
say " 已把「第 $step 级奏效」记进 $KNOWLEDGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 主流程
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
REASON="${1:-monitor 触发}"
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "================================================================================"
|
||||||
|
say " 紧急预案启动:$REASON"
|
||||||
|
say "================================================================================"
|
||||||
|
|
||||||
|
# ⚠ 先复查再记录现场。
|
||||||
|
# collect() 会写一份报告文件;如果网站其实已经好了,
|
||||||
|
# 那一次也会留下报告,把真正出事时的报告淹掉(后台只显示最近 30 份)。
|
||||||
|
if is_healthy; then
|
||||||
|
say " 复查:公网现在是通的,不需要动手 —— 不采现场、不留报告。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
collect "$REASON"
|
||||||
|
|
||||||
|
STEP_NAMES=("重启中转服务" "强杀残留再拉起" "查防火墙并重绑" "重启整机")
|
||||||
|
|
||||||
|
for i in 1 2 3 4; do
|
||||||
|
case $i in
|
||||||
|
1) step_1_restart_relay ;;
|
||||||
|
2) step_2_kill_and_start ;;
|
||||||
|
3) step_3_check_firewall ;;
|
||||||
|
4) step_4_reboot || continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if is_healthy; then
|
||||||
|
say ""
|
||||||
|
say " ✅ 第 $i 级(${STEP_NAMES[$((i-1))]})救回来了,公网已恢复。"
|
||||||
|
remember "$i" "$REASON"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
say " 第 $i 级没解决,继续下一级"
|
||||||
|
done
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say " ❌ 四级都没解决。再采一次现场(状态已经变了)"
|
||||||
|
collect "$REASON(四级抢救后仍不通)"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "⚠ 这份报告是「自动抢救全部失败」之后生成的。"
|
||||||
|
echo " 机器自己试过了:重启中转 / 强杀重拉 / 查防火墙。"
|
||||||
|
echo " (重启整机默认没做。)"
|
||||||
|
echo " 需要人工看一眼 —— 特别是 Lightsail **网页上的防火墙规则**,"
|
||||||
|
echo " 那一层系统内部改不了。"
|
||||||
|
echo
|
||||||
|
cat "$REPORT"
|
||||||
|
} > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT"
|
||||||
|
|
||||||
|
ls -1t "$REPORTS_DIR"/*.txt 2>/dev/null | tail -n +31 | xargs -r rm -f
|
||||||
|
|
||||||
|
say " 报告已生成:$REPORT"
|
||||||
|
exit 1
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""局域网遥控 · Windows 被控端。"""
|
"""局域网遥控 · Windows 被控端。"""
|
||||||
|
|
||||||
__version__ = "2.1.2"
|
__version__ = "2.1.3"
|
||||||
APP_NAME = "ykxmz"
|
APP_NAME = "ykxmz"
|
||||||
|
|||||||
@@ -92,19 +92,6 @@ def _guess_scheme(address: str) -> str:
|
|||||||
return "http://" if _PRIVATE_HOST.match(host) else "https://"
|
return "http://" if _PRIVATE_HOST.match(host) else "https://"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""用户没写 http/https 时,猜一个。
|
|
||||||
|
|
||||||
IP 地址 / localhost -> http://
|
|
||||||
局域网里的中转服务是明文 HTTP。如果猜成 https,永远连不上,
|
|
||||||
而且报的是 SSL 错误,很容易误导人去查证书。
|
|
||||||
域名 -> https://
|
|
||||||
走公网必须加密(Cloudflare 隧道本来就是 https)。
|
|
||||||
"""
|
|
||||||
host = address.split("/", 1)[0]
|
|
||||||
return "http://" if _PRIVATE_HOST.match(host) else "https://"
|
|
||||||
|
|
||||||
|
|
||||||
def _ssl_context() -> ssl.SSLContext:
|
def _ssl_context() -> ssl.SSLContext:
|
||||||
"""给 HTTPS 用的 SSL 上下文。
|
"""给 HTTPS 用的 SSL 上下文。
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ VSVersionInfo(
|
|||||||
[
|
[
|
||||||
StringStruct("CompanyName", "ykxmz"),
|
StringStruct("CompanyName", "ykxmz"),
|
||||||
StringStruct("FileDescription", "ykxmz"),
|
StringStruct("FileDescription", "ykxmz"),
|
||||||
StringStruct("FileVersion", "2.1.2.0"),
|
StringStruct("FileVersion", "2.1.3.0"),
|
||||||
StringStruct("InternalName", "ykxmz"),
|
StringStruct("InternalName", "ykxmz"),
|
||||||
StringStruct("LegalCopyright", "ykxmz"),
|
StringStruct("LegalCopyright", "ykxmz"),
|
||||||
StringStruct("OriginalFilename", "ykxmz.exe"),
|
StringStruct("OriginalFilename", "ykxmz.exe"),
|
||||||
StringStruct("ProductName", "ykxmz"),
|
StringStruct("ProductName", "ykxmz"),
|
||||||
StringStruct("ProductVersion", "2.1.2.0"),
|
StringStruct("ProductVersion", "2.1.3.0"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user