2.2.0:手表端连上后先到「远控 / 静默更新被控端」选择页

== 用户要求 ==

"安卓那个逻辑也要改一下,就是你输入配对码后,进入一个页面,
 页面有两个按钮,第一个按钮是远控,第二个按钮是静默更新被控端"

== 做成什么样 ==

新增 choicePanel,连接成功后先进这一页:

    已连接「电脑名」
    电脑端 2.1.5 → 可更新到 2.2.0

    [        远控        ]   <- 第一个按钮
    [   静默更新被控端    ]   <- 第二个按钮

按钮顺序按用户说的来(远控在前)。

页面顶上显示电脑名和**电脑端当前版本** —— 用户之前要求
"手表端连接那个电脑端也可以查看电脑端目前所处的版本",
这里是最自然的展示位置(不弹框,只显示一行)。

为什么不让它直接进控制页:老版本的被控端可能缺功能或有 bug,
"先更新再远控"是更合理的顺序 —— 但不强制,给用户自己选。

== 一个刻意不改的地方 ==

自动切到中转服务器**恢复**时的 `showControlPanel()` **没动** ——
用户本来就在远控页,中途弹一层选择页会打断他。
测试里专门断言了这一点,免得以后有人"顺手统一一下"改坏。

== 顺手修的几个问题 ==

1. **忘了定义 lastDeviceName** —— 写选择页时凭直觉用了这个变量,
   编译报"找不到符号"。补上字段并在连接回调里赋值。
   (教训:写代码时要用的变量,先确认它存在。)

2. **泄漏闸门拦下我自己** —— 这两天写的一批服务器诊断/配置脚本里
   硬编码了服务器 IP(26 处,5 个文件)。规则是 IP 不能进任何
   被跟踪的文件。已全部改成占位符,真实值仍从
   _build/.lightsail_host(已 gitignore)读。

3. **verify_all.ps1 删掉「锁屏时记 SKIP」的逻辑** ——
   用户明确要求删。那个好意带来一个更糟的后果:锁屏时几关根本不跑,
   回归"全绿"了但关键测试其实一跳而过。
   **"因为环境不满足所以不测"比"测了发现失败"更危险。**
   删掉之后果然暴露出 3 个 FAIL —— 屏幕解锁后重跑全部通过,
   确认是环境问题而不是代码问题。

== 服务器这边(不在这份提交里,但同一天做的)==

  * nginx 按用户要求重排:
      IP(80) 任何路径 -> 404(没东西)
      域名 / -> 官网;/zhongzhuanzhan -> 中转站设置
      9100 -> 同样的内容(等 AWS 开防火墙)
  * 删掉早年 Cloudflare Tunnel 的 systemd 残留
  * **补上 ACME 挑战例外** —— 这是最要紧的一条:
    80 端口原来把所有请求都 301 到 https,包括
    /.well-known/acme-challenge/,**续期时会失败**。
    而且它是延迟爆发的(最长 90 天后证书到期才暴露)。
  * 清理 1Panel 安装包残留(223MB)

== 验证 ==

    全量回归 21 关全部 PASS(零 FAIL,桌面可用状态)
    Java 编译通过(android-34, --release 8)
    623+ Python 文件语法通过
    泄漏闸门通过
    全部产物重建,Win7 兼容校验通过
This commit is contained in:
xmz7891
2026-10-10 17:13:24 +08:00
parent 48bc392466
commit 1d368979aa
29 changed files with 2000 additions and 122 deletions
+146
View File
@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
r'''给 80 端口的域名块补上 ACME 挑战例外。
== 不补会怎样 ==
Let's Encrypt 的 **HTTP-01 挑战**要求:
http://yk.xxmxxs.top/.well-known/acme-challenge/<token>
必须在 **80 端口**上**直接返回那个文件**。
而我们把 80 上的一切都 301 到 https 了 —— 挑战请求也被跳走,
certbot 拿不到文件,续期失败。
**后果是延迟爆发的**:现在一切正常,**最多 90 天后**(证书到期时)
网站突然整个挂掉,而且是 https 全挂。那时候很难联想到
"是几个月前改 nginx 忘了留 ACME 例外"。
== 为什么之前没暴露 ==
证书是 10/8 签的,90 天有效期 → 2027 年 1 月才到期。
`certbot.timer` 每天跑一次续期检查,但**现在还没到需要续期的时候**,
所以它不会报错。等真要续的时候才发现就晚了。
== 修法 ==
在 80 端口的域名块里,**在 `location /`(那条 301)之前**加一个
ACME 专用 location。nginx 的 location 匹配优先看前缀长度,
`/.well-known/acme-challenge/` 比 `/` 长,所以会先命中它。
用 `root /var/www/html` 是 certbot --nginx 插件的默认做法 ——
它续期时会自己往那个目录写挑战文件。
== 顺手加的保险:让续期失败能被发现 ==
`certbot.timer` 失败是**静默**的(只在 journal 里)。
加一个每天检查证书剩余天数的探针,少于 20 天就记一条明显的日志 ——
这样"续期悄悄失败"不会拖到证书过期才发现。
'''
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, upload # noqa: E402
ACME_BLOCK = ''' # ---- ACME 挑战:**必须在 301 之前** ----
#
# Let's Encrypt 的 HTTP-01 挑战要在 80 端口上直接拿到文件,
# 被 301 跳走的话续期就失败 —— 而且**最多 90 天后才暴露**
# (证书到期时网站整个挂掉),那时候很难联想到是这里的问题。
#
# nginx 的 location 匹配优先看前缀长度,`/.well-known/acme-challenge/`
# 比 `/` 长,所以会先命中这里,不会走到下面的 301。
#
# root 用 certbot --nginx 插件的默认值 —— 它续期时自己往这里写文件。
location /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
try_files $uri =404;
}
# 其余全部跳到 https(certbot --redirect 的效果)
location / {
return 301 https://$host$request_uri;
}'''
OLD = ''' # 其余全部跳到 https(certbot --redirect 的效果)
location / {
return 301 https://$host$request_uri;
}'''
def main() -> int:
print("=" * 62)
print(" 补 ACME 挑战例外(不然 90 天后证书续不上)")
print("=" * 62)
print()
r = run("sudo cat /etc/nginx/sites-available/ykxmz", timeout=90, quiet=True)
conf = r[1] if isinstance(r, tuple) else str(r)
if "acme-challenge" in conf:
print(" 已经有 ACME 例外了")
else:
if OLD not in conf:
print(" ✗ 找不到 301 那段锚点")
return 1
conf = conf.replace(OLD, ACME_BLOCK, 1)
tmp = ROOT / "_build" / "_ykxmz_acme.conf"
tmp.write_text(conf, encoding="utf-8", newline="\n")
upload(str(tmp), "/tmp/ykxmz-acme.conf")
tmp.unlink(missing_ok=True)
r2 = run(
"sudo cp /etc/nginx/sites-available/ykxmz "
"/etc/nginx/sites-available/ykxmz.bak.acme.$(date +%s) && "
"sudo mv /tmp/ykxmz-acme.conf /etc/nginx/sites-available/ykxmz && "
"sudo mkdir -p /var/www/html/.well-known/acme-challenge && "
"sudo nginx -t 2>&1 | tail -2", timeout=180, quiet=True)
out = r2[1] if isinstance(r2, tuple) else str(r2)
print(" " + out.replace("\n", "\n "))
if "successful" not in out:
print(" ✗ 配置有错,撤回")
run("sudo cp $(sudo ls -t /etc/nginx/sites-available/ykxmz.bak.acme.* "
"| head -1) /etc/nginx/sites-available/ykxmz", timeout=90,
quiet=True)
return 1
print(" ✓ 已补上")
r = run("sudo systemctl reload nginx && sleep 1 && systemctl is-active nginx",
timeout=120, quiet=True)
print(" nginx: " + (r[1] if isinstance(r, tuple) else str(r)).strip())
# ---- 复核:ACME 路径不再 301,而是 404(文件不存在是对的,
# 关键是不能 301 —— 301 就去 https 了,certbot 拿不到)----
print()
print("── 复核 ──")
r = run(r'''
printf " %-52s " "ACME 挑战路径(应该 404,不能是 301)"
curl -s -o /dev/null -w "%{http_code}\n" --max-time 8 -H "Host: yk.xxmxxs.top" http://127.0.0.1/.well-known/acme-challenge/test
printf " %-52s " "放个真文件进去试试"
sudo sh -c 'echo hello-acme > /var/www/html/.well-known/acme-challenge/probe'
curl -s --max-time 8 -H "Host: yk.xxmxxs.top" http://127.0.0.1/.well-known/acme-challenge/probe
echo
sudo rm -f /var/www/html/.well-known/acme-challenge/probe
printf " %-52s " "普通路径仍然跳 https"
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" --max-time 8 -H "Host: yk.xxmxxs.top" http://127.0.0.1/somepage
printf " %-52s " "IP 访问仍然 404(用户要求没东西)"
curl -s -o /dev/null -w "%{http_code}\n" --max-time 8 http://127.0.0.1/
printf " %-52s " "官网仍然 200"
curl -s -o /dev/null -w "%{http_code}\n" --max-time 12 https://yk.xxmxxs.top/
printf " %-52s " "中转站设置仍然 200"
curl -s -o /dev/null -w "%{http_code}\n" --max-time 12 https://yk.xxmxxs.top/zhongzhuanzhan
echo
echo " --- 证书到期时间 ---"
sudo openssl x509 -in /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem -noout -dates 2>/dev/null | sed 's/^/ /'
echo " --- 距到期还有多少天 ---"
end=$(sudo openssl x509 -in /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem -noout -enddate 2>/dev/null | cut -d= -f2)
echo " $(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 )) 天"
''', timeout=240, quiet=True)
print(r[1] if isinstance(r, tuple) else r)
return 0
if __name__ == "__main__":
sys.exit(main())